1、概念
泛型的本质是参数化类型,即给类型指定一个参数,然后在使用时再指定此参数具体的值,那样这个类型就可以在使用时决定了。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
Java中引入泛型最主要的目的是将类型检查工作提前到编译时期,将类型强转(cast)工作交给编译器,从而让你在编译时期就获得类型转换异常以及去掉源码中的类型强转代码。
2、泛型作用的对象
泛型有三种使用方式,分别为:泛型类、泛型接口和泛型方法。
1、泛型类
在类的申明时指定参数,即构成了泛型类,例如下面代码中就指定T
为类型参数,那么在这个类里面就可以使用这个类型了。例如申明T
类型的变量name
,申明T
类型的形参param
等操作。
public class Generic<T> {public T name;public Generic(T param){name=param;}public T m(){return name;}
}
那么在使用类时就可以传入相应的类型,构建不同类型的实例,如下面代码分别传入了String
,Integer
,Boolean
3个类型:
private static void genericClass(){Generic<String> str=new Generic<>("总有刁民想害朕");Generic<Integer> integer=new Generic<>(110);Generic<Boolean> b=new Generic<>(true);
System.out.println("传入类型:"+str.name+" "+integer.name+" "+b.name);
}
输出结果为:传入类型:总有刁民想害朕 110 true
如果没有泛型,我们想要达到上面的效果需要定义三个类,或者一个包含三个构造函数,三个取值方法的类。
2、泛型接口
泛型接口与泛型类的定义基本一致
3、泛型方法
public class Generic<T> {public T name;public Generic(){}public Generic(T param){name=param;}public T m(){return name;}public <E> void m1(E e){ }public <T> T m2(T e){ }
}
重点看public <E> void m1(E e){ }
这就是一个泛型方法,判断一个方法是否是泛型方法关键看方法返回值前面有没有使用<>
标记的类型,有就是,没有就不是。这个<>
里面的类型参数就相当于为这个方法声明了一个类型,这个类型可以在此方法的作用块内自由使用。
上面代码中,m()
方法不是泛型方法,m1()
与m2()
都是。值得注意的是m2()
方法中声明的类型T
与类申明里面的那个参数T
不是一个,也可以说方法中的T
隐藏了类型中的T
。下面代码中类里面的T
传入的是String
类型,而方法中的T
传入的是Integer
类型。
Generic<String> str=new Generic<>("总有刁民想害朕");
str.m2(123);
3、类型通配符
首先思考以下问题
public class Animal {
}
public class Cat extends Animal {
}
public static void main(String[] args) {Cat cat = new Cat(); Animal animal = cat; ❶List<Cat> cats = new ArrayList<>();List<Animal> animals = cats; ❷
}
以上代码中①处以及②处代码是否正确?
①处的代码没有任何问题,是多态的典型用法,即父类引用指向子类对象
,而②处的代码与①处类似,但是却提示编译错误。原因是因为Java泛型中规定,即使泛型类型具有继承关系,但是并不意味着该泛型类型的容器也具有继承关系。
可以把泛型理解成一个标签,一个list中贴上了animal的标签,另一个list贴上了cat的标签,可以说cat是animal的子类,但是不能说贴了cat标签的list就是贴了animal标签list的子类,他们实际上都是list。
知道泛型的这一特性后,就出现了一个比较麻烦的问题: 泛型容器似乎没办法实现多态。
举个例子,在上例中我们为Animal添加一个方法getFood,同时Cat类需要重写该方法
public class Animal {public String getFood() {return "食肉动物吃肉,食草动物吃草";}
}
public class Cat implements Animal {@Overridepublic String getFood() {return "猫粮,小鱼干,猫薄荷";}
}
如果想在控制台输出不同动物的食物,此时需要定一个print方法,利用多态特性,形参可定义为父类Animal,这样实参就可以传Animal以及其子类对象了。
public void print(Animal animal) {System.out.println(animal.getFood());
}
public static void main(String[] args) {Test test = new Test();test.print(new Animal());test.print(new Cat());
}
但是如果想打印一批动物的食物信息,就无法利用多态这一特性,因为List<Cat>
并不是List<Animal>
的子类,所以只能额外增加方法。
public void printAnimalFood(List<Animal> animals) {animals.forEach(a -> System.out.println(a.getFood());
}
public void printCatFood(List<Cat> cats) {cats.forEach(c -> System.out.println(c.getFood());
}
public static void main(String[] args) {Test test = new Test();List<Cat> cats = Arrays.asList(new Cat());test.printAnimalFood(cats); // 编译错误,List<Cat>不是List<Animal>的子类test.printCatFood(cats); // 正确执行
}
如果还需要增加Dog,Rabbit等子类,那么就需要增加相应对应的方法printDogFood,printRabbitFood等,久而久之代码就会很臃肿
1、无界通配符
Java泛型提供了通配符(Wildcards),用?
表示,例如List<?>
、Set<?>
等。我们知道Object
是任意类的父类,而在Java泛型中,通配符就是任意同泛型类型容器的父类,如List<?>
是所有泛型List的父类,为了与接下来的要介绍的另外两种形式的通配符予以区分,可以称之为无界通配符
,意味没有界限限制。
由于?
不是泛型标识,所以也就无法在类、接口以及方法中传递,以下写法是不允许的
public class TestGeneric<?> {private ? a;
}
public <?> ? test(? a) {// Do something
}
?
只能出现在引用中或方法入参中,如
// 出现在引用中
Class<?> cls = xxx;
// 出现在方法入参中
public void print(List<?> list) {// Do something
}
需要注意的是方法入参中的无界通配符
只起到了告知的作用,并不能进行传递,也不需要传递(因为是任意类型)。
总结:
-
无界通配符
不能标注在泛型类、泛型接口以及泛型方法上 -
无界通配符
只能使用在引用以及方法入参中,并且无法进行传递
另外无界通配符
也可以起到占位以及修饰作用,可以直观告诉使用者这里可以接受任意类型。比如Java中的反射,我们经常写成下面的写法:
Class<?> cls = xxx;
知道了无界通配符的含义之后,我们可以把前言中的例子做一下优化
public void print(List<?> animals) {animals.forEach(a -> System.out.println(((Animal) a).getFood()));
}
public static void main(String[] args) {Test test = new Test();List<Animal> animals = Arrays.asList(new Animal());List<Cat> cats = Arrays.asList(new Cat());test.print(animals); // 正确执行test.print(cats); // 正确执行
}
使用无界通配符
后,即使后续有新增的子类,此方法也可以正确执行。至此我们解决了参数传递问题,但是新问题出现了:使用无界通配符
似乎与不使用泛型而直接使用List
没有任何区别:list中的元素可以是任意类型,并且取元素时需要强制转换。无界通配符
看起来并没有特殊的用途,那为什么要需要使用无界通配符
呢?
-
List
可以理解为持有任意Object类型的原始列表。 -
List<?>
可以理解为想要使用泛型列表,但是不确定具体的类型,用?
标识任意类型都可以。由于是泛型,所以列表中的元素的类型理应一致,但是以下写法是可以通过编译的
// 正确执行
List list = new ArrayList();
list.add(1);
list.add("2");
List<?> list2 = list;
public void print(List<?> list) {//..
}
// 正确执行
public static void main(String[] args) {List list = new ArrayList();list.add(1);list.add("2");new Test().print(list);
}
共同点:由于List<?>
和List
在编译期间都无法确定元素的实际类型,所以获取的时候都为Object类型
List<?> list1 = ...;
Object obj1 = list1.get(0);
List list2 = ...;
Object obj2 = list2.get(0);
不同点:List<?>
由于无法在编译期间确定泛型的实际类型,所以没法向List<?>
中添加除了null
外的任意类型元素。而List
由于可以存放Object对象,所以List
可以添加任意类型的元素。
List<?> list1 = new ArrayList<>();
list1.add(1); // 编译错误
list1.add("2"); // 编译错误
list1.add(null); // 正确执行
List list2 = new ArrayList();
list2.add(1); // 正确执行
list2.add("2"); // 正确执行
应用场景:
public void testMap(Map<String, ?> map) {// Do something
}
Map<String, String> map1 = ...;
Map<String, Integer> map2 = ...;
Map<String, Object> map3 = ...;
Test test = new Test();
test.testMap(map1); // 正确执行
test.testMap(map2); // 正确执行
test.testMap(map3); // 正确执行
testMap方法入参map的value想接受任意类型,只能使用无界通配符
来进行占位。
2、上界通配符
无界通配符由于可以接受任意类型,所以某些情况下还是不太适用,往往需要强制转换类型,一是不方便,二是可能引发转换异常,这个时候就可能需要使用上界通配符
来解决我们的问题了。
上界通配符
(Upper Bounde Wildcard),顾名思义,存在一个最上级的界限,即指定一个最高级别的父类,它表示对于该上界类型以及其子类都适用。
基本写法:
? extends xx
同无界通配符
,由于包含?
,所以这种写法只能出现在引用以及方法入参中,如:
List<? extends Number> numbers = xxx;public void test(Class<? extends Number> cls) {
}
前面分析?
并不能进行泛型类型传递,所以如果想在在泛型类、泛型接口、泛型方法中使用上界通配符
,需要将?
指定为标识类型,如
// 泛型类
public class TestGeneric<T extends Number> {private T t;public TestGeneric(T t) {this.t = t;}
}
// 泛型接口
public interface TestGerneric<T extends Number> {
}
// 泛型
public <E extends Collection> void test(E e) {System.out.println(e.size());
}
多重上界
当需要指定多个上界时,需要使用&
来连接,并且只能在指定泛型标识的时候使用,例如:
public class TestGeneric<T extends LongFur & BlueEye> {
}
上例中表示该泛型类型为同时满足为两个指定上界类型或其子类的类型。
需要注意的是,被指定的多个上界不能有有冲突的方法,否则会编译错误
特点:
我们在编译期只能知道上界通配符
的上界是什么类型,所以在取元素时,只能获取到上界的类型。具体的实际类型只有在运行时才能确定,同时也构成了多态。
List<Integer> ints = Arrays.asList(1);
List<Double> doubles = Arrays.asList(2.2);
public void test(List<? extends Number> numbers) {// 编译期间无法确定实际类型是Integer、Long、Double还是其他,只能使用Number接收// 运行时构成多态:Number number = 1; Number number = 2.2Number number = numbers.get(0);System.out.println(number);
}
同时与无界通配符
相似,由于编译期无法知晓具体的实际类型,所以不支持上界通配符
的容器添加元素(或赋值)
List<? extends Number> list = new ArrayList<>();
list.add(1); // 编译错误
list.add(2.2); // 编译错误
// 如果以上操作允许,那么list中的类型就不统一了
应用场景
public void print(List<? extends Animal> animals) {animals.forEach(a -> System.out.println(a.getFood()));
}
public static void main(String[] args) {Test test = new Test();List<Animal> animals = Arrays.asList(new Animal());List<Cat> cats = Arrays.asList(new Cat());test.print(animals); // 正确执行test.print(cats); // 正确执行
}
由于方法形参定义为List<? extends Animal>
,所以不用担心转型的问题,获取的元素肯定为Animal类型(包括子类),也不用担心实参类型传递错误。
通过优化我们可以感受到上界通配符
的好处,既能够做到向无界通配符
一样的通用型,又能够限制具体的类型范围,所以如果想达到此目的,就可以使用上界通配符
,而实际编码中我们使用上界通配符
的次数也是最多的。
3、下界通配符
与上界通配符
相反,下界通配符
(Lower Bound Wildcard),顾名思义,存在一个最低级的界限,即指定一个最低级别的子类,它表示对于该下界类型以及其父类都适用。
基本用法:
下界通配符
与上界通配符
类似,只需将extends
改为super
即可:
? super xx
List<? super Integer> list = xxx;
public void test(Class<? super Integer> cls) {
}
值得注意的是下界通配符
不支持制定泛型标识以及多重下界的写法。
特点:
与上界通配符
相反,我们在编译期只能知道下界通配符
的下界是什么类型,所以在添加元素时,只能向其中添加下界类型。
public void test(List<? super Integer> list) {// 编译期间无法确定实际类型是Integer还是Number或更高级别的父类,只能添加Integerlist.add(1);
}List<Number> numbers = Arrays.asList(1, 2L, 3.3);
xxx.test(numbers);
同时由于编译期无法知晓具体的实际类型,所以只能使用Object
来接收获取的元素
List<? super Integer> list = new ArrayList<>();
// 编译失败,因为无法确定是否是Integer,有可能是Number
Integer i = list.get(0);
// 编译失败,因为无法确定是否是Number,有可能是Object
Number m = list.get(1);
// 编译正确,Object可以接受任意值
Object o = list.get(2);