Göm menyn

TDDE10 Objektorienterad programmering i Java

Tentatips

Innehåll

Nedan följer en kort lista med koncept som alltid ingår i tentan:
  • Grundläggande java-syntax. D.v.s variabler, konstanter, datatyper, deklarationer, uttryck, in- och utmatning, if-satser, loopar, underprogram o.s.v.
  • Grundläggande objektorientering. D.v.s. Objekt/klasser, ansvar, konstruktorer, arv, klasshierarkier, överskuggning, polymorfism, inkapsling/synlighet, instans- resp. klassvariabler/-metoder, abstrakta metoder/klasser.
  • Abstrakta datastrukturer. D.v.s. att använda javas inbyggda ADT:er (t.ex. listor, stackar, köer, mappar, mängder m.fl.) eller att implementera egna.
  • UML-diagram (klassdiagram) D.v.s. hur man ritar upp vilka klasser systemet har, vilka attribut och operationer de har och vilka relationer de har sins emellan. Ofta kan man tjäna mycket tid på att läsa UML-diagrammen noga, för att få tips om hur man bör strukturera sin lösning.
Nedan följer en kort lista med koncept som ofta kommer på tentor:
  • Generiska klasser
  • Undantagshantering
  • Interface
  • Grafiska gränssnitt
Nedan följer en kort lista med koncept som kan förekomma på tentor:
  • Förbjuda arv/överskuggning (final)
  • Typkontroller och konvertering
  • Jokertecken
  • Nästlade klasser
  • Objektorienterad analys
  • Strömmar/filer
  • Slumptal
  • Rekursion
  • Javadoc
Det bör dock poängteras att kursens innehåll varierar något från år till år och allt som ingår i kursen kan potentiellt sett dyka upp på examinationen. Listorna ovan bör alltså inte betraktas som absoluta i någon mening utan finns endast för att ge någon form av guidning i tentapluggandet.

Vad får jag använda?

Java har många inbyggda paket och klasser. Vi tillåter att man använder sådana klasser. Generellt sett är det fritt fram att använda det så länge det inte explicit står i uppgiften att man inte får använda en viss sak eller att det uppenbarligen är så att det inbyggda man använder överlappar helt eller till mycket stor del med vad man som tentand skall visa att man kan. Detta innebär alltså att man oftast har full rätt att nyttja de saker som finns, t.ex. det som finns i paketet java.util. Här finns det mycket smått och gott som kan göra livet för tentanden lite lättare:
  • ArrayList, en implementation av ADT:n lista.
  • Arrays, en klass med statiska metoder för array-manipulation (t.ex. sortering och sökning).
  • Collections, en klass med statiska metoder för manipulation på samlingar (t.ex. listor, mängder, o.s.v.).
  • HashMap, en implementation av ADT:n ordbok. (Finns även TreeMap).
  • Random, en klass för att slumpa tal.
  • Scanner, en klass för att läsa ut data från en fil/ström/sträng.
  • Stack, en implementation av ADT:n stack.
  • TreeMap, en implementation av ADT:n mängd. (Finns även HashSet).
Om man ändå är osäker på vad som är okej eller inte okej att använda när man implementerar sin lösning så bör man alltid fråga examinatorn under tentan. Utveckling av mjukvara är alltid ett samspel mellan programmerare och kund, detta reflekteras även på tentan. Om man nödvändigtvis måste göra ett antagande så bör man dokumentera detta antagande väl med kommentarer i sin kod. Implementationer som bygger på felaktiga antaganden är dock inget som är önskvärt och leder oftast till komplettering eller (i slutet av tentan) underkänd uppgift.

Vanliga missar

Nedan har vi samlat ihop några av de vanligaste felen som tentander gör som leder till komplettering på uppgifter på tentan.
  • Duplicering av kod i sub/superklass. Om t.ex. superklassen lagrar ett visst data i en instansvariabel så skall inte subklassen också behöva lagra detta data, om det inte finns särskilda goda skäl. Om det finns generella beteenden för klasserna så bör detta ligga i superklassen. Om precis samma (eller nästan exakt samma) kod finns i sub- som superklass så låter man antagligen inte superklassen ta det fulla ansvar som den bör. Tänk på att man kan anropa super.metodAnrop() för att anropa superklassen!
  • Felaktiga konstruktorer. Den som anropar en konstruktor skall behöva ange (med parametrar) de data som klassen behöver få reda på utifrån för att initieras. Den som anropar konstruktorn skall inte behöva ange t.ex. konstant data, eftersom det då finns en risk för att den som anropar gör fel. Ett exempel är klassen Cat som lagrar namn och antal liv. Namnet är något som den som anropar skall ange, men antal liv skall alltid börja på 9. Anropet till konstruktorn bör alltså vara:
      new Cat("Isaac");
    
    Inte på följande sätt:
      new Cat("Isaac", 9);   // Det var givet att katter alltid har 9 liv från början!
                             // Varför låta anroparen få möjligheten till att göra fel?
    
    Internt blir då konstruktorn (på det korrekta sättet):
      public Cat(String name) {
        this.name = name;
        this.lives = 9;
      }
    
    Detsamma gäller när det kommer till att anropa superkonstruktorn. Alla parametrar som kommer in till subklassens konstruktor kanske inte nödvändigtvis skall skickas till superkonstruktorn! Detta beror ju på. Vissa subklasser kanske fixerar en eller flera av superklassens attribut. I slutänden måste man noga tänka över: vad som den som instansierar objektet skall ange, vad som skall sparas i subklassen och vad som skall skickas vidare till en eventuell superklass. Svaret finns nästan alltid i uppgiftstexten, läs noga!
  • Variabeldeklarationer på fel ställen. Ett mycket vanligt fel är att man inte deklarerar sina variabler rätt. Tänk på att deklarationsstället helt och hållet avgör livslängden på variabeln. En variabel lokalt deklarerad i en metod, samt parametrar, överlever bara så länge man är i den metoden. En variabel deklarerad i klassen är en instansvariabel (såvida den inte är static) och överlever så länge som objektet finns. Variabler som bara används lokalt men som har deklarerats som instansvariabler och vice versa ger komplettering.
  • Implementationen följer inte uppgiften. Ofta får man ganska mycket text och ofta får man även ett UML-diagram i uppgiften. En del av texten är ofta av inledande/tematisk natur, men större delen av texten (och diagrammet) beskriver ofta icke-funktionella krav på din implementation. Dessa skall följas så läs dem noga.
  • Fullständiga uppräkningar. Många sådana missar beror helt och hållet på att man inte har ett generellt lösningssätt. Vad händer när problemet blir "1" större, eller "3" större, eller "100" större? Använd loopar för att få bort fullständiga uppräkningar. Ibland kan vanliga fält hjälpa oss här om uppräkningen inte har något generellt mönster. T.ex.:
      String[] weekDays = new String[]{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"};
      for (int i = 0; i < weekDays.length; ++i) {
        if (userInput.equals(weekDays[i])) {
           System.out.println(userInput + " is a weekday.");
           break;
        }
      }
    
    Ofta beror dock fullständiga uppräkningar på att man inte har utnyttjat polymorfi för att delegera utförandet av metoder till rätt subklass. Sådana fullständiga uppräkningar är allra viktigast att få bort och inse varför de inte är bra för koden.
  • Felaktig användning av static. Detta är något som vi vet att många har svårt för så det gäller att vara extra noga med att lära sig det väl. När en variabel deklareras static så existerar bara en sådan för hela klassen (alla objekt har en gemensamt). Detta har enorm påverkan på klassens beteende. När en metod deklareras static så betyder det att man inte behöver ett objekt av klassen för att anropa den. Och när man väl har anropat den så är man inte inne i ett objekt, d.v.s. man har inte tillgång till instansvariabler eller instansmetoder. Klassvariabler och klassmetoder är mindre vanliga än instansvariabler och instansmetoder när man jobbar objektorienterat men de dyker ändå upp här och där. I UML-diagram markeras static med understruken stil. Om man vill initiera en klassvariabel gör man det rimligtvis precis vid deklarationen, t.ex:
      public class SomeClass {
        private static int someClassVariable = 42;
      }
    
    En vanlig miss här är t.ex. att man istället initierar klassvariabler i klassens konstruktor. Detta leder till problem eftersom klassvariabeln då alltid återställs varje gång ett objekt av klassen skapas.
  • Felaktig inkapsling (saknar t.ex. private). Denna är mycket vanlig. Man har helt enkelt glömt att sätta private på sina instansvariabler, eller public på sina metoder. Tänk på att hjälp-metoder i klasserna kan vara private. Om man inte sätter någon synlighet i java så får man paketsynlighet, detta vill man bara ha någon gång ibland. I UML markeras private med ett minustecken, public med ett plustecken och protected med en brädgård (#).
  • Felaktig överskuggning. När en subklass skall överskugga en metod så måste metodnamnet och parametrarna vara precis som i superklassen. Missar man detta så får man ju istället en ny metod i subklassen. Detta kan leda till konstiga fel där man tycker att polymorfiskt anrop borde ske, men det inte blir det. Ett hett tips är att använda taggen @Override ovanför de metoder som man tänker sig skall överskugga. Då gör java-kompilatorn en automatiskt check att detta faktiskt är en metod i någon superklass och att överskuggningen sker korrekt. Har man då gjort något fel (t.ex. stavat fel på metodnamnet) så får man ett kompileringsfel istället för en svårhittad bug.
  • Grova övertramp mot kodkonventionerna. När det är tenta är vi medvetna om att studenterna är lite mer stressade än vanligt och vi har lite överseende med att koden inte alltid kanske blir den vackraste. Det finns dock ingen anledning till att tillåta totalt kaos. Då blir det bara ett försvårat arbete både för den som skriver koden och den som skall rätta. Vi kan ha överseende med att en krull-parentes sitter fel på något enstaka ställe, eller att man kanske har indenterat en eller två rader fel. Men om vi får kod som ser ut på detta sätt:
        private void Print_array() {
                     for (int r = 1; r <= mapArray[0].length; ++r)
     {
            for (int c = 1; c <= mapArray.length; 
            ++c) {
            System.out.print    (mapArray[c-1][r-1].get_symbol()+" ");
            }
            System.out.println(" ")    ;
            }    }
    
    Så får man kompletteringen: "Oläslig kod". Lyckligtvis så finns det verktyg direkt i eclipse för att auto-formatera kod. Ctrl-shift-f. Tänk på att konventioner för namngivning också är mycket viktigt för läsbarhet.
  • "Pekarfel". Detta resulterar vanligen i att programmet kraschar med NullPointerException, men kan även ha andra tråkiga effekter. Om du får ett NullPointerException, gå till den rad där felet har kastats och titta efter punkter. Operatorn punkt är avreferering, d.v.s. att man följer en referens. NullPointerException får man i java när man försöker följa en referens som är null. Alltså, om det på den raden står så här:
      house.getOwner().printYourself();   // här kastas NullPointerException
    
    Så är det troligast att antingen variabeln house är null, eller att det som metoden getOwner() returnerar är null, eftersom det är dessa två referenser som avrefereras på den raden.
    En annan vanlig miss som har med referenser att göra är hur objekt hanteras. Tänk t.ex. på att operatorn "==" jämför två referenser, d.v.s. om de är samma objekt, inte om de råkar ha lika innehåll. Särskilt för strängar innebär detta att man istället skall använda metoden equals() för att jämföra deras innehåll.
  • Felaktig ansvarsfördelning mellan klasser. Detta kan arta sig på olika sätt. Ofta ser man kanske att en klass har blivit enorm medans andra jättesmå. Detta behöver inte vara fel men är ofta ett gott tecken på att något är galet. En klass bör ha så få ansvarsområden som möjligt. När en klass börjar få för mycket ansvar blir den för står, och jobbig att tampas med. Då bör den brytas upp i mindre delar eller så skall ansvar flyttas över på andra klasser.
    Ett annat symptom är att det behövs typkontroller (operatorn instanceof) och castningar för att lösa problem som uppstår när klassen skall interagera med andra klasser. Detta beror inte sällan på att ansvaret har hamnat på fel ställe. Självklart kan det vara så att det behövs typkontroller och castningar ibland (annars skulle de ju inte finnas i språket), men det sker ganska sällan och uppstår oftast när det är någon form av delat ansvar mellan klasser som kanske varken hör hemma i endera klassen, eller känns som att man skulle vilja lägga det i båda. Där typkontroller och castning används, men man kan undvika det om man istället korrekt använder sig av arv och polymorfi så får man komplettering.
  • Avsaknad eller felaktig användning av generiska parametrar. När man har en generisk klass så måste man komma ihåg den där generiska parametern när man använder den! Om man inte gör det så reverterar den generiska typen inuti till klassen Objekt och vi får inga vettiga typkontroller. Ta t.ex. följande exempel:
      ArrayList kittenList = new ArrayList();
      kittenList.add(new Cat("Isaac"));
      kittenList.get(0).getName();    // Kompileringsfel. Här måste vi nu konvertera!
    
    Detta ger kompileringsfel, trots att klassen Cat har metoden getName()! Dessvärre ser ArrayList endast sina lagrade element som "Objekt", eftersom vi inte angav någon generisk parameter när vi deklarerade den! Det korrekta sättet att göra deklarationen är:
      ArrayList<Cat> kittenList = new ArrayList<Cat>();
    
    Då får vi faktiskt ett objekt av typen Cat när vi anropar metoden get() och vi kan göra katt-specifika saker på det!
    Detta blir extra viktigt när man gör och använder egna generiska klasser. Ett tips är att om kompilatorn börjar prata om "Raw Type" så har du nog glömt att ange generisk parameter med <>-parenteser någonstans.

Sidansvarig: Erik Nilsson
Senast uppdaterad: 2019-03-08