Java Genericsのインタビューの質問(回答)
1前書き
この記事では、Javaジェネリックのインタビューに関する質問と回答の例をいくつか紹介します。
ジェネリックスは、Java 5で最初に導入された、Javaの中核的な概念です。このため、ほぼすべてのJavaコードベースがそれらを利用し、開発者がいつかそれらに遭遇することをほぼ保証します。そのため、それらを正しく理解することが不可欠であり、インタビューの過程で質問される可能性が高いのです。
2質問
Q1. ジェネリック型パラメータとは何ですか?
Type
は、
class
または
interface
の名前です。その名前が示すように、ジェネリック型パラメータは、
type
がクラス、メソッド、またはインタフェースの宣言でパラメータとして使用できる場合です。
これを実証するために、総称を含まない簡単な例から始めましょう。
public interface Consumer {
public void consume(String parameter)
}
この場合、
consume()
メソッドのメソッドパラメータ型は__Stringです。パラメータ化されておらず、設定もできません。
それでは、
String
型を__T.と呼ぶ汎用型に置き換えましょう。慣例では次のようになります。
public interface Consumer<T> {
public void consume(T parameter)
}
コンシューマを実装するときに、消費したい
type
を引数として指定できます。これはジェネリック型パラメータです。
public class IntegerConsumer implements Consumer<Integer> {
public void consume(Integer parameter)
}
この場合、今度は整数を消費することができます。この
type
は、必要なものは何でも入れ替えることができます。
Q2. ジェネリック型を使用する利点は何ですか?
総称を使用することの1つの利点は、コストを回避し、型安全性を提供することです。これはコレクションを扱うときに特に便利です。
これを実証しましょう。
これを実証しましょう。
List list = new ArrayList();
list.add("foo");
Object o = list.get(1);
String foo = (String) foo;
この例では、リスト内の要素型はコンパイラにはわかりません。
つまり、保証できるのはそれがオブジェクトであるということだけです。そのため、要素を取得すると、オブジェクトが返されます。
コードの作成者として、私たちはそれが__Stringであることを知っています。これは多くのノイズと定型文を生み出します。
次に、手動エラーの余地について考え始めると、キャスト問題はさらに悪化します。誤ってリストに整数が入った場合はどうなりますか?
list.add(1)
Object o = list.get(1);
String foo = (String) foo;
この場合、
Integer
は
String.
にキャストできないため、実行時に
ClassCastException
が返されます。
それでは、今度は総称を使用して、繰り返します。
List<String> list = new ArrayList<>();
list.add("foo");
String o = list.get(1); //No cast
Integer foo = list.get(1);//Compilation error
ご覧のとおり、** 総称を使用することによって、
ClassCastExceptions
を防ぎ、キャストの必要性を排除するコンパイルタイプチェックがあります。
他の利点はコードの重複を避けることです。総称がなければ、同じコードをコピーして貼り付ける必要がありますが、タイプは異なります。ジェネリックでは、これを行う必要はありません。ジェネリック型に適用するアルゴリズムさえも実装できます。
** Q3. タイプ消去とは
ジェネリック型情報はJVMではなくコンパイラでのみ利用可能であることを認識することが重要です。言い換えれば
、型消去は実行時にはJVMが一般型情報を利用できないことを意味し、コンパイル時のみ
。
主な実装の選択の背後にある推論は単純です – 古いバージョンのJavaとの後方互換性を維持するジェネリックコードがバイトコードにコンパイルされると、ジェネリック型が存在しなかったかのようになります。つまり、コンパイルは次のことを意味します。
-
ジェネリック型をオブジェクトに置き換える
-
境界のあるタイプ(後の質問でこれらについてもっと詳しく)を
ファーストバウンドクラス
。ジェネリックオブジェクトを取得するときには、同等のキャストを挿入します。
タイプ消去を理解することは重要です。そうでなければ、開発者は混乱して、実行時に型を取得できると思うかもしれません。
public foo(Consumer<T> consumer) {
Type type = consumer.getGenericTypeParameter()
}
上記の例は、型が消去されていない場合の外観と同等の擬似コードですが、残念ながら不可能です。繰り返しになりますが、** 総称型情報は実行時には利用できません。
** Q4. オブジェクトのインスタンス化時にジェネリック型が省略された場合、コードはコンパイルされますか
総称はJava 5より前には存在しなかったので、それらをまったく使用しないことは可能です。たとえば、総称は、コレクションなどの標準的なJavaクラスのほとんどに組み込まれました。質問1からリストを見ると、ジェネリック型を省略する例がすでにあることがわかります。
List list = new ArrayList();
コンパイルできても、それでもコンパイラから警告が出る可能性があります。これは、ジェネリックを使用して得られる余分なコンパイル時チェックを失っているためです。
覚えておくべき点は、** 後方互換性と型の消去によってジェネリック型を省略することは可能ですが、これは悪い習慣です。
Q5. ジェネリックメソッドはジェネリック型とどう違うのですか?
-
一般的なメソッドとは、メソッドに型パラメータが導入される場所で、そのメソッドの範囲内にあります。** 例を使って試してみましょう。
public static <T> T returnType(T argument) {
return argument;
}
静的メソッドを使用しましたが、必要に応じて非静的メソッドも使用できました。型推論(次の質問で取り上げます)を利用することで、通常のメソッドと同じようにこれを呼び出すことができます。その際、型引数を指定する必要はありません。
Q6. 型推論とは何ですか?
型推論は、コンパイラがジェネリック型を推論するためにメソッド引数の型を調べることができる場合です。たとえば、
T
を返すメソッドに
T
を渡した場合、コンパイラは戻り型を判断できます。前の質問から私たちの一般的な方法を呼び出してこれを試してみましょう:
Integer inferredInteger = returnType(1);
String inferredString = returnType("String");
ご覧のとおり、キャストは不要で、総称型引数を渡す必要もありません。引数typeは戻り型を推測するだけです。
** Q7. 有界型パラメータとは
これまでのところ、私たちのすべての質問は無制限のジェネリック型引数をカバーしてきました。これは我々の総称型引数が我々が望む任意の型であり得ることを意味します。
境界パラメータを使用するときは、総称型引数として使用できる型を制限します。
例として、ジェネリック型を常に動物のサブクラスにすることを強制したいとしましょう。
public abstract class Cage<T extends Animal> {
abstract void addAnimal(T animal)
}
extends
__を使うことで、
T
を動物のサブクラスにすることを強制しています
.
__そして、猫の檻を作ることができます。
Cage<Cat> catCage;
しかし、オブジェクトは動物のサブクラスではないので、オブジェクトのケージを持つことはできませんでした。
Cage<Object> objectCage;//Compilation error
これの1つの利点は、animalのすべてのメソッドがコンパイラに利用できることです。私達は私達の型がそれを拡張することを知っているので、私達はどんな動物にも作用する一般的なアルゴリズムを書くことができた。これは、異なる動物のサブクラスに対して私たちの方法を再現する必要がないことを意味します。
public void firstAnimalJump() {
T animal = animals.get(0);
animal.jump();
}
Q8. 多重有界型パラメータを宣言することは可能ですか?
ジェネリック型に対して複数の境界を宣言することは可能です。前の例では、単一の範囲を指定しましたが、必要に応じて詳細を指定することもできます。
public abstract class Cage<T extends Animal & Comparable>
この例では、動物はクラスで、匹敵するものはインターフェースです。
今、私たちの型はこれらの上限の両方を尊重しなければなりません。私たちの型がanimalのサブクラスであっても、それに匹敵するものを実装していなければ、そのコードはコンパイルされません。上限の1つがクラスの場合、それが最初の引数でなければならないことも覚えておく価値があります。
Q9. ワイルドカードタイプとは何ですか?
-
ワイルドカードタイプは未知の
type
** を表します。次のように疑問符で爆発します。
public static consumeListOfWildcardType(List<?> list)
ここでは、どんな
type
でも構わないリストを指定しています。このメソッドに何かのリストを渡すことができます。
Q10. 上限のワイルドカードとは何ですか?
-
上限ワイルドカードは、ワイルドカード型が具象型を継承している場合です。これはコレクションや継承を扱うときに特に便利です。
最初にワイルドカードを使わずに、動物を保管する農場クラスでこれを実演してみましょう。
public class Farm {
private List<Animal> animals;
public void addAnimals(Collection<Animal> newAnimals) {
animals.addAll(newAnimals);
}
}
猫や犬のように、動物のサブクラスが複数ある場合は、それらすべてを農場に追加できるという誤った仮定をすることがあります。
farm.addAnimals(cats);//Compilation error
farm.addAnimals(dogs);//Compilation error
これは、コンパイラがサブクラスではなく、具象型animal ____のコレクションを期待しているためです。
それでは、動物の追加方法に上限のワイルドカードを紹介しましょう。
public void addAnimals(Collection<? extends Animal> newAnimals)
もう一度試してみると、コードがコンパイルされます。これは、コンパイラーに、任意のサブタイプの動物のコレクションを受け入れるように指示しているためです。
Q11. 無制限のワイルドカードとは何ですか?
無制限のワイルドカードは、上限も下限もないワイルドカードで、あらゆるタイプを表すことができます。
ワイルドカードタイプはobjectと同義ではないことを知っておくことも重要です。これは、ワイルドカードは任意の型にすることができるのに対し、オブジェクト型は特にオブジェクトである(そしてオブジェクトのサブクラスにすることはできない)ためです。
例を挙げて説明しましょう。
List<?> wildcardList = new ArrayList<String>();
List<Object> objectList = new ArrayList<String>();//Compilation error
2行目がコンパイルされないのは、文字列のリストではなく、オブジェクトのリストが必要だからです。最初の行は、不明なタイプのリストでも問題ないためコンパイルされています。
==== * Q12。下限のワイルドカードとは何ですか?**
下限のワイルドカードは、上限を提供する代わりに
super
キーワードを使用して下限を提供する場合です。言い換えれば、** 下限のワイルドカードは、その型をその型のスーパークラスにすることを強制していることを意味します。例を見てみましょう。
public static void addDogs(List<? super Animal> list) {
list.add(new Dog("tom"))
}
__superを使用すると、オブジェクトのリストに対してaddDogsを呼び出すことができます。
ArrayList<Object> objects = new ArrayList<>();
addDogs(objects);
オブジェクトは動物のスーパークラスなので、これは理にかなっています。下限のワイルドカードを使用しなかった場合、オブジェクトのリストは動物のリストではないため、コードはコンパイルされません。
考えてみれば、猫や犬など、動物のサブクラスのリストに犬を追加することはできません。動物のスーパークラスだけ。たとえば、これはコンパイルされません。
ArrayList<Cat> objects = new ArrayList<>();
addDogs(objects);
==== * Q13。下限タイプと上限タイプのどちらを使用することを選択しますか?**
コレクションを扱う場合、上限または下限のワイルドカードを選択するための一般的な規則はPECSです。 PECSは
生産者拡大、消費者スーパー
の略です。
これは、いくつかの標準のJavaインタフェースとクラスを使用することで簡単に実証できます。
Producer extends
は、ジェネリック型のプロデューサを作成している場合は
extends
キーワードを使用することを意味します。この原則をコレクションに適用して、なぜそれが理にかなっているのかを確認しましょう。
public static void makeLotsOfNoise(List<? extends Animal> animals) {
animals.forEach(Animal::makeNoise);
}
ここでは、コレクション内の各動物に対して
makeNoise()
を呼び出します。
これは私たちのコレクションがプロデューサーであることを意味します。私たちが
extends
を取り除けば、私たちは猫のリスト、犬、その他の動物のサブクラスを渡すことができなくなるでしょう。プロデューサーを適用することで原則が拡張され、私たちは可能な限りの柔軟性を得ます。
Consumer super
は
producer extendsとは反対の意味です。
すべての意味では、消費者が構成する要素を扱う場合は、
super
キーワードを使用する必要があります。前の例を繰り返すことでこれを実証できます。
public static void addCats(List<? super Animal> animals) {
animals.add(new Cat());
}
私たちは動物のリストに追加しているだけなので、私たちの動物のリストは消費者です。これが私たちが
super
キーワードを使う理由です。つまり、スーパークラスの動物のリストを渡すことはできますが、サブクラスは渡すことができません。たとえば、犬や猫のリストを渡してみた場合、コードはコンパイルされません。
考慮すべき最後のことは、コレクションがコンシューマとプロデューサの両方である場合に何をするかです。この例としては、要素が追加および削除されたコレクションがあります。この場合、無制限のワイルドカードを使用する必要があります。
==== * Q14。実行時にジェネリック型情報が利用できる状況はありますか?
実行時にジェネリック型が利用できる状況が1つあります。
これはジェネリック型が次のようにクラスシグネチャの一部である場合です。
public class CatCage implements Cage<Cat>
リフレクションを使用すると、この型パラメータが得られます。
(Class<T>) ((ParameterizedType) getClass()
.getGenericSuperclass()).getActualTypeArguments()[0];
このコードはやや脆いです。たとえば、直接のスーパークラスで定義されているtypeパラメータに依存します。しかし、それはJVMがこのタイプ情報を持っていることを示しています。
次
”
https://www.baeldung.com/java-flow-control-interview-questions【Java
Flow Controlのインタビューの質問(回答)]
-
«** 前へ
メモリ
Javaインタビューでの管理に関する質問(+回答)]