🔥 🚀 Важно для всех, кто работает с Java! 🔥
На JavaRocks ты найдешь уникальные туториалы, практические задачи и редкие книги, которых не найти в свободном доступе. Присоединяйся к нашему Telegram-каналу JavaRocks — стань частью профессионального сообщества!
Java — это объектно-ориентированный язык программирования, а значит, код на Java пишется в соответствии с принципами ООП, с использованием классов и объектов.
На примерах рассмотрим, что такое классы и объекты, а также как встраивать в код базовые принципы ООП: абстракцию, наследование, полиморфизм и инкапсуляцию.
Что такое объект?
Весь наш мир состоит из объектов. Дома, деревья, машины, мебель, посуда, компьютеры — всё это примеры объектов, и у каждого из них есть определённые характеристики, поведение и назначение.
Мы постоянно взаимодействуем с объектами и используем их по назначению. Нужно доехать до работы — садимся в машину. Хотим поесть — берём тарелку и вилку. Хотим отдохнуть — ложимся на диван.
Люди интуитивно мыслят объектами, решая задачи в реальной жизни. Это одна из причин, почему в программировании используется объектно-ориентированный подход (ООП).
Допустим, вы разработали новый смартфон и хотите запустить его в массовое производство. Как создатель, вы знаете, для чего он нужен, как работает и какие у него компоненты: корпус, микрофон, динамик, провода, кнопки и так далее. Более того, только вы понимаете, как все эти элементы взаимодействуют друг с другом.
Конечно, вы не собираетесь собирать телефоны самостоятельно — для этого есть команда, которая займётся производством. Чтобы не тратить время на бесконечные объяснения и чтобы все телефоны были одинаковыми, перед началом производства нужно сделать чертёж, где будет прописано, как устроен телефон. В ООП такой чертёж называется классом — он определяет, как должны выглядеть и вести себя объекты, которые создаются в процессе выполнения программы.
Класс — это описание объекта: его свойства (поля), поведение (методы) и правила создания (конструктор). Объект — это экземпляр класса, созданный на основе его структуры.
Абстракция
Как перейти от реального объекта к объекту в программе? Разберём на примере телефона. За последние 100 лет он сильно изменился — современные модели в разы сложнее телефонов XIX века.
Когда мы пользуемся телефоном, мы не задумываемся о его внутреннем устройстве и процессах, происходящих внутри. Мы просто используем функции, предусмотренные разработчиками: набираем номер с помощью кнопок или сенсорного экрана и совершаем звонки.
Один из первых интерфейсов телефона представлял собой рукоятку, которую нужно было крутить, чтобы совершить звонок. Конечно, это было не слишком удобно. Но со своей задачей такой механизм справлялся безупречно.
Если сравнить первый телефон и современный смартфон, станет очевидно, что их ключевые функции неизменны: возможность совершать и принимать звонки. Именно это определяет телефон как устройство связи.
А теперь обратите внимание: мы только что применили один из принципов ООП. Мы выделили самые важные характеристики объекта и отбросили всё лишнее. Этот принцип называется абстракцией.
В ООП абстракция — это метод представления элементов реального мира в виде программных объектов. Он позволяет выделить ключевые характеристики объекта и отбросить детали, не имеющие значения в рамках конкретной задачи. При этом абстракция может быть многоуровневой.
Попробуем применить этот принцип к телефонам. Для начала выделим основные типы телефонов — от самых первых моделей до современных смартфонов. Например, это можно изобразить в виде схемы на рисунке 1:

Благодаря абстракции можно определить, что объединяет все телефоны:
- Общий абстрактный объект — телефон.
- Общие характеристики телефона (например, год выпуска).
- Общий интерфейс — все телефоны умеют принимать и совершать звонки.
Пример реализации на Java:
public abstract class AbstractPhone {
private int year;
public AbstractPhone(int year) {
this.year = year;
}
public abstract void call(int outgoingNumber);
public abstract void ring(int incomingNumber);
}
Run CodeЭтот абстрактный класс служит основой для создания новых типов телефонов. Дальше мы рассмотрим, как применяются другие принципы ООП для его расширения.
Инкапсуляция
Абстракция определяет общие свойства объектов, но каждый телефон имеет свои особенности. Как же в коде обозначить границы между объектами и сохранить их отличия?
Как сделать так, чтобы никто случайно или намеренно не сломал наш телефон и не попытался превратить одну модель в другую? В реальном мире детали телефона защищает корпус. Без него любые внутренние компоненты были бы уязвимы для внешнего воздействия, что неизбежно привело бы к сбоям.
В программировании эту задачу решает инкапсуляция. Она объединяет свойства и поведение объекта внутри класса, скрывает детали реализации и предоставляет безопасный способ взаимодействия через доступный интерфейс.
Задача программиста — определить, какие атрибуты и методы объекта должны быть доступны извне, а какие являются внутренними деталями реализации и не должны подвергаться внешнему воздействию.
Инкапсуляция и управление доступом
Допустим, при производстве телефона на его корпусе выгравировали год выпуска и логотип компании. Эти данные относятся только к этой модели и остаются неизменными. Можно сказать, что производитель защитил их от изменений — ведь вряд ли кто-то будет пытаться стереть гравировку.
В Java класс определяет состояние объектов с помощью полей, а их поведение — с помощью методов. Доступ к этим данным регулируется модификаторами доступа: private, protected, public и default (модификатор по умолчанию).
Допустим, нам нужно скрыть внутренние детали телефона — например, год выпуска, имя производителя и один из методов, чтобы другие объекты не могли их менять.
Вот как это выглядит в коде:
public class SomePhone {
private int year;
private String company;
public SomePhone(int year, String company) {
this.year = year;
this.company = company;
}
private void openConnection(){
}
public void call() {
openConnection();
System.out.println("Calling");
}
public void ring() {
System.out.println("Ring-ring");
}
}
Run CodeМодификатор private позволяет обращаться к полям и методам класса только внутри этого класса. Это означает, что нельзя получить доступ к приватным полям извне, а приватные методы невозможно вызвать напрямую.
Если мы сделали метод openConnection
приватным, мы можем менять его реализацию как угодно — никто извне его всё равно не тронет. Это удобно, потому что гарантирует, что изменения не нарушат работу других объектов в программе.
Чтобы телефон функционировал, мы оставляем публичными методы call
и ring
, используя модификатор public. Открытие методов для работы с объектами — тоже часть инкапсуляции. Если бы доступ к ним был полностью закрыт, объект просто бы стал бесполезным.
Наследование
Если ещё раз присмотреться к схеме телефонов на рисунке 1, можно увидеть иерархическую структуру. В ней каждая новая модель получает все особенности предыдущих, но при этом добавляет что-то своё.
Например:
- Смартфон поддерживает сотовую связь (унаследовал от мобильного телефона).
- Он беспроводной и портативный (свойства радиотелефона).
- Может звонить и принимать звонки (свойства обычного телефона).
Это и есть наследование свойств объектов.
Наследование в программировании означает создание новых классов на основе уже существующих.
В качестве примера создадим класс Smartphone
с помощью наследования. Все беспроводные телефоны работают от перезаряжаемых батарей, которые имеют ограниченный срок службы. Поэтому добавим это свойство в класс CordlessPhone
:
public abstract class CordlessPhone extends AbstractPhone {
private int hour;
public CordlessPhone (int year, int hour) {
super(year);
this.hour = hour;
}
}
Run CodeМобильные телефоны (класс Cellphone
) наследуют свойства беспроводного телефона (класс CordlessPhone
), и мы реализуем методы call
и ring
в этом классе:
public class CellPhone extends CordlessPhone {
public CellPhone(int year, int hour) {
super(year, hour);
}
@Override
public void call(int outgoingNumber) {
System.out.println("Calling " + outgoingNumber);
}
@Override
public void ring(int incomingNumber) {
System.out.println("Incoming call from " + incomingNumber);
}
}
Run CodeИ, наконец, у нас есть класс Smartphone
, который, в отличие от классических мобильных телефонов, оснащён полноценной операционной системой. Можно расширять функционал смартфона, добавляя новые приложения, которые могут работать на его ОС. В коде класс будет выглядеть так:
public class Smartphone extends CellPhone {
private String operationSystem;
public Smartphone(int year, int hour, String operationSystem) {
super(year, hour);
this.operationSystem = operationSystem;
}
public void install(String program) {
System.out.println("Installing " + program + " for " + operationSystem);
}
}
Run CodeКак видите, для создания класса Smartphone
пришлось написать довольно много кода, но в итоге мы получили класс с дополнительными возможностями. Этот принцип ООП помогает существенно сократить код на Java, облегчая жизнь программисту.
Полиморфизм
Несмотря на различия во внешнем виде и конструкции разных телефонов, у них есть общее поведение: все они могут принимать и совершать звонки, а также имеют понятный и простой интерфейс управления.
Согласно принципу абстракции (который мы уже рассматривали), у всех телефонов есть общий интерфейс. Именно поэтому люди без труда осваивают новые модели, если управление остаётся привычным (например, кнопки или сенсорный экран).
Принцип ООП, который позволяет программе использовать объекты с общим интерфейсом, не зная их внутреннего устройства, называется полиморфизмом.
Представим, что в нашей программе есть пользователь, который может позвонить другому пользователю с любого устройства, поддерживающего звонки. Вот как это можно сделать:
public class User {
private String name;
public User(String name) {
this.name = name;
}
public void callAnotherUser(int number, AbstractPhone phone){
phone.call(number);
}
}
}
Run CodeТеперь мы рассмотрим несколько типов телефонов. Начнём с одного из первых:
public class ThomasEdisonPhone extends AbstractPhone {
public ThomasEdisonPhone(int year) {
super(year);
}
@Override
public void call(int outgoingNumber) {
System.out.println("Crank the handle");
System.out.println("What number would you like to connect to?");
}
@Override
public void ring(int incomingNumber) {
System.out.println("The phone is ringing");
}
}
Run CodeОбычный стационарный телефон:
public class Phone extends AbstractPhone {
public Phone(int year) {
super(year);
}
@Override
public void call(int outgoingNumber) {
System.out.println("Calling " + outgoingNumber);
}
@Override
public void ring(int incomingNumber) {
System.out.println("The phone is ringing");
}
}
Run CodeИ наконец, продвинутый видеотелефон:
public class VideoPhone extends AbstractPhone {
public VideoPhone(int year) {
super(year);
}
@Override
public void call(int outgoingNumber) {
System.out.println("Connecting video call to " + outgoingNumber);
}
@Override
public void ring(int incomingNumber) {
System.out.println("Incoming video call from " + incomingNumber);
}
}
Run CodeВ методе main()
создадим несколько объектов и протестируем вызов метода callAnotherUser()
:
AbstractPhone firstPhone = new ThomasEdisonPhone(1879);
AbstractPhone phone = new Phone(1984);
AbstractPhone videoPhone=new VideoPhone(2018);
User user = new User("Jason");
user.callAnotherUser(224466, firstPhone);
user.callAnotherUser(224466, phone);
user.callAnotherUser(224466, videoPhone);
Run CodeВызов одного и того же метода у объекта User
даёт разные результаты. Конкретная реализация метода call
выбирается динамически внутри callAnotherUser()
, в зависимости от типа объекта, переданного во время выполнения программы.
Ключевое преимущество полиморфизма — способность определять реализацию во время выполнения (runtime polymorphism).
В рассмотренных выше примерах классов телефонов применялось переопределение методов (method overriding) — механизм, при котором метод из базового класса получает новую реализацию в подклассе без изменения его сигнатуры.
Таким образом, при вызове метода во время выполнения программы используется именно реализация из подкласса, а не из родительского класса.
Обычно при переопределении метода используется аннотация @Override
. Она сообщает компилятору, что метод из родительского класса был заменён, и проверяет соответствие сигнатур переопределяемого и нового метода.
Чтобы ваш код соответствовал принципам объектно-ориентированного программирования, придерживайтесь следующих правил:
- выделяйте ключевые характеристики объектов.
- если несколько классов имеют общие свойства и методы, используйте наследование.
- описывайте объекты через абстрактные типы.
- старайтесь скрывать методы и поля, относящиеся к внутренней реализации класса.
Перевод статьи «Principles of OOP».