Java — это основанный на классах объектно-ориентированный язык программирования (ООП), построенный вокруг концепции объектов. Концепции ООП предназначены для улучшения читаемости и повторного использования кода путем определения того, как эффективно структурировать вашу программу на Java. Основными принципами объектно-ориентированного программирования являются:
1. Абстракция
2. Инкапсуляция
3. Наследование
4. Полиморфизм
5. Ассоциация
6. Агрегация
7. Композиция
Java поставляется со специфическими структурами кода для каждой концепции ООП, такими как ключевое слово extends для принципа наследования или методы getter и setter для принципа инкапсуляции.
Хотя эти концепции имеют решающее значение для создания хорошо структурированных Java-программ на этапе разработки, внедрение отчетов о сбоях также может помочь вам выявить ошибки, с которыми сталкиваются ваши конечные пользователи на этапе эксплуатации и сопровождения жизненного цикла разработки программного обеспечения.
В этом руководстве мы рассмотрим как теорию, так и практику объектно-ориентированного программирования, чтобы помочь вам писать производительный и безошибочный код на Java.
Что такое концепции ООП в Java?
Концепции ООП позволяют нам создавать специфические взаимодействия между объектами Java. Они позволяют повторно использовать код без создания рисков для безопасности или ущерба для производительности и читаемости кода.
Существует четыре основных и три второстепенных принципа объектно-ориентированного программирования на Java. Давайте рассмотрим, что это такое и почему они полезны.
1. Абстракция
Абстракция направлена на то, чтобы скрыть сложность от пользователей и показать им только необходимую информацию. Например, если вы управляете автомобилем, вам не нужно знать о его внутреннем устройстве.
То же самое справедливо и для классов Java. Вы можете скрыть внутренние детали реализации, используя абстрактные классы или интерфейсы. На абстрактном уровне вам нужно только определить сигнатуры методов (имя и список параметров) и позволить каждому классу реализовать их по-своему.
Абстракция в Java:
- Скрывает базовую сложность данных
- Помогает избежать повторяющегося кода
- Представляет только сигнатуру внутренней функциональности
- Дает гибкость программистам для изменения реализации абстрактного поведения
- Частичная абстракция (0-100%) может быть достигнута с помощью абстрактных классов
- Полная абстракция (100%) может быть достигнута с помощью интерфейсов
2. Инкапсуляция
Инкапсуляция помогает обеспечить безопасность данных, позволяя защитить данные, хранящиеся в классе, от доступа всей системы. Как следует из названия, она защищает внутреннее содержимое класса, как капсулу.
Вы можете реализовать инкапсуляцию в Java, сделав поля (переменные класса) приватными и получив доступ к ним через их публичные методы getter и setter. JavaBeans являются примерами полностью инкапсулированных классов.
Инкапсуляция в Java:
- Ограничивает прямой доступ к членам данных (полям) класса.
- Поля имеют статус private
- Каждое поле имеет методы getter и setter.
- Методы getter возвращают значение поля
- Методы setter позволяют нам изменять значение поля.
3. Наследование
Наследование позволяет создать дочерний класс, который наследует поля и методы родительского класса. Дочерний класс может переопределять значения и методы родительского класса, но это не обязательно. Он также может добавлять новые данные и функциональные возможности в свой родительский класс.
Родительские классы также называют суперклассами или базовыми классами, а дочерние классы — подклассами или производными классами. Java использует ключевое слово extends для реализации принципа наследования в коде.
Наследование в Java:
- Класс (дочерний класс) может расширять другой класс (родительский класс), наследуя его свойства
- Реализует принцип программирования DRY (Don’t Repeat Yourself)
- Улучшает возможность повторного использования кода
- Многоуровневое наследование разрешено в Java (дочерний класс может иметь свой собственный дочерний класс)
- Множественное наследование в Java не допускается (класс не может расширять более одного класса)
- Абстракция, инкапсуляция, полиморфизм и наследование — это четыре основных теоретических принципа объектно-ориентированного программирования. Но Java также работает с тремя другими концепциями ООП: ассоциация, агрегация и композиция. Агрегация — это особая форма ассоциации, а композиция — особая форма агрегации. Хотя это может показаться немного запутанным, мы сейчас все объясним. Читайте дальше!
4. Полиморфизм
Полиморфизм означает способность выполнять определенное действие различными способами. В Java полиморфизм может принимать две формы: перегрузка методов и переопределение методов.
Перегрузка методов происходит, когда в классе присутствуют различные методы с одинаковым именем. Когда они вызываются, их различают по количеству, порядку или типам параметров. Переопределение метода происходит, когда дочерний класс переопределяет метод своего родителя.
Полиморфизм в Java:
- Одно и то же имя метода используется несколько раз
- Из объекта могут быть вызваны разные методы с одинаковым именем
- Все объекты Java можно считать полиморфными (как минимум, они имеют свой тип и являются экземплярами класса Object).
- Статический полиморфизм в Java реализуется перегрузкой методов
- Динамический полиморфизм в Java реализуется путем переопределения методов
- 5. Ассоциация
- Ассоциация означает акт установления отношений между двумя несвязанными классами. Например, когда вы объявляете два поля разных типов (например, Car и Bicycle) в одном классе и заставляете их взаимодействовать друг с другом, вы создали ассоциацию.
5. Ассоциация
Ассоциация означает акт установления отношений между двумя несвязанными классами. Например, когда вы объявляете два поля разных типов (например, Car и Bicycle) в одном классе и заставляете их взаимодействовать друг с другом, вы создали ассоциацию.
Ассоциация в Java:
- Два отдельных класса связаны через свои объекты
- Два класса не связаны между собой, каждый из них может существовать без другого.
- Отношения могут быть один к одному, один ко многим, много к одному или много ко многим.
6. Агрегация
Агрегация — это более узкий вид ассоциации. Она возникает, когда между двумя классами, которые мы ассоциируем через их объекты, существует односторонняя связь (HAS-A).
Например, у каждого пассажира есть автомобиль, но у автомобиля не обязательно есть пассажир. Когда вы объявляете класс Passenger, вы можете создать поле типа Car, которое показывает, к какому автомобилю принадлежит пассажир. Затем, когда вы создаете новый объект Passenger, вы можете получить доступ к данным, хранящимся в связанном с ним Car.
Агрегация в Java:
- Однонаправленная ассоциация
- Представляет собой отношение HAS-A между двумя классами.
- Только один класс зависит от другого
7. Композиция
Композиция — это более строгая форма агрегации. Она возникает, когда два класса, которые вы связываете, являются взаимозависимыми и не могут существовать друг без друга.
Например, возьмем класс «Автомобиль» и класс «Двигатель». Автомобиль не может работать без двигателя, а двигатель также не может функционировать, не будучи встроенным в автомобиль. Такие отношения между объектами также называются отношениями PART-OF.
Композиция в Java:
- Ограниченная форма агрегации
- Представляет собой отношение ЧАСТЬ-ОФ между двумя классами
- Оба класса зависят друг от друга
- Если один класс перестает существовать, другой не может выжить в одиночку.
Абстракция
Как уже упоминалось выше, абстракция позволяет скрыть внутреннюю работу объекта и показать только те функции, о которых необходимо знать пользователю.
Java предоставляет два способа реализации абстракции: абстрактные классы и интерфейсы. С помощью абстрактных классов можно добиться частичной абстракции, а интерфейсы делают возможной полную (100%) абстракцию.
Абстрактные классы
Абстрактный класс — это суперкласс (родительский класс), который не может быть инстанцирован. Чтобы создать новый объект, необходимо инстанцировать один из его дочерних классов. Абстрактные классы могут иметь как абстрактные, так и конкретные методы. Абстрактные методы содержат только сигнатуру метода, в то время как конкретные методы объявляют также тело метода. Абстрактные классы определяются с помощью ключевого слова abstract.
В приведенном ниже примере кода мы создаем абстрактный класс Animal с двумя абстрактными и одним конкретным методом.
abstract class Animal {
// abstract methods
abstract void move();
abstract void eat();
// concrete method
void label() {
System.out.println("Animal's data:");
}
}
Затем мы расширяем его двумя дочерними классами: Bird и Fish. Оба они определяют свои собственные реализации абстрактных методов move() и eat().
class Bird extends Animal {
void move() {
System.out.println("Moves by flying.");
}
void eat() {
System.out.println("Eats birdfood.");
}
}
class Fish extends Animal {
void move() {
System.out.println("Moves by swimming.");
}
void eat() {
System.out.println("Eats seafood.");
}
}
Теперь мы протестируем его с помощью классов TestBird и TestFish. Оба инициализируют объект (myBird и myFish) и вызывают один конкретный (label()) и два абстрактных (move() и eat()) метода.
Заметьте, однако, что вам не обязательно вызывать все методы, если вы этого не хотите — таким образом абстрактные классы делают возможной частичную абстракцию (например, вы можете вызвать только move()).
class TestBird {
public static void main(String[] args) {
Animal myBird = new Bird();
myBird.label();
myBird.move();
myBird.eat();
}
}
class TestFish {
public static void main(String[] args) {
Animal myFish = new Fish();
myFish.label();
myFish.move();
myFish.eat();
}
}
Как вы можете видеть ниже, конкретный метод был вызван из абстрактного класса Animal, в то время как два абстрактных метода были вызваны из Bird и Fish соответственно.
[Console output of TestBird]
Animal's data:
Moves by flying.
Eats birdfood.
[Console output of TestFish]
Animal's data:
Moves by swimming.
Eats seafood.
Инкапсуляция
С помощью инкапсуляции вы можете защитить поля класса. Для этого необходимо объявить поля как приватные и обеспечить доступ к ним с помощью методов getter и setter.
Приведенный ниже класс Animal полностью инкапсулирован. У него есть три приватных поля, и каждое из них имеет свою пару методов getter и setter.
class Animal {
private String name;
private double averageWeight;
private int numberOfLegs;
// Getter methods
public String getName() {
return name;
}
public double getAverageWeight() {
return averageWeight;
}
public int getNumberOfLegs() {
return numberOfLegs;
}
// Setter methods
public void setName(String name) {
this.name = name;
}
public void setAverageWeight(double averageWeight) {
this.averageWeight = averageWeight;
}
public void setNumberOfLegs(int numberOfLegs) {
this.numberOfLegs = numberOfLegs;
}
}
Класс TestAnimal сначала создает новый объект Animal (с именем myAnimal), затем определяет значение для каждого поля с помощью методов setter и, наконец, выводит значения с помощью методов getter.
class TestAnimal {
public static void main(String[] args) {
Animal myAnimal = new Animal();
myAnimal.setName("Eagle");
myAnimal.setAverageWeight(1.5);
myAnimal.setNumberOfLegs(2);
System.out.println("Name: " + myAnimal.getName());
System.out.println("Average weight: " + myAnimal.getAverageWeight() + "kg");
System.out.println("Number of legs: " + myAnimal.getNumberOfLegs());
}
}
Как вы можете видеть ниже, консоль Java возвращает все значения, которые мы задали с помощью методов setter:
[Console output of TestAnimal]
Name: Eagle
Average weight: 1.5kg
Number of legs: 2
Интерфейсы
Интерфейс — это 100% абстрактный класс. Он может иметь только статические, конечные и публичные поля и абстрактные методы. Его часто называют чертежом класса. Интерфейсы Java позволяют реализовать множественное наследование в коде, поскольку класс может реализовать любое количество интерфейсов. Классы могут обращаться к интерфейсу с помощью ключевого слова implements.
В примере мы определяем два интерфейса: Animal с двумя абстрактными методами (методы интерфейса абстрактны по умолчанию) и Bird с двумя статическими полями и абстрактным методом.
interface Animal {
public void eat();
public void sound();
}
interface Bird {
int numberOfLegs = 2;
String outerCovering = "feather";
public void fly();
}
Класс Eagle реализует оба интерфейса. Он определяет свою собственную функциональность для трех абстрактных методов. Методы eat() и sound() взяты из класса Animal, а fly() — из Bird.
class Eagle implements Animal, Bird {
public void eat() {
System.out.println("Eats reptiles and amphibians.");
}
public void sound() {
System.out.println("Has a high-pitched whistling sound.");
}
public void fly() {
System.out.println("Flies up to 10,000 feet.");
}
}
В тестовом классе TestEagleInterfaces мы создаем новый объект Eagle (называемый myEagle) и выводим все поля и методы на консоль.
Поскольку статические поля (numberOfLegs и outerCovering) принадлежат не конкретному объекту, а интерфейсу, нам нужно обращаться к ним из интерфейса Bird, а не из объекта myEagle.
class TestEagleInterfaces {
public static void main(String[] args) {
Eagle myEagle = new Eagle();
myEagle.eat();
myEagle.sound();
myEagle.fly();
System.out.println("Number of legs: " + Bird.numberOfLegs);
System.out.println("Outer covering: " + Bird.outerCovering);
}
}
Java-консоль возвращает всю информацию, к которой мы хотели получить доступ:
[Console output of TestEagleInterfaces]
Eats reptiles and amphibians.
Has a high-pitched whistling sound.
Flies up to 10,000 feet.
Number of legs: 2
Outer covering: feather
Полиморфизм
Полиморфизм позволяет использовать одну и ту же структуру кода в разных формах. В Java это означает, что вы можете объявить несколько методов с одинаковым именем, если они отличаются по определенным характеристикам.
Как упоминалось выше, Java предоставляет два способа реализации полиморфизма: перегрузка методов и переопределение методов.
Статический полиморфизм (перегрузка методов)
Перегрузка методов означает, что в классе можно иметь несколько методов с одинаковым именем. Однако количество, имена или типы их параметров должны быть разными.
Например, в классе Bird(), представленном ниже, есть три метода fly(). Первый из них не имеет параметров, второй имеет один параметр (высота), а третий — два параметра (имя и высота).
class Bird {
public void fly() {
System.out.println("The bird is flying.");
}
public void fly(int height) {
System.out.println("The bird is flying " + height + " feet high.");
}
public void fly(String name, int height) {
System.out.println("The " + name + " is flying " + height + " feet high.");
}
}
Тестовый класс создает новый объект Bird и вызывает метод fly() три раза: первый — без параметров, второй — с одним целочисленным параметром высоты и третий — с двумя параметрами имени и высоты.
class TestBirdStatic {
public static void main(String[] args) {
Bird myBird = new Bird();
myBird.fly();
myBird.fly(10000);
myBird.fly("eagle", 10000);
}
}
В консоли видно, что Java могла бы различать три полиморфных метода fly():
[Console output of TestBirdStatic]
The bird is flying.
The bird is flying 10000 feet high.
The eagle is flying 10000 feet high.
Динамический полиморфизм (переопределение методов)
Используя функцию переопределения методов в Java, вы можете переопределять методы родительского класса из его дочернего класса.
В приведенном ниже примере кода класс Bird расширяет класс Animal. Оба класса имеют метод eat(). По умолчанию Bird наследует метод eat() своего родителя. Однако, поскольку она также определяет свой собственный метод eat(), Java переопределит исходный метод и вызовет eat() из дочернего класса.
class Animal {
public void eat() {
System.out.println("This animal eats insects.");
}
}
class Bird extends Animal {
public void eat() {
System.out.println("This bird eats seeds.");
}
}
Класс TestBirdDynamic сначала создает новый объект Animal и вызывает его метод eat(). Затем он также создает объект Bird и снова вызывает полиморфный метод eat().
class TestBirdDynamic {
public static void main(String[] args) {
Animal myAnimal = new Animal();
myAnimal.eat();
Bird myBird = new Bird();
myBird.eat();
}
}
Консоль правильно возвращает значения соответствующих методов, потому что Java могла бы различать два метода eat():
[Console output of TestBirdDynamic]
This animal eats insects.
This bird eats seeds.
Наследование
Резюме
Концепции ООП в Java помогают вам более эффективно структурировать вашу программу. Семь принципов объектно-ориентированного подхода, которые мы рассмотрели здесь (абстракция, инкапсуляция, полиморфизм, наследование, ассоциация, агрегация и композиция), могут помочь вам повторно использовать ваш код, предотвратить проблемы безопасности и повысить производительность ваших Java-приложений.