24-泛型
1. 引入
集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在 JDK5 之前只能把元素类型设计为 Object
,JDK5 之后使用 [泛型] 来解决。因为这个时候除了元素的类型不确定,其他的部分是确定的,例如关于这个元素如何保存,如何管理等。因此,此时把元素的类型设计成一个参数,这个类型参数叫做 [泛型]。Collection<E>
,List<E>
,ArrayList<E>
这个 <E>
就是类型参数,即 [泛型]。
所谓泛型,就是允许在定义类、接口时通过一个 标识 表示类中某个属性的类型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)。Java 引入 [参数化类型(Parameterized type)] 的概念,允许我们在创建集合时指定集合元素的类型,正如:List<String>
,这表明该 List
只能保存字符串类型的对象。
JDK5 改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持, 从而可以在声明集合变量、创建集合对象时传入类型实参。
那么为什么要有泛型呢,直接 Object 不是也可以存储数据吗?
- 解决元素存储的安全性问题,编译时就会进行类型检查,好比商品、药品的标签,不会弄错
- 解决获取数据元素时,需要类型强制转换的问题,好比不用每回拿商品、药品都要辨别
2. 在集合中使用泛型
public void test(){
// JDK7 以后,构造器中可以省略泛型类型
// 省略的类型可以从变量的类型推断出来
Map<String, Integer> map = new HashMap<>();
map.put("白世珠", 34);
map.put("陶源", 24);
map.put("刘心悠", 40);
Set<Map.Entry<String, Integer>> entries = map.entrySet();
Iterator<Map.Entry<String, Integer>> iterator = entries.iterator();
while(iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
String key = entry.getKey();
int value = entry.getValue();
System.out.println(key + ":" + value);
}
}
3. 自定义泛型结构
3.1 自定义泛型类、接口
3.1.1 概述
-
泛型的声明
interface List<T> / class GenTest<K,V> // 其中,T,K,V不代表值,而是表示类型。这里使用任意字母都可以
-
泛型的实例化
- 一定要在类名后面指定类型参数的值(类型)。如:
List<String> strList = new ArrayList<>();
- 如果声明了泛型类,实例化中却没有指明泛型类型,则认为此泛型类型为
Object
T
只能是类,不能用基本数据类型填充。但可以使用 [包装类] 填充
- 一定要在类名后面指定类型参数的值(类型)。如:
-
把一个集合中的内容限制为一个特定的数据类型,这就是
Generics
背后的核心思想 -
使用泛型的主要优点是能够在编译时而不是在运行时检测错误,出现编译错误比类在运行时出现类的强制类型转换异常要好得多
-
3.1.2 注意点
-
泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:
<E1, E2, E3>
-
泛型类的构造器:
public GenericClass() {...}
。 而这样写是错误的:public GenericClass<E>() {...}
-
实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致
-
泛型类型不同的同类型引用不能相互赋值(尽管在编译时
ArrayList<String>
和ArrayList<Integer>
是两种类型,但在运行时只有一个ArrayList
被加载到 JVM 中)ArrayList<String> list1 = null; ArrayList<Integer> list2 = null; // list1 = list2; 编译报错!
-
泛型只在编译阶段有效,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型
-
泛型如果不指定,将被擦除,泛型对应的类型均按照 Object 处理,但不等价于 Object。
-
如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象
-
在 类/接口 上声明的泛型,在 本类或本接口 中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型,因为类上声明的泛型类型的确定是在类的实例化阶段,而 static 早于实例化。
-
不能使用
new E[]
,但是可以:E[] elements = (E[]) new Object[capacity]
。 参考 ArrayList 源码中声明:Object[] elementData
,而非泛型参数类型数组。并且,也不用担心 new 的是 Object 类型,到时候会不会放进来 Ojbect 类型的对象问题,因为引用类型是 E 类型,所以在赋值时,编译器会进行检查,避免插入的对象不是 E 或者 E的子类类型。 -
异常类不能是泛型的;捕获异常时,也不能是泛型类型:
catch(T e)
3.1.3 父与子
- 子类不保留父类的泛型:按需实现
- 没有类型(擦除)
- 具体类型
- 子类保留父类的泛型:泛型子类
- 全部保留
- 部分保留
- 子类除了指定或保留父类的泛型,还可以增加自己的泛型
3.2 自定义泛型方法
- 在方法中出现的泛型结构,泛型参数的类型和与泛型类的泛型参数类型没有任何关系。换句话说,泛型方法与其所属的类是不是泛型类没有关系。此时,泛型参数的类型就是传入的形参的数据类型
- 格式:
[访问权限] <泛型> 返回类型 方法名(泛型标识 参数名称, ...) 抛出的异常
- 说明
public
与 返回值中间<T>
非常重要,可以理解为声明此方法为泛型方法- 只有声明了
<T>
的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法 <T>
表明该方法将使用泛型类型T
,此时才可以在方法中使用泛型类型T
- 区分普通方法和泛型方法,虽然在方法中使用了泛型,但是这并不是一个泛型方法。这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey() { return key; }
- 举例
/* public List<E> copyFromArrayToList(E[] arr) // 别忘了<E>,不然编译器会以为 E 是一个类名 -------------------------------------------------------------- 泛型方法可以是 static 的,这个泛型类型是在调用的时候确定的,所以OK 类的泛型类型不同,它是在实例化时确定的,而 static 早于对象的创建,所以不OK */ public static <E> List<E> copyFromArrayToList(E[] arr) { ArrayList<E> list = new ArrayList<>(); for(E e : arr) list.add(e); return list; }
3.3 一种使用情景
public class DAO<T> {
// 添加一条记录
public void add(T t) {}
// 删除一条记录
public boolean remove(int index) {
return false;
}
// 修改一条记录
public void update(int index, T t) {}
// 查询一条记录
public T getIndex(int index) {
return null;
}
// 查询多条记录
public List<Customer> getList(int index, int range) {
return null;
}
// 获取表中一共有多少条记录
public <E> E getValue() {
return null;
}
}
class CustomerDAO extends DAO<Customer> {} // 操作 Customer 表的 DAO
class StudentDAO extends DAO<Student> {} // 操作 Student 表的 DAO
4. 泛型在继承方面的体现
如果 B
是 A
的一个子类型(子类或者子接口),而 G
是具有泛型声明的类或接口,G<B>
并不是 G<A>
的子类型!二者是并列关系。
比如:String
是 Object
的子类,但是 List<String>
并不是 List<Object>
的子类。
补充:A
是 B
的父类,则 A<G>
是 B<G>
的父类
5. 类型通配符 ?
因为泛型没有继承关系,所以当需要用一个泛型引用不同的泛型实现时,泛型中写他们共同的父类是不行的。于是引入一个新的概念,叫做 [泛型通配符 ?]。
比如:List<?>
,Map<?,?>
,List<?>
是 List<String>
、List<Object>
等各种泛型 List
的父类。
注意, [泛型通配符 ?] 只能用在泛型引用上,用来引用不同的泛型实例,? 不能出现在实现上。
将任意元素加入到其中不是类型安全的。因为我们不知道 c 的元素类型,我们不能向其中添加对象。add()
有类型参数 E
作为集合的元素类型。我们传给 add()
的任何参数都必须是一个未知类型的子类。因为我们不知道那是什么类型,所以我们无法传任何东西进去。唯一的例外的是 null
,它是所有类型的成员。
另一方面,我们可以调用 get()
并使用其返回值。返回值是一个未知的类型,但是我们知道,它总是一个 Object
。