1 Samlingar 1.1 Frekvenstabell Kommentarer Se javakoden. • En Integer är icke-muterbar (precis som String, Float, Boolean et.c.). Ickemuterbarhet har många fördelar, men en nackdel är att ett helt nytt objekt måste skapas när ett nytt värde ska lagras, något som kan dra ned programmets prestanda. Ett frekvensvärde i uppgift 2 lagras som en Integer och när ett sådant värde ökas, är det alltså istället ett nytt objekt med det högre värdet som skapas. • Vid testkörning av programmet w5/ex1/frequencytable/sol/HistogramTest är skillnaden inte särskilt stor1 (givetvis lite beroende på mängden unika nycklar). Provkör gärna och se vad du får för skillnad. Typiskt hos mig är att Histogram1 kör på 1.6 sekunder och Histogram2 kör på 2.4 sekunder per test. 1.2 Iteratorrättning Kommentarer Se javakoden. 1.3 En exklusiv union Kommentarer Se javakoden. 1.4 Under huven Kommentarer • Övningen vill visa på vikten av valet av datastruktur, trots att de ofta erbjuder samma funktionalitet genom sina gränssnitt. • En länkad lista (x) är som synes bäst för tillägg av innehåll (add) medan den är betydligt sämre än de andra två andra på att söka upp (contains) ett lagrat element. Detta beror på att allting i en länkad lista läggs ”på rad”, med bara direkt tillgång till första (och ev. sista) elementet i denna rad. Att lägga till ett element är därför lätt, men att hitta det kräver att programmet börjar i ena änden av raden och går igenom ett element i taget, i värsta fall kan det därför behöva stega sig igenom alla. 1 . . . på min maskin, Pentium i5, GNU/Linux 3.0.0-13-generic x86 64, OpenJDK 64-bit Server VM 1.6.0 23 1 Figur 1: Antal steg för olika datastrukturer som en funktion av antal element de innehåller (n), något förenklat. Struktur Mean add Worst add Mean contains Worst contains Länkad lista 1 1 n/2 n Balanserat träd ∼ log2 (n) log2 (n) log2 (n) − 1 log2 (n) Hashtabell ∼1 n−1 ∼1 n • Ett träd (y) visar sig ungefär lika bra vid tillägg och sökningar. Trädet lagrar sina element i form av, ja just det, ett träd :) I förhållande till den länkade listan behöver den (förmodligen) inte gå lika lång väg, den börjar vid trädets rot och letar sig steg för steg ut till rätt gren. Detta behöver den göra vid både tillägg och sökning, och därför tar dessa ungefär lika lång tid. Vid tillägg behöver den ev. också ändra på trädets struktur för att det ska fortsätta vara just ett träd (i värsta fall skulle ett träd annars kunna bli en länkad lista, d.v.s. alla grenar ligger på en rad efter varandra). Denna omstrukturering kallas för balansering 2 . För att kunna avgöra var elementen ska lagras måste de kunna jämföras med varandra (d.v.s. implementera Comparable<T> i Java). • En hashtabell (z) ter sig i det här testet som det bästa alternativet, även om länkade listan slår den för tillägg. Dess snabbhet beror på att den internt använder sig av en array, men den kan ändå inte garantera att alla element lagras på olika positioner. Flera element som lagras på samma position läggs därför i en länkad lista. Om (teoretiskt) alla element lagrades på en position skulle alltså en hashtabell degraderas till en enda länkad lista och heller inte prestera bättre. Utmaningen ligger därför i att försöka placera elementen så jämt över arrayens positioner som möjligt. En position i en array nås som vanligt genom dess index, alltså ett positivt heltal. För att bestämma indexet för ett element utförs ofta en beräkning som involverar elementets inneboende tillstånd (känns det bekant? i Java används metoden hashCode() som ju alla objekt innehåller). • Varför används då träd överhuvudtaget om hashtabellen ändå presterar bättre? Ett träd har ytterligare en egenskap, det håller elementen sorterade. Dessutom är värsta falls-beteendet för hashtabeller linjär, d.v.s. det kan bli lika dyrt att söka som i en länkad lista. Skillnaden mellan förväntad genomsnittstid kontra värsta falls-tid kan ses i figur 1.4 där n är antalet befintliga element i strukturen. Vi antar också att de träd vi använder garanterat är balanserade, vilket är fallet med t.ex. TreeSet och TreeMap. Genomsnittsfallet för hashtabeller är något förenklat men är inte en funktion av antalet element utan av fyllnadsgraden hos tabellen. 2 För mer information, se kurser i datastrukturer och algoritmer. 2 2 2.1 Generics Typsäkerhet Lösning Se programkod. Kommentarer Programmet krashar då en Bicycle inte har Car som superklass. Då programmet kommer till det element som är en Bicycle i vehicles blir det ClassCastException; en cykel släpper inte ut koldioxid (annat än möjligen genom cyklisten själv). Då vi använder ”råa typer”, d.v.s. vi utelämnar den möjliga typparametern finns det ingen möjlighet för kompilatorn att lista ut att vi vid ett senare tillfälle förväntar oss att det endast finns objekt av en given typ i samlingen. Lösningen blir alltså att inskränka samlingen genom att istället deklarera variabeln Collection<Car> vehicles = new HashSet<Car>;. Nu kompilerar programmet inte om man försöker lägga till t.ex. en cykel. 3 3 Singleton-mönstret Lösning Se programkod. Kommentarer • Klassen är inte en singleton eftersom flera instanser kan skapas av klassen. • För att göra klassen till en singleton skulle följande behöva förändras: – Genom att istället göra en enum av klassen blir det omöjligt att skapa instanser av den. Notera semikolonet som indikerar att listan av enumelement är slut. Detta är alltså en enum utan enum-element! – För ett mer utvecklat resonemang, se gärna Item 3 i Joshua Bloch’s ”Effective Java”. • Det finns dock inget att tjäna på att göra klassen till en singleton eftersom det inte finns något tillstånd som behöver initieras vid ett bestämt tillfälle.3 Men eftersom klassen bara innehåller statiska metoder bör den inte gå att instansiera överhuvudtaget. Att göra den till en enum hindrar oss också från att kunna skapa instanser genom deserialisering4 . Att göra en vanlig klass med privat konstruktor skyddar oss inte från detta. • Detta har ingenting med singleton egenskapen att göra men. . . Då denna veckas övningar delvis handlar om generics, studera generaliseringen som gjorts i GameUtils. Det är onödigt oflexibelt att bara kunna skapa och fylla fält av fält av GameTile. Genom några små förändringar har vi nu åstadkommit metoder som kan fylla och skapa fält av fält av vilken referenstyp som helst. Om vi bytt ut typparametern T mot klassnamnet Object hade vi kunna blanda element helt på måfå. Som metoderna ser ut nu kan vi bara fylla dem med element av just typen T. 3 Det finns mycket kritik mot designmönstret Singleton, till stor del p.g.a. att en singleton innebär införande av ett globalt tillstånd. För att läsa mer, gör en internetsökning, det har diskuterats ordentligt. . . 4 Se http://en.wikipedia.org/wiki/Serialization#Java 4 4 En udda dekoratör och iterator Kommentarer • Klassen hade kunnat användas som iterator till en ”svagt icke-muterbar” samling, d.v.s. åtkomst till samlingens ingående objekt tillåts (vilka kan vara muterbara), men samlingen själv går inte att förändra. • Java har t.ex. till skillnad från C++5 en hårt specificerad design av sin iterator. Det hade varit naturligare om denna delats upp på flera klasser, där den ”minsta klassen” bara erbjudit metoderna ”nästa” och ”harNästa”. Större varianter skulle då inkludera fler operationer. Som övningen visar, tvingas vi i Java att erbjuda möjligheten att ta bort element med iteratorn (såvida inte remove() ”fulhackas” bort genom exception-kastning). Dessutom går det bara att stega framåt, inte bakåt. • Eftervillkoren för de två metoderna skiljer sig åt såtillvida att processIterator(ForwardInputIterator<T>) omöjligen kan mutera den underliggande strukturen; remove är ju borta. Det tråkiga med denna ”lösning” är att vi får ett körningsfel om vi försöker anropa remove i metoden processIterator(Iterator<T>) med en ForwardInputIterator. 5 http://www.cplusplus.com/reference/std/iterator/ 5