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
Labb 3 examineras via redovisning utan kodinlämning.
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
-
Tidigare klasser låg i paketet
se.liu.ida.dinadress.tddd78.lab2
. Högerklicka på paketettddd78
, välj och skapalab3
. Detta ska hamna på samma nivå somlab1
ochlab2
i klassträdet. -
Skapa gränssnittet
Shape
i paketetlab3
genom att som förut högerklicka på paketet och välja , men nu väljer vi Interface i drop-down menyn som finns i fönstret Create New Class som kommer fram. -
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
Shape
.
Att göra 3.1.2: Circle
-
Skapa klassen
Circle
. -
Lägg till de publika fälten
int
fältenx
,y
,layer
ochradius
. -
Skapa 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 iColor
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
). -
Skapa en konstruktor som tar in parametrar och tilldelar värden till alla fält.
-
Nu skall vi se till att
Circle
blir enShape
så att vi senare kan rita ut den utan att behöva referera till den som enCircle
. Påclass
-raden lägger vi därförimplements Shape
efterCircle
.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 enShape
. -
Placera markören där du vill ha de metoder som krävs för Shape. Tryck
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 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(). -
Nu minns vi att standardutskriften för ett objekt inte var särskilt användbar. Använd IDEA för att generera en
toString()
(toString()
som innehåller värdet på alla fält.
Info: Extract Variable
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
Att göra 3.1.3: Test
-
Skapa klassen
TestShapes
och enmain()
-metod i den. -
I main() skall vi skapa en lista av
Shape
. Skrivnew ArrayList<Shape>()
och testa IDEAs Extract Variable enligt tidigare inforuta.Skapa ett par cirklar och lägg dem i listan (med metoden add()).
-
Skriv en
for
-loop som går igenom varje Shape i listan (live-template iter = iterate collection) och anropardraw()
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. -
Kör programmet och verifiera att du får ut rätt utskrifter.
Bakgrund 3.1.4: Rectangle
Shape
.
Att göra 3.1.4: Rectangle
-
Skapa klassen
Rectangle
. -
Skapa de publika fälten
int
fältenx
,y
,layer
,width
ochheight
. Skapa även ett publikt fältcolor
precis som förCircle
. -
Låt IDEA generera en konstruktor som tar in parametrar och tilldelar värden till alla fält.
-
Implementera gränssnittet
Shape
och metodernadraw()
och toString() analogt med hur det gjordes förCircle
. -
Utöka testprogrammet så det också lägger några rektanglar i listan. Testa.
Bakgrund 3.1.5: Text
Shape
.
Att göra 3.1.5: Text
-
Skapa klassen
Text
. -
Lägg till de publika fälten
int
fältenx
,y
,layer
ochsize
. Skapa även ett publikt fältcolor
precis som förCircle
. Texten skall lagras i en publik sträng. -
Låt IDEA generera en konstruktor som tar in parametrar och tilldelar värden till alla fält.
-
Implementera gränssnittet
Shape
och metodernadraw()
och toString() analogt med hur det gjordes förCircle
. -
Utöka testprogrammet så det också lägger några cirklar i listan. Testa.
Bakgrund 3.1.6: Paint
Att göra 3.1.6: Paint
-
Skapa klassen
Paint
genom att döpa om TestShapes (ställ markören i klassnamnet och gör Refactor | Rename, Shift-F6). -
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.
-
Skapa och implemetera en metod
show()
som går igenom shapes i "det nuvarande" Paint-objektet och ritar ut alla former. -
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 dessshow()
-metod.
Skapa och implementera metoden public void
addShape(Shape s)
så att den lägger till former i
Paint-objektets shape-lista.
Ö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
-
Hoppa till klassen
Circle
genom att använda och skriva "Cir", som expanderas automatiskt när du trycker Enter. Detta är en mycket användbar genväg. Eftersom vi redan harCircle
i närheten kan vi även ställa markören iCircle
och trycka Ctrl-B för att snabbt hoppa till definitionen. -
När vi är i
Circle
kan vi välja och fylla i alla fält samt avmarkera Set Access och Use accessors even when field is accessible. -
Gör samma sak i
Rectangle
ochText
.
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
x
- och y
-fält är
detta inget problem.
Att göra 3.2.2: Ändra internrepresentation
-
Skapa klassen
Point
med de privataint
-fältenx
ochy
. Skapa även en konstruktor som tar in parametrar för dessa. Skapa sedan setters/getters för dem (Alt-Insert / Getter and Setter). -
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 namnkoordinat
. Sätt sedan denna till ett värde i konstruktorn genom att skapa ettPoint
-objekt från inparametrarnax
ochy
. -
Skriv om getters för
x
ochy
i formerna så att de nu returnerarx
ochy
genom att hämta dem urkoordinat
. -
Ändra i
toString()
-metoderna så attkoordinat.getX()
ochkoordinat.getY()
används istället förx
ochy
.
Ö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
-
Skapa en ny klass
public abstract class AbstractShape
. Se till att den implementerarShape
, alla subklasser tillAbstractShape
skall ju varaShape
s. Klassen kommer från början att vara tom. -
Byt ut
implements Shape
motextends AbstractShape
i klassernaCircle
,Rectangle
ochText
. Detta innebär att klasserna blirAbstractShape
, men de kommer fortfarande att bete sig somShape
eftersom detta gränssnitt implementeras avAbstractShape
. -
Nu är vi redo att flytta ut definitionen av
koordinat
till den abstrakta klassen.Navigera till
Circle
och välj . Välj att medlemmar ska dras upp tillAbstractShape
. Markerakoordinat
så kommer den att flyttas från subklassenCircle
upp till superklassenAbstractShape
. IDEA måste nu göra några ändringar för att koden fortfarande ska fungera.-
Fältet
koordinat
kommer att bliprotected
iAbstractShape
istället förprivate
, så att subklasser fortfarande kan komma åt den. -
Eftersom
koordinat
nu ligger iAbstractShape
är det upp tillAbstractShape
att hantera initialisering av den. Tilldelningenthis.koordinat = new Point(x,y)
i konstruktorn förCircle
ersätts därför med anropetsuper(x,y)
, som vidarebefordrar parametrarna från Circle upp till AbstractShape:s konstruktor. (Ett anrop till super() ligger alltid först i en konstruktor.) -
IDEA skapar också automatiskt en konstruktor för
AbstractShape
och lägger till den kod som behövs för att initialiserakoordinat
.
-
-
Navigera till
Rectangle
ochText
och ta bortkoordinat
ur dessa samt ersätt skapandet av koordinat med ettsuper()
-anrop motsvarande det i Circle-konstruktorn. -
Flytta getters för
x
ochy
frånCircle
tillAbstractShape
och ta bort dem frånRectangle
ochText
. 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()
-
Skapa
equals()
-metoder i alla formerna. Använd IDEA som hjälp via . När IDEA skaparequals()
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 varanull
. Detta är för att inte generera onödiganull
-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). -
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 hanteraposition
.
Bakgrund 3.4.2: Namnbyte
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
-
Navigera till
AbstractShape
och ställ markören ikoordinat
samt välj . Skriv in namnetposition
. Notera att IDEA automatiskt byter alla förekomster avkoordinat
motposition
. Dels gör den det i getters och iAbstractShape
, men även i allatoString()
-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