ホームページ  >  記事  >  Java  >  Javaのワイルドカードの詳細な分析(コード)

Javaのワイルドカードの詳細な分析(コード)

不言
不言オリジナル
2018-09-08 16:46:062318ブラウズ

この記事では、Java のワイルドカードの詳細な分析 (コード) を紹介します。必要な方は参考にしてください。

ジェネリック型のサブタイプの無関係性については、この記事の前の部分で説明しました。しかし、場合によっては、ジェネリック型を通常の型と同じように使用できるようにしたいことがあります。

◆ ジェネリック オブジェクトへの参照のアップキャスト

◆ ジェネリック オブジェクトへの参照のダウンキャスト

ジェネリック オブジェクトのアップキャスト Quote

たとえば、次のようにします。たくさんの箱があり、各箱には異なる果物が入っており、果物の入った箱を普遍的に処理する方法を見つける必要があるとします。より一般的には、A は B のサブタイプであり、型 C のインスタンスを型 C の宣言に割り当てる方法を見つける必要があります。

これを実現するには、次の例のように、ワイルドカードを含む拡張宣言を使用する必要があります:

List<Apple> apples = new ArrayList<Apple>();
List<? extends Fruit> fruits = apples;

"? extends" はジェネリック型のサブタイプです。 依存関係が現実になります: Apple は Fruit Type のサブタイプです。 List は List のサブタイプです。

汎用オブジェクトへの参照をダウンキャストします

次に、別のワイルドカードを紹介します: ? super。タイプ B がタイプ A のスーパータイプ (親タイプ) である場合、C は C のサブタイプになります:

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 オブジェクトの定義を extends Fruit> に割り当てることができます。 ; の発言:

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 );

fruits が Apple のスーパータイプの一部を保持する List を指していることがわかります。繰り返しますが、スーパークラスが何であるかは正確にはわかりませんが、Apple と Apple のサブクラスがその型と互換性があることはわかっています。この未知の型は Apple であり、GreenApple のスーパークラスであるため、次のように書くことができます:

List<Fruit> fruits = new ArrayList<Fruit>();
List<? super Apple> = fruits;

Apple のスーパークラスを追加したい場合、コンパイラーは次のように警告します:

fruits.add( new Apple());
fruits.add( new GreenApple());

なぜなら、私たちはそれを知らないからです。そのようなインスタンスはすべて参加できません。

このタイプからデータを取得するにはどうすればよいですか?オブジェクト インスタンスを取り出すことしかできないことがわかりました。スーパークラスが何であるかわからないため、コンパイラが保証できるのはそれがオブジェクトであることだけです。オブジェクトは任意の Java 型のスーパークラスであるためです。

アクセス原則と 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类型的参数,返回一个T类型的对象。

例子

你既可以使用Java类库里提供的泛型类,也可以使用自己的泛型类。

类型安全的写入数据…

下面的这段代码是个例子,我们创建了一个List实例,然后装入一些数据:

List<String> str = new ArrayList<String>();
str.add( "Hello " );
str.add( "World." );

如果我们试图在List装入另外一种对象,编译器就会提示错误:

 str.add( 1 ); // 不能编译

类型安全的读取数据…

当我们在使用List对象时,它总能保证我们得到的是一个String对象:

 String myString = str.get( 0 );

遍历

类库中的很多类,诸如Iterator,功能都有所增强,被泛型化。List接口里的iterator()方法现在返回的是Iterator,由它的T next()方法返回的对象不需要再进行类型转换,你直接得到正确的类型。

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のワイルドカードの詳細な分析(コード)

在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 和 a List之间又是个什么关系呢?更通用些,如果类型A是类型B的子类型,那C 和 C之间是什么关系?

答案会出乎你的意料:没有任何关系。用更通俗的话,泛型类型跟其是否子类型没有任何关系。

这意味着下面的这段代码是无效的:

List<apple> apples = ...;
List<fruit> fruits = apples;</fruit></apple>

下面的同样也不允许:

List < Apple > apples;
List < Fruit > fruits = ...;
apples = fruits ;

为什么?一个苹果是一个水果,为什么一箱苹果不能是一箱水果?

在某些事情上,这种说法可以成立,但在类型(类)封装的状态和操作上不成立。如果把一箱苹果当成一箱水果会发生什么情况?

List<apple> apples = ...;
List<fruit> fruits = apples;
fruits.add( new Strawberry());</fruit></apple>

如果可以这样的话,我们就可以在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中的访问修饰符详细解析

详解Java Reference源码分析代码

以上がJavaのワイルドカードの詳細な分析(コード)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

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