Привет! В прошлых лекциях ты уже научился создавать собственные полноценные классы, с полями и методами. Это серьезный прогресс, молодец!
Но сейчас я вынужден сообщить тебе неприятную правду. Мы создавали наши классы не совсем правильно! Почему? Никаких ошибок, на первый взгляд, в этом классе нет:
public class Cat {

   public String name;
   public int age;
   public int weight;

   public Cat(String name, int age, int weight) {
       this.name = name;
       this.age = age;
       this.weight = weight;
   }

   public Cat() {
   }

   public void sayMeow() {
       System.out.println("Мяу!");
   }
}
На самом деле — есть. Представь себе, что сидя на работе ты написал вот такой класс Cat, обозначающий кошек. И ушел домой. Пока тебя не было, другой программист пришел на работу, создал свой класс Main, где начал использовать написанный тобой класс Cat.
public class Main {

   public static void main(String[] args) {

       Cat cat = new Cat();
       cat.name = "";
       cat.age = -1000;
       cat.weight = 0;
   }
}
Неважно, зачем он это сделал и как так получилось: может, устал человек или не выспался. Важно другое: наш текущий класс Cat позволяет присваивать полям безумные значения. В результате в программе находятся объекты с некорректным состоянием, типа вот этой кошки с возрастом -1000 лет. Какую же ошибку мы в итоге допустили? При создании класса мы открыли его данные. Поля name, age и weight находятся в публичном доступе. К ним можно обратиться в любом месте программы: достаточно просто создать объект Cat — и все, у любого программиста есть доступ к его данным напрямую через оператор “.
Cat cat = new Cat();
cat.name = "";
Здесь мы напрямую получаем доступ к полю name и устанавливаем для него значение. Нам нужно как-то защитить наши данные от некорректного вмешательства извне. Что же для этого нужно? Во-первых, все переменные экземпляра (поля) необходимо помечать модификатором private. Private — самый строгий модификатор доступа в Java. Если ты его используешь, поля класса Cat не будут доступны за его пределами.
public class Cat {

   private String name;
   private int age;
   private int weight;

   public Cat(String name, int age, int weight) {
       this.name = name;
       this.age = age;
       this.weight = weight;
   }

   public Cat() {
   }

   public void sayMeow() {
       System.out.println("Мяу!");
   }
}

public class Main {

   public static void main(String[] args) {

       Cat cat = new Cat();
       cat.name = "";//ошибка! Поле name в классе Cat имеет private-доступ!
   }
}
Компилятор это видит и сразу выдает ошибку. Теперь поля вроде как защищены. Но получается, что доступ к ним закрыт “намертво”: в программе нельзя даже получить вес существующей кошки, если это понадобится. Это тоже не вариант: в таком виде наш класс практически нельзя использовать. В идеале нам нужно позволить какой-то ограниченный доступ к данным:
  • Другие программисты должны иметь возможность создавать объекты Cat
  • У них должна быть возможность считывать данные из уже существующих объектов (например, получить имя или возраст уже существующей кошки)
  • Также должна быть возможность присваивать значения полей. Но при этом — только корректные значения. От неправильных наши объекты должны быть защищены (никакого “возраст = -1000 лет” и тому подобного).
Список требований приличный! Но на самом деле, все это легко достигается с помощью специальных методов — геттеров и сеттеров.
Название происходит от английского “get” — “получать” (т.е. “метод для получения значения поля”) и set — “устанавливать” (т.е. “метод для установки значения поля”). Давай посмотрим, как они выглядят на примере нашего класса Cat:
public class Cat {

   private String name;
   private int age;
   private int weight;

   public Cat(String name, int age, int weight) {
       this.name = name;
       this.age = age;
       this.weight = weight;
   }

   public Cat() {
   }

   public void sayMeow() {
       System.out.println("Мяу!");
   }

   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }

   public int getAge() {
       return age;
   }

   public void setAge(int age) {
       this.age = age;
   }

   public int getWeight() {
       return weight;
   }

   public void setWeight(int weight) {
       this.weight = weight;
   }
}
Как видишь, всё довольно просто :) Их названия чаще всего состоят из слова get/set + названия поля, за которое они отвечают. Например, метод getWeight() возвращает значение поля weight у того объекта, для которого он был вызван. Вот как это выглядит в программе:
public class Main {

   public static void main(String[] args) {

       Cat barsik = new Cat("Барсик", 5, 4);
       String barsikName = barsik.getName();
       int barsikAge = barsik.getAge();
       int barsikWeight = barsik.getWeight();

       System.out.println("Имя кота: " + barsikName);
       System.out.println("Возраст кота: " + barsikAge);
       System.out.println("Вес кота: " + barsikWeight);
   }
}
Вывод в консоль: Имя кота: Барсик Возраст кота: 5 Вес кота: 4 Теперь из другого класса (Main) есть доступ к полям Cat, но только через геттеры. Обрати внимание — у геттеров стоит модификатор доступа public, то есть они доступны из любой точки программы. А как обстоят дела с присваиванием значений? За это отвечают методы-сеттеры
public void setName(String name) {
   this.name = name;
}
Их работа, как видишь, тоже проста. Мы вызываем метод setName() у объекта Cat, передаем ему в качестве аргумента строку, и эта строка присваивается в поле name нашего объекта.
public class Main {

   public static void main(String[] args) {

       Cat barsik = new Cat("Барсик", 5, 4);

       System.out.println("Изначальное имя кота — " + barsik.getName());
       barsik.setName("Василий");
       System.out.println("Новое имя кота — " + barsik.getName());
   }
}
Здесь мы использовали и геттеры, и сеттеры. Вначале с помощью геттера мы получили и вывели в консоль первоначальное имя кота. Потом с помощью сеттера назначали его полю name новое значение — “Василий”. И потом с помощью геттера получили имя снова, чтобы проверить, действительно ли оно изменилось. Вывод в консоль: Изначальное имя кота — Барсик Новое имя кота — Василий Казалось бы, в чем разница? Мы также можем присваивать полям объекта некорректные значения, даже если у нас есть сеттеры:
public class Main {

   public static void main(String[] args) {

       Cat barsik = new Cat("Барсик", 5, 4);
       barsik.setAge(-1000);

       System.out.println("Возраст Барсика — " + barsik.getAge() + " лет");
   }
}
Вывод в консоль: Возраст Барсика — -1000 лет Разница в том, что сеттер — это полноценный метод. А в метод, в отличие от поля, ты можешь заложить необходимую тебе логику проверки, чтобы не допустить неприемлемых значений. Например, можно легко не позволить назначение отрицательного числа в качестве возраста:
public void setAge(int age) {
   if (age >= 0) {
       this.age = age;
   } else {
       System.out.println("Ошибка! Возраст не может быть отрицательным числом!");
   }
}
И теперь наш код работает корректно!
public class Main {

   public static void main(String[] args) {

       Cat barsik = new Cat("Барсик", 5, 4);
       barsik.setAge(-1000);

       System.out.println("Возраст Барсика — " + barsik.getAge() + " лет");
   }
}
Вывод в консоль: Ошибка! Возраст не может быть отрицательным числом! Возраст Барсика — 5 лет Внутри сеттера есть ограничение, и оно защищает от попытки установить некорректные данные. Возраст Барсика остался без изменений. Геттеры и сеттеры нужно создавать всегда. Даже если в твоих полях нет ограничений на возможные значения, вреда от них не будет. Представь себе ситуацию: ты и твои коллеги пишете программу вместе. Ты создал класс Cat с публичными полями, и все программисты пользуются ими, как хотят. И тут в один прекрасный день до тебя доходит: “Блин, а ведь рано или поздно кто-то может нечаянно присвоить отрицательное число в переменную weight! Надо создать сеттеры и сделать все поля приватными!” Ты их создаешь, и весь написанный твоими коллегами код мгновенно ломается. Ведь они уже написали кучу кода, где обращались к полям Cat напрямую.
cat.name = "Бегемот";
А теперь поля стали приватными и компилятор выдает кучу ошибок!
cat.name = "Бегемот";//ошибка! Поле name класса Cat имеет private-доступ!
В такой ситуации лучше было бы скрыть поля и создать геттеры-сеттеры с самого начала. Все твои коллеги пользовались бы ими, и если бы тебя поздно “осенило”, что надо ограничить значения полей, ты бы просто дописал проверку внутри сеттера. И ни у кого не сломался бы уже написанный код. Конечно, если ты хочешь, чтобы доступ к какому-то полю был только “на чтение”, можно создать для него один геттер. “Снаружи”, то есть за пределами твоего класса, должны быть доступны только методы. Данные должны быть скрыты.
Можно привести аналогию с мобильным телефоном. Представь, что вместо обычного включенного мобильника тебе дали телефон во вскрытым корпусом, где все провода, схемы и т.д. торчат наружу. Телефон при этом работает: если сильно постараться и потыкаться в схемы, может даже получится совершить звонок. Но скорее ты его просто сломаешь. Вместо этого компания-производитель дает тебе интерфейс: клиент просто набирает нужные цифры, нажимает зеленую кнопку с трубкой — и звонок начинается. А уж что там происходит внутри со схемами и проводами и как они выполняют свою задачу — ему без разницы. В этом примере компания ограничила доступ к “внутренностям” (данным) телефона и оставила снаружи только интерфейс (методы). В итоге клиент получит то, что хотел (совершить звонок) и точно ничего не сломает внутри.