目录
2.2通配符的上界,不能进行写入数据,只能进行读取数据 (读取,生产者)
3.2通配符的下界,不能进行读取数据,只能写入数据。(写入,消费者)
(一)什么是泛型
(1)定义
泛型是指在定义类、接口或方法时,使用一个或多个类型变量来表示类中某个属性的类型或某个方法的返回值及参数类型
(2)泛型目的
如果你要编写一个适用于多种类型的代码(传String 能用 ,传Integer能用,传HashMap还能用),那么只能使用具体的类型去定义的话,那显然是做不到的(如果定义的是Interger类型的,那么String类型的就不能用了),这是就需要引入泛型,来适应许许多多的类型,来实现参数化类型
如下图中,一个Object类型的数组,可以放String类型,可以放int类型,甚至还可以放布尔类型,但是在取出的时候编译器会报错,因为getpos方法返回的是Object类型的值,是需要强转的,但是你也不好知道它原来是什么类型的,这显然是不安全的。
就像一个国家,既有英国人,又有美国人,法国人,中国人,小日子,印度人,他们都伪装成了本国人,你要用他的时候要调查他是哪国的人吧,那么这样的国家合理吗?显然是非常荒诞的。
(二)泛型语法
class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> {
}
将上方代码用泛型进行改写
class MyArray<T> {
public T[] array = (T[])new Object[10];//定义一个Object类型的数组强转成T类型
public T getPos(int pos) {//得到数组中pos下标的元素
return this.array[pos];
}
public void setVal(int pos,T val) {//往Onject类型数组中放元素
this.array[pos] = val;
}
}
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
MyArray<String> myArray = new MyArray<>();//指定MyArray数组中只能放String类型的值
myArray.setVal(0,1);//放入整形会报错
myArray.setVal(1,"hfkj");//放入string类型数组没有问题
String str = myArray.getPos(1);//这次获取元素不会报错
}
}
(1)好处
1.泛型是类型安全的
泛型在编译期就可以发现类型不匹配的错误,而不是等到运行时才发现,大大提升了程序的安全性和可靠性
2.避免强制类型转换
使用泛型后,在获取元素时就不需要再进行繁琐的强制类型转换(限制了一开始就只能传某种类型的),减少了出错
有了泛型之后,就相当于一个国家,之前是啥人都能进,现在是比如在中国,只有中国人能进去,如果其他国家的人要进来,就直接警告(编译报错),在美国只有美国人能进,法国只有法国人能进。
(2)泛型标识符
泛型标识符是可以任意设置的,但为了能让人见词知义,一般约定俗称地使用下列标识符,代表特定的含义
1. T(Type) 通常代表不确定类型,如 Integer, String等
2.E(Element) 通常代表集合中的元素类型,如自定义的一个User对象,list<User>
3.K(Key) ,V(Value) 通常用于表示键值对,如在源码Map中 Map<K , V> map = new HashMap<>()
(三)类型擦除
(1)为什么要有类型擦除
因为泛型机制是jdk5引入的,为了实现向后兼容,而不得不实现的类型擦除机制,这在Neal Gafter的文章中有具体阐述 ——>Java泛型擦除机制之答疑解惑 - 知乎 (zhihu.com)
(2)类型擦除规则
2.1:如果泛型类型没有指定类型上界,那么类型被擦除后会被替换为Object
2.2:如果泛型类型指定了上界,那么类型被擦除后会被替换为这个类型的上界类型
如下图,String类型被擦除为Object类型
2.3编译时保留,运行时擦除
java的编译器在编译时会保留泛型的类型信息,但在生成的字节码文件中删除这些信息,也就是说在编译的时候能够知道泛型类的信息,但在运行的时候jvm是无法识别泛型类的
如下图在编译时报错时因为,编译时泛型类型信息会有所保留,知道编译完成后才会擦除
(四)通配符
处理未知类型
当我们想要编写一个方法或者类,通配符就非常有用,
比如你想要该方法接受任何类型的泛型类作为参数,且不关心具体是那种类型,这个时候通配符?能够帮助我们编写更加通用型的代码
表达清晰意图
如当我们在代码中看到List<? extends Number> ,我们就能够立即明白,这个方法是用来处理Number类型或其子类型的集合的
- <?> :无限定通配符。
- <? extends T> :有上界的通配符。
- <? super T> :有下界的通配符
(1)通配符需要解决的问题
假如我们希望有个方法可以接收所有的泛型类型,但是又不能够让用户随意修改。这种情况就需要使用通配符"?"来 处理,
如下代码中,想要方法fun()能够接受所有类型的参数,但就可以使用通配符来解决,虽然使用T可以达到同样的效果,但语法更加繁琐
package fan;
class Message<T> {
private T message ;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class TestDemo {
public static void main(String[] args) {
Message<String> message = new Message<>() ;
message.setMessage("你好");
Message<Integer> message1 = new Message<>() ;
message1.setMessage(100);
fun(message);
fun(message1);//使用T
fun1(message);//使用通配符?
fun1(message1);
}
public static void fun1(Message<?> temp){//使用通配符接受所有泛型类型
System.out.println(temp.getMessage());
}
public static <T > void fun(Message<T> temp){//使用这种写法也能达到同样效果,只是比较麻烦
System.out.println(temp.getMessage());
}
public static <T extends Number> void fun(Message<T> temp){//如果要加上下界,只能再参数前加,不能在参数中加
System.out.println(temp.getMessage());
}
}
(2)上界通配符
2.1语法
<? extends 上界>
举例
<? extends Number>//表示只能接受Number类或Number的子类
只能传入Fruit或Fruit的子类
如下图当使用上界通配符限定接受泛型类型的范围之后,Message<String>类型的泛型类不能再被接受,因为String不是上界类型或上界类型的子类
2.2通配符的上界,不能进行写入数据,只能进行读取数据 (读取,生产者)
下图中由于被上界限定所以不能修改只能读取
原因:因为temp接收的是Number和他的子类,此时存储的元素应该是哪个子类无法确定。所以添加会报错!
例如一个上界是Number,那么可以传入的类型可以是 Double 也可以是 Integer,但具体是Integer还是Number无法确定,你是要是设置100,那如果传入的是Double类型的怎么办,所有就会编译报错
(3)下界通配符
3.1语法
<? super 下界>
<? super Integer>//传入的参数只能是Integer或Integer的父类类型
只能传入Fruit或Fruit的父类
如下图当使用下界通配符限定接受泛型类型的范围之后,Message<String>类型的泛型类不能再被接受,因为String不是下界类型或下界类型的父类
3.2通配符的下界,不能进行读取数据,只能写入数据。(写入,消费者)
下图中由于被下界限定所以不能读取只能修改
原因:因为temp接收的是Integer和他的父类,此时存储的元素应该是哪个父类无法确定。所以添加会报错!
例如一个下界是Apple(以上方食物关系类图为例),那么可以传入的类型可以是 Apple 也可以是 Fruit,也可以是Food,但具体是Fruit还是Food无法确定,向下转型又具有风险,所以就会报错