Overerving versus compositie: hoe te kiezen

Overerving en compositie zijn twee programmeertechnieken die ontwikkelaars gebruiken om relaties tussen klassen en objecten tot stand te brengen. Terwijl overerving de ene klasse van de andere afleidt, definieert compositie een klasse als de som der delen.

Klassen en objecten die door overerving zijn gemaakt, zijn nauw met elkaar verbonden, omdat het wijzigen van de bovenliggende of superklasse in een overervingsrelatie het risico loopt uw ​​code te breken. Klassen en objecten die door compositie zijn gemaakt, zijn losjes gekoppeld , wat betekent dat u de samenstellende delen gemakkelijker kunt wijzigen zonder uw code te breken.

Omdat losjes gekoppelde code meer flexibiliteit biedt, hebben veel ontwikkelaars geleerd dat compositie een betere techniek is dan overerving, maar de waarheid is complexer. Het kiezen van een programmeertool is vergelijkbaar met het kiezen van het juiste keukengereedschap: je zou geen botermes gebruiken om groenten te snijden, en op dezelfde manier zou je niet voor elk programmeerscenario een compositie moeten kiezen. 

In deze Java Challenger leer je het verschil tussen overerving en compositie en hoe je kunt beslissen welke de juiste is voor jouw programma. Vervolgens laat ik je kennismaken met verschillende belangrijke maar uitdagende aspecten van Java-overerving: het overschrijven van methoden, het supersleutelwoord en het casten van typen. Ten slotte test je wat je hebt geleerd door regel voor regel een overervingsvoorbeeld te doorlopen om te bepalen wat de output zou moeten zijn.

Wanneer overerving in Java gebruiken

Bij objectgeoriënteerd programmeren kunnen we overerving gebruiken als we weten dat er een "is een" -relatie is tussen een kind en zijn bovenliggende klasse. Enkele voorbeelden zijn:

  • Een persoon is een mens.
  • Een kat is een dier.
  • Een auto is een   voertuig.

In elk geval is het kind of de subklasse een gespecialiseerde versie van de ouder- of superklasse. Overnemen van de superklasse is een voorbeeld van hergebruik van code. Om deze relatie beter te begrijpen, neemt u even de tijd om de Carklas te bestuderen , die erft van 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(); } } 

Als u overweegt om overerving te gebruiken, vraag uzelf dan af of de subklasse echt een meer gespecialiseerde versie is van de superklasse. In dit geval is een auto een type voertuig, dus de erfenisrelatie is logisch. 

Wanneer compositie in Java gebruiken

Bij objectgeoriënteerd programmeren kunnen we compositie gebruiken in gevallen waarin een object "heeft" (of deel uitmaakt van) een ander object. Enkele voorbeelden zijn:

  • Een auto heeft een accu (een accu is onderdeel van een auto).
  • Een persoon heeft een hart (een hart is een onderdeel van een persoon).
  • Een huis heeft een woonkamer (een woonkamer is onderdeel van een huis).

Om dit type relatie beter te begrijpen, kunt u de samenstelling van een 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 dit geval weten we dat een huis heeft een woonkamer en een slaapkamer, zodat we het kunnen gebruiken Bedroomen  LivingRoomobjecten in de samenstelling van een House

Verkrijg de code

Haal de broncode op voor voorbeelden in deze Java Challenger. U kunt uw eigen tests uitvoeren terwijl u de voorbeelden volgt.

Overerving versus compositie: twee voorbeelden

Beschouw de volgende code. Is dit een goed voorbeeld van overerving?

 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 dit geval is het antwoord nee. De kindklasse erft veel methoden die hij nooit zal gebruiken, wat resulteert in nauw gekoppelde code die zowel verwarrend als moeilijk te onderhouden is. Als je goed kijkt, is het ook duidelijk dat deze code de "is a" -test niet doorstaat.

Laten we nu hetzelfde voorbeeld proberen met compositie:

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

Door compositie voor dit scenario te gebruiken, kan de  CharacterCompositionExampleklas slechts twee van HashSetde methoden gebruiken, zonder ze allemaal te erven. Dit resulteert in eenvoudigere, minder gekoppelde code die gemakkelijker te begrijpen en te onderhouden is.

Overervingsvoorbeelden in de JDK

De Java Development Kit staat vol met goede voorbeelden van overerving:

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

Merk op dat in elk van deze voorbeelden de kindklasse een gespecialiseerde versie is van de ouder; is bijvoorbeeld IndexOutOfBoundsExceptioneen type RuntimeException.

Methode overschrijven met Java-overerving

Inheritance allows us to reuse the methods and other attributes of one class in a new class, which is very convenient.  But for inheritance to really work, we also need to be able to change some of the inherited behavior within our new subclass.  For instance, we might want to specialize the sound a Cat makes:

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

This is an example of Java inheritance with method overriding. First, we extend the Animal class to create a new Cat class. Next, we override the Animal class's emitSound() method to get the specific sound the Cat makes. Even though we've declared the class type as Animal, when we instantiate it as Cat we will get the cat's meow. 

Method overriding is polymorphism

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

Het is mogelijk om een ​​te declareren Dogmet het supertype Animal, maar als we een specifieke methode van willen aanroepen Dog, moeten we deze casten. Wat als we bijvoorbeeld de bark()methode willen gebruiken? Het Animalsupertype kan niet precies weten welke diereninstantie we aanroepen, dus we moeten Doghandmatig casten voordat we de bark()methode kunnen aanroepen :

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

U kunt ook casten gebruiken zonder het object aan een klassetype toe te wijzen. Deze benadering is handig als u geen andere variabele wilt declareren:

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

Ga de Java-overervingsuitdaging aan!

Je hebt een aantal belangrijke concepten van overerving geleerd, dus nu is het tijd om een ​​overervingsuitdaging uit te proberen. Bestudeer om te beginnen de volgende code: