Java泛型总结之定义泛型接口、类和类型通配符
- 前言:在前面总结Collection接口时常常会用到泛型知识,以及在之前对Okhttp3进行封装时用上了泛型,封装需要泛型是因为工具类需要有通用性,适合各种自定义类的数据传入,所以需要有泛型思想。所以,我决心把泛型知识都梳理一遍,方便自己也尽量给同仁们一些帮助。
(一)初识泛型
(一).为什么要使用泛型?我们看下面这个例子:
public class GenericTest {
public static void main(String[] args) {
List list = new ArrayList();
list.add("Hello");
list.add("Hi");
list.add(100);
for (int i = 0; i < list.size(); i++) {
String name = (String) list.get(i); // 出现异常
System.out.println("name:" + name);
}
}
}
- 对于集合List,里面元素默认为Object类,但是在循环过程中,List对加入的元素原始类型全给忘记了,只记得它们是Object类。所以在
String name = (String) list.get(i);
这行代码中,将Integer类型的数据错转为String,引发了ClassCastException,类型转换异常。 - 由此我们发现:当我们将一个对象放入集合中,集合不会记住此对象的类型,当再次从集合中取出此对象时,改对象的编译类型变成了Object类型,但其运行时类型仍然为其本身类型。
(二).使用泛型
- 从Java5以后,Java引入了参数化类型,允许程序在创建集合时指定集合元素的类型。
public class GenericTest {
public static void main(String[] args) {
/*
List list = new ArrayList();
list.add("Hello");
list.add("Hi");
list.add(100);
*/
List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("Hi");
//list.add(100); // 1 提示编译错误
for (int i = 0; i < list.size(); i++) {
String name = list.get(i); // 2
System.out.println("name:" + name);
}
}
}
- 采用泛型写法后,在//1处想加入一个Integer类型的对象时会出现编译错误,通过List,直接限定了list集合中只能含有String类型的元素,从而在//2处无须进行强制类型转换,因为此时,集合能够记住元素的类型信息,编译器已经能够确认它是String类型了。
(三).定义泛型接口、类
- 我们来看一下List接口、Iterator接口、Map的源码片段
//定义接口时指定一个类型形参,该形参名为E
public interface List<E>{
//在该接口中,E可作为类型使用
//下面方法可以使用E作为参数类型
boolean add(E e);
Iterator<E> iterator();
}
//定义该接口时指定了两个类型形参,其形参名为K,V
public interface Map<K,V>{
//在该接口中K,V完全可以作为类型使用
Set<K> keySet();
V put(K key,V value);
}
- 我们看一个最简单的泛型小例子:
public class GenericTest {
public static void main(String[] args) {
Box<String> name = new Box<String>("corn");
System.out.println("name:" + name.getData());
}
}
class Box<T> {
private T data;
public Box() {
}
public Box(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
- 由上面例子可知,允许在定义接口、类时声明类型形参,类型形参在整个接口,类体内都可当成类型使用。
(四).从泛型类派生子类
- 当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类。但是当用这些接口、父类时不能再包含类型参数。如下面代码为错:
//Apple类不能跟类型形参
public class A extends Apple<T>{}
- 必须改为如下代码:
//使用Apple类时,没有为T形参传入实际的类型参数
public class A extends Apple<String>{}
(五)并不存在泛型类
- 先看一个简单例子:
public class GenericTest {
public static void main(String[] args) {
Box<String> name = new Box<String>("corn");
Box<Integer> age = new Box<Integer>(712);
System.out.println("name class:" + name.getClass()); // com.qqyumidi.Box
System.out.println("age class:" + age.getClass()); // com.qqyumidi.Box
System.out.println(name.getClass() == age.getClass()); // true
}
}
由此,我们发现,在使用泛型类时,虽然传入了不同的泛型实参,但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存上只有一个,即还是原来的最基本的类型(本实例中为Box),当然,在逻辑上我们可以理解成多个不同的泛型类型。
究其原因,在于Java中的泛型这一概念提出的目的,导致其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。
【对此总结成一句话】:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型,不管泛型的类型形参传入哪一种类型实参,对于java来说,它们依然被当做一个类来处理,在内存中也只占用一块内存来处理。
(六).类型通配符
- 我们先来看这样一个片段
public void test(List<Object> c){
for(Object o : c ){
system.out.println(o);
}
}
- 上面代码看似没问题:
List<String> strList = new ArrayList<>();
test(strList);
- 上面程序发生编译错误,说明List《String 》对象无法被当成List《String》对象,也就是说List《String 》并不是List《String》对象的子类。
- 为了表示各种泛型List的父类,可以使用类型通配符。类型通配符是一个问号。类型通配符一般是使用 ? 代替具体的类型实参。注意了,此处是类型实参,而不是类型形参!
public class GenericTest {
public static void main(String[] args) {
Box<String> name = new Box<String>("corn");
Box<Integer> age = new Box<Integer>(712);
Box<Number> number = new Box<Number>(314);
getData(name);
getData(age);
getData(number);
}
public static void getData(Box<?> data) {
System.out.println("data :" + data.getData());
}
}
(七).设定类型通配符的上限
- 在上面的例子中,如果需要定义一个功能类似于getData()的方法,但对类型实参又有进一步的限制:只能是Number类及其子类。此时,需要用到类型通配符上限。
public class GenericTest {
public static void main(String[] args) {
Box<String> name = new Box<String>("corn");
Box<Integer> age = new Box<Integer>(712);
Box<Number> number = new Box<Number>(314);
getData(name);
getData(age);
getData(number);
//getUpperNumberData(name); // 1
getUpperNumberData(age); // 2
getUpperNumberData(number); // 3
}
public static void getData(Box<?> data) {
System.out.println("data :" + data.getData());
}
public static void getUpperNumberData(Box<? extends Number> data){
System.out.println("data :" + data.getData());
}
}
(八).设定类型形参的上限
- Java不仅允许在使用通配符形参时设定上限,而且可以在定义类型形参时设定上限,用于传给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类。
public class Apple<T extends Number>{
T col;
public void static main(String[] args){
Apple<Integer> ai = new Apple<>();
Apple<Double> ad = new Apple<>();
//下面代码出现编译错误,下面代码试图把String类型传给T参数
Apple<String> as = new Apple<>();
}
}