Javaのジェネリクスの説明:利点、例、およびベストプラクティス
紹介
Javaのジェネリクス<&47;strong>は、Java 5で導入された最も重要な機能の一つです。Javaコレクションやバージョン5以上で作業している場合、必ず使用したことがあるでしょう。Javaのジェネリクス<&47;strong>はコレクションクラスで非常に簡単ですが、コレクションの型を作成する以上の多くの機能を提供します。この記事では、ジェネリクスの機能を学んでいきます。
Javaのジェネリクス
ジェネリクスはJava 5で追加され、コンパイル時の型チェックを提供し、コレクションクラスを扱う際に一般的だったClassCastException
のリスクを取り除きました。コレクションフレームワーク全体が型安全のためにジェネリクスを使用するように書き直されました。ジェネリクスがコレクションクラスを安全に使用するのにどのように役立つか見てみましょう。
List list = new ArrayList();
list.add("abc");
list.add(new Integer(5)); //OK
for(Object obj : list){
//type casting leading to ClassCastException at runtime
String str=(String) obj;
}
上記のコードは正常にコンパイルされますが、リスト内のObjectをStringにキャストしようとするため、実行時にClassCastExceptionが発生します。要素の1つがInteger型であるためです。Java 5以降は、以下のようにコレクションクラスを使用します。
List<String> list1 = new ArrayList<String>(); // java 7 ? List<String> list1 = new ArrayList<>();
list1.add("abc");
//list1.add(new Integer(5)); //compiler error
for(String str : list1){
//no type casting needed, avoids ClassCastException
}
リスト作成時に、リスト内の要素の型をStringと指定したことに注意してください。したがって、リストに他の型のオブジェクトを追加しようとすると、プログラムはコンパイル時エラーをスローします。また、forループではリスト内の要素の型キャストが不要であるため、実行時のClassCastExceptionを回避できます。
Javaジェネリッククラス
私たちはジェネリック型を使って独自のクラスを定義できます。ジェネリック型とは、型に対してパラメータ化されたクラスまたはインターフェースのことです。型パラメータを指定するために角括弧(<>)を使用します。利点を理解するために、次のようなシンプルなクラスがあるとしましょう:
package com.journaldev.generics;
public class GenericsTypeOld {
private Object t;
public Object get() {
return t;
}
public void set(Object t) {
this.t = t;
}
public static void main(String args[]){
GenericsTypeOld type = new GenericsTypeOld();
type.set("Pankaj");
String str = (String) type.get(); //type casting, error prone and can cause ClassCastException
}
}
このクラスを使用する際には、型キャストを使用する必要があり、実行時にClassCastExceptionを引き起こす可能性があることに注意してください。次に、以下に示すように、javaのジェネリッククラスを使用して同じクラスを書き直します。
package com.journaldev.generics;
public class GenericsType<T> {
private T t;
public T get(){
return this.t;
}
public void set(T t1){
this.t=t1;
}
public static void main(String args[]){
GenericsType<String> type = new GenericsType<>();
type.set("Pankaj"); //valid
GenericsType type1 = new GenericsType(); //raw type
type1.set("Pankaj"); //valid
type1.set(10); //valid and autoboxing support
}
}
メインメソッドでのGenericsType<&47;code>クラスの使用に注意してください。型キャストを行う必要がなく、実行時にClassCastException<&47;code>エラーを取り除くことができます。作成時に型を指定しない場合、コンパイラは「GenericsTypeは生の型です。ジェネリック型GenericsType<T><&47;code>への参照はパラメータ化されるべきです」と警告します。型を指定しないと、型はObject<&47;code>になり、StringとIntegerの両方のオブジェクトを許可します。しかし、生の型で作業する際には型キャストを使用しなければならないため、これを避けるように常に努めるべきです。
ヒント: コンパイラ警告を抑制するために@SuppressWarnings("rawtypes")<&47;code>アノテーションを使用できます。Javaアノテーションチュートリアルをチェックしてください。
また、Javaのオートボクシングをサポートしていることに注意してください。
Javaジェネリックインターフェース
Comparableインターフェースは、インターフェースにおけるジェネリクスの優れた例であり、次のように記述されます:
package java.lang;
import java.util.*;
public interface Comparable<T> {
public int compareTo(T o);
}
同様に、Javaでジェネリックインターフェースを作成できます。また、Mapインターフェースのように複数の型パラメータを持つこともできます。再び、パラメータ化された型にパラメータ化された値を提供することも可能で、例えばnew HashMap<String, List<String>>();<&47;code>は有効です。
Javaのジェネリック型
Javaのジェネリック型命名規則は、コードを簡単に理解するのに役立ち、命名規則を持つことはJavaプログラミング言語のベストプラクティスの一つです。したがって、ジェネリクスにも独自の命名規則があります。通常、型パラメータ名は単一の大文字の文字であり、Javaの変数と区別しやすくなっています。最も一般的に使用される型パラメータ名は次のとおりです。
- E - 要素(Javaコレクションフレームワークで広く使用されている、例えばArrayList、Setなど)
- K - キー(地図で使用)
- N - 数
- T - タイプ
- V - 値(マップで使用)
- S、U、Vなど - 2番目、3番目、4番目のタイプ
Javaのジェネリックメソッド
時には、クラス全体をパラメータ化したくない場合があります。その場合、Javaのジェネリクスメソッドを作成できます。コンストラクタは特別な種類のメソッドであるため、コンストラクタでもジェネリック型を使用できます。以下は、Javaのジェネリックメソッドの例を示すクラスです。
package com.journaldev.generics;
public class GenericsMethods {
//Java Generic Method
public static <T> boolean isEqual(GenericsType<T> g1, GenericsType<T> g2){
return g1.get().equals(g2.get());
}
public static void main(String args[]){
GenericsType<String> g1 = new GenericsType<>();
g1.set("Pankaj");
GenericsType<String> g2 = new GenericsType<>();
g2.set("Pankaj");
boolean isEqual = GenericsMethods.<String>isEqual(g1, g2);
//above statement can be written simply as
isEqual = GenericsMethods.isEqual(g1, g2);
//This feature, known as type inference, allows you to invoke a generic method as an ordinary method, without specifying a type between angle brackets.
//Compiler will infer the type that is needed
}
}
isEqual<&47;em>メソッドのシグネチャが、メソッドでジェネリック型を使用するための構文を示していることに注意してください。また、これらのメソッドを私たちのJavaプログラムでどのように使用するかにも注意してください。これらのメソッドを呼び出す際に型を指定することもできますし、通常のメソッドのように呼び出すこともできます。Javaコンパイラは、使用される変数の型を判断するのに十分賢いです。この機能は型推論<&47;strong>と呼ばれます。
Javaのジェネリクスの境界付き型パラメータ
パラメータ化された型で使用できるオブジェクトの種類を制限したいとします。たとえば、2つのオブジェクトを比較するメソッドで、受け入れられるオブジェクトがComparableであることを確認したい場合です。制約付き型パラメータを宣言するには、型パラメータの名前をリストし、次にextendsキーワードを続け、その上限を示します。以下のメソッドのように。
public static <T extends Comparable<T>> int compare(T t1, T t2){
return t1.compareTo(t2);
}
これらのメソッドの呼び出しは、無制限メソッドに似ていますが、Comparableでないクラスを使用しようとすると、コンパイル時エラーが発生します。制約付き型パラメータは、メソッドだけでなく、クラスやインターフェースでも使用できます。Javaのジェネリクスは、複数の制約もサポートしています。すなわち、
Javaのジェネリクスと継承
Javaの継承により、AがBのサブクラスである場合、変数Aを別の変数Bに割り当てることができることはわかっています。したがって、Aの任意のジェネリック型をBのジェネリック型に割り当てることができると考えるかもしれませんが、そうではありません。簡単なプログラムでこれを見てみましょう。
package com.journaldev.generics;
public class GenericsInheritance {
public static void main(String[] args) {
String str = "abc";
Object obj = new Object();
obj=str; // works because String is-a Object, inheritance in java
MyClass<String> myClass1 = new MyClass<String>();
MyClass<Object> myClass2 = new MyClass<Object>();
//myClass2=myClass1; // compilation error since MyClass<String> is not a MyClass<Object>
obj = myClass1; // MyClass<T> parent is Object
}
public static class MyClass<T>{}
}
MyClass<String><&47;code> 変数を MyClass<Object><&47;code> 変数に割り当てることは許可されていません。なぜなら、これらは関連しておらず、実際には MyClass<T><&47;code> の親は Object だからです。
Javaのジェネリッククラスとサブタイピング
一般的なクラスやインターフェースは、それを拡張または実装することでサブタイプ化できます。一つのクラスまたはインターフェースの型パラメータと別の型パラメータとの関係は、extendsおよびimplementsの条項によって決まります。例えば、ArrayList<E><&47;code>はimplements List<E><&47;code>し、extends Collection<E><&47;code>ですので、ArrayList<String><&47;code>はList<String><&47;code>のサブタイプであり、List<String><&47;code>はCollection<String><&47;code>のサブタイプです。型引数を変更しない限り、サブタイピングの関係は保持されます。以下は複数の型パラメータの例を示しています。
interface MyList<E,T> extends List<E>{
}
List<String><&47;code>のサブタイプには、MyList<String,Object><&47;code>、MyList<String,Integer><&47;code>などがあります。
Javaのジェネリクスワイルドカード
疑問符(?<&47;code>)はジェネリクスにおけるワイルドカードであり、未知の型を表します。ワイルドカードは、パラメータ、フィールド、またはローカル変数の型として使用でき、時には戻り値の型としても使用されます。ジェネリックメソッドを呼び出したり、ジェネリッククラスをインスタンス化したりする際にワイルドカードを使用することはできません。次のセクションでは、上限境界ワイルドカード、下限境界ワイルドカード、およびワイルドカードキャプチャについて学びます。
Javaジェネリクスの上限境界ワイルドカード
上限境界ワイルドカードは、メソッド内の変数の型に対する制限を緩和するために使用されます。リスト内の数値の合計を返すメソッドを書きたいと仮定すると、私たちの実装はこのようになります。
public static double sum(List<Number> list){
double sum = 0;
for(Number n : list){
sum += n.doubleValue();
}
return sum;
}
上記の実装の問題は、整数または倍精度浮動小数点数のリストでは機能しないことです。なぜなら、List<Integer><&47;code>とList<Double><&47;code>は関連していないからです。この場合、上限境界のワイルドカードが役立ちます。extends<&47;strong>キーワードと上限<&47;strong>クラスまたはインターフェースを使用して、上限またはそのサブクラスの型の引数を渡すことができます。上記の実装は、以下のプログラムのように修正できます。
package com.journaldev.generics;
import java.util.ArrayList;
import java.util.List;
public class GenericsWildcards {
public static void main(String[] args) {
List<Integer> ints = new ArrayList<>();
ints.add(3); ints.add(5); ints.add(10);
double sum = sum(ints);
System.out.println("Sum of ints="+sum);
}
public static double sum(List<? extends Number> list){
double sum = 0;
for(Number n : list){
sum += n.doubleValue();
}
return sum;
}
}
インターフェースの観点からコードを書くことに似ており、上記のメソッドでは上限クラスNumberのすべてのメソッドを使用できます。上限付きリストでは、null以外のオブジェクトをリストに追加することは許可されていないことに注意してください。sumメソッド内でリストに要素を追加しようとすると、プログラムはコンパイルされません。
Javaジェネリクスの無制限ワイルドカード
時には、すべての型で動作する汎用メソッドが必要な状況があります。この場合、制約のないワイルドカードを使用できます。それは、<? extends Object><&47;code>を使用するのと同じです。
public static void printData(List<?> list){
for(Object obj : list){
System.out.print(obj + "::");
}
}
List<String><&47;code>やList<Integer><&47;code>、または他の任意の型のオブジェクトリスト引数をprintData<&47;em>メソッドに提供できます。上限リストと同様に、リストに何かを追加することは許可されていません。
Javaのジェネリクスの下限ワイルドカード
整数のリストに整数を追加したいと仮定すると、引数の型をList<Integer><&47;code>のままにしておくと整数に縛られてしまいますが、List<Number><&47;code>やList<Object><&47;code>は整数も保持できるため、下限ワイルドカードを使用してこれを達成できます。これを達成するために、super<&47;strong>キーワードと下限クラスを使用したジェネリクスワイルドカード(?)を使用します。下限または下限の任意のスーパタイプを引数として渡すことができ、この場合、Javaコンパイラはリストに下限オブジェクトタイプを追加することを許可します。
public static void addIntegers(List<? super Integer> list){
list.add(new Integer(50));
}
ジェネリクスワイルドカードを使用したサブタイピング
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // OK. List<? extends Integer> is a subtype of List<? extends Number>
Javaのジェネリクスタイプ消去
Javaのジェネリクスは、コンパイル時に型チェックを提供するために追加され、実行時には使用されないため、Javaコンパイラは型消去機能を使用して、バイトコード内のすべてのジェネリクスタイプチェックコードを削除し、必要に応じて型キャストを挿入します。型消去は、パラメータ化された型のために新しいクラスが作成されないことを保証します。その結果、ジェネリクスは実行時のオーバーヘッドを発生させません。例えば、以下のようなジェネリッククラスがあるとします;
public class Test<T extends Comparable<T>> {
private T data;
private Test<T> next;
public Test(T d, Test<T> n) {
this.data = d;
this.next = n;
}
public T getData() { return this.data; }
}
Javaコンパイラは、制約付き型パラメータT<&47;code>を、以下のコードのように最初の制約インターフェースであるComparableに置き換えます。
public class Test {
private Comparable data;
private Test next;
public Node(Comparable d, Test n) {
this.data = d;
this.next = n;
}
public Comparable getData() { return data; }
}
ジェネリクスに関する一般的な間違い
経験豊富な開発者でさえ、ジェネリクスを使用する際に間違いを犯すことがあります。ここにいくつかの一般的な落とし穴があります:
1. 生の型を使用する
生の型を使用することは、ジェネリクスの目的を無にします。
List list = new ArrayList(); // Avoid this
list.add("Hello");
list.add(123); // No type safety
代わりに、パラメータ化された型を使用してください。
List<String> list = new ArrayList<>();
2. ジェネリクスとレガシーコードの混合
一般的なコードと非一般的なコードを混合することは避けてください。予期しない問題を引き起こす可能性があります。
3. ワイルドカードの誤った使用
例 :
public void addItem(List<? extends Number> list) {
// list.add(10); // Compilation error
}
代わりに、修正が必要な場合は、? super T<&47;code> を使用してください。
public void addItem(List<? super Integer> list) {
list.add(10);
}
4. ワイルドカードの過剰使用
ワイルドカードは柔軟性を向上させますが、不必要に使用するとコードが理解しにくくなる可能性があります。
よくある質問
1. Javaにおけるジェネリクスとは何ですか?
ジェネリクスを使用すると、型パラメータを持つクラスやメソッドを定義できます。例えば:
class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
ここで、T<&47;code>は、実行時に任意の具体的な型に置き換えられる型パラメータです。
2. Javaでジェネリクスを使用する理由は?
型安全性<&47;strong>: 実行時のClassCastExceptionを防ぎます。
コードの再利用性<&47;strong>: 異なるタイプで機能する単一のクラスまたはメソッドを書くことを可能にします。
可読性の向上<&47;strong>: 過度なキャスティングを避け、コードをクリーンにします。
Javaの基本をより深く理解するために、Java EEチュートリアルをチェックしてください。
3. 一般的なメソッドの例を挙げてもらえますか?
ジェネリックメソッドは型パラメータを取ることができます:
public static <T> void printArray(T[] elements) {
for (T element : elements) {
System.out.println(element);
}
}
この方法は、任意の型の配列の要素を印刷します。
4. JavaにおけるList<?><&47;code>の意味は何ですか?
?<&47;code>はJavaのジェネリクスにおけるワイルドカード<&47;strong>です。それは未知の型を表します。例えば:
List<?> list = new ArrayList<String>();
これは、リストが任意の型を保持できることを意味しますが、その正確な型はコンパイル時には不明です。このチュートリアルでは、JavaのComparableとComparatorについて詳しく学ぶことができます。
5. Javaのジェネリクスにおけるワイルドカードの理解
ワイルドカードには主に2つのタイプがあります。
? extends T<&47;code>は、T<&47;code>のサブクラスである不明な型を表します。
public void processElements(List<? extends Number> numbers) { for (Number num : numbers) { System.out.println(num); } }
? super T<&47;code>: T<&47;code>のスーパークラスである未知の型を表します。
public void addNumbers(List<? super Integer> numbers) { numbers.add(42); }
ワイルドカードは、型安全性を維持しながら柔軟性を提供します。
6. Javaでジェネリクスの配列を作成できますか?
いいえ、Javaでは型消去のためにジェネリック配列の作成は許可されていません。これによりClassCastException<&47;code>エラーが発生します。代わりに、List<T><&47;code>を使用できます。
List<String> list = new ArrayList<>();
Javaの型アノテーションの詳細については、Javaアノテーションに関するチュートリアルを参照してください。
7. ジャバにおけるジェネリクスはなぜ安全なのか?
ジェネリクスはコンパイル時に型チェックを強制し、ランタイムエラーの可能性を減少させます。明示的なキャストの必要性を排除し、コードをより読みやすく、保守しやすくします。
8. Javaにおけるジェネリクスのルールは何ですか?
ジェネリック型のインスタンスを作成できません。
class Box<T> { // T obj = new T(); // Illegal }
ジェネリック型の静的メンバーはありません。
class Box<T> { // static T instance; // Illegal }
プリミティブを型パラメータとして使用することはできません(代わりにラッパークラスを使用してください)。
// List<int> list = new ArrayList<>(); // Illegal List<Integer> list = new ArrayList<>();
結論
Javaのジェネリクスは、型安全性を確保し、コードの再利用性を促進するための堅牢なメカニズムを提供します。ワイルドカードのニュアンスを理解し、一般的な落とし穴を避け、ベストプラクティスに従うことで、より保守性が高く、効率的でエラーのないJavaアプリケーションを作成できます。
Javaに関連するトピックについては、Java EEチュートリアルやComparableおよびComparatorの例に関するチュートリアルを参照できます。