Наследование
Наследование является неотъемлемой частью Java. При использовании наследования вы говорите: Этот новый класс похож на тот старый класс. В коде это пишется как extends, после которого указываете имя базового класса. Тем самым вы получаете доступ ко всем полям и методам базового класса. Используя наследование, можно создать общий класс, которые определяет характеристики, общие для набора связанных элементов. Затем вы можете наследоваться от него и создать новый класс, который будет иметь свои уникальные характеристики. Главный наследуемый класс в Java называют суперклассом. Наследующий класс называют подклассом. Получается, что подкласс — это специализированная версия суперкласса, которая наследует все члены суперкласса и добавляет свои собственные уникальные элементы. К примеру, в Android есть класс View и подкласс TextView.
Чтобы наследовать класс, достаточно вставить имя наследуемого класса с использованием ключевого слова extends:
public class MainActivity extends Activity
В этом коде мы наследуемся от класса Activity и добавляем свой код, который будет отвечать за наше приложение.
Подкласс в свою очередь может быть суперклассом другого подкласса. Так например, упоминавший ранее класс TextView является суперклассом для EditText.
В производный класс можно добавлять новые методы.
Для каждого создаваемого подкласса можно указывать только один суперкласс. При этом никакой класс не может быть собственным суперклассом.
Хотя подкласс включает в себя все члены своего суперкласса, он не может получить доступ к тем членам суперкласса, которые объявлены как private.
Помните, мы создавали класс Box для коробки кота. Давайте наследуемся от этого класса и создадим новый класс, который будет иметь не только размеры коробки, но и вес.
В том же файле Box.java после последней закрывающей скобки добавьте новый код:
class HeavyBox extends Box < int weight; // вес коробки // конструктор HeavyBox(int w, int h, int d, int m) < width = w; height = h; depth = d; weight = m; // масса >>
Возвращаемся в главную активность и пишем код:
HeavyBox box = new HeavyBox(15, 10, 20, 5); int vol = box.getVolume(); mInfoTextView.setText("Объём коробки: " + vol + " Вес коробки: " + box.weight);
Обратите внимание, что мы вызываем метод getVolume(), который не прописывали в классе HeavyBox. Однако мы можем его использовать, так как мы наследовались от класса Box и нам доступны все открытые поля и методы. Заодно мы вычисляем вес коробки с помощью новой переменной, которую добавили в подкласс.
Теперь у нас появилась возможность складывать в коробку различные вещи. В хозяйстве всё пригодится.
При желании вы можете создать множество разных классов на основе одного суперкласса. Например, мы можем создать цветную коробку.
class ColorBox extends Box < int color; // цвет коробки // конструктор ColorBox(int w, int h, int d, int c) < width = w; height = h; depth = d; color = c; // цвет >>
Ключевое слово super
В Java существует ключевое слово super, которое обозначает суперкласс, т.е. класс, производным от которого является текущий класс. В данном случае, супер не означает превосходство, скорее даже наоборот, дочерний класс имеет больше методов, чем родительский. Само слово пошло из теории множеств, где используется термин супермножество. Посмотрим, зачем это нужно.
В конструкторе HeavyBox мы дублировали поля width,height и depth, которые уже есть в классе Box. Это не слишком эффективно. Кроме того, возможны ситуации, когда суперкласс имеет закрытые члены данных, но мы хотим иметь к ним доступ. Через наследование это не получится, так как закрытые члены класса доступны только родному классу. В таких случаях вы можете сослаться на суперкласс.
Ключевое слово super можно использовать для вызова конструктора суперкласса и для обращения к члену суперкласса, скрытому членом подкласса.
Использование ключевого слова super для вызова конструктора суперкласса
class HeavyBox extends Box < int weight; // вес коробки // конструктор // инициализируем переменные с помощью ключевого слова super HeavyBox(int w, int h, int d, int m) < super(w, h, d); // вызов конструктора суперкласса weight = m; // масса >>
Вызов метода super() всегда должен быть первым оператором, выполняемым внутри конструктора подкласса.
При вызове метода super() с нужными аргументами, мы фактически вызываем конструктор Box, который инициализирует переменные width, height и depth, используя переданные ему значения соответствующих параметров. Вам остаётся инициализировать только своё добавленное значение weight. При необходимости вы можете сделать теперь переменные класса Box закрытыми. Проставьте у полей класса Box модификатор private и убедитесь, что вы можете обращаться к ним без проблем.
У суперкласса могут быть несколько перегруженных версий конструкторов, поэтому можно вызывать метод super() с разными параметрами. Программа выполнит тот конструктор, который соответствует указанным аргументам.
Вторая форма ключевого слова super действует подобно ключевому слову this, только при этом мы всегда ссылаемся на суперкласс подкласса, в котором она использована. Общая форма имеет следующий вид:
super.член
Здесь член может быть методом либо переменной экземпляра.
Подобная форма подходит в тех случаях, когда имена членов подкласса скрывают члены суперкласса с такими же именами.
class A < int i; >// наследуемся от класса A class B extends A < int i; // имя переменной совпадает и скрывает переменную i в классе A B(int a, int b) < super.i = a; // обращаемся к переменной i из класса A i = b; // обращаемся к переменной i из класса B >void show() < System.out.println("i из суперкласса: " + super.i); System.out.println("i в подклассе: " + i); >> class MainActivity
В результате мы должны увидеть:
i из суперкласса: 1 i в подклассе: 2
Таким образом, знакомое нам выражение super.onCreate(savedInstanceState) обращается к методу onCreate() из базового класса.
Создание многоуровневой иерархии
Мы использовали простые примеры, состоящие из суперкласса и подкласса. Можно строить более сложные конструкции, содержащие любое количество уровней наследования. Например, класс C может быть подклассом класса B, который в свою очередь является подклассом класса A. В подобных ситуациях каждый подкласс наследует все характеристики всех его суперклассов.
Напишем пример из трёх классов. Суперкласс Box, подкласс HeavyBox и подкласс MoneyBox. Последний класс наследует все характеристики классов Box и HeavyBox, а также добавляет поле cost, которое содержит стоимость коробки.
Box.java
package ru.alexanderklimov.expresscourse; class Box < private int width; // ширина коробки private int height; // высота коробки private int depth; // глубина коробки // Конструктор для создания клона объекта Box(Box ob) < // передача объекта конструктору width = ob.width; height = ob.height; depth = ob.depth; >// Конструктор, используемый при указании всех измерений Box(int w, int h, int d) < width = w; height = h; depth = d; >// Конструктор, используемый, когда ни одно из измерений не указано Box() < // значение -1 используется // для указания неинициализированного параллелепипеда width = -1; height = -1; depth = -1; >// Конструктор для создания куба Box(int len) < width = height = depth = len; >// вычисляем объём коробки int getVolume() < return width * height * depth; >>
HeavyBox.java
package ru.alexanderklimov.expresscourse; //Добавление веса class HeavyBox extends Box < int weight; // вес коробки // Конструктор клона объекта HeavyBox(HeavyBox ob) < // передача объекта конструктору super(ob); weight = ob.weight; >// Конструктор, используемый // при указании всех параметров HeavyBox(int w, int h, int d, int m) < super(w, h, d); // вызов конструктора суперкласса weight = m; // масса >// Конструктор по умолчанию HeavyBox() < super(); weight = -1; >// Конструктор для создания куба HeavyBox(int len, int m) < super(len); weight = m; >>
MoneyBox
package ru.alexanderklimov.expresscourse; //Цена коробки class MoneyBox extends HeavyBox < int cost; // Конструирование клона объекта MoneyBox(MoneyBox ob) < // передача объекта конструктору super(ob); cost = ob.cost; >// Конструктор, используемый // при указании всех параметров MoneyBox(int w, int h, int d, int m, int c) < super(w, h, d, m); // вызов конструктора суперкласса cost = c; >// Конструктор по умолчанию MoneyBox() < super(); cost = -1; >// Конструктор для создания куба MoneyBox(int len, int m, int c) < super(len, m); cost = c; >>
Код для основной активности, например, при щелчке кнопки:
public void onClick(View v)
В результате мы получим различные значения, вычисляемые в коде. Благодаря наследованию, класс MoneyBox может использовать классы Box и HeavyBox, добавляя только ту информацию, которая нам требуется для его собственного специализированного применения. В этом и состоит принцип наследования, позволяя повторно использовать код.
Метод super() всегда ссылается на конструктор ближайшего суперкласса в иерархии. Т.е. метод super() в классе MoneyBox вызывает конструктор класса HeavyBox, а метод super() в классе HeavyBox вызывает конструктор класса Box.
Если в иерархии классов конструктор суперкласса требует передачи ему параметров, все подклассы должны передавать эти параметры по эстафете.
В иерархии классов конструкторы вызываются в порядке наследования, начиная с суперкласса и заканчивая подклассом. Если метод super() не применяется, программа использует конструктор каждого суперкласса, заданный по умолчанию или не содержащий параметров.
Вы можете создать три класса A, B, C, которые наследуются друг от друга (A←B←C), у которых в конструкторе выводится текст и вызвать в основном классе код:
C c = new C();
Вы должные увидеть три строчки текста, определённые в каждом конструкторе класса. Поскольку суперкласс ничего не знает о своих подклассах, любая инициализация полностью независима и, возможно, обязательна для выполнения любой инициализацией, выполняемой подклассом.
Переопределение методов
Если в иерархии классов имя и сигнатура типа метода подкласса совпадает с атрибутами метода суперкласса, то метод подкласса переопределяет метод суперкласса. Когда переопределённый метод вызывается из своего подкласса, он всегда будет ссылаться на версию этого метода, определённую подклассом. А версия метода из суперкласса будет скрыта.
Если нужно получить доступ к версии переопределённого метода, определённого в суперклассе, то используйте ключевое слово super.
Не путайте переопределение с перегрузкой. Переопределение метода выполняется только в том случае, если имена и сигнатуры типов двух методов идентичны. В противном случае два метода являются просто перегруженными.
В Java SE5 появилась запись @Override; она не является ключевым словом. Если вы собираетесь переопределить метод, используйте @Override, и компилятор выдаст сообщение об ошибке, если вместо переопределения будет случайно выполнена перегрузка.
Для закрепления материала создадим класс Animal с одним методом.
package ru.alexanderklimov.expresscourse; public class Animal < String sleep()< return "Животные иногда спят"; >>
Теперь создадим класс Cat, наследующий от первого класса.
package ru.alexanderklimov.expresscourse; public class Cat extends Animal
Java знает, у родительского класса есть метод sleep(). Удостовериться можно следующим образом. Находясь в классе Cat, выберите в меню Source | Override/Implement Methods. . Появится диалоговое окно, где можно отметить флажком нужный метод.

В результате в класс будет добавлена заготовка:
@Override String sleep() < // TODO Auto-generated method stub return super.sleep(); >
Попробуем вызвать данный метод в основном классе активности:
public void onClick(View v)
Мы получим текст, который определён в суперклассе, хотя вызывали метод дочернего класса.
Но если мы хотим получить другой текст, совсем не обязательно придумывать новые методы. Достаточно закомментировать вызов метода из суперкласса и добавить свой вариант.
@Override String sleep() < //return super.sleep(); return "Коты постоянно спят!"; >
Запускаем программу и нажимаем на кнопку. И получим уже другой ответ, более соответствующий описанию среднестатистического кота. Заметьте, что код для щелчка кнопки мы не меняем, но система сама разберётся, что выводить нужно текст не из суперкласса, а из дочернего класса.
Рассмотрим другой пример переопределения методов. Создадим суперкласс Figure, который будет содержать размеры фигуры, а также метод для вычисления площади. А затем создадим два других класса Rectangle и Triangle, у которых мы переопределим данный метод.
class Figure < double dim1; double dim2; Figure(double a, double b) < dim1 = a; dim2 = b; >double area() < System.out.printLn("Площадь фигуры"); return 0; >> class Rectangle extends Figure < Rectangle(double a, double b) < super(a, b); >// Переопределяем метод double area() < System.out.println("Площадь прямоугольника"); return dim1 * dim2; >> class Triangle extends Figure < Triangle(double a, double b) < super(a, b); >// переопределяем метод double area() < System.out.println("Площадь треугольника"); return dim1 * dim2 / 2; >> // В главной активности Figure figure = new Figure(10, 10); Rectangle rectangle = new Rectangle(8, 5); Triangle triangle = new Triangle(10, 6); Figure fig; fig = figure; mInfoTextView.setText("Площадь равна " + fig.area); fig = rectangle; mInfoTextView.setText("Площадь равна " + fig.area); fig = triangle; mInfoTextView.setText("Площадь равна " + fig.area);
Как видите, во всех классах используется одно и тоже имя метода, но каждый класс по своему вычисляет площадь в зависимости от фигуры. Это очень удобно и позволяет не придумывать новые названия методов в классах, которые наследуются от базового класса.
Методы доступа. Наиболее популярные ситуации
Статья в первую очередь расчитана на начинающих разработчиков, либо для тех, кто только начинает переходить от процедурного стиля программирования к ООП, посему матерых гуру просьба не вгонять в минуса 🙂
Права доступа к свойствам и методам — это на первый взгляд всего лишь три слова: private, protected и public. Но что скрывается за ними? Какие преимущества это дает в разработке? И как их правильно использовать? Здесь, как и во всех других аспектах программирования, без практики не разобраться…
Одна из трех основных концепций ООП — наследование (другие две: инкапсуляция и полиморфизм). Вобщем-то именно для нее и были реализованы права доступов. Основанная идея наследования: Дочерний объект, при наследовании (extend) родителя перенимает себе все родительские методы и свойства, а так же может обзавестись своими собственными. Понимая эту базу, можно перейти в всему что находится ниже…
Private — объявляет метод или свойство доступным только в том классе в котором он присутствует. Тоесть к private методам и свойствам мы не можем обращаться ни из объектов, ни из дочерних классов.
Protected — объявляет метод или свойство защищенными. Тоесть такими, которые не могут быть доступны из объекта, реализующего класс, но вполне может быть использовано в дочерних классах.
Public — публичный. Классы и методы, объявленные public, могут быть доступны как внутри самого класса, так и в дочерних классах и в объектах, реализовавших класс.
Сразу хочу заметить, что при наследовании, методы доступа изменяться могут только к более лояльным. тоесть в следующей последовательности, но не обратно: private → protected → public
Так же методы могут быть final тоесть такими, которые невозможно переопределить в классах потомках.
Вобщем-то все методы доступа используются исключительно для самодокументации кода и не несут никакой логической составляющей, так что и без них жизнь только тяжела, но не невозможна, что доказывает РНР4, в котором все методы и свойства были публичными…
Практика
Иногда случаются ситуации, когда этих методов доступа недостаточно. Тоесть, например, мы можем хотеть иметь доступ из объекта на чтение какого-то свойства, но при этом не иметь возможности в него писать. Самое простое решение: объявить свойство public и добавить комментарий /* только для чтения */, но про комментарий можно ненароком забыть и испортить логику поведения программы, вклинившись с нестандартным значением посреди выполнения. тогда приходит время использовать геттеры (getter\’s). Геттер — не что иное, как метод класса, реализующий исключительную возможность читать не публичные свойства из объекта. Вот пример:
class A private $a = 7;//мы не можем читать и писать в это свойство из объекта, реализующего этот класс
public function getA() < //публичный метод будет доступен объекту для обращения
return $this->a; //внутри класса мы можем получать доступ к приватным свойствам
>
>
$obj = new A();
echo $obj->getA();//мы получили значение приватной переменной $a
Похожим способом ведут себя и сеттеры (setter\’s), когда нам необходимо иметь возможность установить значение переменной, но не читать ее напрямую, так как она, к примеру, должна быть преобразована прежде чем быть использованной. Пример метода сеттера:
//.
public funtion setA($value) < //метод будет доступен для объекта
$this->a = $value; //приватное свойство $a может быть установленное внутри класса, но не доступно для прямого влияния из объекта
>
//.
Еще одним вариантом реализации доступа к методам, когда метод должен быть отовсюду доступен только для чтения, является введение \«псевдо-свойства\»:
class A public function getValue() static $value;
if (empty($value)) $value = //. тут значение создается по каким-то известным параметрам и повлиять извне на него мы никак не сможем
>
в примере выше, класс А будет обладать псевдо-свойством $value. Псевдо — потому что оно реализуется исключительно через метод, а доступ к нему возможен только на чтение. Еще можете заметить что я использовал паттерн \«ленивой инициализации\», что бы отложить создание свойства до последнего момента и заодно как бы \«закешировать\» его. Где это можно применить, хорошо проиллюстрировано в соседнем топике об ООП в РНР.
Хорошей практикой является сокрытие всех свойств методом private и, в зависимости от нужд, создавать для них сеттеры или геттеры, но нужно быть внимательным, что если для свойства существует и сеттер и геттер, а дополнительной логики обработки данных нет, то не проще ли их убрать, а свойство сделать публичным? 🙂
Наследование. Шаблоны классов
Аннотация: В данной лекции рассматривается простое и множественное наследование классов. Виртуальные методы. Абстрактные классы. Создание и использование шаблонов классов.
Наследование
Презентацию к лекции Вы можете скачать здесь.
При большом количестве никак не связанных классов управлять ими становится невозможным. Наследование позволяет справиться с этой проблемой путем упорядочивания и ранжирования классов, то есть объединения общих для нескольких классов свойств в одном классе и использования его в качестве базового.
Механизм наследования классов позволяет строить иерархии , в которых производные классы получают элементы родительских, или базовых, классов и могут дополнять их или изменять их свойства.
Классы, находящиеся ближе к началу иерархии , объединяют в себе наиболее общие черты для всех нижележащих классов. По мере продвижения вниз по иерархии классы приобретают все больше конкретных черт. Множественное наследование позволяет одному классу обладать свойствами двух и более родительских классов .
Виды наследования
При описании класса в его заголовке перечисляются все классы, являющиеся для него базовыми. Возможность обращения к элементам этих классов регулируется с помощью модификаторов наследования private , protected и public :
class имя : [private | protected | public] базовый_класс;
Если базовых классов несколько, они перечисляются через запятую. Перед каждым может стоять свой модификатор наследования . По умолчанию для классов он private , а для структур — public .
Если задан модификатор наследования public , оно называется открытым. Использование модификатора protected делает наследование защищенным, а модификатора private — закрытым. Это не просто названия: в зависимости от вида наследования классы ведут себя по-разному. Класс может наследовать от структуры, и наоборот.
До сих пор мы рассматривали только спецификаторы доступа private и public , применяемые к элементам класса. Для любого элемента класса может также использоваться спецификатор protected , который для одиночных классов, не входящих в иерархию, равносилен private . Разница между ними проявляется при наследовании . Возможные сочетания модификаторов и спецификаторов доступа приведены в таблице:
| Модификатор наследования | Спецификатор базового класса | Доступ в производном классе |
|---|---|---|
| private | private | нет |
| protected | private | |
| public | private | |
| protected | private | нет |
| protected | protected | |
| public | protected | |
| public | private | нет |
| protected | protected | |
| public | public |
Как видно из таблицы, private элементы базового класса в производном классе недоступны вне зависимости от ключа. Обращение к ним может осуществляться только через методы базового класса .
Элементы protected при наследовании с ключом private становятся в производном классе private , в остальных случаях права доступа к ним не изменяются.
Доступ к элементам public при наследовании становится соответствующим ключу доступа.
Если базовый класс наследуется с ключом private , можно выборочно сделать некоторые его элементы доступными в производном классе , объявив их в секции public производного класса с помощью операции доступа к области видимости :
class Base; class Derived : private Base;
Простое наследование
Простым называется наследование , при котором производный класс имеет одного родителя. Для различных элементов класса существуют разные правила наследования . Рассмотрим наследование классов на примере.
Создадим производный от класса monster класс daemon , добавив полезную в некоторых случаях способность думать:
enum color ; // ------------- Класс monster ------------- class monster < // ------------- Скрытые поля класса: int health, ammo; color skin; char *name; public: // ------------- Конструкторы: monster(int he = 100, int am = 10); monster(color sk); monster(char * nam); monster(monster &M); // ------------- Деструктор: ~monster() // ------------- Операции: monster& operator ++() <++health; return *this;>monster operator ++(int) operator int() bool operator >(monster &M) < if( health >M.get_health()) return true; return false; > monster& operator = (monster &M) < if (&M == this) return *this; if (name) delete [] name; if (M.name) < name = new char [strlen(M.name) + 1]; strcpy(name, M.name); >else name = 0; health = M.health; ammo = M.ammo; skin = M.skin; return *this; > // ------------- Методы доступа к полям: int get_health() const int get_ammo() const // ------------- Методы, изменяющие значения полей: void set_health(int he) < health = he;>void draw(int x, int y, int scale, int position); >; // ------------- Реализация класса monster ------------- monster::monster(int he, int am): health (he), ammo (am), skin (red), name (0)<> monster::monster(monster &M) < if (M.name) < name = new char [strlen(M.name) + 1]; strcpy(name, M.name); >else name = 0; health = M.health; ammo = M.ammo; skin = M.skin; > monster::monster(color sk) < switch (sk) < case red:health = 100; ammo = 10; skin = red; name = 0; break; case green:health = 100;ammo = 20;skin = green; name = 0; break; case blue: health = 100; ammo = 40; skin = blue; name = 0;break; >> monster::monster(char * nam) < name = new char [strlen(nam)+1]; strcpy(name, nam); health = 100; ammo = 10; skin = red; >void monster::draw(int x, int y, int scale, int position) < /* . Отрисовка monster */ >// ------------- Класс daemon ------------- class daemon : public monster < int brain; public: // ------------- Конструкторы: daemon(int br = 10); daemon(color sk) : monster (sk) daemon(char * nam) : monster (nam) daemon(daemon &M) : monster (M) // ------------- Операции: daemon& operator = (daemon &M) < if (&M == this) return *this; brain = M.brain; monster::operator = (M); return *this; >// ------------- Методы, изменяющие значения полей: void draw(int x, int y, int scale, int position); void think(); >; // ------------- Реализация класса daemon ------------- void daemon::draw(int x, int y, int scale, int position) < /* . Отрисовка daemon */ >void daemon:: think()< /* . */ >
В классе daemon введено поле brain и метод think , определены собственные конструкторы и операция присваивания , а также переопределен метод отрисовки draw . Все поля класса monster , операции (кроме присваивания ) и методы get_health , get_ammo и set_health наследуются в классе daemon , а деструктор формируется по умолчанию.
Рассмотрим правила наследования различных методов.
Конструкторы не наследуются, поэтому производный класс должен иметь собственные конструкторы .
- Если в конструкторе производного класса явный вызов конструктора базового класса отсутствует, автоматически вызывается конструктор базового класса по умолчанию (то есть тот, который можно вызвать без параметров). Это использовано в первом из конструкторов класса daemon .
- Для иерархии , состоящей из нескольких уровней, конструкторы базовых классов вызываются начиная с самого верхнего уровня. После этого выполняются конструкторы тех элементов класса, которые являются объектами, в порядке их объявления в классе, а затем исполняется конструктор класса .
- В случае нескольких базовых классов их конструкторы вызываются в порядке объявления.
Если конструктор базового класса требует указания параметров, он должен быть явным образом вызван в конструкторе производного класса в списке инициализации (это продемонстрировано в трех последних конструкторах).
Не наследуется и операция присваивания, поэтому ее также требуется явно определить в классе daemon . Обратите внимание на запись функции-операции: в ее теле применен явный вызов функции- операции присваивания из базового класса . Чтобы лучше представить себе синтаксис вызова, ключевое слово operator вместе со знаком операции можно интерпретировать как имя функции -операции.
Вызов функций базового класса предпочтительнее копирования фрагментов кода из функций базового класса в функции производного. Кроме сокращения объема кода, этим достигается упрощение модификации программы: изменения требуется вносить только в одну точку программы, что сокращает количество возможных ошибок.
Правила для деструкторов при наследовании :
- Деструкторы не наследуются, и если программист не описал в производном классе деструктор , он формируется по умолчанию и вызывает деструкторы всех базовых классов .
- В отличие от конструкторов, при написании деструктора производного класса в нем не требуется явно вызывать деструкторы базовых классов, поскольку это будет сделано автоматически.
- Для иерархии классов , состоящей из нескольких уровней, деструкторы вызываются в порядке, строго обратном вызову конструкторов: сначала вызывается деструктор класса , затем — деструкторы элементов класса, а потом деструктор базового класса .
Поля, унаследованные из класса monster , недоступны функциям производного класса , поскольку они определены в базовом классе как private . Если функциям, определенным в daemon , требуется работать с этими полями, можно либо описать их в базовом классе как protected , либо обращаться к ним с помощью функций из monster , либо явно переопределить их в daemon так, как было показано в предыдущем разделе.
Добавляемые поля в наследнике могут совпадать и по имени, и по типу с полями базового класса . При этом поле предка будет скрыто.
Статические поля, объявленные в базовом классе, наследуются обычным образом. Все объекты базового класса и всех его наследников разделяют единственную копию статических полей базового класса .
Рассматривая наследование методов, обратите внимание на то, что в классе daemon описан метод draw , переопределяющий метод с тем же именем в классе monster (поскольку отрисовка различных персонажей, естественно, выполняется по-разному). Таким образом, производный класс может не только дополнять, но и корректировать поведение базового класса . Доступ к переопределенному методу базового класса для производного класса выполняется через уточненное с помощью операции доступа к области видимости имя.
Класс-потомок наследует все методы базового класса , кроме конструкторов, деструктора и операции присваивания . Не наследуются ни дружественные функции, ни дружественные отношения классов.
В классе-наследнике можно определять новые методы. В них разрешается вызывать любые доступные методы базового класса . Если имя метода в наследнике совпадает с именем метода базового класса , то метод производного класса скрывает все методы базового класса с таким именем. При этом прототипы методов могут не совпадать. Если в методе-наследнике требуется вызвать одноименный метод родительского класса , нужно задать его с префиксом класса. Это же касается и статических методов .
Наследование
Для всех классов в Kotlin родительским суперклассом является класс Any . Он также является родительским классом для любого класса, в котором не указан какой-либо другой родительский класс.
class Example // Неявно наследуется от Any
У Any есть три метода: equals() , hashCode() и toString() . Эти методы определены для всех классов в Kotlin.
По умолчанию все классы в Kotlin имеют статус final, который блокирует возможность наследования. Чтобы сделать класс наследуемым, его нужно пометить ключевым словом open .
open class Base // Класс открыт для наследования
Для явного объявления суперкласса мы помещаем его имя за знаком двоеточия в оглавлении класса:
open class Base(p: Int) class Derived(p: Int) : Base(p)
Если у класса есть основной конструктор, базовый тип может (и должен) быть проинициализирован там же, с использованием параметров основного конструктора.
Если у класса нет основного конструктора, тогда каждый последующий дополнительный конструктор должен включать в себя инициализацию базового типа с помощью ключевого слова super или давать отсылку на другой конструктор, который это делает. Примечательно, что любые дополнительные конструкторы могут ссылаться на разные конструкторы базового типа.
class MyView : View
Переопределение методов класса
Kotlin требует явно указывать модификаторы и для членов, которые могут быть переопределены, и для самого переопределения.
open class Shape < open fun draw() < /*. */ >fun fill() < /*. */ >> class Circle() : Shape() < override fun draw() < /*. */ >>
Для Circle.draw() необходим модификатор override . В случае её отсутствия компилятор выдаст ошибку. Если у функции типа Shape.fill() нет модификатора open , объявление метода с такой же сигнатурой в производном классе невозможно, с override или без. Модификатор open не действует при добавлении к членам final класса (т.е. класса без модификатора open ).
Член класса, помеченный override , является сам по себе open, т.е. он может быть переопределён в производных классах. Если вы хотите запретить возможность переопределения такого члена, используйте final .
open class Rectangle() : Shape() < final override fun draw() < /*. */ >>
Переопределение свойств класса
Переопределение свойств работает также, как и переопределение методов; все свойства, унаследованные от суперкласса, должны быть помечены ключевым словом override , а также должны иметь совместимый тип. Каждое объявленное свойство может быть переопределено свойством с инициализацией или свойством с get -методом.
open class Shape < open val vertexCount: Int = 0 >class Rectangle : Shape()
Вы также можете переопределить свойство val свойством var , но не наоборот. Это разрешено, поскольку свойство val объявляет get -метод, а при переопределении его как var дополнительно объявляется set -метод в производном классе.
Обратите внимание, что ключевое слово override может быть использовано в основном конструкторе класса как часть объявления свойства.
interface Shape < val vertexCount: Int >class Rectangle(override val vertexCount: Int = 4) : Shape // Всегда имеет 4 вершины class Polygon : Shape < override var vertexCount: Int = 0 // Может быть установлено любое количество >
Порядок инициализации производного класса
При создании нового экземпляра класса в первую очередь выполняется инициализация базового класса (этому шагу предшествует только оценка аргументов, передаваемых в конструктор базового класса) и, таким образом, происходит до запуска логики инициализации производного класса.
open class Base(val name: String) < init < println("Инициализация класса Base") >open val size: Int = name.length.also < println("Инициализация свойства size в класса Base: $it") >> class Derived( name: String, val lastName: String, ) : Base(name.replaceFirstChar < it.uppercase() >.also < println("Аргументы, переданные в конструктор класса Base: $it") >) < init < println("Инициализация класса Derived") >override val size: Int = (super.size + lastName.length).also < println("Инициализация свойства size в классе Derived: $it") >> fun main()
Это означает, что свойства, объявленные или переопределенные в производном классе, не инициализированы к моменту вызова конструктора базового класса. Если какое-либо из этих свойств используется в логике инициализации базового класса (прямо или косвенно через другую переопределенную open реализацию члена класса), это может привести к некорректному поведению или сбою во время выполнения. Поэтому при разработке базового класса следует избегать использования членов с ключевым словом open в конструкторах, инициализации свойств и блоков инициализации ( init ).
Вызов функций и свойств суперкласса
Производный класс может вызывать реализацию функций и свойств своего суперкласса, используя ключевое слово super .
open class Rectangle < open fun draw() < println("Рисование прямоугольника") >val borderColor: String get() = "black" > class FilledRectangle : Rectangle() < override fun draw() < super.draw() println("Заполнение прямоугольника") >val fillColor: String get() = super.borderColor >
Во внутреннем классе доступ к суперклассу внешнего класса осуществляется при помощи ключевого слова super , за которым следует имя внешнего класса: super@Outer .
class FilledRectangle: Rectangle() < override fun draw() < val filler = Filler() filler.drawAndFill() >inner class Filler < fun fill() < println("Filling") >fun drawAndFill() < super@FilledRectangle.draw() // Вызывает реализацию функции draw() класса Rectangle fill() println("Нарисованный прямоугольник заполнен $цветом") // Используется реализация get()-метода свойства borderColor в классе > > >
Правила переопределения
В Kotlin правила наследования реализации определены следующим образом: если класс наследует многочисленные реализации одного и того члена от ближайших родительских классов, он должен переопределить этот член и обеспечить свою собственную реализацию (возможно, используя одну из унаследованных).
Для того чтобы отметить конкретный супертип (родительский класс), от которого мы наследуем данную реализацию, используйте ключевое слово super . Для задания имени родительского супертипа используются треугольные скобки, например super .
open class Rectangle < open fun draw() < /* . */ >> interface Polygon < fun draw() < /* . */ >// члены интерфейса открыты ('open') по умолчанию > class Square() : Rectangle(), Polygon < // Компилятор требует, чтобы функция draw() была переопределена: override fun draw() < super.draw() // вызов Rectangle.draw() super.draw() // вызов Polygon.draw() > >
Это нормально, наследоваться одновременно от Rectangle и Polygon , но так как у каждого из них есть своя реализация функции draw() , мы должны переопределить draw() в Square и обеспечить нашу собственную реализацию этого метода для устранения получившейся неоднозначности.
© 2015—2023 Open Source Community