Dziedziczenie a skład: jak wybierać

Dziedziczenie i kompozycja to dwie techniki programowania używane przez programistów do ustanawiania relacji między klasami i obiektami. Podczas gdy dziedziczenie wywodzi jedną klasę z innej, kompozycja definiuje klasę jako sumę jej części.

Klasy i obiekty utworzone przez dziedziczenie są ze sobą ściśle powiązane, ponieważ zmiana klasy nadrzędnej lub nadrzędnej w relacji dziedziczenia grozi złamaniem kodu. Klasy i obiekty utworzone za pomocą kompozycji są luźno powiązane , co oznacza, że ​​można łatwiej zmieniać części składowe bez przerywania kodu.

Ponieważ luźno powiązany kod zapewnia większą elastyczność, wielu programistów nauczyło się, że kompozycja jest lepszą techniką niż dziedziczenie, ale prawda jest bardziej złożona. Wybór narzędzia programistycznego jest podobny do wyboru odpowiedniego narzędzia kuchennego: nie używałbyś noża do masła do krojenia warzyw i tak samo nie powinieneś wybierać kompozycji dla każdego scenariusza programowania. 

W tym programie Java Challenger poznasz różnicę między dziedziczeniem a kompozycją oraz jak zdecydować, który program jest prawidłowy. Następnie przedstawię kilka ważnych, ale trudnych aspektów dziedziczenia w języku Java: przesłanianie metod, supersłowo kluczowe i rzutowanie typów. Na koniec przetestujesz to, czego się nauczyłeś, pracując nad przykładem dziedziczenia wiersz po wierszu, aby określić, jakie powinny być dane wyjściowe.

Kiedy używać dziedziczenia w Javie

W programowaniu obiektowym możemy używać dziedziczenia, gdy wiemy, że istnieje relacja „istnieje” między dzieckiem a jego klasą nadrzędną. Oto kilka przykładów:

  • Człowiek jest człowiekiem.
  • Kot to zwierzę.
  • Samochód to   pojazd.

W każdym przypadku klasa podrzędna lub podklasa jest wyspecjalizowaną wersją klasy nadrzędnej lub nadklasy. Dziedziczenie z nadklasy jest przykładem ponownego wykorzystania kodu. Aby lepiej zrozumieć ten związek, poświęć chwilę na przestudiowanie Carklasy, która dziedziczy po 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(); } } 

Kiedy rozważasz zastosowanie dziedziczenia, zadaj sobie pytanie, czy podklasa jest rzeczywiście bardziej wyspecjalizowaną wersją nadklasy. W tym przypadku samochód jest rodzajem pojazdu, więc związek dziedziczenia ma sens. 

Kiedy używać kompozycji w Javie

W programowaniu obiektowym możemy używać kompozycji w przypadkach, gdy jeden obiekt „ma” (lub jest częścią) innego obiektu. Oto kilka przykładów:

  • Samochód ma akumulator (akumulator jest częścią samochodu).
  • Człowiek ma serce (serce jest częścią osoby).
  • Dom składa się z salonu (pokój dzienny jest częścią domu).

Aby lepiej zrozumieć ten typ relacji, rozważ skład 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 { } } 

W tym przypadku wiemy, że dom ma salon i sypialnię, więc możemy wykorzystać obiekty BedroomLivingRoomw kompozycji House

Zdobądź kod

Pobierz kod źródłowy przykładów w tym Java Challenger. Możesz przeprowadzić własne testy, postępując zgodnie z przykładami.

Dziedziczenie a skład: dwa przykłady

Rozważmy następujący kod. Czy to dobry przykład dziedziczenia?

 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); } 

W tym przypadku odpowiedź brzmi: nie. Klasa potomna dziedziczy wiele metod, których nigdy nie użyje, co powoduje powstanie ściśle powiązanego kodu, który jest zarówno zagmatwany, jak i trudny do utrzymania. Jeśli przyjrzysz się uważnie, zobaczysz również, że ten kod nie przejdzie testu „jest”.

Teraz spróbujmy tego samego przykładu z użyciem kompozycji:

 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); } 

Użycie kompozycji w tym scenariuszu umożliwia  CharacterCompositionExampleklasie użycie tylko dwóch z HashSetmetod, bez dziedziczenia ich wszystkich. Powoduje to prostszy, mniej powiązany kod, który będzie łatwiejszy do zrozumienia i utrzymania.

Przykłady dziedziczenia w JDK

Zestaw Java Development Kit jest pełen dobrych przykładów dziedziczenia:

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

Zauważ, że w każdym z tych przykładów klasa potomna jest wyspecjalizowaną wersją swojego rodzica; na przykład IndexOutOfBoundsExceptionjest typem RuntimeException.

Nadpisywanie metody z dziedziczeniem Java

Dziedziczenie pozwala nam ponownie używać metod i innych atrybutów jednej klasy w nowej klasie, co jest bardzo wygodne. Ale aby dziedziczenie naprawdę działało, musimy także mieć możliwość zmiany niektórych dziedziczonych zachowań w naszej nowej podklasie. Na przykład możemy chcieć wyspecjalizować dźwięk, jaki Catwytwarza:

 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(); } } 

To jest przykład dziedziczenia Java z przesłanianiem metod. Po pierwsze, możemy rozszerzyć na Animalklasę, aby utworzyć nową Catklasę. Następnie możemy zastąpić przez Animalklasę za emitSound()sposób, aby uzyskać specyficzny dźwięk tych Catmarek. Mimo że zadeklarowaliśmy typ klasy jako Animal, kiedy tworzymy jej instancję, otrzymamy miauczenie Catkota. 

Nadrzędną metodą jest polimorfizm

Być może pamiętasz z mojego ostatniego posta, że ​​przesłanianie metody jest przykładem polimorfizmu lub wywołania metody wirtualnej.

Czy Java ma wielokrotne dziedziczenie?

W przeciwieństwie do niektórych języków, takich jak C ++, Java nie pozwala na wielokrotne dziedziczenie z klasami. W przypadku interfejsów można jednak korzystać z dziedziczenia wielokrotnego. Różnica między klasą a interfejsem w tym przypadku polega na tym, że interfejsy nie zachowują stanu.

Jeśli spróbujesz wielokrotnego dziedziczenia, tak jak mam poniżej, kod się nie skompiluje:

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

Rozwiązaniem wykorzystującym klasy byłoby dziedziczenie jedna po drugiej:

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

Innym rozwiązaniem jest zastąpienie klas interfejsami:

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

Używanie „super” do uzyskiwania dostępu do metod klas nadrzędnych

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

Możliwe jest zadeklarowanie a Dogz nadtypem Animal, ale jeśli chcemy wywołać określoną metodę z Dog, będziemy musieli ją rzucić. Na przykład, co by było, gdybyśmy chcieli wywołać bark()metodę? AnimalSupertypem nie ma sposobu, aby wiedzieć dokładnie, co instancji zwierząt jesteśmy powołując się, więc mamy do obsady Dogręcznie, zanim będzie można powołać się na bark()metodę:

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

Możesz także użyć rzutowania bez przypisywania obiektu do typu klasy. Takie podejście jest przydatne, gdy nie chcesz deklarować innej zmiennej:

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

Podejmij wyzwanie dziedziczenia Java!

Nauczyłeś się kilku ważnych koncepcji dziedziczenia, więc teraz nadszedł czas, aby wypróbować wyzwanie dziedziczenia. Aby rozpocząć, przestudiuj następujący kod: