首頁 >Java >java教程 >java中通配符的詳細分析(程式碼)

java中通配符的詳細分析(程式碼)

不言
不言原創
2018-09-08 16:46:062421瀏覽

這篇文章帶給大家的內容是關於java中通配符的詳細分析(程式碼),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

在本文的前面的部分已經說過了泛型類型的子類型的不相關性。但有些時候,我們希望能夠像使用普通型別一樣使用泛型型別:

◆ 向上造型一個泛型物件的參考

◆ 向下造型一個泛型物件的參考

向上造型一個泛型物件的引用

例如,假設我們有很多箱子,每個箱子裡都裝有不同的水果,我們需要找到一種方法能夠通用的處理任何一箱水果。更通俗的說法,A是B的子類型,我們需要找到一種方法能夠將C類型的實例賦給一個C類型的聲明。

為了完成這種操作,我們需要使用帶有通配符的擴充聲明,就像下面的例子裡那樣:

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

“? extends”是泛型類型的子類型相關性成為現實:Apple是Fruit的子類型,List 是List extends Fruit> 的子類型。

向下造型一個泛型物件的參考

現在我來介紹另外一種通配符:? super。如果類型B是類型A的超類型(父類型),那麼C 是 C super A> 的子類型:

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

為什麼使用萬用字元標記能行得通?

原理現在已經很明白:我們如何利用這個新的語法結構?

? extends

讓我們重新看看這第二部分使用的一個例子,其中談到了Java數組的子類型相關性:

Apple[] apples = new Apple[ 1 ];
Fruit[] fruits = apples;
fruits[ 0 ] = new Strawberry();

就像我們看到的,當你往一個宣告為Fruit陣列的Apple物件陣列加入Strawberry物件後,程式碼可以編譯,但在執行時拋出例外。

現在我們可以用萬用字元把相關的程式碼轉換成泛型:因為Apple是Fruit的子類,我們使用? extends 通配符,這樣就能將一個List物件的定義賦到一個List extends Fruit>的宣告上:

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

這次,程式碼就編譯不過去了! Java編譯器會阻止你到一個Fruit list加入strawberry。在編譯時我們就能偵測到錯誤,在執行時就不需要進行檢查來確保往清單裡加入不相容的類型了。即使你往list裡加入Fruit物件也不行:

fruits.add( new Fruit());

你沒有辦法做到這些。事實上你不能夠往一個使用了? extends的資料結構裡寫入任何的值。

原因非常的簡單,你可以這樣想:這個? extends T 通配符告訴編譯器我們在處理一個類型T的子類型,但我們不知道這個子類型究竟是什麼。因為沒辦法確定,為了確保類型安全,我們就不允許在裡面加入任何這種類型的資料。另一方面,因為我們知道,不論它是什麼類型,它總是類型T的子類型,當我們在讀取資料時,能確保得到的資料是一個T類型的實例:

Fruit get = fruits.get( 0 );

#? super

使用? super 通配符一般是什麼情況?讓我們先來看看這個:

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

我們看到fruits指向的是一個裝有Apple的某種超類別(supertype)的List。同樣的,我們也不知道究竟是什麼超類,但我們知道 Apple和任何Apple的子類別都跟它的類型相容。既然這個未知的類型就是Apple,也是GreenApple的超類,我們就可以寫入:

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

如果我們想往裡面加入Apple的超類,編譯器就會警告你:

fruits.add( new Fruit());
fruits.add( new Object());

因為我們不知道它是怎樣的超類,所以所有這樣的實例都不允許加入。

從這種形式的型別取得資料又是怎麼樣的呢?結果表明,你只能取出Object實例:因為我們不知道超類別究竟是什麼,編譯器唯一能保證的只是它是個Object,因為Object是任何Java類型的超類別。

訪問原則與PECS法則

總結? extends 和the ? super 通配符的特徵,我們可以得到以下結論:

◆ 如果你想從一個資料型別取得數據,使用? extends 通配符

◆ 如果你想把物件寫入一個資料結構裡,使用? super 通配符

##◆ 如果你既想存,又想取,那就別用通配符。

這就是Maurice Naftalin在他的《Java Generics and Collections》這本書中所說的訪問原則,以及Joshua Bloch在他的《Effective Java》這本書中所說的PECS法則。

Bloch提醒說,這PECS是指”Producer Extends, Consumer Super”,這個更容易記憶和運用。

上面的接最底下的:

The Java Tutorial

java Generics and Collections, by Maurice Naftalin and Philip Wadler

Effective Java中文版(第2版​​), 由 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中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn