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
Att göra 3.1.1: Circle
-
Tidigare klasser låg i paketet
se.liu.ida.dinadress.tddd78.lab2
. Högerklicka på paketettddd78
, välj New | Package och skapalab3
. Detta ska hamna på samma nivå somlab1
ochlab2
i klassträdet. -
Skapa klassen
Circle
i paketetlab3
. -
Lägg till de publika
int
-fältenx
,y
ochradius
. -
Lägg till ett publikt fält
color
av typenjava.awt.Color
.Abstract Window Toolkit (AWT) är Javas ursprungliga grafiksystem. Anledningen till att vi skriver hela
java.awt.Color
är att det finns mångaColor
-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 ijava.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
). -
Skapa en konstruktor som tar in parametrar och tilldelar värden till alla fält.
Bakgrund 3.1.2: Test
Att göra 3.1.2: Test
-
Skapa klassen
TestCircle
och enmain()
-metod i den. -
I
main()
ska vi skapa en lista avCircle
. Skrivnew ArrayList<Circle>()
, låt IDEA importeraArrayList
, 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 iif
-satsen med variabeln. -
Skapa ett par cirklar och lägg dem i listan (med listmetoden
add()
). -
Använd live-template iter (iterate collection) för att skapa en
for
-loop som går igenom varjeCircle
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. -
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
-
Hoppa från
TestCircle
tillCircle
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 iCircle
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
). -
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
-
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
-
Skapa gränssnittet
Shape
i paketetlab3
. 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. -
Nu ska vi se till att
Circle
implementerarShape
. Påclass
-raden lägger vi därför tillimplements 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
-
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" ({...}
). -
Vi ser att
draw()
blir gul. Det finns nämligen klasser som säger sig implementeraShape
men som saknar denna metod! Motsvarande varning kan ses när man går tillCircle
: 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örShape
. Tryck Alt+Insert och välj Implement Methods och sedandraw()
. 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.) -
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+InserttoString()
) 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
-
Alternativ 1: Lägg manuellt till metoddeklarationer för
getX()
,getY()
ochgetColor()
iShape
. Se till att de har samma returvärden och parametrar som iCircle
, men saknar metodkropp. Vi kan ju inte implementera en fullständiggetX()
här uppe iShape
, eftersom gränssnittet själv inte har någotx
att returnera! -
Alternativ 2: Gå till
Circle
. Använd Refactor | Pull Members Up. Se till attShape
är vald i den översta väljaren. MarkeragetX()
,getY()
ochgetColor()
i medlemslistan. "Make abstract" är automatiskt vald, eftersom man inte kan flytta hela metodimplementationen tillShape
– 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 iCircle
.
Nu har vi deklarerat att alla Shape
-klasser måste ha dessa metoder.
Bakgrund 3.3.4: Test
Att göra 3.3.4: Test
-
Gå till
TestCircle
. Klona klassen med hjälp av F5. Kalla den nya klassenTestShapes
. På det sättet får ni med strukturen i det gamla testprogrammet. -
I
TestShapes.main()
ska vi inte skapa en lista avCircle
, utan en lista avShape
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.
-
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()
. -
Kör
TestShapes
och verifiera att du får ut rätt utskrifter. Även det gamla testprogrammetTestCircle
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
-
Skapa klassen
Rectangle
med de privata fältenx
,y
,width
,height
ochcolor
. Typerna vet ni sedan tidigare.Ge klassen en lämplig konstruktor.
Skapa en
toString()
-metod. -
Implementera gränssnittet
Shape
och de metoder som krävs i detta gränssnitt. -
Utöka testprogrammet så det också lägger några rektanglar i listan. Testa.
Bakgrund 3.4.2: Text
Shape
.
Att göra 3.4.2: Text
-
Skapa klassen
Text
med de privata fältenx
,y
,size
(storleken i punkter),color
ochtext
(enString
) samt en lämplig konstruktor ochtoString()
-metod. -
Implementera gränssnittet
Shape
och de metoder som ingår i gränssnittet. -
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
-
Skapa en ny "tom" klass
public abstract class AbstractShape
. Se till att den implementerarShape
– alla subklasser tillAbstractShape
ska ju varaShape
s. -
Byt ut
implements Shape
motextends AbstractShape
i klassernaCircle
,Rectangle
ochText
. Detta innebär att klasserna blirAbstractShape
, men de kommer fortfarande också att varaShape
s eftersom detta gränssnitt implementeras avAbstractShape
. -
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 tillAbstractShape
, inteShape
. Markera fältenx,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
ellergetRadius()
, eftersom bara cirklar har radier. Inte hellerdraw()
ellertoString()
: 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.
-
De valda fälten blir
protected
iAbstractShape
istället förprivate
, så att subklasser fortfarande kan komma åt dem. -
IDEA skapar också automatiskt en konstruktor för
AbstractShape
. -
Eftersom fälten nu är flyttade är det upp till
AbstractShape
att hantera initialisering av dem. Vissa tilldelningar i konstruktorn förCircle
ersätts därför med anropetsuper(x,y,color)
, som vidarebefordrar parametrarna frånCircle
upp tillAbstractShape
:s konstruktor. (Ett anrop tillsuper()
ligger alltid först i en konstruktor.)
-
-
Tyvärr vet inte IDEA att den ska göra motsvarande ändringar i de andra klasserna.
Navigera till
Rectangle
. Ta bort fältenx/y/color
, ta bort motsvarande getters, och ta bort initialiseringen ur konstruktorn.Lägg sedan till
super()
-anrop motsvarande det iCircle
-konstruktorn. Ta bort de tre getter-implementationer som nu blev onödiga eftersom samma kod redan ärvs ner frånAbstractShape
.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ånAbstractShape
I
OtherShape
kan vi välja att implementera helaShape
själva, kanske för att klassen redan ärver frånOtherBase
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()
-
Skapa
equals()
-metoder i AbstractShape. Använd IDEA som hjälp via Code | Generate | equals() and hashCode(): Alt+Insert. Vi är egentligen bara intresserade avequals()
, 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 ihashCode()
.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 varanull
. Detta är för att inte generera onödiganull
-tester. Om du är osäker så ange att fälten kan varanull
(detta kan vara något mindre effektivt men ger alltid rätt resultat). -
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
-
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! } }
-
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. -
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 manGraphics
-objektet som man får som parameter. NamnetGraphics
är egentligen lite missvisande. Egentligen kunde detta ha hetatPainter
, 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 anropadraw()
, 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 ettGraphics
-objekt. Det betyder i sin tur att metoden måste skrivas om för att få ettGraphics
-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 tilldraw()
om det finns enGraphics
-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.
-
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 anropardraw()
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. -
Ä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);
-
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