Vererbung versus Zusammensetzung: Wie man wählt

Vererbung und Komposition sind zwei Programmiertechniken, mit denen Entwickler Beziehungen zwischen Klassen und Objekten herstellen. Während die Vererbung eine Klasse von einer anderen ableitet, definiert die Komposition eine Klasse als die Summe ihrer Teile.

Durch Vererbung erstellte Klassen und Objekte sind eng miteinander verbunden, da das Ändern des übergeordneten Elements oder der Oberklasse in einer Vererbungsbeziehung die Gefahr birgt, dass Ihr Code beschädigt wird. Durch Komposition erstellte Klassen und Objekte sind lose miteinander verbunden , sodass Sie die Komponenten einfacher ändern können, ohne Ihren Code zu beschädigen.

Da lose gekoppelter Code mehr Flexibilität bietet, haben viele Entwickler gelernt, dass Komposition eine bessere Technik als Vererbung ist, aber die Wahrheit ist komplexer. Die Auswahl eines Programmierwerkzeugs ähnelt der Auswahl des richtigen Küchenwerkzeugs: Sie würden kein Buttermesser zum Schneiden von Gemüse verwenden, und auf die gleiche Weise sollten Sie nicht für jedes Programmierszenario eine Komposition auswählen. 

In diesem Java Challenger lernen Sie den Unterschied zwischen Vererbung und Komposition kennen und wie Sie entscheiden, welche für Ihr Programm richtig ist. Als Nächstes werde ich Ihnen einige wichtige, aber herausfordernde Aspekte der Java-Vererbung vorstellen: Überschreiben von Methoden, superSchlüsselwort und Typumwandlung. Schließlich testen Sie das Gelernte, indem Sie zeilenweise ein Vererbungsbeispiel durcharbeiten, um zu bestimmen, wie die Ausgabe aussehen soll.

Wann wird die Vererbung in Java verwendet?

Bei der objektorientierten Programmierung können wir die Vererbung verwenden, wenn wir wissen, dass zwischen einem Kind und seiner Elternklasse eine "Ist-Beziehung" besteht. Einige Beispiele wären:

  • Eine Person ist ein Mensch.
  • Eine Katze ist ein Tier.
  • Ein Auto ist ein   Fahrzeug.

In jedem Fall ist das Kind oder die Unterklasse eine spezielle Version des Elternteils oder der Oberklasse. Das Erben von der Oberklasse ist ein Beispiel für die Wiederverwendung von Code. Um diese Beziehung besser zu verstehen, nehmen Sie sich einen Moment Zeit, um die CarKlasse zu studieren , die erbt von 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(); } } 

Wenn Sie die Verwendung der Vererbung in Betracht ziehen, fragen Sie sich, ob die Unterklasse wirklich eine speziellere Version der Oberklasse ist. In diesem Fall ist ein Auto ein Fahrzeugtyp, daher ist die Vererbungsbeziehung sinnvoll. 

Wann wird die Komposition in Java verwendet?

In der objektorientierten Programmierung können wir Komposition in Fällen verwenden, in denen ein Objekt ein anderes Objekt "hat" (oder Teil davon ist). Einige Beispiele wären:

  • Ein Auto hat eine Batterie (eine Batterie ist Teil eines Autos).
  • Eine Person hat ein Herz (ein Herz ist Teil einer Person).
  • Ein Haus hat ein Wohnzimmer (ein Wohnzimmer ist Teil eines Hauses).

Um diese Art von Beziehung besser zu verstehen, betrachten Sie die Zusammensetzung von a 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 { } } 

In diesem Fall wissen wir, dass ein Haus ein Wohnzimmer und ein Schlafzimmer hat, so dass wir die Bedroomund  LivingRoomObjekte in der Komposition von a verwenden können House

Holen Sie sich den Code

Holen Sie sich den Quellcode für Beispiele in diesem Java Challenger. Sie können Ihre eigenen Tests ausführen, indem Sie den Beispielen folgen.

Vererbung gegen Zusammensetzung: Zwei Beispiele

Betrachten Sie den folgenden Code. Ist das ein gutes Beispiel für Vererbung?

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

In diesem Fall lautet die Antwort nein. Die untergeordnete Klasse erbt viele Methoden, die sie niemals verwenden wird, was zu eng gekoppeltem Code führt, der sowohl verwirrend als auch schwierig zu warten ist. Wenn Sie genau hinschauen, ist es auch klar, dass dieser Code den "ist ein" -Test nicht besteht.

Versuchen wir nun dasselbe Beispiel mit Komposition:

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

Durch die Verwendung der Komposition für dieses Szenario kann die  CharacterCompositionExampleKlasse nur zwei der HashSetMethoden verwenden, ohne alle zu erben. Dies führt zu einfacherem, weniger gekoppeltem Code, der leichter zu verstehen und zu warten ist.

Vererbungsbeispiele im JDK

Das Java Development Kit enthält viele gute Beispiele für die Vererbung:

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

Beachten Sie, dass in jedem dieser Beispiele die untergeordnete Klasse eine spezielle Version ihres übergeordneten Elements ist. ist zum Beispiel IndexOutOfBoundsExceptioneine Art von RuntimeException.

Überschreiben der Methode mit Java-Vererbung

Durch Vererbung können wir die Methoden und anderen Attribute einer Klasse in einer neuen Klasse wiederverwenden, was sehr praktisch ist. Damit die Vererbung wirklich funktioniert, müssen wir auch in der Lage sein, einige der vererbten Verhaltensweisen in unserer neuen Unterklasse zu ändern. Zum Beispiel möchten wir uns vielleicht auf den Sound spezialisieren, den a Catmacht:

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

Dies ist ein Beispiel für die Java-Vererbung mit überschriebener Methode. Zuerst erweitern wir die AnimalKlasse, um eine neue CatKlasse zu erstellen . Als nächstes überschreiben wir die Methode der AnimalKlasse emitSound(), um den spezifischen Sound zu erhalten, den die CatMarke erzeugt. Obwohl wir den Klassentyp als deklariert haben Animal, erhalten wir beim Instanziieren Catden Miau der Katze. 

Das Überschreiben von Methoden ist Polymorphismus

You might remember from my last post that method overriding is an example of polymorphism, or virtual method invocation.

Does Java have multiple inheritance?

Unlike some languages, such as C++, Java does not allow multiple inheritance with classes. You can use multiple inheritance with interfaces, however. The difference between a class and an interface, in this case, is that interfaces don't keep state.

If you attempt multiple inheritance like I have below, the code won't compile:

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

A solution using classes would be to inherit one-by-one:

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

Another solution is to replace the classes with interfaces:

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

Using ‘super' to access parent classes methods

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

Es ist möglich, a Dogmit dem Supertyp zu deklarieren Animal, aber wenn wir eine bestimmte Methode von aufrufen möchten Dog, müssen wir sie umwandeln. Was wäre als Beispiel, wenn wir die bark()Methode aufrufen wollten ? Der AnimalSupertyp kann nicht genau wissen, welche Tierinstanz wir aufrufen. Daher müssen wir Dogmanuell umwandeln, bevor wir die bark()Methode aufrufen können :

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

Sie können Casting auch verwenden, ohne das Objekt einem Klassentyp zuzuweisen. Dieser Ansatz ist praktisch, wenn Sie keine andere Variable deklarieren möchten:

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

Nehmen Sie die Java-Vererbungsherausforderung an!

Sie haben einige wichtige Konzepte der Vererbung gelernt. Jetzt ist es an der Zeit, eine Herausforderung für die Vererbung auszuprobieren. Studieren Sie zunächst den folgenden Code: