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

Переменные примитивного и ссылочного типа

Состояния переменных примитивного типа

Рассмотренные ранее переменные примитивного типа (boolean, char, byte, short, int, long, float, double) могут находиться в двух состояниях:

Переменные ссылочного типа

К переменным ссылочного типа относятся такие переменные, как:

String s;  //ссылка на объект типа строка
int [] a; //ссылка на объект типа массив
ComplexInteger n;  //ссылка на объект типа ComplexInteger
  

Состояния переменных ссылочного типа

Переменная ссылочного типа может находиться в трёх состояниях:

То есть в процессе выполнения переменная ссылочного типа может находиться в двух состояниях: ссылаться на объект либо равна null.

Инициализация переменных ссылочного типа

При создании массива из ссылок они автоматически инициализируются значением null.

String [] strs = new String [20];

Поля ссылочного типа также инициализируются значением null, а поля примитивного типа – значением 0.

public class X{
 int myValue; // инициализируется 0
 String myString; // инициализируется null
}

final переменные

Модификатор final переменной можно присвоить:

Проинициализировав 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.

  1. с полями

    class X {
        final int myValue = 10;
    }
        

    Подробнее смотри выше.

  2. с локальными переменными

    Результат аналогичен применению const в C++.

    Локальные final переменные обязаны инициализироваться сразу.

    Пример:

    final List list = new List(0);
    	

    final относится только к ссылке list. При этом, естественно, сам объект можно менять, используя, к примеру, метод

    list.addValue(1);
    	

    Но оператор

    list = list2;
    	

    вызовет ошибку на этапе компиляции.

  3. с методами

    final методы нельзя перекрывать (но можно перегружать). final методы связываются при компиляции, так как известно, метод какого класса нужно вызвать. Такое связывание называется статическим.

    Base b1 = new Derived();
    b1.zzz(); // здесь при компиляции сразу указывается откуда вызывать метод final zzz();
    	

    Это гораздо быстрее, чем динамическое связывание методов, описанных без final. Поэтому final методы определяют в основном только для оптимизации.

  4. с классами

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