Liigu peamise sisu juurde

Sissejuhatus andmetüüpidesse

Sissejuhatus

Andmetüüp määrab ära, milliseid väärtuseid see võib sisaldada ning milliseid toiminguid sellega läbi võib viia.

Javas jagunevad andmetüübid kaheks põhikategooriaks: primitiivsed tüübid (primitive types) ja viitetüübid (reference types). Nende mõistmine on oluline, sest need käituvad erinevalt nii mälu haldamise, väärtuste omistamise kui ka võrdlemise osas.

Primitiivsed tüübid

Primitiivsed tüübid hoiavad väärtust otse mälus. Täpsemalt primitiivide puhul salvestatakse väärtus otse muutuja mälukohta, mitte viitena objektile. Need tüübid on keele poolt eelnevalt defineeritud ning nende nimetused on kirjutatud väikeste tähtedega.

Javas on kokku 8 primitiivset tüüpi:

TüüpKirjeldusVaikimisi väärtusSuurus
byteVäike täisarv08 biti
shortLühike täisarv016 biti
intTäisarv032 biti
longPikk täisarv0L64 biti
floatUjukomaarv0.0f32 biti
doubleTopelt-ujukomaarv0.0d64 biti
charUnicode tähemärk'\u0000'16 biti
booleanTõeväärtusfalse-

Näiteks:

int age = 25;
double price = 19.99;
boolean isActive = true;
char grade = 'A';

Primitiivsetel tüüpidel on kindlad omadused:

  • Need hoiavad väärtust otse mälus, mitte läbi viite objektile
  • Neid salvestatakse stack mällu, mille tõttu on juurdepääs neile kiirem
  • Ei saa olla null väärtusega
  • Omavad fikseeritud suurust mälus
    • Näiteks kui määrata numbrile "1" int tüüp, siis mälus reserveeritakse sellele alati 32 bitti.
    • Rakendustes, kus mälukasutus on kriitiline, võib see tähtsaks osutuda, kuna primitiivide valik mõjutab otse programmi mälutarvet.
    • Praktikas ei ole see tavaliselt suur probleem.
  • Primitiivide võrdlemisel kasutatakse == operaatorit.

Näide võrdlemise kohta:

int x = 10;
int y = 10;
System.out.println(x == y); // true - võrreldakse väärtusi

Viitetüübid

Viitetüübid on kõik muud tüübid peale primitiivsete tüüpide.
Levinumateks viitetüüpideks on:

  • String ehk sõned
  • Integer, Double, Boolean jt. - primitiivsete tüüpide mähised (ingl k. wrapper classes)
  • Massiivid (arrays) - int[], String[] jne.
  • Kasutaja poolt loodud klassid

Näiteks:

String name = "Ago";
Integer count = 100;
int[] numbers = new int[]{1, 2, 3};

Erinevalt primitiividele, mis hoiavad väärtust otse mälus, viitetüübid hoiavad endas viidet objekti mäluasukohale. Sellega seoses on ka kahe väärtuse võrdlemine erinev.
Kui primitiivide puhul == operaatorit kasutades saime võrrelda kahte väärtust, siis viitetüüpide puhul antud operaator võrdleb mäluasukohti. Näiteks:

String a = new String("Java");
String b = new String("Java");

System.out.println(a == b); // false — different objects

// Technical demonstration on why they are different - same contents but different identifier:
System.out.println(Integer.toHexString(System.identityHashCode(a))); // e.g., 27716f4
System.out.println(Integer.toHexString(System.identityHashCode(b))); // e.g., 8efb846

Kahe objekti sisuliseks võrdlemiseks kasutame .equals() meetodit:

String a = new String("Java");
String b = new String("Java");

System.out.println(a.equals(b)); // true - same contents

Objektid salvestatakse kuhja ehk heap mällu ning objektide suurus võib olla dünaamiline.
Lisaks viitetüübi väärtus võib olla null ehk tühiväärtus ning see on ka vaikeväärtus:

Integer a = null;   // Correct
int b = null; // Error

null-väärtus ehk tühiväärtus

null tähistab tühiväärtust ehk väärtust, mida pole olemas. null-väärtust saab omistada ainult viitetüüpidele ehk antud muutuja ei osuta ühelegi objektile.
Näiteks:

String name = null;         // OK
System.out.println(name); // null

int age = null; // Error

Kui proovida omistada null-väärtust primitiivile, siis tekib järgnev kompileerimisviga:

java: incompatible types: <nulltype> cannot be converted to int

Samuti tuleb null-viitega töötamisel olla ettevaatlik, kuna selle peal ei saa meetodeid välja kutsuda. Tulemusena tekib NullPointerException erind:

// Will end in an error
String text = null;
System.out.println(text.length()); // Error

// Correct way - if we know value might be null, do a check before calling methods
if (text != null) {
System.out.println(text.length());
}

Mäluhaldus

Primitiivide ja viitetüüpide puhul on oluline mõista, kuidas neid mälus hoitakse. Eelnevalt sai mainitud heap ja stack mälu.

Primitiivsed tüübid stack mälus

Primitiivsed muutujad salvestatakse otse stack mällu. See tähendab, et väärtus salvestatakse otse mäluasukohta ning edasistes tegevustes antud väärtus kopeeritakse:

int x = 10;
int y = x; // Value copied over
y = 20;

System.out.println(x); // 10 - x does not change
System.out.println(y); // 20

Piltlikult tekib selline struktuur mälus:

Viitetüübid heap mälus

Viitemuutujad hoiavad viidet mäluasukohale, kus antud objekt asub. Objekt ise asub heap mälus. Sellega kaasneb näiteks järgmine probleem:

class Person {
String name;
}

Person p1 = new Person();
p1.name = "Ago";

Person p2 = p1; // Copies reference, not the object itself
p2.name = "Taavi"; // As a result both p1 and p2 get the new name because the underlying object is the same

System.out.println(p1.name); // Taavi
System.out.println(p2.name); // Taavi

Kui määrata muutuja p2 väärtuseks p1, siis kopeeritakse p2 väärtuseks p1 all olev viide. Ehk teisisõnu p2 saab väärtuseks juhised, kuidas p1 all oleva objektini jõuda. Kuna objekti ennast ei kopeeritud, siis nii p1 kui ka p2 reageerivad muudatustele samamoodi (ühe nimi muudeti, siis muudeti mõlema nimed).
Probleemi vältimiseks peaks objektist sügava koopia tegema, millest tuleb hiljem juttu.

Piltlikult tekib mälus selline struktuur:

Tüüpide mähkimine - wrapper klassid

Iga primitiivse tüübi jaoks on olemas ka vastav klass, mis võimaldab primitiivset väärtust käsitleda objektina. Nendeks on:

Primitiivne tüüpwrapper klass
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

Kasutusnäide:

int primitiveInt = 42;
Integer wrapperInt = 42;

wrapper-klasse kasutatakse tavaliselt siis, kui soovitakse kasutada teatud meetodeid (nt: Integer.parseInt()) või salvestada primitiivset tüüpi null väärtusega. Kuna primitiivid ise seda ei luba, siis mähitakse see väärtus objekti kaudu ära.
Samuti pole võimalik primitiive hoiustada kollektsioonides nagu ArrayList.
Näiteks:

// Collections
ArrayList<int> numbers; // Error
ArrayList<Integer> numbers; // OK

// null value
int x = null; // Error
Integer y = null; // OK

// Helper methods, string to number conversion for example:
String numStr = "123";
int num = Integer.parseInt(numStr); // "123" -> 123

Autoboxing ja unboxing

Alates Java versioon 5-st teisendab kompilaator automaatselt primitiivseid tüüpe vajadusel objektideks ning ka vastupidi.
Autoboxing - primitiivne väärtus teisendatakse wrapper-tüüpi objektiks
Unboxing - wrapper-tüüpi objekt teisendatakse primitiivseks väärtuseks

Näited:

int a = 10;
Integer b = a; // autoboxing

Integer c = 20;
int d = c; // unboxing

// Practical example:
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(5); // autoboxing: int -> Integer
int value = numbers.get(0); // unboxing: Integer -> int

Selle võttega kaasnevad muidugi ka ohud, näiteks kui viiteobjekt on null-väärtusega, siis unboxing põhjustaks NullPointerException erindi:

Integer wrapper = null;
int primitive = wrapper; // Error

Samuti on primitiivsed tüübid kiiremad ja mälutõhusamad kui viitetüübid. Selle visualiseerimiseks võite enda arvutis järgneva koodijupi tööle panna:

long start = System.currentTimeMillis();
int sumPrimitive = 0;
for (int i = 0; i < 1e9; i++) {
sumPrimitive += i;
}
System.out.println(System.currentTimeMillis() - start + "ms");

start = System.currentTimeMillis();
Integer sumWrapper = 0;
for (Integer i = 0; i < 1e9; i++) {
sumWrapper += i;
}
System.out.println(System.currentTimeMillis() - start + "ms");

Mõlema tüübi puhul käivitatakse tsükkel 1*10^9 ehk 1,000,000,000 (miljard) korda. Tulemus peaks olema, et primitiivide puhul toimus arvutus kiiremini kui viitetüüpidega.