Наследование
Переменные примитивного и ссылочного типа
Состояния переменных примитивного типа
Рассмотренные ранее переменные примитивного типа (boolean
, char
, byte
,
short
, int
, long
, float
,
double
) могут находиться в двух состояниях:
- иметь значение
- не инициализировано – при попытке чтения неинициализированной переменной программа не скомплилируется.
Переменные ссылочного типа
К переменным ссылочного типа относятся такие переменные, как:
String s; //ссылка на объект типа строка int [] a; //ссылка на объект типа массив ComplexInteger n; //ссылка на объект типа ComplexInteger
Состояния переменных ссылочного типа
Переменная ссылочного типа может находиться в трёх состояниях:
- Ссылаться на объект
-
Ни на что не ссылаться – для
обозначения этого используют ключевое слово
null
. А на рисунках используют кружок с точкой:String s = null;
При этом недопустимы операции вызовов методов:s.foo(); // программа сломается, т.к. s равна null s == null; // допустима операция сравнения на равенство bar(s); // передача в качестве параметра
-
не инициализировано – например функция
void foo(){ String s; String s; System.out.println(s); }
не скомпилируется, так как к моменту вызова функции печати строка не имеет никакого значения.
null
.
Инициализация переменных ссылочного типа
При создании массива из ссылок они автоматически инициализируются значением null
.
String [] strs = new String [20];
Поля ссылочного типа также инициализируются значением null
, а поля примитивного типа – значением 0.
public class X{ int myValue; // инициализируется 0 String myString; // инициализируется null }
final
переменные
Модификатор final
переменной можно присвоить:
- сразу при объявлении
- в инициализаторах
- в конструкторах – причём
final
переменная к концу каждого конструктора должна быть проинициализирована. Если используются конструкции ветвления (if … else …
,switch
, циклы), то инициализация должна присутствовать во всех ветвях.
Проинициализировав final
переменную сразу при объявлении, присвоить ей значение в другом месте
(в конструкторе, инициализаторе, теле функции) нельзя, поскольку это вызовет ошибку компиляции. В инициализаторах и конструкторах
инициализировать можно сколько угодно раз.
Пример инициализации final
переменных
class X { final int myValue; X() { if (...) { myValue = 20; } else { System.out.println(“hello”); } } }
Этот код не скомпилируется, т.к. в ветке else
переменная myValue
не инициализирована.
class X{ final int myValue = 10; X() { if (...){ myValue = 20; } else { myValue = 15; } } }
Произойдёт ошибка компиляции в строчке myValue = 20;
, т.к. переменная myValue
инициализирована при объявлении значением 10
.
То же самое относится и к final
переменным ссылочного типа.
Наследование
Односвязный список
Односвязный список – структура данных, представляющая собой последовательность элементов, в каждом из которых хранится
значение и указатель на следующий элемент списка. В последнем элементе указатель на следующий элемент равен null
.
Рассмотрим примитивную (не самую лучшую) организацию списка, при которой невозможно создать пустой список.
Создадим список с методами добавления нового значение и подсчета длины списка.
public class List { private int myValue; private List myNext; public List (int value) { myValue = value; myNext = null; } public void addValue (int value) { List current = this; while (current.myNext != null) { current = current.myNext; } current.myNext = new List (value); } public int length () { int counter = 0; for ( List current=this; current != null; current = current.myNext ) { counter++; } return counter; } }
this
– условное обозначение ссылки на объект, вызванный функцией, неявно используется внутри метода для ссылки
на элементы объекта.
Двусвязный список
Двусвязный список отличается от односвязного тем, что в каждом элементе кроме указателя на следующий элемент находится
указатель на предыдущий. В первом элемента указатель на предыдущий элемент равен null
.
А теперь рассмотрим двусвязный список:
public class DoubleLinkedList { private int myValue; private DoubleLinkedList myNext; private DoubleLinkedList myPrevious; public DoubleLinkedList(int value) { myValue = value; } public void addValue(int value) { DoubleLinkedList current = this; while (current.myNext != null) { current = current.myNext; } current.myNext = new DoubleLinkedList(value); current.myNext.myPrevious = current; } public int length() { int counter = 0; for (DoubleLinkedList current = this; current != null; current = current.myNext) { counter++; } return counter; } }
Механизм наследования
Как видно, поля класса DoubleLinkedList
повторяют поля List
. Методы первого очень незначительно отличаются
от второго.
Приходится два раза писать почти одинаковые методы. Было бы удобно, если их общие поля и методы были описаны в одном классе,
а в другом они были доступны, в котором описаны и свои собственные отличающиеся свойства. Механизм, при котором свойства одного
класса копируются из другого, называется наследованием.
Двусвязный список с использованием наследования
Наследник – объект, состоящий из полей и методов своего родителя и своих характерных полей и методов. Поэтому базовым нужно делать класс, содержащий общие поля и методы.
Общие свойства классов DoubleLinkedList
и List
описаны в последнем, поэтому обозначим его базовым,
а класс DoubleLinkedList
укажем его наследником. Почти всё, что есть в List
, будет доступно в
DoubleLinkedList
. Добавим поле myPrevious
и другие специфические свойства.
Таким образом, DoubleLinkedList
содержит в себе объект List
и свои специфические свойства.
Перепишем DoubleLinkedList
.
public class DoubleLinkedList extends List { private DoubleLinkedList myPrevious; public DoubleLinkedList(int value) { super (value); // вызывается конструктор List myPrevious = null; } public void addValue(int value) { DLList current = this; while (current.myNext != null) { current = current.myNext; } current.myNext = new DoubleLinkedList(value); current.myNext.myPrevious = current; } }
Ключевое слово extends
Ключевое слово extends
указывает, что класс DoubleLinkedList
- наследник класса List
.
Поля myValue
, myNext
доступны
в DoubleLinkedList
, т. к. они унаследованы от суперкласса List
.
При использовании наследования классы List
и DoubleLinkedList
можно называть по-разному:
List
– суперкласс для DoubleLinkedList
; базовый класс DoubleLinkedList
;
родительский класс для DoubleLinkedList
; DoubleLinkedList
– производный класс от List
;
наследник List
.
Ключевое слово super
Ключевое слово super
– условное обозначение вызова конструктора базового класса. В скобках указываются параметры,
требуемые конструктором. В конструкторе наследника слово super
можно не указывать, если в базовом классе есть конструктор
без параметров или нет никакого (поскольку такой конструктор создаётся автоматически). В остальных случаях ключевое слово
super
должно присутствовать и обязательно идти первым в теле конструктора.
super
в конструкторе наследника
public class Base { public int myBase; public Base() { myBase = 13; } public Base(int value) { myBase = value; } } public class Derived extends Base { public int myDerived; public Derived() { myDerived = 666; } }
Класс Base
имеет конструктор без параметров и с параметром. Поэтому в конструкторе класса Derived
ключевое слово super
не указано.
this
в конструкторе класса
Если в классе есть несколько конструкторов, то в первой строке конструктора можно написать слово this()
,
указав в скобках нужные параметры. Это вызовет конструктор класса с таким же набором параметров.
public class Base { public int myBase; public Base(int value) { myBase = value; } public Base() { this(0); //вызовется конструктор Base(int value) со значением value = 0 } }
Наследник - объект родительского класса
DoubleLinkedList
и List
имеют методы с одинаковым именами и одинаковым набором типов параметров -
void addValue(int value)
. Говорят, что метод addValue
в двусвязном списке перекрывает метод
addValue
в односвязном списке. Все остальные методы List
наследуются в DoubleLinkedList
.
То есть int length()
доступно объектам типа DoubleLinkedList
. При этом в методе
int length()
используется переменная current
типа List
со значением this
-
ссылка на объект типа DoubleLinkedList
. Проблема "несоответствия типов" разрешается идеей наследования:
всякий объект наследника в то же время является объектом родительского класса.
Например, если есть функция
foo (List l) {}
то её параметром можно передавать
DoubleLinkedList dll; … foo(dll);
Исправление прав доступа
В классе DoubleLinkedList
в методе addValue
используется доступ к полю myNext
класса
List
. Он был объявлен как private
, поэтому такой код не скомпилируется. Для исправления этого следует
изменить право доступа к этому полю (пока что) на public
. Подробнее в другой лекции.
Исправление ошибки не соответствия типов
Поле myNext
имеет тип List
, а переменная current
- DoubleLinkedList
.
Поэтому если бы мы написали
current = current.myNext;
то есть переменной типа DoubleLinkedList
присваивали значение переменой типа List
, то при компиляции
произошла ошибка. Но так как мы уверены, что myNext
будет указывать на объект типа DoubleLinkedList
,
то используем явное приведение типа, хотя это и противоречит идеям объектно-ориентированного программирования. Но если окажется,
что myNext
указывает на объект не типа DoubleLinkedList
, то программа в процессе выполнения сломается
в точке приведения типа.
Исправленный код
public class List { private int myValue; public List myNext; public List(int value) { myValue = value; myNext = null; } public void addValue(int value) { List current = this; while (current.myNext != null) { current = current.myNext; } current.myNext = new List(value); } public int length() { int counter = 0; for ( List current = this; current != null; current = current.myNext) { counter++; } return counter; } } public class DoubleLinkedList extends List { private DoubleLinkedList myPrevious; public DoubleLinkedList (int value) { super(value); // вызывается конструктор List myPrevious = null; } public void addValue (int value) { DoubleLinkedList current = this; while (current.myNext != null) { current = (DoubleLinkedList) current.myNext; } current.myNext = new DoubleLinkedList(value); ((DoubleLinkedList) current.myNext).myPrevious = current; } }
Пример с Base
и Derived
Рассмотрим ещё пример:
public class Base { public int myBase; public final void zzz() { ... } public void foo() { ... myBase++; } public void bar() { foo(); } } public class Derived extends Base { public int myDerived; public Derived () { myDerived = 666; } public void foo () { ... myBase += myDerived; } public void fff() { myBase += super.foo() + foo() + bar(); } }
Пояснения к примеру.
Класс Derived
наследует от Base
поле myBase
и методы bar()
и
final zzz()
. Метод foo()
перекрывается. Т.к. у Base
отсутствуют конструкторы, то
в конструкторе Derived()
ключевое слово super
не указано. Разберём, что происходит в методе
fff()
в строке myBase += super.foo() + foo() + bar()
.
super.foo()
- вызовется метод foo()
класса Base
, который увеличит myBase
на единицу.
foo()
– вызовется метод у класса Derived
, что прибавит к myBase
значение myDerived
.
bar()
– вызовет общий метод классов, в котором вызывается foo()
. Какой именно foo()
будет
выполняться, зависит от того, у какого объекта вызван метод. Если у Base
, то foo()
увеличит
myBase
на единицу, если у Derived
, то myBase
увеличится на значение myDerived
.
Динамическое связывание
Рассмотрим на примере:
List l1 = new List(0); l1.addValue(1); DoubleLinkedList dll = new DLList(0); List l2 = dll; l2.addValue(2); //вызовется метод addValue из DoubleLinkedList
У ссылки на объект и у объекта могут быть разные типы. Переменная l2
типа List
указывает на объект
типа DoubleLinkedList
. У каждого их этих классов есть свой метод addValue
. Метод вызывается у объекта,
поэтому метод addValue
вызовется из DoubleLinkedList
. Это решается в процессе выполнения.
Java-машина смотрит, к какому классу принадлежит объект и у него вызывает метод. Это и называется динамическим связыванием.
Ключевое слово final
Ключевое слово final
может применяться с 4 видами сущностей языка java.
с полями
class X { final int myValue = 10; }
Подробнее смотри выше.
с локальными переменными
Результат аналогичен применению
const
в C++.Локальные
final
переменные обязаны инициализироваться сразу.Пример:
final List list = new List(0);
final
относится только к ссылкеlist
. При этом, естественно, сам объект можно менять, используя, к примеру, методlist.addValue(1);
Но оператор
list = list2;
вызовет ошибку на этапе компиляции.
с методами
final
методы нельзя перекрывать (но можно перегружать).final
методы связываются при компиляции, так как известно, метод какого класса нужно вызвать. Такое связывание называется статическим.Base b1 = new Derived(); b1.zzz(); // здесь при компиляции сразу указывается откуда вызывать метод final zzz();
Это гораздо быстрее, чем динамическое связывание методов, описанных без
final
. Поэтомуfinal
методы определяют в основном только для оптимизации.с классами
У
final
класса не может быть наследников.final
классы, так же как иfinal
методы используются в основном только для ускорения работы программы и безопасности, так как все методыfinal
класса - тожеfinal
.