Klasser og objekter

Relevant litteratur i offisiell tutorial:


Metaforer

Det finnes et mange metaforer som forsøker å fange opp forholdet mellom et objekt og dens klasse. For eksempel:

En klasse er som en skissetegning som brukes i en bilfabrikk. Et objekt er som en bil. En skissetegningen kan brukes til å lage mange forskjellige biler, og bilene som lages kan ha ulike egenskaper (farge på lakken, type girkasse, stoffet i setene og så videre).

Metaforen fanger opp at mange objekter kan høre til i samme klasse, og at objekter i samme klasse grovt sett har samme funksjon, men kan ha ulike egenskaper. Objekter kan også være helt like uten at de er det samme objektet, akkurat som to biler med helt like spesifikasjoner. En alternativ metafor er:

En klasse er som en sjablong (i.e. en «pepperkakeform» for tegning og maling). Et objekt er som et malt bilde. En sjablong kan brukes til å male mange forskjellige bilder, og bildene kan ha ulike egenskaper (farge, tegnestil, malingstype etc). På samme måte som et bestemt fysisk maleri av månen er en realisering av det abstrakte begrepet «maleri av månen», er et MoonPainting -objekt en instans av klassen MoonPainting.

Poenget er at objekter er individer, mens en klasse beskriver likheter i hvordan objektene oppfører seg såvel som rammer for hvordan objektene kan være forskjellige fra hverandre.

Vi sier at et objekt er en instans av en klasse.

Illustrasjon av sjablong-metaforen

Første eksempel

Et objekt er egentlig bare en samling av data; et slags luksuriøst oppslagsverk med ekstra struktur. Hvert objekt er i nøyaktig én klasse, og denne klassen bestemmer hva slags data objektene i klassen består av. For eksempel, se på klassen Person:

class Person {
  String name;
  int age;
}

I koden over definerte vi en klasse Person. Dette innebærer at:

Variablene name og age ovenfor kalles for feltvariabler eller instansvariabler.

Vi kan opprette objekter i klassen Person og gi dem navn og alder:

Person p1 = new Person();
p1.name = "Ola";
p1.age = 24;
Person p2 = new Person();
p2.name = "Kari";
p2.age = 42;

Vi kan se på og endre verdiene i objektene på samme måte som vi kan se på og endre variabler:

System.out.println(p1.name); // Ola
System.out.println(p2.name); // Kari
p1.name = "Ole";
System.out.println(p1.name); // Ole

class Person {
  String name;
  int age;
}

public class Foo {
  public static void main(String[] args) {
    Person p1 = new Person();
    p1.name = "Ola";
    p1.age = 24;
    Person p2 = new Person();
    p2.name = "Kari";
    p2.age = 42;
    System.out.println(p1.name); // Ola
    System.out.println(p2.name); // Kari
    p1.name = "Ole";
    System.out.println(p1.name); // Ole
  }
}

Objekter i minnet

Husk at vi deler opp minnet i to deler: stacken og heapen (les om objekter og primitive verdier i minnet i kursnotatene om primitive og refererte typer).

Når vi oppretter et nytt objekt (bruker kodeordet new), reserverer vi en ny plass på heapen hvor det er plass til dataene for objektet. Etter å ha utført kodelinjen

Person p = new Person();

vil minnet se omtrent slik ut (nøyaktige minneadresser vil variere fra kjøring til kjøring): Minne etter å ha opprettet et Person-objekt

Merk at objektet slik det er lagret på heapen inneholder to deler med informasjon:

Alle verdier i objektet som ikke er null eller en primitiv verdi er egentlig en referanse til et annet sted i minnet (ja, hver klasse har et sted i minnet hvor det finnes mer informasjon om klassen (ja, dette innebærer at en klasse også teknisk sett i seg selv er et objekt (nei, vær så snill og ikke tenk mer på det, det er forvirrende!))).

Etter å ha utført kodelinjene

p.name = "Ola";
p.age = 24;

vil minnet litt forenklet se slik ut:

Minne etter å ha gitt verdi til name og age

Instansmetoder og this

Metoder er funksjoner som er definert i en klasse. I Java er derfor alle funksjoner teknisk sett metoder, siden det kun er mulig å definere en funksjon inne i en klasse.

På samme måte som en funksjon tar inn argumenter og returnerer en verdi, tar en metode inn argumenter og returnerer en verdi. I tillegg til dette har en metode tilgang til alle feltvariablene i objektet metoden kalles , og kan bruke disse variablene som argumenter eller mutere dem som en del av metodens sideeffekter.

En instansmetode er en metode som ikke er definert med kodeordet static foran seg. Slike metoder kalles et objekt: vi sier at metoden kjører i konteksten av objektet. Det aktuelle objektet kalles i denne konteksten for this.

For eksempel, la oss definere en instansmetode som heter greet i klassen Person:

class Person {
  String name;
  int age;

  void greet() {
    System.out.println("Hello, my name is " + this.name);
  }
}

public class Main {
  public static void main(String[] args) {
    Person p = new Person();
    p.name = "Ola";
    p.age = 24;
    p.greet();
  }
}

Man kan velge å utelate this. og skrive kun name i stedet for this.name i metoden greet. Dette er en snarvei som gjør at man slipper å skrive «this» overalt. Noen vil hevde at koden er tydeligere dersom man kvalifiserer variabelnavn ved å skrive this. foran instansvariabler, og dette forhindrer i det minste forvirring dersom man bruker lokale variabelnavn med samme navn som instansvariablene. Firma kan ha ulike stilregler for i hvilken utstrekning man kvalifiserer variabel- og metodenavn. I INF101 er det opp til deg selv å bestemme hva du foretrekker. Velg én policy i hvert prosjekt og hold deg til den. Mitt råd til nye studenter er å starte med å alltid kvalifisere instansvariabler og kall til instansmetoder med this. – dette kan hjelpe til med forståelsen av objekter.

En metode kan også ha parametre. For eksempel er det vanlig å ha metoder som setter verdien til en instansvariabel. I disse metodene kan man for eksempel sjekke om verdien er gyldig:

class Person {
  String name;
  int age;

  void setName(String name) {
    if (name == null) {
      // Krasj programmet dersom navnet er null
      throw new IllegalArgumentException("Name cannot be null");
    }
    this.name = name;
  }

  void setAge(int age) {
    if (age < 0) {
      // Krasj programmet dersom alderen er negativ
      throw new IllegalArgumentException("Age cannot be negative");
    }
    this.age = age;
  }
}

public class Main {
  public static void main(String[] args) {
    Person p = new Person();
    p.setName("Ole");
    p.setAge(24);
    System.out.println(p.name); // Ole
  }
}

Legg også merke til at variablene this.name og name i dette eksempelet er to forskjellige variabler. Den første er en instansvariabel, mens den andre er en parameter/lokal variabel som er definert i metoden setName.

Konstruktør

En konstruktør er en spesiell metode som kalles én gang i hvert objekt sin levetid: når new blir kalt og objektet blir opprettet. Den brukes vanligvis for å sette initielle verdier til instansvariablene.

En konstruktør angis på samme måte som en vanlig metode, men

(alternativt kan du tenke deg at konstruktør-metoden har samme navn som klassen den blir skrevet i men har ingen angitt returverdi)

class Person {
  String name;
  int age;

  Person() {
    this.name = "Eva";
    this.age = 42;
  }
}

public class Main {
  public static void main(String[] args) {
    Person p;
    p = new Person(); // Konstruktør-metoden kjøres her
    System.out.println(p.name); // Eva
  }
}

En konstruktør kan også ha parametre. Då må det gis argumenter når det opprettes nye objekter

class Person {
  String name;
  int age;

  Person(String name, int age) {
    this.name = name; // this.name og name er to forskjellige variabler
    this.age = age; // this.age og age er to fo forskjellige variabler
  }
}

public class Main {
  public static void main(String[] args) {
    Person a = new Person("Adam", 24); // Konstruktør-metoden kjøres her
    Person b = new Person("Eva", 42); // Konstruktør-metoden kjøres her også
    System.out.println(a.name); // Adam
    System.out.println(b.name); // Eva
  }
}

Det er mulig å ha mer enn én konstruktør, men da må de ha ulike signaturer (parametrene må være forskjellige). Avhengig av hvilke argumenter som blir gitt når new blir kalt, vil den korresponderende konstruktøren bli kalt.

class Person {
  String name;
  int age;

  Person(String name, int age) {
    this.name = name;
    this.age = age;
  }

  Person(String name) {
    this.name = name;
    this.age = -1;
  }
}

public class Main {
  public static void main(String[] args) {
    Person a = new Person("Adam", 24); // konstruktøren med to parametre
    Person b = new Person("Eva");   // konstruktøren med én parameter
    System.out.println(a.age); // 24
    System.out.println(b.age); // -1
  }
}

Det er også mulig å kalle én konstruktør fra en annen. Da brukes kodeordet this som om det var et metodenavn.

class Person {
  String name;
  int age;

  Person(String name, int age) {
    this.name = name;
    this.age = age;
  }

  Person(String name) {
    this(name, -1) // kaller på den andre konstruktøren her
  }
}

public class Main {
  public static void main(String[] args) {
    Person a = new Person("Adam", 24); // konstruktøren med to parametre
    Person b = new Person("Eva");   // konstruktøren med én parameter
    System.out.println(a.age); // 24
    System.out.println(b.age); // -1
  }
}

Det er mulig å ha kode som kjøres ved opprettelse av et objekt før konstruktøren kjøres. Dette gjøres på to måter, som utføres i følgende rekkefølge:

  • direkte initialisering av instansvariabler, og deretter
  • kode i initialiseringsblokken
class Person {
  // Steg 0 ved opprettelse: instansvariabler får standardverdier (null og 0)
  // Steg 1 ved opprettelse: direkte initialisering av instansvariabler
  String name = getEvaAsString();
  int age = 10;

  {
    // Steg 2 ved opprettelse: initialiseringsblokk
    // Dette er kode mellom to frittstående krøllparanteser
    System.out.println("Initialiseringsblokken blir kjørt nå");
    age *= 2;
  }

  Person(String name, int age) {
    // Steg 3 ved opprettelse: konstruktør
    System.out.println("Konstruktøren blir kjørt nå");
    this.name = name;
    this.age += age;
  }

  String getEvaAsString() {
    System.out.println("getEvaAsString blir kalt");
    return "Eva";
  }
}

public class Main {
  public static void main(String[] args) {
    Person p = new Person("Adam", 3);
    System.out.println(p.age); // 23
  }
}