您的位置:首页 > 编程语言 > Java开发

Java之泛型进阶——泛型通配符

2016-03-15 21:21 260 查看

Java之泛型进阶——泛型通配符与边界

简介

主要记录泛型的边界与通配符相关,通配符上限,通配符下限。

边界

从上一篇文章中知道,为了保持向后兼容,有了泛型擦除,但是泛型的擦除绝大多数情况实际上是将类型形参用Object代替,但是有时候又想泛型擦除上限固定在一个指定的范围,即某个类或接口的子类,来允许做一些操作,就有了泛型的边界。

package org.andy.items.thkinjava.generics.generic2016.bound;

public class GenericBound {
public static void main(String[] args) {
Item<GolderFish> item = new Item<>(new GolderFish());
item.doSomething();
}
}

interface Animal{
void speek();
}

interface Fish{
void bubble();
}

/**
* 俩接口的实现类,可以符合Item类T的要求,满足其类型参数要求。
*/
class GolderFish implements Animal, Fish {
@Override
public void speek() {
System.out.println("speek something...");
}
@Override
public void bubble() {
System.out.println("bubble...");
}
}

class HoldItem<T>{
T t;
public HoldItem(T t) {
this.t = t;
}
@SuppressWarnings("unused")
public T getT() {
return t;
}
@SuppressWarnings("unused")
public void setT(T t) {
this.t = t;
}
}

//泛型的extends后如果有多个接口则使用&分开
class Item<T extends Animal & Fish> extends HoldItem<T>{
public Item(T t) {
super(t);
}

/**
* 此时的T可以视为Animal和Fish接口的实现类,可以使用其方法。
*/
public void doSomething() {
super.t.speek();
super.t.bubble();
}
}


通配符相关

主要包括无界通配符,通配符上界,通配符下界。

通配符

通配符不是用来定义泛型的,而是用来代表任何一种类型实参!自己一直困在一种误区中,就是以为通配符是可以在定义泛型类、泛型方法或者泛型接口时使用的。如
class Generic<?>{}
这种语法是错误的。


泛型中没有逻辑上的父子关系,如
List<Number>
并不是
List<Integer>
的父类。两者擦除之后都是List,所以形如

/**
* 两者并不是方法的重载。擦除之后都是同一方法,所以编译不会通过。
* 擦除之后:
*
* void m(List numbers){}
* void m(List strings){} //编译不通过,已经存在相同方法签名
*/
void m(List<Number> numbers) {

}

void m(List<String> strings) {

}


如果想让
List<Number>
逻辑上成为
List<Integer>
的父类(实际的应用场景中就是向方法传入的实际参数是方法声明的参数的子类),则可以使用泛型的通配符”?”,它表示任意一种不确定的类型。如:

/**
* 可以传入泛型为任何类型的List实现类
* 但是因为list并不知道你传入的具体会是什么类型,所以只可以使用每个元素从Object继承的方法。
*/
static void genericWildcard(List<?> list){
list.forEach(java.lang.Object::toString);
}

public static void main(String[] args) {
List<Number> list = new ArrayList<>();
genericWildcard(list);

List<Integer> list1 = new ArrayList<>();
genericWildcard(list1);

//还可以传入泛型为String类型
List<String> list2 = new ArrayList<>();
genericWildcard(list2);
}
}


通配符边界

有时候希望传入的类类型有一个指定的范围,从而可以进行一些允许的操作,这时候就是通配符边界登场的时候了。泛型的边界分两种:上界和下界。

对于通配符的部分,可以从三方面理解(以List为例,方便理解):

含义

查询

与泛型有关的操作

先看三个很简单的类:

public abstract class Animal {

public abstract void animalMethod();

@Override
public String toString() {
return "Animal";
}
}

public class Dog extends Animal {
@Override
public void animalMethod() {
System.out.println("DOG method");
}

@Override
public String toString() {
return "Dog";
}
}

public class Fish extends Animal {
@Override
public void animalMethod() {
System.out.println("Fish method");
}

@Override
public String toString() {
return "Fish";
}
}


Animal是一个抽象类,Fish,Dog是其实现。

通配符上界

extends关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类。


示例:

//list中所有的元素都是Animal或者是其子类
List<? extends Animal> list;
--------------------------------------------------------------------------------
/**
* List<? extends Animal> animals;
* 含义:表示animals集合中所有的元素都是animal或者是animal的子类。
* 查询:如方法体中所写,因为animals中所有元素都是其子类,所以可以调用其子类从animal中实现的方法。
* 增加:见方法体
*/
public static void genericUpperWildcard(List<? extends Animal> animals){
animals.forEach(Animal::animalMethod);

/*
* 下面两行都是编译错误,如果了解前面文章中提到的擦除,则很好理解。
* 编译后的方法签名参数List所用的泛型类会被转换成Animal。假如此时向List中添加的是Dog类,那么当我们使用Dog类特有的方法时
* 肯定是不存在的。所以出于类型安全问题,不允许向含有通配符下界的泛型类中添加元素——null除外,但是添加null没有任何意义。
*/
//        animals.add(new Dog());
//        animals.add(new Animal());
}

public static void main(String[] args) {
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
genericUpperWildcard(dogs);
}


通配符的下界

super关键字声明了类型的下界,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至Object。


/**
* List<? super Animal> list;
* 含义:表示list中所有的元素都是Animal类或者是其父类
* 查询:因为返回的结果并不能保证是那个当初添加的Animal类或则其父类,返回的查询结果的类型只能是Object.
* 增加:可以向list中添加任何Animal实例或者Animal子类,因为list在编译之后泛型先被擦除然后转换为Animal类,
* 此时向里添加的任何类型都是Animal类,所以调用Animal中的任何非私有方法都是允许的。
*
*/

public static void genericLowerWildcard(List<? super Animal> list) {
list.add(new Animal());
list.add(new Dog());

//        Animal animal1 = list.get(0); 编译出错,
Object animal = list.get(0);
}


泛型的通配符,尤其是边界,比较难理解,但是明白其原理之后很多问题就迎刃而解了。了解泛型的擦除,以及编译之后泛型转换成的普通Java代码是什么样的很重要。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息