Primitif Java berbanding Objek

1. Gambaran keseluruhan

Dalam tutorial ini, kami menunjukkan kebaikan dan keburukan menggunakan jenis primitif Java dan rakan sejenisnya.

2. Sistem Jenis Java

Java mempunyai sistem jenis dua kali ganda yang terdiri daripada primitif seperti int , boolean dan jenis rujukan seperti Integer, Boolean . Setiap jenis primitif sesuai dengan jenis rujukan.

Setiap objek mengandungi satu nilai dari jenis primitif yang sesuai. The kelas pembungkus yang tidak berubah-ubah (supaya negeri mereka tidak boleh menukar apabila objek yang dibina) dan adalah muktamad (supaya kami tidak boleh mewarisi daripada mereka).

Di bawahnya, Java melakukan penukaran antara jenis primitif dan rujukan jika jenis sebenarnya berbeza dari yang dinyatakan:

Integer j = 1; // autoboxing int i = new Integer(1); // unboxing 

Proses menukar jenis primitif ke rujukan disebut autoboxing, proses sebaliknya disebut unboxing.

3. Kebaikan dan Kekurangan

Keputusan objek apa yang akan digunakan didasarkan pada prestasi aplikasi apa yang ingin kita capai, berapa banyak memori yang ada, jumlah memori yang ada dan nilai default apa yang harus kita tangani.

Sekiranya kita tidak menghadapi perkara tersebut, kita mungkin akan mengabaikan pertimbangan ini walaupun perlu diketahui.

3.1. Jejak Memori Item Tunggal

Sebagai rujukan, pemboleh ubah jenis primitif mempunyai kesan berikut pada memori:

  • boolean - 1 bit
  • bait - 8 bit
  • pendek, char - 16 bit
  • int, apungan - 32 bit
  • panjang, berganda - 64 bit

Dalam praktiknya, nilai-nilai ini boleh berbeza-beza bergantung pada pelaksanaan Mesin Maya. Dalam VM Oracle, jenis boolean, misalnya, dipetakan ke nilai int 0 dan 1, sehingga diperlukan 32 bit, seperti yang dijelaskan di sini: Jenis dan Nilai Primitif.

Pemboleh ubah jenis ini tinggal di timbunan dan oleh itu dapat diakses dengan pantas. Untuk perinciannya, kami mengesyorkan tutorial kami mengenai model memori Java.

Jenis rujukan adalah objek, ia tinggal di timbunan dan agak lambat untuk diakses. Mereka mempunyai overhead tertentu mengenai rakan sejawat mereka yang primitif.

Nilai konkrit overhead pada amnya adalah khusus JVM. Di sini, kami membentangkan hasil untuk mesin maya 64-bit dengan parameter berikut:

java 10.0.1 2018-04-17 Java(TM) SE Runtime Environment 18.3 (build 10.0.1+10) Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10.0.1+10, mixed mode)

Untuk mendapatkan struktur internal objek, kami mungkin menggunakan alat Layout Objek Java (lihat tutorial kami yang lain tentang cara mendapatkan ukuran objek).

Ternyata satu contoh jenis rujukan pada JVM ini menempati 128 bit kecuali Long dan Double yang menempati 192 bit:

  • Boolean - 128 bit
  • Byte - 128 bit
  • Pendek, Watak - 128 bit
  • Integer, Float - 128 bit
  • Panjang, Berkembar - 192 bit

Kita dapat melihat bahawa satu pemboleh ubah jenis Boolean menempati ruang sebanyak 128 yang primitif, sementara satu pemboleh ubah Integer menempati ruang sebanyak empat int .

3.2. Jejak Memori untuk Susunan

Keadaan menjadi lebih menarik jika kita membandingkan berapa banyak memori yang merangkumi susunan jenis yang dipertimbangkan.

Apabila kita membuat tatasusunan dengan pelbagai elemen untuk setiap jenis, kita memperoleh plot:

yang menunjukkan bahawa jenis dikelompokkan ke dalam empat keluarga sehubungan dengan bagaimana memori m bergantung pada bilangan elemen array:

  • panjang, berganda: m (s) = 128 + 64 s
  • pendek, char: m (s) = 128 + 64 [s / 4]
  • bait, boolean: m (s) = 128 + 64 [s / 8]
  • selebihnya: m (s) = 128 + 64 [s / 2]

di mana tanda kurung persegi menunjukkan fungsi siling standard.

Anehnya, susunan jenis primitif panjang dan berganda memakan lebih banyak memori daripada kelas pembungkus mereka Long dan Double .

Kita dapat melihat bahawa susunan elemen tunggal jenis primitif hampir selalu lebih mahal (kecuali panjang dan berganda) daripada jenis rujukan yang sesuai .

3.3. Persembahan

Prestasi kod Java adalah masalah yang agak halus, sangat bergantung pada perkakasan di mana kod tersebut berjalan, pada penyusun yang mungkin melakukan pengoptimuman tertentu, pada keadaan mesin maya, pada aktiviti proses lain di sistem operasi.

Seperti yang telah kita sebutkan, jenis primitif tinggal di timbunan sementara jenis rujukan tinggal di timbunan. Ini adalah faktor dominan yang menentukan seberapa cepat objek dapat diakses.

Untuk menunjukkan berapa banyak operasi untuk jenis primitif lebih cepat daripada operasi kelas pembungkus, mari buat susunan elemen lima juta di mana semua elemen sama kecuali yang terakhir; kemudian kami melakukan carian untuk elemen tersebut:

while (!pivot.equals(elements[index])) { index++; }

and compare the performance of this operation for the case when the array contains variables of the primitive types and for the case when it contains objects of the reference types.

We use the well-known JMH benchmarking tool (see our tutorial on how to use it), and the results of the lookup operation can be summarized in this chart:

Even for such a simple operation, we can see that it's required more time to perform the operation for wrapper classes.

In case of more complicated operations like summation, multiplication or division, the difference in speed might skyrocket.

3.4. Default Values

Default values of the primitive types are 0 (in the corresponding representation, i.e. 0, 0.0d etc) for numeric types, false for the boolean type, \u0000 for the char type. For the wrapper classes, the default value is null.

It means that the primitive types may acquire values only from their domains, while the reference types might acquire a value (null) that in some sense doesn't belong to their domains.

Though it isn't considered a good practice to leave variables uninitialized, sometimes we might assign a value after its creation.

In such a situation, when a primitive type variable has a value that is equal to its type default one, we should find out whether the variable has been really initialized.

There's no such a problem with a wrapper class variables since the null value is quite an evident indication that the variable hasn't been initialized.

4. Usage

As we've seen, the primitive types are much faster and require much less memory. Therefore, we might want to prefer using them.

On the other hand, current Java language specification doesn't allow usage of primitive types in the parametrized types (generics), in the Java collections or the Reflection API.

Apabila aplikasi kita memerlukan koleksi dengan sejumlah besar elemen, kita harus mempertimbangkan untuk menggunakan tatasusunan dengan jenis "ekonomis" sebanyak mungkin, seperti yang digambarkan pada plot di atas.

5. Kesimpulan

Dalam tutorial ini, kami menggambarkan bahawa objek di Java lebih lambat dan mempunyai kesan memori yang lebih besar daripada analog primitifnya.

Seperti biasa, coretan kod boleh didapati di repositori kami di GitHub.