Тема 6. Введение в ООП - BelyiZ/JavaCourses GitHub Wiki

Содержание:

  1. Основные понятия ООП
    1. Класс
    2. Конструктор
  2. Инкапсуляция, наследование, полиморфизм
    1. Инкапсуляция
    2. Наследование
    3. Полиморфизм
      1. Переопределение
      2. Перегрузка
      3. Параметрический полиморфизм
  3. Список литературы/курсов

Как вы уже заметили, все, кроме примитивов, в Java является объектом. Java - объектно-ориентированный язык, и следует принципам объектно-ориентированного программирования, если сокращенно - ООП.

Основные понятия ООП

Основным понятием в ООП является объект. Все языки, реализующие парадигму ООП, так или иначе оперируют ими.

В ОО языках иногда может не быть классов, как в javascript, наследования (GoLang), интерфейсов (C++), но объекты будут обязательно.

Представим себе, например, игру, в которой пользователь собирает футбольные команды, назначает игроков и тренера, и проводит матчи с другими игроками.

Удобно было бы представить абстракции команды, игрока, тренера. Дать возможность пользователю назначать игроков в команду и убирать из нее, хранить информацию о результативности тренера и игроков. Конечно, у игроков и тренера должны быть имена, а команда должна иметь полевой состав из 11 игроков, среди которых будут нападающие, вратарь и защитники.

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

Тренер и игроки являются людьми, а значит имеют имена. В терминах Java это значит, что мы можем описать их классом, который назовём Person, у которого будут поля name и surname. Значение этих полей будет у тренера одно, а у вратаря другое. Мы будем говорить, что тренер Иван Иванов, как и вратарь Петр Петров это объекты или экземпляры класса Person.

class Person {

    String name;
    String surname;
}

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

class Person {

    String name;
    String surname;

    String toString() {     // возвращает данные объекта в строке
        return "Привет! Я " + this.name + " " + this.surname;
    }
}

Объект может храниться в переменной, как мы уже видели на примере строк (да, они тоже объекты!).

Сравните:

String s = new String();    // s - пустая строка, объект типа String
Person p = new Person();    // p - объект типа Person, пока без имени и фамилии

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

p.name = "Иван";
p.surname = "Иванов";
String s = p.toString();

И мы положили в переменную "s" строку "Привет! Я Иван Иванов".

А теперь представим, как могла бы выглядеть операция добавления игрока в команду:

class Team {

    Player[] players = new Player[20]; // массив игроков
    int playersNumber;
    
    void addPlayer(Player p) { // параметр метода объект типа игрок
        this.players[playersNumber] = p; // переданный игрок добавляется в массив
        playersNumber++; 
    }
}  

Резюмируя:

Объект - это сущность, обладающая данными и определенным набором операций. В Java данные это поля, а операции - это методы.

Могут быть классы и посложнее. Например, почтовый клиент, отправляющий по определенному email указанное письмо может быть использован так:

EmailClient emailClient = new EmailClient(/*Тут все, что нужно при создании объекта.*/); // об этом ниже
emailClient.sendMessage("[email protected]", "Здорово, дед!");

Причем тому, кто будет использовать этот класс, не нужно знать о сложной логике, упрятанной под капотом класса EmailClient. Он будет использовать только удобный интерфейс.

Указал адрес, текст письма, и вуаля, письмо ушло на деревню дедушке. Но о сокрытии данных от любопытных мы поговорим позже.

Обсудим подробнее возможности классов в Java.

Класс

Положим,

String s = "Я строка!";

Про объект s мы знаем, что он принадлежит к типу или классу строк. В Java все объекты относятся к какому-либо классу.

У объектов одного класса определен набор полей и методов. Причем мы сами решим, к каким полям и методам разрешить доступ (public), а к каким ограничить (private). Мы можем определять новые классы, и создавать объекты этих классов.

Рассмотрим небольшой кусочек определения знакомого нам класса String из Java:

package java.lang;                  // <-- Классы расположены в пакетах. 
//     String относится к пакету java.lang 

public final class String {         // <-- Название класса, и модификаторы, 

    private final char[] value;     // <-- Данные, которыми обладает каждый объект этого класса. 

    //     В нашем случае это массив символов, составляющих строку
    public String() {               // <-- Один из конструкторов - методов, создающих String.  
        this.value = new char[0];   //     Этот создает пустую строку.
    }

    public int length() {           // <-- Один из методов класса. 
        //     Например, этот возвращает длину строки.
        return value.length;
    }
}

Группы классов отвечающих за схожую функциональность располагаются в пакетах (package) Структура пакетов совпадает со структурой папок в которых расположены файлы с кодом.

Полное имя класса включает в себя имя пакета, в котором он находится. Например, java.util.ArrayList.

Если в начале файла с кодом (после определения пакета, к которому принадлежит класс) написать import с именем пакета и класса, то он станет доступен по короткому имени. Для объявления переменной этого типа достаточно будет написать тип ArrayList.

Например, мы могли бы упростить работу с игроками в классе футбольной команды, воспользовавшись классом списка ArrayList с которым работать удобней, чем с массивом. Нам не было бы нужно следить за его длиной.

package ru.java.course.football; // пакет, в котором расположен наш класс

import java.util.ArrayList; // хотим использовать динамические списки!

public class Team {  // также известный как ru.java.course.football.Team

    private ArrayList players = new ArrayList(); // Пустой список игроков. И никакого счетчика.

    public void addPlayer(Player p) {   // в параметр p будет передан игрок
        this.players.add(p);            // у списка есть метод добавления
    }

    public int getPlayersNumber() {
        return this.players.size();     // у списка есть метод, возвращающий размер
    }
}

Конструктор

Чтобы создать объект, нужно вызвать один из его конструкторов с помощью ключевого слова new.

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

Объект может содержать несколько конструкторов с разными параметрами. Например, в классе Team могут быть такие конструкторы:

public class Team {

    private String name;

    public Team() {
    }        // Конструктор без параметров. Ленивый.

    public Team(String n) { // Конструктор, заполняющий имя команды. 
        this.name = n;
    }
    /// ... другие методы и поля
}

Чтобы создать команду с именем и поместить в переменную, делаем так:

Team spartak = new Team("Спартак");

Ключевое слово new и имя типа за ним говорят, что мы собираемся создать новый объект указанного типа. Java ищет среди конструкторов подходящий по параметрам, и вызывает его.

Для строк, чисел, логических значений и массивов возможно также создание из литералов, как в случае со строкой "s" выше. Это сделано для упрощения синтаксиса. Например:

String s = "Я строка";
Integer i = 1;
Character c = 'c';
Boolean b = true;
Integer[] arr = {1, 2, 3};

Инкапсуляция, наследование, полиморфизм

ООП стоит на трёх китах (впрочем, некоторые считают, что их четыре, добавляя абстракцию):

  • Инкапсуляция - это ограничение доступа к данным объекта извне
  • Полиморфизм - это реализация одного интерфейса в различных объектах разными способами
  • Наследование - это возможность использовать данные и поведение класса-предка

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

Инкапсуляция

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

  • private - члены класса, помеченные этим модификатором, не будут доступны извне.
  • protected - разрешен доступ внутри класса, в наследниках и в рамках того же пакета.
  • public - общедоступные члены класса.

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

Наследование

Наследование - возможность указать предка при описании класса. При этом все public и protected члены класса-предка автоматически появятся у наследника. Отношения наследования образуют между классами сильнейшую связь. Фактически, это связь is-a или является. Рассмотрим это утверждение на примере:

public class Person {

    protected String name;
}

public class Student extends Person {

    protected int[] marks;
}

Этот код описывает класс "человек", характеризующийся именем. И расширяющий (наследующий) его класс "студент", у которого также есть имя - унаследованное поле, и массив оценок. В настоящей программе наследников у класса может быть множество, и изменение поведения класса Person приведет к изменению поведения всех его наследников. И скорее всего сотне багов в программе. Используйте наследование аккуратно. Простое правило для определения уместности наследования - спросите себя, можно ли про тип-наследник, что он является также и типом-предком? Если да, используйте. Например, каждый студент определенно является человеком.

Все объекты Java являются наследниками типа Object. То есть, если не писать extends, определяя класс, то Java сама втихую допишет extends Object.

Впрочем, методы этого класса весьма полезны. Это equals, который должен сравнивать объекты класса между собой, toString(), возвращающий строковое представление, и многие другие очень важные для работы Java методы.

Менее сильной связью между объектами является композиция. Иначе говоря, один объект может содержать в себе другие в виде полей, как мы видели на примере студента с массивом оценок и команды со списком игроков.

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

Полиморфизм

Полиморфизм - это способность интерфейса иметь несколько конкретных реализаций. В Java реализуется через переопределение и перегрузку методов, generics.

Переопределение

Переопределение, или overriding методов в Java выглядит так. Вышеописанные классы Person и Student могли бы иметь метод toString, который возвращал бы их внутреннее состояние в виде строки.

public class Person {

    protected String name;

    public String toString() {
        return "name: " + this.name;
    }
}

public class Student extends Person {

    protected int[] marks;

    public String toString() {
        String result = "";
        result += "name: " + this.name + "; ";

        result += "marks: ";
        for (int mark : this.marks) {
            result += mark + ", ";
        }
        return result;
    }
}

Метод toString() в классе Person возвращает строку вида name: Vasya. Если мы не определим аналогичный метод в классе Student, то переменные этого типа при вызове метода toString() будут возвращать строку с именем. Если же мы определим в классе Student метод с таким же именем и набором параметров, то при вызове toString() Java найдет нужную реализацию и выведет не только имя, но и список оценок студента через запятую. Более того, мы можем вызвать реализацию метода из предка с помощью ключевого слова super. Метод toString() класса Student мог бы выглядеть так:

public String toString() {
    String result = super.toString();   
    result += "marks: ";
    for (int mark : this.marks) {
        result += mark + ", ";
    }
    return result;
}

Вызов его привел бы к тому же результату.

Перегрузка

Перегрузка, или overloading - это способность Java выбрать правильную реализацию метода из нескольких, имеющих одно имя, на основании количества и типов переданных параметров. Например, у строки есть несколько конструкторов - без параметров, принимающий массив символов, другую строку и еще несколько. Java определяет, какой из них выполнить, на основании конкретного вызова.

String s=new String();     // создаст пустую строку
        String s1=new String('c'); // создаст строку из символа типа char
        String s2=new String(s1);  // создаст копию строки s1, 
// приняв в качестве параметра объект типа String

Перегружен может быть не только конструктор, но и любой метод. Например, мы могли бы перегрузить метод toString в классе Person, чтобы он добавлял переданную строку к информации об имени.

public String toString() {              // первая реализация, без параметров
    return "name: " + this.name;
}
public String toString(String prefix) { // вторая реализация, принимает строку.
    return prefix + " " + this.name;
}

Будучи вызван у переменной p типа Person в программе таким образом: p.toString("Hi, my name is "); этот метод вернет строку Hi, my name is Vasya.

Параметрический полиморфизм (generics)

Об этом понятии пока нам имеет смысл знать, что некоторые типы поддерживают работу с другими в качестве параметра. Например List<Integer> это список содержащий элементы типа Integer. Попытка добавить в такой список строку выдаст ошибку компиляции. Наиболее часто вы будете использовать этот вид полиморфизма в коллекциях.

List<Integer> l = new ArrayList<Integer>(); // создает пустой список на основе массива, 
                                            // в котором могут храниться только Integer

Более подробно мы будем рассматривать эту функциональность в следующих занятиях.

Список литературы/курсов

  1. https://gos-it.fandom.com/wiki/Основные_принципы_ООП:_инкапсуляция,_наследование,_полиморфизм
  2. https://habr.com/ru/post/87119/
  3. https://habr.com/ru/post/87205/
  4. https://javarush.ru/quests/lectures/questcore.level01.lecture01

Тема 5. Работа со строками | Оглавление | Тема 7. Класс Object и его методы

⚠️ **GitHub.com Fallback** ⚠️