Привет! Ты когда-нибудь задумывался, почему Java устроена именно так, как она устроена? В том смысле, что ты создаешь классы, на их основе — объекты, у классов есть методы и т.д.
Но почему структура языка такова, что программы состоят именно из классов и объектов, а не из чего-то другого? Зачем было придумано понятие «объект» и поставлено во главу угла? Все ли языки устроены так и, если нет, какие преимущества это дает Java? Вопросов, как видишь, много :) Попробуем ответить на каждый из них в сегодняшней лекции.

Что такое объектно-ориентированное программирование (ООП)

Конечно, Java не просто так состоит из объектов и классов. Это не прихоть ее создателей, и даже не их изобретение. Есть множество других языков, в основе которых лежат объекты. Первый такой язык назывался Simula, и его изобрели еще в 1960-х годах в Норвегии. Помимо всего прочего, в Simula появились понятия «класс» и «метод».
Кристен Нюгор и Оле Йохан Даль - создатели Simula
Казалось бы, Simula — древний язык по меркам программирования, но их «родственную» связь с Java видно невооруженным глазом. Скорее всего, ты легко прочтешь написанный на нем код и в общих чертах объяснишь, что он делает :)
Begin
	Class Rectangle (Width, Height); Real Width, Height;

	 Begin
	    Real Area, Perimeter;

	    Procedure Update;
	    Begin
	      Area := Width * Height;
              OutText("Rectangle is updating, Area = "); OutFix(Area,2,8); OutImage;
	      Perimeter := 2*(Width + Height);
              OutText("Rectangle is updating, Perimeter = "); OutFix(Perimeter,2,8); OutImage;
	    End of Update;

	    Update;
	    OutText("Rectangle created: "); OutFix(Width,2,6);
	    OutFix(Height,2,6); OutImage;
	 End of Rectangle;

       Rectangle Class ColouredRectangle (Color); Text Color;

	Begin
	    OutText("ColouredRectangle created, color = "); OutText(Color);
	    OutImage;
        End of ColouredRectangle;


      	 Ref(Rectangle) Cr;
	 Cr :- New ColouredRectangle(10, 20, "Green");
End;
Пример кода взят из статьи Simula — 50 лет ООП. Как видишь, Java и его предок не так уж сильно отличаются друг от друга :) Это связано с тем, что появление Simula ознаменовало собой рождение новой концепции — объектно-ориентированного программирования. Википедия дает такое определение ООП: Объе́ктно-ориенти́рованное программи́рование (ООП) — методология программирования, основанная на представлении программы в виде совокупности объектов, каждый из которых является экземпляром определенного класса, а классы образуют иерархию наследования. Оно, на мой взгляд, очень удачное. Ты недавно начал изучать Java, но в нем вряд ли найдутся незнакомые тебе слова:) Сегодня ООП — самая распространенная методология программирования. Помимо Java принципы ООП используются во многих популярных языках, о которых ты, возможно, слышал. Это C++ (его активно применяют разработчики компьютерных игр), Objective-C и Swift (на них пишут программы для устройств Apple), Python (наиболее востребован в машинном обучении), PHP (один из самых популярных языков веб-разработки), JavaScript (проще сказать, чего на нем не делают) и многие другие. Собственно говоря, что это за «принципы» ООП? Расскажем подробнее.

Принципы ООП

Это основа основ. 4 главные особенности, которые вместе образуют парадигму объектно-ориентированного программирования. Их понимание — ключ к становлению успешного программиста.

Принцип 1. Наследование.

Хорошая новость: с некоторыми из принципов ООП ты уже знаком! :) Наследование нам уже пару раз встречалось в лекциях, и мы успели с ним поработать. Наследование — механизм, который позволяет описать новый класс на основе существующего (родительского). При этом свойства и функциональность родительского класса заимствуются новым классом. Для чего нужно наследование и какие преимущества оно дает? Прежде всего — повторное использование кода. Поля и методы, описанные в родительских классах, можно использовать в классах-потомках. Если у всех типов автомобилей есть 10 общих полей и 5 одинаковых методов, тебе достаточно вынести их в родительский класс Auto. Ты сможешь использовать их в классах-потомках безо всяких проблем. Сплошные плюсы: и количественно (меньше кода), и, как следствие, качественно (классы становятся гораздо проще). При этом механизм наследования очень гибкий, и недостающую в потомках функциональность ты можешь дописать отдельно (какие-то специфические для конкретного класса поля или поведение). В общем, как и в обычной жизни: все мы чем-то похожи на наших родителей, а чем-то отличаемся от них :)

Принцип 2. Абстракция

Это очень простой принцип. Абстракция означает выделение главных, наиболее значимых характеристик предмета и наоборот — отбрасывание второстепенных, незначительных. Не будем изобретать велосипед и вспомним пример из старой лекции про классы. Скажем, мы создаем картотеку работников компании. Для создания объектов «работник» мы написали класс Employee. Какие характеристики важны для их описания в картотеке компании? ФИО, дата рождения, номер социального страхования, ИНН. Но вряд ли в карточке такого типа нам нужны его рост, цвет глаз и волос. Компании эта информация о сотруднике ни к чему. Поэтому для класса Employee мы зададим переменные String name, int age, int socialInsuranceNumber и int taxNumber, а от лишней для нас информации вроде цвета глаз откажемся, абстрагируемся. А вот если мы создаем картотеку фотомоделей для агентства, ситуация резко меняется. Для описания фотомодели нам как раз очень важны рост, цвет глаз и цвет волос, а номер ИНН не нужен. Поэтому в классе Model мы создаем переменные String height, String hair, String eyes.

Принцип 3. Инкапсуляция

С ним мы уже сталкивались. Инкапсуляция в Java означает ограничение доступа к данным и возможностям их изменения. Как видишь, в его основе лежит слово «капсула». В эту «капсулу» мы прячем какие-то важные для нас данные, которые не хотим, чтобы кто-то менял. Простой пример из жизни. У тебя есть имя и фамилия. Их знают все твои знакомые. Но у них нет доступа к изменению твоего имени и фамилии. Этот процесс, можно сказать, «инкапсулирован» в паспортном столе: поменять имя фамилию можно только там, и сделать это можешь только ты. Остальные «пользователи» имеют доступ к твоему имени и фамилии «только на чтение» :) Еще один пример — деньги в твоей квартире. Оставлять их на виду посреди комнаты — не лучшая идея. Любой «пользователь» (человек, пришедший к тебе домой) сможет изменить число твоих денег, т.е. забрать их. Лучше инкапсулировать их в сейфе. Доступ будет только у тебя и только по специальному коду. Очевидные примеры инкапсуляции, с которыми ты уже работал, — это модификаторы доступа (private, public и т.д.), а также геттеры-сеттеры. Если поле age у класса Cat не инкапсулировать, кто угодно сможет написать:
Cat.age = -1000;
А механизм инкапсуляции позволяет нам защитить поле age при помощи метода-сеттера, в который мы можем поместить проверку того, что возраст не может быть отрицательным числом.

Принцип 4. Полиморфизм

Полиморфизм — это возможность работать с несколькими типами так, будто это один и тот же тип. При этом поведение объектов будет разным в зависимости от типа, к которому они принадлежат. Звучит сложновато? Сейчас разберемся. Возьмем самый простой пример — животных. Создадим класс Animal с единственным методом — voice(), и двух его наследников — Cat и Dog.
public class Animal {

   public void voice() {

       System.out.println("Голос!");
   }
}

public class Dog extends Animal {


   @Override
   public void voice() {
       System.out.println("Гав-гав!");
   }
}

public class Cat extends Animal {

   @Override
   public void voice() {
       System.out.println("Мяу!");
   }
}
Теперь попробуем создать ссылку Animal и присвоить ей объект Dog.
public class Main {

   public static void main(String[] args) {

       Animal dog = new Dog();
       dog.voice();
   }
}
Как ты думаешь, какой метод будет вызван? Animal.voice() или Dog.voice()? Будет вызван метод класса Dog: Гав-гав! Мы создали ссылку Animal, но объект ведет себя как Dog. При необходимости он может вести себя как кошка, лошадь или другое животное. Главное — присвоить ссылке общего типа Animal объект конкретного класса-наследника. Это логично, ведь все собаки являются животными. Именно это мы имели в виду, когда говорили «поведение объектов будет разным, в зависимости от того, к какому типу они принадлежат». Если бы мы создали объект Cat
public static void main(String[] args) {

   Animal cat = new Cat();
   cat.voice();
}
метод voice() вывел бы «Мяу!». А что значит «возможность работать с несколькими типами так, как будто это один и тот же тип»? Это тоже довольно легко. Давайте представим, что мы создаем парикмахерскую для животных. В нашей парикмахерской должны уметь стричь всех животных, поэтому мы создадим метод shear() («постричь») с параметром Animal — животным, которое мы будем стричь.
public class AnimalBarbershop {

   public void shear(Animal animal) {

       System.out.println("Стрижка готова!");
   }
}
И теперь мы можем передавать в метод shear и объекты Cat, и объекты Dog!
public static void main(String[] args) {

   Cat cat = new Cat();
   Dog dog = new Dog();

   AnimalBarbershop barbershop = new AnimalBarbershop();

   barbershop.shear(cat);
   barbershop.shear(dog);
}
Вот и наглядный пример: класс AnimalBarbershop работает с типами Cat и Dog так, как будто это один и тот же тип. При этом у Cat и Dog разное поведение: они по-разному подают голос.

Причины появления ООП

Почему вообще возникла эта новая концепция программирования — ООП? У программистов были работающие инструменты: например, процедурные языки. Что же побудило их изобретать что-то принципиально новое? Прежде всего, усложнение задач, которые перед ними стояли. Если 60 лет назад задача программиста выглядела как «вычислить математическое уравнение такое-то», сейчас она может звучать как «реализовать 7 различных концовок для игры S.T.A.L.K.E.R. в зависимости от того, какие решения принимал пользователь в игровых моментах A, B, C , D, E, F и комбинаций этих решений». Задачи, как видишь, за прошедшие десятилетия явно усложнились. А значит, усложнились и типы данных. Это еще одна причина возникновения ООП. Пример с уравнением легко можно решить с помощью обычных примитивов, никаких объектов тут не надо. А вот задачу с концовками игры сложно будет даже описать, не используя каких-то придуманных тобой же классов. Но при этом описать ее в классах и объектах достаточно легко: нам явно будет нужен класс Игра, класс Сталкер, класс Концовка, класс РешениеИгрока, класс ИгровойМомент и так далее. То есть даже не приступив к решению задачи, мы в голове можем легко представить «наброски» ее решения. Усложнение задач поставило программистов перед необходимостью делить задачу на части. Но в процедурном программировании сделать это было не так просто. И очень часто программа представляла собой «дерево» из кучи веток со всеми возможными вариантами ее работы. В зависимости от каких-то условий, программа выполнялась по той или иной ветке. Для небольших программ такой вариант был удобен, но поделить на части объемную задачу было очень сложно. Эта необходимость стала еще одной причиной возникновения ООП. Эта концепция предоставила программистам возможность делить программу на кучу «модулей»-классов, каждый из которых делает свою часть работы. Все объекты, взаимодействуя между собой, образуют работу нашей программы. Кроме того, написанный нами код можно повторно использовать в другом месте программы, что также экономит большое количество времени.