Göm menyn

Labb 1: Intro till Java

Introduktion

Förberedelser

I denna version av labb 1 har vi ännu inte hunnit uppdatera alla bilder för den nya versionen av IntelliJ IDEA. Delar av användargränssnittet kan ha ändrats. Om du upptäcker några problem, kontakta gärna Jonas Kvarnström så att detta kan fixas.

Här ger vi ett första smakprov på programmering i Java och programmering med statiskt typade variabler. Vi börjar med enklare uppgifter utan objektorientering, för att ge en grund i det som skiljer sig från t.ex. Python. Detta är ett direkt önskemål från tidigare studenter.

Vi vill också introducera utvecklingsmiljön, så ni hittar funktionalitet som tidigare studenter har haft mycket nytta av. Det sparar tid i det långa loppet.

Labbarna har relativt detaljerade instruktioner, som är integrerade med studiematerial som diskuterar bakgrunden till vad man gör. Det är ett medvetet grepp för att ni ska få lära genom Uppgift, istället för att ni får höra allt teoretiskt på föreläsningar och sedan omedelbart förväntas kunna applicera kunskaperna i praktiken.

Större stycken av studiematerial ligger i egna "rutor" medan enskilda fakta istället kan vara integrerade i själva uppgifterna.

Vägen är målet!

Vill kursledningen desperat ha 160 nästan identiska program som hälsar världen och som summerar, multiplicerar och hittar primtal? Nej, givetvis inte. Slutresultatet är alltså inte en uppsättning små program, utan består av kunskap, förståelse och färdigheter. Programmen finns där för att du ska få undersöka vägen som leder till en viss enkel funktionalitet.

Därför innehåller labben en hel del studiematerial: Diskussioner av sammanhang, förklaringar av varför man gör på ett visst sätt, och generaliseringar från nuet till det du kommer att ha nytta av i framtiden. Ta dig tid att läsa och reflektera!

Efter föreläsning 1, intro till Java

Uppgift 1.1: Ett första program

Syfte: Komma igång!

Nu vill vi så snabbt som möjligt skriva och köra ett första Java-program. Vi börjar därför med en enkel "Hello World" och förutsätter att du har gått genom förberedelserna överst på sidan.

Du har väl genomfört förberedelserna överst på sidan? Då har du alltså ditt IDEA-projekt öppet.

Att skapa paket och klasser

Java kräver att man alltid "kapslar in" sin kod i en klass, så trots att vi egentligen inte fokuserar på objektorientering behöver vi börja med att skapa en klass där vi kan lägga vår kod.

För att organisera klasser, och undvika namnkollisioner när flera vill använda samma namn, använder Java hierarkiska paket, packages. Vi måste alltså också se till att det finns ett paket att lägga klassen i, och detta paket kommer att motsvaras av en katalogstruktur i filsystemet.

Vi har redan förberett "grundpaketet" se.liu.liuid123 i projektet. Det finns också ett paket som heter se.liu.tddd78.examples, som vi inte ska använda just nu.

Uppgift: Skapa paket och klass för övning 1

  1. Korrigera LiU-ID. Alla klasser i den här labben ska läggas i paketet se.liu.liuid123.lab1, där liuid123 är ditt LiU-ID. Det "förberedda" paketet har inte just ditt LiU-id, så du behöver döpa om liuid123 till korrekt ID. Välj liuid123 i projektvyn och högerklicka.

    Välj Refactor | Rename och skriv in ditt riktiga liuid.

  2. Skapa underpaket för labb 1. Högerklicka ditt liuid igen. Välj New | Package och skapa underpaketet se.liu.[ditt-liu-id].lab1.

  3. IDEA kan visa paket på olika sätt, t.ex. "ihoptryckta" så att tomma mellannivåer göms. Ta gärna en titt på dokumentationen.

  4. Skapa klassen Exercise1 genom att högerklicka på paketet (lab1) och välja New | Java Class. Du anger bara Exercise1, men det fulla namnet för klassen inklusive paket blir se.liu.liuid123.lab1.Exercise1.

    Klassen skapas med attributet public vilket betyder att kod i alla klasser kan komma åt den. Mer info om åtkomsträttigheter kommer under föreläsningarna.

Varför så konstiga färger i IDEA?

Nu har "skelettet" till en klass skapats, men en del färger kan se lite underliga ut. Klassnamnet "Exercise1" kan till exempel visas i underliga färger. Varför då?

Pekar du på färgläggningen får du svaret: IDEA har upptäckt att din klass är tom. Detta är två av hundratals olika varningar som IDEA kan ge. (I bilden ser du också att klassen saknar Javadoc; den varningen kan vara avstängd i din profil eftersom Javadoc inte behövs i alla klasser i labbarna.)

  • Ibland, som nu, kommer en varning helt enkelt för att vi inte har hunnit skriva klart.

  • Ibland kan det hända att vi har använt koden tidigare men slutat med det, och då är varningen bra att ha.

  • Ibland upptäcks allvarligare problem där varningarna kan vara livräddare.

  • Och ibland är varningarna helt enkelt felaktiga, eftersom analysen aldrig kan ha full förståelse av hur koden är tänkt att användas. "I genomsnitt" brukar varningarna dock ge mycket hjälp med många typer av problem.

Om huvudmetoder i klasser

I ett enkelt "Hello World"-program behöver vi inga objekt, men vi behöver veta vilken kod som körs när ett visst program startas. Python kör all kod som är på "toppnivå" i den Python-fil man anger:

def funktion():
    # Detta körs bara om vi anropar funktionen
    pass

print("Den här satsen är på toppnivå och körs när vi kör filen")

I Java anger man istället en klass, och en speciell main-metod (funktion) i den klassen startas. Metoden måste ha en specifik signatur (parametrar, returvärden, ...) för att bli igenkänd:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Den här metoden kommer att köras när vi 'kör' klassen");
    }
}

Uppgift: Skapa huvudmetod

Skriv ett litet Hello World-program enligt bilden nedan.

Man kan med fördel använda IDEAs Live Template-funktion genom att ställa markören i klassen (inom måsvingarna), välja Code | Insert Live Template: Ctrl-J , och sedan välja main() method declaration. Ett ännu snabbare alternativ är att skriva psvm och trycka Ctrl-J eller Tab. Det samma gäller System.out.println(); som kan fås via sout.

Som synes fick "Exercise1" nu en annan färg, eftersom vissa varningar försvann (klassen är inte tom och den kan användas genom att main() anropas) medan andra tillkom. I labb 1 kan ni ignorera de flesta varningarna eftersom de gäller sådant vi ännu inte har diskuterat.

Att köra ett program

Precis som Python kan Java bara köra ett program efter att källkodsfilerna har kompilerats översatts till ett internt bytekod-format.

  • I standardimplementationen av Python görs detta automatiskt "bakom kulisserna" av det vanliga python-kommandot. I vissa fall (när man importerar en modul) lagras den kompilerade bytekoden i en .pyc-fil.

  • Java har istället ett separat kompileringssteg, kommandot javac, som bara kompilerar och sparar den genererade bytekoden i form av .class-filer. Efter kompilering kör man programmet med t.ex. "java se.liu.ida.dinadress.tddd78.lab1.Exercise1".

IDEA och andra utvecklingsmiljöer tar dock hand om kompileringssteget automatiskt när man vill starta ett program.

Uppgift: Kör programmet

Högerklicka någonstans i den öppna filen och välj Run Exercise1.main(). Detta kompilerar automatiskt alla okompilerade filer, verifierar att inga kompileringsfel uppstod, och startar det program vi högerklickade. Du bör se utskriften i IDEAs körfönster:

Då man kör programmet skapar IDEA en körkonfiguration. Man kan välja bland körkonfigurationer och köra dem enkelt via knappmenyn (toolbar), och editera dem för att t.ex. lägga till kommandoradsparametrar. Om det inte syns en knappmeny i IDEA kan denna tas fram genom View | Toolbar.

Att kompilera utan att köra (IDEA)

IDEA kompilerar som sagt automatiskt innan ett program startas, men ibland vill man kompilera utan att köra, t.ex. för att dubbelkolla att inga syntaxfel finns kvar. Detta görs genom:

  • Build | Make Project Ctrl-F9, som kompilerar "det som behövs" men behåller det som inte ändrats.

  • Build | Rebuild project, som rensar gamla resultat och sedan kompilerar allt.

Uppgift 1.2: Kontrollstrukturer

Syfte: Bli bekant med kontrollstrukturer

Java har flera olika kontrollstrukturer för iteration (upprepning). Tidigare studenter har önskat sig övningar på detta, för att bekanta sig med alla och se hur de fungerar.

Uppgift: Skapa ny klass

I labb 1 skapar vi oftast en ny klass för varje uppgift. Här skapar du klassen Exercise2 i det paket du skapade tidigare. Klassen kan skapas:

  • (a) på samma sätt som förut genom att högerklicka på paketet (lab1) och välja New | Java Class, eller
  • (b) genom att ställa markören på klassnamnet Exercise1, trycka F5 (clone) och svara på frågorna.

I det senare fallet behöver man också ta bort onödig kod i den nya klassen – eller kommentera bort den, om man tror att den snart ska användas igen. IDEA underlättar att kommentera bort kodavsnitt genom att man markerar dem och trycker CTRL-/.

Att använda for-loopar i java

Vi ska nu titta på for-loopar. I Java har de i grunden samma "format" som i till exempel C, C++, JavaScript, Perl och PHP, med följande syntax:

for (initialization; condition; increment) {
  statements
} 
Initialization

Här deklareras och initialiseras den räknare som vi använder. Som alltid i Java måste man ange variabelns typ vid deklarationen. Exempelvis: int i = 1 om i är en tidigare okänd variabel som vi vill använda som räknare. Vi använder ofta just i,j,k som loopvariabel när vi har en kort loop över heltal och bara vill räkna antal gånger loopen körs. Om variabeln betyder något mer, döper vi den så att den blir mer förståelig (x, pos, cardNumber, enemyIndex, ...).

Condition
Här beskrivs det villkor som måste vara uppfyllt för att iterera ett varv – detta testas även för den allra första iterationen, så det är fullt möjligt att göra en loop som utförs noll gånger. Exempelvis: i < 10.
Increment
Här anges hur räknaren skall uppdateras. Exempelvis: i++ (som betyder samma som i+=1).

Ett fullständigt exempel för att iterera 10 gånger ser ut så här:

int summa = 42;
for (int i = 0; i < 10; i++) {
    summa = summa + 20;
    System.out.println("Nu har a värdet " + a);
} 

Detta är det kanoniska (vanliga, standardiserade) sättet att loopa över ett intervall av heltal i Java, C, C++ med mera, till skillnad från Python som använder sin range-funktionalitet.

När koden exekveras:

  • Först kommer initialization att köras. Det betyder att det kommer att finnas en int-variabel med namn i och värde 0 i for-loopen.

  • Innan varje iteration av koden i loopen, inklusive den första, kontrolleras condition-villkoret. Om det är falskt avbryts loopen. Annars exekveras den inre koden en gång. I det här fallet kommer värdet på variabeln summa att ökas med 20.

  • När koden i for-loopen har exekverats anropas increment-koden som i detta fall räknar upp värdet på i med 1. Därefter sker nästa test av condition, och så vidare.

När exempelkoden körs är resultatet att man 10 gånger ökar värdet på summa med 20.

Variablers scope / räckvidd i Java

Alla variabler har ett scope, en "räckvidd", som anger var de är tillgängliga. Detta scope kan fungera olika i olika språk.

I Java blir en variabel som deklareras inuti ett {block} bara tillgänglig inuti detta block. När man "går ut" en nivå är variabeln "borta".

Det gäller även variabler som deklareras inuti själva for-satsen. Därför är variabeln i ovanför bara känd i for-loopen och kan inte användas efter loopen – då har variabeln "försvunnit". Det går däremot bra att återanvända namnet till andra variabler efter loopen.

Uppgift: Summera tal med for-loop

  1. Skriv en publik statisk metod sumFor(int min, int max) som beräknar och returnerar summan av talen min,min+1,...,max med hjälp av en for-loop.

  2. Skapa en main()-metod.

    Vi vill vänta lite till med att titta på hur man hämtar input från användaren. Vi börjar därför med att hårdkoda värden på min och max. Vi kommer senare att läsa in dessa från användaren.

    Definiera därför final int min = 10; och sätt på liknande sätt max till 20 (inuti main). Nyckelordet final säger att vi inte ska ändra på variabelns värde, vilket är bra för att indikera att det här är en konstant. Det gör också att kodinspektionen inte klagar på att vi använder "magiska tal", eftersom den ser final-deklarationen som att detta namnger värdet.

    Därefter skall programmet anropa sumFor(min,max) och skriva ut resultatet med hjälp av System.out.println(), ungefär som i exemplet ovan.

  3. Testkör!

Andra sätt att iterera: while, do-while

Java har också två andra loop-konstruktioner, while och do-while. De ser ut så här:

while (condition) {
  statements...
}

do {
  statements...
} while (condition); 

Skillnaden mellan konstruktionerna är att do-while alltid genomför loopen minst en gång, eftersom villkoret inte testas förrän efteråt.

Dessa loop-konstruktioner är i grunden enklare än for-loopen, och kan passa bra när man har andra typer av slutvillkor som inte direkt kopplas till en variabel som räknas upp i varje steg. Exempel (en kö är en lista element där man bara lägger till i slutet och bara plockar i början, som när man står i kö):

while (!queue.isEmpty()) {
  // ... take first element from queue ...
  // ... do something with it ...
}

Uppgift: Prova while

Eftersom konstruktionerna är så lika testar vi bara en av dem.

  1. Skapa nu metoden sumWhile(...), som har samma parametrar och returvärden som sumFor() men istället använder en while-loop för att beräkna samma summa.

  2. Ändra main() så den även anropar sumWhile() och skriver ut resultatet från båda anropen. Testkör programmet. Får du alltid samma summor?

Uppgift 1.3: Multiplikationstabell

Syfte

Syftet med denna uppgift är att arbeta vidare med kontrollstrukturer samt att testa att skapa en namngiven konstant för att göra koden mer lättförståelig.

Om namngivna konstanter

Vi börjar med att skapa ett program som skriver ut multiplikationstabellen för ett "hårdkodat" (fixerat) tal, t.ex. 5.

Istället för att bara använda en konstant som 5 "rakt av" är det dock oftast bättre att ge den ett symboliskt namn, t.ex. genom att deklarera en konstant TABELL med värdet 5. Detta underlättar både kodförståelse ("TABELL" är lättare att förstå än "5") och senare ändringar (lättare att ändra den enda deklarationen än att ändra värdet "5" på massor av platser i koden).

Uppgift: Välja multiplikationstabell

Börja med att skapa klassen Exercise3 för uppgiften.

Deklarera en konstant som anger vilken multiplikationstabell vi skall skriva ut. Denna skall vara synlig endast i klassen och bör deklareras som följer, på toppnivå i klassen:

private final static int TABELL = 5;

Här stöter vi på Javas namngivningsstandard som säger att namn på denna typ av konstanter skall skrivas med stora bokstäver. Att "variabeln" är final gör att dess värde inte kan ändras, så att den faktiskt blir en konstant. Att den är static gör att den bara lagras en gång i klassen, istället för i varje objekt av typ Exercise3 (om vi nu hade skapat några sådana objekt). Djupare förklaringar av detta kommer under föreläsningarna.

Om kodkomplettering

IDEA kan komplettera / föreslå fortsättning på ord, något ni kan använda för att snabbt fylla i långa namn eller för att se vilka fortsättningar som överhuvudtaget finns. Man väljer det alternativ som passar, t.ex. genom att skriva tills man lätt kan markera rätt namn och sedan trycka på TAB.

Beroende på inställningar kan förslagen komma upp automatiskt efter en kort fördröjning. Annars kan man trycka Ctrl-Space för vanlig komplettering.

Utöver den vanliga kompletteringen finns "smart completion" som tar hänsyn till vilken datatyp som förväntas (Ctrl-Shift-Space), "statement completion" som kompletterar vissa kodkonstruktioner (Ctrl-Shift-Enter), "hippie completion" som helt enkelt letar efter matchande ord överallt, "postfix code completion" med mera – se dokumentationen om du är intresserad.

Om utskrifter och strängkonkatenering

Nu är det dags att programmera iterationen och utskriften. I Java sätts strängar för utskrift samman genom konkatenering via operatorn '+'. När man försöker "addera" ett tal och en sträng förstår Java automatiskt att talen ska konverteras till strängar. Man kan därför göra en utskrift enligt följande exempel, där i är en loopvariabel:

   System.out.println(i + " * "+TABELL + " = " + ...); 

Uppgift: Utskrift

  1. Skapa en main-metod och konstruera en loop som skriver ut den valda multiplikationstabellen: 1*TABELL, 2*TABELL, osv. upp till 12*TABELL.

  2. Testkör programmet på samma sätt som i tidigare uppgift.

Uppgift 1.4: Inmatning

Syfte: Testa inmatning från användaren!

Syftet med denna uppgift är dels att testa inmatning av värden från en användare, dels att utforska typer via konvertering från strängar till heltal.

Att förstå: Inmatning

Vi vill nu låta användaren ange vilken multiplikationstabell som ska visas. Vi gör detta genom en utökning i samma klass som tidigare.

I Java kan vi läsa in information från kommandofönstret med hjälp av klassen Console. Här väljer vi istället att läsa in ett värde från en grafisk dialogruta, vilket faktiskt är lika enkelt!

Uppgift: Inmatning

  1. Utöka programmet från förra uppgiften för att läsa in ett värde från användaren. Detta måste göras i mainmetoden, innan loopen. Exempel:

       String input =
          JOptionPane.showInputDialog("Please input a value"); 

    IDEA kommer att markera att den inte känner till klassen JOptionPane. Vi måste berätta var denna finns genom att antingen trycka Alt + Enter när IDEA föreslår detta, t.ex. då markören står framför raden, eller genom att trycka på den röda glödlampan som dyker upp efter en stund till vänster på raden och välja Import Class.

  2. Värdet som läses in är en sträng, och måste konverteras till ett heltal (om möjligt). Exempel:

       int tabell = Integer.parseInt(input); 

    Då detta är en lokal variabel namnges den inte med stor bokstav. Ändra därför från TABELL till tabell i den tidigare utskriften så att det nya värdet används.

  3. Provkör!

    Testa gärna att mata in en icke-numerisk sträng och se vad som händer. Vi lämnar som överkurs (frivilligt!) att ta hand om felet som uppstår och låta användaren försöka på nytt. De här typerna av fel (exceptions och "unchecked exceptions") kommer att diskuteras mer senare i kursen och det är just nu helt OK att låta programmet krascha vid felaktig input.

Uppgift 1.5: Felsökning med IDEAs hjälp

Syfte: Hitta syntaxfel

Denna uppgift syftar till att testa hur en programmeringsmiljö kan hjälpa till att hitta fel i koden.

Att förstå: Syntaxfärgläggning som ett verktyg

Det händer ofta när man skriver kod att något inte blir syntaktiskt korrekt. IDEA och många andra omgivningar hjälper till att lösa dessa problem innan man kompilerar genom syntaxfärgläggning och markering av fel och problem, något som är vardagsmat nuförtiden men var en mindre revolution när det introducerades. Vi demonstrerar detta med hjälp av fakultetsfunktionen.

Uppgift: Syntaxfärgläggning

  1. Börja med att precis som ovan skapa klassen Exercise5. I den, inuti klassdefinitionen (mellan måsvingarna {}), klistrar du in följande felaktiga kod:

    public static void main(String[] args) {
      for (int i = 0; i < 10; i+1) {
        system.out.println(i + "-fakultet: " + facrotial(i));
      }
    }
    
    /**
    * Calculates f! given f.
    * @param f
    * @return f!
    */
    private factorial(int f) {
      if (f = 0)
        return 1;
      }
    
      int result = 1
      for (int i = 1; i <= f; i+1) {
        result *= i;
      }
    
      return result
    }
    

    IDEA kommer direkt att känna av och markera en hel del fel. Det finns ett 10-tal fel varav ett är att "static"-kod i detta fall endast kan anropa annan "static"-kod.

  2. Se till att koden blir körbar och skriver ut 0-fakultet till och med 10-fakultet.
  3. Testa att skriva ut n-fakultet för större värden på n. Hur långt kan man komma? Var börjar det bli fel? Varför? (Om du inte vet svaret kommer vi att titta på det senare.)

Uppgift 1.6: Felsökning med debugger

Syfte: Börja testa debuggern

En debugger (avlusare) kan vara ett extremt användbart verktyg vid programmering. Syftet med denna uppgift är att introducera debuggern så tidigt som möjligt, för att förhoppningsvis spara mycket tid i senare skeden. Målet här är egentligen inte är att få ett fungerande program, utan att se hur man lagar ett trasigt program – därför får vi ta en del "omvägar"!

Om att dela upp funktionalitet + statiska metoder

För att även få lite mer programmeringsvana i Java skriver vi ett nytt program att avlusa. Detta program kommer att testa om ett tal är ett primtal.

När vi ska införa en väl avgränsad funktionalitet, som att testa primtal, bör den separeras ut för att förbättra modularitet och läsbarhet. Primtalstestet implementeras därför i en separat metod.

Att testa primtal är en sorts primitiv funktionalitet där man skulle kunna hävda att objektorientering egentligen inte behövs. Med andra ord, vi vill inte nödvändigtvis skapa ett objekt som är en primtalstestare, och sedan fråga det objektet om ett visst tal är ett primtal. Istället kan det kännas naturligt att använda en ren funktion isPrime(), precis som i Pythonkursen.

Men Java har inga "rena" funktioner som existerar utanför klasser. Vad vi har är statiska metoder som existerar i själva klassen utan att vi först skapar objekt av klassen. Detta är något som ska användas sparsamt men som faktiskt är lämpligt just för isPrime(). Vi kommer att diskutera detta i mer detalj under en av föreläsningarna.

Uppgift: Primtalsletare

  1. Skapa en ny klass Exercise6 precis som tidigare.

  2. Skapa en egen metod, public static boolean isPrime(int number), vars ansvar är att testa om number är ett primtal.

    I metoden lägger du en for-loop som går igenom alla tal 2,3,..., number-1 och ser om något av dessa tal delar number. I så fall returneras false, annars true. Här behövs alltså en if-sats inuti for-loopen.

    För att avgöra om ett tal delar inparametern använder vi följande felaktiga kod:

        int rest = number / i;
        if (rest == 0) {
            // number är en jämn multipel av i
            ...
        } 
  3. Vi testkör nu funktionen. Låt main() skriva ut resultatet av isPrime(5). Starta klassen som vanligt. Svaret bör bli true om du har gjort rätt.

    Modifiera programmet så att main() även skriver ut isPrime(4). Det blir också true vilket ju är fel. Vi har alltså upptäckt ett fel i programmet.

Om avlusning och brytpunkter

Vi kunde nu lägga till utskrifter för att ta reda på vad som egentligen händer i programmet, men det finns ett mer flexibelt sätt: Vi använder debuggern och ser till att programmet automatiskt stannar för inspektion vid en specifik brytpunkt (breakpoint).

Uppgift: Påbörja avlusning

  1. Sätt en brytpunkt på raden if (rest == 0) genom att vänsterklicka i listen till vänster om koden eller genom att stå på raden och trycka CTRL-F8. En brytpunkt indikeras genom en röd cirkel i listen till vänster samt genom att raden blir färgmarkerad.

  2. Starta programmet i debuggern genom att högerklicka och välja Debug Exercise5.main() (inte "Run"!). Det ger följande fönster:

    Programmet har nu startats, men har automatiskt pausats då brytpunken nåtts. Detta visas genom att raden med brytpunkten får blå färg.

    Notera att debug-fönstret visas under kodfönstret. I debug-fönstret finns som standard tre mindre fönster. Det vänstra visar vilken anropskedja som ledde oss till funktionen vi befinner oss i körs – i det här fallet anropades den från main(). Det mittersta visar intressanta variabler och deras värden. Det högra visar Watches som är variabler vi valt att hålla extra koll på (för närvarande inga).

  3. Nu är det dags att se vad som har hänt.

    I debuggern ser vi att vi att number=4 och i=2. Vi ser i koden att vi försökte hitta resten vid division med i, och i debuggern att resten faktiskt blev 2, vilket inte är vad vi förväntat oss. 4 är ju jämnt delbart med 2!

    Alltså måste något vara fel på den tidigare raden och mycket riktigt, / borde ju vara en annan operator. Vilken? Ändra detta och testkör (starta om debuggern) för att se att 4 inte längre klassas som ett primtal.

  4. När allt fungerar kan du avsluta debuggningen genom att trycka på den röda stoppknappen till vänster eller genom att trycka Ctrl-F2. Notera att debug-fönstret under koden stannar kvar. Du kan antingen stänga det eller växla tillbaka till run-fönstret.

Diskussion: Vad har vi uppnått?

Just i detta fallet ser man kanske inte omedelbart poängen. Men för mer komplicerade fel är debuggern enormt användbar, och under kursen kommer ni sannolikt att bekanta er mer med den.

En stor fördel är att ni omedelbart kan se värden på alla variabler. Ni kan också inspektera egenskaper hos objekt och följa pekare till andra objekt, till exempel gå in på detaljerna i varje objekt i en lista. Detta kommer ni att se mer av när vi kommer in på objektorientering.

Att debugga kod utan att ta hjälp av en debugger är möjligt, så kallad "print-debuggning" där man lägger in utskrifter av önskade variabler eller programflöden på strategiska ställen. Sedan kompilerar man och flyttar utskrifterna allt eftersom man får mer information om var man skall söka efter buggarna.

Med hjälp av debuggern kan man istället få ut all information direkt och behöver inte kompilera och köra om programmet många gånger för att lokalisera en bugg. Det är därför väldigt tidsbesparande och ger även en inblick i hur koden faktiskt exekverar vilket leder till ökad förståelse.

Man kan också skapa automatiska brytpunkter när undantag (exceptions) kastas, eller se till att programmet bara avbryts (pausas) om vissa villkor är uppfyllda. Det gör det mycket enklare att hitta fel som är oväntade eller som bara inträffar ibland. I den situationen är det speciellt värdefullt att kunna utforska hela programmets tillstånd, och alla variabler. Vid "print-debuggning" är risken att man lägger till några utskrifter, testar i flera timmar för att trigga felet, och sedan till slut upptäcker att man borde ha skrivit ut ytterligare en variabel...

Vad mer kan man göra i debuggern?

Run-menyn visar tillgängliga operationer Uppgift i debugläget. De vanligaste är:

  • Step Over | F8 som exekverar en rad kod och går till nästa utan att gå in i funktionsanrop.
  • Step Into | F7 som exekverar en rad kod och om raden innehåller ett funktionsanrop så hoppar den in i funktionen.
  • Run to Cursor | Alt+F9 som exekverar tills den kommer till nuvarande markörposition. Detta är samma som att lägga till en brytpunkt, köra till den och sedan ta bort den igen, ett förfarande som ofta används.
  • Run | F9 kör vidare till nästa brytpunkt.

Det finns många avancerade sätt att underlätta debuggning, t.ex. att köra tills ett givet villkor är sant, eller tills en given rad exekverats ett bestämt antal gånger. Ni kan hitta mer information via IDEAs hjälpsida.

Uppgift: Hitta fler primtal

Vi avslutar den här uppgiften genom att skriva ut alla primtal under 100.

Gör en for-loop i main() metoden. Iterera över talen från 2 till 100 och skriv ut alla som klassificeras som primtal.

Det finns en fori-mall för att snabbt få till en for-loop. Använd den på samma sätt som psvm och sout.

Uppgift 1.7: Mer kontrollstrukturer

Syfte: Bli bekant med switch-satsen

Vi ska nu titta på switch, som i vissa fall är ett mer läsbart och koncist alternativ till if.

Om switch-satsen

Vi ska nu testa en alternativ villkorssats: switch. Denna villkorssats testar inte godtyckliga villkor. Istället anger man ett uttryck, och vilken gren man utför beror enbart på detta uttrycks värde. Detta kan bli mer läsbart då man slipper upprepa uttrycket man vill jämföra.

Ända sedan Java 7 (2011) fungerar switch med heltal, enum-typer, och strängar. Mest stöd från IDEA fås vid användning av enum-uttryck i switch-satsen eftersom IDEA då känner till vilka möjliga grenar som finns.

Uppgift: Switch

  1. Vi skall utöka koden i uppgift 1.2, alltså i Exercise2. Låt main() använda JOptionPane.showInputDialog för att läsa in en textsträng som skall vara "for" eller "while".

    Låt switch-satsen anropa rätt summeringsfunktion beroende på denna input. Glöm inte break!

    Lägg även till en default-etikett i switch-satsen. Denna gren utförs om man inte skrev in något av de acceptabla valen och bör alltså skriva ut ett felmeddelande.

  2. Testa programmet!

Uppgift 1.8: Kortslutning av logiska operationer

Syfte: Testa logiska operationer

I Java finns två olika sätt att utföra de logiska operationerna and och or. Det är viktigt att man känner till skillnaderna i hur de fungerar. Om man gör fel kan det leda till att onödiga eller potentiellt farliga beräkningar görs.

Om kortslutning i uttryck – i Python

I Python finns operatorerna and och or. Båda dessa är kortslutande, vilket betyder att om resultatet är känt efter att första operanden beräknats kommer den andra aldrig att beräknas.

Om Python behöver värdet av fun1(a) and fun2(b) sker alltså detta:

  • Python anropar fun1(a) för att beräkna dess värde.

  • Om resultatet är falskt vet vi redan att hela uttrycket, fun1(a) and fun2(b), också måste vara falskt: Både False and False och False and True är ju falska! Då garanterar Python att man inte alls anropar fun2(b).

  • Om resultatet istället var sant måste fun2(b) anropas.

På motsvarande sätt fungerar även a or b, fast där får man så klart avsluta beräkningarna om man har fått reda på att a är sant.

Om kortslutning i uttryck – i Java

Samma funktionalitet finns i Java, med && istället för and och med || istället för or.

Men det finns också en variant av vardera operator där bara ett tecken används istället: a & b och a | b. I dessa uttryck garanteras att både a och b räknas ut. I vissa fall kan detta vara användbart, men som vi ska illustrera nedan är det oftast den kortslutande varianten man vill ha.

Uppgift: Kortslutning

  1. Skapa en ny klass, Exercise8, med en tom main-metod. Skriv sedan funktionen askUser som tar som inparameter en sträng som kommer att presenteras för användaren av programmet. Funktionen skall returnera sant om användaren svarar "ja", vilket åstadkoms genom att returnera det booleska värdet av följande uttryck:

    JOptionPane.showConfirmDialog(null, question, "",
      JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION
              
  2. Lägg till kod i main-metoden så att programmet ligger i en evig loop med hjälp t.ex. av en while(true)-loop.

    Nu skall man få möjligheten att avbryta, men bara om man är helt säker. Detta görs genom att ställa frågorna askUser("Quit?") och askUser("Really ?"). Om båda svaren från askUser blir true skall programmet avslutas (return från main-metoden) och annars ska det skriva ut att programmet fortsätter köra.

    Prova båda varianter nedan (en i taget!) och ge olika svar på frågorna:

    if (askUser("Quit?") && askUser("Really?")) { ... }
    if (askUser("Quit?") & askUser("Really?")) { ... }

    Vilken verkar bäst att använda i detta fall? Vad händer med respektive utan kortslutning?

Efter föreläsning 2, typer / allmän OO

Uppgift 1.9: Typning

Syfte

Typning i Java skiljer sig från hur det hanteras i Python. I Java sker en stor del av typkontrollen vid kompilering, eller i vårt fall ännu tidigare, så fort IDEA kan avgöra typerna.

Uppgift: Testa typning i Java

Vi skall återanvända ett exempel från Python-kursen, implementationen av Newton-Raphsons klassiska metod, i det här fallet hårdkodad för att hitta kvadratroten ur ett tal.

  1. Skapa en ny klass, Exercise9, med en tom main-metod. Skriv sedan funktionen findRoot som tar som inparameter en double (vi vill ju räkna med decimaler) och returnerar en double.

  2. Givet att inparametern heter x skall funktionen först sätta variabeln guess till detta värde och sedan utföra guess -= (guess*guess-x) / (2*guess); 10 gånger innan den returnerar det slutliga värdet på guess. (Mer information om metoden finns här: Newton-Raphson.)

  3. Lägg till inläsning av det värde vi vill hitta roten ur i main-metoden. Inläsning sker med hjälp av JOptionPane på samma sätt som tidigare:

    String x =
        JOptionPane.showInputDialog("Please input a value");

    Vad händer när du matar in koden? Går det att använda x som argument till findRoot? Hur kan IDEA annars veta att det inte går innan koden körs?

  4. Fixa koden genom att konvertera inmatningen till en double med hjälp av Double.parseDouble.

  5. Se till att koden skriver ut resultatet, och testkör!

Uppgift: Komplettering och typning

Det här är ett bra tillfälle att illustrera hur IDEAs funktion för komplettering blir mer kraftfull med hjälp av variabeltyper.

Testa i koden för uppgift 1.9 att ta bort parametern som skickas till findRoot, och ställ sedan markören i den tomma parentesen precis som om du höll på att skriva den här koden just nu:

System.out.println("Roten ur " + x + " är " + findRoot([markör]));

När du trycker Ctrl-space (basic code completion) föreslår IDEA allt som skulle kunna stå i den här positionen. Bläddra gärna i listan och se vad som är möjligt att skriva här. Ett exempel är x, vilket kanske verkar underligt eftersom x är en sträng – men man hade kunnat använda till exempel x.length(), vilket börjar med x.

Men nu vet du kanske att du vill använda dig av en variabel som redan är av rätt typ. Då trycker du Ctrl-shift-space (smart completion). IDEA föreslår då endast att man kompletterar med något som direkt matchar typen på den valda positionen. I detta fall minskas antalet alternativ avsevärt. Du kan läsa mer här: auto completion.

Uppgift 1.10: Primitiva numeriska datatyper

Syfte

Vi skall nu titta lite mer på skillnader mellan de olika datatyper som används vid numeriska beräkningar. De vanligaste är int, long, float och double.

Om olika numeriska datatyper – heltal

Det är viktigt att känna till skillnaden mellan olika primitiva datatyper. Speciellt kan man ibland välja fel datatyp för numeriska beräkningar.

I Python 3 finns en enda datatyp för heltal: int. Den skiljer sig från heltal i de flesta andra språk genom att den själv utökar sitt lagringsutrymme så att den kan lagra godtyckligt stora tal:

>>> 2 ** 1000

10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376

>>> 2 ** 10000

19950631168807583848837421626835850838234968318861924548520089498529438830221946631919961684036194597899331129423209124271556491349413781117593785932096323957855730046793794526765246551266059895520550086918193311542508608460618104685509074866089624888090489894838009253941633257850621568309473902556912388065225096643874441046759871626985453222868538161694315775629640762836880760732228535091641476183956381458969463899410840960536267821064621427333394036525565649530603142680234969400335934316651459297773279665775606172582031407994198179607378245683762280037302885487251900834464581454650557929601414833921615734588139257095379769119277800826957735674444123062018757836325502728323789270710373802866393031428133241401624195671690574061419654342324638801248856147305207431992259611796250130992860241708340807605932320161268492288496255841312844061536738951487114256315111089745514203313820202931640957596464756010405845841566072044962867016515061920631004186422275908670900574606417856951911456055068251250406007519842261898059237118054444788072906395242548339221982707404473162376760846613033778706039803413197133493654622700563169937455508241780972810983291314403571877524768509857276937926433221599399876886660808368837838027643282775172273657572744784112294389733810861607423253291974813120197604178281965697475898164531258434135959862784130128185406283476649088690521047580882615823961985770122407044330583075869039319604603404973156583208672105913300903752823415539745394397715257455290510212310947321610753474825740775273986348298498340756937955646638621874569499279016572103701364433135817214311791398222983845847334440270964182851005072927748364550578634501100852987812389473928699540834346158807043959118985815145779177143619698728131459483783202081474982171858011389071228250905826817436220577475921417653715687725614904582904992461028630081535583308130101987675856234343538955409175623400844887526162643568648833519463720377293240094456246923254350400678027273837755376406726898636241037491410966718557050759098100246789880178271925953381282421954028302759408448955014676668389697996886241636313376393903373455801407636741877711055384225739499110186468219696581651485130494222369947714763069155468217682876200362777257723781365331611196811280792669481887201298643660768551639860534602297871557517947385246369446923087894265948217008051120322365496288169035739121368338393591756418733850510970271613915439590991598154654417336311656936031122249937969999226781732358023111862644575299135758175008199839236284615249881088960232244362173771618086357015468484058622329792853875623486556440536962622018963571028812361567512543338303270029097668650568557157505516727518899194129711337690149916181315171544007728650573189557450920330185304847113818315407324053319038462084036421763703911550639789000742853672196280903477974533320468368795868580237952218629120080742819551317948157624448298518461509704888027274721574688131594750409732115080498190455803416826949787141316063210686391511681774304792596709376

I de flesta språk har man istället fixerad storlek på heltalstyperna. Java har bland annat de primitiva typerna int (32 bitar) och long (64) bitar. Då är det viktigt att förstå vad som kan hända om en beräkning överskrider kapaciteten hos variabeln, vilket vi snart ska testa.

Överkurs
Java kan lagra godtyckligt stora tal i objekt av typen BigInteger, men detta kräver en helt annan syntax.

Om olika numeriska datatyper – flyttal

När det gäller flyttal är det inte lika enkelt att ha godtycklig "storlek", bland annat för att många beräkningar (som 1/3) inte alls kan representeras med ett ändligt antal decimaler. Där har även Pythons "vanliga" flyttal ändlig precision – i de flesta implementationer används 64-bitars flyttal.

Java erbjuder istället två typer: double, som alltid har 64 bitar, och float, som har 32 bitar. Här använder man normalt double, men float kan t.ex. användas då man lagrar många flyttal och vill spara minne.

Uppgift: Olika numeriska datatyper

  1. Skapa en ny klass, Exercise10, med en tom main-metod. Vi skall nu lägga till kod till main-metoden.

  2. Börja med att lägga till int number = 16777216;. Vi skall nu konvertera denna till en float och sedan tillbaka till int för att se vad som händer. Lägg därför till float decimal = number;. Lägg märke till att eftersom det talområde som kan representeras av en float är större än motsvarande för en int accepteras detta av Java. Det sker en implicit typkonvertering, dvs vi behöver inte skriva något extra.

    Lägg till utskrifter för variablernas värden och testkör. Vad ser du?

  3. Konvertera nu tillbaka genom att lägga till int integerAgain = decimal;. Lägg till utskrifter för variablernas värden och försök testköra. Vad händer?

    I det här fallet kan inte decimalerna för en float rymmas i en int och Java kräver att man hanterar detta, t.ex. genom att göra en explicit typkonvertering (avrundning) genom att skriva int integerAgain = (int)decimal;. Då visar man att man verkligen ville bli av med decimalerna.

  4. Skriv ut number, decimal och integerAgain och jämför vilka värden som skrivs ut. Ändra sedan från 16777216 till 16777217 och jämför utskrifterna. Vad händer?

  5. Byt typ på decimal till double och jämför resultatet. Vad händer?

  6. Lägg till nya variabler int big = 2147483647; samt int bigger = big+1. Gissa först vad som kommer att skrivas ut och skriv sedan ut värdena. Blev det som ni tänkt? Byt datatyp på bigger till en större, long i stället för int, och jämför. Vad hände?

  7. Testa att ändra biggers definition till long bigger = big+1L; och notera resultatet. Slutligen ändrar vi till long bigger = (long)big+1; och jämför.

Mer om Javas primitiva numeriska datatyper

Vi skall nu förklara vad vi sett.

Till att börja med sker konvertering implicit om den mottagande typen innehåller talområdet för den föregående typen, dvs från int och long sker implicit konvertering till float och double. På andra hållet krävs explicit konvertering eftersom det inte är säkert att den mottagande typen kan representera värdet.

Vad som inte skyddas mot är att man förlorar information vid konvertering mellan heltal och flyttalstyper. Vi såg exempel på det när 16777216 och 16777217 representerades av samma float. Detta beror på att flyttal kan representera ett större talområde, men innehåller bara samma mängd information som motsvarande heltal (int och float består av 32 bitar data medan long och double består av 64 bitar data). När vi bytte från float till double gick det däremot bra att behålla informationen om heltalet 16777217 eftersom det finns mer plats i en double vilken därför har noggrannare precision i de tal den representerar.

Vi får liknande problem när vi avrundar från float till int då t.ex. (int)Math.PI blir 3. Även har tappas information, men detta är dock något vi är mer vana vid och därför mer sällan gör fel på.

I steg 6-7 undersökte vi relationen mellan int och long. Vi såg att om vi försöker representera ett tal större än vad som ryms i en int så slår talet runt och blir negativt. Detta är uppenbart något man behöver hålla koll på. Med hjälp av en long kan man minska risken att detta händer då mycket större tal kan representeras.

Vi såg att det inte sker någon automatisk konvertering från int till long när talet blir för stort för en int. I vårt fall (long bigger = big+1) är det så att både big och 1 är av typen int vilket innebär att resultatet av additionen blir en int. När detta sedan tilldelas till en variabel av typ long är skadan redan skedd. För att få additionen att ske i storleken long måste minst en av de båda ingående talen vara en long. Om det är olika storlek/precision på de ingående talen kommer det mindre att konverteras till den större typen och resultatet vara av den större typen. I vårt fall uppnås detta genom att vi antingen deklarera konstanten 1 som en long (1L) eller genom att explicit konvertera big till long.

Efter föreläsning 3, OO i Java

Uppgift 1.11: En egen "fullständig" klass

Syfte: Skapa egna datatyper och 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 tittar även på utskrifter och ser hur vi ger en klass en egen anpassad utskriftsmetod.

Tänk på att du behöver kunna objektorientering från föreläsning 3 innan du börjar!

Om modellering och objekt

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.

Uppgift: Skapa en klass för personer

  1. Skapa klassen Person i paketet lab1.

  2. Addera ett privat fält med namn name av typen String som ska användas för att lagra personens namn. Eftersom varje person har sitt eget namn (som lagras i personobjektet) ska fältet inte vara static.

  3. Addera ett privat icke-statiskt fält birthDay av typen LocalDate som ska användas för att lagra personens födelsedatum. När LocalDate 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 importera LocalDate 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 klassen LocalDate.

    Här finns mer information om LocalDate för den som är intresserad.

  4. Lägg till en konstruktor som tar en String och en LocalDate som inparametrar och lagrar dessa värde i objektets "privata" fält. IDEA kan snabbt skapa en konstruktor via Alt+Insert->Constructor.

  5. (Adderat 2021-01-27) Se till att allt kommer i rätt ordning – fält, sedan konstruktorer, sedan metoder – så som vi har diskuterat på föreläsningen. Lägg helst main() sist i klassen.

  6. Ett LocalDate-objekt som representerar ett datum kan t.ex. skapas med hjälp av anropet LocalDate.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!

Om 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 perioden består av genom metoden getYears().

Uppgift: Tidsperioder

  1. Implementera metoden getAge() som returnerar personens ålder. Eftersom varje person har sin egen ålder (som lagras i personobjektet) ska metoden inte vara static.

  2. Testa att metoden fungerar genom att skriva ut åldern på den person med dina data som skapats i main()-metoden.

Uppgift: Skapa och skriva ut objekt

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.

  1. Se till att main() skapar minst ett Person-objekt och skriver ut det medSystem.out.println(). Du skapar objekt med ett anrop till en konstruktor, på formen new Person(...).

  2. Testkör. Får du ett underligt resultat, i stil med "Person@28cd724"? Då är allt rätt.

Om utskrifter och metoden 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.

Överkurs
Alla klasser har egentligen redan en 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.

Uppgift 1.11.4: Utskrifter och toString()

  1. Tryck Alt+Insert och välj Override. Välj sedan toString().

  2. 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åt toString() returnera värdet name + " " + getAge().

  3. Skapa några personer till och skriv ut var och en på sin egen rad.

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.

Avslutning

Här slutar första laborationen. Det är dags att visa och demonstrera slutresultatet för din handledare, så du kan få godkänt på labben!

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–2021.


Sidansvarig: Jonas Kvarnström
Senast uppdaterad: 2021-02-01