Arv
- Hva er arv
- Eksempel på arv:
ManualGearCar
utviderCar
- Hvordan bruke arv
- Object
Hva er arv
I Java er det mulig å opprette en klasse (kalt subklasse) som «arver» fra en annen klasse (kalt superklasse) ved å bruke kodeordet extends
i klassedeklarasjonen. Dette innebærer at
- subklassen automatisk får alle felter og metoder som er definert i superklassen, og
- objekter i subklassen har typen til både subklassen og til superklassen (mens objekter i superklassen ikke vil ha typen til subklassen).
Subklassen kan være forskjellig fra superklassen på to måter:
- ved å ha ekstra felter og metoder, og
- ved å overskrive metoder som allerede er definert i superklassen.
Fordeler:
- En enkel og rask måte å gjenbruke kode på. Hjelper til med DRY-prinsippet (Don’t Repeat Yourself).
- Kan i sjeldne tilfeller representere naturlige relasjoner mellom klasser, og slik sett være en modell som er enkel å forstå.
- Kan styrke single responsibility -prinsippet ved å skille ut ett ansvarsområde til én klasse.
- Kan gjøre det lettere å forstå subklassen hvis man allerede kjenner superklassen; forskjellene drukner ikke i likhetene.
- Kan brukes for å lage rammeverk. For eksempel krever Swing at vi arver deres klasser når vi bruker rammeverket.
Ulemper:
- Kan redusere modularitet ved at endringer i superklassen kan ødelegge for subklassene.
- Kan bryte innkapsling ved at implementasjonsdetaljer i superklassen blir eksponert til subklassene.
- Kan redusere modularitet ved at subklasser må ta hensyn til superklassen sine implementasjonsdetaljer.
- Kan føre til komplekse relasjoner som er utfordrende å forstå og feilsøke.
Ulempene gjør at det er svært krevende å i ettertid modifisere en klasse som blir arvet av andre. Dersom et alternativt design ikke medfører spesielt store ulemper, vil det ofte være å foretrekke.
Eksempel på arv
Vi lager en klasse ManualGearCar
som uvider klassen Car
. Legg merke til i koden under:
ManualGearCar extends Car
er syntaksen som brukes for å definere ManualGearCar som subklasse av Car.- Alt et Car -objekt kan gjøre, kan også et ManualGearCar -objekt gjøre.
super
er et kodeord vi kan bruke i konteksten av en subklasse for å referere til superklassen. I eksemplet under brukessuper.stop()
for å kallestop()
-metoden i superklassen først, og deretter legge til en ekstra linje kode som nullstiller gir.
class Car {
int speed;
void changeSpeed(int delta) {
speed += delta;
}
void stop() {
speed = 0;
}
int getSpeed() {
return speed;
}
}
class ManualGearCar extends Car {
// Ekstra felter og variabler
int gear;
void setGear(int newGear) {
this.gear = newGear;
}
int getGear() {
return this.gear;
}
// Overskrevne metoder
@Override
void stop() {
super.stop(); // Kaller stop-metoden i superklassen først
this.gear = 0;
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car();
ManualGearCar manualGearCar = new ManualGearCar();
// Begge objektene har changeSpeed og getSpeed som definert i superklassen
car.changeSpeed(10);
manualGearCar.changeSpeed(11);
System.out.println(car.getSpeed()); // 10
System.out.println(manualGearCar.getSpeed()); // 11
// Kun ManualGear -variabler har tilgang til metodene setGear og getGear
manualGearCar.setGear(2);
System.out.println(manualGearCar.getGear()); // 2
// Begge objektene har typen Car
System.out.println(car instanceof Car); // true
System.out.println(car instanceof ManualGearCar); // false
System.out.println(manualGearCar instanceof Car); // true
System.out.println(manualGearCar instanceof ManualGearCar); // true
// Vi kan derfor lagre ManualGearCar -objekter i Car -variabeler
Car mcar = manualGearCar; // mcar er samme objekt som manualGearCar
// Selv om både car og mcar er variabler av typen Car, er objektene de
// refererer til likevel i ulike klasser
System.out.println(car.getClass()); // Car
System.out.println(mcar.getClass()); // ManualGearCar
// Metoden stop() er definert ulikt i de to klassene, og vil derfor ha
// ulik effekt. Dette til tross for at begge variablene car og mcar
// har typen Car; det er **klassen objektet er i** som bestemmer
// hvilken metode som blir kalt, ikke hvilken type variabelen har.
car.stop(); // Kaller stop-metoden slik den er definert i Car
mcar.stop(); // Kaller stop-metoden slik den er definert i ManualGearCar
System.out.println(car.getSpeed()); // 0
System.out.println(manualGearCar.getSpeed()); // 0
System.out.println(manualGearCar.getGear()); // 0
}
}
Hvordan bruke arv
TL;DR
- Vær svært forsiktig med bruk av arv hvis du ikke har en god grunn. Det er ofte bedre å bruke komposisjon.
- La klasser være
final
med mindre du designer en klasse spesielt for å bli arvet.- Når du arver en klasse:
- Bruk alltid
@Override
-annotasjonen når du overskriver metoder.- Bruk
super
-referansen for å kalle metoder i superklassen når du overskriver metoder.- Overhold Liskovs substitusjonsprinsipp.
Liskovs substitusjonsprinsipp
Hvis S er en subklasse av T , da må alt som kan bevises om T også kunne bevises om S . |
Barbara Liskov Bilde: Kenneth C. Zirkel CC-BY-SA 3.0 |
Liskovs substitusjonsprinsipp sier med andre ord at hvis S
er en subklasse av T
, skal ethvert S
-objekt kunne benyttes som om det var et T
-objekt uten at den som bruker objektet (i den tro at det egentlig er en T
) på noe tidspunkt blir overrasket.
I noen få tilfeller stemmer denne relasjonen naturlig for to klasser. Da kan arv være hensiktsmessig. For eksempel kan vi i en database for utlånsobjekter i et bibliotek ha en superklasse LoanableItem
og subklasser Book
og CD
. Men å kjenne igjen når to konsepter har en slik relasjon er ikke alltid lett.
Anta at vi har en klasse S
som arver fra en klasse T
. Da:
-
S
kan legge til metoder og feltvariabler som ikke finnes iT
, men kan ikke fjerne metoder eller feltvariabler som finnes iT
. -
Alle metoder i
S
kan kun endre feltvariablene arvet fraT
slik at de fremdeles er en gyldig tilstand hvis tolket som etT
-objekt. -
En metode i
S
som overskriver en metode fraT
må akseptere minimum de argumentene som metoden iT
aksepterer, og må returnere verdier eller utføre tilstandsendringer som er innenfor kontrakten/intensjonen til den overskrevne metoden iT
.
Man leser gjerne på internettsider som forklarer arv at konseptet innebærer et «er» -forhold mellom to klasser. For eksempel, en bil med manuelt gir er en bil, så derfor gir det mening at ManualGearCar arver fra Car. Dette kan gi oss en pekepinn på at arv kan være mulig; men det er på ingen måte nok, som vi skal se i neste avsnitt.
Er et kvadrat et rektangel?
En klasse som representerer et rektangel kan være:
public class Rectangle {
private int width;
private int height;
/**
* Change the size of the rectangle
* @param width new width
* @param height new height
*/
public void setSize(int width, int height) {
this.width = width;
this.height = height;
}
/** Get the area of the rectangle */
public int getArea() {
return this.width * this.height;
}
}
Om vi skulle tenke tanken at alle kvadrater er rektangler, så lager vi kanskje en subklasse av Rectangle som representerer kvadrater fordi vi tenker at arv representerer et er -forhold. Åpenbart kan vi ikke beholde setSize
-metoden slik den er, fordi vi må sørge for at width
og height
er like. Men hvordan skal den være?
// Alternativ 1: Endre kommentaren til metoden
/**
* Change the size of the rectangle. Caller must use the same
* number for width and height.
*
* @param width new width
* @param height new height
*/
@Override
public void setSize(int width, int height) {
super.setSize(width, height);
}
// Alternativ 2: Benytter kun én av parametrene
@Override
void setSize(int width, int height) {
super.setSize(width, width);
}
// Alternativ 3: Ekstra metodesignatur som kun har én parameter
// (vil ikke fjerne den arvede metoden, kommer bare i tillegg)
void setSize(int edgeLength) {
super.setSize(edgeLength, edgeLength);
}
// Alternativ 4: Krasj hvis width og height ikke er like
@Override
void setSize(int width, int height) {
if (width != height) {
throw new IllegalArgumentException("Width and height must be equal");
}
super.setSize(width, height);
}
Alternativer 1 og 3 er dårlige alternativer fordi objektene plutselig kan slutte å faktisk representere et kvadrat, noe som tross alt var poenget med å ha et objekt av denne typen. Alternativer 2 og 4 bryter med Liskovs substitusjonsprinsipp fordi de ikke kan brukes som om de var et rektangel. Dermed må vi konkludere med at selv om et kvadrat «er» et rektangel rent matematisk, er det likevel ikke et arve-forhold mellom disse begrepene.
Dersom vi ønsker å lage båden en kvadrat-klasse og en rektangel-klasse og gjenbruke så mye kode som mulig, hva skal vi gjøre i stedet? Noen muligheter:
Man kan gjenbruke koden fra rektangel-klassen i en kvadrat-klasse ved bruk av komposisjon. Da har vi et rektangel-objekt som feltvariabel i kvadrat-klassen. Slik møter vi prinsippet om å ikke skrive den samme koden mer enn én gang samtidig som vi unngår problemene med arv.
Ulempen med denne løsningen er at vi får mange «videresend» -metoder som kaller på en tilsvarende metode i feltvariabelen. Dette er likevel en liten kostnad, og disse metodene er relativt lite utsatt for feil.
public class Square {
private Rectangle rectangle;
public Square(int edgeLength) {
this.rectangle = new Rectangle();
this.rectangle.setSize(edgeLength, edgeLength);
}
public void setSize(int edgeLength) {
this.rectangle.setSize(edgeLength, edgeLength);
}
public int getArea() {
return this.rectangle.getArea();
}
}
Dersom det er et poeng at både rektangler og kvadrater kan lagres i den samme variabelen, kan vi la både rektangler og kvadrater implementere et felles grensesnitt som inneholder de felles metodene (her: getArea). Dette kan gjerne kombineres med komposisjon.
interface Shape {
int getArea();
}
class Rectangle implements Shape {
// ...
}
class Square implements Shape {
// ...
}
Problemet med at et kvadrat ikke kan benyttes som et rektangel senere, var knyttet til at det var mulig å endre størrelsene til objektene etter at de var først opprettet. Hvis vi fjerner denne muligheten og i stedet lager objekter som er uforanderlige (som kun kan endre størrelse i konstruktøren), da kan vi lage en klasse som representerer et kvadrat ved å arve en rektangel-klasse uten å bryte med Liskovs substitusjonsprinsipp.
public class Rectangle {
private final int width;
private final int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public int getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
public Square(int edgeLength) {
super(edgeLength, edgeLength);
}
}
Klasser som er final
Når man definerer en klasse er det mulig å definere den som final
, for eksempel slik:
public final class Rectangle {
// ...
}
Da vil det ikke være mulig å arve fra denne klassen. Dette er en god praksis.
- Du tvinger den som ønsker å arve klassen til å tenke nøye gjennom designet. Klasser som blir arvet av andre er vanskelige å vedlikeholde/modifisere, så ved å gjøre «dine» klasser
final
sparer du deg selv for mye arbeid. - Hvis den som vil arve bestemmer seg for at arv likevel er riktig, må den fjerne
final
-modifikatoren. Dette vil vises i git-historikken, og vil gjøre det lettere å forstå hvorfor arv er brukt; og vil avsløre hvem som skylder teamet en runde med sorgdruknende brus dersom det i ettertid viser seg å være et feiltrinn. - Å fjerne
final
senere er enkelt og ødelegger ingenting; men å legge tilfinal
etter at noen andre først har begynt å arve kan kreve store omstruktureringer.
Unntaket er selvfølgelig dersom klassen er designet spesielt for å bli arvet.
Object
Klassen Object er en spesiell klasse i Java. Dersom man oppretter en klasse uten å spesifisere at den arver en annen klasse, arver den i stedet Object
-klassen. Derfor er alle klasser enten direkte eller indirekte en subklasse av Object
. I denne klassen finnes det en rekke metoder som kan kalles på ethvert objekt, for eksempel
toString()
(denne metoden kalles automatisk når objektet skrives ut. Ved å overskrive den kan man velge selv hvordan objekter i en klasse representeres som strenger)equals(Object)
oghashCode()
(les mer i kursnotater om likhet i egne klasser)getClass()
- og noen flere.