相关文章:

1、《夯实 JAVA 基本之一 —— 泛型详解 (1): 基本使用》
2、《夯实 JAVA 基本之一——泛型详解 (2):高级进阶》
3、《夯实 JAVA 基本之二 —— 反射(1):基本类周边信息获取》
4、《夯实 JAVA 基本之二 —— 反射(2):泛型相关周边信息获取》
5、《夯实 JAVA 基本之二 —— 反射(3):类内部信息获取》

上一篇给大家初步讲解了泛型变量的各种应用环境,这篇将更深入的讲解一下有关类型绑定,通配符方面的知识。

一、类型绑定

1、引入

我们重新看上篇写的一个泛型:

class Point<T> {
    private T x;       // 表示X坐标
    private T y;       // 表示Y坐标
 
    public void setX(T x) {
        this.x = x;
    }
 
    public void setY(T y) {
        this.y = y;
    }
 
    public T getX() {
        return this.x;
    }
 
    public T getY() {
        return this.y;
    }
}
 
//使用
Point<Integer> p1 = new Point<Integer>();
p1.setX(new Integer(100));
System.out.println(p1.getX());

首先,我们要知道一点,任何的泛型变量(比如这里的 T)都是派生自 Object,所以我们在填充泛型变量时,只能使用派生自 Object 的类,比如 String,Integer,Double,等而不能使用原始的变量类型,比如 int,double,float 等。
然后,问题来了,那在泛型类 Point<\T> 内部,利用泛型定义的变量 T x 能调用哪些函数呢?

private T x;  
 

当然只能调用 Object 所具有的函数,因为编译器根本不知道 T 具体是什么类型,只有在运行时,用户给什么类型,他才知道是什么类型。编译器唯一能确定的是,无论什么类型,都是派生自 Object 的,所以 T 肯定是 Object 的子类,所以 T 是可以调用 Object 的方法的
那么问题又来了,如果我想写一个找到最小值的泛型类;由于不知道用户会传什么类型,所以要写一个接口, 让用户实现这个接口来自已对比他所传递的类型的大小。
接口如下:

public interface Comparable<T>{
    public boolean compareTo(T i);
}

但如果我们直接利用 T 的实例来调用 compareTo()函数的话,会报错,编译器截图如下:

这是因为,编译器根本无法得知 T 是继承自 Comparable 接口的函数。那怎么样才能让编译器知道,T 是继承了 Comparable 接口的类型呢?
这就是类型绑定的作用了。

2、类型绑定:extends

(1)、定义
有时候,你会希望泛型类型只能是某一部分类型,比如操作数据的时候,你会希望是 Number 或其子类类型。这个想法其实就是给泛型参数添加一个界限。其定义形式为:

<T extends BoundingType>
 

此定义表示 T 应该是 BoundingType 的子类型(subtype)。T 和 BoundingType 可以是类,也可以是接口。另外注意的是,此处的”extends“表示的子类型,不等同于继承。
一定要非常注意的是,这里的 extends 不是类继承里的那个 extends!两个根本没有任何关联。在这里 extends 后的 BoundingType 可以是类,也可以是接口,意思是说,T 是在 BoundingType 基础上创建的,具有 BoundingType 的功能。目测是 JAVA 的开发人员不想再引入一个关键字,所以用已有的 extends 来代替而已。
(2)、实例:绑定接口
同样,我们还使用上面对比大小的接口来做例子
首先,看加上 extends 限定后的 min 函数:

public interface Comparable<T> {
    public boolean compareTo(T i);
}
//添加上extends Comparable之后,就可以Comparable里的函数了
public static <T extends Comparable>  T min(T...a){
    T smallest = a[0];
    for(T item:a){
        if (smallest.compareTo(item)){
            smallest = item;
        }
    }
    return smallest;
}

这段代码的意思就是根据传进去的 T 类型数组 a,然后调用其中 item 的 compareTo() 函数,跟每一项做对比,最终找到最小值。
从这段代码也可以看出,类型绑定有两个作用:1、对填充的泛型加以限定 2、使用泛型变量 T 时,可以使用 BoundingType 内部的函数。
这里有一点非常要注意的是,在这句中 smallest.compareTo(item),smallest 和 item 全部都是 T 类型的,也就是说,compareTo 对比的是同一种类型。
然后我们实现一个派生自 Comparable 接口的类:

public class StringCompare implements Comparable<StringCompare> {
    private String mStr;
 
    public StringCompare(String string){
        this.mStr = string;
    }
 
    @Override
    public  boolean compareTo(StringCompare str) {
        if (mStr.length() > str.mStr.length()){
            return true;
        }
        return false;
    }
}

在这段代码,大家可能会疑惑为什么把 T 也填充为 StringCompare 类型,记得我们上面说的吗:smallest.compareTo(item),smallest 和 item 是同一类型!!所以 compareTo 的参数必须是与调用者自身是同一类型,所以要把 T 填充为 StringCompare;
在这段代码中 compareTo 的实现为,对比当前 mstr 的长度与传进来实例的 mstr 长度进行比较,如果超过,则返回 true, 否则返回 false;
最后是使用 min 函数:

StringCompare result = min(new  StringCompare("123"),new StringCompare("234"),new StringCompare("59897"));
Log.d(TAG,"min:"+result.mStr);

结果如下:

这里有 extends 接口,我们开篇说过,extends 表示绑定,后面的 BindingType 即可以是接口,也可以是类,下面我们就再举个绑定类的例子。

源码在文章底部给出

(3)、实例:绑定类
我们假设,我们有很多种类的水果,需要写一个函数,打印出填充进去水果的名字:
为此,我们先建一个基类来设置和提取名字:

class Fruit {
    private String name;
 
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

然后写个泛型函数来提取名字:

public static <T extends Fruit> String getFruitName(T t){
    return t.getName();
}

这里泛型函数的用法就出来了,由于我们已知水果都会继承 Fruit 基类,所以我们利用 <\T extends Fruit> 就可以限定填充的变量必须派生自 Fruit 的子类。一来,在 T 中,我们就可以利用 Fruit 类中方法和函数;二来,如果用户填充进去的类没有派生自 Fruit,那编译器就会报错。
然后,我们新建两个类,派生自 Fruit,并填充进去它们自己的名字:

class Banana extends Fruit{
    public Banana(){
        setName("bababa");
    }
}
class Apple extends Fruit{
    public Apple(){
        setName("apple");
    }
}

最后调用:

String name_1 = getFruitName(new Banana());
String name_2 = getFruitName(new Apple());
Log.d(TAG,name_1);
Log.d(TAG,name_2);

结果如下:

源码在文章底部给出

(4)、绑定多个限定
上面我们讲了,有关绑定限定的用法,其实我们可以同时绑定多个绑定, 用 & 连接,比如:

public static <T extends Fruit&Serializable> String getFruitName(T t){
    return t.getName();
}

再加深下难度,如果我们有多个泛型,每个泛型都带绑定,那应该是什么样子的呢:

public static <T extends Comparable & Serializable, U extends Runnable> T foo(T a, U b){
	…………
}

大家应该看得懂,稍微讲一下:这里有两个泛型变量 T 和 U, 将 T 与 Comparable & Serializable 绑定,将 U 与 Runnable 绑定。
好了,这部分就讲完了,下面讲讲有关通配符的用法。

二、通配符

通配符是一个非常令人头疼的一个功能,理解与掌握难度比较大,下面我尽力去讲明白它与泛型变量的区别与用法。

1、引入

重新来看我们上篇用的 Point 泛型定义:

class Point<T> {
    private T x;      
    private T y;      
    
    public Point(){
        
    }
    public Point(T x,T y){
        this.x = x;
        this.y = y;
    }
 
    public void setX(T x) {
        this.x = x;
    }
 
    public void setY(T y) {
        this.y = y;
    }
 
    public T getX() {
        return this.x;
    }
 
    public T getY() {
        return this.y;
    }
}

这段代码很简单,引入了一个泛型变量 T,然后是有两个构造函数,最后分别是利用 set 和 get 方法来设置和获取 x,y 的值。这段代码没什么难度,不再细讲。
我们看看下面这段使用的代码:

Point<Integer> integerPoint = new Point<Integer>(3,3);
…………
Point<Float> floatPoint = new Point<Float>(4.3f,4.3f);
…………
Point<Double> doublePoint = new Point<Double>(4.3d,4.90d);
…………
Point<Long> longPoint = new Point<Long>(12l,23l);
…………

在这段代码中,我们使用 Point<\T> 生成了四个实例: integerPoint,floatPoint,doublePoint 和 longPoint;
在这里,我们生成四个实例,就得想四个名字。如果我们想生成十个不同类型的实例呢?那不得想十个名字。
光想名字就是个事,(其实我并不觉得想名字是个什么大事…… T _ T ,没办法,想不出更好的例子了…… )
那有没有一种办法,生成一个变量,可以将不同类型的实例赋值给他呢?

2、无边界通配符:?

(1)、概述
先不讲无边界通配符是什么,同样拿上面的例子来看,如果我们这样实现:

Point<?> point;
 
point = new Point<Integer>(3,3);
point = new Point<Float>(4.3f,4.3f);
point = new Point<Double>(4.3d,4.90d);
point = new Point<Long>(12l,23l);

在这里,我们首先,利用下面的代码生成一个 point 实例,注意到,在填充泛型时,用的是?

Point<?> point;
 

然后,各种类型的 Point 实例,都可以赋值给 point 了:

point = new Point<Integer>(3,3);
point = new Point<Float>(4.3f,4.3f);
point = new Point<Double>(4.3d,4.90d);
point = new Point<Long>(12l,23l);

这里的? 就是无边界通配符。通配符的意义就是它是一个未知的符号,可以是代表任意的类。
所以这里可能大家就明白了,这里不光能将泛型变量 T 填充为数值类型,其实任意 Point 实例都是可以传给 point 的:比如这里的 Point<\String>(),Point<\Object>() 都是可以的

(2)、?与 T 的区别
大家可能会有疑问,那无边界通配符?与泛型变量 T 有什么区别呢?
答案是:他们俩没有任何联系!!!!!
泛型变量 T 不能在代码用于创建变量,只能在类,接口,函数中声明以后,才能使用。
比如:

public class Box<T> {
   public T get(){
	   …………
   };
   public void put(T element){
	   …………
   };
}

而无边界通配符?则只能用于填充泛型变量 T,表示通配任何类型!!!!再重复一遍:?只能用于填充泛型变量 T。它是用来填充 T 的!!!!只是填充方式的一种!!!
比如:

//无边界通配符填充
Box<?> box;
//其它类型填充
Box<String> stringBox;

(3)、通配符只能用于填充泛型变量 T, 不能用于定义变量
大家一定要记得,通配符的使用位置只有:

Box<?> box;
box = new Box<String>();

即填充泛型变量 T 的位置,不能出现在后面 String 的位置!!!!
下面的第三行,第四行,都是错误的。通配符不能用于定义变量。

再次强调,?只能出现在 Box<?> box; 中,其它位置都是不对的。

3、通配符?的 extends 绑定

(1)、概述
从上面我们可以知道通配符?可以代表任意类型,但跟泛型一样,如果不加以限定,在后期的使用中编译器可能不会报错。所以我们同样,要对?加以限定。
绑定的形式,同样是通过 extends 关键字,意义和使用方法都用泛型变量一致。
同样,以我们上面的 Point<\T> 泛型类为例,因为 Point 在实例意义中,其中的值是数值才有意义,所以将泛型变量 T 填充为 Object 类型、String 类型等都是不正确的。
所以我们要对 Point<?> point 加以限定:只有数值类型才能赋值给 point;
我们把代码改成下面的方式:

我们给通配符加上限定: Point<? extends Number> point;
此时,最后两行,当将 T 填充为 String 和 Object 时,赋值给 point 就会报错!
这里虽然是指派生自 Number 的任意类型,但大家注意到了没: new Point<\Number>(); 也是可以成功赋值的,这说明包括边界自身。
再重复一遍:无边界通配符只是泛型 T 的填充方式,给他加上限定,只是限定了赋值给它(比如这里的 point)的实例类型。
如果想从根本上解决乱填充 Point 的问题,需要从 Point 泛型类定义时加上 <\T extends Number>:

class Point<T extends Number> {
    private T x;       // 表示X坐标
    private T y;       // 表示Y坐标
 
   …………
}

(2)注意:利用 <? extends Number> 定义的变量,只可取其中的值,不可修改
看下面的代码:

明显在 point.setX(Integer(122)); 时报编译错误。但 point.getX() 却不报错。
这是为什么呢?
首先,point 的类型是由 Point<? extends Number> 决定的,并不会因为 point = new Point<\Integer>(3,3); 而改变类型。
即便 point = new Point<\Integer>(3,3); 之后,point 的类型依然是 Point<? extends Number>,即派生自 Number 类的未知类型!!!这一点很好理解,如果在 point = new Point<\Integer>(3,3); 之后,point 就变成了 Point<\Integer > 类型,那后面 point = new Point<\Long>(12l,23l); 操作时,肯定会因为类型不匹配而报编译错误了,正因为,point 的类型始终是 Point<? extends Number>,因此能继续被各种类型实例赋值。
回到正题,现在说说为什么不能赋值
正因为 point 的类型为 Point<? extends Number> point,那也就是说,填充 Point 的泛型变量 T 的为 <? extends Number>,这是一个什么类型?未知类型!!!怎么可能能用一个未知类型来设置内部值!这完全是不合理的。
但取值时,正由于泛型变量 T 被填充为 <? extends Number>,所以编译器能确定的是 T 肯定是 Number 的子类,编译器就会用 Number 来填充 T
也就是说,编译器,只要能确定通配符类型,就会允许,如果无法确定通配符的类型,就会报错。

4、通配符?的 super 绑定

(1)、概述
如果说 <? extends XXX> 指填充为派生于 XXX 的任意子类的话,那么 <? super XXX > 则表示填充为任意 XXX 的父类!
我们先写三个类,Employee,Manager,CEO, 分别代表工人,管理者,CEO
其中 Manager 派生于 Employee,CEO 派生于 Manager, 代码如下:

class CEO extends Manager {
}
 
class Manager extends Employee {
}
 
class Employee {
}

然后,如果我这样生成一个变量:

 List<? super Manager> list;
 

它表示的意思是将泛型 T 填充为 <? super Manager>,即任意 Manager 的父类;也就是说任意将 List<\T > 中的泛型变量 T 填充为 Manager 父类的 List 变量,都可以赋值给 list;

从上面的代码中可以看出 new ArrayList<\Employee>(),new ArrayList<\Manager>() 都是正确的,而 new ArrayList<\CEO>() 却报错,当然是因为 CEO 类已经不再是 Manager 的父类了。所以会报编译错误。
这里还要注意一个地方,从代码中可以看出 new ArrayList<\Manager>() 是可以成功赋值给 List<? super Manager> list 的,可见,super 关键字也是包括边界的。即边界类型(这里是 Manager)组装的实例依然可以成功赋值。
(2)、super 通配符实例内容:能存不能取
上面我们讲了,extends 通配符,能取不能存,那 super 通配符情况又怎样呢?我们试试看:

先看存的部分:

List<? super Manager> list;
list = new ArrayList<Employee>();
//存
list.add(new Employee()); //编译错误
list.add(new Manager());
list.add(new CEO());

首先,需要声明的是,与 Point<? extends Number> point 中 point 的类型是由 Point<? extends Number > 确定的,相同的是 list 的类型是也是由 List<? super Manager> ;list 的 item 的类型始终是 <? super Manager>,即 Manager 类的任意父类,即可能是 Employee 或者 Object.
大家可能疑惑的地方在于,为什么下面这两个是正确的!而 list.add(new Employee()); 却是错误的!

list.add(new Manager());
list.add(new CEO());

因为 list 里 item 的类型是 <? super Manager>, 即 Manager 的任意父类,我们假如是 Employee,那下面这段代码大家能理解了吧:

List<Employee> list = new ArrayList<Employee>();
list.add(new Manager());
list.add(new CEO());

在这里,正因为 Manager 和 CEO 都是 Employee 的子类,在传进去 list.add() 后,会被强制转换为 Employee!
现在回过头来看这个:

List<? super Manager> list;
list = new ArrayList<Employee>();
//存
list.add(new Employee()); //编译错误
list.add(new Manager());
list.add(new CEO());

编译器无法确定 <? super Manager> 的具体类型,但唯一可以确定的是 Manager()、CEO() 肯定是<? super Manager > 的子类,所以肯定是可以 add 进去的。但 Employee 不一定是<? super Manager > 的子类,所以不能确定,不能确定的,肯定是不允许的,所以会报编译错误。
最后再来看看取:

在这段代码中,Object object = list.get(0); 是不报错的,而 Employee employee = list.get(0); 是报错的;
我们知道 list 中 item 的类型为 <? super Manager>,那编译器能肯定的是 <? super Manager > 肯定是 Manger 的父类;但不能确定,它是 Object 还是 Employee 类型。但无论是填充为 Object 还是 Employee,它必然是 Object 的子类!
所以 Object object = list.get(0); 是不报错的。因为 list.get(0); 肯定是 Object 的子类;
而编译器无法判断 list.get(0) 是不是 Employee 类型的,所以 Employee employee = list.get(0); 是报错的。
这里虽然看起来是能取的,但取出来一个 Object 类型,是毫无意义的。所以我们认为 super 通配符:能存不能取;

5、通配符?总结

总结 ? extends 和 the ? super 通配符的特征,我们可以得出以下结论:
◆ 如果你想从一个数据类型里获取数据,使用 ? extends 通配符(能取不能存)
◆ 如果你想把对象写入一个数据结构里,使用 ? super 通配符(能存不能取)
◆ 如果你既想存,又想取,那就别用通配符。

6、常见问题注意

(1)、Point 与 Point<\T> 构造泛型实例的区别
同样以 Point 泛型类为例:

class Point<T> {
    private T x;       // 表示X坐标
    private T y;       // 表示Y坐标
 
    public Point(){
 
    }
    public Point(T x,T y){
        this.x = x;
        this.y = y;
    }
 
    public void setX(T x) {
        this.x = x;
    }
 
    public void setY(T y) {
        this.y = y;
    }
 
    public T getX() {
        return this.x;
    }
 
    public T getY() {
        return this.y;
    }
}

我们来看看下面这种构造 Point 泛型实例有什么区别:

//使用Point<?>
Point<?> point1 = new Point(new Integer(23),new Integer(23));
Point<?> point2 = new Point(new String(""),new String(""));
//直接使用Point
Point point3 = new Point(new Integer(23),new Integer(23));
Point point4 = new Point(new String(""),new String(""));

上面的四行代码中,point1,point2 生成的是 Point<?>的实例,填充的是无边界通配符。而 point3 和 point4 则非常奇怪,没有了泛型的 <> 标识,直接使用 Point 生成的实例,那它填充的是什么呢?
这四行代码在编译和运行时,都没有报错,而且输出结果也一样!
那么问题就来了:

Point<?> point1 = new Point(new Integer(23),new Integer(23));
Point<?> point2 = new Point(new String(""),new String(""));

在上面的代码中,使用了无界通配符,所以能够将各种 Point 实例赋值给 Point<?> point1
而省略了泛型标识的构造方法,依然能将各种 Point 实例赋值给它:

Point point3 = new Point(new Integer(23),new Integer(23));
Point point4 = new Point(new String(""),new String(""));

这说明:构造泛型实例时,如果省略了填充类型,则默认填充为无边界通配符!
所以下面这两个是对等的:

Point point3 = new Point(new Integer(23),new Integer(23));
Point<?> point3 = new Point(new Integer(23),new Integer(23));

最后重复一遍:构造泛型实例时,如果省略了填充类型,则默认填充为无边界通配符!
好了,快累死了,这部分真是太难讲了,有关通配符捕获和编译器类型擦除的知识,就不讲了,在实际项目中基本用不到,有兴趣的同学可以自行去补充下。
下篇给大家讲下反射。

如果本文有帮到你,记得加关注哦

本文涉及源码下载地址:http://download.csdn.net/detail/harvic880925/9275551

请大家尊重原创者版权,转载请标明出处:http://blog.csdn.net/harvic880925/article/details/49883589 谢谢

参考文章:

1、《 java 泛型编程(一)》
2、《Java 泛型 — 泛型应用 — 泛型接口、泛型方法、泛型数组、泛型嵌套》
3、《Java 泛型编程最全总结》
4、《java 通配符解惑》
5、《《Java 编程思想》学习笔记 8——泛型编程高级》
6、《步步理解 JAVA 泛型编程 (三)》
7、《Java - 泛型编程 - 类型擦除 (Type Erasure)》
8、《Java 泛型 — 泛型入门》
9、《Java 泛型 — 通配符》
10、《在 Java 的泛型类型中使用通配符》
11、《Java 理论与实践: 使用通配符简化泛型使用》
12、《Java 泛型学习三 通配符》
13、《Java 泛型 — 通配符(转载)》