Объектно-ориентированное программирование

Объектно-ориентированное программирование (ООП) — это «парадигма» программирования, т.е. подход к тому, из каких базовых компонентов и на основе каких принципов составлять программу. В ООП программа состоит из взаимодействующих друг с другом объектов. Большинство современных и популярных языков программирования поддерживают ООП. Это верно и для Java с Python. Более того, Java неотделима от ООП, вы уже сами видели, что даже минимальная Java программа требует введения класса. Класс — одно из базовых понятий ООП в Java.

ООП — не единствення парадигма, но самая широко используемая. Есть и другие. Для сравнения, функциональная парадигма программирования предполагает, что программу нужно составлять из функций, которые вызывают друг друга и возвращают неизменяемые значения. Я рекомендую для общего развития попробовать изучить язык Haskell, чтобы увидеть функциональное программирование в действии. Если вы не делали это раньше, то он расширит ваше сознание и изменит восприятие реальности.

Мы же продолжим обсуждать ООП. Я не буду сейчас отдельно обсуждать принципы ООП и его теоретические основы, хотя это принято делать вначале. Предлагаю сразу перейти к практике. Когда практики будет достаточно, мы сможем обобщить наши знания и ввести все необходимые понятия и принципы, как правильно писать программы объектно-ориентированно.

Мы не изучали ООП в Python во время курса по Python, но вы сможете изучить эту тему самостоятельно. В этом вам поможет знание ООП из Java. В двух языках они устроены похоже, и я буду иногда для пояснения некоторых особенностей сравнивать ООП в Java и Python.

Класс и объект

Начнем с практической задачи. Представим, что мы хотим хранить в одной переменной сразу много информации. Например, если в нашей программе нужно хранить информацию о студентах, давайте опишем новый тип данных, который позволит хранить сразу и имя студента, и его возраст, и курс, на котором он учится:

public class Student {
    String name;     //имя
    int yearOfBirth; //год рождения
    int yearOfStudy; //первый курс, второй, третий и т.п.
}

На что обратить внимание

  1. Новый класс — это новый тип данных. В Java есть 8 базовых типов данных (int, boolean и т.п.), а все остальные типы данных называются объектными и за единичными исключениями описываются в виде классов. String, Scanner — всё это классы.
  2. Мы будем всегда создавать классы в отдельных файлах. Один класс = один файл. На самом деле, в Java это не обязательно, но для того, чтобы писать несколько классов в одном файле, нужны веские основания. Нам это не потребуется. Единственное исключение, анонимные классы, которые мы изучим сильно позже, для них отдельный файл не создается.

    Для перемещения между классами можно использовать сочетание клавиш Ctrl + N, после этого достаточно написать начало имени класса, чтобы к нему перейти.

  3. Имя класса — всегда с большой буквы. Это важно. С большой буквы в Java пишутся только классы и интерфейсы.
  4. Имя файла должно совпадать с именем класса внутри. Поэтому лишний раз напоминаю, что любые переименовывания нужно делать в IDEA с помощью рефакторинга. Конкретно переименовывание делается с помощью сочетания Shift + F6. Иначе, если вы будете самостоятельно переименовывать класс, то потом будете разбираться, как переименовать отдельно файл с классом.

Внутри класса Student заведено три поля. В полях хранятся данные. Поля заводятся как переменные, сначала указывается тип, потом имя.

Как же теперь использовать новый тип данных? Создадим класс для запуска наших экспериментов:

public class ExperimentsWithStudents {
    public static void main(String[] args) {
        // Здесь будем писать наши эксперименты, см. код ниже 
    }
}

Для начала введем несколько переменных для хранения студентов:

Student s1 = new Student();
Student s2 = new Student();
Student s3 = s1; 

В этом коде операция new Student() создает нового студента, в памяти выделяется область для хранения имени и двух чисел — года рождения и года обучения. Конструкция Student s1 вводит новую переменную, указывая, что в ней будут храниться студенты. В конце для примера заведена еще одна переменна s3, для нее нового студента не создается, она хранит ссылку на того же студента, который был создан для переменной s1. См. картинку:

objects in memory

Класс Student описывает, как устроена информация о студентах. А сами студенты должны создаваться отдельно оператором new. Конкретные воплощения класса называются объектами. В нашем примере создано ровно два объекта. В Java оператор new это основной метод создания объекта. Чтобы появился объект, нужно прямо или косвенно вызвать этот оператор.

Мы обсудили два основных понятия ООП в Java: класс и объект. Чтобы еще раз закрепить понимание, скажем, что класс описывает объекты, а объекты – это воплощения класса. В программе всегда ровно 1 класс, и произвольное количество объектов, оно зависит от того, сколько объектов было создано во время работы программы. Т.е. сколько раз во время работы программы был вызван оператор new.

Обращение к полям класса

Как теперь пользоваться полями, т.е. записывать в них значения и читать их. Продолжим наш код:

s1.name = "Ilya";
s1.yearOfBirth = 1910;
s1.yearOfStudy = 1;

s2.name = "Olya";
s2.yearOfBirth = 1912;
s2.yearOfStudy = 2;

System.out.println("Имя первого студента: " + s1.name);

Здесь видно, что для обращения к полю нужно написать ОБЪЕКТ.ИМЯ_ПОЛЯ. Мы не можем писать только имя поля, например, name, потому что будет неясно, чье это имя. У нас же два студента. Поэтому явно пишем в начале, у какого объекта нам требуется поле.

Методы

В классе могут быть методы. Если поля хранят данные, то методы производят вычисления. Давайте введем метод sayHello, который будет заставлять студента здороваться. Синтаксис описания метода полностью соответствует синтаксису написания того, что мы раньше называли фунциями, только сейчас мы уберем все служебные слова типа private, static, public, которые писали раньше. Будем добавлять эти слова позже, объясняя, наконец, что они значат.

public class Student {
    String name;     //имя
    int yearOfBirth; //год рождения
    int yearOfStudy; //первый курс, второй, третий и т.п.

    void sayHello() {
        System.out.println(
                "Добрый вечер, меня зовут " +
                this.name +  // про this читайте ниже
                ", я учусь на " +
                this.yearOfStudy + 
                " курсе."
        );
    }
}

Для проверки работы метода продолжим наш код внутри класса ExperimentsWithStudents. Попросим каждого из студентов поздороваться:

s1.sayHello();
s2.sayHello();
s3.sayHello();

Вывод:

Добрый вечер, меня зовут Ilya, я учусь на 1 курсе.
Добрый вечер, меня зовут Olya, я учусь на 2 курсе.
Добрый вечер, меня зовут Ilya, я учусь на 1 курсе.

Почему разные вызовы sayHello() выдали разный результат? Внутри метода sayHello() используется выражение this.name. Это обращение к полю name объекта this. Объект this означает тот объект, для которого вызван метод. При первом вызове s1.sayHello() переменной this присваивается объект из переменной s1. При вызове s2.sayHello() переменной this присваивается объект из s2.

Кстати, переменную this в Java можно не указывать. Поэтому метод sayHello() может быть переписан:

public class Student {
    String name;     //имя
    int yearOfBirth; //год рождения
    int yearOfStudy; //первый курс, второй, третий и т.п.

    void sayHello() {
        System.out.println(
                "Добрый вечер, меня зовут " +
                name +  // this подразумевается
                ", я учусь на " +
                yearOfStudy + 
                " курсе."
        );
    }
}

В этом примере name может относиться только к полю name, поэтому можно не писать полностью this.name. Моя рекомендация, не писать this, если оно не нужно.

В Python вместо this пишут self. В отличие от Java, сокращать self нельзя.

Для примера напишем еще один метод, чтобы вспомнить, как оформлять методы. Допустим, сделаем метод проверки, совершеннолетний ли студент. Вставим это в класс Student:

boolean isAdult() {
    return 2020 - yearOfBirth >= 18;
}

Можем воспользоваться этим методом в классе ExperimentsWithStudents:

if (s1.isAdult())
    System.out.println("студент " + s1.name + " совершеннолетний");

Здесь вы можете взять код, который получился к этому моменту: Student.java и ExperimentsWithStudents.java.

Конструкторы

Давайте вспомним, какой код был у нас написан, чтобы создать и заполнить данными одного студента:

Student s1 = new Student();

s1.name = "Ilya";
s1.yearOfBirth = 1910;
s1.yearOfStudy = 1;

Этот код слишком длинный, и его нужно повторять, если необходимо создать несколько студентов. Код можно сократить, если воспользоваться конструктором. Конструктор — это метод, который вызывается каждый раз при создании объекта. Оператор new, на самом деле, не только выделяет память для хранения полей объекта, но и вызывает конструктор.

Пока что в нашем классе Student не написан конструктор, но это не значит, что его нет. В классе Student сейчас есть пустой конструктор по-умолчанию, который ничего не делает. Давайте добавим полезный конструктор, который позволит создавать студента следующим образом:

Student s1 = new Student("Ilya", 1910, 1);

Чтобы этот код заработал, нужно описать конструктор:

public class Student {
    String name;     //имя
    int yearOfBirth; //год рождения
    int yearOfStudy; //первый курс, второй, третий и т.п.
    
    //вот он, это конструктор
    Student(String name, int yearOfBirth, int yearOfStudy) {
        this.name = name;
        this.yearOfBirth = yearOfBirth;
        this.yearOfStudy = yearOfStudy;
    }
    
    // здесь пропущены методы класса
}

Конструктор описывается как метод, только у него нет возвращаемого типа, и имя метода совпадает с именем класса. В этом примере конструктор получает на вход аргументы: имя, год рождения. Задача конструктора присвоить эти значения полям. Здесь приходится явно разделять this.name и name. Первое — это поле класса, второе — это аргумент функции. Здесь внутри конструктора видна ситуация, когда сокращать слово this нельзя. Иначе name уже не будет означать поле.

Добавленный конструктор портит код, который был раньше. Больше не работает выражение new Student(), потому что оно подразумевает вызов конструктора без аргументов. Чтобы код работал как раньше, нужно добавить конструктор без аргументов явно. Заодно давайте добавим в конструкторы печать приветствия при создании объекта:

public class Student {
    String name;     //имя
    int yearOfBirth; //год рождения
    int yearOfStudy; //первый курс, второй, третий и т.п.
    
    // новый конструктор без аргументов
    Student() {
        System.out.println("Меня создали");
    }

    Student(String name, int yearOfBirth, int yearOfStudy) {
        System.out.println("Меня создали");
        this.name = name;
        this.yearOfBirth = yearOfBirth;
        this.yearOfStudy = yearOfStudy;
    }
    
    // здесь пропущены методы класса
} 

Для проверки добавим в ExperimentsWithStudents:

Student s4 = new Student();
Student s5 = new Student("John", 1921, 3); 

Оба способа создания объекта работают, оба печатают фразу “Меня создали”, во втором случае поля сразу заполняются указанными значениями.

Смысл конструктора в том, что он производит инициализацию объекта, подготавливает его к использованию. При использовании объекта вы всегда можете быть уверены, что для него был вызван конструктор.

Конструктор, который мы написали, и который нужен только для копирования переданных значений в поля, это самый распространенный вид конструкторов. В большинстве случаев они бывают именно такими: получают значения и записывают их в поля. Необходимость писать подобные конструкторы — это очередная особенность Java, которая удлиняет код.

Инициализация полей

Какие значения имеют поля до того, как им что-то присваивается? Если вызвать конструктор new Student(), для полей нового объекта не будет выполнено явного присваивания. Тем не менее, можем попробовать узнать, что там записано:

Student s6 = new Student();
System.out.println(s6.name);         // null
System.out.println(s6.yearOfBirth);  // 0
System.out.println(s6.yearOfStudy);  // 0

В этом случае поля имеют значения по-умолчанию: 0 для int, null для String. Эти значения аналогичны значениям, которыми заполняется новый только что созданный массив. Напомню, что new int[10] заполняется нулями, а new String[10] заполняется null.

В классе можно инициализировать значения полей, присваивая им значения сразу при определении:

 public class Student {
     String name = "без имени";
     int yearOfBirth;
     int yearOfStudy = 1;
     
     // здесь пропущены методы и конструкторы класса
 } 

Теперь код, который мы писали раньше, выдаст другие результаты:

Student s6 = new Student();
System.out.println(s6.name);         // "без имени"
System.out.println(s6.yearOfBirth);  // 0, потому что не присвоено другого
System.out.println(s6.yearOfStudy);  // 1

Конечно, вызов второго из определенным нами конструкторов уже не оставит исходных значений:

Student s7 = new Student("ИМЯ", 2000, 5);
System.out.println(s7.name);         // "ИМЯ"
System.out.println(s7.yearOfBirth);  // 2000
System.out.println(s7.yearOfStudy);  // 5

Инициализация полей выполняется до вызова конструктора. Т.е. при создании объекта у него сначала полям присваиваются значения из инициализаторов, и только потом вызывается конструктор, который, возможно, перезапишет эти значения другими.

Вот окончательная версия созданных нами классов: Student.java и ExperimentsWithStudents.java.

Мы больше не будем изменять эти классы. Все следующие разделы создадут для примеров новые классы.

Ключевое слово final

У слова final много применений, мы рассмотрим применения с локальными переменными и полями класса.

Слово final при определении локальных переменных и полей класса означает, что значение нельзя изменять после первого присваивания. Другими словами слово final используется для определения констант. Например, можно ввести константу pi = 3.1415926, и никакого смысла присваивать другое значение для $\pi$ нет, наоборот, нужно защитить программиста от случайного переприсваивания значения.

С локальной переменной внутри метода это работает следующим образом:

final int x = 10;
x = 42;  // нельзя!

final int y; // можно не присваивать сразу
y = 10; // первое присваивание
y = 11; // нельзя! это уже второе присваивание

final int z;
if (...)
    z = 10; // это первое присваивание
else
    z = 20; // или это
z = 30; // нельзя! это точно второе присваивание.

Ключевое слово final можно написать при определении поля, его смысл полностью соответствует смыслу финальной локальной переменной, но правила немного более сложные из-за того, что значения полям можно присваивать в разных местах.

Например, final int x означает, что полю x необходимо присвоить значение один раз, в инициализаторе или конструкторе, и после этого значение изменять нельзя. Посмотрим это на синтетическом примере:

public class A {
    final int x = 42;
    final int y;
    final int z;

    A() {
        x = 123; // нельзя, потому что значение уже есть
        y = 1; // надо обязательно что-то присвоить, т.к. нет инициализатора
    
        // можно выбрать, что присвоить, но присвоить все равно
        // нужно только один раз
        if (x > 0)
            z = 5;
        else
            z = -5;
    }

    // другой конструктор. Объект будет создан либо с помощью
    // этого конструктора, либо предыдущего
    A(int y) {
        this.y = y;
        z = -5; // в этом конструкторе тоже надо что-то присвоить для z
    }   

    // какой-то метод
    void f() {
        z = 123; //нельзя, потому что после создания объекта
                 //значение уже присвоено, его нельзя менять
    }   
}

Модификаторы доступа

Мы уже умеем описывать внутри класса поля, методы и конструкторы. Будем называть всё это элементами класса. Так вот, у каждого элемента класса есть модификатор доступа, который описывает, откуда можно обращаться к этому элементу класса. Есть ровно четыре модификатора:

Как видите, все элементы класса Student в предыдущих примерах имели пакетную видимость, потому что мы использовали пустой модификатор.

Давайте определим новый класс, он будет называться Person, это модифицированная версия класса Student. Укажем еще везде модификаторы доступа.

public class Person {
    private String name;
    private int yearOfBirth;

    public Person(String name, int yearOfBirth) {
        this.name = name;
        this.yearOfBirth = yearOfBirth;
    }

    // метод вычисления возраста
    public int getAge() {
        return 2020 - yearOfBirth;
    }

    // метод, чтобы поздороваться
    public void sayHello() {
        System.out.printf(
                "Здравствуйте, меня зовут %s, мне %d лет.\n",
                name,
                getAge()
        );
    }
}

И класс для запуска примера:

public class ExperimentsWithPersons {

    public static void main(String[] args) {
        Person p = new Person("Илья", 1910);
        p.sayHello(); //Выводит: Здравствуйте, меня зовут Илья, мне 110 лет.
    }
}

Давайте обсудим выбранные модификаторы доступа и договоримся о том, как мы будем выбирать их в своих программах.

Приватные поля и принцип инкапсуляции

Все поля класса Person выбраны приватными. Это означает, что доступ к ним возможен только внутри класса Person. Действительно, мы обращаемся к ним из метода sayHello(). А вот из класса ExperimentsWithPersons обратиться к полям невозможно: p.name = "Вова"; вызовет ошибку компиляции.

Поля выбираются приватными в соответствии с принципом инкапсуляции: никто не должен знать, как объект устроен внутри и тем более, никто не должен иметь возможность бесконтрольно изменять внутреннее состояние объекта. Другими словами, принцип инкапсуляции говорит, что необходимо скрывать детали реализации класса. Пользователю класса нужно знать только, что умеют объекты этого класса, но не нужно знать, как они это умеют.

Давайте сделаем маленькое упражнение, чтобы продемонстрировать работу принципа. Нам нужно открыть исходный код класса String. Мы хорошо знакомы с этим классом и постоянно им пользуемся, но еще ни разу не смотрели, как он устроен.

Чтобы открыть класс String, нажмите Ctrl + N, введите String или несколько первых букв, выберите класс String из пакета java.lang.

Поищите внутри определения полей, они должны быть в самом начале класса. Все ли поля вы понимаете, что они хранят, и зачем они нужны? Вряд ли. Представьте, если бы вы могли изменять значения этих полей в своей программе: "asdf".value = null. Что-то наверняка сломается.

Кстати, реализация класса String меняется между версиями Java. Добавляются и удаляются поля, изменяется способ хранения строк в памяти, изменяются алгоритмы. Это позволяет оптимизировать и ускорять работу со строками в новых версиях. Но нам, пользователям String, это не важно. У нас нет доступа к этой части класса, потому что она приватна. Мы имеем доступ только к публичной части, доступным нам методам класса String. Например, к методу substring для выделения подстроки. Это публичный метод, он предназначен для того, чтобы им пользовались мы.

Давайте считать, что поля класса хранят внутреннюю информацию, не предназначенную для использования из других классов. Это значит, что их надо определять как приватные. Именно так мы поступили в классе Person и будем поступать дальше.

Модификатры доступа для конструкторов и методов

Для конструкторов и методов выбирать нужно между двумя вариантами — они могут быть публичными или приватными. Я предлагаю не использовать другие варианты. Дело не в том, что они никогда не нужны, а в том, что они не понадобятся нам в наших задачах.

Модификатор public нужно писать только если вы планируете использовать метод или конструктор где-то вне класса. Иначе оставляйте метод приватным. Т.е. пользуйтесь принципом наименьшего доступа: чем меньшей части программы доступен ваш метод, тем лучше. Это упрощает поиск ошибок. IDEA показывает предупреждение, если видит, что вы вводите публичный метод, который мог бы быть приватным.

Заметим, что конструкторы практически всегда будут публичными, потому что обычно мы хотим создавать объекты в других классах. Но можно делать и приватные конструкторы, это запретит создание объекта из других классов.

get- методы (геттеры)

Мы решили делать поля классов приватными, поэтому теперь у нас нет возможности узнать имя человека:

Person p = new Person("Илья", 1910);
System.out.println(p.name); // ! нельзя обращаться к полю

Чтобы иметь возможность узнать имя, нужен публичный метод для узнавания имени. Методы, которые нужны, только чтобы узнать какую-то информацию об объекте, традиционно начинаются на get- (получить). Для узнавания имени в этом случае метод нужно назвать getName(). Напишем внутри класса Person:

public String getName() {
    return name; // поле name доступно внутри класса `Person`
}

get- метод возвращает значения, не требуя аргументов.

Аналогично можно ввести метод год рождения:

public getYearOfBirth() {
    return yearOfBirth;
}

Чтоб воспользоваться методами, напишите в ExperimentsWithPersons:

Person p = new Person("Илья", 1910);
p.sayHello(); //Выводит: Здравствуйте, меня зовут Илья, мне 110 лет.

System.out.println("Имя p: " + p.getName());
System.out.println("Год рождения p: " + p.getYearOfBirth());

Видно, что get- методы достаточно однообразны. В Java их нужно писать вручную, это очередная особенность, удлиняющая код на Java. К счастью, IDEA может помогать писать такие методы: поставьте курсор в то место, где нужно писать get- метод и нажмите Alt + Ins. С помощью этой же возможности можно создавать и конструкторы типа тех, которые мы писали выше, и set- метотды, про которые напишем ниже.

get- методы не обязаны только возвращать значения полей. Например, мы уже написали метод getAge() в классе Person:

public int getAge() {
    return 2020 - yearOfBirth;
}

Это тоже get- метод, потому что он позволяет получить информацию об объекте, и не требует никаких аргументов. Им можно воспользоваться в ExperimentsWithPersons:

System.out.println("Возраст p: " + p.getYearOfBirth());

Проиллюстрируем еще раз принцип инкапсуляции. Сравним методы getAge() и getYearOfBirth(). Они возвращают возраст и год рождения. При использовании класса Person мы не должны знать, как на самом деле работают эти методы. На самом деле в классе хранится информация о годе рождения, и для определения возраста производится вычисления. Но можно было бы хранить в объектах возраст, и по возрасту вычислять год рождения. Те, кто пользуется классом Person, не должны знать, как точно это устроено. Это позволит изменять класс Person независимо от остального кода. Чем больше независимы части кода программы, тем проще искать в программе ошибки и изменять ее со временем.

Последняя короткая мысль о get- методах — это частный случай именования в случае, если возвращаемое значение логическое. Например, если мы хотим определить совершеннолетие человека, метод лучше назвать не getAdult(), а isAdult():

public boolean isAdult() {
    return getAge() >= 18;
}

set- методы (сеттеры)

Когда мы сделали поле name приватным и добавили для него get- метод, мы добились того, что любой сторонний код может узнать имя человека, но не может его изменить. Часто это ровно то, что нужно, но иногда мы все-таки считаем, что надо позволить изменять у человека имя. В этом случае нужен set- метод. Он принимает новое имя человека и не возвращает значений:

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

Чтобы воспользоваться методом и сразу проверить, что имя действительно изменилось, напишите:

p.setName("Ваня");
p.sayHello(); // Здравствуйте, меня зовут Ваня, мне 110 лет.

Текущая версия классов Person и ExperimentsWithPersons.

Статические элементы класса

Я прошу разобраться с понятием статических элементов. По моему опыту мало кто помнит различие статических и обычных элементов уже к экзамену, но это важная тема и, мне кажется, очень простая. Если вы помните, что такое классы и объекты, для вас совершенно естественным должно быть и различие между обычными и статическими элементами.

Обычные элементы относятся к объектам, а статические — к классам. Например, когда мы вводим поле name в класс Person, мы имеем в виду, что все Люди (объекты класса Person) имеют имена, причем, у всех людей эти имена свои. Имя хранится в памяти столько раз, сколько объектов создано:

objects and classes in memory

Если создать поле и пометить его модификатором static, такое поле будет храниться в памяти ровно один раз, независимо от того, сколько объектов создано. Добавим статические поля в класс Person, ниже обсудим их другие модификаторы кроме static.

public class Person {

    public static final String SPECIES_NAME = "Homo Sapiens";
    private static int count = 0;

    // предыдущее содержимое класса
}

На рисунке сверху видно, что эти поля хранятся в памяти в отдельном месте, будем считать это место с информацией о классе, оно обведено двойной рамкой. Еще есть два прямоугольника с информацией об объектах, там хранятся имена и годы рождения. Независимо от того, сколько объектов создано, информация о классе все равно хранится в памяти ровно один раз.

Для обращения к статическим полям используется синтаксис ИМЯ_КЛАССА.ИМЯ_ПОЛЯ. Это значит, что мы можем написать следующий код в ExperimentsWithPersons:

System.out.println(Person.SPECIES_NAME); 

Мы увидим латинское название вида, к которому относится человек. На что обратим внимание.

Статическая константа SPECIES_NAME

Название вида одинаковое у всех людей, нет смысла делать это поле обычным, нестатическим, в этом случае одна и та же информация хранилась бы несколько раз.

Поле объявлено финальным с помощью ключевого слова final, это значит, что его нельзя изменить, и значение там всегда будет только Homo Sapiens.

Поле объявлено публичным, поэтому мы имеем к нему доступ из другого класса, не из Person. Вроде бы мы договаривались так не делать, и оформлять все поля как приватные. Здесь мы встречаемся с важным исключением — глобальными константами. Комбинация модификаторов поля public static final означает, что поле доступно в любом месте программы по имени класса, и оно хранит константное, неизменяемое значение. Глобальные константы принято именовать заглавными буквами, а слова разделять подчеркиванием: SPECIES_NAME.

Мы уже встречались с глобальными константами раньше. Например, c константой PI из класса Math мы встречались, когда рассчитывали параметры схемы замещения трансформатора. Обращаться к этой константе нужно так: Math.PI.

Статическое поле count

Второе введенное нами статическое поле count, как мы и договаривались про все поля, описано приватным. Давайте хранить в нем количество созданных объектов класса Person. Изначально значение поля ноль. Создать объект можно только с помощью конструктора, поэтому будем в конструкторе увеличивать значение это поля. Ну а чтобы получить значение поля где-то вне класса Person, можно добавить get- метод:

public class Person {
    
    // статическая часть

    public static final String SPECIES_NAME = "Homo Sapiens";
    private static int count;
    
    public static int getCount() {
        return count; // либо Person.count, но лучше сократить
    }
    
    // нестатическая часть

    private String name;
    private int yearOfBirth;

    public Person(String name, int yearOfBirth) {
        count += 1; // подсчет количества созданных объектов
        
        this.name = name;
        this.yearOfBirth = yearOfBirth;
    }

    // оставшиеся методы пропущены
}

Теперь можно в ExperimentsWithPersons написать

System.out.println("Уже создано людей: " + Person.getCount());

Вы, скорее всего, увидите 1, если точно следовали моим примерам выше, хотя все зависит от того, какую программу вы написали. Мою версию программы, как обычно, сможете скачать в конце.

Метод getCount() статический, это естественно, потому что информация о количестве созданных объектов (людей) одинакова для всех объектов, логично получать к ней доступ не через конкретных людей, а через класс всех людей. Хотя, если сделать метод не статическим, программа тоже будет работать. Нужно будет писать p.getCount(), и результат не будет зависеть от конкретного p, для которого вызван метод.

Вопросы для самопроверки:

  1. Можно ли в нестатическом методе обратиться к статическому полю или методу того же класса? Имеется в виду, можно ли написать, например, System.out.println(someField);, если someField определено как статическое в том же классе?
  2. Аналогичный вопрос, но метод статический, а поле — не статическое.
  3. Можно ли в статическом методе пользоваться переменной this?

Ответы.

  1. Статическое поле или метод существует ровно в одном экземпляре. Нет никакой проблемы обратиться к нему из любого объекта.
  2. А вот если метод статический, он вызывается для класса независимо ни от какого конкретного объекта. Поэтому если обращаться к нестатическому полю, неясно, для какого объекта брать значение поля. Кроме того, вызов someField эквивалентен this.someField, и возникает вопрос, какой конкретно объект this имеется в виду при статическом вызове. Итого, попытка обратиться к нестатическому полю из статического метода приведет к ошибке компиляции.
  3. Из прошлого ответа следует, что это не имеет смысла, и просто приводит к ошибке компиляции.

Порядок элементов в классе

В Java элементы класса могут идти в любом порядке. Можно перемешать порядок полей, методов, конструкторов как угодно. Это никогда не может повлиять на работу класса. Кстати, рекомендую сочетание клавиш Ctrl + F12, чтобы увидеть список всех элементов класса. Элементы помечены значками, которые показывют, поле это, метод, конструктор, приватный он или нет, статический ли и т.п. Как всегда в IDEA, вы можете начать набирать имя элемента, чтобы искать его в появившемся диалоге.

Давайте договоримся о порядке элементов. Обычно в команде программистов есть рекомендуемый или требуемый порядок элементов. Я предлагаю такой и прошу его придерживаться, если нет веских оснований нарушить:

public class A {
    // -- статические элементы --
    // Публичные поля
    // Приватные поля
    // Публичные методы (включая main метод)
    // Приватные методы

    // -- нестатические элементы --
    // Публичные поля
    // Приватные поля
    // Публичные конструкторы
    // Приватные конструкторы
    // Публичные методы
    // Приватные методы
} 

Во всех примерах я соблюдал этот порядок.

Окончательная версия классов: Person.java и ExperimentsWithPersons.java.