简介
我们平时开发中使用集合一般都会指定集合的泛型类型,这样就保证了一个集合存放的是同一种类型的对象,而在JDK5之前没有泛型的时候,集合将所以存储的对象当做object进行处理。所以如果存放集合中不是同一个对象,当需要进行强制类型转换的时候就会造成ClassCastException
,例如下面的程序:
如果指定泛型的类型,则可以帮我们在编译期就可以检查集合存储元素类型是否合法,例如下面定义泛型为String类型的集合后,再次添加Integer类型的1将会提示错误。
java7增强的泛型写法
在java7之前定义集合的泛型时,创建构造器的集合类型也需要和定义变量引用的泛型类型一致,例如下面这个语句:
// 等式右边的泛型类型也需要与变量的泛型类型一致,且不可省略
List<String> list = new ArrayList<String>();
而在java7之后我们就可以省略构造器定义的泛型类型使用<>
代替即可,这是因为java可以根据左边推算出右边的泛型类型。
定义泛型
- 自定义泛型
可以为任何类和接口指定泛型,下面就自定义了一个泛型Order。
public class Order<T> {
private String name;
private BigDecimal price;
private T product;
public Order() {
}
public Order(String name, BigDecimal price, T product) {
this.name = name;
this.price = price;
this.product = product;
}
public T getProduct() {
return product;
}
public static void main(String[] args) {
// 3.5元的牛奶来自河北
Order<String> order = new Order<>("牛奶",BigDecimal.valueOf(3.5),"河北");
System.out.println(order.getProduct());
// 10份辣条25元
Order<Integer> laTiao = new Order<>("辣条",BigDecimal.valueOf(25),10);
System.out.println(laTiao.getProduct());
}
}
@ToString
public class SonOrder<T> extends Order<T> {
private String haha;
public SonOrder() {
}
public SonOrder(String name, BigDecimal price, T product, String haha) {
super(name, price, product);
this.haha = haha;
}
}
泛型类order无论传入什么类型的T,它的字节码文件是相同的。这是因为JVM在最终会将泛型进行擦除,所以上面order.getClass() == laTiao.getClass()
这两个字节码输出为true,需要注意的是静态方法、静态变量以及静态代码块中不能使用泛型参数类型,如下图一个静态所示:
泛型通配符
有个需求是一个类方法有一个集合参数而这个集合参数的泛型类型是不确定的那么怎么定义这种方法呢?
那么有一种是如下代码所示:
// 定义方法型参集合泛型为Object类型
public void setProducts(List<Object> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
上面的定义方法看起来没有任何问题,但是调用可能会出现问题如下所示:
上面程序出现了编译错误,告诉我们 List<String>不能被当做List<Object>使用。简而言之就是说 List<String>不是List<Object>的子类!!!
,那如果一个方法的参数为数组,它能不能添加子类型的数组呢?下面定义了一个参数为数组类型的方法:
public void setOrder(Order[] arr) {
System.out.println(arr.getClass().getName());
}
我们发现数组不像之前的集合一样,子数组可以向上转型为父型数组。所以这里需要注意的是:如果A是B的子类,那么List<B>不是List<A>的父类,但是数组new B [] {} 可以是new A [] {} 的父类
。
- 泛型通配符的上限
如果想要达成数组那样的效果呢?我们可以将上面的方法改为如下所示:
public void setProducts(List<? extends Object> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
改写后的setProducts
方法传入的List的集合编译通过且运行后输出? extend Object
这里List<? extends Object>
表示传入的集合参数类型可以是Object类型也可以是Object的子类型,所以这种可以很好解决最开始的集合类型无法向上转型的问题,这种List<? extends Object>
我们称之为泛型通配符(?)的上限,当然有了上限肯定就有下限,下限是这么表示的:List<? super SonOrder>
。
- 泛型通配符的下限
如果上面的list的泛型可以改成List<? super SonOrder>
,如下所示:
public void setOrders(List<? super SonOrder<T> > list)
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
上面这个方法表示参数的集合泛型可以是SonOrder<T>
,也可以是SonOrder<T>
的父类型Order<T>
,如下面程序,参数传入的是Order<String>类型的参数。
定义泛型方法
前面定义的是类泛型以及该类泛型的成员变量、方法入参的集合泛型参数,这些可以直接使用类定义的泛型,但是有些接口或者类并没有定义泛型类型,我们同样也可以方法泛型形参类型。下面我们有一个数组转集合的一个需求代码如下:
不采用泛型将数组转数组:
public static void ArrayToListNOGeneric(Object[]objects,List<Object> list) {
for (Object object : objects) {
list.add(object);
}
}
通过之前的介绍,我们知道这个方法的局限性在于传入的List集合数据的泛型只能是Object类型,无法传入泛型为子类型的集合,借助泛型通配符的泛型方法可以解决上面这个问题,泛型方法语法格式如下:
修饰符<T,R> 返回值类型 方法名(形式参数列表)
泛型优化:
// 传入的集合泛型参数类型可以为E,也可以为E的父类型。
public static <E> void ArrayToList(E[] e, List<? super E> list) {
for (E e1 : e) {
list.add(e1);
}
}
测试调用:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
List<Object> objects = new ArrayList<>();
String[] arr = {"1"};
ArrayToList(arr, list);
ArrayToList(arr, objects);
}
上面的代码调用ArrayToList方法即可以传入String类型的泛型集合,也可以传入Object类型的泛型集合。
泛型方法与泛型通配符的区别
大多数的时候可以使用泛型方法来代替泛型通配符如List有下面两个方法:
// 取两个集合的交集
boolean retainAll(Collection<?> c);
// 添加C集合中的1元素
boolean addAll(Collection<? extends E> c);
上面入参的泛型通配符可以使用下面两个泛型方法来替代:
<T> boolean retainAll(Collection<T> t);
<T extends E> addAll(Collection<T extends E> c);
请注意上面可以传入T参数类型只被用了一次,所以这里我们传入任一类型都可以,所以可以采用泛型通配符?的使用。
泛型的擦除
泛型类的的泛型参数我们可以指定也可以不指定,当不指定泛型的类时,一般该泛型类的参数为Object,例如将List 给赋值另一个不带泛型参数的List,则这个不带参数的泛型List其默认泛型类型为Object类型,如下代码演示:
@Test
public void testGeneric() {
List<String> list = new ArrayList<String>();
list.add("heihei");
List list1 = new ArrayList();
// 将 List<String> 赋值给List,则此时的泛型参数为Object,所以下面添加5是可以的
list1 = list;
list1.add(5);
list1.stream().forEach(e -> {
System.out.println(e);
});
}
还有一个常见的是JVM在处理集合泛型的时候,都会将集合泛型进行擦除。
泛型的限制
- 类型参数不能用于在方法中实例化其对象
如果想要完成实例化可借助反射进行实现如下所示:
- 不能使用基本类型实例
这里只有Order<Integer>,没有Order<int>,泛型参数只能是类对象。 - 不能使用instance of
由于泛型会被虚拟机进行擦除,Order与Order是无法通过instance of 进行比较。 - 不能使用静态域泛型
- 泛型不能进行转化
Number类是Integer的父类,但是Order与Order两者并没有继承关系,大多数程序员最开始都认为是继承关系,其实这个是错误的,类似的转化关系可以参考上文的泛型通配符的上限
部分的setProducts
方法。 - 不能使用泛型数组
先看下面一段代码:
编译期会进行报错,这是因为泛型擦除的原因,擦除之后就变成Objec[]
类型,如果向其添加元素将会报ArrayStoreException
。