Тема 6. Введение в ООП - BelyiZ/JavaCourses GitHub Wiki
Как вы уже заметили, все, кроме примитивов, в Java является объектом. Java - объектно-ориентированный язык, и следует принципам объектно-ориентированного программирования, если сокращенно - ООП.
Основным понятием в ООП является объект. Все языки, реализующие парадигму ООП, так или иначе оперируют ими.
В ОО языках иногда может не быть классов, как в javascript
, наследования (GoLang
), интерфейсов (C++
), но объекты
будут обязательно.
Представим себе, например, игру, в которой пользователь собирает футбольные команды, назначает игроков и тренера, и проводит матчи с другими игроками.
Удобно было бы представить абстракции команды, игрока, тренера. Дать возможность пользователю назначать игроков в команду и убирать из нее, хранить информацию о результативности тренера и игроков. Конечно, у игроков и тренера должны быть имена, а команда должна иметь полевой состав из 11 игроков, среди которых будут нападающие, вратарь и защитники.
Все вышеперечисленное, а также другие абстракции, представляющие логику игры, будем называть ее объектной моделью.
Тренер и игроки являются людьми, а значит имеют имена. В терминах Java это значит, что мы можем описать их классом,
который назовём Person
, у которого будут поля name
и surname
. Значение этих полей будет у тренера одно, а у
вратаря другое. Мы будем говорить, что тренер Иван Иванов, как и вратарь Петр Петров это объекты или экземпляры
класса Person
.
class Person {
String name;
String surname;
}
Кроме данных, связанных с объектом, ООП декларирует привязку к объекту действий или операций, которые он может
совершать. Например, положим, что все объекты типа Person
должны уметь вывести данные о себе в виде строки. В Java это
делается с помощью методов, то есть функций, привязанных к объекту, и имеющих доступ к его данным.
class Person {
String name;
String surname;
String toString() { // возвращает данные объекта в строке
return "Привет! Я " + this.name + " " + this.surname;
}
}
Объект может храниться в переменной, как мы уже видели на примере строк (да, они тоже объекты!).
Сравните:
String s = new String(); // s - пустая строка, объект типа String
Person p = new Person(); // p - объект типа Person, пока без имени и фамилии
Мы можем обратиться к данным объекта, или посредством его методов, или даже напрямую. Например, зададим имя для человека, хранящегося в переменной "p".
p.name = "Иван";
p.surname = "Иванов";
String s = p.toString();
И мы положили в переменную "s" строку "Привет! Я Иван Иванов".
А теперь представим, как могла бы выглядеть операция добавления игрока в команду:
class Team {
Player[] players = new Player[20]; // массив игроков
int playersNumber;
void addPlayer(Player p) { // параметр метода объект типа игрок
this.players[playersNumber] = p; // переданный игрок добавляется в массив
playersNumber++;
}
}
Резюмируя:
Объект - это сущность, обладающая данными и определенным набором операций. В Java данные это поля, а операции - это методы.
Могут быть классы и посложнее. Например, почтовый клиент, отправляющий по определенному email указанное письмо может быть использован так:
EmailClient emailClient = new EmailClient(/*Тут все, что нужно при создании объекта.*/); // об этом ниже
emailClient.sendMessage("[email protected]", "Здорово, дед!");
Причем тому, кто будет использовать этот класс, не нужно знать о сложной логике, упрятанной под капотом
класса EmailClient
. Он будет использовать только удобный интерфейс.
Указал адрес, текст письма, и вуаля, письмо ушло на деревню дедушке. Но о сокрытии данных от любопытных мы поговорим позже.
Обсудим подробнее возможности классов в Java.
Положим,
String s = "Я строка!";
Про объект s
мы знаем, что он принадлежит к типу или классу строк. В Java все объекты относятся к какому-либо
классу.
У объектов одного класса определен набор полей и методов. Причем мы сами решим, к каким полям и методам разрешить
доступ (public
), а к каким ограничить (private
). Мы можем определять новые классы, и создавать объекты этих классов.
Рассмотрим небольшой кусочек определения знакомого нам класса String
из Java:
package java.lang; // <-- Классы расположены в пакетах.
// String относится к пакету java.lang
public final class String { // <-- Название класса, и модификаторы,
private final char[] value; // <-- Данные, которыми обладает каждый объект этого класса.
// В нашем случае это массив символов, составляющих строку
public String() { // <-- Один из конструкторов - методов, создающих String.
this.value = new char[0]; // Этот создает пустую строку.
}
public int length() { // <-- Один из методов класса.
// Например, этот возвращает длину строки.
return value.length;
}
}
Группы классов отвечающих за схожую функциональность располагаются в пакетах (package
)
Структура пакетов совпадает со структурой папок в которых расположены файлы с кодом.
Полное имя класса включает в себя имя пакета, в котором он находится. Например, java.util.ArrayList
.
Если в начале файла с кодом (после определения пакета, к которому принадлежит класс) написать import
с именем пакета и
класса, то он станет доступен по короткому имени. Для объявления переменной этого типа достаточно будет написать
тип ArrayList
.
Например, мы могли бы упростить работу с игроками в классе футбольной команды, воспользовавшись классом
списка ArrayList
с которым работать удобней, чем с массивом. Нам не было бы нужно следить за его длиной.
package ru.java.course.football; // пакет, в котором расположен наш класс
import java.util.ArrayList; // хотим использовать динамические списки!
public class Team { // также известный как ru.java.course.football.Team
private ArrayList players = new ArrayList(); // Пустой список игроков. И никакого счетчика.
public void addPlayer(Player p) { // в параметр p будет передан игрок
this.players.add(p); // у списка есть метод добавления
}
public int getPlayersNumber() {
return this.players.size(); // у списка есть метод, возвращающий размер
}
}
Чтобы создать объект, нужно вызвать один из его конструкторов с помощью ключевого слова new
.
Конструктор - это специальный метод. При его определении в классе мы не пишем типа возвращаемого значения (результатом его работы является сам конструируемый объект), а его имя должно совпадать с именем класса. Если не написать никакого конструктора, Java создаст его сама. Он будет без параметров, и ничего делать не будет.
Объект может содержать несколько конструкторов с разными параметрами. Например, в классе Team
могут быть такие
конструкторы:
public class Team {
private String name;
public Team() {
} // Конструктор без параметров. Ленивый.
public Team(String n) { // Конструктор, заполняющий имя команды.
this.name = n;
}
/// ... другие методы и поля
}
Чтобы создать команду с именем и поместить в переменную, делаем так:
Team spartak = new Team("Спартак");
Ключевое слово new
и имя типа за ним говорят, что мы собираемся создать новый объект указанного типа. Java ищет среди
конструкторов подходящий по параметрам, и вызывает его.
Для строк, чисел, логических значений и массивов возможно также создание из литералов, как в случае со строкой "s" выше. Это сделано для упрощения синтаксиса. Например:
String s = "Я строка";
Integer i = 1;
Character c = 'c';
Boolean b = true;
Integer[] arr = {1, 2, 3};
ООП стоит на трёх китах (впрочем, некоторые считают, что их четыре, добавляя абстракцию):
- Инкапсуляция - это ограничение доступа к данным объекта извне
- Полиморфизм - это реализация одного интерфейса в различных объектах разными способами
- Наследование - это возможность использовать данные и поведение класса-предка
Инкапсуляция дает возможность определять доступные извне операции и данные объектов, скрывая сложность реализации, чтобы выставить для использования хорошо продуманный и удобный в использовании интерфейс. Наследование позволяет еще раз использовать однажды написанный в производных типах, а полиморфизм расширяет потенциал ООП возможностью изменять поведение классов в линии наследования.
Это ограничение доступа к данным объекта извне. В Java реализуется с помощью модификаторов доступа.
-
private
- члены класса, помеченные этим модификатором, не будут доступны извне. -
protected
- разрешен доступ внутри класса, в наследниках и в рамках того же пакета. -
public
- общедоступные члены класса.
Мы уже видели выше некоторые из этих модификаторов. Что ж, теперь мы знаем, что они означают.
Классы в объектно-ориентированных языках предоставляют удобный (иногда) интерфейс для использования. Интерфейсом
называется набор всех открытых (public
) членов класса.
Наследование - возможность указать предка при описании класса. При этом все public
и protected
члены класса-предка
автоматически появятся у наследника. Отношения наследования образуют между классами сильнейшую связь. Фактически, это
связь is-a или является. Рассмотрим это утверждение на примере:
public class Person {
protected String name;
}
public class Student extends Person {
protected int[] marks;
}
Этот код описывает класс "человек", характеризующийся именем. И расширяющий (наследующий) его класс "студент", у
которого также есть имя - унаследованное поле, и массив оценок. В настоящей программе наследников у класса может быть
множество, и изменение поведения класса Person
приведет к изменению поведения всех его наследников. И скорее всего
сотне багов в программе. Используйте наследование аккуратно. Простое правило для определения уместности наследования -
спросите себя, можно ли про тип-наследник, что он является также и типом-предком? Если да, используйте. Например,
каждый студент определенно является человеком.
Все объекты Java являются наследниками типа Object
. То есть, если не писать extends
, определяя класс, то Java сама
втихую допишет extends Object
.
Впрочем, методы этого класса весьма полезны. Это equals
, который должен сравнивать объекты класса между
собой, toString()
, возвращающий строковое представление, и многие другие очень важные для работы Java методы.
Менее сильной связью между объектами является композиция. Иначе говоря, один объект может содержать в себе другие в виде полей, как мы видели на примере студента с массивом оценок и команды со списком игроков.
У студента из настоящей программы различных данных будет существенно больше - это сведения о его курсе и группе, учебной программе, преподавателях и вообще чем угодно, имеющим отношение к объектной модели программы, содержащей этот тип.
Полиморфизм - это способность интерфейса иметь несколько конкретных реализаций. В Java реализуется через переопределение и перегрузку методов, generics.
Переопределение, или overriding методов в Java выглядит так. Вышеописанные классы Person
и Student
могли бы иметь
метод toString
, который возвращал бы их внутреннее состояние в виде строки.
public class Person {
protected String name;
public String toString() {
return "name: " + this.name;
}
}
public class Student extends Person {
protected int[] marks;
public String toString() {
String result = "";
result += "name: " + this.name + "; ";
result += "marks: ";
for (int mark : this.marks) {
result += mark + ", ";
}
return result;
}
}
Метод toString()
в классе Person возвращает строку вида name: Vasya
. Если мы не определим аналогичный метод в классе
Student, то переменные этого типа при вызове метода toString()
будут возвращать строку с именем. Если же мы определим
в классе Student
метод с таким же именем и набором параметров, то при вызове toString()
Java найдет нужную
реализацию и выведет не только имя, но и список оценок студента через запятую. Более того, мы можем вызвать реализацию
метода из предка с помощью ключевого слова super
. Метод toString()
класса Student
мог бы выглядеть так:
public String toString() {
String result = super.toString();
result += "marks: ";
for (int mark : this.marks) {
result += mark + ", ";
}
return result;
}
Вызов его привел бы к тому же результату.
Перегрузка, или overloading
- это способность Java выбрать правильную реализацию метода из нескольких, имеющих одно
имя, на основании количества и типов переданных параметров. Например, у строки есть несколько конструкторов - без
параметров, принимающий массив символов, другую строку и еще несколько. Java определяет, какой из них выполнить, на
основании конкретного вызова.
String s=new String(); // создаст пустую строку
String s1=new String('c'); // создаст строку из символа типа char
String s2=new String(s1); // создаст копию строки s1,
// приняв в качестве параметра объект типа String
Перегружен может быть не только конструктор, но и любой метод. Например, мы могли бы перегрузить метод toString
в
классе Person
, чтобы он добавлял переданную строку к информации об имени.
public String toString() { // первая реализация, без параметров
return "name: " + this.name;
}
public String toString(String prefix) { // вторая реализация, принимает строку.
return prefix + " " + this.name;
}
Будучи вызван у переменной p
типа Person
в программе таким образом: p.toString("Hi, my name is ");
этот метод
вернет строку Hi, my name is Vasya
.
Об этом понятии пока нам имеет смысл знать, что некоторые типы поддерживают работу с другими в качестве параметра.
Например List<Integer>
это список содержащий элементы типа Integer
. Попытка добавить в такой список строку выдаст
ошибку компиляции. Наиболее часто вы будете использовать этот вид полиморфизма в коллекциях.
List<Integer> l = new ArrayList<Integer>(); // создает пустой список на основе массива,
// в котором могут храниться только Integer
Более подробно мы будем рассматривать эту функциональность в следующих занятиях.
- https://gos-it.fandom.com/wiki/Основные_принципы_ООП:_инкапсуляция,_наследование,_полиморфизм
- https://habr.com/ru/post/87119/
- https://habr.com/ru/post/87205/
- https://javarush.ru/quests/lectures/questcore.level01.lecture01
Тема 5. Работа со строками | Оглавление | Тема 7. Класс Object и его методы