Göm menyn

Labb 3: Intro till objektorientering i Java

Syfte

I denna labb kommer vi att konstruera arvshierarkier via arvsmekanismen (inheritance) i Java. Med arv kan vi modellera att en klass är ett specialfall av en annan klass. En lastbil är t.ex. en sorts bil, och en länkad lista är en sorts lista. Detta kan kontrasteras med sammansättning (composition), som vi tidigare har vi använt för att modellera det faktum att ett objekt har något: En kö har en lista där den lagrar sina element, men den är inte en lista.

Vi introducerar också fler funktioner i IDEA som underlättar programmering. Eftersom man ofta inte modellerar perfekt från början fokuserar vi på funktionalitet som hjälper till när man upptäcker problem och felaktigheter. Därför inför vi till en början också vissa sådana problem och felaktigheter i koden. Se inte detta som ineffektivitet utan ett sätt att lära sig effektivt underhåll av kod!

Labben utförs enskilt. Vi hjälper till med flera handgrepp i programmeringen, men det är viktigt att ni reflekterar över varför ni gör som ni gör.

Förberedelser

Gör bara denna labb om (1) du har gått på föreläsningen om ärvning, eller (2) du redan är Java-programmerare och känner till hur ärvning fungerar i Java.

I annat fall: Vänta.

Deadlines och krav

  1. Labb 3 examineras via redovisning utan kodinlämning.

  2. Labb 3 ska redovisas senast 140221. Därefter ges en ny chans vid projektdemo i maj/augusti.

Översikt / motivation

Föreställ dig att du skall skapa ett vektorbaserat grafikprogram så som Inkscape eller Adobe Illustrator. Då kommer du att behöva ett antal klasser som representerar vektorbaserade former, t.ex. cirklar och rektanglar.

Vi inser att olika former har olika egenskaper, men också många likheter. De ha en position där de skall målas ut, en färg, och kanske också beskrivning av dess ram och innehåll. Kanske har formen en bild som ska visas inuti, ska den vara fylld med ett mönster, osv.

Vi kommer nu att modellera sådana former för att undersöka hur ärvning kan vara till hjälp för att representera både likheter och skillnader. Vi går inte vidare till att måla ut formerna på skärmen – grafik undersöks istället i nästa labb.

Övning 3.1: Ett gränssnitt för former

Syfte

Vi ska nu se hur ett interface kan användas för modellera ett allmänt programmeringsgränssnitt som kan delas av flera klasser, t.ex. former.

Bakgrund 3.1.1: Shape Interface

Vi kommer att behöva många olika typer av former, men programmet vill kunna hantera dessa på ett gemensamt sätt. Till exempel vill vi att man ska kunna ha en "lista av former", där vi inte behöver ange om det är kvadrater, cirklar, romber eller 18-hörningar som finns i listan. Vi åstadkommer detta genom att skapa gränssnittet Shape, som ska implementeras av samtliga formklasser. Då kan vi helt enkelt använda en "lista av Shape", och denna lista accepterar alla former (men enbart former, inte t.ex. strängar).

När vi plockar ut en Shape ur en sådan lista vill vi kunna göra vissa saker med den utan att nödvändigtvis veta exakt vilken sorts Shape det handlar om. Vi vill till exempel kunna säga till en godtycklig Shape att måla ut sig på skärmen. Vi åstadkommer detta genom att skapa metoden draw() redan i Shape. Detta ses som ett "krav" att alla klasser som implementerar Shape måste tillhandahålla en sådan metod. Då behöver inte utomstående kod känna till några detaljer om hur t.ex. en 18-hörning ska ritas ut, eller ens att formen är en 18-hörning. Det räcker att veta att man har en Shape och att varje Shape kan rita ut sig själv med draw().

Att göra 3.1.1: Shape Interface

  1. Tidigare klasser låg i paketet se.liu.ida.dinadress.tddd78.lab2. Högerklicka på paketet tddd78, välj New | Package och skapa lab3. Detta ska hamna på samma nivå som lab1 och lab2 i klassträdet.

  2. Skapa gränssnittet Shape i paketet lab3 genom att som förut högerklicka på paketet och välja New | Java Class, men nu väljer vi Interface i drop-down menyn som finns i fönstret Create New Class som kommer fram.

  3. Lägg till en public metod som heter draw(). Den skall inte ha några inparametrar och inte heller lämna något returvärde.

Bakgrund 3.1.2: Circle

Vår första form är en cirkel. Den skall implementera gränssnittet Shape.

Att göra 3.1.2: Circle

  1. Skapa klassen Circle.

  2. Lägg till de publika fälten int fälten x, y, layer och radius.

  3. Skapa ett publikt fält color av typen java.awt.Color.

    Abstract Window Toolkit (AWT) är Javas ursprungliga grafiksystem. Anledningen till att vi skriver hela java.awt.Color är att det finns många Color-klasser i olika paket och vi vill testa ett alternativt sätt att få fram exakt rätt klass. Om du ställer markören i Color så kommer IDEA att visa en glödlampa till vänster. Klicka på den och välj replace qualified name with import så kommer IDEA att istället importera klassen och vi kan använda namnet utan att ange paketdelen (java.awt).

  4. Skapa en konstruktor som tar in parametrar och tilldelar värden till alla fält.

  5. Nu skall vi se till att Circle blir en Shape så att vi senare kan rita ut den utan att behöva referera till den som en Circle. På class-raden lägger vi därför implements Shape efter Circle.

    Detta leder genast till en röd understrykning av raden eftersom IDEA hjälper oss att inte glömma bort att implementera draw() metoden som ju krävs av en Shape.

  6. Placera markören där du vill ha de metoder som krävs för Shape. Tryck Alt+Insert och välj Implement Methods och sedan draw(). Då fyller IDEA i rätt metodbeskrivning. För enkelhets skull kommer vi inte att måla ut något i den här labben utan bara att skriva ut innehållet i de former som skall målas ut. Skriv därför ut "Ritar: " + this med System.out.println().

  7. Nu minns vi att standardutskriften för ett objekt inte var särskilt användbar. Använd IDEA för att generera en toString() (Alt+Insert toString() som innehåller värdet på alla fält.

Info: Extract Variable

Genom att markera ett uttryck eller stå i en variabel kan man snabbt få IDEA att introducera en variabel som representerar detta. Man gör det med Refactor | Extract | Variable: Ctrl-Alt-V. Det är användbart t.ex. då man vill skapa en variabel snabbt. Antag att vi vill ha en array med 5 Random-objekt. Vi skriver då

   new Random[5]

Efter Ctrl-Alt-V blir detta

   final Random[] randoms = new Random[5];

Extract Variable är också användbart om man har ett långt uttryck som man vill dela upp, t.ex. en if-sats där villkoret är väldigt långt. Genom att markera en del av den och ta extract variable skapas en variabel som tilldelas värdet av deluttrycket och sedan ersätts detta i if-satsen med variabeln.

Bakgrund 3.1.3: Test

Vi gör nu ett första test av gränssnittet och klassen. Detta ska göras tidigt för att vi inte ska låta eventuella fel få nya följdfel när vi fortsätter programmera.

Att göra 3.1.3: Test

  1. Skapa klassen TestShapes och en main()-metod i den.

  2. I main() skall vi skapa en lista av Shape. Skriv new ArrayList<Shape>() och testa IDEAs Extract Variable enligt tidigare inforuta.

    Skapa ett par cirklar och lägg dem i listan (med metoden add()).

  3. Skriv en for-loop som går igenom varje Shape i listan (live-template iter = iterate collection) och anropar draw() för varje objekt. När man fyller i en live-template kan man använda Tab för att navigera mellan variabler som namnges.

  4. Kör programmet och verifiera att du får ut rätt utskrifter.

Bakgrund 3.1.4: Rectangle

Vår nästa form är en rektangel. Även den skall implementera gränssnittet Shape.

Att göra 3.1.4: Rectangle

  1. Skapa klassen Rectangle.

  2. Skapa de publika fälten int fälten x, y, layer, width och height. Skapa även ett publikt fält color precis som för Circle.

  3. Låt IDEA generera en konstruktor som tar in parametrar och tilldelar värden till alla fält.

  4. Implementera gränssnittet Shape och metoderna draw() och toString() analogt med hur det gjordes för Circle.

  5. Utöka testprogrammet så det också lägger några rektanglar i listan. Testa.

Bakgrund 3.1.5: Text

Vår sista form är en text. Även den skall implementera gränssnittet Shape.

Att göra 3.1.5: Text

  1. Skapa klassen Text.

  2. Lägg till de publika fälten int fälten x, y, layer och size. Skapa även ett publikt fält color precis som för Circle. Texten skall lagras i en publik sträng.

  3. Låt IDEA generera en konstruktor som tar in parametrar och tilldelar värden till alla fält.

  4. Implementera gränssnittet Shape och metoderna draw() och toString() analogt med hur det gjordes för Circle.

  5. Utöka testprogrammet så det också lägger några cirklar i listan. Testa.

Bakgrund 3.1.6: Paint

Vi skapar nu "ritprogrammet" som håller koll på vilka former som finns och kan rendera bilden. Mycket av denna funktionalitet finns redan i testprogrammet.

Att göra 3.1.6: Paint

  1. Skapa klassen Paint genom att döpa om TestShapes (ställ markören i klassnamnet och gör Refactor | Rename, Shift-F6).

  2. Gör Shape-listan till ett fält istället för en lokal variabel, så att den inte försvinner efter ett metodanrop utan finns kvar så länge ett Paint-objekt finns kvar.

    Initialiseringen av fältet kan antingen ligga där fältet deklarerades (genom att vi helt enkelt flyttar hela raden ut från metoden) eller i en ny konstruktor för Paint.

  3. Skapa och implementera metoden public void addShape(Shape s) så att den lägger till former i Paint-objektets shape-lista.

  4. Skapa och implemetera en metod show() som går igenom shapes i "det nuvarande" Paint-objektet och ritar ut alla former.

  5. Låt main() testa hittills gjorda former genom att skapa ett Paint-objekt, lägga till ett par former av varje typ till detta objekt via dess addShape()-metod, och sedan anropa dess show()-metod.

Övning 3.2: Dölja information, ändra representation

Syfte

Hittills har vi låtit all information i våra klasser vara public (alla kan komma åt den och ändra den). Så borde det inte vara! Vi vill istället dölja informationen så ingen utomstående kommer åt den.

(Varför gjorde vi inte rätt från början? Dels lär man sig mer från misstag, dels vill vi visa hur man fixar sina misstag och att de inte behöver leva kvar i koden.)

Bakgrund 3.2.1: Dölja information

I slutet av main()-metoden i Paint skulle vi kunna skriva:

   	Circle cir = new Circle(10,10,1,25, Color.BLACK);
	cir.radius = -1;
	cir.draw();

Detta skapar en Circle som vi sedan ändrar radien på "utifrån". Men hur skall Circle rita en cirkel med negativ radie? För att förhindra felaktiga värden skulle det kunna finnas ett test i konstruktorn som såg till att radien var positiv, men det räcker inte för att garantera bra värden. Problemet är att fältet har publik åtkomst så det kan ändras när som helst, varifrån som helst.

En bra lösning på detta är att ändra åtkomsten till privat. Då skyddar vi klassen så att Circle själv kan bestämma att inga felaktiga värden kan tilldelas redan i konstruktorn. Med IDEAs hjälp kan detta åtgärdas snabbt.

Att göra 3.2.1: Dölja information

  1. Hoppa till klassen Circle genom att använda Navigate | Class: Ctrl-N och skriva "Cir", som expanderas automatiskt när du trycker Enter. Detta är en mycket användbar genväg. Eftersom vi redan har Circle i närheten kan vi även ställa markören i Circle och trycka Ctrl-B för att snabbt hoppa till definitionen.

  2. När vi är i Circle kan vi välja Refactor | Encapsulate Fields och fylla i alla fält samt avmarkera Set Access och Use accessors even when field is accessible.

  3. Gör samma sak i Rectangle och Text.

Nu är alla former skyddade ifrån yttre ändringar, och all kod som eventuellt hade hämtat värden med t.ex. minCirkel.x har automatiskt skrivits om till att hämta detta med minCirkel.getX() istället.

Eftersom vi inte har några andra funktioner som ändrar dessa värden så är en form fast när den väl skapats.

Bakgrund 3.2.2: Ändra internrepresentation

Vi har nu flera klasser som representerar positioner med hjälp av x- och y-koordinater. Eftersom denna typ av representation är vanlig och troligen kommer att ingå i många operationer i ett komplett grafikprogram kan det vara lämpligt att skapa en klass innehållande koordinaterna. Vi behöver då byta representation i formerna. Eftersom vi inte längre har publika x- och y-fält är detta inget problem.

Att göra 3.2.2: Ändra internrepresentation

  1. Skapa klassen Point med de privata int-fälten x och y. Skapa även en konstruktor som tar in parametrar för dessa. Skapa sedan setters/getters för dem (Alt-Insert / Getter and Setter).

  2. Vi vill inte påverka klasser som använder våra former. De skall inte behöva ändra sitt beteende bara för att vi designar om internt i formerna. Därför ändrar vi inte inparametrarna till formernas konstruktorer, utan de får fortfarande ta x och y som separata int-parametrar.

    Däremot byter vi ut själva lagringen i våra formklasser. Byt först ut deklarationerna av de publika fälten x och y mot en Point med namn koordinat. Sätt sedan denna till ett värde i konstruktorn genom att skapa ett Point-objekt från inparametrarna x och y.

  3. Skriv om getters för x och y i formerna så att de nu returnerar x och y genom att hämta dem ur koordinat.

  4. Ändra i toString()-metoderna så att koordinat.getX() och koordinat.getY() används istället för x och y.

Övning 3.3: Shapes med abstrakt klass

Syfte

Vi inser nu att vissa delar av våra klasser är väldigt lika, och att vi kunde tjäna på att ha en gemensam implementation för dessa. Vi fortsätter därför med att skapa en abstrakt klass med sådan gemensam kod.

Återigen: Vi kunde ha gjort "rätt" från början, men eftersom man inte alltid gör det, behöver man lära sig hur man fixar sina problem!

Bakgrund 3.3.1: AbstractShape

Vi inser nu att många fält och metoder är samma i alla formklasser.

Det hade varit smidigt om man kunde hantera detta i Shape istället. Ett interface tillåter dock inte detta eftersom det bara specificerar vilka metoder som måste finnas. Lösningen är att använda oss av en abstrakt klass istället. Detta ger också flexibilitet: Man kan välja att ärva från abstrakta klassen eller att implementera gränssnittet direkt.

Vi skapar då en ny klass AbstractShape som kommer att kunna innehålla även fält och metoder. Sedan kan formerna ärva sitt beteende ifrån denna. En abstrakt klass har även den egenskapen att man inte behöver implementera alla metoder i den. Eftersom vi inte vet hur draw() skall se ut kan vi lämna till subklasser att implementera denna. Detta får som effekt att man inte kan skapa objekt av klassen AbstractShape eftersom den inte är fullständigt specificerad.

Att göra 3.3.1: AbstractShape

  1. Skapa en ny klass public abstract class AbstractShape. Se till att den implementerar Shape, alla subklasser till AbstractShape skall ju vara Shapes. Klassen kommer från början att vara tom.

  2. Byt ut implements Shape mot extends AbstractShape i klasserna Circle, Rectangle och Text. Detta innebär att klasserna blir AbstractShape, men de kommer fortfarande att bete sig som Shape eftersom detta gränssnitt implementeras av AbstractShape.

  3. Nu är vi redo att flytta ut definitionen av koordinat till den abstrakta klassen.

    Navigera till Circle och välj Refactor | Pull Members Up. Välj att medlemmar ska dras upp till AbstractShape. Markera koordinat så kommer den att flyttas från subklassen Circle upp till superklassen AbstractShape. IDEA måste nu göra några ändringar för att koden fortfarande ska fungera.

    1. Fältet koordinat kommer att bli protected i AbstractShape istället för private, så att subklasser fortfarande kan komma åt den.

    2. Eftersom koordinat nu ligger i AbstractShape är det upp till AbstractShape att hantera initialisering av den. Tilldelningen this.koordinat = new Point(x,y) i konstruktorn för Circle ersätts därför med anropet super(x,y), som vidarebefordrar parametrarna från Circle upp till AbstractShape:s konstruktor. (Ett anrop till super() ligger alltid först i en konstruktor.)

    3. IDEA skapar också automatiskt en konstruktor för AbstractShape och lägger till den kod som behövs för att initialisera koordinat.

  4. Navigera till Rectangle och Text och ta bort koordinat ur dessa samt ersätt skapandet av koordinat med ett super()-anrop motsvarande det i Circle-konstruktorn.

  5. Flytta getters för x och y från Circle till AbstractShape och ta bort dem från Rectangle och Text. Nu har vi blivit av med den duplicerade koden.

Övning 3.4: Likhet, identitet och namn

Syfte

Vi ska nu se på hur man definierar om två objekt är "lika". Vi ska också göra koden mer läsbar genom att döpa om ett fält.

Bakgrund 3.4.1: equals() och hashCode()

I Java används operatorn "==" för att jämföra värden, t.ex. objektpekare. Därför skulle följande kod skriva ut false trots att de två jämförda objekten har identiskt "innehåll".

    Circle c1 = new Circle(1,1,1);
    Circle c2 = new Circle(1,1,1);
    if (c1 == c2) System.out.println("true");
    else System.out.println("false");

Om man istället vill jämföra vad objekten representerar används i Java metoden equals() som jämför ett objekt med ett annat godtyckligt objekt. En exempelimplementation av equals() skulle kunna vara:

class Circle {
    Point center;
	int r;
	...
    public boolean equals(Object other) {
        // Null can't be equal to me!
        if (other == null) return false;

        // Does the other one have the same class?
        // Otherwise we're not equal!
        if (other.getClass() != this.getClass())
            return false;

        // Use casting to get a pointer of type Circle
        Circle that = (Circle) other;

        // Check if all fields are equal
        return this.center.equals(that.center) && 
               this.r == that.r;
    }
}

Under förutsättning att liknande kod finns i Point skulle denna koden kunna användas för att få true ur följande kod:

    Circle c1 = new Circle(1,1,1);
    Circle c2 = new Circle(1,1,1);
    if (c1.equals(c2)) System.out.println("true");
    else System.out.println("false");

Att göra 3.4.1: equals() och hashCode()

  1. Skapa equals()-metoder i alla formerna. Använd IDEA som hjälp via Code | Generate | equals()and hashCode(): Alt+Insert. När IDEA skapar equals() skapar den också hashCode(). Denna metod kommer att diskuteras mer på föreläsningarna.

    När du väljer att skapa equals() får du välja vilka fält du anser vara relevanta för att två objekt skall vara lika. Om det finns icke-primitiva fält, t.ex. color som är en objektpekare, kommer IDEA att fråga om de kan garanteras att inte vara null. Detta är för att inte generera onödiga null-tester. Om du är osäker så ange att fälten kan vara null (detta kan vara mindre effektivt men ger alltid rätt resultat).

  2. Titta på de generarade equals()-metoderna. Verkar de rimliga? Man kan aldrig vara säker på att automatgenererad kod helt överensstämmer med vad man själv tänkte. Fundera på hur och föreslå hur man kan hantera position.

Bakgrund 3.4.2: Namnbyte

Ibland inser man sent i ett projekt att man vill ändra namnet på något fält. Nu inser vi att koordinat var ett dåligt namn: Dels är det på svenska, dels är det ett koordinat-par och inte en enda koordinat. Därför vill vi byta det mot position.

Att göra 3.4.2: Namnbyte

  1. Navigera till AbstractShape och ställ markören i koordinat samt välj Refactor | Rename:Shift+F6. Skriv in namnet position. Notera att IDEA automatiskt byter alla förekomster av koordinat mot position. Dels gör den det i getters och i AbstractShape, men även i alla toString()-metoder i subklasserna. Skulle IDEA detektera "koordinat" i en kommentar kommer den att låta dig välja om detta avsåg ett fältnamn och skall ändras eller bara var text som "råkade" innehålla samma bokstäver.

Avslutning

Här slutar tredje laborationen. Visa slutresultatet för din handledare och passa på att fråga om det är något du undrar över. Skulle handledaren vara upptagen går det bra att börja med laboration 4.

Labb av Mikael Nilsson, Jonas Kvarnström 2014.


Sidansvarig: Jonas Kvarnström
Senast uppdaterad: 2015-11-09