Klasser og objekter
Relevant litteratur i offisiell tutorial:
- konseptuelt om objekter og klasser
- konkrete eksempler med klasser og objekter
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 klassenMoonPainting
.
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.
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:
- vi også har opprettet en type som heter
Person
, og - vi kan senere opprette ett eller flere objekter i klassen
Person
. Hvert av disse objektene vil inneholde to deler med data:- en streng som kalles
name
, og - en int som kalles
age
.
- en streng som kalles
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):
Merk at objektet slik det er lagret på heapen inneholder to deler med informasjon:
- et område for metadata (lysegult område), inkludert hvilken klasse objektet tilhører, og
- et område for instansvariablene (litt sterkere gult område), i dette tilfellet
- en plass for
name
(med verdiennull
), og - en plass for
age
(med verdien0
).
- en plass for
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:
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 på, 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 på et objekt: vi sier at metoden kjører i konteksten av objektet. Det aktuelle objektet kalles i denne konteksten forthis
.
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();
}
}
- Legg merke til at vi kaller metoden ved å skrive
p.greet()
. Her erp
objektet metoden kalles på. Dette er egentlig bare en fancy måte å sendep
som argument til metodengreet
. I metodengreet
vil den spesielle variablenthis
være den man bruker for å referere til dette objektet. Under kjøring av eksempelet over er altsåthis
i greet-metoden det samme objektet somp
i main-metoden.
Man kan velge å utelate
this.
og skrive kunname
i stedet forthis.name
i metodengreet
. 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 å skrivethis.
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 medthis.
– dette kan hjelpe til med forståelsen av objekter.
- Ordet
void
foran metodenavnet betyr at metoden som defineres ikke returnerer noen verdi. Generelt definerer man en metode ved å først gi returtypen, så metodenavnet, så parametrene i parenteser, og så kroppen til metoden i krøllparenteser.
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
- den kan ikke være
static
- returtypen må være klassen den blir skrevet i, og
- den har ikke navn
(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
}
}