Java中的泛型存在一些编译器特性, 是在编译期间就将泛型代码转换成具体的类型.
泛型的概念
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
- 泛型参数 泛型形参都称为泛型参数
- 泛型形参 泛型形参可以类比于对象方法中的具体的形参。
- 泛型实参 泛型实参就是实际要绑定的具体类型。
泛型表示
Java中泛型都用大写符号表示.但一般有比较规范的表示意思
- K 表示Key
- E 表示Element
- V 表示Value
- T 表示Type
泛型的三种应用
java中有泛型类、泛型接口、泛型方法三种泛型。
使用步骤
- 声明泛型名称(类和接口相识、泛型方声明在 访问修饰符号和返回类型之间)
- 如果是类, 可以使用泛型名称修饰非静态成员
- 如果是方法, 可以使用泛型名称修饰返回类型, 参数列表
泛型类和接口(以类为例)
泛型类
语法:
[修饰符] class 类名<泛型名称> {
}
示例代码:
public class TestGenericTest {
public static void main(String[] args) {
Student<Integer> student = new Student<Integer>();
student.setScore(100);
student.setName("小明");
System.out.println(student);
}
}
class Student<T> {
private String name;
private T score;
public Student() {
}
public Student(String name, T score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public T getScore() {
return score;
}
public void setScore(T score) {
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", score=" + score +
'}';
}
}
定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。
也就是说泛型类型并不一定要参数实参,但是如果不传入实参就起不到本来应该起的限制作用,此时泛型类型可以为任意类型
示例代码
Student student1 = new Student("小王", 100);
Student student2 = new Student("小陈", "100");
Student student3 = new Student("小李", false);
System.out.println(student1.getScore());
System.out.println(student2.getScore());
System.out.println(student3.getScore());
输出
100
100
false
泛型类总结
- 泛型的类型参数只能是类类型,不能是简单类型。
- 泛型类型并不一定要参数实参,但是如果不传入实参就起不到本来应该起的限制作用,此时泛型类型可以为任意类型。
- 泛型的类型不能用在静态成员上。
泛型方法
为什么会有泛型方法, 主要是为了应用于以下两种场景
- 某个类不是泛型类, 而某个方法需要使用泛型
- 某个类是泛型类, 但是需要再静态方法上使用泛型
语法:
[修饰符] <泛型名称> 返回类型 方法名(形参类型 形参列表) {
}
其中返回类型、形参类型也可以用声明的泛型类型进行修饰
示例代码:
public class TestGenericMethod {
public static void main(String[] args) {
Integer[] nums = new Integer[]{1,2,3,4,5,6};
String numsStr = MyArray.toString(nums);
System.out.println(numsStr);
}
}
// 类不是泛型类。又想声明泛型方法
class MyArray {
public static <T> String toString(T[] arr) {
String str = "[";
int i = 0;
for (T t:arr) {
str += t;
if (i != arr.length - 1)
str+= ",";
i++;
}
str +="]";
return str;
}
}
泛型通配符
- ? 问号泛型通配符, 表示任意匹配
- extends: 泛型中, 表示上限,不管是类还是接口,都用extends表示匹配的类型必须是这个类或者是这个类的子类,对于接口则表示必须是实现了该接口的类
- super: 泛型中, 表示下限。表示类必须是这个类或者是起父类
通配符上限设置
<? extends className>
假设有如下继承体系, 则Box <? extends Fruit>
表示图中蓝色部分
通配符下限设置
<? super className>
假设有如下继承体系, Box<? super Fruit>
表示的如下黄色部分, Fruit就是下限.支持的只能是Fruit或是其父类.
以上两张图均是取自网络
具体例子
现在有四个类,分别是 Food,Fruit,Apple 和 Plate、rice。其中 Apple 继承自 Fruit,Fruit 继承自 Food,Plate 是用来盛放这些东西的容器.Rice是谷物,继承自Food
定一个泛型类Plate
class Plate<T> {
T x;
public Plate(){}
public Plate(T x){this.x = x;}
public void set(T x){this.x=x;}
public T get(){return x;}
}
我们能实例化这个泛型类盘子, 让它为装载水果的盘子.因为是水果盘子,所以苹果也能放进去,但是取的时候,我们只知道这个果盘装的是水果,但不知道是什么水果。因为泛型的实参是Fruit,绑定的是Fruit
Plate<Fruit> plate = new Plate<Fruit>();
plate.set(new Apple());
Fruit apple = plate.get();
其实我们可以进一步限制这个盘子能装什么类型的水果, 比如限制这个盘子只能装苹果或者水果
public class Test {
public static void main(String[] args) {
Plate<? extends Fruit> plate = new Plate<Fruit>();
Plate<? extends Fruit> plate2 = new Plate<Apple>();
// 上限不能往外取, 编译器是禁止的.因为编译器
// 只知道plate和plate2是 能存水果类的盘子,但是并不知道这个盘子
// 到底是哪一种盘子,如上代码, 可能是放苹果类的盘子,也可能是水果类盘子
// 如果是苹果类盘子,那只能放苹果类的苹果
// plate.set(new Apple()); 编译器报错
// plate.set(new Fruit()); 编译器报错
Fruit f1 = plate.get();
Fruit f2 = plate2.get();
// Apple a1 = plate2.get(); 编译器报错
}
}
上限不能往外取, 编译器是禁止的.因为编译器
上述代码只知道plate和plate2是 能存水果类的盘子,但是并不知道这个盘子到底是哪一种盘子,如上代码, 可能是放苹果类的盘子,也可能是水果类盘子, 如果是苹果类盘子,那只能放苹果类的苹果。因为苹果类型是小类型, 不能往大类型,水果类型自动转,而JVM不保证强转的成功,自然就不能调用set.
总结一句话上限通配符,只能往外取,不能往里存。
下界通配符规律恰好想法, 下界通配符,能往外取,也能往里存,但是取出来会丢失类型,存进去只能存子类或者本类,不能存父类.
泛型通配符使用总结
- 上限通配符,只能往外取,不能往里存。
- 下限通配符,既能往外取,有能往里存。只是外取丢失类型信息,里存只能存子类或本类对象。
泛型通配符使用场景
- 在代码中避免泛型类和原始类型的混用。比如List
- 在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念。由于具体的类型是未知的,很多操作是不允许的。
- 泛型类最好不要同数组一块使用。你只能创建new List<?>[10]这样的数组,无法创建new List[10]这样的。这限制了数组的使用能力,而且会带来很多费解的问题。因此,当需要类似数组的功能时候,使用集合类即可。
- 不要忽视编译器给出的警告信息。
PECS 原则
- 如果要从集合中读取类型T的数据, 并且不能写入,可以使用 上界通配符(<?extends>)—Producer Extends。
- 如果要从集合中写入类型T 的数据, 并且不需要读取,可以使用下界通配符(<? super>)—Consumer Super。
如果既要存又要取, 那么就要使用任何通配符。