Arv kontra komposition: Hur man väljer

Arv och komposition är två programmeringstekniker som utvecklare använder för att skapa relationer mellan klasser och objekt. Medan arv härrör en klass från en annan definierar komposition en klass som summan av dess delar.

Klasser och objekt som skapas genom arv är tätt kopplade eftersom att ändra föräldern eller superklassen i ett arvsförhållande riskerar att bryta din kod. Klasser och objekt som skapas genom komposition är löst kopplade , vilket innebär att du lättare kan ändra komponentdelarna utan att bryta din kod.

Eftersom löst kopplad kod erbjuder mer flexibilitet har många utvecklare lärt sig att komposition är en bättre teknik än arv, men sanningen är mer komplex. Att välja ett programmeringsverktyg liknar att välja rätt köksverktyg: Du skulle inte använda en smörkniv för att skära grönsaker, och på samma sätt borde du inte välja komposition för varje programmeringsscenario. 

I denna Java Challenger lär du dig skillnaden mellan arv och komposition och hur du bestämmer vilken som är rätt för ditt program. Därefter introducerar jag dig till flera viktiga men utmanande aspekter av Java-arv: metodöverstyrning, supernyckelord och typgjutning. Slutligen testar du vad du har lärt dig genom att arbeta igenom ett arvsexempel rad för rad för att avgöra vad resultatet ska vara.

När ska du använda arv i Java

I objektorienterad programmering kan vi använda arv när vi vet att det finns en "är en" relation mellan ett barn och dess föräldraklass. Några exempel kan vara:

  • En person är en människa.
  • En katt är ett djur.
  • En bil är ett   fordon.

I båda fallen är barnet eller underklassen en specialversion av föräldern eller superklassen. Att ärva från superklassen är ett exempel på kodåteranvändning. För att bättre förstå detta förhållande, ta en stund att studera Carklassen, som ärver från 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(); } } 

När du funderar på att använda arv, fråga dig själv om underklassen verkligen är en mer specialiserad version av superklassen. I det här fallet är en bil en typ av fordon, så arvsförhållandet är vettigt. 

När ska du använda komposition i Java

I objektorienterad programmering kan vi använda komposition i fall där ett objekt "har" (eller ingår i) ett annat objekt. Några exempel kan vara:

  • En bil har ett batteri (ett batteri är en del av en bil).
  • En person har ett hjärta (ett hjärta är en del av en person).
  • Ett hus har ett vardagsrum (ett vardagsrum är en del av ett hus).

För att bättre förstå denna typ av relation, överväga sammansättningen av en 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 { } } 

I det här fallet vet vi att ett hus har ett vardagsrum och ett sovrum, så att vi kan använda Bedroomoch  LivingRoomföremål i sammansättningen av en House

Hämta koden

Hämta källkoden för exempel i denna Java Challenger. Du kan köra dina egna tester medan du följer exemplen.

Arv vs komposition: Två exempel

Tänk på följande kod. Är detta ett bra exempel på arv?

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

I det här fallet är svaret nej. Barnklassen ärver många metoder som den aldrig kommer att använda, vilket resulterar i tätt kopplad kod som är både förvirrande och svår att underhålla. Om du tittar noga är det också tydligt att den här koden inte klarar testet "är ett".

Låt oss nu prova samma exempel med 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); } 

Med hjälp av komposition för detta scenario kan  CharacterCompositionExampleklassen använda bara två av HashSetmetoderna utan att ärva dem alla. Detta resulterar i enklare, mindre kopplad kod som blir lättare att förstå och underhålla.

Arvsexempel i JDK

Java Development Kit är fullt av bra exempel på arv:

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

Observera att i varje av dessa exempel är barnklassen en specialversion av sin förälder; till exempel IndexOutOfBoundsExceptionär en typ av RuntimeException.

Metod åsidosättande med Java-arv

Arv tillåter oss att återanvända metoderna och andra attribut för en klass i en ny klass, vilket är mycket bekvämt. Men för att arv verkligen ska fungera måste vi också kunna ändra något av det ärvda beteendet inom vår nya underklass. Till exempel kanske vi vill specialisera ljudet som Catgör:

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

Detta är ett exempel på Java-arv med metodöverstyrning. Först vi utöka den Animalklassen för att skapa en ny Catklass. Därefter vi åsidosätta den Animalklassens emitSound()metod för att få den specifika ljuda Catfabrikat. Även om vi har förklarat klasstypen som Animal, när vi instantierar det så får Catvi kattens meow. 

Metod åsidosättande är polymorfism

Du kanske kommer ihåg från mitt senaste inlägg att metodöverstyrning är ett exempel på polymorfism eller virtuell metodinropning.

Har Java flera arv?

Till skillnad från vissa språk, till exempel C ++, tillåter Java inte flera arv med klasser. Du kan dock använda flera arv med gränssnitt. Skillnaden mellan en klass och ett gränssnitt, i det här fallet, är att gränssnitt inte håller tillstånd.

Om du försöker flera arv som jag har nedan kommer koden inte att kompileras:

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

En lösning som använder klasser är att ärva en efter en:

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

En annan lösning är att ersätta klasserna med gränssnitt:

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

Använda 'super' för att komma åt föräldraklasser

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

Det är möjligt att deklarera a Dogmed supertypen Animal, men om vi vill åberopa en specifik metod från Dogmåste vi kasta den. Som ett exempel, tänk om vi ville åberopa bark()metoden? Den AnimalSupertype har ingen möjlighet att veta exakt vilka djur instans vi åberopar, så vi måste kasta Dogmanuellt innan vi kan åberopa bark()metod:

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

Du kan också använda casting utan att tilldela objektet till en klasstyp. Detta tillvägagångssätt är praktiskt när du inte vill deklarera en annan variabel:

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

Ta Java-arvsutmaningen!

Du har lärt dig några viktiga begrepp med arv, så nu är det dags att prova en arvsmässig utmaning. För att börja, studera följande kod: