Klasser

Vad är en klass?

En klass kan sägas vara en ritning eller beskrivning hur en viss typ objekt ser ut (innehåller). Beskrivningen omfattar dels egenskaper i form av data och dels operationer.

De "tekniska" komponenterna i en klass är instansvariabler och metoder. Vanligen använder man instansvariabler för att representera egenskaper och metoder för operationer (även om inte gränsdragningen är kristallklar)

Exempel: I klassen Turtle finns egenskaper som storlek, färg, position och färdriktning. Alla dessa är representerade med instansvariabler.

Med metoder kan man fråga en specifik padda om aktuella värden på egenskaperna (ex getXPos), ändra på (en del av) egenskaperna (ex setWidth) samt be paddan röra sig på olika sätt (ex forward).

En speciell typ av metoder är de så kallade konstruktorerna.

Hur skriver man en klass?

I en klass finns alltså instansvariabler, konstruktorer och metoder. Dessa kan skriva i vilken ordning som helst (man kan inte ha metoder inuti metoder) men för att underlätta läsbarheten skall man hålla sig till följande ordning: läsbarheten av klassen.

  1. Instansvariabler som så gott som alltid skall deklareras som private (eller protected).
  2. Konstruktorer
  3. Övriga metoder - gärna i bokstavsordning om klassen har många metoder.
  4. Om klassen har en main-metod så placeras den lämpligen sist.

Vi kommer använda nedanstående, antagligen bekanta, klass som exempel. Den kan, trots sin enkelhet, illustrera många saker. Observera att klassen har ett "skönhetsfel" som vi rättar till under avsnittet om konstruktorer.

public class Dice { // Instance variables private int numberOfSides; private int value; // Constructors public Dice(int numberOfSides) { if (numberOfSides < 2) { throw new RuntimException("Illegal number of sides: " + numberOfSides); } this.numberOfSides = numberOfSides; roll(); } public Dice() { numberOfSides = 6; roll(); } // Methods public int roll() { value = (int)(Math.random()*numberOfSides) + 1; return value; } public int getValue() { return value; } public int getNumberOfSides() { return numberOfSides; } public String toString() { return value + "/" + numberOfSides; } public boolean equals(Dice d) { return this.value == d.value && this.numberOfSides == d.numberOfSides; } public int compareTo(Dice d) { return this.value - d.value; } }

Instansvariabler

Dessa deklareras vanligen (dvs alltid) först i klassen. De är åtkomliga från alla metoder men kan döljas genom att samma namn används i en lokal deklaration (formell parameter eller lokal variabel). I så fall man får sätta this. före namnet för att ange att det är instansvariabeln man avser. Se den första konstruktorn i Dice!

Eftersom instansvariablerna existerar i hela klassen är det viktigt att de har tydliga, beskrivande namn.

Som instansvariabler skall man bara ha saker som verkligen är egenskaper hos objekten. Att ha loopvariabler och andra arbetsvariabler i metoder som instansvariabler är helt förkastligt!

Instansvariablerna skall normalt deklareras som private (eller möjligen protected).

Alla instansvariabler får ett initialvärde som är typiskt 0, 0., '\0', false eller null beroende på typ. Observera att motsvarande inte gäller för variabler som deklareras lokalt i metoder!

Det går också att tilldela instansvariablerna initialvärden i deklarationen men detta görs oftast bäst i konstruktorerna.

Konstruktorer

En konstruktor ser ut som en metod men den saknar typ och heter samma sak som klassen (alternativt kan man säga att den har en typ som är klassen den finns i men saknar namn).

Konstruktorerna definierar vad som skall hända när ett objekt skapas.

En uppenbar uppgift är att ge instansvariablerna startvärden men de bör också kontrollera att värdena är orimliga. Se den den första konstruktorn i Dice. (Satsen throw new RuntimeException("message") medför att programmet avbryts med felutskrift)

Man kan ha flera konstruktorer som då måste ha olika antal eller typer på parametrarna. I exempelklassen finns en konstruktor där man anger önskat antal sidor som parameter och en annan utan parameter. I det senare fallet får man en 6-sidig tärning.

Man kan låta en konstruktor utnyttja en annan genom anropet this(parametrar). I klassen Dice kan alltså den andra konstruktorn skrivas

public Dice() { this(6); }

Även om det i denna lilla kod inte spelar så stor roll så är det mycket bättre att göra så. Det följer principen att man inte ska upprepa identisk eller nästan identisk kod utan finna ett sätt att skriva koden på ett ställe och sedan anropa det "stället". Om man t ex upptäcker fel eller av annat skäl vill ändra i koden så behöver man bara göra det på ett ställe. Om koden finns på många platser utspridd i programmet är risken stor att man glömmer något.

Av samma skäl ordnar konstruktorn ett slumpmässigt startvärde genom att anropa metoden roll i stället för att kopiera in koden från roll.

Om någon eller några av parametrarna till en konstruktor är en objektreferens så skall man tänka på att den som anropade konstruktorn troligen också har en referens till objektet i fråga. Det gör det möjligt att manipulera objektet utifrån trots att den egna instansvariabeln är deklarerad som private. Om detta skall förhindras måste konstruktorn göra en egen kopia av objektet.

Instansmetoder

Instansmetoder (oftast säger man bara metoder) anropas via en objektreferens (t ex t.move(100) där t refererar ett Turtle-objekt). Inom klassen kan man anropa metoderna med this.metodnamn(parametrar) men det går också bra att utelämna this..

Metoderna är ofta deklarerade som public men det ibland finns det skäl att göra dem som private eller protected.

Olika metoder kan ha samma namn under förutsättning att de har olika antal och/eller typer på parametrarna. (Ex: int distance(Turtle t) och int distance(int x, int y)).

Metoden roll i tärningsklassen sparar tärningens värde i instansvariabeln value men returnerar också värdet. Det gör att den kan anropas på två olika sätt.
Exempel:

Dice d = new Dice(); d.roll(); int v = d.roll();

I första fallet slår man tärningen men tar inte emot värdet (men kan naturligtvis fråga efter det i efterhand med getValue()). I det andra fallet slår man den och tar emot värdet direkt.

Särskilda metoder

Metoden toString

Alla klasser har en toString-metod som ärvs från den generella basklassen Object. Den som ärvs är dock sällan särskilt upplysande. Om man enkelt vill kunna får reda på tillståndet hos ett objekt (och det vill man oftast åtminstone medan man utvecklar programmet) bör man alltså skriva sin egen toString. Denna metod anropas automatiskt i många situationer (t ex när man lägger ihop en sträng och ett objekt med +). För att man skall ta över den ärvda metoden så måste den exakt ha metodhuvudet:

public String toString() { ... }

Vad som skall returneras av metoden bestämmer man själv.

Kom ihåg att toString-metoder INTE skriver ut något - de returnerar bara en sträng. Anroparen avgör vad som skall göras med resultatet!

Metoden equals

Operatorn == på referensvariabler returnerar true om det är samma fysiska objekt, annars false. Ofta vill man ha något annat kriterium på att två objekt är lika. Två tärningsobjekt vill man kanske betrakta som lika om de visar samma värde och har samma antal sidor medan två personobjekt antagligen är lika om de har samma personnummer.

För att definiera vad man menar med lika brukar metoden equals användas. Se exemplet för hur den kan se ut för tärningsobjekt.

I en Person-klass där personnumret är lagrat som en sträng skulle metoden kunna ha utseendet

public boolean (Person p) { return this.pnummer.equals(p.getPnummer()); }

(Strängar har alltså en fungerande equals-metod)

Många av Javas inbyggda klasser använder metoden equals och har man inte skrivit en egen används en som ärvs från basklassen Object. Den ärvda metoden gör sällan vad man vill så man måste skriva en egen om man vill att objekt skall kunna jämföras.

Generellt:

Hur den skall definieras beror naturligtvis på tillämpningen.

Metoden compareTo

Denna metod används för att avgöra vilken ordning objekten ha t ex i en sortering. Metoden ärvs inte från basklassen Object men den finns i många standardklasser (t ex i klassen String). Metoden tar emot ett objekt (t ex i parametern p, på samma sätt som equals) och returnerar ett negativt tal om det egna objektet anses mindre än p, ett positivt tal om det egna objektet anses större än p och 0 om objekten anses lika (bör vara liktydigt med att equals skulle returnera true) .

Exempel: Antag att klassen Person bl a har ett attribut String lastname och ett int age Om vi vill att efternamnet skall styra ordningen kan vi utnyttja String-klassens compareTo:

public int compareTo(Person p) { return this.lastname.compareTo(p.lastname); }

Om vi i stället vill att åldern skall styra ordningen kan metoden definieras

public int compareTo(Person p) { if (this.age < p.age) { return -1; } else if ( this.age > p.age) { return 1; } else { return 0; } }
eller, kortare,
public int compareTo(Person p) { return this.age - p.age; }

Se också compareTo-metoden i klassen Dice som baserar sitt svar på tärningsvärdet oavsett antalet sidor.

Så kallade get-metoder

Metoder som returnerar information om ett objekt brukar kallas getmetoder. Det kan röra sig om värdet på någon instansvariabel (t ex geValue i tärningsklassen, int getAge()) i Person-exemplet ovan eller getXPos() i Turtle-klassen men den kan också vara resultatet av någon beräkning som görs (t ex int getDistanceToOrigo()).

Metodnamnet behöver inte heller inledas med get även om det är en bra upplysning av vad metoden gör. (Metoden som berättar hur stor en ArrayList heter t ex size(), inte getSize())

Om man har en get-metod som returnerar en objektreferens måste man vara medveten att man då utelämnar det private-deklarerade attributet till omvärlden. Om detta skall förhindras måste man skapa en kopia av objektet och returnera en referens till denna.

String-objekt kan alltid riskfritt lämnas ut eftersom dessa är "opåverkbara".

Så kallade set-metoder

Metoder som ändrar ett objekts status (position, storlek, ...) kallas set-metoder. Man skall givetvis överväga om det skall vara tillåtet att ändra på statusen på detta sätt (man vill antagligen inte tillåta metoden setValue(int v) som ändrar en tärnings värde).

Samma övervägande man gör för get-metoder bör göras även för set-metoder när det gäller objektreferenser. Lagrar man en referens till ett objekt som man fått som parameter så har man ingen kontroll på vad omvärlden gör med objektet. Om man behöver garantera attributets integritet måste skaffa en egen kopia av det.

Klassmetoder och klassvariabler

Om variabler eller metoder deklareras som static hör de inte till enskilda objekt utan är gemensamma för klassen som helhet och kallas därför för klass-variabler respektive klass-metoder. Eftersom de inte hör till något objekt kommer de inte åt instansvariablerna eller instansmetoderna direkt utan måste gå via en objektreferens.

main-metoden måste alltid vara static men den kan naturligtvis skapa objekt och via objektreferenser använda instansmetoder och instansvariabler.

I static-metoder finns naturligtvis inget this.

Valid CSS!