Производные классы и наследование

 

class Car extends Vechicle  { }

 

Терминология:

 

           

Car

Vechicle

Базовый (base) класс

Производный (derived) класс

Суперкласс

Подкласс

Родительский (parent) класс

Класс-потомок (child)

 

 

Не наследуются – все статичные члены базового класса.  Почему?

 

Не все наследованные члены видны в методах производного класса!

NB! protected

 

Внутри каждого объекта производного типа существует подобъект базового типа (наследованная часть базового класса).   

 

Инициализация объекта производного типа. Вызов конструктора суперкласса.

 

Переопределение наследованных методов в производном классеoverriding.

 

Переопределять можно любые видимые в производном классе не финальные методы. Исключение - переопределение конструкторов базового класс невозможно!

 

«Переопределение» полей – разрешается создавать два одноименных поля:

            this.n   vs  super.n

 

В производном классе разрешается перегружать (overload) любые видимые методы.

 

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

 

Отсюда связь „он и есть“ используется в наследовании, а „содержит“ при композиции.

 

 

Полиморфизм

 

Полиморфизм - третья неотъемлемая часть объектно-ориентированного программирования, после абстракции и наследования соответственно.

 

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

 

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

 

Приведение к базовому типу (upcasting)

 

Наиболее важный аспект наследования заключается вовсе не в снабжении нового класса новыми методами. А заключается он в отношении между новым классом и базовым классом. Данное отношение можно определить так "Новый класс имеет тип существующего класса."

 

Это описание, не просто причудливая форма раскрытия сущности наследования, такая форма поддерживается напрямую языком Java. В примере, рассматриваемый базовый класс называется Instrument и представляет музыкальные инструменты, а дочерний класс называется Wind (духовые инструменты).

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

Если класс Instrument имеет метод play( ), то и Wind так же может его использовать. Это означает, что мы можем точно так же сказать, что объект Wind так же и типа Instrument. Следующий пример показывает, как компилятор поддерживает это высказывание:

//: c06:Wind.java

// Наследование и приведение к базовому типу.

import java.util.*;

 

class Instrument {

  public void play() {}

  static void tune(Instrument i) {

    // ...

    i.play();

  }

}

 

// Объект Wind так же Instrument

// потому что они имеют общий интерфейс:

class Wind extends Instrument {

  public static void main(String[] args) {

    Wind flute = new Wind();

    Instrument.tune(flute); // Upcasting

  }

} ///:~

Что действительно интересно в этом примере, так это то, что метод tune( ) поддерживает ссылку на Instrument. Однако, в Wind.main( ) метод tune( ) вызывается с передачей ссылки на Wind.

Из этого следует, что Java специфична с проверкой типов, это выглядит достаточно странно, если метод принимающий в качестве параметра один тип, вдруг спокойно принимает другой, но так пока вы не поймете, что объект Wind так же является и объектом типа Instrument, и в нем нет метода tune( ) который можно было бы вызвать для Instrument.

Внутри tune( ), код работает с типами Instrument и с чем угодно от него произошедшим, а факт конвертации ссылки на Wind в ссылку на Instrument называется приведением к базовому типу (upcasting).

 

Почему upcasting?

 

 

Вы так же можете осуществить обратную приведению к базовому типу операцию, называемую приведение базового типа к дочернему (downcasting).

 

Забывание типа объекта?

Wind flute = new Wind();

Instrument i = flute;  // upcasting

 

Пример

 

// Перегрузка, вместо приведедния к базовому типу.

 

class Note {

  private int value;

  private Note(int val) { value = val; }

  public static final Note

    MIDDLE_C = new Note(0),

    C_SHARP = new Note(1),

    B_FLAT = new Note(2);

} // И т.д.

 

class Instrument {

  public void play(Note n) {

    System.out.println("Instrument.play()");

  }

}

 

class Wind extends Instrument {

  public void play(Note n) {

    System.out.println("Wind.play()");

  }

}

 

class Stringed extends Instrument {

  public void play(Note n) {

    System.out.println("Stringed.play()");

  }

}

 

class Brass extends Instrument {

  public void play(Note n) {

    System.out.println("Brass.play()");

  }

}

 

public class Music2 {

  public static void tune(Wind i) {

    i.play(Note.MIDDLE_C);

  }

  public static void tune(Stringed i) {

    i.play(Note.MIDDLE_C);

  }

  public static void tune(Brass i) {

    i.play(Note.MIDDLE_C);

  }

  public static void main(String[] args) {

    Wind flute = new Wind();

    Stringed violin = new Stringed();

    Brass frenchHorn = new Brass();

    tune(flute); // Раннее связывание!!!

    tune(violin);

    tune(frenchHorn);

  }

} ///:~

 

Ура, работает, но при этом возникает большая работа по переписки кода: Вы должны писать типо-зависимые методы, для каждого нового класса Instrument, которые Вы добавите. А это означает, что во-первых нужно больше программировать, во-вторых, если Вы захотите добавить новый метод по типу tune( ) или просто новый тип инструмента, то придется проделать много работы.

 

 

А не было бы намного лучше, если бы Вы написали один метод, который получает в качестве аргумента базовый класс, а не каждый по отдельности дочерний класс?

 

Было бы, но не было бы хорошо, если бы Вы смогли забыть, что есть какие-то дочерние классы и написали бы ваш код только для базового класса?

 

Именно это полиморфизм и позволяет делать. Но все равно, многие программисты пришедшие из процедурного программирования имеют небольшие проблемы при работе с полиморфизмом.

 

Отношение:  метод - вызов

 

Соединение вызова метода с телом метода называется связыванием (binding).

Когда свзяывание осуществляется до запуска программы (компилятором и компоновщиком, если такой используется), то оно (связывание) называется ранним связыванием (early binding).

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

 

Позднее связывание (late binding) означает, что связывание происходит во время работы программы и основывается на типе объекта. Позднее связывание так же иногда называют динамическим связыванием или связыванием во время выполнения.

 

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

 

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

 

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

 

Упражнение. Модифицировать проект Music:

·         Instrument.tune(flute); ….

·         Insrument orchestra[];   orchestra[k].play;

 

Расширяемость

Теперь давайте вернемся к нашему примеру с музыкальными инструментами.

В полиморфизме, Вы можете добавить столько новых типов, сколько захотите, без изменения метода tune( ).

В хорошо спроектированной ООП программе, большинство или все ваши методы будут следовать модели tune( ) и будут соединятся только с интерфейсом базового класса.

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

 

Упражнение. Модифицировать проект Music:

·         class Woodwind extents Wind { }

 

Зачем нужно определять метод как final? Об этом написано в предыдущей главе, при помощи final осуществляется защита метода от переопределения. Возможно более важно или эффективно выключить динамическое связывание или сказать компилятору, что динамическое связывание не нужно. При этом компилятор может компилировать более эффективный код для элементов final. Однако в большинстве случаев не будет разницы в производительности вашей программы, так что лучше использовать final только как решение, принятое в угоду дизайну программы, а не для того, что бы повысить производительность.

 

 

Как происходит позднее связывание?   LateBinding.doc

 

 

 

Абстрактные методы и классы

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

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

Он создает основную форму, так что Вы можете сказать, что общего во всех дочерних классах. Другой путь сказать то же самое, это вызов Instrument абстрактного базового класса (или просто абстрактного класса).

Вы создаете абстрактный класс, когда Вы хотите управлять набором классов, через этот общий интерфейс. Все методы дочерних классов, совпадающие с объявлением сигнатуры базового класса, используют динамическое связывание. (Однако как было написано в предыдущей секции, если имя метода совпадает с именем метода базового класса, а аргументы различны, то это означает перегрузку, что обычно не то, что требуется.)

Если у Вас есть абстрактный класс типа Instrument, объекты этого класса всегда ничего не значат. Это означает, что Instrument является только интерфейсом, а не частным случаем реализации, так что создание объектов Instrument бессмысленно, и Вы вероятно хотели бы оградить пользователей от этой возможности.

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

Java предоставляет механизм для этого, называемый вызов абстрактного метода. Такой метод является не законченным; он имеет только объявление и не имеет тела метода. Ниже приведен синтаксис объявления абстрактного метода:

abstract void f();

Класс, содержащий абстрактные методы, называется абстрактным классом. Если класс содержит один или больше абстрактных методов, этот класс должен быть определен как abstract. (В противном случае компилятор выдаст сообщение об ошибке.)

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

Если Вы наследуете от абстрактного класса и Вы хотите создать объект нового типа, то Вы должны предоставить определения всех абстрактных методов базового класса.

Если же Вы этого не сделаете (а Вы можете решить не делать этого), то дочерний класса будет так же абстрактным и компилятор насильно установит модификатор abstract для этого класса.

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

Класс Instrument может быть с легкостью превращен в abstract класс. Только некоторые из методов будут abstract, поскольку создание абстрактного метода не требует от вас определение всех методов abstract. Здесь показано, на что это похоже:

TIJ217

Ниже пример с оркестром, модифицированный для использования abstract классов и методов:

//: c07:music4:Music4.java
// Абстрактные методы и классы.
import java.util.*;
 
abstract class Instrument {
  int i; // хранилище зарезервировано  для всех
  public abstract void play();
  public String what() {
    return "Instrument";
  }
  public abstract void adjust();
}
 
class Wind extends Instrument {
  public void play() {
    System.out.println("Wind.play()");
  }
  public String what() { return "Wind"; }
  public void adjust() {}
}
 
class Percussion extends Instrument {
  public void play() {
    System.out.println("Percussion.play()");
  }
  public String what() { return "Percussion"; }
  public void adjust() {}
}
 
class Stringed extends Instrument {
  public void play() {
    System.out.println("Stringed.play()");
  }
  public String what() { return "Stringed"; }
  public void adjust() {}
}
 
class Brass extends Wind {
  public void play() {
    System.out.println("Brass.play()");
  }
  public void adjust() { 
    System.out.println("Brass.adjust()");
  }
}
 
class Woodwind extends Wind {
  public void play() {
    System.out.println("Woodwind.play()");
  }
  public String what() { return "Woodwind"; }
}
 
public class Music4 {
  // Не беспокойтесь от типах, поскольку новые типы добавляемые
  // в систему, не мешают ей работать правильно:
  static void tune(Instrument i) {
    // ...
    i.play();
  }
  static void tuneAll(Instrument[] e) {
    for(int i = 0; i < e.length; i++)
      tune(e[i]);
  }
  public static void main(String[] args) {
    Instrument[] orchestra = new Instrument[5];
    int i = 0;
    // Приведение к базовому типу во время добавления в массив:
    orchestra[i++] = new Wind();
    orchestra[i++] = new Percussion();
    orchestra[i++] = new Stringed();
    orchestra[i++] = new Brass();
    orchestra[i++] = new Woodwind();
    tuneAll(orchestra);
  }
} ///:~

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

Так поступать очень удобно, создавая abstract классы и методы, потому что создается понятное и для пользователя и для компилятора намеренье об его использования.

 

Конструкторы и полиморфизм

 

 

Проектировка с наследованием

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

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

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

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

Следующий пример иллюстрирует это высказывание:

//: c07:Transmogrify.java
// Динамическое изменение поведения
// при композиции объекта.
 
abstract class Actor {
  abstract void act();
}
 
class HappyActor extends Actor {
  public void act() { 
    System.out.println("HappyActor"); 
  }
}
 
class SadActor extends Actor {
  public void act() { 
    System.out.println("SadActor");
  }
}
 
class Stage {
  Actor a = new HappyActor();
  void change() { a = new SadActor(); }
  void go() { a.act(); }
}
 
public class Transmogrify {
  public static void main(String[] args) {
    Stage s = new Stage();
    s.go(); // Выводит "HappyActor"
    s.change();
    s.go(); // Выводит "SadActor"
  }
} ///:~

Объект Stage содержит ссылку на Actor, которая проинициализирована на объект HappyActor. Это означает, что go( ) предоставляет специфическое поведение.

Но поскольку ссылка может быть перенаправлена на другой объект во время выполнения, то ссылка на объект SadActor может быть подставлена в a а затем посредством go( ) может быть изменена линия поведения.

Так Вы наживаетесь на динамическом изменении во время работы программы. (Это так же называется статический шаблон ОО проектирования (State Pattern). В противоположность, Вы не можете решить использовать наследование с различными типами в режиме выполнения, типы должны быть полностью определены на стадии компиляции.

Основная линия поведения при этом может быть выражена фразой "Используй наследование для выражения различия в поведении и поля для выражения различий в состояниях объектов".

В предыдущем примере использовались оба принципа из высказывания: два различных класса были наследованы для получения различий в методе act( ), а Stage использует композицию для изменения своего значения. В этом случае, такое изменение означает и изменение в поведении метода.