この記事では、Java のワイルドカードの詳細な分析 (コード) を紹介します。必要な方は参考にしてください。
ジェネリック型のサブタイプの無関係性については、この記事の前の部分で説明しました。しかし、場合によっては、ジェネリック型を通常の型と同じように使用できるようにしたいことがあります。
◆ ジェネリック オブジェクトへの参照のアップキャスト
◆ ジェネリック オブジェクトへの参照のダウンキャスト
ジェネリック オブジェクトのアップキャスト Quote
たとえば、次のようにします。たくさんの箱があり、各箱には異なる果物が入っており、果物の入った箱を普遍的に処理する方法を見つける必要があるとします。より一般的には、A は B のサブタイプであり、型 C のインスタンスを型 C の宣言に割り当てる方法を見つける必要があります。
これを実現するには、次の例のように、ワイルドカードを含む拡張宣言を使用する必要があります:
List<Apple> apples = new ArrayList<Apple>(); List<? extends Fruit> fruits = apples;
"? extends" はジェネリック型のサブタイプです。 依存関係が現実になります: Apple は Fruit Type のサブタイプです。 List
汎用オブジェクトへの参照をダウンキャストします
次に、別のワイルドカードを紹介します: ? super。タイプ B がタイプ A のスーパータイプ (親タイプ) である場合、C は C< スーパー A> のサブタイプになります:
List<Fruit> fruits = new ArrayList<Fruit>(); List<? super Apple> = fruits;
ワイルドカード マーカーの使用はなぜ機能しますか?
原理はもう明らかです: この新しい文法構造をどのように活用するか?
? extends
Java 配列のサブタイプの依存関係について説明する、この第 2 部で使用した例をもう一度見てみましょう:
Apple[] apples = new Apple[ 1 ]; Fruit[] fruits = apples; fruits[ 0 ] = new Strawberry();
ご覧のとおり、Strawberry オブジェクトを追加した後、Fruit として宣言されたオブジェクトに移動すると、 Apple オブジェクト配列の配列の場合、コードはコンパイルできますが、実行時に例外がスローされます。
これで、ワイルドカードを使用して関連コードをジェネリックに変換できます。Apple は Fruit のサブクラスであるため、List
List<Apple> apples = new ArrayList<Apple>(); List<? extends Fruit> fruits = apples; fruits.add( new Strawberry());
今回はコードをコンパイルできません! Java コンパイラにより、フルーツ リストにイチゴを追加できなくなります。コンパイル時にエラーを検出できるため、実行時に互換性のない型がリストに追加されていないかどうかをチェックする必要はありません。 Fruit オブジェクトをリストに追加しても、それは機能しません:
fruits.add( new Fruit());
これはできません。実際、extends を使用するデータ構造に値を書き込むことはできません。
理由は非常に簡単で、次のように考えることができます: この ? extends T ワイルドカードは、型 T のサブタイプを扱っていることをコンパイラーに伝えますが、このサブタイプが何であるかはわかりません。確認する方法がないため、型の安全性を確保するために、この型のデータを追加することは許可されていません。一方、それがどのような型であっても、それは常に型 T のサブタイプであることがわかっているため、データを読み取るときに、取得したデータが型 T のインスタンスであることを確認できます。 super
super ワイルドカードを使用する場合の一般的な状況は何ですか?まずこれを見てみましょう:
Fruit get = fruits.get( 0 );
List<Fruit> fruits = new ArrayList<Fruit>(); List<? super Apple> = fruits;
fruits.add( new Apple()); fruits.add( new GreenApple());
アクセス原則と PECS ルール
? extends と ? スーパー ワイルドカードの特徴をまとめると、次の結論を導き出すことができます:
◆ データ型からデータを取得したい場合は、? extends ワイルドカードを使用します。◆ オブジェクトをデータ構造に書き込む場合は、? スーパー ワイルドカードを使用します
◆ 保存と取得の両方を行う場合は、ワイルドカードを使用しないでください。
これは、Maurice Naftalin が著書『Java Generics and Collections』でアクセス原則と呼んでいるものであり、Joshua Bloch が著書『Effective Java』で PECS ルールと呼んでいるものです。
ブロック氏は、PECS は「Producer Extends, Consumer Super」の略であり、覚えやすく使いやすいことを思い出させました。
上記は下から続きます:
The Java Tutorial
java Generics and Collections、Maurice Naftalin および Philip Wadler 著
Effective Java Chinese Edition (2nd Edition)、Joshua Bloch 著
尽管有这么多丰富的资料,有时我感觉,有很多的程序员仍然不太明白Java泛型的功用和意义。这就是为什么我想使用一种最简单的形式来总结一下程序员需要知道的关于Java泛型的最基本的知识。
Java泛型由来的动机
理解Java泛型最简单的方法是把它看成一种便捷语法,能节省你某些Java类型转换(casting)上的操作:
List<Apple> box = ...; Apple apple = box.get( 0 );
上面的代码自身已表达的很清楚:box是一个装有Apple对象的List。get方法返回一个Apple对象实例,这个过程不需要进行类型转换。没有泛型,上面的代码需要写成这样:
List box = ...; Apple apple = (Apple) box.get( 0 );
很明显,泛型的主要好处就是让编译器保留参数的类型信息,执行类型检查,执行类型转换操作:编译器保证了这些类型转换的绝对无误。
相对于依赖程序员来记住对象类型、执行类型转换——这会导致程序运行时的失败,很难调试和解决,而编译器能够帮助程序员在编译时强制进行大量的类型检查,发现其中的错误。
泛型的构成
由泛型的构成引出了一个类型变量的概念。根据Java语言规范,类型变量是一种没有限制的标志符,产生于以下几种情况:
◆ 泛型类声明
◆ 泛型接口声明
◆ 泛型方法声明
◆ 泛型构造器(constructor)声明
泛型类和接口
如果一个类或接口上有一个或多个类型变量,那它就是泛型。类型变量由尖括号界定,放在类或接口名的后面:
public interface List<T> extends Collection<T> { ... }
简单的说,类型变量扮演的角色就如同一个参数,它提供给编译器用来类型检查的信息。
Java类库里的很多类,例如整个Collection框架都做了泛型化的修改。例如,我们在上面的第一段代码里用到的List接口就是一个泛型类。在那段代码里,box是一个List对象,它是一个带有一个Apple类型变量的List接口的类实现的实例。编译器使用这个类型变量参数在get方法被调用、返回一个Apple对象时自动对其进行类型转换。
实际上,这新出现的泛型标记,或者说这个List接口里的get方法是这样的:
T get( int index);
get方法实际返回的是一个类型为T的对象,T是在List
泛型方法和构造器(Constructor)
非常的相似,如果方法和构造器上声明了一个或多个类型变量,它们也可以泛型化。
public static <t> T getFirst(List<T> list)
这个方法将会接受一个List
例子
你既可以使用Java类库里提供的泛型类,也可以使用自己的泛型类。
类型安全的写入数据…
下面的这段代码是个例子,我们创建了一个List
List<String> str = new ArrayList<String>(); str.add( "Hello " ); str.add( "World." );
如果我们试图在List
str.add( 1 ); // 不能编译
类型安全的读取数据…
当我们在使用List
String myString = str.get( 0 );
遍历
类库中的很多类,诸如Iterator
for (Iterator<String> iter = str.iterator(); iter.hasNext();) { String s = iter.next(); System.out.print(s); }
使用foreach
“for each”语法同样受益于泛型。前面的代码可以写出这样:
for (String s: str) { System.out.print(s); }
这样既容易阅读也容易维护。
自动封装(Autoboxing)和自动拆封(Autounboxing)
在使用Java泛型时,autoboxing/autounboxing这两个特征会被自动的用到,就像下面的这段代码:
List<Integer> ints = new ArrayList<Integer>(); ints.add( 0 ); ints.add( 1 ); int sum = 0 ; for ( int i : ints) { sum += i; }
然而,你要明白的一点是,封装和解封会带来性能上的损失,所有,通用要谨慎的使用。
子类型
在Java中,跟其它具有面向对象类型的语言一样,类型的层级可以被设计成这样:
在Java中,类型T的子类型既可以是类型T的一个扩展,也可以是类型T的一个直接或非直接实现(如果T是一个接口的话)。因为“成为某类型的子类型”是一个具有传递性质的关系,如果类型A是B的一个子类型,B是C的子类型,那么A也是C的子类型。在上面的图中:
◆ FujiApple(富士苹果)是Apple的子类型
◆ Apple是Fruit(水果)的子类型
◆ FujiApple(富士苹果)是Fruit(水果)的子类型
所有Java类型都是Object类型的子类型。
B类型的任何一个子类型A都可以被赋给一个类型B的声明:
Apple a = ...; Fruit f = a;
泛型类型的子类型
如果一个Apple对象的实例可以被赋给一个Fruit对象的声明,就像上面看到的,那么,List
答案会出乎你的意料:没有任何关系。用更通俗的话,泛型类型跟其是否子类型没有任何关系。
这意味着下面的这段代码是无效的:
List<Apple> apples = ...; List<Fruit> fruits = apples;
下面的同样也不允许:
List < Apple > apples; List < Fruit > fruits = ...; apples = fruits ;
为什么?一个苹果是一个水果,为什么一箱苹果不能是一箱水果?
在某些事情上,这种说法可以成立,但在类型(类)封装的状态和操作上不成立。如果把一箱苹果当成一箱水果会发生什么情况?
List<Apple> apples = ...; List<Fruit> fruits = apples; fruits.add( new Strawberry());
如果可以这样的话,我们就可以在list里装入各种不同的水果子类型,这是绝对不允许的。
另外一种方式会让你有更直观的理解:一箱水果不是一箱苹果,因为它有可能是一箱另外一种水果,比如草莓(子类型)。
这是一个需要注意的问题吗?
应该不是个大问题。而程序员对此感到意外的最大原因是数组和泛型类型上用法的不一致。对于泛型类型,它们和类型的子类型之间是没什么关系的。而对于数组,它们和子类型是相关的:如果类型A是类型B的子类型,那么A[]是B[]的子类型:
Apple[] apples = ...; Fruit[] fruits = apples;
可是稍等一下!如果我们把前面的那个议论中暴露出的问题放在这里,我们仍然能够在一个apple类型的数组中加入strawberrie(草莓)对象:
Apple[] apples = new Apple[ 1 ]; Fruit[] fruits = apples; fruits[ 0 ] = new Strawberry();
这样写真的可以编译,但是在运行时抛出ArrayStoreException异常。因为数组的这特点,在存储数据的操作上,Java运行时需要检查类型的兼容性。这种检查,很显然,会带来一定的性能问题,你需要明白这一点。
重申一下,泛型使用起来更安全,能“纠正”Java数组中这种类型上的缺陷。
现在估计你会感到很奇怪,为什么在数组上会有这种类型和子类型的关系,我来给你一个《Java Generics and Collections》这本书上给出的答案:如果它们不相关,你就没有办法把一个未知类型的对象数组传入一个方法里(不经过每次都封装成 Object[]),就像下面的:
void sort(Object[] o);
泛型出现后,数组的这个个性已经不再有使用上的必要了(下面一部分我们会谈到这个),实际上是应该避免使用
相关推荐:
以上がJavaのワイルドカードの詳細な分析(コード)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。