Föreläsning 5 Klasser och objekt Henrik Johansson August 20, 2008 Ett objekt är en modell av ett fysisikt eller ett tänkt ting. Objektet och det som vi kan göra med det beskrivs av en mall, en klass. Ett objekt som vi skapat utifrån klassen kallas för en instans av objektet. Skillnaden mellan objekt och instans är liten, objektet är modellen och instansen det skapade objektet. I många sammanhang när vi kommer att använda ordet objekt menar vi formellt egentligen en viss instans av objektet. Varför är det bra att använda klasser och objekt? I sin enklaste form underlättar användandet av objekt lagring av data. I vårt exempel med studentregistret så har varje student ett antal attribut namn, adress, avklarade kurser osv. Genom att skapa en studentmodell så kan vi lagra all relevant information om en specifik student i en enda instans av ett studentobjekt. Om vi skulle tvingas programmera utan att använda objekt så behöver vi lagra informationen om studenterna i separata variabler som inte är relaterade till varandra (i en instans av ett objekt sitter ju all information ihop med instansen). När informationen är frikopplad så ökar risken för att blanda ihop data för olika studenter, det blir helt enkelt svårare att skriva programmet eftersom man måste vara ännu mer noggrann. En viktigare orsak till att använda objekt är att det gör konstruktionen av stora program enklare. Genom att skapa modeller av alla viktiga ingredienser i ett problem så blir det enklare att implementera en lösning. När modellerna är skapade behöver vi bara koppla ihop dem för att lösa vårt problem, istället för att tvingas skriva en enda stor lösning som tar upp allt på en gång. En viktig egenskap hos objekten är att de kan bestå av andra objekt, vi kan anpassa nivån på vårt program genom att lägga till eller ta bort olika delobjekt. Ett bra exempel är en bil. Vi representerar bilen och dess egenskaper som ett objekt. Eftersom vi använder objekt så kan vi anpassa noggrannheten i vår modell beroende på vad bilen ska användas till. I vissa fall räcker det med en enkel modell men ibland behöver vi t ex gå in mer på 1
hur bilen verkligen fungerar. Vi kan då lägga till objekt som en del av bilen. De nya objekten kan beskriva bilens delsystem, t ex motor, däck osv. Dessa objekt kan i sin tur bestå av andra objekt, motorn har t ex ett tändsystem och en kylare. Vi kan alltså bygga upp en hierarki av modeller/objekt som tillsammans beskriver en helhet. Om något behöver förändras så behöver vi bara ändra i just den delen av modellen (klassen), övriga delar kan lämnas omodiferade. Klassens anatomi Vi börjar med att se hur en klass måste se ut rent formellt. // Kommentar som beskriver klassen och dess syfte modifierare class Klassnamn { // Deklaration av instansvariabler. // Instansvariabeler initieras automatiska till noll // om initieringsdel (konstruktor) saknas. // I allmänhet sätter man modifieraren till private // så att instansvariabeln blir inkapslad. modifierare datatyp variabelnamn; // Eller om man vill skapa en varibel med ett värde // som inte kan ändras (dvs. en konstant) modifierare final datatyp variabelnamn = initieringsvärde; } // Metoder modifierare returtyp metodnamn(parameterlista) { lokala variabler satser } Som vi kan se så består klassen av två huvuddelar, deklaration och definition av instansvariabler respektive metoder. En instansvariabel beskriver de egenskaper (attribut) som objektet har (t ex postion, färg och storlek hos cirklarna i inlupp1). Vi måste alltid deklarera våra instansvariabler först i klassen. Första ordet i deklarationen är en modifierare (se avsnittet om inkapsling), sedan följer datatypen och variabelnamnet. Använder man inte en initieringsdel (kallas konstruktor, vi kommer att gå igenom det senare), 2
så sätts alla instansvariabler till noll (eller null) när ett objekt skapas. Det går också att skapa konstanta instansvariabler där variabelns värde inte kan ändras under en programkörning. Metoder kommer vi att prata om på nästa föreläsning. Inkapsling och synlighet Stora program är ofta svåra att överblicka. De innehåller många olika klasser som tillsammans bygger upp en helhet. En programmerare arbetar ofta endast med en liten del av ett sådant här program. Det kan också vara så att du i ditt arbete har tillgång till ett programpaket, t ex för beräkningar på bladen hos ett vindkraftverk. När du använder paketet inser du att du måste lägga till några egna metoder för att ta hänsyn till ett nytt fenomen. Båda när man arbetar med en liten av det problem och när man behöve modifiera existerande kod så har inkapsling en nyckelroll. Inkapsling (encapsulation, ibland information hiding) betyder att ett objekt ska vara självstyrande. Det ska endast vara möjligt att modifiera objektet med hjälp av objektets egna metoder. Det ska därför (i princip) vara omöjlig för kod som ligger utanför klassen/mallen att modifiera objektets värden (instansvariablerna). Vi skyddar objektet, kapslar in det och döljer dess interna funktionalitet. Vad ska det här vara bra för då? Vi tänker oss att vi har konstruerat en klass för att representera och styra en liten del av ett stort system, t ex vätgastanken hos en bränslecell. Vi har noga tänkt igenom allt som behövs för att beskriva tanken och dess funktionalitet. Vi har även kommit fram till hur tanken fungerar tillsammans med omgivningen (vi kanske har en metod för fylla på bränsle (väte) och en annan som används när själva bränslecellen behöver mer vätgas osv). Vi tänker oss nu följande situation. Vi vet att det finns en gräns för hur snabbt man får fylla på vätgas respektive ta vätgas från tanken. Följs inte dessa regler kan vi inte garantera säkerheten för tanken och de komponenter som sitter ihop med den. Vi har därför skrivit kod som kontrollerar att dessa regler följs. På en annan avdelningen har en konstruktör problem med effektiviteten hos bränslecellen. Konstruktören kommer fram till att den enklaste lösningen är att periodvis kraftigt och under väldigt kort tid öka mängden vätgas i bränslecellen. Konstruktörer vet inget om tankens funktion och de faror detta eventuellt kan medföra (när trycket i tanken ändras för fort). Om objektets instansvariabler och samtliga metoder var fullt åtkomliga överallt i programmet (styrsystemet) har konstruktören inga som helst prob- 3
lem med att implementera sin lösning i bränslecellens styrsystem. Han eller hon kan med ett enkelt metodanrop, eller kanske t o m med bara en tilldelning, på mycket kort tid få styrsystemet att hämta en godtycklig mängd vätgas från tanken. Om vi istället kapslat in objektets variabler och metoder hade detta varit omöjligt. Konstruktören hade då enbart haft tillgång till ett fåtal noggrant specificerade metoder (som vi konstruerat med just detta i åtanke) som inte tillåtit den snabba överföringen. Med inkapsling minimerar vi alltså antalet kontaktvägar mellan olika typer av objekt. Varje klass gränssnitt utåt är så litet som möjligt. Detta leder också till vi inte behöver någon kunskap om hur ett objekt som vi interagerar med egentligen fungerar. Konstruktören varken vill eller behöver veta hur bränsletanken fungerar det räcker med att han eller hon har tillgång till några metoder som ger vätgas till bränslecellen (jmfr. med inmatning och Scanner-klassen, vi behöver inte hur den fungerar för att kunna använda den). Hur klasserna är konstruerade och implementerade är ointressant i sammanhanget. Konstruktören ska aldrig kunna interagera direkt med själva bränsletanken. Släpper vi på denna egenskap kan vi förlora kontrollen över vår klass (och i exemplet, bränslecellen). För att skydda en metods instansvariabler och metoder använder vi därför olika modifierare som talar om hur dessa får användas. Vi kommer att använda två av dessa. Private De metoder och funktioner som är deklarerade som private kan endast användas inom den aktuella klassen. Instansvariabler skall alltid deklareras som private. Public Metoder och instansvariabler (gud förbjude) som är deklarerade som public kan användas av alla klasser. Ju färre metoder som är public, desto bättre. Men om en instansvariabel bara är synlig inom sin egen klass, hur kan vi då använda och förändra den från en annan klass? Klassen bränslecell behöver ju t ex komma åt instansvariablen vätgas i tanken. Vi löser det här genom att använda metoder som är deklarerade som public. Tänk tillbaka på lab1 - när ni flyttade på en cirkel eller bytte färg på en kvadrat så gjorde ni det genom metoder, aldrig genom att direkt förändra värdet på en instansvariabel. Eftersom metoderna tillhör klassen kan de förändra eller läsa av instansvariablerna. Ni kommer att använda precis samma metodik i de program som ni senare skriver själva. En metod vars funktion är att returnera (läsa av) värdet på en instansvariabel ska namnges som getvariabelnamn(). 4
En metod som förändrar värdet på en instansvariabel ska heta setvariabelnamn() (jag har dock använt addvariabelnamn i vårt studentregister eftersom det är lite mer beskrivande för exemplet). Eftersom det också går att skydda en samling av klasser på liknande sätt kan man bygga avancerade hierarkier som i detalj reglerar hur programmet fungerar. UML Ni har stött på UML tidigare under kursen och jag ska försöka att använda det genomgående för att illustera klasser. Nedan följer ett exempel på hur ett UML-diagram ser ut för en klass. Figure 1: Ett UML-diagram över en klass. Ett minustecken står för private medan plus betyder public Skapande av objekt Man skapar ett objekt (här ett studentobjekt) i sin programkod genom att skriva Student pelle = new Student(); Hela uttrycket måste skrivas, det räcker t ex inte med att bara skriva Student pelle som man kan göra när man deklarerar en variabel. Gör vi det skapar vi enbart en variabel som heter pelle som kan referera ( peka ) till ett objekt av typen student. En sådan variabel kallas referensvariabel. När vi deklarerar en referensvariabel, t ex Student pelle, så skapas alltså ingen instans av objektet. Instansen skapas istället av uttrycket pelle = new Student(). Följande två varianter är alltså funktionellt identiska: 5
//Skapa en referensvariabel till ett objekt av typen student Student pelle; //Skapa ett studentobjekt pelle = new Student(); // Allt på samma gång Student pelle = new Student(); Om vi glömmer att använda en referensvariabel då vi skapar vårt objekt, dvs vi skriver bara new Student(), så skapas faktiskt ett objekt men vi kan aldrig använda det eftersom det inte är associerat till ett namn (variabel). Tilldelning Titta på följande kodsnutt: Student pelle = new Student(); Student gustav; gustav = pelle; Det är lätt att tro att variabeln gustav refererar till en instans som är identiskt med den instans som pelle refererar till, dvs att vi har två likadana instanser. Både pelle och gustav är dock referensvariabler vilket betyder att de bara kan referera ( peka ) till instanser, de innehåller inte själva instansen. Med det i åtanke inser vi att pelle och gustav refererar till samma instans (vi har ju bara skapat en instans med hjälp av new). Om vi förändar instansen via referensen pelle så blir förändringen också synlig hos gustav. Om vi vill att pelle och gustav ska innehålla identiska men skilda instanser så måste vi skapa två instanser och sedan kopiera det enas egenskaper till det andra, instansvariabel för instansvariabel. Det är mycket farligt att ha två referensvariabler som refererar till samma instans. Undvik det. Jämförelse Samma problem som ovan uppstår om vi vill jämföra två objekt. Skriver vi if (pelle == gustav) så undersöker vi om referenserna pelle och gustav är lika. Referenserna är lika endast om de pekar på samma objekt. Om vi istället vill jämföra om två objekt är lika i den betydelsen att de har identiska instansvariabler så måste vi jämföra de enskilda variablerna var för sig (vilket bäst görs i en metod). 6
Exempel Vi ska nu gå igenom ett litet större exempel den studentklass som vi pratade om under den andra föreläsningen. Figure 2: Ett UML-diagram över en klassen Student. import java.util.scanner; public class Student { // Deklaration av instansvariabler private String name, address, zipcode, city; private String [] courses = new String[100]; private int credits; // Programmets metoder ska stå här // Vi tittar dem på under nästa föreläsning // main-metoden // Kontrollerar programmets förlopp. public static void main(string [] arg) { // Deklarera två personobjekt Student student1 = new Student(); Student student2 = new Student(); // Instanserna ska få namn student1.addname("erika Pettersson"); student2.addname("kalle Eriksson"); 7
// Student1 ska få en adress student1.addaddress("djäknegatan 87"); student1.addzipcode("75425"); student1.addcity("uppsala"); // student2 har klarat progi student2.addcourse("programmeringstekniki, HT07"); student2.addcredits(6); // Skriv ut information om objekten System.out.println(student1.toString()); System.out.println(student2.toString()); } } // Slut på klassdefinitionen --------------------------- Resultat: Erika Pettersson Djäknegatan 87 75425 Uppsala Inga avklarade kurser Kalle Eriksson Ingen gatuadress Inget postnummer Ingen postort Avklarade kurser: ProgrammeringsteknikI, HT05 Totalt antal poäng: 4 Först importerar vi Scanner-klassen för enklare in- och utmatningar. Vi definierar sedan klassen och kallar den Student. Efter klassdefinitionen följer klassens instansvariabler, två strängar som kan innehålla studentens namn och address, en array som kan kan hålla strängar för avklarade kurser (tänk en byrå där man kan stoppa en kurs i varje låda, vi kommer att prata mer arrayer längre fram) samt en int för avklarade poäng. Efter instansvariablerna så kommer metoderna. Eftersom vi ännu inte gått igenom metoder så låter jag platsen vara tom. Sista i klassen kommer sedan main-metoden, den metod som alltid utförs ( dirigenten ). Mainmetoden börjar med att vi deklarerar två referensvariabler och sedan skapar ett studentobjekt för respektive variabel. Vi ger sedan studenterna varsitt 8
namn och en adress. Efter detta låter vi den ena studenten klara av Prog1 och vi för in detta i registret. Slutligen skriver vi ut den information som finns om studenterna i registret med hjälp av tostring-metoden. En tostring-metod är en slags standardmetod som omvandlar värdena på ett skapat objekts instansvariabler till en String som man sedan kan skriva ut. Det är god praxis att alltid inkludera en tostring-metod i sin klass. Eftersom instansvariablerna är skyddade (inkapslade) från andra klasser behöver så vi också en metod för att skriva ut deras värde inifrån en annan klass. Hur själva utskriften ska se ut finns det dock ingen standard för, utan det får programmeraren själv bestämma. 9