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 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 ger fortfarande hjälp med många handgrepp och detaljer i programmeringen, men det är viktigt att ni också reflekterar över varför ni gör som ni gör.

Förberedelser, krav och deadlines

Börja bara med denna labb om du redan har lärt dig hur ärvning fungerar, vad abstrakta metoder är, och så vidare. I annat fall: Läs på i kurslitteraturen eller vänta till efter föreläsningen om ärvning.

Labben examineras via demonstration utan kodinlämning senast vid deadline.

Genomgående uppgift: Grafikprogram

Föreställ dig att du ska 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.

Olika former har olika egenskaper, men också många likheter. De har en position där de ska 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, kanske 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.

Övning 3.1: Vår första form

Bakgrund 3.1.1: Circle

Vi vill komma igång snabbt! Otåliga som vi är bestämmer vi oss för att skapa en cirkelklass, utan att tänka så mycket på framtiden eller hur den ska kopplas till övriga formklasser.

Att göra 3.1.1: Circle

  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 klassen Circle i paketet lab3.

  3. Lägg till de publika int-fälten x, y och radius.

  4. Lägg till 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 java.awt.Color så kommer IDEA att visa en glödlampa till vänster. Klicka på den eller tryck Alt+Enter, och välj Replace with import, så kommer IDEA att istället importera klassen och vi kan använda namnet utan att ange paketdelen (java.awt).

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

Bakgrund 3.1.2: Test

Vi gör nu ett första test av klassen. Även om vi är otåliga ska detta 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.2: Test

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

  2. I main() ska vi skapa en lista av Circle. Skriv new ArrayList<Circle>(), låt IDEA importera ArrayList, och testa IDEAs Extract Variable enligt nedan.

    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. Anta att vi har skrivit i koden:

       new ArrayList<Circle>()

    Efter att vi trycker Ctrl-Alt-V och väljer namn på variabeln får vi:

       final ArrayList<Circle> circles = new ArrayList<>();

    IDEA har själv hittat på ett variabelnamn, som man givetvis kan byta ut. Notera att "<>" står för "samma typparametrar som tidigare på raden", dvs. "<Circle>".

    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.

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

  4. Använd live-template iter (iterate collection) för att skapa en for-loop som går igenom varje Circle i listan och för varje cirkel skriver ut dess x- och y-koordinater. När man fyller i en live-template kan man använda Tab för att navigera mellan variabler som namnges.

    Notera att iter använder en for-loop-variant som är mer lik Pythons:
    for (Type element : collection), som itererar en gång för varje element i en samling eller array.

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

Info: Klassdiagram

Nu kan vi använda ett klassdiagram i UML för att se strukturen hos de klasser vi har skapat. Här nedan syns att TestCircle skapar instanser av klassen Circle. Vi kommer att se fler UML-diagram senare.

Övning 3.2: Defensiv programmering

Bakgrund 3.2.1: Kontrollera parametrar

Hittills har vi låtit anropare skicka in vilka värden som helst när ett objekt skapas. På detta sätt kan vi till exempel skapa och rita ut en Circle med negativ radie:

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

Men hur ska en sådan cirkel kunna ritas ut på skärmen, när vi kommer till den grafiska delen av labben? Vi borde förbjuda sådana cirklar från att skapas.

Att göra 3.2.1: Kontrollera parametrar

  1. Hoppa från TestCircle till 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 en referens till Circle i närheten kan vi även ställa markören i Circle och trycka Ctrl-B för att snabbt hoppa till definitionen. Detta fungerar för alla klasser, metoder och fält, inklusive de från "språkets egna" klasser (t.ex. System).

  2. Lägg till följande rader överst i Circle-konstruktorn:

        if (radius < 0) {
            throw new IllegalArgumentException("Negativ radie!");
        }
    

    Detta är en exception, ett undantag. Vi kommer att gå genom mer om exceptions i kursen, men hittills räcker det att veta att denna kod avbryter konstruktorn och signalerar ett fel till anroparen. Eftersom konstruktorn avbröts skapas heller inget cirkelobjekt!

Detta exemplifierar två viktiga programmeringsbegrepp.

  • En klass har kontroll över sina objekt. Nu kan ingen skapa en cirkel med negativ radie.

  • Defensiv programmering. Vi ska inte tänka att "ingen galning skulle få för sig att ge en negativ radie, så det fallet kan vi ignorera". Man vet aldrig hur radien räknas ut eller vad anroparen har missat att tänka på (eller hur galen någon är, för den delen). Vi tar hand om alla tänkbara och otänkbara fall, så får vi färre svårhittade buggar.

Bakgrund 3.2.2: Kapsla in information

Vi har kommit en bit, men inte hela vägen. Hittills har vi låtit all information i våra klasser vara public (alla kan komma åt den och ändra den). Därför kan någon fortfarande skriva:

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

Detta skapar en Circle med positiv radie, som vi sedan ändrar på "utifrån". Att göra en kontroll i konstruktorn räckte alltså inte! 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.

(Varför gjorde vi inte rätt från början? Att man gör den här typen av misstag ibland är oundvikligt. Vi vill inte bara ge en tillrättalagd instruktion utan visa hur man fixar problem så att de inte behöver leva kvar i koden.)

Att göra 3.2.2: Kapsla in information

  1. I klassen Circle, välj Refactor | Encapsulate Fields. Bocka för alla fält samt avmarkera Set Access och Use accessors even when field is accessible.

All kod som hämtade värden med t.ex. minCirkel.x har nu automatiskt skrivits om till att hämta detta med minCirkel.getX() istället. Detta kan vi till exempel se i TestCircle.

Eftersom fälten nu är privata är det bara kod i Circle själv som kan ändra på dem efter att en cirkel skapas. Eftersom Circle aldrig gör det, är alla cirklar skyddade ifrån att deras fält ändras efter att de skapas.

Hade vi bockat för Set Access hade även setter-metoder skapats. Då hade man kunnat ändra värden "i efterhand", men bara genom dessa metoder, som då kunde innehålla samma typ av säkerhetskontroller som konstruktorn.

Övning 3.3: Gränssnittet Shape

Bakgrund 3.3.1: Gränssnittet Shape

Vi kommer inte att vara nöjda med cirklar utan kommer att skapa många olika typer av former, men vi 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).

Att göra 3.3.1: Gränssnittet Shape

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

  2. Nu ska vi se till att Circle implementerar Shape. På class-raden lägger vi därför till implements Shape.

    Eftersom Shape ännu inte har några metoder är detta tillräckligt för tillfället.

Info: Klassdiagram

Nu kan vi även illustrera att Circle realiserar (implementerar) gränssnittet Shape.

Bakgrund 3.3.2: Rita ut en Shape

Det gränssnitt vi har skapat kan användas för att koppla ihop klasser i en hierarki, men den säger absolut ingenting om vad de klasserna behöver kunna göra. Har vi en List<Shape> kan vi alltså plocka ut formerna ur listan, men sedan kan vi inte be dem göra något.

Vi behöver ha några metoder i Shape. 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() som deklareras 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.3.2: Rita ut en Shape

  1. Lägg till en public metod som heter draw(). Den ska inte ha några inparametrar och inte heller lämna något returvärde (vad skriver man för att indikera detta?). Som alla metoder i gränssnitt ska den också sakna "kodkropp" ({...}).

  2. Vi ser att draw() blir gul. Det finns nämligen klasser som säger sig implementera Shape men som saknar denna metod! Motsvarande varning kan ses när man går till Circle: IDEA stryker under "implements Shape" med röd färg.

    Placera markören någonstans i Circle 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 än, utan bara skriva ut "innehållet" i de former som ska målas ut. Låt därför metoden skriva ut "Ritar: " + this.

    (Vad är "@Override", som IDEA genererar? Detta är en annotering som visar att detta implementerar eller override:ar en metod som är definierad längre upp i klasshierarkin. Ni får veta mer om detta under en senare föreläsning.)

  3. Nu minns vi att standardutskriften för ett objekt inte var särskilt användbar. IDEA varnar till och med för detta genom att "this" blir gul ("Call to default 'toString' on 'this'").

    Använd IDEA för att generera en toString() (Alt+Insert toString()) som innehåller värdet på alla fält.

Bakgrund 3.3.3: Flera metoder i Shape

Vad gör vi med de metoder som redan fanns i Circle? Alla formklasser borde ju ha x- och y-koordinater samt en färg. Därmed borde även getX(), getY() och getColor() deklareras redan i Shape!

Att göra 3.3.3: Flera metoder i Shape

  1. Alternativ 1: Lägg manuellt till metoddeklarationer för getX(), getY() och getColor() i Shape. Se till att de har samma returvärden och parametrar som i Circle, men saknar metodkropp. Vi kan ju inte implementera en fullständig getX() här uppe i Shape, eftersom gränssnittet själv inte har något x att returnera!

  2. Alternativ 2: Gå till Circle. Använd Refactor | Pull Members Up. Se till att Shape är vald i den översta väljaren. Markera getX(), getY() och getColor() i medlemslistan. "Make abstract" är automatiskt vald, eftersom man inte kan flytta hela metodimplementationen till Shape – bara en abstrakt deklaration.

    Vilken ordning metoderna hamnar i kan bero på i vilken ordning man väljer dem i listan!

    Välj Refactor. Detta skapar rätt metoddeklarationer i Shape och lägger också automatiskt till @Override på metoderna i Circle.

Nu har vi deklarerat att alla Shape-klasser måste ha dessa metoder.

Bakgrund 3.3.4: Test

Vi gör nu ett första test av gränssnittet och klassen tillsammans. Detta gör vi i en ny testklass.

Att göra 3.3.4: Test

  1. Gå till TestCircle. Klona klassen med hjälp av F5. Kalla den nya klassen TestShapes. På det sättet får ni med strukturen i det gamla testprogrammet.

  2. I TestShapes.main() ska vi inte skapa en lista av Circle, utan en lista av Shape där vi så småningom även ska kunna lägga in rektanglar och andra former.

    Vi måste också ändra loopen på motsvarande sätt, så att den plockar ut godtyckliga former och inte kräver att få cirklar.

    Variabler bör så klart också döpas om, så vi inte har en lista av former som heter "circles". Ställ markören i ett variabelnamn och välj Refactor | Rename:Shift+F6. Skriv in det nya namnet på variabeln (eller välj ett av förslagen som kommer upp) och tryck enter. IDEA byter automatiskt alla förekomster av variabelnamnet.

  3. Nu bör slutet av koden se ut ungefär så här:

    	for (Shape shape : shapes) {
    	    System.out.println(shape.getX() + " " + shape.getY());
    	}

    Men nu har vi ju en riktig "ritmetod", så vi ersätter utskriftsraden med ett anrop till shape.draw().

  4. Kör TestShapes och verifiera att du får ut rätt utskrifter. Även det gamla testprogrammet TestCircle ska fortfarande fungera!

Info: Klassdiagram

Nu vill vi illustrera att det finns två olika klasser som skapar cirklar.

Övning 3.4: Flera former

Bakgrund 3.4.1: Rectangle

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

Att göra 3.4.1: Rectangle

  1. Skapa klassen Rectangle med de privata fälten x, y, width, height och color. Typerna vet ni sedan tidigare.

    Ge klassen en lämplig konstruktor.

    Skapa en toString()-metod.

  2. Implementera gränssnittet Shape och de metoder som krävs i detta gränssnitt.

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

Bakgrund 3.4.2: Text

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

Att göra 3.4.2: Text

  1. Skapa klassen Text med de privata fälten x, y, size (storleken i punkter), color och text (en String) samt en lämplig konstruktor och toString()-metod.

  2. Implementera gränssnittet Shape och de metoder som ingår i gränssnittet.

  3. Utöka testprogrammet på lämpligt sätt. Testa.

Info: Klassdiagram

Nu kan vi illustrera att en av testklasserna skapar cirklar, medan den andra skapar både cirklar, texter och rektanglar. Vi kan också visa att de olika formklasserna implementerar Shape.

Övning 3.5: Dela på kod med en abstrakt klass

Bakgrund 3.5.1: Kodlukter och upprepad kod

Vissa delar av de klasser som just har skapats är väldigt lika. Vi har upprepat deklarationer av till exempel x, y, color och motsvarande getters, och upprepning är sällan ett gott tecken. Det är inte direkt en bugg, eftersom programmet fortfarande kan fungera utmärkt. Men vi har en kodlukt, vilket kan definieras som ett symptom som tyder på ett djupare problem i koden. Koden luktar helt enkelt illa, och det måste vi fixa. (Återigen kunde vi så klart ha gjort rätt från början istället, men eftersom man inte alltid gör det, behöver man lära sig hur man fixar problem på ett effektivt sätt!)

Vi borde alltså ha en gemensam implementation. Det hade varit smidigt om man kunde hantera detta i Shape, men ett gränssnitt tillåter inte det: Gränssnitt anger vilka metoder som måste finnas men låter oss inte implementera dem direkt*. 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 den abstrakta klassen eller att implementera gränssnittet direkt.

Vi skapar därför 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 kan implementera en "generell" draw() för godtyckliga former 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.

(*Överkurs: Java 8 tillåter faktiskt vissa metoder att implementeras i ett gränssnitt/interface. Detta leder till mer komplicerade regler för overriding / prioritering av metoder, och vi hoppar därför över detta i den här grundkursen.)

Att göra 3.5.1: AbstractShape

  1. Skapa en ny "tom" klass public abstract class AbstractShape. Se till att den implementerar Shape – alla subklasser till AbstractShape ska ju vara Shapes.

  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 också att vara Shapes eftersom detta gränssnitt implementeras av AbstractShape.

  3. Nu är vi redo att flytta ut vissa definitioner till den abstrakta klassen.

    Navigera till Circle och välj Refactor | Pull Members Up. Välj att medlemmar ska dras upp till AbstractShape, inte Shape. Markera fälten x,y,color, som är gemensamma för alla former. Markera även motsvarande getters. Den här gången görs metoderna inte abstrakta: Implementationen ska följa med upp.

    Vi markerar inte radius eller getRadius(), eftersom bara cirklar har radier. Inte heller draw() eller toString(): De ska visserligen finnas i alla former men behöver ha en egen implementation i varje klass.

    Vilken ordning medlemmarna och konstruktorparametrarna hamnar i kan bero på i vilken ordning man väljer dem i listan! Blir det fel kan ni göra "undo" och göra om refactoring.

    Välj Refactor. IDEA gör nu några ändringar för att koden fortfarande ska fungera.

    1. De valda fälten blir protected i AbstractShape istället för private, så att subklasser fortfarande kan komma åt dem.

    2. IDEA skapar också automatiskt en konstruktor för AbstractShape.

    3. Eftersom fälten nu är flyttade är det upp till AbstractShape att hantera initialisering av dem. Vissa tilldelningar i konstruktorn för Circle ersätts därför med anropet super(x,y,color), som vidarebefordrar parametrarna från Circle upp till AbstractShape:s konstruktor. (Ett anrop till super() ligger alltid först i en konstruktor.)

  4. Tyvärr vet inte IDEA att den ska göra motsvarande ändringar i de andra klasserna.

    Navigera till Rectangle. Ta bort fälten x/y/color, ta bort motsvarande getters, och ta bort initialiseringen ur konstruktorn.

    Lägg sedan till super()-anrop motsvarande det i Circle-konstruktorn. Ta bort de tre getter-implementationer som nu blev onödiga eftersom samma kod redan ärvs ner från AbstractShape.

    Gör samma sak för Text.

Som ni märker kan det krävas lite jobb för att hålla koden i god form. Det är lätt hänt att man undviker det, men det förlorar man ofta på i det långa loppet. I framtida labbar kommer vi att ge komplettering för duplicerad kod av den här typen.

Info: Varför både Shape och AbstractShape?

Varför behåller vi både Shape och AbstractShape? Jo, om vi gör på det sättet får vi mer flexibilitet:

  • I Circle kan vi välja att ärva ner användbar kod från AbstractShape

  • I OtherShape kan vi välja att implementera hela Shape själva, kanske för att klassen redan ärver från OtherBase och alltså inte kan ärva från någon annan klass.

Övning 3.6: Likhet, identitet och namn

Bakgrund 3.6.1: equals() och hashCode()

I Java används operatorn "==" för att jämföra värden, och värdet av en "objektvariabel" är en pekare. Därför skulle följande kod skriva ut false: Eftersom c1 och c2 pekar på olika objekt har de olika värde, trots att de två objekten har har identiskt "innehåll" / state.

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

Om man istället vill jämföra objekten som pekas på, används i Java metoden equals() som jämför ett objekt med ett annat godtyckligt objekt. Så här kunde det se ut innan vi införde AbstractShape:

public class Circle implements Shape {
    ...
    public boolean equals(Object other) {
        // I am an object and can't be equal to "no object"!
        if (other == null) return false;

        // Does the other one have exactly 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.
        // For primitive types we use ==,
        // and for objects we use equals().
        return this.x == that.x && 
               this.y == that.y &&
               this.color.equals(that.color) && 
               this.radius == that.radius;
    }
}

Men nu när vi har AbstractShape har vi ju delat upp fälten i två metoder. Då måste det istället se ut ungefär så här, så att varje klass kan ta hand om sina egna fält:

public class AbstractShape implements Shape {
    public boolean equals(Object other) {
        // I am an object and can't be equal to "no object"!
        if (other == null) return false;

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

        AbstractShape that = (AbstractShape) other;

        return this.x == that.x && 
               this.y == that.y &&
               this.color.equals(that.color);
    }
}

public class Circle extends AbstractShape {
    public boolean equals(Object other) {
        // Are we equal according to the superclass?
        // Using "super.method(...)" calls the implementation that
        // was defined in the superclass: AbstractShape (above),
        // which tests that o!=null, that o has the same class,
        // and that x/y/color are equivalent.
        if (!super.equals(o)) return false;

        // Are all of my own "new" fields equal as well?
        return this.radius == that.radius;
    }
}

Då skulle följande kod skriva ut true:

    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.6.1: equals() och hashCode()

  1. Skapa equals()-metoder i AbstractShape. Använd IDEA som hjälp via Code | Generate | equals() and hashCode(): Alt+Insert. Vi är egentligen bara intresserade av equals(), men IDEA skapar också hashCode(). Denna metod används för vissa datastrukturer men är inte relevant för oss just nu.

    IDEA frågar: "Accept subclasses as parameter to equals() method?". Kryssa inte i den rutan. Detta diskuteras på föreläsningen.

    Därefter får du välja vilka fält du anser vara relevanta för att två objekt ska vara lika. Om två AbstractShape-objekt har samma koordinater men olika färg, ska de då anses vara likadana eller inte? Det avgör du själv utifrån programmets behov. Normalt anger man exakt samma fält när IDEA frågar vad som ska vara med i hashCode().

    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 något mindre effektivt men ger alltid rätt resultat).

  2. Titta på de skapade 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.

Övning 3.7: Grafiskt gränssnitt

Bakgrund 3.7.1: Rita ut former

Nu är det dags att se till att ritprogrammet faktiskt kan rita ut sina former.

När du kommer hit har vi med största sannolikhet inte hunnit fram till föreläsningarna om grafiska gränssnitt. Det är inget problem: Vi ska inte gå vidare och skriva ett fullfjädrat ritprogram, utan nöjer oss med att implementera ett mycket enkelt program som visar upp ett fönster, plus att skriva om utritningsmetoderna så att de faktiskt ritar i detta fönster. För detta kan vi ge ett enkelt kodskelett.

Att göra 3.7.1: Rita ut former

  1. Vi kallar en uppsättning former för ett diagram. Skapa en ny klass, DiagramComponent, enligt följande mönster. Detta ska bli en grafisk komponent som vet hur man ritar upp ett helt diagram på skärmen.

    import javax.swing.*;
    import java.awt.*;
    
    public class DiagramComponent extends JComponent
    {   
        @Override protected void paintComponent(final Graphics g) {
    	super.paintComponent(g);
    
    	// Rita upp alla former här!
        }
    }
  2. Lägg till ett privat fält shapes som innehåller en lista av former.

    Skapa en konstruktor som sätter shapes till en ny lista (detta har ni gjort förr!).

    Lägg till metoden public void addShape(Shape s), som ska addera en ny form till listan.

  3. Metoden paintComponent() kommer automatiskt att anropas av Java när det är dags att rita upp den här grafiska komponenten på skärmen. Er uppgift blir att fylla i den med kod som ritar upp alla former som finns i listan. För att göra detta använder man Graphics-objektet som man får som parameter. Namnet Graphics är egentligen lite missvisande. Egentligen kunde detta ha hetat Painter, eftersom det är den som har metoder för att rita ut pixlar, linjer och så vidare på skärmen, t.ex. drawLine().

    Men det är inte PaintComponent som ska ha "centraliserad" kunskap om hur varje typ av form ritas ut. Det ska formerna själva veta. Det första steget är därför att iterera över de former som finns i listan och för var och en av dem anropa draw(), så formen själv kan rita ut sig.

    Men draw() var ju bara en platshållare som just nu skriver ut lite text. Den metoden måste göras om för att rita med hjälp av ett Graphics-objekt. Det betyder i sin tur att metoden måste skrivas om för att ett Graphics-objekt som parameter.

    Även här kan IDEA hjälpa till. Ställ markören i ordet draw och tryck Ctrl-F6: Refactor | Change Signature. Tryck det gröna plusset och fyll in type Graphics, name g. Det talar om att metoden ska få en ny parameter, både i Shape och i alla tre konkreta implementationer. Klicka sedan i "use any var", vilket gör att IDEA också lägger till en parameter till anropen till draw() om det finns en Graphics-variabel tillgänglig där anropet sker. Tryck sedan Refactor.

    Denna refactoring kan även användas för att byta ordning på parametrar i metoddeklaration och alla metodanrop.

  4. Nu undrar vi om det här införde några problem. Tryck Ctrl-F9 för att kompilera. Jodå: TestShapes fungerar inte längre, eftersom den anropar draw() utan parametrar. Vi kommer snart att testa detta i vår nya grafiska klass istället, så vi kommenterar bort anropet och kompilerar om igen.

  5. Ändra implementationerna av draw() i alla tre konkreta formklasser. Kommentera bort utskrifterna och använd istället följande kodrader som ledning till vad ni ska göra:

        g.setColor(color);
        g.drawOval(x, y, width, height); // calculated from radius!
        g.drawRect(x, y, width, height);
        g.setFont(new Font("serif", Font.PLAIN, size));
        g.drawString(text, x, y);
  6. Nu behöver vi till slut ett fönster som kan visa upp diagramkomponenten. Vi diskuterar hur detta fungerar på GUI-föreläsningarna. För tillfället nöjer vi oss med att använda detta kodskelett för klassen DiagramViewer och fylla på det med kod som skapar olika former:

    import javax.swing.*;
    import java.awt.*;
    
    public class DiagramViewer
    {
        public static void main(String[] args) {
    
    	DiagramComponent comp = new DiagramComponent();
    
    	// Add several shapes to the component
    	comp.addShape(...);
    
    	JFrame frame = new JFrame("Mitt fönster");
    	frame.setLayout(new BorderLayout());
    	frame.add(comp, BorderLayout.CENTER);
    	frame.setSize(800,600);
    	frame.setVisible(true);
        }
    }

    Gör klart klassen, starta, och se ditt nya konstverk!

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–2015.


Sidansvarig: Jonas Kvarnström
Senast uppdaterad: 2015-01-30