Duboko razumevanje Java autoboxinga i unboxinga: razumevanje automatske konverzije tipova i odnosa između omotačkih klasa
„Brate, čuo sam da svaki osnovni tip podataka u Javi ima odgovarajuću omotnu klasu, na primer int omotnu klasu Integer, double omotnu klasu Double, tačno tako?" Iz rečenice Sanmej može se naslutiti da je unapred naučila ovaj sadržaj.
„Da, Sanmej." Nastavljam da odgovaram na pitanje Sanmej.
- Java je objektno-orijentisani programski jezik, ali radi poboljšanja efikasnosti programa, Java je uvela osnovne tipove podataka, kao što su int, double, boolean i tako dalje. Kasnije ću objasniti zašto.
- Međutim, osnovni tipovi podataka ne mogu da zadovolje sve scenarije upotrebe. Na primer, ako definišemo ArrayList tipa int, moraš koristiti
List<Integer> list = new ArrayList<>();, a neList<int> list = new ArrayList<>();, jer generici ne podržavaju osnovne tipove podataka. Kasnije ću i to objasniti zašto.
Budući da postoje i osnovni tipovi podataka i omotni tipovi, između njih sigurno postoje razlike u upotrebi, i u određenim scenarijima je potrebna konverzija tipova. O danas ćemo pričati o autoboxingu i unboxingu.
Autoboxing je konverzija objekta omotne klase u odgovarajući osnovni tip podataka, dok je unboxing konverzija osnovnog tipa podataka u odgovarajuću omotnu klasu.
Primer koda:
Integer chenmo = new Integer(10); // autoboxing
int wanger = chenmo.intValue(); // unboxingRazlike između omotnog tipa i osnovnog tipa
Dobro, prvo ćemo predstaviti razlike između omotnog tipa i osnovnog tipa.
Omotni tip može biti null, osnovni tip ne može
Ne podcenjuj ovu razliku, omogućava nam da omotni tip koristimo u POJO-u, a osnovni tip ne.
Šta je POJO?
Engleska skraćenica za POJO je Plain Ordinary Java Object, prevodi se kao: jednostavan običan Java objekat, ima samo polja i odgovarajuće setter i getter metode. Pogledajmo sledeći kod:
class Writer {
private Integer age;
private String name;
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}Ovo je vrlo čist, vrlo tipičan POJO, koji ćemo često koristiti u našim Java aplikacijama.
„Brate, da li je POJO zapravo JavaBean?" pita tada Sanmej.
„Da, ako definicija nije tako stroga, JavaBean je takođe vrsta POJO-a."
Slično kao POJO, postoje i:
- DTO (Data Transfer Object, objekat za prenos podataka između sloja prikaza i sloja servisa)
- VO (View Object, objekat koji enkapsulira podatke sa određene stranice)
- PO (Persistant Object, može se posmatrati kao Java objekat koji se mapira u tabelu baze podataka).
U projektu Paicoding ima mnogo POJO-a, evo snimka da osetite, u stvarnom radu ćete ih često sretati.

Samo, mi više ne pišemo setter i getter metode, već koristimo Lombok za automatsko generisanje. To je onaj @Data napomena na gornjoj slici.
„Pa zašto polja u POJO-u moraju da budu omotnog tipa?" opet pita Sanmej.
„U Alibaba Java razvojnom priručniku ima detaljno objašnjenje, pogledaj." Otvaram PDF i okrenem na odgovarajuću stranu, pokazujem ekran i čitam:
Rezultat upita baze podataka može biti null, ako se koristi osnovni tip, zbog potrebnog automatskog unboxing-a može se desiti NullPointerException izuzetak.
„Šta je automatski unboxing?"
„Automatski unboxing znači konverziju omotnog tipa u osnovni tip, na primer konverziju Integer objekta u int vrednost; analogno tome, konverziju osnovnog tipa u omotni tip zovemo automatski boxing."
„Oh."
Omotni tip se može koristiti u generičkim tipovima, osnovni tip ne može
„Zatim ćemo pogledati drugu razliku. Omotni tip se može koristiti u generičkim tipovima, osnovni tip ne može, inače će se desiti greška pri kompajliranju." Dok pričam, u Intellij IDEA počinjem da brzo kucam.
„Sanmej, pogledaj, kompajler je prijavio grešku."
List<int> list = new ArrayList<>(); // Greška pri sintaksi: ubaci 'Dimensions' da dovrši referentni tip
List<Integer> list = new ArrayList<>();„Zašto?" na vreme pita Sanmej.
„Zato što generici prilikom kompajliranja vrše brisanje tipova, na kraju ostaje samo originalni tip, a originalni tip može biti samo Object klasa i njene potklase — osnovni tipovi su izuzetak."
Ovo smo detaljno objasnili kada smo govorili o genericima, liči se da se još sećaš?
„Da, sećam se."
Osnovni tip je efikasniji od omotnog tipa
„Brate, ranije si rekao da je Java uvela osnovne tipove podataka radi poboljšanja efikasnosti programa, zašto?" opet pita Sanmej.
Ovde je zapravo reč o tom problemu.
„Dobro, sada ćemo reći treću tačku, osnovni tip je efikasniji od omotnog tipa." Popijem malo mlečnog čaja i nastavljam.
„Kada su lokalne promenljive, osnovni tipovi se čuvaju direktno na steku u konkretnim vrednostima, dok se omotni tipovi čuvaju referencu na heap." Dok pričam, otvaram draw.io i počinjem da crtam dijagram.

O heap-u i steku ćemo detaljno govoriti kada budemo učili JVM runtime strukture podataka
Vidno je, poređenjem sa osnovnim tipovima, omotni tip zauzima mnogo više memorijskog prostora.
- Osnovni tip: zauzima samo fiksnu količinu memorije dovoljnu za čuvanje njegove vrednosti. Na primer, int vrednost zauzima 4 bajta.
- Omotni tip: zauzima mnogo više memorijskog prostora, jer je objekat i čuva metapodatke objekta. Na primer, Integer objekat zauzima 16 bajtova.
Pored toga, ne treba samo čuvati objekat, već i referencu. Ako ne bi postojali osnovni tipovi, za podatke kao što su numeričke vrednosti koje se često koriste, svaki put bi morali da pravimo novi omotni tip, što bi bilo veoma teško.
Potvrdićemo ovu tvrdnju jednim jednostavnim primerom:
public class MemoryUsageTest {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
long memoryBefore, memoryAfter;
int size = 1000000;
// Testiranje zauzećine memorije osnovnog tipa int
runtime.gc();
memoryBefore = runtime.totalMemory() - runtime.freeMemory();
int[] intArray = new int[size];
memoryAfter = runtime.totalMemory() - runtime.freeMemory();
System.out.println("Zauzećina memorije nizom osnovnog tipa: " + (memoryAfter - memoryBefore));
// Testiranje zauzećine memorije omotnog tipa Integer
runtime.gc();
memoryBefore = runtime.totalMemory() - runtime.freeMemory();
Integer[] integerArray = new Integer[size];
for (int i = 0; i < size; i++) {
integerArray[i] = i; // automatski boxing
}
memoryAfter = runtime.totalMemory() - runtime.freeMemory();
System.out.println("Zauzećina memorije nizom omotnog tipa: " + (memoryAfter - memoryBefore));
}
}Kreiramo milion nizova, jedan sa osnovnim tipom, jedan sa omotnim tipom, zatim upoređujemo njihovu memorijsku zauzećinu.
Zauzećina memorije nizom osnovnog tipa: 5342192
Zauzećina memorije nizom omotnog tipa: 22790672Vidno je da niz sa osnovnim tipom zauzima manje memorijskog prostora za jedan red veličine od niza sa omotnim tipom. Naravno, ova metoda nije tako rigorozna, ali više-manje dokazuje neke stvari.
Gde se čuvaju podaci različitih tipova
„Sanmej, da li želiš da saznaš gde se podaci čuvaju prilikom izvršavanja programa?"
„Da da, brate, reci mi."
„Obično postoji 4 mesta gde se podaci mogu čuvati."
Registar. Ovo je najbrže memorijsko područje, jer se nalazi u CPU-u, koristi se za privremeno čuvanje podataka i rezultata računanja koji učestvuju u operacijama.
Stek. Nalazi se u RAM-u (Random Access Memory, glavna memorija koja direktno razmenjuje podatke sa CPU-om), brzina je odmah posle registara. Međutim, prilikom dodele memorijskog prostora na steku, mora unapred biti poznata veličina i životni ciklus podataka koji se čuvaju, nedostaje fleksibilnost. Vrednosti osnovnih tipova i reference objekata se obično čuvaju u ovom području.
Heap. Takođe se nalazi u RAM-u, može dinamički alocirati memorijski prostor, kompajler ne mora unapred da zna koliko memorije treba alocirati sa heap-a, životni ciklus takođe ne mora unapred da kaže kompajleru, Java sakupljač đubreći automatski čisti nekorišćene podatke, stoga se dobija veća fleksibilnost. Međutim, dinamička alokacija memorije i uništavanje objekata zahtevaju vreme, stoga je efikasnost manja od steka. Objekti kreirani pomoću new ključne reči će se čuvati u ovom području.
Disk. Ako su podaci potpuno izvan programa, mogu biti nezavisni od ograničenja programa, postoje i kada program nije pokrenut. Fajlovi, baze podataka, to je način da se objekti trajno čuvaju na disku. Kada zatreba, mogu se deserializovati u objekte koje program može prepoznati.
„Razumeš li, Sanmej?"
„Nakon što završim ovo lekciju, pa dobro probam da svarim."
„Ne brini, kasnije ćemo ponovo govoriti o JVM runtime strukturi podataka."
Vrednosti dva omotna tipa mogu biti iste, ali nisu jednake
„Dobro, sada ćemo reći četvrtu tačku, vrednosti dva omotna tipa mogu biti iste, ali nisu jednake."
Integer chenmo = new Integer(10);
Integer wanger = new Integer(10);
System.out.println(chenmo == wanger); // false
System.out.println(chenmo.equals(wanger )); // true„Kada se dva omotna tipa porede „==" operatorom, porede se da li su adrese koje pokazuju iste, jer su to dva različita objekta, adrese su različite."
„A izlaz chenmo.equals(wanger) je true, jer equals() metod unutra poređuje da li su dve int vrednosti jednake."
private final int value;
public int intValue() {
return value;
}
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}Iako su vrednosti chenmo i wanger obe 10, one nisu jednake. Drugim rečima: kada se „==" operator primeni na poređenje omotnih tipova, rezultat verovatno neće odgovarati očekivanju.
Automatski boxing i automatski unboxing
„Sanmej, ovaj ((Integer)obj).intValue() deo koda služi za unboxing. Međutim, ovo je ručni unboxing, postoji i automatski unboxing, detaljno ćemo ga objasniti."
Budući da postoje osnovni tipovi podataka i omotni tipovi, u nekim situacijama je potrebno vršiti konverziju između njih. Proces konverzije osnovnog tipa podataka u omotni tip zovemo boxing (pakovanje). Suprotno, proces konverzije omotnog tipa u osnovni tip podataka zovemo unboxing (raspakivanje).
Pre Jave 1.5, programeri su morali ručno da vrše pakovanje i raspakivanje, na primer:
Integer chenmo = new Integer(10); // ručni boxing
int wanger = chenmo.intValue(); // ručni unboxingJava 1.5 uvodi automatski boxing i automatski unboxing radi smanjenja rada programera, sada je mnogo pogodnije.
Integer chenmo = 10; // automatski boxing
int wanger = chenmo; // automatski unboxingDa pogledamo dekompajlirani kod.
Integer chenmo = Integer.valueOf(10);
int wanger = chenmo.intValue();To jest, automatski boxing se obavlja kroz Integer.valueOf(), a automatski unboxing kroz Integer.intValue().
„Sad ću ti dati jedan zadatak za razmislanje, Sanmej."
// 1) osnovni tip i omotni tip
int a = 100;
Integer b = 100;
System.out.println(a == b);
// 2) dva omotna tipa
Integer c = 100;
Integer d = 100;
System.out.println(c == d);
// 3)
c = 200;
d = 200;
System.out.println(c == d);„Dajem ti 3 minuta, prvo razmisli, ja ću ići da zapalim jedan Huazi, kad se vratim, onda ćemo analizirati zašto."
.........
„Da, brate, dođi, reknuću ti svoje misli."
Prvi deo koda, porede se osnovni tip i omotni tip pomoću == operatora, ovde će b biti automatski unboxing, direktno se porede vrednost sa a, rezultat je true.
Drugi deo koda, oba omotna tipa su dodeljena vrednošću 100, ovde dolazi do automatskog box-inga, prema tome si ranije rekla, kada se primeni „==" operator na poređenje omotnih tipova, rezultat verovatno neće odgovarati očekivanju, mislim da će rezultat biti false.
Treći deo koda, oba omotna tipa su ponovo dodeljena vrednošću 200, ovde takođe dolazi do automatskog box-inga, mislim da će rezultat i dalje biti false.
„Da, Sanmej, tvoja analiza je veoma logična, ali rezultat drugog dela koda je true, da li se čudiš?"
„Zašto je tako?" hitro pita Sanmej.
IntegerCache
„Tvoja pretpostavka je tačna, automatski boxing se obavlja kroz Integer.valueOf() method, pogledajmo izvorni k tog metoda i biće ti jasno."
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}Da li si videla klasu koju smo ranije videli — IntegerCache?
„Da li je to keš klasa za Integer?" Sanmej donosi svoj zaključak.
„Da, pogledajmo izvorni kod IntegerCache klase."
private static class IntegerCache {
// Minimalna vrednost keša, podrazumevano je -128
static final int low = -128;
// Maksimalna vrednost keša, podrazumevano je 127, ali se može konfigurisati kroz JVM parametre
static final int high;
static final Integer cache[];
static {
// Podrazumevana high vrednost je 127
int h = 127;
// Preko sistemskih osobina dobija korisnički podešena viša vrednost keša
// integerCacheHighPropValue je string, predstavlja konfigurisanu višu vrednost
int i = parseInt(integerCacheHighPropValue);
// Osigurava da je najviša vrednost keša barem 127
i = Math.max(i, 127);
// Podešava high vrednost, ali ne sme prelazi Integer.MAX_VALUE - (-low) - 1
h = Math.min(i, Integer.MAX_VALUE - (-low) - 1);
high = h;
// Inicijalizacija niza keša, veličina je high - low + 1
cache = new Integer[(high - low) + 1];
// Popunjavanje keša, počevši od low, za svaku vrednost kreira Integer objekat
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// Assert osigurava da je high vrednost barem 127, ovo zahteva Java specifikacija jezika
assert IntegerCache.high >= 127;
}
}Ukratko pogledaj ovaj kod i sve će ti biti jasno. Brojevi od -128 do 127 će se uzeti iz IntegerCache-a i upoređivati, stoga je rezultat drugog dela koda (100 je u ovom opsegu) true, a rezultat trećeg dela koda (200 nije u ovom opsegu, stoga su kreirana dva Integer objekta) je false.
„Sanmej, nakon gore analize, nadam se da zapamtiš jednu stvar: kada je potrebno automatsko pakovanje, ako je broj u opsegu od -128 do 127, direktno će se koristiti objekat iz keša, a ne kreirati novi objekat."
Napomene pri automatskom unboxingu
„Automatski pakovanje i raspakivanje je veoma korisna funkcionalnost, znatno smanjuje rad programera, ali može doneti i neke probleme, na primer sledeći kod, performanse su vrlo loše."
long t1 = System.currentTimeMillis();
Long sum = 0L;
for (int i = 0; i < Integer.MAX_VALUE;i++) {
sum += i;
}
long t2 = System.currentTimeMillis();
System.out.println(t2-t1);„Zašto? Sanmej."
„Možda zato što je sum deklarisan kao omotni tip Long umesto osnovnog tipa long." Sanmej razmišlja.
„Tačno, zato što je sum tipa Long, a i je tipa int, sum += i pri izvršavanju će prvo konvertovati i u long tip, zatim sum unboxovati u long tip i sabrati, zatim ponovo automatski pakovati u Long tip i dodeliti sum."
Kasnije kada naučiš javap komandu, možećeš kroz javap -v xxx.class komandu da pregledaš proces izvršavanja ovog koda.

U ovome ćeš videti Long.valueOf() i Long.longValue() prisutan. One odgovaraju automatskom pakovanju i automatskom raspakivanju.
„Sanmej, možeš da isprobaš, zameni sum sa long tipom i uporedi njihova vreme izvršavanja."
.........
„Vau, kad je sum tipa Long, trajalo je punih 5825 milisekundi; kad je sum tipa long, trebalo je samo 679 milisekundi."
„Dobro, Sanmej, današnja tema ćemo za sada ovako završiti. Još malo ću da zapalim jedan Huazi."
Zaključak
Danas smo govorili o konceptu autoboxinga i unboxinga, kao i razlikama između omotnih i osnovnih tipova. Na kraju, govorili smo i o principe automatskog box-inga i automatskog unbox-inga, kao i napombama pri automatskom unboxingu.
Otvoreni kod na GitHubu sa više od 17000 zvezdica „Ergeov put do naprednog Jave“ konačno je izašao u PDF formatu prvog izdanja! Uključuje osnovnu sintaksu Jave, nizove & stringove, OOP, okvir kolekcija, Java IO, obradu izuzetaka, nove karakteristike Jave, mrežno programiranje, NIO, konkurentno programiranje, JVM i tako dalje, ukupno preko 320.000 reči, više od 500 ručno crtanih slika, može se reći da je jednostavno i razumevo, duhovito... Detalje: Sjajno, Java tutorijal na GitHubu sa više od 17000 zvezdica
