ООП в Java: четыре принципа с примерами
Объектно-ориентированное программирование (ООП) — это методология программирования с использованием объектов и классов.
Объект характеризует состояние и поведение.Например, у кота есть такие свойства, как имя, порода, цвет. Представим, что они отражают его состояние. Кот может мурчать, спать, охотиться, есть и так далее — это проявления его поведения.
С помощью таких характеристик можно описать любого кота. Шаблон, в котором описаны общие черты и действия похожих друг на друга объектов, называется классом. А объект — это конкретный экземпляр класса. Например, рыжий короткошерстный кот Альбатрос и серый пушистый кот Петька — это объекты класса «кот».
В классах Java состояние представлено в виде полей, а поведение — в виде методов.
Принципы ООП
Объектно-ориентированное программирование опирается на четыре принципа:
- Наследование — это передача всех свойств и поведения от одного класса другому, более конкретному. У карася и ерша, как и у всех рыб, есть плавники, хвосты, жабры и чешуя, они живут в воде и плавают.
- Абстракция — это сокрытие подробностей и предоставление пользователю лишь самых важных характеристик объекта. Например, в адресе здания важны такие данные, как почтовый индекс, страна, населенный пункт, улица и номер дома. Его этажность и материал стен в таком случае не имеют значения.
- Инкапсуляция — это размещение данных и методов для их обработки в одном объекте, а также сокрытие деталей его реализации. Мы знаем, как включать и выключать телевизор, переключать программы и регулировать громкость. Для этого не обязательно знать, как он устроен.
- Полиморфизм — это проявление одного поведения разными способами. Животные могут издавать звуки, при этом кошка мяукает, а собака лает.
Рассмотрим эти принципы подробнее.
Чтобы полностью разобраться в особенностях языка Java, лучше всего записаться на специальные курсы. Наши друзья из Mate Academy и Hillel подготовили подробную программу курса, которая поможет с освоением языка. Практикующие менторы смогут ответить на любые вопросы.
Наследование
Наследование позволяет использовать код повторно. Это достигается за счет того, что в одном классе содержатся свойства и методы, общие для более конкретных классов. Класс, от которого наследуются свойства и методы, называется суперклассом (родительским классом). Классы, которые наследуют их, называются подклассами (дочерними классами). Таким образом создается иерархия классов.
На вершине иерархии находится базовый класс. Он не является подклассом, то есть не наследует свойств и методов от других классов. На его основе создаются остальные классы иерархии.
Создадим базовый класс Animal, который описывает животное.
class Animal { }
Допустим, у животного есть имя и оно издает какой-то звук. Определим имя и звук как строковые поля.
class Animal { private String name; private String voice; }
Ключевое слово private — это модификатор доступа, который означает, что поле будет доступно только в данном классе и его подклассах. Таким образом мы запрещаем изменение значений двух полей этого класса извне.
Чтобы создать экземпляр класса (объект) и задать начальные значения полей, объявим общедоступный конструктор, используя модификатор доступа public. Он позволит обращаться к конструктору извне.
class Animal { private String name; private String voice; public Animal(String name, String voice){ this.name = name; this.voice = voice; } }
Ключевое слово this — это ссылка на создаваемый объект. Для обращения к полю внутри объекта используется синтаксис:
this.<имя поля>
В этом конструкторе мы присваиваем полям объекта значения, переданные в формальных параметрах.
На данном этапе уже реализовано состояние объекта. Теперь реализуем его поведение.
class Animal { private String name; private String voice; public Animal(String name, String voice){ this.name = name; this.voice = voice; } public void speak() { System.out.println(this.name + " says " + this.voice); } }
Мы объявили общедоступный метод speak(), в котором на консоли выводится значение поля voice.
Создадим класс Cat, который будет представлять кота и унаследует от класса Animal его свойства и поведение. Для создания подкласса используется ключевое слово extends.
class Cat extends Animal { public Cat(String name){ super(name, "meow"); } }
Благодаря наследованию нам не пришлось еще раз писать код, чтобы дать коту имя и указать звук, который он издает. Имя конкретного кота мы заранее не знаем, но знаем, что коты мяукают. Поэтому конструктор этого класса принимает только один формальный параметр name.
Для обращения к суперклассу из подкласса используется ключевое слово super. В данном случае мы вызываем конструктор суперкласса и передаем ему формальный параметр name и литерал meow. Конструктор суперкласса присваивает унаследованным переменным объекта переданные значения.
Теперь мы можем создать экземпляр класса Cat и воспользоваться методом speak(), унаследованным от суперкласса, чтобы «услышать», как мяукает кот.
В языке Java все (точнее, почти все) является объектом. Поэтому мы создаем класс Main с методом main, в котором содержатся инструкции программы. В нем мы объявляем переменную класса Cat для создания объекта. Чтобы инициализировать его, обращаемся к конструктору, используя ключевое слово new, и задаем имя питомца:
public class Main { public static void main(String args[]) { Cat albatros = new Cat("Albatros"); albatros.speak(); } }
Полный код будет выглядеть так:
public class Main { public static void main(String args[]) { Cat albatros = new Cat("Albatros"); albatros.speak(); } } class Animal { private String name; private String voice; public Animal(String name, String voice){ this.name = name; this.voice = voice; } public void speak() { System.out.println(this.name + " says " + this.voice); } } class Cat extends Animal { public Cat(String name){ super(name, "meow"); } }
Абстракция
Для решения сложной задачи нужно разделить ее на части, с которыми удобно работать. Некоторые части могут быть похожими друг на друга, то есть иметь общие признаки. Например, у сотрудника компании и у клиента есть имя, фамилия, адрес. Эти общие свойства можно вынести в отдельный более абстрактный класс.
При моделировании реальных объектов совсем необязательно учитывать все их характеристики. Как правило, для решения определенной задачи бывает достаточно лишь нескольких. Поэтому в определении клиента и сотрудника неважен рост или цвет волос (если только этого не требует задача).
Создадим класс Person и определим в нем общие характеристики.
class Person { protected String firstName; protected String lastName; protected String address; public Person(String firstName, String lastName, String address){ this.firstName = firstName; this.lastName = lastName; this.address = address; } public void display(){ System.out.println(this.firstName + " " + this.lastName); System.out.println("Address: " + this.address); } }
Унаследуем от него классы Customer и Employee. Добавим для клиента номер банковского счета, а для сотрудника — размер зарплаты.
class Customer extends Person { private String bankAccountNumber; public Customer(String firstName, String lastName, String address, String bankAccountNumber) { super(firstName, lastName, address); this.bankAccountNumber = bankAccountNumber; } @Override public void display() { super.display(); System.out.println("Bank account number: " + this.bankAccountNumber); } } class Employee extends Person { private double salary; public Employee(String firstName, String lastName, String address, double salary) { super(firstName, lastName, address); this.salary = salary; } @Override public void display() { super.display(); System.out.println("Salary: " + this.salary); } }
Так, мы избавились от повторного написания кода, выделив общие признаки в суперкласс. Рабочий пример выглядит так:
public class Main { public static void main(String args[]) { Customer henry = new Customer("Henry", "Baskerville", "Baskerville Hall", "GB29 NWBK 6016 1331 9268 19"); Employee sherlock = new Employee("Sherlock", "Holmes", "221b Baker St", 61632); henry.display(); System.out.println(""); sherlock.display(); } } class Person { protected String firstName; protected String lastName; protected String address; public Person(String firstName, String lastName, String address){ this.firstName = firstName; this.lastName = lastName; this.address = address; } public void display(){ System.out.println(this.firstName + " " + this.lastName); System.out.println("Address: " + this.address); } } class Customer extends Person { private String bankAccountNumber; public Customer(String firstName, String lastName, String address, String bankAccountNumber) { super(firstName, lastName, address); this.bankAccountNumber = bankAccountNumber; } @Override public void display() { super.display(); System.out.println("Bank account number: " + this.bankAccountNumber); } } class Employee extends Person { private double salary; public Employee(String firstName, String lastName, String address, double salary) { super(firstName, lastName, address); this.salary = salary; } @Override public void display() { super.display(); System.out.println("Salary: " + this.salary); } }
Инкапсуляция
Инкапсуляция дает возможность предоставить пользователю методы, которые нужны для работы с объектами, и скрыть от него детали их реализации. Площадь треугольника и площадь прямоугольника вычисляются по разным формулам. Несмотря на это, можно объявить для обеих фигур метод square, который получит разные реализации.
Площадь прямоугольника равна произведению длин его сторон. Площадь треугольника по сторонам можно вычислить по формуле Герона. Создадим абстрактный класс Area, который будет представлять геометрическую фигуру.
abstract class Shape { public abstract double area(); }
Абстрактный класс, как и его абстрактный метод, объявляются с помощью ключевого слова abstract. Абстрактный метод не имеет реализации, он лишь объявлен в коде класса.
Создадим производные классы Rectangle и Triangle.
class Rectangle extends Shape { private final double width, length; public Rectangle(double width, double length) { this.width = width; this.length = length; } @Override public double area() { return width * length; } } class Triangle extends Shape { private final double a, b, c; public Triangle(double a, double b, double c) { this.a = a; this.b = b; this.c = c; } @Override public double area() { double halfPerimeter = (this.a + this.b + this.c) / 2; return Math.sqrt(halfPerimeter * (halfPerimeter - this.a) * (halfPerimeter - this.b) * (halfPerimeter - this.c)); } }
В этих классах объявлены стороны и переопределен унаследованный метод area().
Стороны объявлены с использованием модификатора final, который означает, что значение данного поля — это константа, и поэтому не может быть изменено во время выполнения программы. Если объявить класс как final, то он не сможет иметь подклассов.
Подклассы могут переопределять методы суперкласса с использованием аннотации @Override. Как видим, в переопределенных методах, в отличие от абстрактного, содержится код вычисления площади.
Для вычисления площади треугольника мы используем статический метод sqrt() класса Math. Чтобы воспользоваться таким методом в программе, его нужно импортировать:
import java.lang.Math;
Полный код будет выглядеть так:
import java.lang.Math; public class Main { public static void main(String args[]) { Rectangle r = new Rectangle(5, 10); Triangle t = new Triangle(10, 10, 10); System.out.println("Square of the rectangle is " + r.area()); System.out.println("Square of the triangle is " + t.area()); } } abstract class Shape { public abstract double area(); } class Rectangle extends Shape { private final double width, length; public Rectangle(double width, double length) { this.width = width; this.length = length; } @Override public double area() { return width * length; } } class Triangle extends Shape { private final double a, b, c; public Triangle(double a, double b, double c) { this.a = a; this.b = b; this.c = c; } @Override public double area() { double halfPerimeter = (this.a + this.b + this.c) / 2; return Math.sqrt(halfPerimeter * (halfPerimeter - this.a) * (halfPerimeter - this.b) * (halfPerimeter - this.c)); } }
Хоть площадь этих фигур определяется по разным формулам, мы просто вызываем метод area(), не заботясь о том, как производятся вычисления.
Полиморфизм
Используя полиморфизм, можно обращаться к методам экземпляров суперкласса и его подклассов, как к методам одинаковых объектов. Допустим, существует два музыканта: клавишник и гитарист. Оба они могут играть, но играют на разных инструментах.
Рассмотрим полный пример кода:
import java.util.List; import java.util.Arrays; public class Main { public static void main(String args[]) { Guitarist ritchie = new Guitarist("Ritchie"); Keyboardist john = new Keyboardist("John"); Musician david = new Musician("David"); List<Musician> musicians = Arrays.asList(ritchie, john, david); for (Musician m : musicians){ m.play(); } } } class Musician { protected String name; public void play(){ System.out.println(this.name + " plays anything he sees."); } public Musician(String name){ this.name = name; } } class Guitarist extends Musician { public Guitarist(String name) { super(name); } @Override public void play() { System.out.println(this.name + " plays a guitar."); } } class Keyboardist extends Musician { public Keyboardist(String name) { super(name); } @Override public void play() { System.out.println(this.name + " plays a piano."); } }
Обратите внимание, что в определении суперкласса мы используем модификатор protected для поля name. Этот модификатор позволяет обращаться к нему не только из данного класса, но и из его подклассов. Прямой доступ извне по-прежнему закрыт.
В методе Main мы создаем список объектов класса Musician, в котором могут находиться и экземпляры унаследованных от него классов:
List<Musician> musicians = Arrays.asList(ritchie, john);
Затем в цикле мы перечисляем музыкантов и вызываем для каждого из них метод play(). Поскольку этот метод реализован во всех классах, не приходится заботиться о том, на чем именно играет каждый музыкант, и писать код вызова метода для каждого из них отдельно.
Причины появления ООП
По мере того, как совершенствовались компьютеры, требовалось создавать все больше функций. Разобраться в коде и разделить задачу на части становилось труднее и труднее.
Объектно-ориентированное программирование было создано как ответ на эти трудности. Оно позволило объединить связанные участки кода и отделить их от тех участков, с которыми они были связаны слабо.
В результате вместо огромного количества процедур и переменных требовалось помнить лишь те, которые нужны для применения объекта (интерфейс). Остальная часть его реализации была скрыта.
Если требовалось внести изменения или улучшить код, это стало происходить незаметно для пользователя, потому что интерфейс не менялся. Кроме того, наследование давало возможность повторно использовать код.
ООП упрощает понимание кода и позволяет экономить много времени при его написании.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: