Skip to main content

02. Классы

Объектно-ориентированный подход является следующим этапом развития программирования после переменных и методов. Вернее, этот подход является одним из двух подходов нового этапа развития. Второй подход - это функциональное программирование. Там реализуются совершенно другие принципы, поэтому в рамках данного курса функциональное программирование мы рассматривать не будем.

Дальше, говоря об объектно-ориентированном подходе, будем называть его объектно-ориентированным программированием или ООП.

Аналогом переменных в ООП являются структуры, а методов - интерфейсы. Структуры позволяют объединять несколько переменных. Переменные, объединённые в структуру, называются полями. Точно так же, как переменные скрывают логику работы с процессором, структуры скрывают внутри себя логику выделения памяти для каждого из полей, а также механизм обращения к ним. Сокрытие внутренней логики работы называется инкапсуляцией.

Структуры

В старых языках структуры и классы создаются разными командами. О классах будет рассказано ниже. В java всё, что можно было без ощутимой потери производительности переложить на логику классов, было переложено. В том числе и структуры. Поэтому структуры в java объявляются, как и классы, с помощью одного и того же ключевого слова class.

Задача

Пусть нам нужно хранить в оперативной памяти сведения о nn сотрудниках фирмы. О каждом сотруднике необходимо знать следующие данные:

  • Фамилию Имя (строка String, теория по строкам здесь)
  • Возраст (целое число int)
  • Стаж (целое число int)

Без использования структур можно было бы создать три массива (про них написано здесь): первый - под фамилию с именем, второй - под возраст, третий - под стаж.

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

2
Иванов Василий
25
2
Сергеев Павел
40
14

Тогда код программы будет таким:

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:

xor

В центре экрана появится окно, в котором вам нужно ввести название структуры или класса (в нашем случае - Worker) и нажать Enter

xor

Появится новый файл:

xor

О названиях структур и классов

Т.к. структуры и, тем более, классы являются надсущностями по отношению к переменным, то в java названия переменных принято писать с маленькой буквы, а названия структур, классов и прочих надсущностей - с большой. Например, String - это класс, поэтому его название пишется с большой буквы.

Название файла должно совпадать с названием класса. Idea называет файлы так же, как и класс автоматически.

На данный момент в структуре нет полей:

package com.company;

public class Worker {
}
Обратите внимание

Если вы создали структуру Worker в папке src, то строчки package com.company; у вас быть не должно.

Чтобы добавить в структуру поле, нужно внутри её тела (между фигурными скобками {...}) объявить переменные:

package com.company;

public class Worker {
// фамилия и имя
String name;
// возраст
int age;
// стаж
int exp;
}

До этого мы работали с примитивными типами данных: int, double, boolean и т.д. Примитивными они называются потому, что в переменные этих типов хранится непосредственное значение.

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

Т.к. логика выделения памяти под каждое из полей скрыта от нас, то в 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:

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, у базовых типов они задокументированы:

Типbyteshortintlongdoublefloatcharboolean
Значение00000.00.0f'\u0000'false

Т.к. массив - это объект класса, то значение по умолчанию у переменной массива тоже null.

Перепишем теперь код с использованием структуры Worker:

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, запускающий программу, объявлен статическим:

package com.company;

public class Main {

public static void main(String[] args) {
}
}

Он не использует поля класса Main, просто потому что их нет, да и при работе программы не предполагается, что нам когда-нибудь может понадобиться создать объект класса Main.

Пропишем четыре метода для структуры Worker (как только мы добавим хотя бы один метод, структура станет классом):

  • увеличить возраст на 1,
  • изменить стаж на заданное значение,
  • получить флаг (логическое значение: true/false), отработал ли сотрудник больше пяти лет
  • получить флаг (логическое значение: true/false), исполнилось ли сотруднику 30 лет
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
}
}

Усложним теперь задачу:

Задача

Пусть нам нужно хранить в оперативной памяти сведения о nn сотрудниках фирмы. О каждом сотруднике необходимо знать следующие данные:

  • Фамилию Имя (строка String, теория по строкам здесь)
  • Возраст (целое число int)
  • Стаж (целое число int)

Необходимо увеличить возраст на 1 год у всех сотрудников, кто работает больше пяти лет. После этого необходимо увеличить у всех сотрудников стаж на три года и вывести на экран Фамилию и Имя сотрудников, старше 30 лет, которые работают больше пяти лет с учётом изменения стажа.

Программа будет такой:

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() следующим образом:

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 новый метод, который будет возвращать нового сотрудника по совету рабочего.

Этот метод должен возвращать нового сотрудника с нулевым стажем, случайным возрастом от 2525 до 6060 и фамилией с именем построенными следующим образом: фамилия должна быть такая же, как у рекомендующего сотрудника, а именем должно быть слово "рекомендация".

Например, если метод рекомендации 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():

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() окрашен серым цветом:

xor

Кликните по нему левой кнопкой мыши и нажмите Alt+Enter

xor

В выпадающем меню выберите Remove redundant 'toString()' call.

Код упростится:

xor

Дело в том, что команда 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():

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 и проверим, равны ли они друг другу:

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);
}

Перепишем теперь сравнивание сотрудников:

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

Весь код

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);
}
}

Математика

Площадь треугольника по трём сторонам:

S=p(pa)(pb)(pc),S = \sqrt{p(p-a)(p-b)(p-c)},

где p=a+b+c2p=\frac{a+b+c}{2} - полупериметр треугольника

Задание

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

Прочитайте внимательно, что именно нужно отправить в тот или иной тест.

Часть вывода в консоль система выполняет за вас. Вам нужно выводить только то, что требуется в задании.

xor

Ссылка на контест