Прежде чем мы поговорим об объектно-ориентированном программировании, нам нужно поговорить о самом объекте в java.

Объект в Java

Объектно-ориентированное программирование - это парадигма программирования, в которой объекты играют главную роль. Объект - это сущность со свойствами и методами. Очень часто методы объекта манипулируют свойствами объекта. Еще одна важная вещь, о которой следует помнить, что объекты работают со ссылками, или мы также можем сказать указатели. В Java объект создается из своего класса. Класс предоставит нам шаблон и определит, какие свойства и методы должен иметь объект этого класса. Класс - это план объекта, а объект - реализация своего класса. Давайте посмотрим на пример

В приведенном выше примере мы создали два объекта из класса Employee. Стрелки показывают, что emp и emp2 являются ссылками на свои объекты. Также мы видим, что два объекта не связаны друг с другом и их свойства имеют разные значения. Два объекта созданы по-разному. Первый был создан с помощью пустого конструктора, и мы видим, что свойства объекта изначально будут иметь значения по умолчанию (для объектов - null, для numeric - 0, а для boolean - false). Второй объект создается конструктором с двумя аргументами, и мы присваиваем значения свойствам объекта при создании.

Employee emp = new Employee();

Вышеупомянутая строка создаст объект. Java выделит место в куче для объекта, и emp укажет на него. emp сам по себе не содержит никаких значений или свойств объекта. Он будет содержать адрес (ссылку) на объект, и, используя эту ссылку, мы можем работать с объектом.

Этот пример ясно показывает, что класс является планом для своих объектов, а объект - реализацией своего класса.

Зачем нам нужны объекты в наших программах?

Потому что мы всегда абстрагируемся, чтобы сосредоточиться на бизнес-логике и мышлении с более высокой точки зрения. Например, все мы знаем, что компьютер понимает только 0 и 1. Пишем ли мы код в 0 и 1 или мы общаемся с компьютерами, используя 0 и 1? Конечно, нет, у нас есть много абстракций поверх основной двоичной системы. То же самое и с предметами. В Java у нас есть восемь основных типов данных:

byte       for whole numbers
short      for whole numbers
int        for whole numbers
long       for whole numbers
float      for floating numbers
double     for floating numbers
char       for single character(ACII table)
boolean    logical - can be true or false

Все в Java на их основе. Вы можете видеть, что с помощью примитивов мы можем представлять числа, отдельные символы и логические значения. Это уже хорошо, но недостаточно, и вот на картинке появляются предметы. Мы можем абстрагироваться от примитивов и создавать нужные нам типы данных.

Допустим, мы пишем программное обеспечение для управления сотрудниками. Поэтому нам нужно будет представлять сотрудников в нашей программе. Как бы мы это сделали? Да, мы можем создать класс Employee, который будет представлять сотрудников, и каждый объект будет представлять каждого сотрудника, с которым мы будем работать. Хорошо, давайте сделаем шаг назад и ответим на этот вопрос - что мы будем использовать для хранения слов и предложений в нашей программе? Конечно, String. String - это объект, основанный на char массива, и он уже предоставлен для использования с основными библиотеками Java. Нам обязательно нужны предметы.

Последнее, что я хочу упомянуть, - это статические свойства и методы в классе. Статические свойства принадлежат самому классу, а не конкретному объекту / экземпляру (даже если конкретный объект имеет доступ к статическим свойствам и методам). Правильный способ использования статических членов - по имени класса. Классы, которые состоят из статических методов, обычно являются вспомогательными классами, и они не служат типом данных, и мы не создаем из них объект (например, java.util.Arrays и java.util.Math). Существует множество примеров гибридных классов, которые имеют вместе статические члены и члены экземпляра.

Объектно-ориентированное программирование на Java

В Java есть 4 основных концепции ООП:

  1. Инкапсуляция
  2. Наследование
  3. Абстракция
  4. Полиморфизм

Инкапсуляция - это механизм сокрытия или защиты данных. Способ, которым мы достигаем инкапсуляции, удаляя прямой доступ к свойствам, делая их частными. Мы предоставляем общедоступные методы (обычно геттеры и сеттеры) для чтения и установки значений свойств объекта.

public class Person {
   private String name;
   private int age;
   public String getName() {
      return name;
   }
   public void setName(String name) {
      this.name = name;
   }
   public int getAge() {
      return age;
   }
   public void setAge(int age) {
      if (age < 0) {
       throw new IllegalArgumentException("age cannot be negative");
      }
      this.age = age;
   }
}
  1. Зачем нужна инкапсуляция? Если свойство имеет открытый доступ, клиентский код может иметь прямой доступ и может назначать любое значение. Благодаря инкапсуляции у нас есть один уровень, на котором мы можем контролировать то, что приходит к нашему свойству в нашем методе установки. В приведенном выше примере мы видим, как мы ограничиваем отрицательный возраст в методе setAge. Другой пример. Допустим, мы создаем настраиваемую структуру данных List на основе массива. Подчеркивающая структура данных массива должна быть частной, если мы сделаем ее общедоступной, она будет доступна для клиентского кода. Есть вероятность, что клиентский код создаст большой беспорядок, манипулируя напрямую с массивом.
  2. От кого мы защищаем наши данные? Кажется, очень простой вопрос, однако понимание этого вопроса - основная часть для начала разработки с использованием концепции инкапсуляции. Мы удаляем прямой доступ к свойствам объекта, поэтому мы защищаем его от клиентского кода, который будет использовать этот класс (объект). Мы скрываем свойства от самих себя, если будем использовать этот класс в других частях проекта.
  3. Если тип свойства объекта - изменяемый объект. Мы не можем вернуть исходный адрес, потому что клиент будет осуществлять прямой доступ, используя возвращенную ссылку. Нам всегда нужно взять копию и вернуть ссылку на нее.
  4. Несмотря на то, что вы можете создавать свои методы установки и получения с любым именем или если требование их наличия не требуется, их можно полностью избежать. Но имейте в виду, что если вы хотите использовать свои объекты с внешними библиотеками, они могут предположить, что у вас есть все сеттеры и геттеры с правильными именами.

Наследование - это процесс, при котором один класс может наследовать видимые свойства и методы от другого класса - отношения родитель-потомок между двумя классами (или суперклассом и подклассом).

// in Person.java file
public class Person {
   public String name;
   public String address;
   public int age;
   
   public void walk() {
      System.out.println(name + " is walking.");
   }
}
// in Student.java file
public class Student extends Person {
   public static void main(String[] args){
      Student student = new Student();
      student.name = "John Doe";
      student.address = "101 Main St";
      student.age = 22;
      student.walk();
   }
}

В приведенном выше примере класс Student расширяет класс Person, поэтому наш класс Student является дочерним классом класса Person. Класс ученика расширит все видимые (в зависимости от модификаторов доступа переменных и методов) переменные и методы.

Наследование полезно для повторного использования кода, например, у нас может быть один общий класс, который будет иметь общие свойства и методы с поведением по умолчанию, а дочерние классы могут просто расширять его и повторно использовать большой объем кода. Если дочерний класс хочет иметь собственную реализацию для методов, определенных в родительском классе, мы всегда можем переопределить эти методы в дочернем классе.

Хорошим примером является класс java.lang.Object. Класс объекта - это родительский класс для всех классов в java. Java автоматически вводит синтаксис extends Object после каждого объявления класса. Почему каждый класс должен расширяться от класса super java.lang.Object? Итак, из каждого класса в java мы потенциально можем создать объект. Это может быть Person, это может быть Student, или это может быть Car и так далее, и если мы подумаем об этих классах, все они являются объектами. Java хочет дать общее поведение для каждого объекта, который когда-либо будет создан в java. Java.lang.Object имеет 11 методов (Java 8), поэтому каждый класс наследует эти методы.

equals(Object obj)

Это один из методов, которые будут исходить от класса Object. Нам нужен метод equals для сравнения двух объектов одного класса на равенство. Итак, наш суперкласс предоставляет для этого метод равных. По умолчанию он не будет сравнивать два свойства объекта, он будет сравнивать, если две ссылки указывают на один и тот же объект или нет (то же самое, что и ==). Нам нужно переопределить метод equals и написать логику того, как именно мы хотим сравнивать наши объекты.

Хорошо иметь несколько общих методов для всех объектов, потому что другие библиотеки могут предполагать, что для сравнения ваших объектов они могут использовать метод equals. Такая же логика и для других методов.

Мы говорим, что каждый класс расширяет Object, но в этом примере наш класс Student расширяет наш класс Person, а не класс Object. Да, класс Student расширит класс Object с помощью класса Person.

Java допускает только один тип наследования. Несколько классов могут наследовать от одного класса, но один класс не может наследовать несколько классов одновременно.

Абстракция позволяет нам сосредоточиться на том, что делает объект, а не на том, как он это делает. Абстракция достигается абстрактными методами. В java абстрактные методы могут быть созданы в абстрактных классах и интерфейсах. Давайте обсудим оба варианта и посмотрим, когда нам понадобится абстракция.

Абстрактный класс - это класс в java, который может иметь абстрактные методы. Мы не можем напрямую создавать объекты из абстрактного класса. Абстрактный класс станет полезным только тогда, когда у него будут классы реализации (конкретные). Конкретный класс - это неабстрактный класс, который расширяет абстрактный класс и реализует все его абстрактные методы.

// In FileService.java file
public abstract class FileService {
   public abstract void saveFile(String source, String target);
   public abstract String getFileContent(String source);
   public abstract void copyFile(String source, String target);
   public abstract boolean deleteFile(String path);
}
// In FileServiceLocalImpl.java
public class FileServiceLocalImpl extends FileService {
   @Override
   public void saveFile(String source, String target){
      // code that will save file into file system
   }
   
   @Override
   public String getFileContent(String source){
      // code that will get file content
   }
   @Override
   public void copyFile(String source, String target){
      // code that will copy file
   }
   @Override
   public boolean deleteFile(String path){
      // code that will delete file
   }
}

В приведенном выше примере у нас есть абстрактный класс FileService, который имеет абстрактные методы. У абстрактных методов нет тел, потому что они абстрактны. Действительно, он не будет компилироваться, если вы добавите тело для абстрактного метода. У нас есть неабстрактный класс FileServiceLocalImpl, который расширяет абстрактный класс. Когда неабстрактный класс расширяет абстрактный класс, неабстрактный класс подписывает контракт с абстрактным классом - неабстрактный, расширяющий абстрактный класс, должен реализовывать все его абстрактные методы. Мы реализуем абстрактные методы, переопределяя их и предоставляя тело.

public class Main{
   public static void main(String[] args) {
      // FileService fService = new FileService();
      // It will not compile because we cannot create object
      // from abstract class directly
      
      FileService fService = new FileServiceLocalImpl();
      // ...
      // code that use fService do resolve some problem
   }
}

Итак, мы создаем объект для FileService, например

FileService fService = new FileServiceLocalImpl();

Это полиморфный способ создания объекта. Левая часть будет определять, какие методы и свойства доступны, а правая часть - это реальный объект. Если правая сторона переопределена, некоторые методы во время работы программы будут выполнять переопределенные методы из FileServiceLocalImpl в нашем случае.

Так зачем нам дополнительная работа и создание всех этих абстрактных методов, а затем создание другого класса для их реализации?

Нам необходимо разработать наше программное обеспечение, которое будет относительно легко изменять. Например, предположим, что для хранения файлов мы используем локальную файловую систему на нашем сервере, где работает наше программное обеспечение, а затем мы решили переключиться на использование корзины S3 для хранения всех наших файлов и управления ими. Поэтому нам всегда нужно быть готовыми к такого рода изменениям, и если мы будем проектировать наше программное обеспечение, исходя в первую очередь с использованием концепции абстракции, это сэкономит нам много работы. У нас может быть абстрактный класс или интерфейс, который будет предопределять все манипуляции с файлами с помощью абстрактных методов, и у нас может быть один класс реализации для локальной файловой системы. Мы будем использовать этот абстрактный класс для всех манипуляций с проектом. И когда придет время изменить локальную файловую систему на ведро S3, мы сможем создать один конкретный класс для манипуляций с ведром S3. И нам просто нужно будет заменить часть реализации при создании объекта для FileService. Код бизнес-логики, использующий файловую систему, изменять не нужно. В этом настоящая сила абстракции.

Код с бизнес-логикой должен работать с компонентом через Интерфейс. В будущем, когда мы решим заменить компонент, нам не нужно будет беспокоиться о коде части бизнес-логики, мы можем просто создать другую реализацию на основе интерфейса.

Интерфейс - это тип данных в Java, который может иметь абстрактные методы. Я не хочу углубляться в синтаксические различия между абстрактным классом и интерфейсом. Интерфейс также является механизмом для достижения абстракции, главное отличие в том, что абстрактный класс может также иметь неабстрактные методы и взаимодействовать только с абстрактными методами (кроме статических и стандартных). Мы можем реализовать несколько интерфейсов одновременно, но мы можем расширить только один абстрактный класс.

Полиморфизм - это способность объекта принимать различные формы. Полиморфизм работает вместе с наследованием и абстракцией.

FileService fService = new FileServiceLocalImpl();

и, например, если у нас есть другой конкретный класс FileService, мы могли бы сделать

FileService fService = new FileServiceS3Impl();

FileService может быть FileServiceLocalImpl и FileServiceS3Impl, поэтому полиморфизм - это способность объекта принимать множество форм.

Это все, что касается ООП в java. Спасибо!

This article is part of the series of articles to learn Java programming language from Tech Lead Academy:
1. Introduction to programming 
2. OS, File, and File System
3. Working with terminal 
4. Welcome to Java Programming Language
5. Variables and Primitives in Java
6. Methods with Java
7. Java Math Operators and special operators
8. Conditional branching in Java
9. Switch statement in Java
10. Ternary operator in Java
11. Enum in Java
12. String class and its methods in Java
13. Loops in Java
14. Access modifiers in Java
15. Static keyword in Java
16. The final keyword in Java
17. Class and Object in Java
18. Object Oriented Programming in Java
19. OOP: Encapsulation in Java
20. Inheritance in Java
21. Abstraction in Java
22. Polymorphism in Java
23. Overriding vs Overloading in Java
24. OOP Design Principles in Java
25. Array in Java
26. Data Structures with Java
27. Collection framework in Java
28. ArrayList in Java
29. Set in Java
30. Map in Java
31. LocalDate in Java
32. Exception in Java
33. IO in Java
34. Design Patterns
35. Generics in Java
36. Multithreading in java
37. JUnit
38. Big O Notation for coding interviews
39. Top 17 Java coding interview questions for SDET