Наследяване срещу състав: Как да изберем

Наследяването и композицията са две техники за програмиране, които разработчиците използват, за да установят връзки между класове и обекти. Докато наследяването извлича един клас от друг, съставът определя клас като сбор от неговите части.

Класовете и обектите, създадени чрез наследяване, са тясно свързани, тъй като промяната на родителя или суперкласа в връзка за наследяване рискува да счупи вашия код. Класовете и обектите, създадени чрез композиция, са свободно свързани , което означава, че можете по-лесно да променяте съставните части, без да нарушавате кода си.

Тъй като слабо свързаният код предлага по-голяма гъвкавост, много разработчици са научили, че композицията е по-добра техника от наследяването, но истината е по-сложна. Изборът на инструмент за програмиране е подобен на избора на правилния кухненски инструмент: Не бихте използвали маслен нож за нарязване на зеленчуци и по същия начин не бива да избирате състав за всеки сценарий на програмиране. 

В този Java Challenger ще научите разликата между наследяването и състава и как да решите кое е правилно за вашата програма. След това ще ви запозная с няколко важни, но предизвикателни аспекта на наследяването на Java: заместване на метода, superключова дума и отливане на тип. И накрая, ще тествате наученото, като работите през пример за наследяване ред по ред, за да определите какъв трябва да бъде изходът.

Кога да се използва наследяване в Java

В обектно-ориентираното програмиране можем да използваме наследяване, когато знаем, че има връзка "е" между дете и неговия родителски клас. Някои примери ще бъдат:

  • Човек е човек.
  • Котката е животно.
  • Колата е   превозно средство.

Във всеки случай детето или подкласът е специализирана версия на родителя или суперкласа. Наследяването от суперкласа е пример за повторно използване на код. За да разберете по-добре тази връзка, отделете малко време за изучаване на Carкласа, който наследява от Vehicle:

 class Vehicle { String brand; String color; double weight; double speed; void move() { System.out.println("The vehicle is moving"); } } public class Car extends Vehicle { String licensePlateNumber; String owner; String bodyStyle; public static void main(String... inheritanceExample) { System.out.println(new Vehicle().brand); System.out.println(new Car().brand); new Car().move(); } } 

Когато обмисляте да използвате наследяване, запитайте се дали наистина подкласът е по-специализирана версия на суперкласа. В този случай автомобилът е вид превозно средство, така че наследствените отношения имат смисъл. 

Кога да се използва композиция в Java

В обектно-ориентираното програмиране можем да използваме композиция в случаите, когато един обект „има“ (или е част от) друг обект. Някои примери ще бъдат:

  • Колата има батерия (батерията е част от автомобила).
  • Човек има сърце (сърцето е част от човека).
  • Къщата има хол (всекидневната е част от къща).

За да разберете по-добре този тип взаимоотношения, помислете за състава на House:

 public class CompositionExample { public static void main(String... houseComposition) { new House(new Bedroom(), new LivingRoom()); // The house now is composed with a Bedroom and a LivingRoom } static class House { Bedroom bedroom; LivingRoom livingRoom; House(Bedroom bedroom, LivingRoom livingRoom) { this.bedroom = bedroom; this.livingRoom = livingRoom; } } static class Bedroom { } static class LivingRoom { } } 

В този случай знаем, че къщата има хол и спалня, така че можем да използваме Bedroomи  LivingRoomобектите в състава на a House

Вземете кода

Вземете изходния код за примери в този Java Challenger. Можете да провеждате свои собствени тестове, докато следвате примерите.

Наследяване срещу състав: Два примера

Обмислете следния код. Това добър пример за наследство ли е?

 import java.util.HashSet; public class CharacterBadExampleInheritance extends HashSet { public static void main(String... badExampleOfInheritance) { BadExampleInheritance badExampleInheritance = new BadExampleInheritance(); badExampleInheritance.add("Homer"); badExampleInheritance.forEach(System.out::println); } 

В този случай отговорът е отрицателен. Детският клас наследява много методи, които никога няма да използва, което води до плътно свързан код, който е едновременно объркващ и труден за поддръжка. Ако се вгледате внимателно, също така е ясно, че този код не преминава теста „е“.

Сега нека опитаме същия пример, използвайки състав:

 import java.util.HashSet; import java.util.Set; public class CharacterCompositionExample { static Set set = new HashSet(); public static void main(String... goodExampleOfComposition) { set.add("Homer"); set.forEach(System.out::println); } 

Използването на композиция за този сценарий позволява на  CharacterCompositionExampleкласа да използва само два от HashSetметодите на, без да наследява всички от тях. Това води до по-опростен, по-малко свързан код, който ще бъде по-лесен за разбиране и поддръжка.

Примери за наследяване в JDK

Комплектът за разработка на Java е пълен с добри примери за наследяване:

 class IndexOutOfBoundsException extends RuntimeException {...} class ArrayIndexOutOfBoundsException extends IndexOutOfBoundsException {...} class FileWriter extends OutputStreamWriter {...} class OutputStreamWriter extends Writer {...} interface Stream extends BaseStream
    
      {...} 
    

Забележете, че във всеки от тези примери класът дъщер е специализирана версия на своя родител; например, IndexOutOfBoundsExceptionе вид RuntimeException.

Замяна на метода с наследяване на Java

Наследяването ни позволява да използваме повторно методите и другите атрибути на един клас в нов клас, което е много удобно. Но за да може наследството наистина да работи, ние също трябва да можем да променим някои от наследственото поведение в новия ни подклас. Например, може да искаме да специализираме звука, който Catправи:

 class Animal { void emitSound() { System.out.println("The animal emitted a sound"); } } class Cat extends Animal { @Override void emitSound() { System.out.println("Meow"); } } class Dog extends Animal { } public class Main { public static void main(String... doYourBest) { Animal cat = new Cat(); // Meow Animal dog = new Dog(); // The animal emitted a sound Animal animal = new Animal(); // The animal emitted a sound cat.emitSound(); dog.emitSound(); animal.emitSound(); } } 

Това е пример за наследяване на Java с заместване на метода. Първо, ние се разшири на Animalкласа да се създаде нов Catклас. След това заместваме метода на Animalкласа, за emitSound()да получим конкретния звук, който Catпроизвеждат. Въпреки че сме декларирали типа на класа като Animal, когато го създадем, Catще получим мяукането на котката. 

Отменянето на метода е полиморфизъм

Може би си спомняте от последния ми пост, че заменянето на метода е пример за полиморфизъм или виртуално извикване на метод.

Java има ли множествено наследяване?

За разлика от някои езици, като C ++, Java не позволява многократно наследяване с класове. Можете обаче да използвате множествено наследяване с интерфейси. Разликата между клас и интерфейс, в този случай, е, че интерфейсите не поддържат състояние.

Ако се опитате с множествено наследяване, както имам по-долу, кодът няма да се компилира:

 class Animal {} class Mammal {} class Dog extends Animal, Mammal {} 

Решение, използващо класове, би било наследяването едно по едно:

 class Animal {} class Mammal extends Animal {} class Dog extends Mammal {} 

Друго решение е да замените класовете с интерфейси:

 interface Animal {} interface Mammal {} class Dog implements Animal, Mammal {} 

Използване на „super“ за достъп до методите на родителски класове

When two classes are related through inheritance, the child class must be able to access every accessible field, method, or constructor of its parent class. In Java, we use the reserved word super to ensure the child class can still access its parent's overridden method:

 public class SuperWordExample { class Character { Character() { System.out.println("A Character has been created"); } void move() { System.out.println("Character walking..."); } } class Moe extends Character { Moe() { super(); } void giveBeer() { super.move(); System.out.println("Give beer"); } } } 

In this example, Character is the parent class for Moe.  Using super, we are able to access Character's  move() method in order to give Moe a beer.

Using constructors with inheritance

When one class inherits from another, the superclass's constructor always will be loaded first, before loading its subclass. In most cases, the reserved word super will be added automatically to the constructor.  However, if the superclass has a parameter in its constructor, we will have to deliberately invoke the super constructor, as shown below:

 public class ConstructorSuper { class Character { Character() { System.out.println("The super constructor was invoked"); } } class Barney extends Character { // No need to declare the constructor or to invoke the super constructor // The JVM will to that } } 

If the parent class has a constructor with at least one parameter, then we must declare the constructor in the subclass and use super to explicitly invoke the parent constructor. The super reserved word won't be added automatically and the code won't compile without it.  For example:

 public class CustomizedConstructorSuper { class Character { Character(String name) { System.out.println(name + "was invoked"); } } class Barney extends Character { // We will have compilation error if we don't invoke the constructor explicitly // We need to add it Barney() { super("Barney Gumble"); } } } 

Type casting and the ClassCastException

Casting is a way of explicitly communicating to the compiler that you really do intend to convert a given type.  It's like saying, "Hey, JVM, I know what I'm doing so please cast this class with this type." If a class you've cast isn't compatible with the class type you declared, you will get a ClassCastException.

In inheritance, we can assign the child class to the parent class without casting but we can't assign a parent class to the child class without using casting.

Consider the following example:

 public class CastingExample { public static void main(String... castingExample) { Animal animal = new Animal(); Dog dogAnimal = (Dog) animal; // We will get ClassCastException Dog dog = new Dog(); Animal dogWithAnimalType = new Dog(); Dog specificDog = (Dog) dogWithAnimalType; specificDog.bark(); Animal anotherDog = dog; // It's fine here, no need for casting System.out.println(((Dog)anotherDog)); // This is another way to cast the object } } class Animal { } class Dog extends Animal { void bark() { System.out.println("Au au"); } } 

When we try to cast an Animal instance to a Dog we get an exception. This is because the Animal doesn't know anything about its child. It could be a cat, a bird, a lizard, etc. There is no information about the specific animal. 

The problem in this case is that we've instantiated Animal like this:

 Animal animal = new Animal(); 

Then tried to cast it like this:

 Dog dogAnimal = (Dog) animal; 

Because we don't have a Dog instance, it's impossible to assign an Animal to the Dog.  If we try, we will get a ClassCastException

In order to avoid the exception, we should instantiate the Dog like this:

 Dog dog = new Dog(); 

then assign it to Animal:

 Animal anotherDog = dog; 

In this case, because  we've extended the Animal class, the Dog instance doesn't even need to be cast; the Animal parent class type simply accepts the assignment.

Casting with supertypes

Възможно е да декларираме a Dogс супертип Animal, но ако искаме да извикаме конкретен метод от Dog, ще трябва да го хвърлим. Като пример, какво, ако искаме да извикаме bark()метода? В Animalподтип на няма как да се знае точно какво животно например сме, която се позовава, така че ние трябва да се отливка Dogръчно, преди да можем да се позове на bark()метода:

 Animal dogWithAnimalType = new Dog(); Dog specificDog = (Dog) dogWithAnimalType; specificDog.bark(); 

Можете също да използвате кастинг, без да присвоявате обекта на тип клас. Този подход е удобен, когато не искате да декларирате друга променлива:

 System.out.println(((Dog)anotherDog)); // This is another way to cast the object 

Приемете предизвикателството за наследяване на Java!

Научихте някои важни концепции за наследяване, така че сега е време да изпробвате предизвикателство за наследство. За да започнете, изучете следния код: