ホームページ  >  記事  >  Java  >  Java Genericsのまとめ(1) ~基本的な使い方と型消去について詳しく解説~

Java Genericsのまとめ(1) ~基本的な使い方と型消去について詳しく解説~

黄舟
黄舟オリジナル
2017-03-22 10:22:051523ブラウズ

この記事では主に Java ジェネリックスの使用法と型消去に関連する問題について紹介します。非常に良い基準値を持っています。以下のエディターで見てみましょう

はじめに

Java は 1.5 でジェネリックメカニズムを導入しました。ジェネリックの本質はパラメーター化された型です。つまり、変数 の型はパラメーターであり、使用時に指定されます。特定のタイプの場合。ジェネリックスはクラス、インターフェイス、メソッドに使用できます。ジェネリックスを使用すると、コードがより単純かつ安全になります。ただし、Java のジェネリックは型消去を使用するため、単なる疑似ジェネリックです。この記事では、主に「Java プログラミングの考え方」を参考に、ジェネリックスの使用方法と既存の問題点をまとめます。

このシリーズの別の 2 つの記事:

  • Java ジェネリックの概要 (2): ジェネリックと配列

  • Java ジェネリックの概要 (3): ワイルドカードの使用

基本的な使用法

ジェネリックclass

変数をラップするために使用されるクラス Holder がある場合、この変数の型は任意で構いません。ジェネリックが使用される前は、次のようにすることができました。

public class Holder1 {
 private Object a;
 public Holder1(Object a) {
 this.a = a;
 }
 public void set(Object a) {
 this.a = a;
 }
 public Object get(){
 return a;
 }
 public static void main(String[] args) {
 Holder1 holder1 = new Holder1("not Generic");
 String s = (String) holder1.get();
 holder1.set(1);
 Integer x = (Integer) holder1.get();
 }
}

Holder1 には、Object によって参照される変数があります。任意の型をオブジェクトにアップキャストできるため、この Holder は任意の型を受け入れることができます。それを取り出すとき、Holder はオブジェクト

object

を保存していることだけを知っているため、対応する型に強制する必要があります。 main メソッドでは、holder1 はまず String オブジェクトである

string を保存し、次に Integer オブジェクトを保存します (パラメーター 1 は自動的にボックス化されます)。 Holder から変数を取り出すときのキャストはすでに面倒です。ここでもさまざまな型を覚えておく必要があります。間違えると実行時例外が発生します。 Holder のジェネリック バージョンを見てみましょう:

public class Holder2<T> {
 private T a;
 public Holder2(T a) {
 this.a = a;
 }
 public T get() {
 return a;
 }
 public void set(T a) {
 this.a = a;
 }
 public static void main(String[] args) {
 Holder2<String> holder2 = new Holder2<>("Generic");
 String s = holder2.get();
 holder2.set("test");
 holder2.set(1);//无法编译 参数 1 不是 String 类型
 }
}

Holder2 では、変数 a はパラメータ化された型 T です。T は単なる識別子であり、他の文字も使用できます。 Holder2 オブジェクトを作成するとき、パラメータ T の型は山括弧で囲まれて渡され、このオブジェクトでは、T が出現するすべてが String に置き換えられるのと同じになります。 get が取り出すのは Object ではなく String オブジェクトなので、

型変換

は必要ありません。さらに、set を呼び出すときは String 型のみを渡すことができ、それ以外の場合はコンパイルが通過しません。これにより、holder2 のタイプの安全性が確保され、間違ったタイプが誤って渡されることが回避されます。

上記の例を通して、pan によりコードがよりシンプルかつ安全になることがわかります。ジェネリックスの導入後、一般的に使用されるコンテナ クラスなどの Java ライブラリの一部のクラスも、ジェネリックスをサポートするように書き直されました。ArrayList list = ArrayList> などのパラメータ タイプを渡します。 ;>();。

ジェネリックメソッド

ジェネリックはクラスをターゲットにするだけでなく、メソッドを個別にジェネリックにすることもできます。例:

public class GenericMethod {
 public <K,V> void f(K k,V v) {
 System.out.println(k.getClass().getSimpleName());
 System.out.println(v.getClass().getSimpleName());
 }
 public static void main(String[] args) {
 GenericMethod gm = new GenericMethod();
 gm.f(new Integer(0),new String("generic"));
 }
}

代码输出:
 Integer
 String

GenericMethod クラス自体はジェネリックではないため、そのオブジェクトを作成するときにジェネリックパラメータを使用する必要はありません。が渡されますが、そのメソッド f は汎用メソッドです。戻り値の型の前にパラメータ識別子 があります。ここには 2 つのジェネリック パラメータがあるため、複数のジェネリック パラメータが存在する可能性があることに注意してください。

上記の呼び出しの場合とは異なり、ジェネリック メソッドを呼び出すときにジェネリック パラメーターを明示的に渡す必要はありません。これは、コンパイラーがパラメーターの型推論を使用して、渡された引数の型 (ここでは整数と文字列) に基づいて K と V の型を推論するためです。

型消去

型消去とは

Javaのジェネリックスは型消去機構を使用しているため、Javaのジェネリック機能は「疑似」としか言えません。 -ジェネリック"。型消去とは何ですか?簡単に言うと、型パラメータはコンパイル時にのみ存在し、実行時には Java 仮想マシン (JVM) はジェネリックスの存在を認識しません。まず例を見てみましょう:

public class ErasedTypeEquivalence {
 public static void main(String[] args) {
 Class c1 = new ArrayList<String>().getClass();
 Class c2 = new ArrayList<Integer>().getClass();
 System.out.println(c1 == c2);
 }
}

上記のコードには、ArrayListc0f559cc8d56b43654fcbe4aa9df7b4a と ArrayListf7e83be87db5cd2d9a8a0b8117b38cd4 という 2 つの異なる ArrayList があります。私たちの意見では、それらのパラメータ化された型は異なり、1 つは整数を保存し、もう 1 つは文字列を保存します。ただし、それらの Class オブジェクトを比較すると、上記のコード出力は true になります。これは、JVM のビューではそれらが同じクラスであることを意味します。真のジェネリックスをサポートする C++ や

C#

などの言語では、これらは異なるクラスです。

ジェネリック パラメーターは最初の境界まで消去されます。たとえば、上記の Holder2 クラスでは、パラメーターの型が単一の T の場合、オブジェクトに消去されます。これは、出現するすべての T を Object に置き換えることと同じです。 。したがって、JVM の観点から見ると、保存された変数 a は依然として Object 型です。自動的に取り出される理由は、渡したパラメータの型です。これは、コンパイラがコンパイルされたバイトコード ファイルに型変換コードを挿入するため、手動で変換する必要がありません。パラメータ タイプに境界がある場合は、最初の境界まで消去します。これについては次のセクションで説明します。

擦除带来的问题

擦除会出现一些问题,下面是一个例子:

class HasF {
 public void f() {
 System.out.println("HasF.f()");
 }
}
public class Manipulator<T> {
 private T obj;
 public Manipulator(T obj) {
 this.obj = obj;
 }
 public void manipulate() {
 obj.f(); //无法编译 找不到符号 f()
 }
 public static void main(String[] args) {
 HasF hasF = new HasF();
 Manipulator<HasF> manipulator = new Manipulator<>(hasF);
 manipulator.manipulate();
 }
}

上面的 Manipulator 是一个泛型类,内部用一个泛型化的变量 obj,在 manipulate 方法中,调用了 obj 的方法 f(),但是这行代码无法编译。因为类型擦除,编译器不确定 obj 是否有 f() 方法。解决这个问题的方法是给 T 一个边界:

class Manipulator2<T extends HasF> {
 private T obj;
 public Manipulator2(T x) { obj = x; }
 public void manipulate() { obj.f(); }
}

现在 T 的类型是 1f179c3e268e631bc7ed98c5289251b7,这表示 T 必须是 HasF 或者 HasF 的导出类型。这样,调用 f() 方法才安全。HasF 就是 T 的边界,因此通过类型擦除后,所有出现 T 的

地方都用 HasF 替换。这样编译器就知道 obj 是有方法 f() 的。

但是这样就抵消了泛型带来的好处,上面的类完全可以改成这样:

class Manipulator3 {
 private HasF obj;
 public Manipulator3(HasF x) { obj = x; }
 public void manipulate() { obj.f(); }
}

所以泛型只有在比较复杂的类中才体现出作用。但是像 1f179c3e268e631bc7ed98c5289251b7 这种形式的东西不是完全没有意义的。如果类中有一个返回 T 类型的方法,泛型就有用了,因为这样会返回准确类型。比如下面的例子:

class ReturnGenericType<T extends HasF> {
 private T obj;
 public ReturnGenericType(T x) { obj = x; }
 public T get() { return obj; }
}

这里的 get() 方法返回的是泛型参数的准确类型,而不是 HasF。

类型擦除的补偿

类型擦除导致泛型丧失了一些功能,任何在运行期需要知道确切类型的代码都无法工作。比如下面的例子:

public class Erased<T> {
 private final int SIZE = 100;
 public static void f(Object arg) {
 if(arg instanceof T) {} // Error
 T var = new T(); // Error
 T[] array = new T[SIZE]; // Error
 T[] array = (T)new Object[SIZE]; // Unchecked warning
 }
}

通过 new T() 创建对象是不行的,一是由于类型擦除,二是由于编译器不知道 T 是否有默认的构造器。一种解决的办法是传递一个工厂对象并且通过它创建新的实例。

interface FactoryI<T> {
 T create();
}
class Foo2<T> {
 private T x;
 public <F extends FactoryI<T>> Foo2(F factory) {
 x = factory.create();
 }
 // ...
}
class IntegerFactory implements FactoryI<Integer> {
 public Integer create() {
 return new Integer(0);
 }
}
class Widget {
 public static class Factory implements FactoryI<Widget> {
 public Widget create() {
 return new Widget();
 }
 }
}
public class FactoryConstraint {
 public static void main(String[] args) {
 new Foo2<Integer>(new IntegerFactory());
 new Foo2<Widget>(new Widget.Factory());
 }
}

另一种解决的方法是利用模板设计模式

abstract class GenericWithCreate<T> {
 final T element;
 GenericWithCreate() { element = create(); }
 abstract T create();
}
class X {}
class Creator extends GenericWithCreate<X> {
 X create() { return new X(); }
 void f() {
 System.out.println(element.getClass().getSimpleName());
 }
}
public class CreatorGeneric {
 public static void main(String[] args) {
 Creator c = new Creator();
 c.f();
 }
}

具体类型的创建放到了子类继承父类时,在 create 方法中创建实际的类型并返回。

总结

本文介绍了 Java 泛型的使用,以及类型擦除相关的问题。一般情况下泛型的使用比较简单,但是某些情况下,尤其是自己编写使用泛型的类或者方法时要注意类型擦除的问题。接下来会介绍数组与泛型的关系以及通配符的使用。

以上がJava Genericsのまとめ(1) ~基本的な使い方と型消去について詳しく解説~の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。