Labb 2: Intro till objektorientering i Java
Introduktion
I denna labb ser vi bland annat hur man skapar och använder objekt och klasser.
Vi kommer att hjälpa till med flera handgrepp i programmeringen, men det är som tidigare viktigt att ni inte genomför dem "mekaniskt" utan reflekterar över varför ni gör som ni gör.
Bra att tänka på
-
Arbeta på egen tid! Man ska behöva betydligt mer tid än det som schemalagts. Hela kursen är ju på 6 hp = 160 timmar.
-
Använd gärna referenskortet för IDEA när du programmerar. Många funktioner kan vara mycket användbara här och i det stora projektet.
-
Läs genom varje deluppgift (t.ex. hela uppgift 2.1) innan du utför den.
-
En viss mängd dokumentation av era lösningar krävs. Kommentera!
Efter föreläsning 3, OO i Java
Tetris 2.1: Börja programmera Tetris
Syfte: Börja så smått med spelplanen!
Om vi ska implementera Tetris behöver vi bland annat ett sätt att representera spelets nuvarande tillstånd, t.ex. spelplanen. Vissa aspekter behöver vi lämna till labb 2, eftersom de kräver mer kunskaper om objektorientering, men vi kan i alla fall börja med:
Att diskutera hur spelplanen ser ut och hur delar av den kan modelleras.
Att skapa vår första egna datatyp, för "kvadrater" (detta kommer att förklaras snart).
Vi vill speciellt fokusera på hur man tänker när man modellerar. Därför går vi genom steg för steg hur vi har tänkt för att komma fram till de föreslagna datatyperna.
Att förstå: Skapa paket
Vi kommer att fortsätta i samma Git-arkiv och samma IDEA-projekt som du använder för alla labbar. Vi vill däremot separera Tetris från övriga klasser genom att lägga dem i olika (under)paket.
Att göra: Skapa projekt och paket
Skapa ett paket för Tetris, med lämpligt namn -- t.ex.
se.liu.ida.dinadress.tddd78.tetris
.Paketnamnet måste innehålla "tetris" någonstans! Namnet kommer att användas av mjukvara som automatiskt separerar Tetris-kod från annan kod vid inlämning.
Att förstå: Tetris-begrepp!
Som vi vet efter förra uppgiften har Tetris en spelplan där tetrisblock steg för steg "ramlar ner" uppifrån. Varje block består av exakt fyra sammanhängande kvadrater i en viss färg och ett visst mönster – själva namnet Tetris kommer faktiskt från tetra (4).
Totalt 7 olika blockmönster är möjliga, bortsett från möjligheten att
rotera dem. Nedan syns en illustration från Wikipedia. Standardnamnen på
dessa block
är I
, J
, L
, O
, S
, T
och Z
.

Så vad kallas egentligen den här typen av block, mer generellt? Man kan se dem som en variant av domino, som har 2 sammanhängande kvadrater:

Om vi då generaliserar från domino ("di-omino", di=2) får vi namnet polyomino, med specialfallen monomino (1), domino (2), tromino (3), tetromino (4), pentomino (5), hexonimo (6), och så vidare.
Varje tetrisblock är alltså en tetromino.
Att förstå: Modellering av kvadrater
Nu ska vi börja tänka genom alternativ för modellering. Läs genom hela rutan innan du börjar genomföra detta. Vi påminner om att vi vill diskutera hur vi tänker, inte bara slutresultatet!
Vi vill på något sätt representera den nuvarande spelplanen, det nuvarande läget i spelet. När man har spelat ett tag kan det till exempel se ut så här:

Men hur ska vi alltså representera detta tillstånd i ett program?
Vi kunde tänka oss att lagra en lista av de hela tetrisblock som
har ramlat ner samt x- och y-positioner för dessa block på planen. Men när
ett tetrisblock väl har "fallit på plats" kan ju vissa av dess kvadrater
så småningom försvinna medan andra finns kvar (att fulla rader försvinner
är ju själva poängen med spelet). Ovan ser vi t.ex. ett "helt"
grönt L
men även 6
partiella L
där vissa av
kvadraterna från det ursprungliga mönstret är borta. Detta vore det rätt
krångligt att representera med en lista över tetrisblock eftersom vi
skulle behöva hålla reda på vilka delar av varje tetrisblock som
fortfarande finns kvar.
Istället kan vi modellera på följande sätt: För varje position
(x,y) på spelplanen, är den tom? Och om inte, vilken typ
av tetromino tillhör kvadraten på den positionen? Varje gång ett block
faller på plats läggs det då in information om fyra nya kvadrater på
lämpliga (x,y)
-koordinater. Vi behöver inte hålla reda på
vilket tetrisblock varje enskild kvadrat kom från. Vi behöver bara något
som indikerar vilken typ av kvadrat vi har på en viss position:
Kommer kvadraten ursprungligen från ett L
,
ett J
, ...? Det behöver vi så att vi senare vet vilken färg
som ska ritas ut.
Hur representerar vi då en "typ av kvadrat"? Vi vill kunna
representera exakt 8 olika värden: "här finns ingen kvadrat", "här
finns en kvadrat från ett I
-block", "här finns en kvadrat från ett
T
-block", och så vidare.
Java stödjer enum-typer, som är gjorda just för situationen när en datatyp har ett fast antal värden. "Enum" kommer från "enumeration", dvs. "uppräkning". Vi vill räkna upp de 8 värden som finns, och sedan ska man inte kunna skapa nya.
Enum-typer är egentligen klasser, och typens värden är objekt, men det behöver vi egentligen inte tänka så mycket på just nu.
Vi ska därför skapa en enum-typ med namn SquareType
.
Att göra: Kvadrater
Skapa en enum-typ genom att högerklicka på Tetris-paketet i IDEA, välja New | Java Class och sätta "Kind" till Enum. Ge klassen namnet
SquareType
.Lägg in följande inom klamrarna:
EMPTY, I, O, T, S, Z, J, L
Klassen ser alltså ut så här:
public enum SquareType { EMPTY, I, O, T, S, Z, J, L }
Detta betyder helt enkelt att typen
SquareType
har värdenaSquareType.EMPTY
,SquareType.I
,SquareType.O
, och så vidare. Eventuellt klagar IDEA på att namnen är för korta. Detta kan du ignorera.Här finns mer information om enum-typer om du skulle vara nyfiken på mera detaljer.
Tetris 2.2: Skapa objekt med new
+ testning
Syfte: Komma igång med objekt; testa testning!
Nu ska vi komma igång och skapa våra första objekt.
Detta är inte helt sant. Vi har skapat objekt förut, men det har
då varit "underförstått" i andra operationer vi har utfört. De
sju konstanterna i SquareType
(L
, I
, ...) blev till exempel till
objekt. Men nu ska vi för första gången
uttryckligen välja att skapa nya objekt, med
operatorn new
.
För att komma dit så snabbt som möjligt skapar vi objekt av en redan existerande klass från Javas klassbibliotek. Då kan vi även prova på att:
Skapa en
main
-metod som används specifikt för testning av en klass / typ såsomSquareType
.Använda Javas "inbyggda" dokumentation, Javadoc.
Använda typbaserad autokomplettering, som drar nytta av att veta variablernas typer.
Att förstå 2.2.1: Testning och slumptal
Ofta är det bra att ha ett sätt att testa en enskild klass
(datatyp) för att se att implementationen fungerar som den ska.
Då kan man använda testramverk
som TestNG och
JUnit, men så långt går vi inte i
den här kursen. Istället provar vi på att skapa en
egen main
-metod inuti själva klassen. Då kan man
helt enkelt "köra klassen" för att testa den. Vill man istället
starta huvudprogrammet (spela Tetris!) gör man detta med hjälp av
en huvudklass vars main
-metod inte är till för
testning utan för programstart.
SquareType
har inte så mycket egen funktionalitet, så
vi ska helt enkelt testa att slumpa fram några värden, skriva
ut dem, och verifiera att utskriften ser ut som den borde.
Vi behöver alltså kunna skapa slumptal med en (pseudo-)slumptalsgenerator.
En sådan initialiseras normalt med ett "frö" och ger sedan en sekvens av slumptal. Samma frö ger alltid samma sekvens av slumptal (som ska vara svår att förutse och därmed efterlikna "äkta slump"), medan olika frön förhoppningsvis ger olika sekvenser.
Slumptalsgeneratorn måste alltså hålla reda på (bland annat) sitt frö – den har ett eget tillstånd (state). Den har också ett beteende, en funktionalitet: Den kan lämna ut nästa slumptal. Med både tillstånd och beteende passar det att modellera den som ett objekt.
Så är det också i Java: Slumptalsgeneratorer är objekt av
klassen Random
(eller SecureRandom
,
om man vill
ha kryptografiskt
säkra pseudoslumptal).
Att göra 2.2.1: Slumptal
-
Skapa en
main()
-metod iSquareType
. -
Vi skall nu skapa ett första objekt med hjälp av operatorn
new
:Random rnd = new Random();
Som alla klasser i Java ligger
Random
i ett paket. Olika paket kan ha klasser med samma namn. När du skriver inRandom
vill IDEA därför veta vilken Random du menar. Den ger då en popup som föreslår import avjava.util.Random
, den endaRandom
som hittas just nu. Tryck Alt+Enter för att acceptera detta. Detta resulterar i enimport
-sats, som skiljer sig en del frånimport
i Python (mer om detta på en föreläsning).Koden deklarerar att vi vill ha ett objekt av typen
Random
som vi kommer att referera till med namnetrnd
. Vi skickar inte med några initialiseringsparametrar till konstruktorn utan är nöjda med defaultbeteendet hosRandom
.
Att förstå 2.2.2: Javadoc
Javadoc är Javas standardiserade dokumentationsformat. Det byggs av speciella kommentarer som skrivs omedelbart före en klass, metod eller fält. Javadoc-verktyget gör om detta till indexerade HTML-filer. Dokumentationen för Javas standardklasser finns så klart färdigindexerad på nätet.
Att göra 2.2.2: Visa Javadoc i IDEA
Prova att placera markören i t.ex. Random()
och trycka
Ctrl-Q. Detta visar javadoc i en liten popup-ruta så
att du snabbt får fram information utan att behöva lämna
utvecklingsmiljön och navigera genom HTML-filerna.
Så småningom ska du skriva Javadoc-kommentarer även för din egen kod. Även där fungerar Ctrl-Q för att snabbt få fram information.
Exempel från fältet "out" i System.out
:
Verktyg 2.2.3: Auto-komplettering
IDEAs autokomplettering visar vilka valmöjligheter som finns för fortsatt kodskrivning och kan vara mycket användbar för att upptäcka vad man kan göra med ett objekt.
Att göra 2.2.3: Använda autokomplettering
Testa att skriva "rnd.
" på raden under new
Random();
. Beroende på dina inställningar kanske rutan med
möjliga fortsättningar kommer automatiskt, eller så får du ta fram
den själv med Ctrl-Space.
Eftersom IDEA vet vilken typ variabeln har vet den exakt vad som
kan stå efter "rnd.
", vilket är ett väldigt bra sätt
att lära sig mer om tillgänglig funktionalitet.
Du kan välja ett alternativ i listan eller fortsätta skriva. Om
du fortsätter skriva kommer IDEA att filtrera listan och försöka
hitta rätt metod utifrån det som skrivs. Skriver du t.ex. "on"
kommer IDEA att föreslå " nextLong()
".
Man kan navigera i alternativlistan med pil upp/ner. Man kan också trycka Ctrl-Q vilket tar fram javadoc-information för den entitet man just har markerat.
Att göra 2.2.4: Generera slumptal
Vi ska nu skapa och skriva ut slumptal med en hjälp
av Random
.
-
Testa först slumptalen genom att låta
main()
skriva ut 25 slumptal på varsin rad. Använd valfri typ av loop för att iterera 25 gånger. Använd t.ex.nextInt(100)
för att generera ett slumptal mellan 0 och 99. -
Testkör programmet.
-
Nu ska vi även skriva ut slumpmässiga
SquareType
-värden.Med hjälp av klassmetoden (statiska metoden)
SquareType.values()
får vi fram en array som innehåller alla värden i enum-typenSquareType
. Om vi inte har kommit till arrayer än, räcker det att veta att de fungerar ungefär som listor med fixerad längd, och att man indexerar dem som listor i Python:minArray[index]
, därindex
startar på 0. Längden får vi ut medminArray.length
.Utifrån detta kan du fundera på hur man skapar slumptal av rätt storlek. Skriv sedan en loop som 25 gånger skriver ut en slumpmässig
SquareType
(ett slumpmässigt index iSquareType.values()
). -
Testkör programmet.
Sammanfattning
Vi kan nu skapa objekt och anropa deras metoder. I övningen har vi
använt en klass som är implementerad i
Javas util
-paket. I nästa övning skall vi skriva vår
egen första "fullständiga" klass.
Uppgift 2.3: En egen "fullständig" klass
Syfte: Skapa egna objekt!
Vi ska nu gå vidare med skillnaderna mellan objektorienterad och icke objektorienterad programmering. Detta görs genom att vi skapar vår första mycket enkla "fullständiga" klass, som har både internt tillstånd (som lagras i fält), beteende (som anges av metoder), och en egen konstruktor.
Vi går även vidare med utskrifter och ser hur vi ger en klass en egen anpassad utskriftsmetod.
Att göra 2.3.1: Skapa paket
Vi fortsätter i samma IDEA-projekt, men skapar ett nytt paket för de enskilda övningarna i labb 2 så vi kan hålla isär de olika övningarna bättre.
-
Tidigare klasser låg i paketet
se.liu.ida.dinadress.tddd78.lab1
ellerse.liu.ida.dinadress.tddd78.tetris
. Högerklickasrc
, välj New | Package och skapase.liu.ida.dinadress.tddd78.lab2
. Detta ska hamna på samma nivå som lab1 i klassträdet.
Att förstå 2.3.2: Modellering av personer
Vi tänker oss att vi skall arbeta med olika personer i en applikation. Då är det naturligt att samla information och funktionalitet som rör en person i en klass.
För att kunna konstruera olika objekt av typen Person
behöver vi en
konstruktor, en metod som initialiserar varje nytt objekt. Den heter
alltid samma som klassen, har ingen returtyp, och tar noll eller flera
parametrar. Den kan bland annat sätta värden på alla fält i det objekt som
den skapar, t.ex. genom att "spara undan" värden som den har fått som
parametrar.
Att göra 2.3.2: Personklassen
-
Skapa klassen
Person
i paketetlab2
. -
Addera ett privat fält med namn
name
av typenString
som skall användas för att lagra personens namn. Eftersom varje person har sitt eget namn (som lagras i personobjektet) ska fältet inte varastatic
. -
Addera ett privat fält
birthDay
av typenLocalDate
som skall användas för att lagra personens födelsedatum. NärLocalDate
matas in kommer IDEA inte att känna till denna klass, men föreslå att den importeras av personklassen så att den känns igen och kan användas. Skulle man trycka bort möjligheten att låta IDEA importeraLocalDate
kommer IDEA att rödmarkera namnet. Ställer man markören i namnet kommer en röd lampa att tändas där man åter kan välja att IDEA skall importera klassenLocalDate
.Här finns mer information om
LocalDate
för den som är intresserad. -
Lägg till en konstruktor som tar en
String
och enLocalDate
som inparametrar och lagrar dessa värde i objektets "privata" fält. IDEA kan snabbt skapa en konstruktor via Alt+Insert->Constructor. -
Ett
LocalDate
-objekt som representerar ett datum kan t.ex. skapas med hjälp av anropetLocalDate.of(1990, 6, 1)
om man vill skapa ett datum som representerar den 1:a juni 1990. Skapa en main()-metod (snabbast genom att skriva psvm och därefter trycka CTRL+Space eller CTRL-J och välja att ni vill ha en main() metod). I main()-metoden kan du skapa en person som motsvarar dig själv.Kör ditt testprogram och se att det inte kraschar!
Att förstå 2.3.2: Tidsperioder
Vi skall nu utöka vår personklass med en metod för att beräkna personens
ålder. Till vår hjälp har vi redan sett att LocalDate
kan
användas för att representera ett datum. För att få nuvarande tid använder
vi LocalDate.now()
som returnerar
ett LocalDate
-objekt som representerar dagens datum.
Om vi vill räkna ut en persons ålder behöver vi vet hur lång tid det är
mellan dess födelse och dagens datum. Vi kan räkna ut hur lång tidsperiod
det är mellan två datum med hjälp av klassen Period
. Om man
anropar Period.between(start,slut)
där start och slut är av
typen LocalDate
får man ett Period
-objekt. Man kan
ta reda på hur många år som perioden består av genom
metoden getYears()
.
Att göra 2.3.2: Tidsperioder
-
Implementera metoden
getAge()
som returnerar personens ålder. Eftersom varje person har sin egen ålder (som lagras i personobjektet) ska metoden inte varastatic
. -
Testa att metoden fungerar genom att skriva ut åldern på den person med dina data som skapats i
main()
-metoden.
Att göra 2.3.3: Utskrifter
Vi vill kunna skriva ut våra personer på ett läsligt format. Först
provar vi vad som händer när vi skriver ut ett Person
-objekt.
-
Se till att
main()
skriver ut minst ettPerson
-objekt medSystem.out.println()
. -
Testkör. Får du ett underligt resultat, i stil med
"Person@28cd724"
? Då är allt rätt.
Att förstå 2.3.4: Utskrifter och toString()
Till skillnad från t.ex. listor finns det inget bra standardiserat
sätt att skriva ut enskilda objekt. Att bara visa värdet på alla fält
t.ex. är ofta inte det bästa sättet, även om det hade fungerat för
personklassen som den ser ut just nu. Som standard skriver Java
därför ut objekt på formen "klassnamn@objektID
", där objektID är
olika för varje objekt.
För att ändra detta implementerar man metoden public String
toString()
i sin klass. Vid ett anrop
såsom System.out.println(mittObjekt)
anropas då (indirekt och
automatiskt) mittObjekt.toString()
, och det är den returnerade
strängen som skrivs ut istället för "klassnamn@objektID
". Nu
ska vi implementera en sådan metod.
IDEA kan själv skapa en toString()
via Alt+Insert
/ toString()
. Detta kan vara användbart när man snabbt vill
generera en toString()
för användning i debuggning (debuggern
visar varje objekts toString()
för att identifiera det). Den
skulle dock generera ett resultat av typen "Person{name='namn',
birthDay=1990-06-01}". I vår applikation är det viktigt att snabbt
kunna se åldern på personer och därför gör vi på ett annat sätt.
toString()
-metod.
Implementerar man ingen egen ärvs en ned från superklassen. Superklassen
till Person
är Object, och dess toString()
har detta
standardbeteende. Detta kommer att diskuteras när vi har gått genom arv.
Att göra 2.3.4: Utskrifter och toString()
-
Tryck Alt+Insert och välj Override. Välj sedan
toString()
. -
Vi sätter nu ihop en sträng som får representera vår person. Man kan sätta ihop två strängar med hjälp av "
+
"-operatorn. Om man sätter ihop en sträng med något som inte är en sträng kommer Java att konvertera det andra objektet till en sträng. LåttoString()
returnera värdetname + " " + getAge()
. -
Skapa några personer till och skriv ut var och en på sin egen rad.
Verktyg: Funktionsargument och Smart Completion
IDEA har flera hjälpsamma funktioner. T.ex. så kommer en funktion
som inte får inparametrar av rätt typ att rödmarkeras av IDEA,
något som inte skulle fungera i Python där parametrar inte har
typer. Om man sätter markören över får man reda på varför anropet
inte fungerar. Testa att anropa konstruktorn till Person
men med
ett heltal istället för en sträng på namnpositionen. IDEA kommer
då visa att man skickar med ett heltal när konstruktorn kräver en
sträng. Det sparar mycket tid att man får reda på fel innan
kompilering. Dessutom får man reda på vad som är rätt också!
Om du deklarerar String namn = "Anders Andersson";
och sedan någonstans längre ner i programmet skall skapa en person
med detta namn kan IDEA hjälpa till genom "Smart Completion". Man
kommer åt denna hjälp genom att
trycka CTRL-Shift-Space. Skulle man stå på ett ställe
där en String
förväntas kommer IDEA att föreslå
variabler som matchar. Om du gör ett anrop till
Person
-konstruktorn och
trycker CTRL-Shift-Space när det är dags att ange
name
-parametern kommer IDEA föreslå att du skickar
med namn
-variabeln.
Sammanfattning
Vi kan nu skapa egna objekt och vet hur man konsturerar en strängrepresentation av dessa. Vi har sett hur man definierar klasser och initialiserar objekt då de skapas. Detta skiljer sig från programmering i icke objektorienterade språk.
Uppgift 2.4: Samverkande klasser – almanackan
Syfte
Nu ska vi prova på att skapa en större sammanhängande uppsättning klasser för att lösa en specifik uppgift. Detta görs genom en almanacksuppgift som låter oss kontrastera de olika stegen mot den icke objektorienterade och "icke-typade" Python-programmering de flesta av er gjort i en tidigare kurs. Vi kommer även att kunna jämföra hur dataabstraktionen fungerar när vi programmerar objektorienterat. Vi går dock inte lika långt som tidigare – bara tillräckligt för att se kontrasterna i modellering.
(Om du inte har gjort motsvarande övning i Python kommer du ändå att kunna lära dig hur man börjar modellera data i Java.)
Att förstå 2.4.1: Typad OO-programmering
Vi skall nu skapa en klass Month
som har motsvarande
funktionalitet som i Python-almanackan.
I Python-almanackan behövde många funktioner programmeras explicit för varje typ, t.ex. för månader. Detta orsakades delvis av att programmet inte använde klasser, där man får mycket av detta "gratis" eller i alla fall "billigare".
-
is_month: objekt -> sanningsvärde testade om en lista var en månad med hjälp av
get_tag()
. Nu ska vi istället skapa klassenMonth
. Kräver en metod ett månadsobjekt behöver vi inte testa i efterhand om det var en månad som skickades – vi deklarerar helt enkelt en parameter av typenMonth
, så vet vi att bara sådana objekt kan komma in. Detta tar också bort många anrop tillensure()
från implementationen. -
new_month: sträng -> month, som skapar en månad av en textsträng som "january", blir en konstruktor. Detta använde
attach_tag()
, som inte längre behövs.
Funktioner som month_name()
, month_number()
och number_of_days()
blir nu metoder i Month
. Detta
samlar ihop relaterad kod till en sammanhängande enhet.
Att förstå 2.4.1: Objektorientering
-
Skapa klassen
Month
med fälten:- name - månadens namn ("January")
- number - månadens nummer (1)
- days - antal dagar i månaden (31)
Eftersom vi inte vill att någon skall ändra dessa värden utifrån lägger vi attributet "private" framför deklarationerna.
-
Använd IDEA för att skapa en konstruktor som initialiserar
Month
åt dig. Tryck Alt+Insert och välj Constructor. I detta fall ska alla fält anges som parametrar till konstruktorn, så markera alla fält innan du trycker OK. -
Tryck Alt+Insert och välj Getter, klicka i alla tre fälten och sedan Ok. Nu genererar IDEA funktioner så att andra klasser kan få ut informationen men inte ändra den.
Bakgrund 2.4.2: Månadsdata
Month
.
Vi kommer att bortse från skottår då det inte tillför något av värde ur objektorienteringssynpunkt!
Att göra 2.4.2: Månadsdata
-
Skapa i klassen
Month
metodernagetMonthNumber(String name)
ochgetMonthDays(String name)
som med hjälp av switch-satser (se labb 1) rapporterar månadsnummer och antal dagar i en månad utifrån det givna månadsnamnet. Låtdefault:
returnera -1. Då kan vi använda någon av funktionerna för att testa om en sträng faktiskt representerar en existerande månad.Detta ska motsvara MONTH_NUMBERS och MONTH_DAYS i Python-almanackan, men vi implementerar det som en metod istället för en dictionary.
I just detta fall vill vi använda statiska metoder (deklarerade
static
). Då behöver vi inte skapa ettMonth
-objekt för att kunna ta reda på antal dagar i en månad, utan anropar metoderna direkt i klassen istället. Metoderna anropas därmed somMonth.getMonthNumber(...)
. Detta ska vi bara göra för metoder som verkligen är "globala" och oberoende av information om enskilda månadsobjekt. Mer om detta kommer på senare föreläsningar.
Att göra 2.4.3: Tid och Datum
Vi kommer inte att skapa speciella typer för timmar, minuter och dagar. Eftersom vi inte ska gå vidare och ge dessa tidsenheter mer funktionalitet än att just innehålla ett heltal, använder vi i denna labb helt enkelt int för att representera dessa. Däremot behöver vi representera datum som är sammansatta av år, dag och månad. Vi kommer också att behöva representera tidpunkter samt tidsintervall.
-
Skapa klassen
Date
med fältenyear
(int),month
(Month
) ochday
(int). Lägg till en konstruktor som tar in dessa som parametrar. Lägg också till getters för dem. Skriv entoString()
-metod presenterar ett datum i ett format som du tycker är lämpligt. -
Skapa klassen
TimePoint
som innehåller ett klockslag för att representera start/slut på en aktivitet. Klassen skall ha fältenhour
ochminute
.I konstruktorn skall
hour
ochminute
tilldelas värden. Till skillnad från Python-almanackan tar vi in dessa som separata heltalsparametrar till konstruktorn istället för att dela upp en tidssträng i delar.Generera också getters för
hour
ochminute
.De fält och metoder vi skapar nu ska anropas i ett objekt, inte i en klass. De ska alltså inte vara statiska! Vi frågar till exempel ett specifikt
Date
-objekt, inte datumklassen, vilket år det har. -
Skriv en
toString()
-metod iTimePoint
. Användhour
ochminute
för att skapa en lämplig sträng att returnera. Här kan man t.ex. utnyttja String.format(...) med formatsträngen "%02d:%02d" – vi lämnar medvetet några detaljer till er att utforska!ÖverkursSkapa enint compareTo(TimePoint other)
-metod som returnerar -1 om den aktuellaTimePoint
kommer före other, 0 om de representerar samma tid och 1 omother
kommer före. -
Skapa till sist klassen
TimeSpan
. Den behöver två fält av typenTimePoint
,start
ochend
. Gör getters och en konstruktor som tar in dessa. Skriv sedan entoString()
-metod som skapar utskrifter av typen "12:15 - 13:15". Detta görs bland annat genom anrop tilltoString()
istart
ochend
. På detta sätt bygger man rekursivt upp en textrepresentation av ett sammansatt objekt.
Att förstå: Listor i Java
Java har ingen egen syntax för listor. Istället är de klasser som
alla andra, och det finns flera olika listklasser med olika
egenskaper. Den vanligaste och mest använda
är ArrayList
.
Listklasserna är generiska typer. Detta innebär i princip att man kan ange en parameter som talar om vilken typ av element de innehåller, så att kompilatorn kan utföra bättre typkontroller. Om vi vill ha en lista som bara innehåller strängar:
ArrayList<String> myList = new ArrayList<String>();
Vi kan förenkla detta genom att utelämna elementtypen på högersidan:
ArrayList<String> myList = new ArrayList<>();
Vi kan därefter stoppa in och plocka ut element:
myList.add("foo"); // Lägg till sist i listan
String x = myList.get(0); // Hämta första elementet
String y = myList.get(3); // Hämta fjärde elementet
int antal = myList.size(); // Antal element i listan
ArrayList
är nämligen ett specialfall av den mer
generella typen List
, och IDEA tycker att man ska skriva
skriva så här istället. Detta förklaras senare i kursen.
List<String> myList = new ArrayList<>();
Att göra 2.4.4: Möten och kalendern
Allt som återstår nu är att lägga till en klass för möten och att skapa
själva kalendern. Vi skapar en simpel kalender där alla möten lagras i
en lista. En stor del av koden i metoden book()
som används
för att lägga möten i kalendern utgörs av parameterkontroll. Att
kontrollera användarens inparametrar är ofta en stor del av den kod som
ligger i ett API. Vi kommer att generera ett undantag om man försöker
skapa ett otillåtet möte.
-
Skapa klassen
Appointment
med tre fält,subject
(String
),date
(typ Date) ochtimeSpan
(typTimeSpan
). Skapa getters och en konstruktor som tar samtliga som parametrar. Skapa även entoString()
-metod som använder sig avDate.toString()
ochTimeSpan.toString()
för att formattera en fin utskrift. -
Skapa klassen
Cal
. Den skall ha en privat lista av appointments som realiseras av enArrayList
, vilket är en collection som implementerar list-gränssnittet, i en konstruktor utan inparametrar. Detta sker genom radernaprivate List<Appointment> appointments;
i klassen och
appointments = new ArrayList<>();
i konstruktorn.
Varför inteCalendar
? Jo, det finns redan en sådan klass i Java (fast den representerar en tidpunkt), och även om det skulle fungera att ha en till, kan det vara förvirrande att ha namnkrockar med välkända klasser. -
Implementera metoden
show()
som går igenom listan och skriver ut alla appointments. Detta kan med fördel göras genom att använda Live-Template (Ctrl+J) iter som leder till en så kallad for-each-loop där varje element i t.ex. en lista besöks. Denna loop har formatet "for (element : behållare)". -
Implementera metoden
public void book(int year, String month, int day, int startHour, int startMinute, int endHour, int endMinute, String subject)
Notera att vi här tar in ett månadsnamn som en sträng. Att skapa ett
Date
-objekt kräver däremot att vi har ett månadsobjekt. Det är upp tillbook()
att anropa lämpliga statiska metoder iMonth
för att ta reda på bl.a. vilket nummer månaden har och sedan skapa ett lämpligt månadsobjekt.Metoden skall skall kontrollera alla inparametrar. Detta betyder att
- year > 1970
- För start och end är 0 <= hour <= 23 och 0 <= minute <= 59
- month skall vara ett namn som motsvarar en existerande månad
- månaden skall ha tillräckligt många dagar för att day skall vara tillåten
Om någon av parametrarna är fel ska detta signaleras till anroparen. Detta gör man normalt med exceptions, "undantag". Exakt hur detta fungerar kommer vi att gå genom i en senare del av kursen. För tillfället räcker det med att ni vet att ett
IllegalArgumentException
ska kastas, och att detta görs med:throw new IllegalArgumentException("felmeddelande");
När parametrarna är kontrollerade skall ett
Date
-objekt skapas utifrån datuminformationen och tvåTimePoint
-objekt skapas somstart
/end
. Dessa två används sedan för att skapa ettTimeSpan
och slutligen ettAppointment
som läggs till i listan. -
Kalendern är nu färdig och du skall skriva ett testprogram som skapar en kalender, bokar 5-10 appointments och sedan skriver ut kalendern.
ÖverkursSkriv ut appointments i sorterad ordning.
Sammanfattning
Du har nu byggt ett objektorienterat program med flera klasser. Du är bekväm med Setters/Getters, har testat for-each och listor i Java, inklusive generics, samt har kastat Exceptions.
Tetris 2.5: Spelplanen
Syfte: Modellering av spelplan
Nu återvänder vi till Tetris genom att skapa fler datatyper för spelets
nuvarande tillstånd – speciellt en typ för själva
spelplanen, som vi kan kalla Board
. Som tidigare
vill vi speciellt fokusera på hur man tänker när man
modellerar. Därför diskuterar vi hur vi har tänkt för att komma
fram till de föreslagna datatyperna.
Att förstå 2.5: Spelplan, informationsfokus, arrayer
När vi tänker på Tetris är det lätt att vi fokuserar på det visuella: Ett spelbräde visas upp på skärmen med ritade rutor i olika färger. Just nu ska vi dock fokusera på den information som behövs för att hålla reda på spelet och spelmekaniken. Till exempel måste man veta:
Hur stor spelplanen är
Vilka rutor som är upptagna, och vad de innehåller
Formen och koordinaterna för den bit som håller på att ramla ner just nu
Den här informationen är nödvändig: Utan den kan man överhuvudtaget inte spela. Koden som ritar upp ett spelbräde är jämförelsevis mindre central – den skulle till exempel inte alls behövas om man vill programmera en AI-baserad spelare (bot), eller om man vill spara informationen om ett spel i en fil så att man kan återuppta den senare.
Nu behöver vi alltså en spelplansklass, Board
, som
till att börja med innehåller information om vilken typ av kvadrat som för
tillfället finns på varje position på spelplanen. Hur ska vi lagra detta?
Ett sätt är att ha en lista på kvadrater, där varje kvadrat känner till sin egen position. Då skulle en spelplan t.ex. kunna innehålla 25 olika L-kvadrater, var och en med sina egna koordinater. Det skulle fungera utmärkt när vi ritar ut spelplanen men blir kanske mindre bra när vi vill undersöka om en viss rad är full. I sådana fall är det ju bra om vi snabbt kan hitta alla kvadrater på en viss rad, vilket vi inte får om vi måste leta genom en lista på alla kvadrater.
Ett annat sätt är att lagra ett rutnät som man kan indexera med x- och y-koordinater för att se vad som finns på en viss position. Det kan man göra med en nästlad lista, som i Python. Ett alternativ är att använda en tvådimensionell array. Arrayer är i princip som listor, men har (i Java) en fast längd som inte kan ändras efter att arrayen skapades. Den begränsningen är inget problem för oss eftersom vi inte vill ändra storleken på spelbrädet.
Arraysyntaxen i Java liknar i vissa delar Pythons listsyntax, men det finns skillnader.
Första raden nedan skapar en array med plats för 10
element. Arrayen innehåller 10 st null
, vilket på
vissa sätt motsvarar Pythons None
: "Det finns inget
här". Andra raden stoppar in ett värde på den fjärde
positionen, som har index 3 (index börjar på 0). Tredje raden
visar hur man hämtar ut ett element.
SquareType[] array = new SquareType[10];
array[3] = SquareType.EMPTY;
SquareType pos3 = array[3];
Men nu är det en tvådimensionell array, ett "rutnät", som vi behöver för spelplanen. Första raden nedan skapar en sådan array. Andra raden används för att komma åt en rad i arrayen, tredje raden visar hur man kommer åt en cell, och fjärde raden visar hur man ändrar en cell.
SquareType[][] array = new SquareType[rows][columns];
SquareType[] rowZero = array[0];
SquareType row5col9 = array[5][9];
array[5][9] = SquareType.EMPTY;
Mer information finns t.ex. i Java Tutorial, en av våra kursböcker.
Ett Board
kan alltså innehålla en
tvådimensionell array av lämplig storlek, där varje
element är en SquareType
som identifierar typen av
kvadrat.
Detta fält bör vara private, eftersom andra klasser inte ska kunna komma åt det. Detta kommer att diskuteras i mer detalj på föreläsningarna, men just nu kan du bara följa instruktionerna.
Board
behöver också ha en konstruktor som tar en
bredd och höjd på spelplanen som argument och skapar en lämplig
tvådimensionell array. Detta verkar vara det enda vi behöver
just nu, men fler fält och metoder tillkommer säkert senare.
Att göra 2.5: Spelplan
Skapa klassen
Board
i Tetris-paketet.Ge klassen fältet
private SquareType[][] squares
. Detta deklarerar en tvådimensionell array av SquareType-värden. Lägg även till privata heltalsfältwidth
ochheight
där konstruktorn nedan ska spara undan brädets dimensioner.Ge klassen en konstruktor som tar parametrarna
width
ochheight
. Låt konstruktorn sättasquares
tillnew SquareType[height][width]
. Detta skapar den faktiska arrayen som fältet ska peka på.-
Alla positioner i arrayen är nu
null
, dvs. vi har en array av pekare som inte pekar på några verkliga objekt. Se till att konstruktorn fyller alla positioner i arrayen (alla kolumner i alla rader) med "tomma kvadraten" istället. -
Gör en
main
-metod som helt enkelt skapar ettBoard
med valfri storlek. Testkör detta för att se att programmet inte kraschar, t.ex. genom att man försöker skriva utanför arrayens gränser.
Att förstå: Gömma data
Varför skulle den tvådimensionella arrayen vara privat
i Board
? För att vi vill att spelplansklassen ska
ha full koll över hur planen manipuleras – andra klasser
får anropa metoder i planen för att be den göra något, men de
får inte gå in och ändra direkt i arrayen. Man ska inte heller
skapa en metod som returnerar hela arrayen, eftersom det låter
utomstående manipulera den direkt. Detta kommer som sagt att
diskuteras mer under föreläsningarna.
Tetris 2.6: Visualisering av spelplan
Syfte: Visualisera snabbt
Vi vill gärna kunna visa spelplanen för att se om vi har gjort rätt. Istället för att gå direkt på grafiken kommer vi i ett första steg att köra textbaserat. Dels är detta enklare och gör att vi snabbt kan komma vidare, dels kan det vara användbart för debuggning av programmet, och dels är det ett steg i att demonstrera användningen av strängar i Java.
Att förstå: Modellering; vems ansvar är visualiseringen?
I all programmering är det viktigt att man delar upp koden i olika moduler med tydliga ansvarsområden. Detta gör koden enklare att läsa, skriva, förstå, och strukturera. Java delar t.ex. upp koden i paket och klasser.
Vems ansvarsområde är det då att visualisera något på skärmen? Det kan man argumentera om. Det finns som vanligt många sätt att dela upp ansvarsområdena, alla med sina fördelar och nackdelar. Vilket som blir bäst beror ofta på situationen.
Tänk dig att vi programmerar ett plattformsspel med många olika sorters objekt på skärmen: Bakgrunder, spelare, fiender, bonusar att plocka upp, med mera. Då kanske vi tycker att varje objekt borde hålla reda på hur det själv ska se ut. Då får vi en klar och tydlig uppdelning:
En klass håller reda på allt om spelaren, inklusive hur den rör sig, hur den ser ut på skärmen, hur många poäng spelaren har, och så vidare
En annan klass håller reda på allt om fiender av typ 1, inklusive hur de ser ut, hur de beter sig, hur starka de är, och så vidare
En tredje klass håller reda på allt om fiender av typ 2, inklusive hur de ser ut, hur de rör sig på skärmen, hur starka de är, och så vidare
...
Om det görs på rätt sätt kan detta bli en bra och modulär uppdelning och ansvarsfördelning. Vill man lägga till en ny typ av fiende lägger man till en ny klass, och behöver inte ändra i de andra.
Men nu programmerar vi en annan typ av spel, där det inte kommer att finnas så många olika sorters objekt på skärmen. Vi har en bakgrund, kvadrater från block som har ramlat ner, och eventuellt en fallande tetromino. Då kan vi göra en annan typ av uppdelning, som också är klar och tydlig:
En klass (
Board
) håller reda på all grundläggande information om spelplanen, inklusive hur stor den är, var kvadraterna finns, och så vidare. Den vet också hur spelmekaniken fungerar (hur saker faller, hur rader försvinner).En annan klass vet hur man ritar upp spelplanen.
Här har vi delat upp funktionaliteten åt ett annat håll, genom att samla hela modellen på en plats och hela visualiseringen, vyn, på en annan plats. Detta är en del av ett designmönster som i sin helhet kallas Model-View-Controller.
Så vilket ska vi välja?
I många projekt tror vi att den första lösningen kommer att fungera bäst, eftersom man annars får en alltför stor centralisering av "hur man ritar" för många olika typer av skärmobjekt. Detta skulle strida mot principer om modularisering.
Men i Tetris har vi inte så många olika typer av skärmobjekt, och vi tjänar antagligen mer på att separera utritning från datamodellen än på att separera utritning av kvadrater från utritning av bakgrunden.
Därför bestämmer vi att vi ska skapa en separat vy-klass som är ansvarig för att rita ut hela spelbrädet.
Att förstå: Getters/Setters
Vy-klassen kommer att behöva information från Board
för att veta vad den ska rita ut.
Om man tillåter objekt från en klass att direkt använda
fälten i objekt som tillhör en annan klass, låser man fast sig i
en viss representation som sedan blir svår att ändra. Därför
vill vi till exempel inte att klassen som visar ett spelbräde på
skärmen ska komma åt den
tvådimensionella squares
-arrayen
i Board
direkt. Istället ska denna
representation vara Board
:s hemlighet.
Men på något sätt måste ju visaren få reda på informationen. Hur gör man då? Jo, vi skapar ett mellanlager av metoder som gör informationen tillgänglig. Som vi ska diskutera på föreläsningarna kan det mellanlagret exponera informationen utan att tala om exakt hur och var den lagras. En av fördelarna är att den interna representationen kan ändras utan att andra klasser behöver modifiera sin kod.
Metoderna som hämtar och ändrar information kan kallas för getters respektive setters. Java har en konvention att dessa metoder kallas för getXYZ() respektive setXYZ(), där XYZ är någon egenskap man är intresserad av.
Att förstå 2.6.1: Ge vy-klassen tillgång till information
Vy-klassen kommer bland annat att vara intresserad av spelbrädets bredd
och höjd. Dessa lagras just nu i fälten width
och height
, och vi vill skriva
metoderna getWidth()
och getHeight()
. Här ser
vi att metodnamnen direkt motsvarar fältnamnen, och att funktionaliteten
blir att direkt returnera ett värde.
Vi kommer också att vara intresserade av vilken typ av kvadrat
(om någon) som finns på en viss position. Detta kunde man rent
tekniskt göra tillgängligt genom att returnera
hela squares
-arrayen, men då exponerar vi lite för
mycket av den interna representationen. Det blir bättre att
istället skapa en metod som bara returnerar information om
en specifik position. En sådan metod blir lättare att
ändra om man senare vill ändra Board
för att
t.ex. lagra spelbrädet i nästlade listor eller någon annan
representation.
Att göra 2.6.1: Ge vy-klassen tillgång till information
-
Skapa getters för
width
ochheight
iBoard
samt en metod för att ta reda på vilkenSquareType
som är lagrad i en given cell (x,y) isquares
.Skapa inte en metod som returnerar hela
squares
!
Att förstå: Strängmanipulation
Javas strängar är oföränderliga: Man kan inte ändra på dem när de väl har skapats.
För att inkrementellt bygga upp en lång sträng använder man lämpligen
klassen StringBuilder
och dess append()
-metoder. För att lägga till en
radbrytning efter varje rad används "\n
" (backslash n). När
strängen är klar tas den fram som String
-objekt med
toString()
. Här är ett exempel:
// Create new StringBuilder.
StringBuilder builder = new StringBuilder();
// Loop and append values.
for (int i = 0; i < 5; i++) {
builder.append("abc ");
}
// Create a string with the same contents.
String result = builder.toString();
// Print result.
System.out.println(result);
Givet en
SquareType
behöver vi veta vilken symbol den ska illustreras med.
Man kunde tänka sig att detta skulle läggas in direkt i själva
SquareType
-objektet men då har vi blandat ihop modellen
(SquareType
) med ett specifikt sätt att visa modellen
på (text). Istället kan vi låta vyn känna till
vilka SquareType
s som finns och ha en hjälpmetod som
returnerar symbolen motsvarande en viss SquareType
.
Att förstå 2.6.2: Skriv ut ett spelbräde
Vi ska nu skapa en klass vars ansvarsområde är att visa upp ett spelbräde. Men hur ska den nuvarande situationen egentligen visas?
Just nu kommer vi att använda textgrafik, men så länge som vi
designar våra (objektorienterade) gränssnitt på ett bra sätt
kommer vi senare att kunna byta ut detta mot "vanlig" grafik.
Vi skulle också kunna "visa"
spelplanen som en lista på upptagna
positioner
[A7,A8,B5,...]
– inte så intutivt
för oss, men det skulle fungera bra för en AI-spelare som vill
"se" spelplanen. Vi skulle kunna visa det på en 3D-display för
blinda (se bilden), och så vidare.
Att göra 2.6.2: Skriv ut ett spelbräde
-
Skapa klassen
BoardToTextConverter
, med metodenconvertToText(Board)
som returnerar en strängrepresentation av ett modellobjekt – den skriver alltså inte ut på skärmen själv utan returnerar en sträng som anroparen kan skriva ut. Tomma rutor ska då representeras med t.ex. mellanslag eller bindestreck, och olika typer på kvadrater (SquareType
s) ska representeras som textsymboler, t.ex. "#
", "%
" och "-
".Du behöver en nästlad loop som itererar över rader och kolumner i lämplig ordning. Inuti loopen kan man använda en
switch
-sats med ettcase
för varjeSquareType
-värde, något som är möjligt på grund av attSquareType
är en enum. Det vill säga, om elementet på en viss position ärSquareType.EMPTY
lägger du till ett mellanslag, om elementet ärSquareType.I
lägger du till en annan symbol, osv.Glöm inte radbrytningarna
"\n"
mellan raderna. -
Skriv en testklass,
BoardTest
, som skapar en tom spelplan, konverterar den till en sträng med hjälp av ettBoardToTextConverter
-objekt, och skriver ut resultatet. När det är klart har du åstadkommit ett testbart ramverk och kan se att du har gjort konkreta framsteg!
Tetris 2.7: Slumpning av spelplan
Att göra 2.7: Test
För vår testning vill vi också ha en metod för att slumpa fram en spelplan.
-
Skapa en metod i
Board
som kan användas för att ersätta det nuvarande innehållet isquares
med genererat slumpmässigt innehåll (en slumpmässigSquareType
i varje ruta).Som tidigare använder du ett objekt av klassen
java.util.Random
. Lagra Random-objektet i ett fält iBoard
så att du kan skapa det en gång iBoard
-konstruktorn och anropa det varje gång du behöver ett slumptal! Annars blir det (a) onödigt långsamt eftersom vi måste skapa många objekt i onödan, och (b) dålig kvalitet på slumptalen eftersom vi inte får en chans att utnyttja serien av slumptal. -
Ändra klassen
BoardTest
så att den 10 gånger slumpar om spelplanen och skriver ut den. Testkör så att du ser att utskriften fungerar.
Tetris 2.8: Tetrisblock / tetrominoes
Att förstå 2.8.1: Att modellera polyominos
Efter spelplanen kommer själva Tetrisblocken. Det finns som sagt totalt 7 varianter i grundspelet, och vi kan tänkas vilja utöka detta på olika sätt i framtiden – genom nya former (utökning till pentominoes med fem kvadrater per block, penta=5) eller andra varianter (block som exploderar). Frågan är då hur man ska implementera block på bästa sätt för att tillåta sådana utökningar.
-
En klass per blocktyp (L, T, ...)? Det är så klart en möjlighet, men en grundregel är att vi bara ska ha olika klasser om objekten faktiskt beter sig annorlunda. Här beter de sig blocken egentligen inte olika, utan de har bara olika utseende. Det kan vi enkelt representera som data (information om blockens form) istället för olika klasser.
Vi upprepar regeln: Skapa bara nya klasser om de verkligen har olika beteende som ger större skillnad i koden. Skapa inte nya klasser när det räcker med att några värden skiljer sig. I projektet ger detta komplettering!
-
En enda klass? Det verkar vara ett bättre val. Och om vi sedan vill implementera exploderande block kan vi göra det med en enda ny klass, snarare än sju olika subklasser ("exploderande L", "exploderande O", osv) – eller till och med i den ursprungliga klassen.
Vi skapar därför en enda blockklass, som vi kan
kalla Poly
(kort för Polyomino).
Vi behöver ett sätt för varje Poly
-objekt att veta
vilken "form" den har. Detta har inte att göra med visualisering
(pixlar på skärmen) utan är en del av den fundamentala modellen
för ett block (till exempel avgör det hur långt ett block kan
falla). Därför ska det definitivt finnas med i
Poly
-klassen. Eftersom vi ska placera ut
Poly
-objekt på vårt Board
kan vi med
fördel representera denna på samma sätt. Vi väljer därför en
tvådimensionell array för vår Poly
.
Tanken är att klassen Poly
ska kunna användas för
vilken typ av block som helst, inklusive t.ex. block med 3 eller 5
kvadrater. Därför vill vi inte direkt i Poly
hårdkoda
kunskapen om exakt vilka blocktyper som finns. I stället låter vi
konstruktorn ta in en tvådimensionell array av kvadrater som
parameter. Den exakta storleken på arrayen bestäms då av den som
anropar konstruktorn.
Att göra 2.8.1: Polyominos
-
Implementera klassen
Poly
enligt beskrivningen ovan.Klassens konstruktor ska alltså ta in en array som beskriver konfigurationen hos en godtycklig polyomino. Sådana arrayer kommer sedan att skickas in av
TetrominoMaker
i nästa uppgift – det är där de 7 specifika blocktyperna definieras.
Att förstå 2.8.2: TetrominoMaker
Någon måste hålla reda på vilka Poly
som ska finnas
i spelet, hur de ser ut, och hur de skapas. Detta är ett
tydligt ansvarsområde som man kan skapa en egen klass för
– med en fabriksmetod. Vi skapar alltså
klassen TetrominoMaker
med dessa metoder:
public int getNumberOfTypes() { ... }
public Poly getPoly(int n) { ... }
En anropare kan då skapa en TetrominoMaker
och
anropa getNumberOfTypes()
för att fråga om hur
många blocktyper som den kan ge oss (just nu 7).
Anroparen kan sedan slumpa fram ett tal mellan 0
och getNumberOfTypes()-1
och anropa
objektets getPoly()
med detta som argument.
Metoden getPoly()
returnerar då ett
nytt Poly
-objekt av den givna typen.
getPoly()
gör detta genom att skapa en 2D-array av
lämplig storlek, fylla den med kvadrater motsvarande det begärda
blocket, skapa en ny Poly
för denna array, och
returnera det nya objektet. Om ett ogiltigt nummer skickas in kan
ett fel signaleras: throw new IllegalArgumentException("Invalid index: " + n);
Vilken storlek ska då arrayen ha? För att underlätta för senare rotation av blocken är det en fördel att använda en storlek på 2x2, 3x3 eller 4x4, beroende på vilken tetromino som den innehåller. Arrayerna kan då skapas enligt vänstra kolumnen i följande bild (där de övriga kolumnerna visar hur blocken ser ut när de roteras, vilket vi kommer till senare). Blocket O ("kvadraten") ser ut att ligga i en array med storlek 4x3, men för att förenkla för oss själva kan vi helt enkelt lägga det i storleken 2x2 istället.
Bilden kommer från en beskrivning av det standardiserade rotationssystemet.
Att göra 2.8.2: TetrominoMaker
-
Implementera klassen
TetrominoMaker
enligt beskrivningen ovan.Tänk på att inte skapa för långa metoder – om
getPoly()
blir för lång när du lägger in skapandet av alla blocktyper i den metoden kan du bryta ut delar till sju olika hjälpmetoder istället.
Att förstå: Fabriker och fabriksmetoder
En TetrominoMaker
är en fabrik (factory): Ett
objekt som kan skapa andra objekt och returnera dem till anroparen.
Man kan också bredda detta begrepp och kalla getPoly()
för en fabriksmetod (factory method).
Att förstå 2.8.3: Tetrisblock på spelplanen
Vår spelplan kan representera de
block som redan har fallit ner. Detta representeras som en array av
SquareType
s, vilket ger oss all information vi behöver för att rita
upp det som är kvar av de block som fallit ner. Nu behöver den
också kunna representera ett block som är på väg att falla
ner, så att all information om spelets nuvarande tillstånd sparas på
ett och samma ställe. Hur åstadkommer vi det? Läs klart innan du börjar!
Alternativ 1 är att de fyra kvadraterna
(SquareType
) från det fallande blocket läggs in i samma
tvådimensionella array som används för att lagra de block som redan
har fallit ner. Det har en fördel: Eftersom allt som ska ritas ut
ligger lagrat på samma sätt som förut, behöver vi inte behöver ändra
uppritningsfunktionen (BoardToTextConverter
). Å andra
sidan har vi en nackdel: När det fallande blocket flyttar sig
ett steg (eller roteras) måste vi komma ihåg var vi hade lagt in det,
ta bort dess SquareType
s från dessa positioner
i arrayen och lägga till dem igen på nya positioner. Detta kan bli
lite omständigt.
Alternativ 2 är att den tvådimensionella arrayen
i Board
bara lagrar de block som redan har fallit
färdigt. Det block som fortfarande faller får Board
hålla reda på genom att ha ett fält som pekar på en
fallande Poly
(eller null
om inget block
faller just nu) och två fält som anger dess x- och y-koordinater.
Då blir det lätt att flytta det fallande blocket genom att helt
enkelt ändra dess koordinater, utan att överhuvudtaget peta
i Board
:s tvådimensionella array. Det underlättar
också framtida utökningar där ett block inte nödvändigtvis faller
och roterar med ett "hopp" från ett läge till ett annat utan
animeras jämnt och mjukt. Å andra sidan behöver vi ändra
uppritningsfunktionen så den tar hänsyn till både de block som
fallit färdigt och det fallande blocket.
Här ska det andra alternativet användas, eftersom det
första ger många studenter problem. Det kräver nya fält och
metoder i Board
samt utökningar
i BoardToTextConverter
.
Just nu (utan mjuka animeringar) gäller alltså för varje position
(x,y) som ska ritas ut (läggas till i textsträngen): (1) Om
positionen täcks av en kvadrat i ett fallande block gäller
denna SquareType
. (2) Annars gäller
den SquareType
som anges av Board
.
(Just nu kan det finnas positioner (x,y) där det både finns en ruta från spelplanen och en ruta från den fallande tetrominon. I dessa fall är det alltid rutan från den fallande tetrominon, inte rutan från spelplanen, som ska "ritas ut" (läggas till i strängen). Det vill säga, den fallande tetrominon ska aldrig "döljas bakom" de rutor som redan ligger i spelplanen, utan ska alltid synas. När vi har spelmekaniken på plats kommer den här typen av överlapp inte att kunna ske!)
Hur åstadkommer vi det? Ett sätt är
att BoardToTextConverter
känner till fallande block,
och vet hur de ska ritas ut. Ett annat är att implementera en ny
metod SquareType getSquareAt(x,y)
i Board
som vet vilken kvadrat som ska synas på en
viss position (enligt ovan) och returnerar detta:
- Blocket
falling
har sitt övre vänstra hörn på vissa koordinater, som vi kan kalla (x1,y1). Utifrån detta ochPoly
:ns storlek kan vi beräkna vilka kvadrater som täcks av dennaPoly
: Från (x1,y1) till (x2,y2). - Om (x,y) inte ligger mellan (x1,y1) och (x2,y2) kan
falling
definitivt inte täcka över (x,y). Då är det bara att titta iBoard
-arrayen. - Annars täcker
falling
över positionen (x,y) – men den kan göra det med en "genomskinlig" ruta (EMPTY
) på sin interna position (x-x1,y-y1). I så fall ska vi också titta iBoard
-arrayen. - Slutligen finns fallet där
falling
täcker över positionen (x,y) och faktiskt har en annanSquareType
änEMPTY
där. Då måste vi returnera rättSquareType
enligtfalling
.
Då kan BoardToTextConverter
anropa den metoden utan
speciell kunskap om fallande block. Välj själv!
Att göra 2.8.3: Tetrisblock på spelplanen
-
Lägg till ett fält
Poly falling
iBoard
. Där ska du lagra en pekare till "den poly som just nu håller på att ramla ner", eller null om ingen poly faller just nu. Du behöver också ha koll på polyns nuvarande position (x,y), så lägg till fält för detta. Lägg slutligen till getters så attBoardToTextConverter
kan få ut den fallande polyn och dess koordinater. -
Ändra så att även den fallande tetrominon "ritas" ut (i textsträngen). Se "Hur åstadkommer vi det?" ovan.
-
Testa genom att skapa en spelplan, placera ut ett block på någon hårdkodad position, och skriva ut resultatet som text. Ser du det fallande blocket på den position där du vill ha det?
Tetris 2.9: Textgrafik i ett fönster
Att förstå 2.9.1: Textgrafik
Det är nu dags att visa upp textgrafiken i ett fönster istället för i terminalen. Om du arbetar enligt det föreslagna schemat kommer du hit innan vi har diskuterat hur grafiska gränssnitt programmeras i Java. Gränssnittet kommer därför att vara väldigt simpelt och vi kommer att ge extra tips om de viktigaste sakerna man behöver veta.
Du kommer alltså att börja med att skriva ett grafiskt gränssnitt som
använder en grafisk textkomponent, en JTextArea
, för att visa
den gamla textrepresentationen av en "spelplan". Det verkar kanske som ett
lite underligt sätt, men det gör dels att du kan börja med GUIt redan
innan du lär dig hur man skapar egna komponenter, dels att du får ett
enklare "mellansteg" så att du inte behöver göra om hela
användargränssnittet på en gång. Det gör det också möjligt att göra en
väldigt enkel animering av t.ex. nedfallande block genom att du i
JTextArea
byter ut texten i varje steg, istället för att
som med System.out.println()
skriva ut varje ny
spelplan under den förra.
Denna flerstegsimplementation är något vi gör i just Tetris-projektet men är absolut inte ett krav för det senare projektet!
Att göra 2.9.1: Textgrafik
Skapa den nya klassen
TetrisViewer
.På något sätt ska
TetrisViewer
få tillgång till ettBoard
-objekt. Man kunde tänka sig attTetrisViewer
själv skulle skapa ettBoard
, men det är bättre om man istället skickar in ettBoard
som parameter till konstruktorn. Detta följer principen att varje klass ska ha en väl avdelad uppgift att utföra och ska inte göra något som ligger utanför den uppgiften!TetrisViewer
s uppgift är att visa en spelplan och låta oss interagera med den. Spelstartarens uppgift är (bland annat) att skapa en spelplan och ge den till ett fönster och till alla andra som kan behöva tillgång till den.Vi vill nu att
TetrisViewer
ska kunna öppna ett fönster för att visa upp spelet. Var gör vi det?En möjlighet vore att lägga detta i konstruktorn. Å andra sidan kan också argumentera för att konstruktorer ska initialisera objekt men inte göra särskilt mycket "riktigt arbete". Den som anropar konstruktorn kan vänta sig att snabbt få tillbaka ett nytt objekt och sedan be det objektet att göra något, som att öppna ett fönster.
Därför skapar vi istället en ny metod
show()
iTetrisViewer
. Denna metod ska skapa ett fönster, enJFrame
, som den ska använda för att visa upp spelet – både själva spelplanen och annan information runt omkring, till exempel poängräkning.JFrame
representerar alltså ett GUI-fönster som kan innehålla ett antal olika komponenter såsom knappar, menyer och textrutor. Klassen har en konstruktor som saknar argument, men då får vi ingen fönstertitel. Det är bättre att anropa denJFrame
-konstruktor som tar emot en fönstertitel som parameter.Nedan antar vi att fönsterobjektet lagras i en variabel (eller ett fält) som heter
frame
.I
show()
(eller i metoder som denna anropar) ska du sedan bygga upp ett lämpligt användargränssnitt. Detta sker bland annat genom attshow()
lägger till komponenter i fönstret som den har skapat.Den viktigaste delen just nu är den
JTextArea
som ska användas till att visa själva "spelplanen". Skapa alltså en sådan och ange i konstruktorn tillJTextArea
hur många kolumner och rader som ska visas – detta avgör vilken preferred size textarean ska ha och därmed hur stor den blir på skärmen. Antalet kolumner och rader får du reda på genom att fråga detBoard
som konstruktorn har fått som parameter, inte genom att hårdkoda!Hur hittar man vilka konstruktorer som finns? I IDEA, skriv "
new JTextArea(
" och tryck Ctrl-P. Då får du se alla varianter på parametrar. Välj en av dem och tryck Ctrl-Q så får du dokumentation för parametrarna.Textarean behöver också ges sitt första innehåll. Här förutsätter vi att du har följt instruktionerna och skrivit en
BoardToTextConverter
med en metod som returnerar en sträng, inte en metod som skriver ut en spelplan direkt till t.ex.System.out
. Då kan du använda den metoden för att få en sträng motsvarande ettBoard
, och sedan använda textareans metodsetText()
för att sätta rätt text. Att uppdatera textarean när spelplanen ändras blir en senare uppgift.I övrigt behövs en del kod i
show()
för att definiera fönsterlayouten, se till att fönstret visas, osv.:Layouten för ett fönster hanteras av en layouthanterare som applicerar en layoutalgoritm för att placera ut komponenterna (inte genom att man själv anger koordinater för komponenterna). En lämplig start för just detta enkla fönster kan vara att
TetrisViewer
sätter fönstrets layouthanterare medframe.setLayout(new BorderLayout())
.Vi kan sedan lägga till textarean i fönstret med
frame.add(textarea, BorderLayout.CENTER)
.CENTER
placerar textarean "i mitten" i fönstret (givet att vi har just enBorderLayout
som layouthanterare). Eftersom vi inte har lagt till några andra komponenter i fönstret kommer textarean att ta upp hela utrymmet i fönstret.För att rutor i tetrisbrädet skall visas lika stora oavsett vilka tecken man valt att representera olika
SquareType
s lägger vi tilltextarea.setFont(new Font("Monospaced", Font.PLAIN, 20));
. Det sätter fonten till en monospaced font.För att utföra layouten och göra fönstret synligt:
frame.pack(); frame.setVisible(true);
Ändra en existerande testklass, eller skapa en ny, så att den skapar ett
Board
och enTetrisViewer
, och som ber dennaTetrisViewer
att öppna ett fönster. När testet körs ska slutresultatet vara att du ser ett lagom stort fönster med en framslumpad spelplan (inte 10 spelplaner, som tidigare). Just nu finns inga menyer eller knappar för att avsluta, så du får använda "stoppknappen" i utvecklingsmiljön för att stänga av testprogrammet (eller ctrl-c i terminalfönstret).
Tetris 2.10: Kodinspektion!
Att förstå 2.10.1: Kodinspektion
Det är viktigt att man granskar sin egen kod då och då, för att eventuella problem inte ska bli för långlivade. Ju tidigare man kan förbättra koden, desto längre tid har man att utnyttja de förbättringarna. Det gäller ännu mer i den här kursen, när ni själva håller på att lära er objektorientering och Java.
Det är ofta en bra idé att stanna till ett tag och gå genom koden man har skrivit "för hand". Men nu ska vi för första gången också börja titta närmare på vilken hjälp man kan få från automatiska verktyg – i vårt fall IDEAs kodvarningar.
Att göra 2.10.1: Kodinspektion
-
Kör IDEAs kodinspektioner med profil TDDD78-2020-v1.
- Välj Analyze | Inspect Code i menyn.
-
Välj inspektionsprofil "TDDD78-2020-v1" och tryck OK.
Använd INTE defaultprofilen.
Det här är till stor del ett sätt att lära sig skriva kod som inte bara gör vad den ska utan även är välskriven och strukturerad.
Förstår du inte en varning? Utmärkt! Då har du en chans att lära dig något nytt, redan innan du lämnar in koden! Läs IDEAs inbyggda beskrivningar, läs våra utökade beskrivningar, och om det inte hjälper, fråga gärna assistenten eller examinatorn.
Inget automatiskt inspektionsverktyg är 100% korrekt. Varningen ska alltså oftast tolkas som "har du tänkt på det här?" snarare än "du har fel!". Så småningom måste sådana kommenteras på plats i koden, med god motivering till varför varningen var "ogiltig" i detta fall. Vissa varningar kanske du vill kommentera redan nu.
Det kan även finnas varningar som kommer från att du helt enkelt inte är klar än, t.ex. varningar för kod som inte används. De varningarna kan du så klart ignorera i det här läget.
Saknar du inspektionsprofilen? Om du kör på universitetets datorer, och installerade IDEAs konfigurationsfiler korrekt, borde profilen finnas där. Felanmäl till oss! Om du kör hemma behöver du ladda ner konfigurationsfilerna (se "arbeta hemma"-sidan).
Avslutning
Här slutar andra laborationen. Det är dags att visa och demonstrera slutresultatet för din handledare – det krävs för att få godkänt!
Du behöver inte skicka in din kod just nu, utan handledaren tittar på det viktigaste vid demonstrationen. Det du har skrivit kommer däremot att följa med i en senare inlämning, och då kan handledaren göra en övergripande genomgång av allt du har gjort.
Passa gärna på att fråga om det är något du undrar över, och be om återkoppling på det du har skrivit!
Fortsätt direkt med nästa labb om handledaren är upptagen.
Labb av Jonas Kvarnström, Mikael Nilsson 2014–2020.
Sidansvarig: Jonas Kvarnström
Senast uppdaterad: 2020-01-16