Göm menyn

TDDE10 Objektorienterad programmering i Java

Grunderna för OOP i Java

Inledning

Syftet med denna laboration är att ge dig en grundläggande uppfattning om objektorienterade koncept i java. Den är tänkt att göras steg för steg, på egen hand eller med en labkamrat, och skall ge stöd inför kommande laboration, laboration 2, om objektorientering. Laborationen har följande innehåll:
  • Klasser och instanser
  • Meddelandesändning
  • Polymorfi
  • Arv
  • Instansvariabler och klassvariabler (static-variabler)
  • Metoder och klassmetoder (static-metoder)
  • Inkapsling och "information hiding" (skydda implementationen)
  • Skyddsnivåerna private, protected, public

Under laborationen kommer du att modifiera och bygga på klasserna i programmet på olika sätt. Det kan vara klokt att spara undan koden från de tidigare stegen genom att skapa kopior av filerna. Eclipse låter dig göra detta direkt via "Package Explorer" panelen.

Handledningens struktur

Laborationen är indelad i 4 steg. Varje steg exemplifierar olika begrepp.

I varje steg finns litet kodexempel. Det kan vara exempel på hur er kod ska se ut eller annan kod som ska kunna använda din kod.

Steg 1 och 2 följs i sin helhet för att fullborda stegen.

Varje steg avslutas med ett antal frågor. Frågorna ska du själv använda som kontrollpunkter att du förstår vad som händer. Det är en mycket god idé att bolla dina svar mot en laborationsassistent för att få bästa förståelse.

Steg 1 - Ansvar/Polymorfi

På filerna Main.java och Animal.java har man skapat ett program där man representerar djur och dess beteenden (så som att göra ljud, flyga och presentera sig själva). Programmet är skrivet helt imperativt. I animal-klassen ligger dock tre påbörjade metoder som är ett litet steg i riktningen mot en objektorienterad lösning. I denna första del skall vi försöka arbeta om koden så att ansvaret hamnar på en mer naturlig plats, istället för att allt ligger huller om buller i Main. Verktygen som vi kommer att använda är två av de mest centrala i objektorientering, nämligen arv och polymorfi.
  • Ladda ner filerna, kompilera och kör programmet.
  • Läs koden och se hur detta är löst. Upplägget borde vara mycket bekant från din tidigare imperativa programmeringskurs. Lösningsmetoden kanske du även kommer ihåg namnet på... fullständig uppräkning... något som vi allra helst vill slippa eftersom vi vet att det leder till icke-generella lösningar, grötig och svårhanterad kod.
Frågor:
  • Antag att vi vill lägga till ett eller flera djur. Vad händer med koden?
  • Antag att vi vill ändra något djurs beteende. Är det "lätt" eller "svårt"?
  • Antag att något djur har fel beteende (en bugg i koden), hur pass "lätt"/"svårt" är det att hitta problemet?
  • Antag att vi låter någon annan (som inte är insatt i koden) lägga till ett djur. Hur stor del av programmet behöver man sätta sig in i?

Uppgift:
  • Skapa nu istället fyra nya klasser för att representera varje djur som en egen typ; hundar, katter, fåglar och fjärilar. Alla djur har ett namn (en sträng), detta behöver bara ligga i Animal-klassen. Klasserna Dog, Cat, Bird och Butterfly ska ärva från klassen Animal. Detta innebär att Dog, Cat o.s.v. har alla de egenskaper som Animal har, utöver det som är specifikt för hundar resp. katter. Animal blir då superklass och de fyra nya klasserna blir subklasser till Animal.

    Om vi vill beskriva klasshirarkin ovan med ett UML-diagram så kan vi rita det på följande sätt (här visar vi bara Animal, Cat och Dog. Bird och Butterfly blir på precis samma sätt som Cat och Dog.):


    Det kommer mer om UML-diagram och vad de olika symbolerna betyder längre fram.

    TIPS 1: För att beskriva ett arv i kod använder man nyckelordet extends. För att komma åt superklassen används ordet super:

       class Cat extends Animal {
    	  Cat(String name) {
    	    super(name);   // Animals konstruktor anropas
    	  }
    	
    Detta säger att Cat är en vidareutveckling av Animal, dvs. ett arv. I bilden ovan representeras detta med pilarna med triangulära huvuden.
  • Varje klass behöver nu en egen konstruktor. Konstruktorn är den metod, med samma namn som klassen själv, som anropas då någon instansierar klassen. D.v.s. gör new på klassen för att skapa ett sådant objekt. Varje djurs konstruktor behöver ta en parameter (name) och skicka den vidare till Animals konstruktor (genom att anropa super, se koden ovan). Variabeln type i Animal kommer inte behövas längre (eftersom vi nu har separata klasser för att representera de olika typerna) så den kan tas bort, samt motsvarande parameter i Animals konstruktor.
  • Ändra de fyra första raderna i huvudprogrammet (metoden main i klassen Main) så att vi skapar instanser (objekt) av rätt typ (klass). D.v.s.:
    
    	Animal cat = new Cat("Kurre");
    	Animal dog = new Dog("Vilma");
    	...
        
  • Alla djur har metoderna introduceYourself(), makeSound(), och fly(). Detta är metoderna som är påbörjade i klassen Animal. Skapa dessa metoder även i Cat, Dog, Bird och Butterfly. Metoderna överskuggar då superklassens metoder. Det är dessa metoder som kommer köras om metoden anropas på objekt av respektive typ. Om man har ett objekt av den generella supertypen Animal så körs superklassens metod.
  • Flytta över funktionaliteten från Main-klassens if-satser till motsvarande metoder i "rätt" klass.
  • Det blir nu inget kvar (förutom tomma if-satser) i metoderna i Main-klassen. Vi kan ta bort dem och formulerar om anropen i huvudprogrammet så att det istället ser ut så här:
    
    	cat.introduceYourself();
    	dog.introduceYourself();
    	...
        
  • När du är klar bör det inte bli en enda if-sats i hela programmet!
  • OBS, om du märker att en av klasserna inte behöver någon viss metod (för att det går lika bra att använda superklassens) så kan du ta bort just den metoden från subklassen.
  • Nyckelordet super har även ett annat användningssätt. Något man t.ex. skulle kunna göra är att i introduceYourself för Cat anropa super.introduceYourself() för att anropa original-introduceYourself som ligger i Animal (fast därmed inte sagt att man ska göra det!).
  • När allt fungerar, gå till variabeln som lagrar namnet (för du har väl bara en för samtliga djur, eller hur?) och testa att sätt synligheten till public, protected och private. Beroende på hur du har skrivit ert program kommer kanske delar nu sluta att fungera. Fundera på vad skillnaden mellan dessa tre är, baserat på de fel du får, och välj sedan den du tycker är mest lämplig och gå vidare.
  • Gå igenom koden och sätt dit synlighet public på alla klasser och metoder (inklusive konstruktorerna). Om vi utelämnar det så får vi "package-private" vilket nästan aldrig är önskvärt. Givetvis kan man ha private hjälp-metoder också, men det har vi inte använt oss av här. Metoder som är protected är bara tillgängliga inom klassen och från subklasserna.

Frågor:

  • Trots att namn-variabeln bara lagras på en plats, så kan alla djur använda den. Hur fungerar det?
  • Hur kommer det sig att rätt ljud (metoden makeSound) kommer ut, trots att fältet vi anropar makeSound på är för Animal, och inte Cat/Dog/Bird/Butterfly?
  • Vad menas med polymorfi?
  • Hur fungerar polymorfin i programmet nu?
  • Metoden introduceYourself i Animal verkar ju aldrig anropas? Varför inte?
  • Kommentera bort metoden introduceYourself i Dog. Vad händer nu när du kör programmet?
  • Var lagras namnet för instanserna av Cat och Dog? (I vilken/vilka klasser har du lagt instansvariabeln som refererar till namnet på djuret? I både Cat och Dog, eller bara i Animal?)
  • Gå tillbaka till frågorna i början av Steg 1 och titta på dem igen. Hur pass "bra" är koden nu m.a.p. hur "lätt"/"svårt" det är att ändra, felsöka, utöka. Hur mycket kod måste en utomstående programmerare sätta sig in i för att lägga till ett nytt djur?

Varje klass har nu fått ett tydligt ansvar istället för att allt ansvar ligger i Main. Klassen Animal (och dess subklasser) är nu riktiga objekt med både data och bettenden, istället för att Animal bara är en "rå" databehållare som metoder agerar på.

Steg 2 - Instansvariabler och klassvariabler

Uppgift:
  • Lägg till en publik instansvariabel i klassen Animal av typen int som heter age.
  • Modifiera metoderna introduceYourself i klasserna Animal, Cat, Dog, Bird och Butterfly så att de också skriver ut hur gammalt djuret är. Här är ett exempel på kod för klassen Cat:
    
    public void introduceYourself() {
        System.out.println("Mjau. Jag är en katt som heter " + this.name + ".");
        System.out.println("Jag är " + this.age + " år gammal.");
    }
    
    
  • Testkör sedan med följande kod:
    
    public class TutorialSteg2 {
    
        public static void main(String[] args) {
            Animal kurre = new Cat("Kurre");
            Animal vilma = new Dog("Vilma");
            
            kurre.age = 6;
            vilma.age = 3;
            
            kurre.introduceYourself();
            vilma.introduceYourself();
        }
    }
    
    
  • Frågor:
    • Vad blir utskriften?
    • Förklara hur det kommer sig att instansvariabeln age i Animal kan användas i Cat och Dog när den är deklarerad i Animal.
    • Vad består egentligen en instans av?
    • Och vad består en klass av?
    • Vad är skillnaden mellan en klass och en instans?
    • Ändra deklarationen av instansvariabeln age i Animal till en klassvariabel genom att använda static på det här sättet:
      public static int age;
    • Vad blir resultatet av utskriften nu? Varför då?
    • (Det kan hända att du får varningar från kompilatorn om att du borde accessa age som Animal.age, men det borde gå att köra programmet i alla fall. Ändra annars referenserna till age i koden till Animal.age.)
    • Var lagras värdet på en instansvariabel?
    • Var lagras värdet på en klassvariabel?
    • Vad refererar variabeln this till?
    • Kan man lösa utskriften av åldern genom att bara lägga till den på ett ställe?

    Steg 3 - Publika vs. privata variabler

    Här får du lära dig varför man ska skydda representationen (instansvariablerna) och du får också lära dig en del om konstruktorer, och prova att skriva en klassmetod (static-metod).

    Själva uppgiften för Steg 3 kommer i slutet av steget.

    Det är mycket dåligt att använda publika variabler på det sätt vi gjorde i Steg 2. Anledningen är att koden blir känslig för ändringar eftersom man exponerar klassens implementation.

    Antag att vi vill ändra representationen av hur gammalt ett djur är från age till birthyear. Så här skulle det kunna se ut i klassen Animal i så fall:

    
    public class Animal {
        public String name;
        public int birthyear;
        // variabeln age är borttagen
        ...
    }
    
    
    Plötsligt fungerar inte koden som använder variabeln age längre! Vi måste gå in och ändra i alla klasser där age används och skriva om koden så att den använder variabeln birthyear istället, och vi måste dessutom räkna ut åldern varje gång vi vill veta denna genom att ta nuvarande årtal minus birthyear'et.

    I t.ex klassen Cat hade vi fått ändra till följande kod:

    
    public void introduceYourself() {
        System.out.println("Mjau. Jag är en katt som heter " + this.name + ".");
        System.out.println("Jag är " + (2016 - this.birthyear) + " år gammal.");
    }
    
    

    Hade vi haft en metod som hette getAge istället hade vi däremot bara behövt ändra på ett enda ställe i koden - i klassen Animal.

    Så här skulle det kunnat ha sett ut i klassen Animal om vi hade gömt variabeln age från första början och kommit åt den via en metod istället (vi gör självklart även samma sak med instansvariabeln name):

    
    ...
    public class Animal {
        private String name;
        private int age;
        
        public Animal(String name, int age) {
            this.name = name;
            this.age = age;
        }
        
        public int getAge() {
            return age;
        }
    
        public String getName() {
            return name;
        }
    
        ...
    }
    
    
    Koden i Cat hade då t.ex kunnat se ut så här:
    
    public void introduceYourself() {
        System.out.println("Mjau. Jag är en katt som heter " + getName() + ".");
        System.out.println("Jag är " + getAge() + " år gammal.");
    }
    
    

    Nu när vi vill ändra representationen av age till birthyear, så behöver vi endast ändra i koden för Animal. Koden i övriga klasser fungerar utan ändringar eftersom vi kommer åt age via metoden getAge i Animal.

    Koden i klassen Animal kan då se ut så här:

    
    public class Animal {
        private static int currentYear = 2019;
        private String name;
        private int birthyear;
        
        public Animal(String name, int age) {
            this.name = name;
            this.birthyear = Animal.currentYear - age;
        }
        
        public int getAge() {
            return Animal.currentYear - this.birthyear;
        }
    
        ...
    }
    
    

    I ovanstående kod används en klassvariabel för att hålla reda på vilket år som är det nuvarande året. Årtalet är ju rimligtvis det samma för alla djur. (Såvida de inte börjar hålla på med tidsresor...)

    Uppgift:

    • Skriv om ditt program enligt de riktlinjer vi har gått igenom så att alla instansvariabler skyddas med private. Byt representationen av age så att födelsedatum lagras istället för age. Lägg alla instansvariabler i klassen Animal. Du kommer även behöva modifiera konstruktorerna så att allt funkar.
    • Koden för klasserna ska fungera med följande testprogram:
      
      public class TutorialSteg3 {
      
          public static void main(String[] args) {
              Animal.setYear(2019); // Så här anropar man en klassmetod,
                                    // dvs en statisk metod.
                                    // Hur ska koden för setYear se ut?
              
              Animal kurre = new Cat("Kurre", 6);
              Animal vilma = new Dog("Vilma", 3);
              
              kurre.introduceYourself();
              vilma.introduceYourself();
      
              Animal.setYear(2013); // Nu blir är ett nytt år för alla djur.
      
              kurre.introduceYourself();
              vilma.introduceYourself();
          }
      }
      
      
    • Det kan hända att kompilatorn klagar på koden i de tidigare testprogrammen (TutorialSteg1 och TutorialSteg2). Du kan i så fall ignorera det eller kommentera bort koden inuti dessa klasser så försvinner problemen.

    Frågor:

    • Vad är skillnaden mellan private och public?
    • Hur kommer man åt instansvariabler som är deklarerade som private från andra klasser?
    • Kan man komma åt private-variabler i subklasser?
    • Vi har inte använt protected ännu, men vad innebär det? Hade det hjälpt att använda det här?
    • Vad innebär det att använda private, public och protected på metoder?
    • Vad betyder this nu igen? När kan man utelämna this och när måste man ha med this?
    • Hur skriver man en klassmetod (en static-metod)?
    • Vad skiljer en klassmetod från en vanlig metod?
    • Kan man komma åt instansvariabler från en klassmetod?
    • Vilket språk (som i mänskligt talspråk, t.ex. norska, etc) skulle du välja att skriva din kod i? Varför?
    • Slutligen: Varför är det känsligt att accessa en instansvariabel, men mycket mindre känsligt att anropa en metod?

    Sammanfattningsvis: Att gömma implementationen (t.ex genom att göra instansvariablerna privata, men även metoder kan göras privata) är en av grundstenarna i Objektorienterad Programmering (OOP). Det är en del extra arbete att skriva metoder för att komma åt variablerna, men det är väl värt detta i lite större program, och i yrkesmässig programmering är det en självklarhet.

    Steg 4 - Relationer mellan objekt

    Det här är det sista steget i tutorialen och här får du lära dig mer om relationer mellan objekt.

    Objekt kan hänga ihop med varandra. Det kallas för relationer. Ett hus kan t.ex. innehålla ett antal olika djur. För att hålla reda på vilka djur som finns i huset, ska huset ha en lista över djuren. Ett djur i sin tur kanske kan ha en leksak. Ett djur kanske också kan ha en kompis, dvs. en relation till ett annat djurobjekt. En av de viktigaste distinktioner man måste kunna göra när man resonerar om klasser och objekts relationer till varandra är huruvida de är har-relationer eller är-relationer. Detta för att det skall bli tydligt för den som läser din kod (och för dig själv!) hur klasserna hänger ihop. Det första implementerar man med instans- eller klassvariabler, det senare implementeras ju med arv. När vi ritar klassdiagram är dessa två relationer representerade på följande sätt:

    Ett djur har en leksak. En struts är ett djur.

    Vi vill i detta steg även belysa hur klassobjekt/instanser fungerar. När vi hämtar listan av djur i huset (se nedan) är det samma lista som huset har, och djuren i listan är samma djur som vi lade till i huset. Om vi hämtar listan från huset och loopar över den, och lägger till en egen leksak till varje djur, bör utskriften reflektera detta när vi säger till huset att köra print().

    Uppgift

    Din uppgift är att skriva ett program som har följande struktur:

    • Du ska skapa två nya klasser, House och Toy
    • Det ska finnas en metod getAnimals() i House som skickar tillbaka listan på alla djur i huset.
    • Det ska finnas en metod print() i House som skriver ut information om alla djur, deras vänner och leksaker. För att åstadkomma detta behöver troligtvis alla djur också en motsvarande print()-metod.
    • Instanser av typen House kan innehålla godtyckligt många djur.
    • Klassen Toy har en sträng som är namnet på leksaken. Namnet sätts i konstruktorn, och kan kommas åt med den publika metoden getName().
    • Instanser av klassen Animal (eller instanser av dess subklasser Dog och Cat) kan ha en kompis, som är ett annat djur. Animal kan även ha godtyckligt många leksaker.

    Din kod för klasserna House, Animal, Dog, Cat, och Toy, ska fungera med följande testprogram:

      
    import java.util.ArrayList;
        
    public class TutorialSteg4 {
        
        public static void main(String[] args) {
            // Sätt årtalet för djuren.
            Animal.setYear(2016);
    
            // Skapa några djur.
            Animal kurre  = new Cat("Kurre", 6);
            Animal vilma  = new Dog("Vilma", 3);
            Animal bamse  = new Cat("Bamse", 12);
            Animal smilla = new Dog("Smilla", 1);
    
            // Skapa leksaker.
            Toy ball = new Toy ("Boll");
            Toy shoe  = new Toy ("Tuggsko");
            Toy mouse  = new Toy ("Plastmus");
            Toy gnome = new Toy("Tomte");
    
            // Lägg till leksakerna i en lista så vi kan loopa över dem senare.
            ArrayList<Toy> toys = new ArrayList<>();
            toys.add(ball);
            toys.add(shoe);
            toys.add(mouse);
            toys.add(gnome);
          
            // Skapa huset.
            House house = new House();
    
            // Skapa relationer mellan objekten.
            house.addAnimal(kurre);
            house.addAnimal(vilma);
            house.addAnimal(bamse);
            house.addAnimal(smilla);
    
            kurre.setFriend(vilma);
            vilma.setFriend(smilla);
            bamse.setFriend(kurre);
    
    	// Loopa över listorna (lika långa) och lägg till en leksak för varje djur i huset.
            for (int i = 0; i < house.getAnimals().size(); i++) {
    	     house.getAnimals().get(i).addToy(toys.get(i));
    	}
    
            // Skriv ut vad som finns i huset.
            house.print();
        }
    }
    
    

    Tips: Lite hjälp på vägen, så här kan metoden print i House se ut:

    
    public void print() {
        System.out.println("Följande djur finns i huset:");
        
        for (Animal animal : animalList) {
            animal.print();
        }
    }
    
    

    Ovanstående kod förutsätter att instansvariabeln animalList i House är av typen ArrayList<Animal>.

    Det är en klass i Javas klassbibliotek och för att kunna använda den måste du importera den med:

    
    import java.util.ArrayList; 
     
    

    Tips: En icke-primitiv variabel (dvs. en variabel som inte är något simpelt som int, float, boolean, etc..) kan vara tom, och den har då värdet null. Om vi försöker använda värdet i den här variabeln när den innehåller null händer det tråkiga saker, så vi måste ibland kontrollera dess värde. Det kanske kan se ut så här:

    
    public void print() {
        ...
    
       // Kolla att friend är skild från null innan
       // meddelandet introduceYourself skickas.
       if (friend != null) {
           System.out.println("Här är uppgifter om min kompis:");
           friend.introduceYourself();
        } else {
            System.out.println("Jag har ingen kompis.");
        }
    
        ...
    }
    
    
    Detta gäller även för din ArrayList. Innan du försöker stoppa in eller ta ut något ur den måste du därför först instansiera den genom att skriva:
    
    List<Animal> animals = new ArrayList<Animal>(); 
    
    

    Tips: Tänk noga efter vart print-metoden för själva djuren och övriga nya metoder för djuren borde ligga bland era klasser. Lägger man dem på rätt plats kan man spara en del arbete..

    Frågor:

    • Hur skapar man en ArrayList?
    • Hur lägger man in ett objekt i en ArrayList?
    • Hur itererar man över elementen i en ArrayList? Finns det flera sätt?
    • Kan du beskriva vilka meddelanden som skickas till vilka instanser och vilka metoder som anropas när print i House utförs?
    • Kan du berätta vad en kodstandard är bra för?
    • Ligger bara den kod som behövs i Cat och Dog, och den kod som blir likadan för både Cat och Dog i Animal? Vad är det som skiljer kod som måste ligga i Cat respektive Dog från den kod som kan delas via Animal?
    • Vad är en klass?
    • Kan du berätta vilka delar en klassbeskrivning i Java innehåller? Ge exempel med hjälp av en klass i ert program.
    • Kan du förklara skillnaden mellan en klass och en instans?
    • Kan du förklara vad instansvariabler är?
    • Kan du förklara varför instanser har egna värden på instansvariabler, men delar på koden för metoderna?
    • Kan du berätta vad synlighet innebär?
    • Kan du ge exempel på hur "information hiding" används och vad det är bra för?
    • Kan du förklara syntaxen för de olika kontrollstrukturer du har använt i ert program?
    • Kan du visa ett exempel på en loop i ert program som itererar över en lista med objekt och förklara vad koden gör?

    Sidansvarig: Magnus Nielsen
    Senast uppdaterad: 2024-01-18