Ohjelmointi

Perintö vs. koostumus: Kuinka valita

Perintö ja sommittelu ovat kaksi ohjelmointitekniikkaa, joita kehittäjät käyttävät luodakseen suhteita luokkien ja objektien välille. Perintö johtaa luokkaan toisesta, mutta koostumus määrittelee luokan sen osien summana.

Perinnöllä luodut luokat ja objektit ovat tiukasti kytketty koska vanhemman tai superluokan vaihtaminen perintösuhteessa saattaa rikkoa koodisi. Sommittelun avulla luodut luokat ja esineet ovat löyhästi kytketty, mikä tarkoittaa, että voit vaihtaa komponentteja helpommin rikkomatta koodiasi.

Koska löyhästi kytketty koodi tarjoaa enemmän joustavuutta, monet kehittäjät ovat oppineet, että sommittelu on parempi tekniikka kuin perintö, mutta totuus on monimutkaisempi. Ohjelmointityökalun valinta on samanlainen kuin oikean keittiövälineen valinta: Et käytä voiveitsiä vihannesten leikkaamiseen, ja samalla tavoin sinun ei pitäisi valita koostumusta jokaiselle ohjelmointiskenaarialle.

Tässä Java Challengerissa opit eron perinnön ja sommittelun välillä ja kuinka päättää, mikä on oikea ohjelmallesi. Seuraavaksi esitän sinulle useita tärkeitä, mutta haastavia Java-perinnön näkökohtia: menetelmän ohittaminen, super avainsana ja tyyppivalu. Lopuksi testaat oppimasi työskentelemällä perintöesimerkin läpi rivi riviltä selvittääkseen, minkä tuotoksen tulisi olla.

Milloin perintöä tulee käyttää Java-sovelluksessa

Objektikeskeisessä ohjelmoinnissa voimme käyttää perintöä, kun tiedämme, että lapsen ja sen vanhempien luokan välillä on "on" -suhde. Joitakin esimerkkejä ovat:

  • Henkilö on ihmisen.
  • Kissa on eläin.
  • Auto on ajoneuvo.

Kummassakin tapauksessa lapsi tai alaluokka on a erikoistunut vanhemman tai superluokan versio. Perinnöinti superluokasta on esimerkki koodin uudelleenkäytöstä. Käytä tätä hetkeä ymmärtääksesi paremmin tämän suhteen Auto luokka, joka perii Ajoneuvo:

 luokan ajoneuvo {merkkijono; Jousen väri; kaksinkertainen paino; kaksinkertainen nopeus; void move () {System.out.println ("ajoneuvo liikkuu"); }} julkisen luokan auto laajentaa ajoneuvoa {String licensePlateNumber; Merkkijono omistaja; JousirunkoTyyli; public static void main (String ... perintöesimerkki) {System.out.println (uusi ajoneuvo (). merkki); System.out.println (uusi auto (). Merkki); uusi auto (). siirry (); }} 

Kun harkitset perinnön käyttöä, kysy itseltäsi, onko aliluokka todella erikoisluokiteltu versio yliluokasta. Tässä tapauksessa auto on tietyntyyppinen ajoneuvo, joten perintösuhteella on järkeä.

Milloin sävellystä tulee käyttää Java-tilassa

Kohdekeskeisessä ohjelmoinnissa voimme käyttää sommittelua tapauksissa, joissa yhdellä objektilla "on" (tai osa sitä) toinen esine. Joitakin esimerkkejä ovat:

  • Auto on akku (akku on osa auto).
  • Henkilö on sydän (sydän on osa henkilö).
  • Talo on olohuone (olohuone on osa talo).

Harkitse paremmin tämän tyyppistä suhdetta ymmärtämällä a Talo:

 public class CompositionExample {public static void main (String ... houseComposition) {uusi talo (uusi makuuhuone (), uusi LivingRoom ()); // Talossa on nyt makuuhuone ja LivingRoom} staattisen luokan talo {makuuhuoneen makuuhuone; LivingRoom livingRoom; Talo (makuuhuoneen makuuhuone, LivingRoom livingRoom) {this.bedroom = makuuhuone; this.livingRoom = livingRoom; }} staattinen luokan makuuhuone {} staattinen luokan LivingRoom {}} 

Tässä tapauksessa tiedämme, että talossa on olohuone ja makuuhuone, joten voimme käyttää sitä Makuuhuone ja Olohuone a: n koostumuksessa olevat esineet Talo

Hanki koodi

Hanki lähdekoodi esimerkkejä tästä Java Challengerista. Voit suorittaa omat testisi noudattamalla esimerkkejä.

Perintö vs. koostumus: Kaksi esimerkkiä

Harkitse seuraavaa koodia. Onko tämä hyvä esimerkki perinnöstä?

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

Tässä tapauksessa vastaus on ei. Lapsiluokka perii monia menetelmiä, joita se ei koskaan käytä, mikä johtaa tiukasti yhdistettyyn koodiin, joka on sekä hämmentävä että vaikea ylläpitää. Jos katsot tarkkaan, on myös selvää, että tämä koodi ei läpäise "on" -testiä.

Kokeillaan nyt samaa esimerkkiä sommittelun avulla:

 tuo java.util.HashSet; tuo java.util.Set; public class CharacterCompositionExample {staattinen joukko = uusi HashSet (); public static void main (String ... goodExampleOfComposition) {set.add ("Homer"); set.forEach (System.out :: println); } 

Sävellyksen käyttäminen tässä skenaariossa sallii CharacterCompositionExample luokan käyttää vain kahta HashSetmenetelmät perimättä niitä kaikkia. Tämä johtaa yksinkertaisempaan, vähemmän kytkettyyn koodiin, joka on helpompi ymmärtää ja ylläpitää.

Perintöesimerkkejä JDK: ssa

Java Development Kit on täynnä hyviä esimerkkejä perinnöstä:

 class IndexOutOfBoundsException laajentaa RuntimeException {...} class ArrayIndexOutOfBoundsException laajentaa IndexOutOfBoundsException {...} class FileWriter laajentaa OutputStreamWriter {...} luokka OutputStreamWriter laajentaa Writer {...} käyttöliittymän Stream laajentaa BaseStream {...} 

Huomaa, että jokaisessa näistä esimerkeistä lapsiluokka on erikoistunut versio vanhemmastaan; esimerkiksi, IndexOutOfBoundsException on eräänlainen Ajonaikainen poikkeus.

Menetelmä, joka korvaa Java-perinnön

Perintö antaa meille mahdollisuuden käyttää yhden luokan menetelmiä ja muita ominaisuuksia uudessa luokassa, mikä on erittäin kätevää. Mutta jotta perintö todella toimisi, meidän on myös pystyttävä muuttamaan joitain periytyviä käytäntöjä uudessa alaluokassamme. Haluamme esimerkiksi erikoistua äänen a Kissa tekee:

 luokka Eläin {void emitSound () {System.out.println ("Eläin antoi äänen"); }} luokka Kissa laajentaa eläimiä {@Override void emitSound () {System.out.println ("Miau"); }} luokka Koira jatkaa eläimiä {} julkinen luokka Pää {julkinen staattinen void main (String ... doYourBest) {Eläinkissa = uusi kissa (); // Miau-eläinkoira = uusi koira (); // Eläin lähetti äänen Eläineläin = uusi eläin (); // Eläin lähetti äänen cat.emitSound (); koira.emitSound (); animal.emitSound (); }} 

Tämä on esimerkki Java-perinnöstä menetelmän ohittamisen kanssa. Ensin me pidentää Eläin luokassa luoda uuden Kissa luokassa. Seuraavaksi me ohittaa Eläin luokan emitSound () menetelmä saada tietty ääni Kissa tekee. Vaikka olemme ilmoittaneet luokan tyypin Eläin, kun me välitämme sen Kissa saamme kissan miau.

Menetelmän ohittaminen on polymorfismi

Saatat muistaa viimeisestä viestistäni, että menetelmän ohittaminen on esimerkki polymorfismista tai virtuaalisen menetelmän kutsumisesta.

Onko Java: lla useita perintöominaisuuksia?

Toisin kuin jotkut kielet, kuten C ++, Java ei salli useita perintöjä luokkien kanssa. Voit kuitenkin käyttää useita perintöjä rajapintojen kanssa. Luokan ja käyttöliittymän ero tässä tapauksessa on, että rajapinnat eivät pidä tilaa.

Jos yrität useita perintöjä, kuten minulla on alla, koodia ei koota:

 luokka Eläin {} luokka Nisäkäs {} luokka Koira ulottuu Eläin, Nisäkäs {} 

Luokkoja käyttävä ratkaisu olisi periä yksitellen:

 luokka Eläin {} luokka Nisäkäs jatkuu Eläin {} luokka Koira laajentaa Nisäkäs {} 

Toinen ratkaisu on korvata luokat rajapinnoilla:

 käyttöliittymä Animal {} interface Nisäkäs {} -luokka Koira toteuttaa Animal, Nisäkäs {} 

"Super" -toiminnon käyttäminen pääluokkamenetelmiin

Kun kaksi luokkaa liittyy perintöön, lapsiluokan on kyettävä pääsemään kaikkiin vanhempiensa luokkien käytettävissä oleviin kenttiin, menetelmiin tai rakentajiin. Javassa käytämme varattua sanaa super varmistaa, että lapsiluokka voi silti käyttää vanhempiensa ohittamaa menetelmää:

 public class SuperWordExample {class Character {Character () {System.out.println ("Merkki on luotu"); } void move () {System.out.println ("Hahmon kävely ..."); }} luokka Moe laajentaa Merkki {Moe () {super (); } void giveBeer () {super.move (); System.out.println ("Anna olutta"); }}} 

Tässä esimerkissä Merkki on Moen vanhemmuusluokka. Käyttämällä super, voimme käyttää Merkkion liikkua() menetelmällä, jotta Moelle saadaan olutta.

Rakentajien käyttö perinnöllä

Kun yksi luokka perii toisen, superluokan konstruktori ladataan aina ensin, ennen kuin alaluokka ladataan. Useimmissa tapauksissa varattu sana super lisätään automaattisesti rakentajaan. Jos kuitenkin yliluokan konstruktorissa on parametri, joudumme tietoisesti käyttämään sitä super rakentaja, kuten alla on esitetty:

 public class ConstructorSuper {class Character {Character () {System.out.println ("Superkonstruktori kutsuttiin"); }} luokka Barney laajentaa merkkiä {// Ei tarvitse ilmoittaa rakentajaa tai kutsua superrakentajaa // JVM tulee siihen}} 

Jos pääluokassa on konstruktori, jolla on vähintään yksi parametri, meidän on ilmoitettava konstruktori alaluokassa ja käytettävä super vetoamaan nimenomaisesti vanhempaan konstruktoriin. super varattua sanaa ei lisätä automaattisesti eikä koodi käänny ilman sitä. Esimerkiksi:

 public class CustomizedConstructorSuper {class Character {Character (String name) {System.out.println (nimi + "kutsuttiin"); }} luokka Barney laajentaa merkkiä {// Meillä on käännösvirhe, jos emme vedota konstruktoria nimenomaisesti // Meidän on lisättävä se Barney () {super ("Barney Gumble"); }}} 

Tyyppivalu ja ClassCastException

Suoratoisto on tapa ilmoittaa kääntäjälle nimenomaisesti, että aiot todella muuntaa tietyn tyypin. Se on kuin sanoa: "Hei, JVM, tiedän mitä teen, joten heittäkää tämä luokka tämän tyyppiseen." Jos lähettämäsi luokka ei ole yhteensopiva ilmoittamasi luokkatyypin kanssa, saat ClassCastException.

Perinnössä voimme osoittaa lapsiluokan vanhempaluokkaan ilman heittoa, mutta emme voi määrittää vanhempaluokaa lapsiluokkaan käyttämättä heittoa.

Harkitse seuraavaa esimerkkiä:

 julkinen luokka CastingExample {public static void main (String ... castingExample) {Eläineläin = uusi eläin (); Koira dogAnimal = (Koira) eläin; // Saamme ClassCastException Dog dog = new Dog (); EläinkoiraWithAnimalType = uusi koira (); Koirakohtainen koira = (Koira) dogWithAnimalType; specificDog.bark (); Eläin toinenKoira = koira; // Tässä on hieno, ei tarvitse suoratoistaa System.out.println ((((Koira) anotherDog)); // Tämä on toinen tapa heittää objekti}} luokka Eläin {} luokka Koira ulottaa eläimen {void bark () {System.out.println ("Au au"); }} 

Kun yritämme heittää Eläin esimerkiksi a Koira saamme poikkeuksen. Tämä johtuu siitä Eläin ei tiedä mitään lapsestaan. Se voi olla kissa, lintu, lisko jne. Ei ole tietoa tietystä eläimestä.

Tässä tapauksessa ongelmana on, että olemme havainnollistaneet Eläin kuten tämä:

 Eläineläin = uusi eläin (); 

Sitten yritti heittää se näin:

 Koira dogAnimal = (Koira) eläin; 

Koska meillä ei ole Koira Esimerkiksi on mahdotonta määrittää Eläin että Koira. Jos yritämme, saamme a ClassCastException

Poikkeuksen välttämiseksi meidän tulisi välittää Koira kuten tämä:

 Koirakoira = uusi koira (); 

määritä se sitten Eläin:

 Eläin toinenKoira = koira; 

Tässä tapauksessa, koska olemme laajentaneet Eläin luokka, Koira instanssia ei tarvitse edes heittää; Eläin vanhemman luokan tyyppi yksinkertaisesti hyväksyy tehtävän.

Valu supertyypeillä

On mahdollista ilmoittaa a Koira supertyypin kanssa Eläin, mutta jos haluamme käyttää tiettyä menetelmää Koira, meidän on heitettävä se. Esimerkiksi, mitä jos haluaisimme vedota haukkua() menetelmä? Eläin supertyypillä ei ole mitään keinoa tietää tarkalleen, mihin eläimen esiintymiin vetoamme, joten meidän on heitettävä Koira manuaalisesti, ennen kuin voimme käyttää haukkua() menetelmä:

 EläinkoiraWithAnimalType = uusi koira (); Koirakohtainen koira = (Koira) dogWithAnimalType; specificDog.bark (); 

Voit käyttää myös suoratoistoa määrittämättä objektia luokkatyypille. Tämä lähestymistapa on kätevä, kun et halua ilmoittaa toista muuttujaa:

 System.out.println (((koira) toinen koira)); // Tämä on toinen tapa heittää objekti 

Vastaa Java-perintökysymykseen!

Olet oppinut joitain tärkeitä perintökäsitteitä, joten nyt on aika kokeilla perintöhaastetta. Aloita tutkimalla seuraavaa koodia: