Primitive og refererte typer
- Primitive typer
- Refererte typer (objekter)
- Objekter og primitive verdier i minnet
- Null og standardverdier
- Likhet og sammenligning med
- Oppsummering av likhet og sammenligning
- En tricky bug
- Egne 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:
- Boolske verdier
boolean
- en logisk verdi som kan være ententrue
ellerfalse
- Heltall
byte
- et heltall mellom -128 og 127short
- et heltall mellom -32 768 og 32 767int
- et heltall mellom -2 147 483 648 og 2 147 483 647long
- et heltall mellom -9 223 372 036 854 775 808 og 9 223 372 036 854 775 807
- Flyttall
float
- et flyttall med 32 biters presisjondouble
- et flyttall med 64 biters presisjon
- Skrifttegn
char
- et enkelt tegn, for eksempel en bokstav
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:
String
- en streng med tekst, for eksempel"Hello World"
LocalDateTime
- et tidspunktArrayList
- en liste med elementerHashMap
- et oppslagsverk med nøkkel-verdi parHashSet
- en mengde med elementer
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:
Boolean
Byte
Short
Integer
Long
Float
Double
Character
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;
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.
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
a == b
sammenligner om to verdier er like slik de er (eller ville vært) lagret på stacken.- Brukes for å sammenligne primitive typer.
- Brukes for å sammenligne med
null
. - Brukes for å sjekke om to variabler refererer til det samme objektet.
a.equals(b)
sammenligner om to objekter er like.- Forutsetter at
a
ikke ernull
for å unngå krasj med NullPointerException.
- Forutsetter at
Objects.equals(a, b)
er beste praksis for å sjekke om to objektera
ogb
er like når man også tar hensyn til ata
kan ha verdiennull
.
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:
- Bruk
a.equals(b)
eller aller helstObjects.equals(a, b)
for å sammenligne om to verdier med refererte typer er like. - Bruk alltid noen eksempler med tall større enn 127 for å teste og feilsøke kode med Integer -verdier.
Egne typer
En viktig underkategori av refererte typer er de vi definerer selv. 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.
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();
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 + "!");
}
}