Programmering

Arv kontra komposisjon: Hvordan velge

Arv og komposisjon er to programmeringsteknikker som utviklere bruker for å etablere forhold mellom klasser og objekter. Mens arv stammer en klasse fra en annen, definerer sammensetning en klasse som summen av delene.

Klasser og gjenstander opprettet gjennom arv er sammensveiset fordi det å bytte foreldre eller superklasse i et arveforhold risikerer å bryte koden din. Klasser og objekter opprettet gjennom komposisjon er løst koblet, noe som betyr at du lettere kan endre komponentene uten å bryte koden.

Fordi løst koblet kode gir mer fleksibilitet, har mange utviklere lært at komposisjon er en bedre teknikk enn arv, men sannheten er mer kompleks. Å velge et programmeringsverktøy ligner på å velge riktig kjøkkenverktøy: Du vil ikke bruke en smørkniv til å kutte grønnsaker, og på samme måte bør du ikke velge komposisjon for hvert programmeringsscenario.

I denne Java Challenger lærer du forskjellen mellom arv og sammensetning og hvordan du bestemmer hvilket som er riktig for programmet ditt. Deretter vil jeg introdusere deg for flere viktige, men utfordrende aspekter ved Java-arv: metodeoverstyring, super nøkkelord og type avstøpning. Til slutt vil du teste det du har lært ved å jobbe gjennom et arveeksempel linje for linje for å bestemme hva utdataene skal være.

Når skal du bruke arv i Java

I objektorientert programmering kan vi bruke arv når vi vet at det er et "er et" forhold mellom et barn og dets overordnede klasse. Noen eksempler kan være:

  • En person er en menneskelig.
  • En katt er en dyr.
  • En bil er en kjøretøy.

I begge tilfeller er barnet eller underklassen en spesialisert versjon av foreldrene eller superklassen. Arving fra superklassen er et eksempel på gjenbruk av kode. For å bedre forstå dette forholdet, ta deg tid til å studere Bil klasse, som arver fra Kjøretøy:

 klasse Kjøretøy {String merkevare; Strengfarge; dobbel vekt; dobbel hastighet; void move () {System.out.println ("Kjøretøyet beveger seg"); }} public class Car extends Vehicle {String licensePlateNumber; Strengeier; String bodyStyle; public static void main (String ... inheritanceExample) {System.out.println (new Vehicle (). brand); System.out.println (ny bil (). Merkevare); ny bil (). flytt (); }} 

Når du vurderer å bruke arv, kan du spørre deg selv om underklassen virkelig er en mer spesialversjon av superklassen. I dette tilfellet er en bil en type kjøretøy, så arveforholdet gir mening.

Når skal du bruke komposisjon i Java

I objektorientert programmering kan vi bruke komposisjon i tilfeller der ett objekt "har" (eller er en del av) et annet objekt. Noen eksempler kan være:

  • En bil har en batteri (et batteri Er del av en bil).
  • En person har en hjerte (et hjerte Er del av en person).
  • Et hus har en stue (en stue Er del av et hus).

For å forstå denne typen forhold bedre, bør du vurdere sammensetningen av en Hus:

 public class CompositionExample {public static void main (String ... houseComposition) {new House (new Bedroom (), new LivingRoom ()); // Huset er nå sammensatt med et soverom og et LivingRoom} statisk klassehus {Soverom; LivingRoom livingRoom; Hus (Soverom, Soverom, LivingRoom livingRoom) {this.bedroom = soverom; this.livingRoom = livingRoom; }} statisk klasse soverom {} statisk klasse LivingRoom {}} 

I dette tilfellet vet vi at et hus har en stue og et soverom, slik at vi kan bruke Soverom og Stue objekter i sammensetningen av en Hus

Få koden

Få kildekoden for eksempler i denne Java Challenger. Du kan kjøre dine egne tester mens du følger eksemplene.

Arv vs komposisjon: To eksempler

Vurder følgende kode. Er dette et godt eksempel på arv?

 importere java.util.HashSet; public class CharacterBadExampleInheritance utvider HashSet {public static void main (String ... badExampleOfInheritance) {BadExampleInheritance badExampleInheritance = new BadExampleInheritance (); badExampleInheritance.add ("Homer"); badExampleInheritance.forEach (System.out :: println); } 

I dette tilfellet er svaret nei. Barneklassen arver mange metoder som den aldri vil bruke, noe som resulterer i tett koblet kode som er både forvirrende og vanskelig å vedlikeholde. Hvis du ser nøye etter, er det også klart at denne koden ikke består "er en" -testen.

La oss nå prøve det samme eksemplet ved hjelp av komposisjon:

 importere java.util.HashSet; importere 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); } 

Bruk av komposisjon for dette scenariet tillater CharacterCompositionExample klasse å bruke bare to av HashSetmetoder, uten å arve dem alle. Dette resulterer i enklere, mindre koblet kode som blir lettere å forstå og vedlikeholde.

Arveeksempler i JDK

Java Development Kit er fullt av gode eksempler på arv:

 klasse IndexOutOfBoundsException utvider RuntimeException {...} klasse ArrayIndexOutOfBoundsException utvider IndexOutOfBoundsException {...} klasse FileWriter utvider OutputStreamWriter {...} klasse OutputStreamWriter utvider Writer {...} grensesnitt Stream utvider BaseStream {...} 

Legg merke til at i hvert av disse eksemplene er barneklassen en spesialversjon av foreldrene; for eksempel, IndexOutOfBoundsException er en type RuntimeException.

Metodeoverstyring med Java-arv

Arv lar oss gjenbruke metodene og andre attributter til en klasse i en ny klasse, noe som er veldig praktisk. Men for at arv virkelig skal fungere, trenger vi også å kunne endre noe av den arvede atferden i vår nye underklasse. For eksempel vil vi kanskje spesialisere lyden a Katt gjør at:

 class Animal {void emitSound () {System.out.println ("Dyret sendte ut en lyd"); }} klasse Cat utvider Animal {@Override void emitSound () {System.out.println ("Meow"); }} klasse Dog utvider Animal {} public class Main {public static void main (String ... doYourBest) {Animal cat = new Cat (); // Meow Animal dog = new Dog (); // Dyret sendte ut en lyd Dyredyr = nytt Dyr (); // Dyret sendte ut en lydkatt.emitSound (); dog.emitSound (); animal.emitSound (); }} 

Dette er et eksempel på Java-arv med overstyring av metoden. Først vi forlenge de Dyr klasse for å lage en ny Katt klasse. Neste, vi overstyring de Dyr klassen emitSound () metode for å få den spesifikke lyden Katt gjør at. Selv om vi har erklært klassetypen som Dyr, når vi instantierer det som Katt vi får kattens mjau.

Metodeoverstyring er polymorfisme

Du husker kanskje fra mitt siste innlegg at overstyring av metoden er et eksempel på polymorfisme eller virtuell metodeinnkalling.

Har Java flere arv?

I motsetning til noen språk, for eksempel C ++, tillater Java ikke flere arv med klasser. Du kan imidlertid bruke flere arv med grensesnitt. Forskjellen mellom en klasse og et grensesnitt, i dette tilfellet, er at grensesnitt ikke holder tilstand.

Hvis du prøver flere arv som jeg har nedenfor, kompileres ikke koden:

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

En løsning ved bruk av klasser vil være å arve en etter en:

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

En annen løsning er å erstatte klassene med grensesnitt:

 grensesnitt Animal {} interface Mammal {} klasse Dog implementerer Animal, Mammal {} 

Bruke 'super' for å få tilgang til foreldreklassemetoder

Når to klasser er relatert gjennom arv, må barneklassen ha tilgang til alle tilgjengelige felt, metoder eller konstruktører i sin overordnede klasse. I Java bruker vi det reserverte ordet super for å sikre at barneklassen fremdeles kan få tilgang til foreldrenes overstyrte metode:

 offentlig klasse SuperWordExample {class Character {Character () {System.out.println ("Et tegn er opprettet"); } void move () {System.out.println ("Character walking ..."); }} klasse Moe utvider karakter {Moe () {super (); } ugyldig giveBeer () {super.move (); System.out.println ("Gi øl"); }}} 

I dette eksemplet, Karakter er foreldreklassen til Moe. Ved hjelp av super, vi har tilgang til Karakters bevege seg() metode for å gi Moe en øl.

Bruke konstruktører med arv

Når en klasse arver fra en annen, vil superklassens konstruktør alltid lastes først, før den laster underklassen. I de fleste tilfeller er det reserverte ordet super vil bli lagt til automatisk til konstruktøren. Imidlertid, hvis superklassen har en parameter i sin konstruktør, må vi bevisst påberope super konstruktør, som vist nedenfor:

 public class ConstructorSuper {class Character {Character () {System.out.println ("Super konstruktøren ble påkalt"); }} klasse Barney utvider karakter {// Ingen grunn til å erklære konstruktøren eller å påkalle superkonstruktøren // JVM vil til det}} 

Hvis foreldreklassen har en konstruktør med minst en parameter, må vi erklære konstruktøren i underklassen og bruke super å eksplisitt påkalle foreldrekonstruktøren. De super reservert ord blir ikke lagt til automatisk, og koden kompileres ikke uten det. For eksempel:

 offentlig klasse CustomisedConstructorSuper {class Character {Character (String name) {System.out.println (name + "was invoked"); }} klasse Barney utvider karakter {// Vi vil ha kompileringsfeil hvis vi ikke påkaller konstruktøren eksplisitt // Vi må legge den til Barney () {super ("Barney Gumble"); }}} 

Skriv casting og ClassCastException

Casting er en måte å eksplisitt kommunisere til kompilatoren om at du virkelig har tenkt å konvertere en gitt type. Det er som å si "Hei, JVM, jeg vet hva jeg gjør, så vær så snill å kaste denne klassen med denne typen." Hvis en klasse du har kastet ikke er kompatibel med klassetypen du erklærte, får du en ClassCastException.

I arv kan vi tilordne barneklassen til foreldreklassen uten å kaste, men vi kan ikke tilordne en overordnet klasse til barneklassen uten å bruke avstøpning.

Tenk på følgende eksempel:

 offentlig klasse CastingExample {public static void main (String ... castingExample) {Animal animal = new Animal (); Dog dogAnimal = (Hund) dyr; // Vi får ClassCastException Dog dog = new Dog (); Animal dogWithAnimalType = new Dog (); Dog specificDog = (Dog) dogWithAnimalType; specificDog.bark (); Animal anotherDog = hund; // Det er greit her, ikke behov for å kaste System.out.println (((Dog) anotherDog)); // Dette er en annen måte å kaste objektet}} klasse Animal {} klasse Dog extends Animal {void bark () {System.out.println ("Au au"); }} 

Når vi prøver å kaste en Dyr eksempel til en Hund vi får et unntak. Dette er fordi Dyr vet ikke noe om barnet sitt. Det kan være en katt, en fugl, en øgle osv. Det er ingen informasjon om det spesifikke dyret.

Problemet i dette tilfellet er at vi har instantiert Dyr som dette:

 Dyredyr = nytt Dyr (); 

Så prøvde å kaste den slik:

 Dog dogAnimal = (Hund) dyr; 

Fordi vi ikke har en Hund for eksempel er det umulig å tildele en Dyr til Hund. Hvis vi prøver, får vi en ClassCastException

For å unngå unntaket, bør vi sette i gang Hund som dette:

 Hundhund = ny Hund (); 

deretter tilordne den til Dyr:

 Animal anotherDog = hund; 

I dette tilfellet fordi vi har utvidet Dyr klasse, den Hund eksempel trenger ikke engang å bli kastet; de Dyr foreldreklassetypen godtar ganske enkelt oppgaven.

Støping med supertyper

Det er mulig å erklære en Hund med supertypen Dyr, men hvis vi ønsker å påkalle en bestemt metode fra Hund, må vi kaste den. Som et eksempel, hva om vi ønsket å påberope oss bark() metode? De Dyr supertype har ingen måte å vite nøyaktig hvilken dyreinstans vi påberoper oss, så vi må kaste Hund manuelt før vi kan påberope oss bark() metode:

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

Du kan også bruke avstøpning uten å tilordne objektet til en klassetype. Denne tilnærmingen er praktisk når du ikke vil erklære en annen variabel:

 System.out.println ((((Dog) another Dog)); // Dette er en annen måte å kaste objektet på 

Ta Java-arveutfordringen!

Du har lært noen viktige begreper om arv, så nå er det på tide å prøve en arveutfordring. For å starte, studer følgende kode:

$config[zx-auto] not found$config[zx-overlay] not found