TDA550 Objektorienterad programmering, fortsättningskurs Föreläsning 2 Polyfmorfism Dynamisk bindning Interface och abstrakta klasser Överlagring Överskuggning Accessorer och mutatorer Objektorienterad programmering fk 1 Föreläsning 2
Polymorfism Java tillåter att man till variabel av en viss typ C kan tilldela objekt som är av typ C eller är subtyper till C. Till exempel är i tilldelningssatsen Runnable r = new FastRunner(); variabeln r av typen Runnable, medan det objekt som denna variabel refererar till är av subtypen FastRunner. Egenskapen att ett objekt som är en subtyp till typen C legal att använda varhelst ett objekt typen C förväntas kallas polymorfism. Polymorfism är en av de viktigaste koncepten i objektorientering och den allra viktigaste orsaken till att objektorienterade programspråk är så användbara. Objektorienterad programmering fk 2 Föreläsning 2
Betydelsen av polymorfism När man diskuterar hur bra designen hos en mjukvara är, kommer ofta konceptet plug-and-play upp. Idén med plug-and-play är att en komponent kan pluggas in i ett system och kan användas, utan att omfigurera systemet. Antag att vi i ett programsystem använder klassen LinkedList, från Javas standardbibliotek, för att hålla reda på t.ex. varor i ett lager. LinkedList list = new LinkedList(); List list = new LinkedList(); Vidga typtillhörigheten Vidga typtillhörigheten Collection list = new LinkedList(); Använd en factory-metod Manager factory = new Factory(); Collection list = factory.createnewlist(); public class Factory {... public Collection createnewlist() { return new LinkedList(); } } Objektorienterad programmering fk 3 Föreläsning 2
Återanvändbar kod Att en variabel kan deklareras som interfacetyp och referera till objekt som tillhör någon klasser som realiserar interfacetypen, innebär att vi kan minska beroendet av en specifik klass. Detta ger oss möjlighet att skriva kod som är mycket mer flexibel och återanvändbar än vad som annars skulle vara fallet. Objektorienterad programmering fk 4 Föreläsning 2
Interface vs abstrakta klasser En klass är abstrakt om någon metod som deklareras i klassen saknar implementation. Det är subklassernas uppgift att implementera de abstrakta metoderna. En abstrakt klass beter sig exakt som en vanlig klass med ett enda undantag: man kan inte skapa instanser av en abstrakt klass. Men det är möjligt att ha variabler vars typ är en abstrakt klass. Naturligtvis måste en sådan variabel ha referens till ett objekt av en konkret subtyp (eller null). Objektorienterad programmering fk 5 Föreläsning 2
Interface vs abstrakta klasser Abstrakta klasser är behändiga att använda för att faktorisera ut gemensamma beteenden. <<interface>> SceneShape isselected() setselected() draw() drawselection() translate() contains() SelectableShape {abstract} selected isselected() setselected() CarShape draw() drawselection() translate() contains() HouseShape draw() drawselection() translate() contains() Abstrakta klasser har en fördel jämfört med interface typer: de kan definiera gemensamma beteenden. Men de har också en allvarlig nackdel: en klass kan endast göra extend på en klass, men kan implementera många olika interface. Slutsats: Använd aldrig abstrakta klasser där alla metoder saknar implementation, utan använd istället interface. Använd abstrakta klasser för att faktorisera ut gemensam kod. Objektorienterad programmering fk 6 Föreläsning 2
Dynamisk bindning Vi har sett att arv tillhandahåller åtminstone två fördelar för mjukvaruutvecklaren. Först av allt ger det en mekanism för återanvändning av kod. För det andra är polyformism ett flexibilitet och kraftfullet verktyg. Vi skall ny behandla koncept dynamisk bindning som tillsammans med polyformism ger programutvecklaren ytterligare flexibilitet. Antag att vi har en superklass Automobile samt att denna har de tre subklasserna Sedan, Minivan och SportCar. Automobile +getcapacity():int... Sedan +getcapacity():int Minivan +getcapacity():int SportCar +getcapacity():int Automobile har metoden getcapacity() som även implementeras av de tre subklasserna, dvs dessa subklasser överskuggar (override) denna metod. Objektorienterad programmering fk 7 Föreläsning 2
Dynamisk bindning Eftersom Sedan, Minivan och SportCar är subklasser till Automobile är följande kod legal: Automobile[] fleet = new Automobile[3]; fleet[0] = new Sedan(); fleet[1] = new Minivan(); fleet[2] = new SportCar(); Fälet fleet innehåller således värden från tre olika klasser. Antag nu att vi vill ta reda på den totala kapaciteten för objekten som finns i fälet fleet. Här är ett första försök att göra det: //--- VERSION 1 --// int totalcapacity = 0; for (int i = 0; i < fleet.length; i++) { if (fleet[i] instanceof Sedan) totalcapacity += ((Sedan) fleet[i]).getcapacity(); else if (fleet[i] instanceof Minivan) totalcapacity += ((Minivan) fleet[i]).getcapacity(); else if (fleet[i] instanceof SportCar) totalcapacity += ((SportCar) fleet[i]).getcapacity(); } Koden fungerar, men är den bra? Objektorienterad programmering fk 8 Föreläsning 2
Dynamisk bindning Genom att dra nytta av dynamisk bindning kan vi få en elegant lösning. //--- VERSION 2 --// int totalcapacity = 0; for (int i = 0; i < fleet.length; i++) totalcapacity += fleet[i].getcapacity(); Javas runtime system (Java Virtual Machine) tittar efter aktuell typ för ett objekt när programmet exekvera och inte på vilken deklarerad typ det har. I vårt exempel innebär detta t.ex. att objektet som refereras av fleet[1] vid exekveringen har typen Minivan och inte typen Automobile som det har som deklarerad typ. Koden i Version 2 är mycket elegant i jämförelse med koden i Version1. Den är inte bara kortare, enklare och mer överskådlig, den behöver inte förändras om man senare behöver lägga till en ny subklass till Automobile, t.ex. subklassen SUV. Objektorienterad programmering fk 9 Föreläsning 2
Dynamisk bindning Vad händer om metoden getcapacity() inte finns deklarerad i superklassen Automobile? Finns inte metoden, finns det heller ingen metod som blivit överskuggad!! Kompilatorn kommer att ge ett kompileringsfel, eftersom fleet[i] är av typen Automobile och klassen Automobile då inte har någon metod getcapacity(). Vi har alltså två delar som är inblandade när det gäller anropet av fleet[i].getcapacity(). Den första delen sker under kompileringen. När kompilatorn ser att fleet[i] är deklarerad av typen Automobile, söker kompilatorn efter metoden i klassen Automobile. Det andra steget sker under exekveringen är den korrekta implementation av metoden getcapacity() skall väljas. Om det inte finns någon metod getcapacity() deklarerad i superklassen Automobile måste man använda lösningsmetoden som gavs i Version 1 ovan. Objektorienterad programmering fk 10 Föreläsning 2
Överlagring kontra överskuggning Om det finns två metoder i samma klass som har samma namn, men med olika signaturer, innebär detta att namnet på metoderna är överlagrat (overload). Signaturen för en metod bestäms av vilka typer som parametrarna i metodens parameterlista har (namnen på parametrarna är irrelevant). Två signaturer är lika om antal, typ och ordning på parametrarna är identiska. Överlagring betyder alltså att ett metodnamn används av två eller flera helt skilda metoder i samma klass. Överskuggning däremot involverar en superklass och en subklass, och berör två metoder som har samma metodsignatur den ena metoden finns lokaliserad i superklassen och den andra metoden finns lokaliserad i subklassen. Objektorienterad programmering fk 11 Föreläsning 2
Överlagring I klassen String finns metoderna: public String substring(int beginindex) public String substring(int beginindex, int endindex) Dessa metoder är överlagrade - de har samma namn men olika signaturer. Naturligtvis skall överlagring användas om och endast om de överlagde metoderna i allt väsentligt gör samma sak (som med metoderna substring i klassen String ovan). När parametrarna i de överlagrade metoderna är primitiva datatyper, som substring-metoderna ovan, är det enkelt att förstå vilken metod som blir anropad. Det är svårare att förstå vilken metod som verkligen blir anropad när parametrarna utgörs av referenstyper och därför kan anropas med subtyper till de typer som anges i parameterlistan. Objektorienterad programmering fk 12 Föreläsning 2
Överlagring ett exempel Låt oss titta på klassen Automobile igen. Eftersom alla klasser i Java är subklasser till klassen Object betyder detta att klassen Automobile bl.a. ärver en metod med följande metodhuvud: public boolean equals(object obj) (1) Antag nu att vi lägger till en metod i klassen Automobile som har metodhuvudet: public boolean equals(automobile auto) (2) Object +equals(o:object): boolean Automobile +equals(o:automobile): boolean Observera att parametern till metoden equals i klassen är Automobile, medan parametern till metoden equals i metoden Object är Object. På grund av detta har vi alltså inte överskuggning. Objektorienterad programmering fk 13 Föreläsning 2
Överlagring ett exempel Klassen Automobile, har två metoder med namnet equals - en deklarerad lokalt i klassen och en ärvd från Object. Dessa båda metoder är alltså överlagrade. Betrakta nedanstående kodavsnitt: Object o = new Object(); Automobile auto = new Automobile(); Object autoobject = new Automobile(); auto.equals(o); auto.equals(auto); auto.equals(autoobject); Kan du för vart och ett av de tre anropen av equals i koden ovan tala om ifall det är den lokala metoden i klassen Automobile som kommer att exekveras eller om det är den ärvda metoden från Object? Objektorienterad programmering fk 14 Föreläsning 2
Överlagring hur fungerar det Vilken metod som skall exkveras avgörs av den deklarerade typen på objektet som skickar anropet och på de deklarerade typerna på argumenten som skickas i metoden. När kompilatorn ser ett anrop t.ex. auto.equals(o) händer följande: 1. Kompilatorn tar reda på vilken deklarerad typ auto har. 2. Kompilatorn tittar i den aktuella i klassen (eller interfactet) och i dess superklasser (eller superinterface) efter alla metoder med namnet equals. 3. Kompilatorn tittar på parameterlistorna för dessa metoder och väljer den parameterlista vars typer bäst överensstämmer med de deklarerade typerna som skickas i det aktuella anropet. I anropet auto.equals(o), är auto av typen Automobile och o av typen Object. Parametertypen för den lokala equals-metoden i klassen är Automobile och är således to narrow, eftersom denna är av typen Automobile. Således väljs equals-metoden som har en parameter av typen Object. Objektorienterad programmering fk 15 Föreläsning 2
Överlagring hur fungerar det Vid runtime skall metoden med den signatur som kompilatorn valt letas upp. Sökningen startar i den aktuella klassen för objekt som skickar anropet. Har denna klass ingen sådan metod genomsöks kedjan av klassens superklasser tills metoden påträffas. I vårt fall påbörjas sökningen i klassen Automobile och metoden påträffas i klassen Object, således är det denna metod som kommer att exekveras. För anropet auto.equals(auto) kommer equals-metoden i klassen Automobile att väljas, eftersom denna metod har en parametera av typen Automobile (d.v.s.samma typ som den aktuella parametern auto i anropet). För anropet auto.equals(autoobject) är parametern autoobject deklarerad som Object och metoden equals i klassen Object väljs (pga av samma orsaker som var fall för anropet auto.equals(o) ovan). Notera att de aktuella typerna på argumenten i metodanropet inte påverkar vilken metod som väljs, eftersom metoden väljs vid kompileringen. De aktuella typerna på argumenten beaktas endast vid exekveringen. Objektorienterad programmering fk 16 Föreläsning 2
Fråga: Vilken metod väljs i nedanstående anrop o.equals(o) (1) o.equals(auto) (1) o.equals(autoobject) (1) autoobject.equals(o) (1) autoobject.equals(auto) (1) autoobject.equals(autoobject) (1) Objektorienterad programmering fk 17 Föreläsning 2
Överskuggning Låt oss nu införa metoden public boolean equals(object o) (3) i klassen Automobile. Detta innebär att klassen Automobile överskuggar metoden som ärvs från klassen Object. Object +equals(o:object): boolean Automobile +equals(o:automobile): boolean +equals(o:object): boolean Vad inträffar nu för de nio olika anropen vi diskuterade ovan? Object o = new Object(); Automobile auto = new Automobile(); Object autoobject = new Automobile(); auto.equals(o); (3) auto.equals(auto); (2) auto.equals(autoobject); (3) o.equals(o); (1) o.equals(auto); (1) o.equals(autoobject); (1) autoobject.equals(o); (3) autoobject.equals(auto); (3) autoobject.equals(autoobject); (3) Objektorienterad programmering fk 18 Föreläsning 2
Åtkomstmodifierare När man säger att en subklass ärver all data och alla metoder från sin superklass, betyder nödvändigtvis inte detta att subklassen direkt kan få access till all data och alla metoder i superklassen. I Java finns fyra olika synlighetsmodifierare: public protected utelämnad private tillåter access för alla andra klasser. tillåter access för alla andra klasser i samma paket och för subklasser i andra paket. tillåter access för alla andra klasser i samma paket. tillåter access endast inom klassen själv. När skall man använda protected- eller paketåtkomst? En bra tumregel är aldrig, eftersom den interna implementationen exponeras. Ett undantag är när man har ett paket som utgör en integrerad komponent, då kan det ibland på grund av effektivitetsorsaker vara meningsfullt. Dock kan en förändring i en klass kräva förändringar i andra klasser i paketet, men förändringarna är begränsade till paketet. Objektorienterad programmering fk 19 Föreläsning 2
Åtkomstmodifierare Om man vill göra klassen så generellt återanvändningsbar som möjligt, så måste man förutse att subklasser till klassen kanske vill förändra datat. Men subklasser kan inte ändra ärvd data om den är privat och det endast finns publika getter-metoder. Lösningen är att deklarera setter-metoderna i klassen som protected: public class SSNWrapper { private int socialsecuritynumber; private SSNWrapper(int ssn) { socialsecuritynumber = ssn; } public int getssn() { return socialsecuritynumber; } protected void setssn(int ssn) { socialsecuritynumber = ssn; } } public class SettableSSNWrapper extends SSNWrapper { public SettableSSNWrapper(int ssn) { super(ssn); } public void setsnn(int ssn) { super.setssn(ssn); } } Objektorienterad programmering fk 20 Föreläsning 2
Inkapsling Antag att vi har en klass som använder publika instansvariabler //-- Bad code --// public class Day { public int year; public int month; public int date; public Day(int ayear, int amonth, int adate) { } public int getyear() {...} public int getmonth() {...} public int getday() {...}... } En programmerare som använder klassen Day har i ett program skrivit koden Day d = new Day(2009, 10, 30);... int month = d.month; Vad händer som man bestämmer sig för att byta implementation i klassen Day, t.ex. att avbilda en dag som ett Julianskt datum? Objektorienterad programmering fk 21 Föreläsning 2
Inkapsling Ny implementation av klassen Day: //-- Bad code --// public class Day { public int julian; public Day(int ayear, int amonth, int adate) { } public int getyear() {...} public int getmonth() {...} public int getday() {...}... } Oförändrad kod i användarprogrammet Day d = new Day(2009, 10, 30);... int month = d.month; kommer nu att rendera i ett fel! Komponenten d.month finns inte längre. Objektorienterad programmering fk 22 Föreläsning 2
Inkapsling Använd aldrig synliga instansvariabler. En förändring i tillståndet hos ett objekt skall endast kunna ske via en publik metod. Om instansvariablerna i klassen Day är inkapslade public class Day { private int year; private int month; private int date; public Day(int ayear, int amonth, int adate) { } public int getyear() {...} public int getmonth() {...} public int getday() {...}... } måste koden i föregående exempel skrivas Day d = new Day(2009, 10, 30);... int month = d.getmonth(); Detta innebär att det är möjligt att byta implementation i klassen Day utan att program som använder klassen behöver förändras. Slutsats: Gör alla instansvariabler privata. Objektorienterad programmering fk 23 Föreläsning 2
Accessorer och Mutatorer Vi gör en konceptuell skillnad mellan metoder som förändrar tillståndet i ett objekt (mutator methods) och metoder som endast läser av värdet av en instansvariabel (accessor methods). En klass som inte har några mutator-metoder är en ickemuterbar (immutable) klass. Klassen String är ett exempel på en icke-muterbar klass. Objektorienterad programmering fk 24 Föreläsning 2
Accessorer och Mutatorer Klassen Day public class Day { private int julian; public Day(int ayear, int amonth, int adate) {...} public int getyear() {...} public int getmonth() {...} public int getday() {...} public Day adddays(int n) {...} public int daysfrom(day other) {...} } är icke-muterbar. Skall vi lägga till metoderna public void setyear(int ayear) public void setmonth(int amonth) public void setdate(int adate) i klassen Day? Objektorienterad programmering fk 25 Föreläsning 2
Accessorer och Mutatorer Antag att klassen Day har mutator-metoderna public void setyear(int ayear) public void setmonth(int amonth) public void setdate(int adate) och man i ett användarprogram ger koden Day firstddeadline = new Day(2010,1, 1); firstdeadline.setmonth(2); Day seconddeadline = new Day(2010,1, 31); seconddeadline.setmonth(2); Vad händer? Objektet seconddeadline hamnar i ett ogiltigt tillstånd, eftersom 31/2 inte är ett giltigt datum! Slutsats: Tillhandahåll inte slentrianmässigt set-metoder för alla instansvariabler! Objektorienterad programmering fk 26 Föreläsning 2
Accessor eller mutator När man har en klass som både har access-metoder och mutator-metoder är det en god ide att hålla dessa båda roller isär. En metod som returnerar information om ett objekt, dvs en access-metod, skall inte ändra tillståndet hos objektet, dvs samtidigt vara en mutator-metod. En mutator-metod skall inte returnera information om objektet, dvs metoden skall vara en void-metod. The Commmand-Query Separation principle: En metod är antingen en accessor eller en mutator, inte både och. Objektorienterad programmering fk 27 Föreläsning 2
Accessor eller mutator Det finns många exempel på klasser i Javas standardbibliotek som inte följer denna princip. Scanner sc = new Scanner("token1 token2 token3"); String t = sc.next(); Metod next() returnerar nästa token. Metoden är således en accessor, men metoden förändrar även tillståndet i hos Scanner-objektet. Nästa gång man anropar metoden next() får man en annan token. Således är metoden också en mutator. Varför konstruktörerna gjort som de gjort, kan de endast själva svara på. I klassen Stack finns en metod pop som både returnerar och tar bort det översta elementet på stacken. I detta exempel kan man dock säga att orsaken är att metoden pop() med detta beteende är vedertagen praxis. Klassen Stack har dock även en metod peek() som returnerar det översta värdet på stacken. Slutsats: Överallt där det är möjligt skall en metod antingen vara accessor eller mutator. Det är okey att ur bekvämlighetssynpunkt för användaren låta en mutator returnera ett värde, men då skall det även finnas en accessor som returnerar samma värde utan att objektets tillstånd förändras. Objektorienterad programmering fk 28 Föreläsning 2