Nasljeđivanje nasuprot sastavu: Kako odabrati

Nasljeđivanje i kompozicija dvije su programske tehnike koje programeri koriste za uspostavljanje odnosa između klasa i objekata. Dok nasljeđivanje izvodi jednu klasu iz druge, kompozicija definira klasu kao zbroj njezinih dijelova.

Klase i objekti stvoreni nasljeđivanjem usko su povezani, jer promjena roditelja ili superklase u odnosu nasljeđivanja riskira razbijanje koda. Klase i objekti stvoreni kroz kompoziciju labavo su povezani , što znači da možete lakše mijenjati dijelove komponenata bez razbijanja koda.

Budući da slabo spojeni kod nudi veću fleksibilnost, mnogi su programeri naučili da je kompozicija bolja tehnika od nasljeđivanja, ali istina je složenija. Odabir programskog alata sličan je odabiru ispravnog kuhinjskog alata: Ne biste koristili nož za maslac za rezanje povrća, a na isti način ne biste trebali odabrati sastav za svaki scenarij programiranja. 

U ovom Java Challengeru naučit ćete razliku između nasljeđivanja i sastava i kako odlučiti koji je ispravan za vaš program. Dalje ću vas upoznati s nekoliko važnih, ali izazovnih aspekata nasljeđivanja Java: nadjačavanje metode, superključna riječ i lijevanje tipa. Konačno, testirat ćete ono što ste naučili radeći kroz primjer nasljeđivanja redak po redak kako biste utvrdili kakav treba biti izlaz.

Kada koristiti nasljedstvo u Javi

U objektno orijentiranom programiranju možemo koristiti nasljeđivanje kada znamo da postoji odnos "je" između djeteta i njegove roditeljske klase. Primjeri bi bili:

  • Osoba je čovjek.
  • Mačka je životinja.
  • Automobil je   vozilo.

U svakom je slučaju dijete ili podrazred specijalizirana verzija roditelja ili nadrazreda. Nasljeđivanje superklase primjer je ponovne upotrebe koda. Da biste bolje razumjeli ovaj odnos, odvojite trenutak da proučite Carrazred koji nasljeđuje od 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(); } } 

Kad razmišljate o nasljeđivanju, zapitajte se je li potklasa doista specijaliziranija verzija superklase. U ovom je slučaju automobil vrsta vozila, tako da nasljedni odnos ima smisla. 

Kada koristiti sastav u Javi

U objektno orijentiranom programiranju možemo koristiti sastav u slučajevima kada jedan objekt "ima" (ili je dio) drugog objekta. Primjeri bi bili:

  • Automobil ima bateriju (baterija je dio automobila).
  • Osoba ima srce (srce je dio osobe).
  • Kuća ima dnevni boravak (dnevni boravak je dio kuće).

Da biste bolje razumjeli ovu vrstu odnosa, razmotrite sastav 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 { } } 

U tom slučaju, znamo da kuća ima dnevni boravak i spavaću sobu, tako da možemo koristiti BedroomLivingRoomobjekte u pripravka a House

Uzmi kod

Nabavite izvorni kod za primjere u ovom Java Challengeru. Slijedeći primjere možete pokrenuti vlastite testove.

Nasljeđivanje vs sastav: Dva primjera

Razmotrite sljedeći kod. Je li ovo dobar primjer nasljeđivanja?

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

U ovom je slučaju odgovor negativan. Dijete klasa nasljeđuje mnoge metode koje nikada neće koristiti, što rezultira čvrsto povezanim kodom koji je i zbunjujući i težak za održavanje. Ako dobro pogledate, također je jasno da ovaj kôd ne prolazi test "je".

Pokušajmo sada na istom primjeru koristeći sastav:

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

Korištenje sastava za ovaj scenarij omogućuje  CharacterCompositionExampleklasi da koristi samo dvije HashSetmetode bez nasljeđivanja svih. To rezultira jednostavnijim, manje povezanim kodom koji će biti lakši za razumijevanje i održavanje.

Primjeri nasljeđivanja u JDK

Java Development Kit prepun je dobrih primjera nasljeđivanja:

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

Primijetite da je u svakom od ovih primjera podređena klasa specijalizirana verzija svog roditelja; na primjer, IndexOutOfBoundsExceptionje vrsta RuntimeException.

Nadjačavanje metode s nasljeđivanjem Java

Nasljeđivanje nam omogućuje ponovnu upotrebu metoda i drugih atributa jedne klase u novoj klasi, što je vrlo povoljno. Ali da bi nasljeđivanje stvarno funkcioniralo, također moramo biti u mogućnosti promijeniti dio naslijeđenog ponašanja u našoj novoj podklasi. Na primjer, možda bismo željeli specijalizirati zvuk koji Catproizvodi:

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

Ovo je primjer nasljeđivanja Java s nadjačavanjem metode. Prvo, mi proširiti na Animalklase za stvaranje nove Catklase. Zatim smo nadjačati u Animalklasi je emitSound()način da se specifični zvuk se Catčini. Iako smo tip klase proglasili kao Animal, kada ga napravimo kao instancu, Catdobit ćemo mačje mijaukanje. 

Nadjačavanje metode je polimorfizam

Možda se sjećate iz mog posljednjeg posta da je nadjačavanje metode primjer polimorfizma ili pozivanja virtualne metode.

Ima li Java višestruko nasljeđivanje?

Za razliku od nekih jezika, poput C ++, Java ne dopušta višestruko nasljeđivanje s klasama. Međutim, možete koristiti višestruko nasljeđivanje sa sučeljima. Razlika između klase i sučelja, u ovom slučaju, je u tome što sučelja ne zadržavaju stanje.

Ako pokušate višestruko nasljeđivanje kao što je navedeno u nastavku, kod se neće kompajlirati:

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

Rješenje pomoću klasa bilo bi nasljeđivanje jednog po jednog:

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

Drugo rješenje je zamjena klasa sučeljima:

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

Korištenje 'super' za pristup metodama roditeljskih klasa

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

Moguće je proglasiti a Dogsa supertipom Animal, ali ako želimo prizvati određenu metodu Dog, trebat ćemo je emitirati. Kao primjer, što ako bismo htjeli prizvati bark()metodu? AnimalSupertype nema načina da znam točno što životinja instancu smo doziva, tako da moramo cast Dogručno prije nego što možemo pozivaju na bark()način:

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

Također možete koristiti lijevanje bez dodjeljivanja objekta tipu klase. Ovaj pristup je zgodan kada ne želite deklarirati drugu varijablu:

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

Prihvatite izazov nasljeđivanja Java!

Naučili ste neke važne koncepte nasljeđivanja, pa je sada vrijeme da isprobate izazov nasljeđivanja. Za početak proučite sljedeći kod: