Javaコレクションインタビューの質問

Javaコレクションインタビューの質問

1. 前書き

Javaコレクションは、Java開発者向けの技術インタビューでよく取り上げられるトピックです。 この記事では、最も頻繁に尋ねられる重要な質問を確認します。

2. 質問

Q1. コレクションの種類の階層を説明します。 主なインターフェースは何ですか、そしてそれらの間の違いは何ですか?

Iterableインターフェースは、for-eachループを使用して反復できるすべてのコレクションを表します。 CollectionインターフェイスはIterableを継承し、要素がコレクション内にあるかどうかの確認、コレクションへの要素の追加と削除、サイズの決定などのための汎用メソッドを追加します。

ListSet、およびQueueインターフェースは、Collectionインターフェースから継承します。

Listは順序付けられたコレクションであり、その要素にはリスト内のインデックスからアクセスできます。

Setは、集合の数学的概念と同様に、個別の要素を持つ順序付けられていないコレクションです。

Queueは、要素を追加、削除、および検査するための追加のメソッドを備えたコレクションであり、処理前に要素を保持するのに役立ちます。

Mapインターフェースもコレクションフレームワークの一部ですが、Collectionを拡張していません。 これは、一般的な抽象化では収集が困難なコレクションとマッピングの違いを強調するための仕様です。 Mapインターフェースは、一意のキーを持ち、キーごとに1つ以下の値を持つKey-Valueデータ構造を表します。

 

Q2. マップインターフェイスのさまざまな実装とそれらのユースケースの違いについて説明します。

Mapインターフェースの最も頻繁に使用される実装の1つは、HashMapです。 これは典型的なハッシュマップデータ構造であり、一定時間、つまりO(1)で要素にアクセスできますが、does not preserve order and is not thread-safeです。

要素の挿入順序を維持するために、HashMapを拡張し、さらに要素をリンクリストに結び付けるLinkedHashMapクラスを使用できますが、予測可能なオーバーヘッドがあります。

TreeMapクラスは、その要素を赤黒木構造に格納します。これにより、対数時間、つまりO(log(n))で要素にアクセスできます。 ほとんどの場合、HashMapよりも低速ですが、いくつかのComparatorに従って要素を順番に保つことができます。

ConcurrentHashMapは、ハッシュマップのスレッドセーフな実装です。 取得の完全な同時実行性(get操作はロックを伴わないため)と、予想される更新の同時実行性を提供します。

Hashtableクラスは、バージョン1.0以降Javaに含まれています。 非推奨ではありませんが、ほとんどが廃止されたと見なされます。 これはスレッドセーフなハッシュマップですが、ConcurrentHashMapとは異なり、そのすべてのメソッドは単純にsynchronizedです。つまり、このマップブロックに対するすべての操作は、独立した値の取得も含みます。

 

Q3. LinkedlistとArraylistの違いを説明してください。

ArrayListは、配列に基づくListインターフェイスの実装です。 ArrayListは、要素が追加または削除されたときに、この配列のサイズ変更を内部的に処理します。 配列内のインデックスにより、一定の時間でその要素にアクセスできます。 ただし、要素を挿入または削除すると、配列が巨大で挿入または削除された要素がリストの先頭に近い場合、結果の要素がすべてシフトすることが推測されます。

LinkedListは二重にリンクされたリストです。単一の要素は、前と次のNodeを参照するNodeオブジェクトに配置されます。 この実装は、リストのさまざまな部分に多くの挿入または削除がある場合、特にリストが大きい場合、ArrayListよりも効率的に見える場合があります。

ただし、ほとんどの場合、ArrayListLinkedListよりもパフォーマンスが優れています。 ArrayListでシフトする要素でさえ、O(n)操作でありながら、非常に高速なSystem.arraycopy()呼び出しとして実装されます。 Nodeオブジェクトをインスタンス化し、内部で複数の参照を更新する必要があるLinkedListのO(1)挿入よりも速く表示される場合もあります。 LinkedListは、複数の小さなNodeオブジェクトが作成されるため、メモリのオーバーヘッドが大きくなる可能性もあります。

 

Q4. ハッシュセットとツリーセットの違いは何ですか?

HashSetクラスとTreeSetクラスはどちらも、Setインターフェイスを実装し、個別の要素のセットを表します。 さらに、TreeSetNavigableSetインターフェースを実装します。 このインターフェイスは、要素の順序を利用するメソッドを定義します。

HashSetは内部的にHashMapに基づいており、TreeSetはそのプロパティを定義するTreeMapインスタンスによってサポートされています。HashSetは特定の要素を保持しません注文。 HashSetの要素を反復すると、シャッフルされた順序で要素が生成されます。 一方、TreeSetは、いくつかの事前定義されたComparatorに従って要素を順番に生成します。

 

Q5. HashmapはJavaでどのように実装されていますか? その実装はオブジェクトのハッシュコードと等しいメソッドをどのように使用しますか? そのような構造から要素を配置および取得する時間の複雑さは何ですか?

HashMapクラスは、特定の設計上の選択肢がある典型的なハッシュマップデータ構造を表します。

HashMapは、2の累乗のサイズを持つサイズ変更可能な配列によって支えられています。 要素がHashMapに追加されると、最初にそのhashCodeが計算されます(int値)。 次に、この値の下位ビットの特定の数が配列インデックスとして使用されます。 このインデックスは、このキーと値のペアを配置する配列のセル(バケットと呼ばれる)を直接指します。 配列内のインデックスによる要素へのアクセスは、非常に高速なO(1)操作です。これは、ハッシュマップ構造の主な機能です。

hashCodeは一意ではありませんが、hashCodesが異なっていても、同じ配列位置を受け取る場合があります。 これは衝突と呼ばれます。 ハッシュマップデータ構造の衝突を解決する方法は複数あります。 JavaのHashMapでは、各バケットは実際には単一のオブジェクトではなく、このバケットに到達したすべてのオブジェクトの赤黒木を参照します(Java 8より前は、これはリンクリストでした)。

したがって、HashMapがキーのバケットを決定すると、このツリーをトラバースして、キーと値のペアをその場所に配置する必要があります。 そのようなキーを持つペアがバケットに既に存在する場合、新しいペアに置き換えられます。

キーでオブジェクトを取得するには、HashMapは再びキーのhashCodeを計算し、対応するバケットを見つけ、ツリーをトラバースし、ツリー内のキーでequalsを呼び出して検索する必要があります。一致するもの。

HashMapには、要素の配置と取得のO(1)の複雑さ、または一定時間の複雑さがあります。 もちろん、すべての要素が単一のバケットに到着する最悪の場合、衝突が多数発生するとパフォーマンスがO(log(n))時間の複雑さまで低下する可能性があります。 これは通常、均一な分布で適切なハッシュ関数を提供することで解決されます。

HashMapの内部配列がいっぱいになると(次の質問で詳しく説明します)、2倍の大きさに自動的にサイズ変更されます。 この操作は、コストのかかる再ハッシュ(内部データ構造の再構築)を推測するため、事前にHashMapのサイズを計画する必要があります。

 

Q6. ハッシュマップの初期容量と負荷係数パラメーターの目的は何ですか? それらのデフォルト値は何ですか?

HashMapコンストラクターのinitialCapacity引数は、HashMapの内部データ構造のサイズに影響しますが、マップの実際のサイズについて推論するのは少し注意が必要です。 HashMapの内部データ構造は、2の累乗サイズの配列です。 したがって、initialCapacity引数値は次の2の累乗に増加します(たとえば、10に設定すると、内部配列の実際のサイズは16になります)。

HashMapの負荷率は、要素数をバケット数で割った比率です(つまり、 内部配列サイズ)。 たとえば、16バケットHashMapに12個の要素が含まれている場合、その負荷率は12/16 = 0.75です。 負荷率が高いということは、衝突が多いことを意味します。つまり、マップのサイズを次の2のべき乗に変更する必要があります。 したがって、loadFactor引数は、マップの負荷係数の最大値です。 マップがこの負荷係数を達成すると、内部配列のサイズを次の2のべき乗値に変更します。

initialCapacityはデフォルトで16であり、loadFactorはデフォルトで0.75であるため、デフォルトのコンストラクターでインスタンス化されたHashMapに12個の要素を配置でき、サイズは変更されません。 同じことがHashSetにも当てはまります。これは、内部でHashMapインスタンスによってサポートされています。

したがって、ニーズを満たすinitialCapacityを思い付くのは簡単ではありません。 これが、GuavaライブラリにMaps.newHashMapWithExpectedSize()メソッドとSets.newHashSetWithExpectedSize()メソッドがあり、サイズを変更せずに予想される数の要素を保持できるHashMapまたはHashSetを作成できる理由です。

===

Q7. 列挙型の特別なコレクションを説明します。 通常のコレクションと比較して、それらの実装の利点は何ですか?

EnumSetおよびEnumMapは、それぞれSetおよびMapインターフェースの特別な実装です。 これらの実装は非常に効率的であるため、列挙型を扱うときは常にこれらの実装を使用する必要があります。

EnumSetは、セットに存在する列挙型の序数に対応する位置に「1」が付いた単なるビットベクトルです。 列挙値がセット内にあるかどうかを確認するには、実装はベクトル内の対応するビットが「1」であるかどうかを確認するだけで、非常に簡単な操作です。 同様に、EnumMapは、列挙型の序数値をインデックスとして使用してアクセスされる配列です。 EnumMapの場合、ハッシュコードを計算したり、衝突を解決したりする必要はありません。

 

Q8. フェイルファストイテレータとフェイルセーフイテレータの違いは何ですか?

異なるコレクションのイテレータは、同時変更に対する反応に応じて、フェイルファーストまたはフェイルセーフのいずれかになります。 同時変更は、別のスレッドからのコレクションの変更だけでなく、同じスレッドからの変更であるが、別のイテレーターを使用するか、コレクションを直接変更します。

Fail-fastイテレータ(HashMapArrayList、およびその他のスレッドセーフでないコレクションによって返されるもの)は、コレクションの内部データ構造を反復処理し、ConcurrentModificationExceptionをスローします。同時変更を検出します。

Fail-safeイテレータ(ConcurrentHashMapCopyOnWriteArrayListなどのスレッドセーフコレクションによって返される)は、反復する構造体のコピーを作成します。 それらは、同時変更による安全性を保証します。 それらの欠点には、過剰なメモリ消費と、コレクションが変更された場合に古くなった可能性のあるデータの反復が含まれます。

 

Q9. ComparableおよびComparatorインターフェイスを使用してコレクションを並べ替えるにはどうすればよいですか?

Comparableインターフェースは、ある順序に従って比較できるオブジェクトのインターフェースです。 その単一のメソッドはcompareToであり、オブジェクト自体と同じタイプの引数オブ​​ジェクトの2つの値を操作します。 たとえば、IntegerLong、およびその他の数値タイプは、このインターフェイスを実装します。 Stringもこのインターフェイスを実装し、そのcompareToメソッドは文字列を辞書式順序で比較します。

Comparableインターフェイスを使用すると、対応するオブジェクトのリストをCollections.sort()メソッドで並べ替え、SortedSetおよびSortedMapを実装するコレクションの反復順序を維持できます。 オブジェクトを何らかのロジックを使用して並べ替えることができる場合は、Comparableインターフェイスを実装する必要があります。

Comparableインターフェースは通常、要素の自然な順序を使用して実装されます。 たとえば、すべてのIntegerの数値は、小さい値から大きい値の順に並べられます。 ただし、たとえば、数字を降順に並べ替えるなど、別の種類の順序を実装することもできます。 Comparatorインターフェースがここで役立ちます。

ソートするオブジェクトのクラスは、このインターフェースを実装する必要はありません。 実装クラスを作成し、2つのオブジェクトを受け取り、それらの順序を決定するcompareメソッドを定義するだけです。 次に、このクラスのインスタンスを使用して、Collections.sort()メソッドまたはSortedSetおよびSortedMapインスタンスの自然な順序をオーバーライドできます。

Comparatorインターフェースは関数型インターフェースであるため、次の例のように、ラムダ式に置き換えることができます。 これは、自然な順序付け(IntegerComparableインターフェース)とカスタムイテレーター(Comparator<Integer>インターフェース)を使用したリストの順序付けを示しています。

List list1 = Arrays.asList(5, 2, 3, 4, 1);
Collections.sort(list1);
assertEquals(new Integer(1), list1.get(0));

List list1 = Arrays.asList(5, 2, 3, 4, 1);
Collections.sort(list1, (a, b) -> b - a);
assertEquals(new Integer(5), list1.get(0));