02. Классы
Объектно-ориентированный подход является следующим этапом развития программирования после переменных и методов. Вернее, этот подход является одним из двух подходов нового этапа развития. Второй подход - это функциональное программирование. Там реализуются совершенно другие принципы, поэтому в рамках данного курса функциональное программирование мы рассматривать не будем.
Дальше, говоря об объектно-ориентированном подходе, будем называть его объектно-ориентированным программированием или ООП.
Аналогом переменных в ООП являются структуры, а методов - интерфейсы. Структуры позволяют объединять несколько переменных. Переменные, объединённые в структуру, называются полями. Точно так же, как переменные скрывают логику работы с процессором, структуры скрывают внутри себя логику выделения памяти для каждого из полей, а также механизм обращения к ним. Сокрытие внутренней логики работы называется инкапсуляцией.
Структуры
В старых языках структуры и классы создаются разными командами. О классах будет рассказано ниже.
В java
всё, что можно было без ощутимой потери производительности переложить на логику классов,
было переложено. В том числе и структуры. Поэтому структуры в java
объявляются, как и
классы, с помощью одного и того же ключевого слова class
.
Пусть нам нужно хранить в оперативной памяти сведения о сотрудниках фирмы. О каждом сотруднике необходимо знать следующие данные:
- Фамилию Имя (строка
String
, теория по строкам здесь) - Возраст (целое число
int
) - Стаж (целое число
int
)
Без использования структур можно было бы создать три массива (про них написано здесь): первый - под фамилию с именем, второй - под возраст, третий - под стаж.
Напишем такую программу, которая читает данные о сотрудниках и сохраняет их в соответствующие массивы. Предполагается, что записи о сотрудниках имеют следующий вид:
2
Иванов Василий
25
2
Сергеев Павел
40
14
Тогда код программы будет таким:
- Main.java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
// сканер
Scanner scanner = new Scanner(System.in);
// кол-во записей
int n = scanner.nextInt();
// переходим на новую строку
scanner.nextLine();
// фамилия и имя
String [] names = new String[n];
// возраст
int ages[] = new int[n];
// стаж
int exps[] = new int[n];
// читаем данные
for (int i = 0; i < n; i++) {
// читаем фамилию и имя
names[i] = scanner.nextLine();
// читаем возраст
ages[i] = scanner.nextInt();
// читаем стаж
exps[i] = scanner.nextInt();
// переходим на новую строку
scanner.nextLine();
}
}
}
Если в каждой структуре всего по три поля, то такой подход, хоть и неудобен, но уместен. Если полей будет гораздо больше, то читаемость кода станет слишком низкой, и без структур уже не обойтись.
Чтобы создать новую структуру, нужно кликнуть правой кнопки мыши по папке, в которой лежит Main.java
.
Если у вас этот файл лежит просто в папке src
, новую структуру нужно создавать в ней.
В появившемся меню выберите New
->Java Class
:
В центре экрана появится окно, в котором вам нужно ввести название структуры или класса (в нашем
случае - Worker
)
и нажать Enter
Появится новый файл:
Т.к. структуры и, тем более, классы являются надсущностями по отношению к переменным, то
в java
названия переменных принято писать с маленькой буквы, а названия структур, классов
и прочих надсущностей - с большой. Например, String
- это класс, поэтому его название
пишется с большой буквы.
Название файла должно совпадать с названием класса. Idea называет файлы так же, как и класс автоматически.
На данный момент в структуре нет полей:
- Worker.java
package com.company;
public class Worker {
}
Если вы создали структуру Worker
в папке src
, то строчки package com.company;
у вас быть не должно.
Чтобы добавить в структуру поле, нужно внутри её тела (между фигурными скобками {...}
)
объявить переменные:
- Worker.java
package com.company;
public class Worker {
// фамилия и имя
String name;
// возраст
int age;
// стаж
int exp;
}
До этого мы работали с примитивными типами данных: int
, double
, boolean
и т.д.
Примитивными они называются потому, что в переменные этих типов хранится непосредственное значение.
Если же мы создаём экземпляр структуры, то она будет содержать в себе несколько переменных. Поэтому этот экземпляр называется объект, а не переменная. Сама структура является просто набором инструкций, как выделить память под каждое из полей и как получать их значения.
Т.к. логика выделения памяти под каждое из полей скрыта от нас, то в java
инициализация
переменных и объектов выполняется по-разному:
- Main.java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
// объявляем переменную
int a;
// инициализируем её
a = 10;
// объявляем объект
Worker worker;
// инициализируем его
worker = new Worker();
}
}
При создании объекта структуры необходимо написать конструкцию вида new ИмяСтруктуры()
.
Объект worker
в действительности хранит в себе всего лишь адрес его данных и то,
как эти данные прочитать Сами данные хранятся в особой структуре, эта структура
называется куча. Обычные переменные хранятся в стеке.
Команда new
говорит о том, что будет выполнено выделение памяти, круглые скобки в конце ()
показывают, что при выделении памяти будет вызван специальный метод - конструктор, о
нём будет рассказано в следующей главе.
После того, как объект создан, можно обратиться к его полям в методе main
класса Main.java
:
- Main.java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
// объявляем объект
Worker worker;
// инициализируем его
worker = new Worker();
// имя
worker.name = "Сергеев Павел";
// возраст
worker.age = 40;
// стаж
worker.exp = 14;
// сохраним стаж в переменную базового типа
int wExp = worker.exp;
// уменьшим стаж на три года
worker.exp = worker.exp - 3;
// увеличим возраст на 1 год
worker.age++;
}
}
Обращение к полю объекта выполняется с помощью точки. Общий вид команды такой имя_объекта.имя_поля
.
Команда вида имя_объекта.имя_поля
позволяет работать с полем, как с локальной переменной.
С ним можно строить все те же команды, что и с обычными переменными. Пусть рассматриваемое поле
имеет вещественный тип double
, тогда поле имя_объекта.имя_поля
можно
использовать везде, где можно было бы использовать локальную переменную double d
.
Если вручную не задать значения для полей класса, то в них будут
лежать значения по умолчанию. Для небазовых типов (классов) значения будут null
, у базовых типов они
задокументированы:
Тип | byte | short | int | long | double | float | char | boolean |
Значение | 0 | 0 | 0 | 0 | 0.0 | 0.0f | '\u0000' | false |
Т.к. массив - это объект класса, то значение по умолчанию у переменной массива тоже null
.
Перепишем теперь код с использованием структуры Worker
:
- Main.java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
// сканер
Scanner scanner = new Scanner(System.in);
// кол-во записей
int n = scanner.nextInt();
// переходим на новую строку
scanner.nextLine();
// создаём массив работников
Worker[] workers = new Worker[n];
// читаем данные
for (int i = 0; i < n; i++) {
workers[i] = new Worker();
// читаем фамилию и имя
workers[i].name = scanner.nextLine();
// читаем возраст
workers[i].age = scanner.nextInt();
// читаем стаж
workers[i].exp = scanner.nextInt();
// переходим на новую строку
scanner.nextLine();
}
}
}
Код стал компактнее, однако с использованием конструкторов он будет ещё короче. Об этом будет рассказано в следующей главе.
Для массивов объектов верно всё то же самое, что и для массивов
переменных. Чтобы получить i
-ю структуру, нужно обратиться по индексу workers[i]
,
а чтобы получить поле i
-й структуры, нужно написать его название через точку
workers[i].exp
Интерфейс
С развитием объектно-ориентированного подхода стало понятно, что над объединёнными в структуру
переменными хорошо бы с ними научиться выполнять те или иные действия.
Причём выполнение этих действий было бы удобно запускать так же, как и обращаться к полям - через
.
Правда, любое действие - это метод, поэтому после названия этого действия нужно
добавить круглые скобки ()
.
Совокупность действий над той или иной структурой называют интерфейсом. В java
есть отдельная
сущность - интерфейс, но о ней будет рассказано в главе про абстракции.
Пока что про интерфейс нужно понимать только то, что это - объединение описаний действий. Каждое действие характеризуется возвращаемым значением, названием и списком аргументов.
Если действие имеет реализацию, то оно становится методом.
Структура, реализующая тот или иной интерфейс, называется классом. Иначе говоря, если в структуре мы пропишем хотя бы один метод, то она станет классом.
При этом у каждого объекта будет где-то в оперативной памяти храниться своя реализация, хотя она просто скопирована из описания класса, но важно понимать, что каждый объект при запуске действия обращается к своему уникальному участку памяти.
Если метод не взаимодействует с полями структуры, то его не нужно создавать для каждого класса. Достаточно объявить его один раз. Такие методы называются статическими. О статических элементах рассказано в главе статика.
Именно поэтому метод main
, запускающий программу, объявлен статическим:
- Main.java
package com.company;
public class Main {
public static void main(String[] args) {
}
}
Он не использует поля класса Main
, просто потому что их нет, да и при работе программы
не предполагается, что нам когда-нибудь может понадобиться создать объект класса Main
.
Пропишем четыре метода для структуры Worker
(как только мы добавим хотя бы один метод,
структура станет классом):
- увеличить возраст на 1,
- изменить стаж на заданное значение,
- получить флаг (логическое значение:
true
/false
), отработал ли сотрудник больше пяти лет - получить флаг (логическое значение:
true
/false
), исполнилось ли сотруднику 30 лет
- Worker.java
package com.company;
public class Worker {
// фамилия и имя
String name;
// возраст
int age;
// стаж
int exp;
// увеличить возраст на 1
void incAge() {
age++;
}
// изменить стаж
void changeExp(int d) {
exp += d;
}
// отработал ли сотрудник больше пяти лет
boolean isOldTimer() {
return exp > 5;
}
// исполнилось ли сотруднику 30 лет
boolean isUp30() {
return age >= 30;
}
}
Общий вид объявления класса:
class Имя_класса {
тип_поля1 имя_поля1; // имя_переменной1
тип_поля2 имя_поля2; // имя_переменной2
тип_результата1 имя_метода1 (параметры_метода1) {
тело_метода1
}
тип_результата2 имя_метода2 (параметры_метода2) {
тело_метода2
}
}
Усложним теперь задачу:
Пусть нам нужно хранить в оперативной памяти сведения о сотрудниках фирмы. О каждом сотруднике необходимо знать следующие данные:
- Фамилию Имя (строка
String
, теория по строкам здесь) - Возраст (целое число
int
) - Стаж (целое число
int
)
Необходимо увеличить возраст на 1 год у всех сотрудников, кто работает больше пяти лет. После этого необходимо увеличить у всех сотрудников стаж на три года и вывести на экран Фамилию и Имя сотрудников, старше 30 лет, которые работают больше пяти лет с учётом изменения стажа.
Программа будет такой:
- Main.java
package com.company;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
// сканер
Scanner scanner = new Scanner(System.in);
// кол-во записей
int n = scanner.nextInt();
// переходим на новую строку
scanner.nextLine();
// создаём массив работников
Worker[] workers = new Worker[n];
// читаем данные
for (int i = 0; i < n; i++) {
workers[i] = new Worker();
// читаем фамилию и имя
workers[i].name = scanner.nextLine();
// читаем возраст
workers[i].age = scanner.nextInt();
// читаем стаж
workers[i].exp = scanner.nextInt();
// переходим на новую строку
scanner.nextLine();
}
// перебираем работников
for (Worker worker : workers) {
if (worker.isOldTimer())
worker.incAge();
}
// перебираем работников второй раз
for (Worker worker : workers) {
worker.changeExp(3);
// сохраняем результат метода
boolean ot = worker.isOldTimer();
if (worker.isUp30() && ot)
System.out.println(worker.name);
}
}
}
Введём с клавиатуры следующие данные:
4
Иванов Василий
25
2
Сергеев Павел
40
14
Сидорова Мария
31
3
Фёдоров Игорь
29
3
В ответ получим:
Сергеев Павел
Сидорова Мария
При переборе элементов в массиве работников используется конструкция for..each
. Она перебирает
элементы массива workers
, каждый объект(адрес) он копирует в локальную переменную
worker
и выполняется с ней все действия, написанные в теле цикла.
Специальные методы
Нам осталось рассмотреть несколько специальных методов
Сеттеры и геттеры
При создании реальных классов чаще всего поля меняют не напрямую, а с помощью специальных методов-сеттеров. Значения полей получают с помощью методов-геттеров. О них подробнее будет рассказано в следующих главах.
Пока что напишем только сеттер и геттер стажа:
// задать стаж
public void setExp(int exp) {
this.exp = exp;
}
// получить стаж
public int getExp() {
return exp;
}
Тут нам встретилась новая конструкция this
.
Очень часто в параметрах методов класса мы
передаём аргументы(параметры) с такими же названиями, как и поля самого класса. Чтобы
разграничить, в каком случае мы работаем с локальными переменными, которые объявлены в параметрах и
полями класса, перед названием поля класса необходимо добавить ключевое слово this
.
Т.к. два объекта одного и того же класса - это разные области оперативной памяти, то изменение
одного объекта никак не повлияет на второй объект. Перепишем main()
следующим образом:
- Main.java
package com.company;
public class Main {
public static void main(String[] args) {
Worker worker1 = new Worker();
worker1.setExp(10);
Worker worker2 = new Worker();
worker2.setExp(20);
System.out.println(worker1.getExp() + " " + worker2.getExp());
}
}
На консоль будет выведено:
10 20
Порождающие методы
Рассмотрим методы, которые возвращают в качестве результата новый объект того же класса, такие методы будем называть порождающими.
Добавим в класс работника Worker
новый метод, который будет возвращать
нового сотрудника по совету рабочего.
Этот метод должен возвращать нового сотрудника с нулевым стажем, случайным возрастом от до и фамилией с именем построенными следующим образом: фамилия должна быть такая же, как у рекомендующего сотрудника, а именем должно быть слово "рекомендация".
Например, если метод рекомендации offer()
будет вызван у сотрудника Фёдорова Игоря, то
поле фамилии и имени будет хранить строку "Фёдров Рекомендация".
// предложение от сотрудника
Worker offer() {
// создаём нового сотрудника
Worker worker = new Worker();
// имя сотрудника получается из соединения первого
// элемента разбиения строки по пробелу и строки " Рекомендация"
worker.name = name.split(" ")[0] + " Рекомендация";
// опыта пока что нет
worker.exp = 0;
// создаём генератор случайных чисел
Random random = new Random();
// возраст работника - случайное число от 25 до 60
worker.age = random.nextInt() % (60 - 25) + 26;
return worker;
}
Тогда вызов этого метода из main()
будет таким:
// создаём нового сотрудника по совету worker
Worker offered = worker.offer();
Строковое представление
В java для многих типов определено автоматическое преобразование типов к строке.
int a = 5;
int b = 1;
System.out.println((a + b) + "");
На экран будет выведено:
6
Автоматическое преобразование объекта нашего класса к строке реализовать довольно просто. Для
этого нам достаточно в классе прописать метод toString()
, возвращающий объект класса String
.
В тело класса (то, что между скобками {...}
) сотрудника Worker
добавим следующие строки:
// строковое представление
@Override
public String toString() {
return "Worker{" + name + ": " + exp + "}";
}
Перед объявлением метода аннотация @Override
. Аннотации - это специальные
слова в java, помогающие компилятору правильно выполнить свою работу. Аннотация переопределения @Override
говорит
java
, что мы переопределяем метод, который есть в классе-родителе,
зачем ключевое слово public
перед методом. Пока что просто запомните, что
при переопределении метода toString()
, нужно перед его объявлением написать ключевое слово public
,
а наследование воспринимайте, как автоматическое копирование компилятором всего кода из класса-родителя
в класс-потомок.
На самом деле все классы, которые мы создаём, по умолчанию наследуются от
класса Object
. У этого класса очень мало методов и рассматривать их в рамках данного курса мы не
будем, но метод toString()
у него есть. Именно поэтому мы пишем аннотацию переопределения. На самом деле
многие аннотации, в частности @Override
, не вносят изменений в работу программы. Они просто
добавляют автоматические проверки кода перед компиляцией.
Перепишем теперь метод main()
:
- Main.java
package com.company;
public class Main {
public static void main(String[] args) {
Worker worker = new Worker();
worker.name = "Сидоров Павел";
worker.setExp(10);
System.out.println(worker.toString());
}
}
На консоль будет выведено:
Worker{Сидоров Павел: 10}
При этом вызов метода toString()
окрашен серым цветом:
Кликните по нему левой кнопкой мыши и нажмите Alt+Enter
В выпадающем меню выберите Remove redundant 'toString()' call
.
Код упростится:
Дело в том, что команда System.out.println()
обрабатывает любые объекты
и вызывает у них метод toString()
.
Поэтому правильнее писать просто
System.out.println(worker);
Добавим в класс сотрудника Worker
вещественное поле продуктивности kpi
:
// продуктивность
double kpi;
Чтобы сформировать строковое представление вещественного поля с заданным кол-во знаков после запятой,
проще всего использовать команду String.format()
Аргументы этого метода устроены точно так же, как у команды System.out.printf()
, только String.format
возвращает строку, построенную по заданному правилу, а System.out.printf()
- выводит эту строку на консоль:
double d = 1.281232;
String r = String.format("%.3f",d);
System.our.println(r);
На консоль будет выведено:
1,281
Добавим в строковое представление класса сотрудника Worker
продуктивность с точностью до двух знаков после запятой:
// строковое представление
@Override
public String toString() {
return "Worker{" + name + ": " + exp + "/" + String.format("%.2f", kpi) + "}";
}
Слегка изменим метод main()
:
- Main.java
package com.company;
public class Main {
public static void main(String[] args) {
Worker worker = new Worker();
worker.name = "Сидоров Павел";
worker.setExp(10);
worker.kpi = 4.81238;
System.out.println(worker);
}
}
На консоль будет выведено
Worker{Сидоров Павел: 10/4,81}
Сравнивание объектов
Т.к. объекты класса напрямую не хранятся в переменных, а хранятся только их адресы, то и сравнивать их так, как мы привыкли, worker == worker2 нельзя.
Создадим два одинаковых объекта worker
и проверим, равны ли они друг другу:
- Main.java
package com.company;
public class Main {
public static void main(String[] args) {
Worker worker = new Worker();
Worker worker2 = new Worker();
worker.name = "Сидоров Павел";
worker2.name = "Сидоров Павел";
worker.setExp(10);
worker2.setExp(10);
worker.kpi = 4.81238;
worker2.kpi = 4.81238;
worker.age = 25;
worker2.age = 25;
System.out.println(worker == worker2);
}
}
На консоль будет выведено
false
Оператор ==
у объектов сравнивает лишь адресы оперативной памяти, где лежит их содержимое. Это касается и
присваивания. Если мы напишем
worker2 = worker
то обе переменные будут указывать на одну и ту же область памяти.
Теперь, если мы поменяем объект worker
, объект worker2
изменится автоматически, т.к. по сути worker
и worker2
являются одним и тем же объектом.
Чтобы проверить объекты на равенство, принято писать метод equals()
. Причём в качестве
аргумента будет передан объект самого класса:
// проверка на равенство
public boolean equals(Worker worker) {
if (age != worker.age)
return false;
if (exp != worker.exp)
return false;
// встроенное сравнивание вещественных чисел
if (Double.compare(worker.kpi, kpi) != 0)
return false;
// встроенное сравнивание строк
return name.equals(worker.name);
}
Перепишем теперь сравнивание сотрудников:
- Main.java
package com.company;
public class Main {
public static void main(String[] args) {
Worker worker = new Worker();
Worker worker2 = new Worker();
worker.name = "Сидоров Павел";
worker2.name = "Сидоров Павел";
worker.setExp(10);
worker2.setExp(10);
worker.kpi = 4.81238;
worker2.kpi = 4.81238;
worker.age = 25;
worker2.age = 25;
System.out.println(worker.equals(worker2));
}
}
На консоль будет выведено
true
Весь код
- Worker.java
package com.company;
import java.util.Random;
public class Worker {
// фамилия и имя
String name;
// возраст
int age;
// стаж
int exp;
// продуктивность
double kpi;
// увеличить возраст на 1
void incAge() {
age++;
}
// изменить стаж
void changeExp(int d) {
exp += d;
}
// отработал ли сотрудник больше пяти лет
boolean isOldTimer() {
return exp > 5;
}
// исполнилось ли сотруднику 30 лет
boolean isUp30() {
return age >= 30;
}
// предложение от сотрудника
Worker offer() {
// создаём нового сотрудника
Worker worker = new Worker();
// имя сотрудника получается из соединения первого
// элемента разбиения строки по пробелу и строки " Рекомендация"
worker.name = name.split(" ")[0] + " Рекомендация";
// опыта пока что нет
worker.exp = 0;
// создаём генератор случайных чисел
Random random = new Random();
// возраст работника - случайное число от 25 до 60
worker.age = random.nextInt() % (60 - 25) + 26;
return worker;
}
// задать стаж
public void setExp(int exp) {
this.exp = exp;
}
// получить стаж
public int getExp() {
return exp;
}
// строковое представление
@Override
public String toString() {
return "Worker{" + name + ": " + exp + "/" + String.format("%.2f", kpi) + "}";
}
// проверка на равенство
public boolean equals(Worker worker) {
if (age != worker.age)
return false;
if (exp != worker.exp)
return false;
// встроенное сравнивание вещественных чисел
if (Double.compare(worker.kpi, kpi) != 0)
return false;
// встроенное сравнивание строк
return name.equals(worker.name);
}
}
Математика
Площадь треугольника по трём сторонам:
где - полупериметр треугольника
Задание
В качестве задания вам необходимо написать требуемые классы либо решить задачу с их использованием. Система совмещает ваш код со своим и запускает тестирование.
Прочитайте внимательно, что именно нужно отправить в тот или иной тест.
Часть вывода в консоль система выполняет за вас. Вам нужно выводить только то, что требуется в задании.