Java создание класса наследника
Привет всем читающим 🙂 value это приватное статичное поле класса String, ей присвоено значение объекта String. String text = «123»; String text2 = «456»; text.equals(text2); // «123» это value
Про реализацию метода equals в классе String: Для пояснений можно взять следующий код: (можно в IDE его скопировать, чтоб посмотреть вывод)
String first = "привет"; String linkToFirst = first; String second = new String ("привет"); System.out.println(first.equals(linkToFirst)); System.out.println(first.equals(second));
Первый System.out.println выведет true. И дойдет проверка только до первого if. Мы вызываем у объекта «first«, типа String, метод equals, передавая в качестве параметра другую строку «linkToFirst«, которая, как мы видим, просто ссылается на тот же самый объект. Оператор «this» ссылается на объект, который вызвал этот метод. В нашем случае — это «first» вызвал метод «equals«, и в качестве передаваемого параметра отправил туда second. Когда мы написали в коде this, то подразумевали — «first«. Т.е. Это можно прочитать как:
public boolean equals(Object anObject) < if (first == second)
Так почему же мы попадем только в первый if? Потому что при использовании оператора == при сравнении объектов, сравниваются не сами объекты, а только ссылки на них. А у нас как раз такая история - мы ссылаемся на один и тот же объект, получаем return true, и вывод в консоль true.
public boolean equals(Object anObject)
Второй System.out.println выведет также true, но проверка уже пойдет по всему методу, ибо первый if выдаст false. Ведь second не ссылается на first привет, а ссылается на новый объект
if (anObject instanceof String) < //Проверяем, является ли строкой, // переданный в метод объект (second) String anotherString = (String) anObject; //если да, то приводим // переданный объект к типу String int n = value.length; // value - это массив символов*, объекта String, // который вызывает метод equals. // value.length - его длина. *- на самом деле это массив байтов, и каждый // символ может быть представлен в виде 1, 2, 3 или даже 4 байтами, // но сути это не меняет if (n == anotherString.value.length) < // сравниваем длину массива // символов текущего объекта, с переданным т.е. длину first и second) char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) < // далее мы в цикле уменьшаем количество // заходов в while и поочередно сравниваем между собой элементы (символы) // массивов исходного "first " и переданного // "second" (берется один и тот же индекс массива - "i"). // Как только они не совпадут - false и выход из цикла. // Если цикл завершился без преждевременного окончания, // значит объекты идентичны. if (v1[i] != v2[i]) return false; i++; > return true; > > return false;
Java создание класса наследника
Одним из ключевых аспектов объектно-ориентированного программирования является наследование. С помощью наследования можно расширить функционал уже имеющихся классов за счет добавления нового функционала или изменения старого. Например, имеется следующий класс Person, описывающий отдельного человека:
class Person < String name; public String getName()< return name; >public Person(String name) < this.name=name; >public void display() < System.out.println("Name: " + name); >>
И, возможно, впоследствии мы захотим добавить еще один класс, который описывает сотрудника предприятия - класс Employee. Так как этот класс реализует тот же функционал, что и класс Person, поскольку сотрудник - это также и человек, то было бы рационально сделать класс Employee производным (наследником, подклассом) от класса Person, который, в свою очередь, называется базовым классом, родителем или суперклассом:
class Employee extends Person < public Employee(String name)< super(name); // если базовый класс определяет конструктор // то производный класс должен его вызвать >>
Чтобы объявить один класс наследником от другого, надо использовать после имени класса-наследника ключевое слово extends , после которого идет имя базового класса. Для класса Employee базовым является Person, и поэтому класс Employee наследует все те же поля и методы, которые есть в классе Person.
Если в базовом классе определены конструкторы, то в конструкторе производного классы необходимо вызвать один из конструкторов базового класса с помощью ключевого слова super . Например, класс Person имеет конструктор, который принимает один параметр. Поэтому в классе Employee в конструкторе нужно вызвать конструктор класса Person. То есть вызов super(name) будет представлять вызов конструктора класса Person.
При вызове конструктора после слова super в скобках идет перечисление передаваемых аргументов. При этом вызов конструктора базового класса должен идти в самом начале в конструкторе производного класса. Таким образом, установка имени сотрудника делегируется конструктору базового класса.
Причем даже если производный класс никакой другой работы не производит в конструкторе, как в примере выше, все равно необходимо вызвать конструктор базового класса.
public class Program < public static void main(String[] args) < Person tom = new Person("Tom"); tom.display(); Employee sam = new Employee("Sam"); sam.display(); >> class Person < String name; public String getName()< return name; >public Person(String name) < this.name=name; >public void display() < System.out.println("Name: " + name); >> class Employee extends Person < public Employee(String name)< super(name); // если базовый класс определяет конструктор // то производный класс должен его вызвать >>
Производный класс имеет доступ ко всем методам и полям базового класса (даже если базовый класс находится в другом пакете) кроме тех, которые определены с модификатором private . При этом производный класс также может добавлять свои поля и методы:
public class Program < public static void main(String[] args) < Employee sam = new Employee("Sam", "Microsoft"); sam.display(); // Sam sam.work(); // Sam works in Microsoft >> class Person < String name; public String getName()< return name; >public Person(String name) < this.name=name; >public void display() < System.out.println("Name: " + name); >> class Employee extends Person < String company; public Employee(String name, String company) < super(name); this.company=company; >public void work() < System.out.printf("%s works in %s \n", getName(), company); >>
В данном случае класс Employee добавляет поле company, которое хранит место работы сотрудника, а также метод work.
Переопределение методов
Производный класс может определять свои методы, а может переопределять методы, которые унаследованы от базового класса. Например, переопределим в классе Employee метод display:
public class Program < public static void main(String[] args) < Employee sam = new Employee("Sam", "Microsoft"); sam.display(); // Sam // Works in Microsoft >> class Person < String name; public String getName()< return name; >public Person(String name) < this.name=name; >public void display() < System.out.println("Name: " + name); >> class Employee extends Person < String company; public Employee(String name, String company) < super(name); this.company=company; >@Override public void display() < System.out.printf("Name: %s \n", getName()); System.out.printf("Works in %s \n", company); >>
Перед переопределяемым методом указывается аннотация @Override . Данная аннотация в принципе необязательна.
При переопределении метода он должен иметь уровень доступа не меньше, чем уровень доступа в базовом класса. Например, если в базовом классе метод имеет модификатор public, то и в производном классе метод должен иметь модификатор public.
Однако в данном случае мы видим, что часть метода display в Employee повторяет действия из метода display базового класса. Поэтому мы можем сократить класс Employee:
class Employee extends Person < String company; public Employee(String name, String company) < super(name); this.company=company; >@Override public void display() < super.display(); System.out.printf("Works in %s \n", company); >>
С помощью ключевого слова super мы также можем обратиться к реализации методов базового класса.
Запрет наследования
Хотя наследование очень интересный и эффективный механизм, но в некоторых ситуациях его применение может быть нежелательным. И в этом случае можно запретить наследование с помощью ключевого слова final . Например:
public final class Person
Если бы класс Person был бы определен таким образом, то следующий код был бы ошибочным и не сработал, так как мы тем самым запретили наследование:
class Employee extends Person
Кроме запрета наследования можно также запретить переопределение отдельных методов. Например, в примере выше переопределен метод display() , запретим его переопределение:
В этом случае класс Employee не сможет переопределить метод display.
Динамическая диспетчеризация методов
Наследование и возможность переопределения методов открывают нам большие возможности. Прежде всего мы можем передать переменной суперкласса ссылку на объект подкласса:
Person sam = new Employee("Sam", "Oracle");
Так как Employee наследуется от Person, то объект Employee является в то же время и объектом Person. Грубо говоря, любой работник предприятия одновременно является человеком.
Однако несмотря на то, что переменная представляет объект Person, виртуальная машина видит, что в реальности она указывает на объект Employee. Поэтому при вызове методов у этого объекта будет вызываться та версия метода, которая определена в классе Employee, а не в Person. Например:
public class Program < public static void main(String[] args) < Person tom = new Person("Tom"); tom.display(); Person sam = new Employee("Sam", "Oracle"); sam.display(); >> class Person < String name; public String getName() < return name; >public Person(String name) < this.name=name; >public void display() < System.out.printf("Person %s \n", name); >> class Employee extends Person < String company; public Employee(String name, String company) < super(name); this.company = company; >@Override public void display() < System.out.printf("Employee %s works in %s \n", super.getName(), company); >>
Консольный вывод данной программы:
Person Tom Employee Sam works in Oracle
При вызове переопределенного метода виртуальная машина динамически находит и вызывает именно ту версию метода, которая определена в подклассе. Данный процесс еще называется dynamic method lookup или динамический поиск метода или динамическая диспетчеризация методов.
Java создание класса наследника
Исходя из схемы выше, реализуем 4 класса: Pixel , Snake , Food , Bonus .
Получаем дилемму: если объект класса Enemy вызывает метод canBeEaten () , определенный в классе Pixel и при этом не переопределенный в классе Enemy , а классы Snake и Food определили этот метод каждый по-своему. Возникает вопрос: от какого класса должен наследовать свое поведение экземпляр класса Enemy : Snake или Food ? Такая конфигурация наследования называется «проблемой множественного наследования».
В Java для решения этой проблемы применяются интерфейсы.
Что такое интерфейс?
Интерфейс – это объект языка Java схожий по своей сути с абстрактным классом. Для определения интерфейса в языке есть отдельное ключевое слово interface.
Интерфейс не обязан иметь в себе сигнатуры методов. Пустые интерфейсы называются интерфейсами-маркерами. Классическим примером интерфейса-маркера можно считать Cloneable из пакета java.lang .
Отличия абстрактного класса от интерфейса
Если интерфейс так похож на абстрактный класс, зачем тогда в языке присутствуют обе эти конструкции? Для этого есть несколько причин:
- Идеологическая. В парадигме ООП все классы – существительные. Абстрактный класс не исключение. Следовательно, он описывает общие свойства объектов. Интерфейс же — это контракт. Он описывает общие действия доступные всем классам, реализующим его. Интерфейс не располагает реализацией – он лишь гарантирует наличие действия у объекта. Именно поэтому в некоторых нотациях принято именовать интерфейсы как Noun-able.
- Практическая. Класс может расширять только один класс и имплементировать множество интерфейсов. С помощью интерфейса можно решить проблему множественного наследования.
Пример интерфейса
Создадим интерфейс, определяющий возможность перемещаться, и имплементируем его:
Проведем рефакторинг в соответствии со схемой. Вынесем из абстрактного класса метод canBeEaten () в отдельный интерфейс и имплементируем его в абстрактном классе. Таким образом, мы реализовали контракт о том, что все классы дочерние от абстрактного обязаны у себя реализовать метод canBeEaten () что решает проблему выбора родителя для неопределенного метода и, как следствие, саму проблему ромба.
interface Eatable < boolean canBeEaten(); >abstract class Pixel implements Eatable < >
Добавим также классу Snake интерфейс-маркер, чтобы отличать «живые» объекты. Например, змеек и, возможно, других персонажей от еды.
interface Alivable<> class Snake extends Pixel implements Movable, Alivable < >
Таким образом, видно, что класс может расширять только один другой класс, но имплементировать множество интерфейсов.
Ссылки
Тема данной статьи достаточно обширна, и в ней остался не раскрыт ряд вопросов: применение интерфейсов маркеров или примесей, однако я надеюсь, что мне удалось осветить основные аспекты наследования в Java.
Подведем итог. В этой статье мы:
- Научились создавать интерфейсы, абстрактные классы и методы.
- Поняли, чем отличается абстрактный класс от интерфейса.
- Познакомились с проблемой множественного наследования, а также методами ее решения.