Java Genericsのインタビューの質問(回答)

Javaジェネリックインタビューの質問(回答)

1. 前書き

この記事では、Javaジェネリックスのインタビューの質問と回答の例をいくつか紹介します。

ジェネリックは、Java 5で最初に導入されたJavaのコアコンセプトです。 このため、ほぼすべてのJavaコードベースがそれらを使用し、開発者が何らかの時点でそれらに遭遇することをほぼ保証します。 これが、それらを正しく理解することが不可欠である理由であり、面接の過程で質問される可能性が高い理由です。

2. 質問

Q1. ジェネリック型パラメーターとは何ですか?

Typeは、classまたはinterfaceの名前です。 名前が示すように、a generic type parameter is when a type can be used as a parameter in a class, method or interface declaration.

これを示すために、ジェネリックを含まない簡単な例から始めましょう。

public interface Consumer {
    public void consume(String parameter)
}

この場合、consume()メソッドのメソッドパラメータータイプはString.です。パラメーター化も構成もできません。

次に、String型をT.と呼ぶジェネリック型に置き換えましょう。慣例により、次のような名前が付けられています。

public interface Consumer {
    public void consume(T parameter)
}

コンシューマーを実装するとき、引数として消費させたいtypeを指定できます。 これはジェネリック型パラメーターです。

public class IntegerConsumer implements Consumer {
    public void consume(Integer parameter)
}

この場合、整数を消費できます。 このtypeを必要なものに交換できます。

Q2. ジェネリック型を使用する利点は何ですか?

ジェネリックを使用する1つの利点は、コストを回避し、型の安全性を提供することです。 これは、コレクションを操作するときに特に役立ちます。 これをデモンストレーションしましょう:

List list = new ArrayList();
list.add("foo");
Object o = list.get(1);
String foo = (String) o;

この例では、リストの要素タイプはコンパイラに認識されていません。 これは、保証できる唯一のものがオブジェクトであることを意味します。 したがって、要素を取得すると、オブジェクトが返されます。 コードの作成者として、それがString,であることはわかっていますが、問題を明示的に修正するには、オブジェクトを1つにキャストする必要があります。 これは多くのノイズと定型句を生成します。

次に、手動エラーの余地について考え始めると、キャストの問題が悪化します。 誤ってリストに整数があった場合はどうなりますか?

list.add(1)
Object o = list.get(1);
String foo = (String) o;

この場合、IntegerString.にキャストできないため、実行時にClassCastExceptionを取得します。

それでは、今度はジェネリックを使用して、繰り返してみましょう。

List list = new ArrayList<>();
list.add("foo");
String o = list.get(1);    // No cast
Integer foo = list.get(1); // Compilation error

ご覧のとおり、by using generics we have a compile type check which prevents ClassCastExceptions and removes the need for casting.

The other advantage is to avoid code duplication。 ジェネリックがなければ、同じコードをコピーして貼り付けますが、タイプは異なります。 ジェネリックでは、これを行う必要はありません。 ジェネリック型に適用されるアルゴリズムを実装することもできます。

Q3. 型消去とは何ですか?

ジェネリック型の情報はコンパイラーのみが利用でき、JVMは利用できないことを理解することが重要です。 言い換えれば、, type erasure means that generic type information is not available to the JVM at runtime, only compile timeです。

主要な実装の選択の背後にある理由は単純です。つまり、Javaの古いバージョンとの後方互換性を維持しています。 ジェネリックコードをバイトコードにコンパイルすると、ジェネリックタイプが存在しないかのようになります。 これは、コンパイルが以下を行うことを意味します。

  1. ジェネリック型をオブジェクトで置き換える

  2. バインドされた型(後の質問でこれらについて詳しく説明します)を最初のバインドされたクラスに置き換えます

  3. ジェネリックオブジェクトを取得するときにキャストに相当するものを挿入します。

型消去を理解することが重要です。 そうしないと、開発者は混乱して、実行時に型を取得できると考える可能性があります。

public foo(Consumer consumer) {
   Type type = consumer.getGenericTypeParameter()
}

上記の例は、型を消去せずに物事がどのように見えるかと同等の擬似コードですが、残念ながら不可能です。 もう一度、the generic type information is not available at runtime.

Q4. オブジェクトをインスタンス化するときにジェネリック型が省略された場合でも、コードはコンパイルされますか?

ジェネリックはJava 5より前には存在しなかったため、ジェネリックをまったく使用しないことが可能です。 たとえば、ジェネリックは、コレクションなどのほとんどの標準Javaクラスに後付けされました。 質問1のリストを見ると、ジェネリック型を省略した例が既にあることがわかります。

List list = new ArrayList();

コンパイルは可能ですが、コンパイラから警告が表示される可能性があります。 これは、ジェネリックを使用することで得られる余分なコンパイル時チェックが失われているためです。

覚えておくべきポイントは、while backward compatibility and type erasure make it possible to omit generic types, it is bad practice.

Q5. ジェネリックメソッドはジェネリックタイプとどのように異なりますか?

A generic method is where a type parameter is introduced to a method,living within the scope of that method.例を使ってこれを試してみましょう。

public static  T returnType(T argument) {
    return argument;
}

静的メソッドを使用しましたが、必要に応じて非静的メソッドを使用することもできます。 型推論(次の質問で説明します)を利用することで、通常のメソッドと同様に、型引数を指定する必要なく呼び出すことができます。

Q6. 型推論とは何ですか?

型推論とは、コンパイラがメソッド引数の型を調べてジェネリック型を推論できる場合です。 たとえば、T,を返すメソッドにTを渡した場合、コンパイラーは戻り値の型を把握できます。 前の質問から一般的なメソッドを呼び出して、これを試してみましょう。

Integer inferredInteger = returnType(1);
String inferredString = returnType("String");

ご覧のとおり、キャストの必要はなく、ジェネリック型の引数を渡す必要もありません。 引数の型は戻り値の型のみを推測します。

Q7. 有界型パラメータとは何ですか?

これまでのすべての質問は、制限のないジェネリック型の引数を扱ってきました。 これは、ジェネリック型の引数が任意の型である可能性があることを意味します。

制限付きパラメーターを使用する場合、ジェネリック型引数として使用できる型を制限しています。

例として、ジェネリック型を常に動​​物のサブクラスにするように強制したいとします。

public abstract class Cage {
    abstract void addAnimal(T animal)
}

extends,を使用することにより、Tを動物.のサブクラスにするように強制します。次に、猫のケージを作成できます。

Cage catCage;

しかし、オブジェクトは動物のサブクラスではないため、オブジェクトのケージを持つことはできません。

Cage objectCage; // Compilation error


この利点の1つは、動物のすべての方法がコンパイラーで利用できることです。 私たちは型がそれを拡張することを知っているので、どんな動物でも動作する一般的なアルゴリズムを書くことができます。 これは、さまざまな動物のサブクラスに対してメソッドを再現する必要がないことを意味します。

public void firstAnimalJump() {
    T animal = animals.get(0);
    animal.jump();
}

Q8. 複数の有界型パラメーターを宣言することは可能ですか?

ジェネリック型の複数の境界を宣言することは可能です。 前の例では、単一の境界を指定しましたが、必要に応じてさらに指定することもできます。

public abstract class Cage

この例では、動物はクラスであり、匹敵するものはインターフェースです。 ここで、型はこれらの上限の両方を尊重する必要があります。 私たちの型が動物のサブクラスであるが、同等のものを実装しなかった場合、コードはコンパイルされません。 It’s also worth remembering that if one of the upper bounds is a class, it must be the first argument.

Q9. ワイルドカードタイプとは何ですか?

A wildcard type represents an unknown type。 次のように疑問符で爆発します。

public static consumeListOfWildcardType(List list)

ここでは、任意のtypeのリストを指定しています。 このメソッドに任意のリストを渡すことができます。

Q10. 上限付きワイルドカードとは何ですか?

An upper bounded wildcard is when a wildcard type inherits from a concrete type。 これは、コレクションと継承を扱うときに特に役立ちます。

最初にワイルドカードタイプを使用せずに、動物を保存するファームクラスでこれを実証してみましょう。

public class Farm {
  private List animals;

  public void addAnimals(Collection newAnimals) {
    animals.addAll(newAnimals);
  }
}

catやdog,などのanimal,のサブクラスが複数ある場合、それらすべてをファームに追加できるという誤った仮定を行う可能性があります。

farm.addAnimals(cats); // Compilation error
farm.addAnimals(dogs); // Compilation error

これは、コンパイラがサブクラス化されたものではなく、具象型animal,のコレクションを予期しているためです。

それでは、動物を追加する方法に上限のワイルドカードを導入しましょう。

public void addAnimals(Collection newAnimals)

もう一度試してみると、コードがコンパイルされます。 これは、動物のあらゆるサブタイプのコレクションを受け入れるようにコンパイラーに指示しているためです。

Q11. 無制限のワイルドカードとは何ですか?

無制限のワイルドカードは、上限または下限のないワイルドカードであり、任意のタイプを表すことができます。

ワイルドカードタイプはオブジェクトと同義ではないことを知っておくことも重要です。 これは、ワイルドカードには任意のタイプを使用できますが、オブジェクトタイプは具体的にはオブジェクトであるためです(オブジェクトのサブクラスにはできません)。 例を挙げてこれを示しましょう。

List wildcardList = new ArrayList();
List objectList = new ArrayList(); // Compilation error


繰り返しますが、2行目がコンパイルされない理由は、文字列のリストではなく、オブジェクトのリストが必要だからです。 未知のタイプのリストが受け入れられるため、最初の行がコンパイルされます。

Q12. 下限ワイルドカードとは何ですか?

下限ワイルドカードは、上限を提供する代わりに、superキーワードを使用して下限を提供する場合です。 つまり、a lower bounded wildcard means we are forcing the type to be a superclass of our bounded typeです。 例を挙げてこれを試してみましょう。

public static void addDogs(List list) {
   list.add(new Dog("tom"))
}

super,を使用することで、オブジェクトのリストでaddDogsを呼び出すことができます。

ArrayList objects = new ArrayList<>();
addDogs(objects);


オブジェクトは動物のスーパークラスなので、これは理にかなっています。 下限のワイルドカードを使用しなかった場合、オブジェクトのリストは動物のリストではないため、コードはコンパイルされません。

考えてみると、猫や犬などの動物のサブクラスのリストに犬を追加することはできません。 動物のスーパークラスのみ。 たとえば、これはコンパイルされません。

ArrayList objects = new ArrayList<>();
addDogs(objects);

Q13. 下限タイプとvs. 上限タイプ?

コレクションを扱う場合、上限または下限のワイルドカードを選択するための一般的なルールはPECSです。 PECSはproducer extends, consumer super.の略。

これは、いくつかの標準Javaインターフェイスとクラスを使用して簡単に実証できます。

Producer extendsは、ジェネリック型のプロデューサーを作成する場合は、extendsキーワードを使用することを意味します。 この原則をコレクションに適用して、なぜそれが理にかなっているのかを見てみましょう。

public static void makeLotsOfNoise(List animals) {
    animals.forEach(Animal::makeNoise);
}

ここでは、コレクション内の各動物のmakeNoise()を呼び出します。 これは、コレクションがプロデューサー,であることを意味します。これは、コレクションを使用して、操作を実行するために動物を返すことだけを行っているためです。 extendsを削除すると、猫,犬またはその他の動物のサブクラスのリストを渡すことができなくなります。 プロデューサーが原則を拡張することを適用することで、可能な限りの柔軟性が得られます。

Consumer superは、producer extends.の反対を意味します。つまり、要素を消費するものを扱う場合は、superキーワードを使用する必要があります。 これは、前の例を繰り返すことで実証できます。

public static void addCats(List animals) {
    animals.add(new Cat());
}

動物のリストに追加するだけなので、動物のリストは消費者です。 これが、superキーワードを使用する理由です。 これは、サブクラスではなく、動物のスーパークラスのリストを渡すことができることを意味します。 たとえば、犬または猫のリストを渡そうとした場合、コードはコンパイルされません。

最後に考慮すべきことは、コレクションが消費者と生産者の両方である場合の対処方法です。 この例は、要素が追加および削除されるコレクションです。 この場合、無制限のワイルドカードを使用する必要があります。

Q14. 実行時にジェネリック型情報が利用できる状況はありますか?

実行時にジェネリック型が使用できる状況が1つあります。 これは、ジェネリック型がクラス署名の一部である場合です:

public class CatCage implements Cage

リフレクションを使用することにより、次の型パラメーターを取得します。

(Class) ((ParameterizedType) getClass()
  .getGenericSuperclass()).getActualTypeArguments()[0];

このコードはやや脆弱です。 たとえば、直接のスーパークラスで定義されているタイプパラメータに依存します。 しかし、JVMがこのタイプ情報を持っていることを示しています。