Objektorienterad programmering

I nästan alla program utom de enklaste finns ett behov att föra samman värden av primitiva typer som hör ihop. Detta för att kunna representera mer komplex data. Ett enkelt exempel är tredimensionella vektorer med flyttalskomponenter. De är naturligast att representera som tre komponenter, en för x-, en för y- och en för z-koordinaten.

I C, som inte är objektorienterat, finns möjlighet att definera strukturtyper:

struct Vector {
  double x, y, z;
}

När man introducerade objektorientering, i t.ex. C++, ville man förbättra vissa saker med detta sätt att representera sammansatta värden.

Istället för att kunna dyka upp var som helst i koden kan (och bör) man definiera metoder som gör saker med den sammansatta datan på samma ställe där datatypen deklareras. Strukturer kallas klasser istället och kan innehålla metoder. Dessa metoder anropas alltid för en instans av klassen och har tillgång till dess komponenter.

I Java skriver vi motsvarigheten till strukturen så här:

public class Vector {
  public double x, y, z;
}

Konventionen är att klasser, som är typer, har namn som inleds med stor bokstav (liksom typer i Haskell).

En metod för att t.ex. räkna ut absolutbeloppet av en vektor är naturligt att lägga i klassen.

public class Vector {
  public double x, y, z;
  
  public double abs() {
    return Math.sqrt(x * x + y * y + z * z);
  }
}

Detta är en s.k. objektmetod. Den tar inte en Vector som argument. Varje anrop av en objektmetod har en associerad instans av klassen, ett objekt, som skickas implicit till metoden. När man i definitionen av metoden hänvisar till x, y och z så är det komponenterna i objektet man kommer åt.

Om man har ett objekt, i detta fall t.ex. v av typen Vector så anropar man objektmetoden så här:

double l = v.abs();

På detta sätt samlar man kod som hör ihop på ett naturligt sätt och koden som använder anropar metoder associerade med objekt utgår från objekten, snarare än metoderna. Den är objektorienterad.

Klasser har också speciella metoder som är till för att skapa instanser, ny objekt. Dessa kallas konstruktorer och anropas när man skapar ett objekt med new-operatorn.

public class Vector {
  public double x, y, z;
  
  public Vector() {
    x = y = z = 0;
  }
  
  ...
 }

En konstructor definieras genom att använda samma namn på metoden som klassens namn och inte ange någon returtyp. Konstruktorn behöver inte allokera minne för det nya objektet. Det är redan fixat av system och komponenterna, som kallas instansvariabler, är tillgängliga. Det man behöver göra i konstruktorn är att initiera instansvariablernas värden. Konstruktorn ovan initierar en vektor till 0-vektorn.

Konstruktorer kan också ha parametrar, precis som andra metoder, och man kan definiera flera konstruktorer så länge de inte har samma antal eller typ av parametrar.

public class Vector {
  public double x, y, z;
  
  public Vector() {
    x = y = z = 0;
  }
  
  public Vector(double x, double y, double z) {
     this.x = x;
     this.y = y;
     this.z = z;
  }
  
  ...
 }

Den andra konstruktorn initierar en vektor med komponenter enligt de tre parametrarna. Parametrarna har samma namn som instansvariablerna och har ett mer lokalt scope. Alltså kopplas förekomster av dessa variabelnamn till metodparametrarna, inte instansvariablerna. Detta kan man förstår undvika genom att välja andra namn, men man kan också specificera att man menar instansvariablerna genom att skriva this.. this representerar aktuellt objekt och kan användas också i vanliga instansmetoder.

En annan, viktigare mekanism med klasser är att man kan dölja innehållet, d.v.s. hur representationen är implementerad, för användaren av klassen. När man deklarerar klasser, metoder och variabler kan man använda attribut. Det har vi gjort många gånger i exemplen men inte sagt vad det innebär. Några av dessa attribut påverkar i vilka delar av koden som det man deklarerar är synligt, åtkomligt.

I klassen ovan står det public framför alla deklarationer. Det betyder att man kommer åt dem överallt i koden. Detta är vanligt och naturligt för klasser, kontruktorer och metoder, men däremot brukar instantsvariabler vara deklarerade som private. Det innebär att bara metoder i klassen har tillgång till dem. Detta för att objektens interna representation inte ska exponeras för omgivningen, den kod som använder klassen.

Varför vill man inte detta? Dels så vill man hålla all kod som hör ihop med hur man valt att representera objekten på samma plats, i klassen, så att övrig kod inte kan förstöra tillståndet i ett objekt. I stora projekt är det ofta olika personer som implementerar klassen och som använder dem och man vill att beroendena mellan klass-koden och den användande koden ska bli så små som möjligt. Därför döljer man representationen av objekten och definierar ett antal metoder som använder anropar istället för att manipulera direkt med instansvariablerna. Metoderna utgör ett gränssnitt som skiljer de olika delarna av koden åt. Den som använder klassen kan inte ställa till det i den interna representationen genom sin okunskap. Den som implementerar klassen kan utan problem ändra den interna representationen utan att resten av koden behöver ändras, så länge gränssnittet förblir detsamma.

I exemplet ovan är objektet inte särskilt komplext och valet av representation är naturligt, så att låta komponenterna vara public är inte så dumt. Man i allmänhet när det gäller mer komplexa objekt är valet av representation inte självklart och risken att göra fel stor. Då är möjligheten att dölja representationen för omvärlden väsentlig.

Om vi gör instansvariablerna ovan private så försvinner möjligheten för en användare av klassen att avläsa och förändra värdena. Detta gör att man behöver så kallade getters och setters, metoder som bara är till för att göra detta.

public class Vector {
  private double x, y, z;
  ...
  
  public double getXComp() {
    return x;
  }
  public double getYComp() {
    return y;
  }
  public double getZComp() {
    return z;
  }

  public void setXComp(double x) {
    this.x = x;
  }
  public void setYComp(double y) {
    this.y = y;
  }
  public void setZComp(double z) {
    this.z = z;
  }
  
  ...
 }

I klasser finns det ofta anledning att definiera hjälp-metoder som bara är tänkta att användas av andra metoder i klassen. Dessa är lämpliga att deklarera som private.

I metoder där två eller flera objekt är inblandade brukar ha det ena som aktuellt objekt och de andra som explicita argument:

public class Vector {
  ...
  public Vector add(Vector v) {
    return new Vector(x + v.x, y + v.y, z + v.z);
  }
}

Klassmetoder och klassvariabler

Ett annat attribut är static. Metoder som deklareras som statiska kallas klassmetoder och de har inget aktuellt objekt som implicit argument. I alla exempel vi gjort tidigare har metoder varit statiska av denna anledning. Main-metoden måste deklarerar som static och public.

Variabler som deklareras statiska kallas klassvariabler och är globala variabler. När ett program körs finns bara en instans av klassvariabler, inte en per objekt som gäller för instansvariablerna. Statiska metoder kan bara komma åt statiska variabler.

För att anropa statiska metoder eller komma åt statiska variabler utanför klassen inleder man man klassens namn följt av punkt. Vi har t.ex. använt metoden Integer.parseInt. Detta är en statisk metod som tillhör klassen Integer.

Objekt som variabler

Instans- och klassvariabler behöver inte vara primitiva typer. De kan vara objekt och arrayer.

Exempel

Klass som på ett förenklat sätt motsvarar en person i statens personregister.

class Person {
  private static Datum dagensDatum;
  private static int räknare;
  
  public static nästaDag() {
    dagensDatum.ökaDag();
    räknare = 0;
  }

  private Datum födelseDatum;
  private String födelseOrt;
  private boolean kvinna;  // true = kvinna, false = man
  private String personNummer;
  private String förNamn, efterNamn;
  
  public Person(String ort, boolean kv) {
    // konstruktor som används för nyfödda
    födelseDatum = dagensDatum;
    födelseOrt = ort;
    kvinna = kv;
    int serienummer = räknare++;
    personNummer = födelseDatum.toString() + "-" + serieNummer;
      // Detta är förenklat, fyra sista siffrorna har egentligen
      // kontrollsiffra och tredje siffran är udda eller jämn
      // beroende på kön.
  }
  
  public void ändraFörNamn(String namn) {
    förNamn = namn;
  }
  ...
}

Klassen håller globalt reda på dagens datum och hur många som registrerats hittills idag.

Koden som använder denna klass skulle anropa Person.nästaDag() varje gång det blir ett nytt dygn.

Konstanter

Man kan ange att variabler ska vara konstanter, d.v.s. ej gå att ändra. Det gör man genom attributet final. Konstanter kan förstås initieras i deklarationen, men även initieras och ändras i klassens konstruktorer. Det är först när konstruktorn är klar som värdet fixeras och inte kan ändras mer.

class Employee {
  private final int id;
  private String name;
  private int salary;
  
  public Employee(int id) {
    this.id = id;
  }
  
  public void setId(int newId) {
    id = newId;  // Detta går inte.
  }
}

För statiska variabler gäller samma sak. De kan initieras direkt eller i en s.k. static initializer:

class A {
  public static final double ratio = 16.0/9.0;
}

eller

class A {
  public static final double ratio;
  
  static {  // static initializer
    ratio = 16.0/9.0;
  }
}

Viktigt att notera är för objekt som är final så är det bara objektet i helhet som inte kan förändras. Komponenterna kan förändras, om inte dessa också är deklarerade static.

Vector v = new Vector(1,2,0);

v.x = 5;  // Detta är möjligt.

v = new Vector();  // Detta går inte.

null-objekt och likhet på objekt

Förvalt värde för objekt och arrayer som inte initieras explicit är ett specialvärde som heter null. Detta innebär att variabeln inte refererar till någonting. Möjligheten att sätta ett objekt till null används ganska mycket för att situationer där man kan använda Maybe A i Haskell, d.v.s. man kanske har ett A. Man kan säga att alla objekt av klassen A i java egentligen är Maybe A.

Man kan test om en variabel refererar till ett objekt eller inte.

Vector v;

if (v == null) {
  // hantera fallet att v inte är en instans
}

Man kan också jämföra objektvariabler med varandra. Detta avgör om de refererar till exakt samma instans eller ej, ej om deras komponenter är lika.

Vector v1 = new Vector(1,0,0);
Vector v2 = new Vector(1,0,0);
Vector v3 = v1;
System.out.println(v1 == v2);  // -> false
System.out.println(v1 == v3);  // -> true

Omvandling mellan typer

I java sker implicit omvandling från typer till andra.

int x = 25;
System.out.println("x = " + x);  // -> x = 25

Här omvandlas heltalet till sträng innnan det slås ihop med första strängen.

int x = 25;
System.out.println(x + 1.25);  // -> 26.25

Här omvandlas heltalet 25 till flyttal innan det adderas till det andra talet. Implicit omvandling sker till typer som är större, d.v.s. kan representera en mängd av värden som är en supermängd till ursprungstypen, ex. int -> long, int -> String, int -> double.

Man kan också explicit omvandla till vissa andra typer genom att skriva önskad typ inom parantes före uttrycket:

int x = 25;
System.out.println(x + (int)1.25);  // -> 26

Se upp med hur uttryck i strängkonkateneringar grupperas:

int x = 25, y = 7;
System.out.println("x + y = " + x + y);  // -> x + y = 257
System.out.println("x + y = " + (x + y));  // -> x + y = 32