Primitive og refererte typer


På samme måte som i Python, har Java en rekke ulike datatyper. Et forvirrende moment ved Java sine typer, er at det finnes to fundamentalt forskjellige familier med typer: primitive typer og refererte typer.

Primitive typer

Det finnes kun noen få primitive typer. De er:

Alle refererte typer er til syvende og sist bygget opp av disse primitive typene. Les mer om primitive typer i offisiell tutorial fra Oracle.

Refererte typer (objekter)

Alle typer som ikke er primitive, kalles refererte typer. Verdier av en referert type kalles for objekter. Noen eksempler på innebygde typer som er refererte:

Det finnes refererte varianter for alle de primitive typene; disse typene er en «wrapper» som gjør at vi kan bruke primitive typer alle steder hvor vi egentlig legger opp til at typen skal være referert:

I tillegg er alle typer som vi selv definerer, refererte typer. Hver gang man definerer en klasse, et grensesnitt (interface), en abstrakt klasse, en enum eller en record definerer man nemlig også en ny type (som alltid er en referert type). Denne typen vil ha navn som er lik navnet på henholdsvis klassen, grensesnittet, abstrakte klassen, enumet eller record’en.

Objekter og primitive verdier i minnet

Vi deler opp datamaskinen sitt minne (RAM) i to deler: stack og heap. Veldig (veldig!!!) forenklet kan vi tenke på stacken som et register av variabler som er definert for øyeblikket, mens heapen er hvor vi lagrer data.

Alle variabler som er i bruk har fått tildelt en posisjon på stacken. Hvis variabelen er av en primitiv type, lagres verdien direkte på denne posisjonen, men hvis variabelen er av en referert type, lagres en referanse til heapen der selve objektet (altså verdien) faktisk ligger.

For eksempel kan vi sammenligne en variabel av den primitive typen int med en variabel av den refererte typen Integer:

int x = 12345;
Integer y = 12345;

Illustrasjon av minnets tilstand etter utførelse av de to linjene over

Her vises en forenklet illustrasjon av minnets tilstand etter utførelse av kodesnutten over. Legg merke til at hver variabel har én posisjon på stacken (øverst i minnet). Den primitive variabelen x har verdien 12345 liggende direkte på sin posisjon i stacken, mens den refererte variabelen y inneholder en referanse til heapen der objektet med verdien 12345 faktisk ligger.

Se også illustrasjonen av minnet du får ved å trykke «Se steg» i kodeeksempelet over. For i det hele tatt å se forskjell på x og y i visualiseringsverkøyet må du ha instillingene slik at «render all objects on the heap» er aktivert i pythontutor.org når du klikker på «Edit this code».

Data i heapen er organisert i objekter. Et objekt er i bunn og grunn et sammenhengende stykke minne et eller annet sted i heapen med en gitt struktur; størrelsen på dette stykket med minne og strukturen det har er bestemt av hvilken klasse objektet er i.

Null og standardverdier

En variabel av en referert type kan ha den spesielle verdien null. Dette innebærer at variabelen ikke peker på noe objekt. Egentlig innebærer dette at bare at verdien på stacken for den gitte variabelen er 0; men det kan ikke finnes noe objekt på denne spesielt reserverte minneadressen, så vi sier at variabelen har verdien null. Dette er standardverdien for refererte typer dersom en variabel ikke blir initialisert. null er altså ikke et objekt i seg selv, men representerer fraværet av et objekt.

Variabler med primitive typer kan ikke ha verdien null, men har i stedet en standardverdi som er definert av typen. For eksempel er standardverdien for int 0, standardverdien for boolean er false, mens standardverdien for double er 0.0 (felles for alle standardverdier for variabler er at bit-representasjonen av verdien på stacken er 0).

Likhet og sammenligning

Sammenligning av primitive verdier

For å sjekke om to primitive variabler har samme verdi, bruker vi ==. For eksempel:

int x = 12344;
int y = 12345;
int z = x + 1;
System.out.println(x == y); // false
System.out.println(y == z); // true

Sammenligning av objekter

Bruk av == virker ikke som man først forventer for refererte typer/objekter. For eksempel:

Integer x = 12344;
Integer y = 12345;
Integer z = x + 1;
System.out.println(x == y); // false
System.out.println(y == z); // false (?!)

Grunnen til at dette ikke fungerer, er fordi == kun sammenligner det som er på stacken. Det vil si at y == z sammenligner om y og z er like minneadresser som peker på samme objekt i heapen. Dette er ikke tilfelle fordi det opprettes et nytt objekt når z regnes ut; så vi får false som svar. Under ser du en illustrasjon av minnet etter utførelse av kodesnutten over. Legg merke til at y og z peker til ulike minneadresser, selv om objektene har samme verdi.

Illustrasjon av minnets tilstand etter utførelse av de tre første linjene over, og hva sammenligningen av y == z innebærer

For å sammenligne om to refererte variabler peker på objekter med samme verdi, bruker vi .equals -metoden i stedet. For eksempel:

Integer x = 12344;
Integer y = 12345;
Integer z = x + 1;
System.out.println(x.equals(y)); // false
System.out.println(y.equals(z)); // true

Metoden equals er definert for alle objekter i Java. Den er derimot ikke definert for null og primitive verdier.

Likhet og sammenligning med null

La oss si at vi ønsker å sammenligne om to variabler med referert type er like, men den ene verdien er null. Da vil vi få en krasj hvis vi bruker .equals, fordi metoden ikke er definert for null. For eksempel:

String a = null;
String b = "Foo";
System.out.println(a.equals(b)); // Krasjer med NullPointerException

For å unngå dette, kan vi først sjekke om objektet vi kaller .equals på er null:

String a = null;
String b = "Foo";
// Sjekk om a og b er like
boolean aEqualsB;
if (a == null) {
  aEqualsB = (b == null);
} else {
  aEqualsB = a.equals(b);
}
System.out.println(aEqualsB); // false

Koden over krever litt kognitiv kapasitet å lese. Det er en bedre praksis å bruke Objects.equals(a, b) i stedet, som gjør akkurat det samme. Merk at vi må importere java.util.Objects for å kunne bruke denne metoden:

import java.util.Objects;

public class Foo {
  public static void main(String[] args) {
    String a = null;
    String b = "Foo";
    boolean aEqualsB = Objects.equals(a, b);
    System.out.println(aEqualsB); // false
  }
}

Oppsummering av likhet og sammenligning

En tricky bug

Av effektivitets-hensyn har Java implementert en intern cache for alle Integer -verdier mellom -128 og 127. For alle verdier i dette intervallet vil det alltid være samme objekt i heapen som representerer verdien. Dermed vil == faktisk oppføre seg som .equals for verdier i dette intervallet, til tross for hva vi nettopp har lært i avsnittet over. For eksempel:

Integer x = 12344;
Integer y = 12345;
Integer z = x + 1;
System.out.println(y == z); // false -- som forklart tidligere

Integer a = 44;
Integer b = 45;
Integer c = a + 1;
System.out.println(b == c); // true (?!)

Denne overraskende oppørselen i Java fører til bugs som er vanskelig å feilsøke. For eksempel: en utvikler bruker == for å sammenligne Integer -verdier, men skulle egentlig brukt .equals for å få riktig funksjonalitet; men denne bug’en er umulig å finne så lenge hen kun tester koden med verdier i intervallet -128 til 127.

Moralen i historien:

Egne typer

En viktig underkategori av refererte typer er de vi definerer selv. Hver gang man definerer

definerer man nemlig også en ny type (som alltid er en referert type). Denne typen vil ha navn som er lik navnet på henholdsvis klassen, grensesnittet, abstrakte klassen, enumet eller record’en.

Kode som definerer klassen Person definerer automatisk en ny type som også heter Person:

class Person {
  // ...
}

Man kan deretter opprette variabler av typen Person:

Person p;

Og man kan opprette objekter av typen Person:

p = new Person();

(eller man kan opprette variabler og objekter på én gang:)

Person p = new Person();

Komplett eksempel:
class Person {
  String name;
  int age;
}

public class Main {
  public static void main(String[] args) {
    Person p = new Person();
    p.name = "Ola Nordmann";
    p.age = 42;
    System.out.println(p.name + " er " + p.age + " år gammel.");
  }
}

Kode som definerer grensesnittet Player definerer automatisk en ny type som også heter Player:

interface Player {
  // ...
}

Man kan deretter opprette variabler av typen Player, for eksempel en parameter-variabel i en metode:

static void askForName(Player p) {
  // ...
}

For å opprette et objekt som har typen Player, må vi opprette et objekt i en klasse som implementerer grensesnittet Player. For eksempel klassen AIPlayer:

class AIPlayer implements Player {
  // ...
}

Vi kan nå opprette et objekt av typen AIPlayer og lagre det i en variabel av typen Player:

Player p = new AIPlayer();

Eventuelt kan vi kalle på en metode som tar inn et objekt av typen Player:

AIPlayer ai = new AIPlayer();
askForName(ai);

Komplett eksempel. For å kjøre eksempelet under, kopier koden inn i en fil Main.java og kjør med kommandoene javac Main.java og så java Main:

import java.util.Scanner;

interface Player {
  String getName();
}

class AIPlayer implements Player {
  @Override
  public String getName() {
    return "AI";
  }
}

class HumanPlayer implements Player {
  Scanner scanner = new Scanner(System.in);

  @Override
  public String getName() {
    return scanner.nextLine();
  }
}

public class Main {
  public static void main(String[] args) {
    Player ai = new AIPlayer();
    HumanPlayer human = new HumanPlayer();
    askForName(ai);
    askForName(human); // HumanPlayer er en undertype av Player
  }
  
  static void askForName(Player p) {
    System.out.println("Hva heter du?");
    String name = p.getName();
    System.out.println("Hei, " + name + "!");
  }
}