Javaプリミティブとオブジェクト
1. 概要
このチュートリアルでは、Javaプリミティブ型とそれに対応するラップされた型を使用することの長所と短所を示します。
2. Java型システム
Javaには、 int 、 boolean などのプリミティブと、 Integer、 Booleanなどの参照型で構成される2つの型システムがあります。 すべてのプリミティブ型は参照型に対応します。
すべてのオブジェクトには、対応するプリミティブ型の単一の値が含まれています。 ラッパークラスは不変(オブジェクトが構築されると状態を変更できないようにするため)であり、final(継承できないようにするため)です。
内部的には、実際の型が宣言された型と異なる場合、Javaはプリミティブ型と参照型の間の変換を実行します。
Integer j = 1; // autoboxing
int i = new Integer(1); // unboxing
プリミティブ型を参照型に変換するプロセスはオートボクシングと呼ばれ、反対のプロセスはアンボクシングと呼ばれます。
3. 長所と短所
使用するオブジェクトの決定は、達成しようとするアプリケーションパフォーマンス、使用可能なメモリの量、使用可能なメモリの量、および処理する必要のあるデフォルト値に基づいています。
これらのいずれにも直面しない場合は、知っておく価値はありますが、これらの考慮事項を無視する可能性があります。
3.1. 単一アイテムのメモリーフットプリント
参考までに、プリミティブ型変数はメモリに次の影響を及ぼします。
- ブール値–1ビット
- バイト–8ビット
- 短い、char –16ビット
- int、float –32ビット
- long、double –64ビット
実際には、これらの値は仮想マシンの実装によって異なります。 たとえば、OracleのVMでは、ブール型はint値0および1にマップされるため、プリミティブ型と値で説明されているように、32ビットが必要です。
これらのタイプの変数はスタックに存在するため、高速にアクセスされます。 詳細については、Javaメモリモデルのチュートリアルをお勧めします。
参照型はオブジェクトであり、ヒープ上に存在し、アクセスが比較的遅くなります。 それらは、それらの原始的な対応物に関して一定のオーバーヘッドを持っています。
オーバーヘッドの具体的な値は、一般にJVM固有です。 ここでは、次のパラメータを使用した64ビット仮想マシンの結果を示します。
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)
オブジェクトの内部構造を取得するには、 Javaオブジェクトレイアウトツールを使用できます(オブジェクトのサイズを取得する方法については、別のチュートリアルを参照してください)。
このJVM上の参照型の単一インスタンスは、192ビットを占めるLongとDoubleを除いて、128ビットを占めることがわかります。
- ブール値–128ビット
- バイト–128ビット
- 短い文字–128ビット
- 整数、浮動小数点–128ビット
- ロング、ダブル–192ビット
Boolean タイプの単一の変数が128のプリミティブ変数と同じくらいのスペースを占有し、1つのInteger変数が4つのint変数と同じスペースを占めることがわかります。
3.2. アレイのメモリーフットプリント
検討中のタイプの配列を占めるメモリの量を比較すると、状況はさらに興味深いものになります。
タイプごとにさまざまな数の要素を含む配列を作成すると、次のプロットが得られます。
これは、メモリ m(s)が配列の要素数sにどのように依存するかに関して、タイプが4つのファミリにグループ化されていることを示しています。
- long、double:m(s)= 128 + 64 s
- 短い、char:m(s)= 128 + 64 [s / 4]
- バイト、ブール値:m(s)= 128 + 64 [s / 8]
- 残り:m(s)= 128 + 64 [s / 2]
ここで、角括弧は標準の天井関数を示します。
驚いたことに、プリミティブ型longおよびdoubleの配列は、ラッパークラスLongおよびDoubleよりも多くのメモリを消費します。
プリミティブ型の単一要素配列は、対応する参照型よりもほとんどの場合(longとdoubleを除く)高価であることがわかります。
3.3. パフォーマンス
Javaコードのパフォーマンスは非常に微妙な問題であり、コードが実行されるハードウェア、特定の最適化を実行する可能性のあるコンパイラ、仮想マシンの状態、他のプロセスのアクティビティに大きく依存します。オペレーティング·システム。
すでに述べたように、プリミティブ型はスタックに存在し、参照型はヒープに存在します。 これは、オブジェクトへのアクセス速度を決定する主要な要因です。
プリミティブ型の操作がラッパークラスの操作よりもどれだけ速いかを示すために、最後の要素を除いてすべての要素が等しい500万要素の配列を作成しましょう。 次に、その要素のルックアップを実行します。
while (!pivot.equals(elements[index])) {
index++;
}
配列にプリミティブ型の変数が含まれている場合と、参照型のオブジェクトが含まれている場合のこの操作のパフォーマンスを比較します。
よく知られているJMHベンチマークツールを使用しており(使用方法についてはチュートリアルを参照)、ルックアップ操作の結果を次のグラフにまとめることができます。
このような単純な操作でも、ラッパークラスの操作を実行するのに時間がかかることがわかります。
合計、乗算、除算などのより複雑な演算の場合、速度の差が急増する可能性があります。
3.4. デフォルト値
プリミティブタイプのデフォルト値は0です(対応する表現では、つまり 数値型の場合は0、 0.0d など)、ブール型の場合は false 、文字型の場合は \u0000。 ラッパークラスの場合、デフォルト値はnullです。
つまり、プリミティブ型はドメインからのみ値を取得できますが、参照型は、ある意味でドメインに属していない値( null )を取得する可能性があります。
変数を初期化しないままにしておくことは良い習慣とは見なされませんが、作成後に値を割り当てる場合があります。
このような状況で、プリミティブ型の変数の値がデフォルトの型と等しい場合、変数が実際に初期化されているかどうかを確認する必要があります。
null 値は、変数が初期化されていないことを明確に示しているため、ラッパークラス変数にはそのような問題はありません。
4. 使用法
これまで見てきたように、プリミティブ型ははるかに高速で、必要なメモリもはるかに少なくて済みます。 したがって、それらを使用することをお勧めします。
一方、現在のJava言語仕様では、パラメーター化された型(ジェネリック)、Javaコレクション、またはReflectionAPIでのプリミティブ型の使用は許可されていません。
アプリケーションで多数の要素を含むコレクションが必要な場合は、上のプロットに示されているように、できるだけ「経済的な」タイプの配列を使用することを検討する必要があります。
5. 結論
このチュートリアルでは、Javaのオブジェクトは、プリミティブアナログよりも低速で、メモリへの影響が大きいことを示しました。
いつものように、コードスニペットはGitHubのリポジトリにあります。