一、定义
泛型:是JDK5中引入的特性,可以在编译阶段约束操作的数据类型,并进行检查。
格式:<数据类型>
细节:泛型只能支持引用数据类型,基本数据类型需要变成其包装类。
question:在JDK5之前,没有泛型的时候,集合如何存储数据?
我们注意到,如果没有泛型,add 方法的形参为 Object 类型。
所以使用 add 方法可以添加任意类型的元素。
public class Demo {
public static void main(String[] args) {
//1.创建集合
ArrayList list = new ArrayList();
//2.添加数据
list.add(123);
list.add("aaa");
list.add(true);
//3.遍历集合的每一个元素
Iterator it = list.iterator();
while (it.hasNext()) {
Object obj = it.next();
System.out.println(obj);
}
}
}
由于添加的数据都是Object类的直接或者间接子类,所以我们可以使用多态的方式,获取数据。
但我们知道,多态的缺点:不能访问子类的特有功能。
比如我想调用 String 类的一些成员方法,比如获取字符串的长度时,则无法调用。
虽然我们可以使用强制类型转换,但由于添加的数据是任意类型的。
对于该集合中,Integer 和 Boolean 类型的元素则会发生类型转换异常。
因此,JDK5推出了泛型。
泛型的好处:
① 统一数据类型
② 把运行时期的问题提前到了编译时期,避免了强制类型转换可能出现的异常,因为在编译阶段类型就能确定下来。
Tips:Java中的泛型是伪泛型(编译的时候限定类型,编译之后底层存的事实上还是Object类型,取的时候会将 Object 类型强转成限定的数据类型)。
这种过程被称为 “泛型的擦除”。
question:为什么要这样设计呢?
在JDK5以前,java已经写了很多的代码了,这些代码市面上也有很多人用了。
所以,我们不能直接改动 java 底层的代码,这样所有人的代码都要发生变化。
但可以使用伪泛型给它加一个限定,这样既不用修改之前的代码,也可以进行类型的统一。
注意点:
① 泛型中不能写基本数据类型(伪泛型使得底层还是 Object 类型,而基本数据类型无法转变成Obejct 类型)。
② 指定泛型的具体类型后,传递数据时,可以传入该类类型或者其子类类型。
③ 如果不写泛型,类型默认是Object 类型。
二、泛型的使用
泛型可以在很多地方进行定义
1.泛型类
使用场景:当一个类中,某个变量的数据类型不确定时,就可以定义带有泛型的类。
这里的 E 可以理解为一个变量,但不是用来记录数据的,而是记录数据的类型。
如果有多个变量类型不确定,但属于不同的泛型,可以加逗号,如 <E,T>。
泛型类:
public class MyList<E> {
final int len = 10;
Object[] obj = new Object[len];
int size;
public boolean add(E e) {
if (size >= len) {
return false;
}
obj[size] = e;
size++;
return true;
}
public E get(int index) {
//底层取的时候要进行强制转换
return (E) obj[index];
}
//重写toString方法,使其打印的不是地址值,而是属性值
@Override
public String toString() {
//将数组变为字符串
return Arrays.toString(obj);
}
}
测试类:
public class Test {
public static void main(String[] args) {
MyList<String> list1 = new MyList<>();
list1.add("aaa");
list1.add("bbb");
list1.add("ccc");
System.out.println(list1);
//[aaa, bbb, ccc, null, null, null, null, null, null, null]
MyList<Integer> list2 = new MyList<>();
list2.add(1);
list2.add(2);
list2.add(3);
System.out.println(list2);
//[1, 2, 3, null, null, null, null, null, null, null]
}
}
2.泛型方法
方法形参类型不确定时,可以使用类名后面定义的泛型<E>。
但是如果类中只有一个方法的形参时不确定,就没必要在整个类中定义泛型了。
此时就可以在方法声明上定义泛型。
这里的 T 同样可以理解为一个变量,但不是用来记录数据的,而是记录数据的类型。
区别:
泛型类的话,类中的所有方法都可以用该泛型;但泛型方法的话,只有本方法可以使用。
需求:定义一个集合工具类,类中有一个泛型方法 addAll,可以添加多个数据。
//工具类
public class ListUtil {
//私有化构造方法
private ListUtil() {
}
//...表示可变参数(一个参数也行,多个参数也可以)
public static <E> void addAll(ArrayList<E> list, E... e) {
for (E element : e) {
list.add(element);
}
}
}
测试类:
public class Test {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
ListUtil.addAll(list1, "aaa", "bbb", "ccc");
System.out.println(list1);//[aaa, bbb, ccc]
ArrayList<Integer> list2 = new ArrayList<>();
ListUtil.addAll(list2, 1, 2, 3, 4);
System.out.println(list2);//[1, 2, 3, 4]
}
}
3.泛型接口
当一个接口当中变量类型不确定时,就可以定义泛型接口。
重点:如何使用一个带泛型的接口呢?
(1)方式一
List 本身是一个泛型接口
定义一个实现类,给出数据类型。一旦给出后,类型就确定了。
测试类:
实现类已经给处数据类型,此时数据类型就已经限定了,只能添加该数据类型的元素了。
(2)方式二
实现类继续延续泛型,不给出确定的数据类型。
测试类:
由于实现类并没有限定类型,所以只能在创建对象时给泛型限定类型。
三、泛型的继承和通配符
1.泛型的“继承”
注:泛型不具备继承性,但数据具备继承性。
在 method 方法中,形参已经限定泛型为 Ye 类型。那么调用 method 方法传入的集合的元素数据了类型也只能是 Ye 类型,Ye 的子类也不行,因为泛型不具有继承性。
但数据具有继承性,前面说了, 指定泛型的具体类型后,传递数据时,可以传入该类类型或者其子类类型。
注意区分:一个是传递整个限定数据类型的集合作为数据,一个是往限定数据类型的集合里面添加数据。
2.通配符
需求:定义一个方法,形参是一个集合,集合中的数据类型不确定,但数据类型属于某个继承体系。即:集合的类型可以是 Ye,Fu,Zi,但不能是 String,Integer等。
如果使用前面的泛型方法,可是实现数据类型不确定,但缺点是可以接收任意的数据类型,String 也可以。
注:
① 只写一个 ? 也表示任意数据类型,和 <E> 没有区别,只是修饰符后面不需要再加 ? 了而已。
② 写了通配符 ? 后,extend 或者 super 的 E ,就必须限定数据类型。
这里并没有限定集合的数据类型,而是限定了数据类型的范围。
关键点:通配符可以限定数据类型的范围。
3.使用场景
① 定义类,方法,接口的时候,如果类型不确定,就可以定义泛型。
② 如果类型不确定,但是能知道属于哪个继承体系中,可以使用泛型的通配符。