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üüp | Kirjeldus | Vaikimisi väärtus | Suurus |
|---|---|---|---|
byte | Väike täisarv | 0 | 8 biti |
short | Lühike täisarv | 0 | 16 biti |
int | Täisarv | 0 | 32 biti |
long | Pikk täisarv | 0L | 64 biti |
float | Ujukomaarv | 0.0f | 32 biti |
double | Topelt-ujukomaarv | 0.0d | 64 biti |
char | Unicode tähemärk | '\u0000' | 16 biti |
boolean | Tõeväärtus | false | - |
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
nullväärtusega - Omavad fikseeritud suurust mälus
- Näiteks kui määrata numbrile "1"
inttüü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.
- Näiteks kui määrata numbrile "1"
- 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:
Stringehk sõnedInteger,Double,Booleanjt. - 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üüp | wrapper klass |
|---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
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.