この記事では主に Java ジェネリックスの使用法と型消去に関連する問題について紹介します。非常に良い基準値を持っています。以下のエディターで見てみましょう
はじめに
Java は 1.5 でジェネリックメカニズムを導入しました。ジェネリックの本質はパラメーター化された型です。つまり、変数 の型はパラメーターであり、使用時に指定されます。特定のタイプの場合。ジェネリックスはクラス、インターフェイス、メソッドに使用できます。ジェネリックスを使用すると、コードがより単純かつ安全になります。ただし、Java のジェネリックは型消去を使用するため、単なる疑似ジェネリックです。この記事では、主に「Java プログラミングの考え方」を参考に、ジェネリックスの使用方法と既存の問題点をまとめます。
このシリーズの別の 2 つの記事:基本的な使用法
ジェネリック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 のタイプの安全性が確保され、間違ったタイプが誤って渡されることが回避されます。ジェネリックメソッド
ジェネリックはクラスをターゲットにするだけでなく、メソッドを個別にジェネリックにすることもできます。例:
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 StringGenericMethod クラス自体はジェネリックではないため、そのオブジェクトを作成するときにジェネリックパラメータを使用する必要はありません。が渡されますが、そのメソッド f は汎用メソッドです。戻り値の型の前にパラメータ識別子
型消去
型消去とは
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#
などの言語では、これらは異なるクラスです。擦除带来的问题
擦除会出现一些问题,下面是一个例子:
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 サイトの他の関連記事を参照してください。