什么是泛型
泛型:在定义类,接口,方法时,可以把类型当作参数。
为什么需要泛型
泛型是在JDK1.5中添加的新特性。
在JDK1.5之前,为了让代码可以应用于多种类型,需要将参数设置为Object类。
例如我们在创建一个ArrayList时,内部实际是用一个Object的数组实现的。(为了向上兼容,现在创建ArrayList时,不指定类型的话,还是会被当作Object类)。
这样做的缺点是:任意类型的数据都可以存到这个ArrayList,在取出数据时,我们可能需要强制转型成目标类型,这样很容在运行期间出现转型错误。
List list = new ArrayList();
list.add("abc");
list.add(1);
for (Object o : list) {
String s = (String) o;
System.out.println(s.charAt(0));
}
当然这种错误也可以通过在转型之前进行类型检查来避免。
List list = new ArrayList();
list.add("abc");
list.add(1);
for (Object o : list) {
if (o instanceof String) {
String s = (String) o;
System.out.println(s.charAt(0));
}
}
但是每次都要这么搞,确实很麻烦.
因此在JDK1.5中引入了泛型,泛型最主要的作用就是通过指定参数类型,使编译器可以在编译期间检查程序中是否有类型转换的错误。
三种泛型的实现
在定义类时,把类型当作参数
格式如下,可以使用多个类型参数。
class 类名<类型参数1, 类型参数2...> {
}
代码实现如下:
class Map<K,V> {
private K key;
private V value;
public Map(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
使用方法如下;在创建对象时声名类型参数代表的类型
public class Test_2 {
public static void main(String[] args) {
Map<Integer, String> map1 = new Map<>(1, "a");
System.out.println(map1.getKey() + ": " + map1.getValue());
Map<String, Integer> map2 = new Map<>("a", 1);
System.out.println(map2.getKey() + ": " + map2.getValue());
}
}
需要注意的时map1不能引用map2,因为他们的类型参数不同,如果让map1引用map2编译器会报错
错误: 不兼容的类型: Map<Integer,String>无法转换为Map<String,Integer>
map2 = map1;
在定义方法时,把类型当作参数
每一个方法可以有它们自己的类型参数,并且是独立于所属类的类型参数的
格式如下:
<类型参数1, 类型参数2...> 返回值 方法名(参数...)
代码实现如下:
public static <K,V> boolean compare(Map<K, V> map1, Map<K, V> map2) {
return Objects.deepEquals(map1, map2);
}
使用方法如下,编译器可以自动检测方法的参数的类型,所以不用指定方法的类型参数:
public static void main(String[] args) {
Map<Integer, String> m1 = new HashMap<>();
m1.put(1, "a");
Map<Integer, String> m2 = new HashMap<>();
m2.put(1, "a");
System.out.println(compare(m1, m2));
}
在定义接口时,把类型当作参数
使用格式:
interface 接口名<类型参数1, 类型参数2...> {
}
例如:
interface GenericsInterface<K, V> {
void printKV(K k, V v);
}
我们有两种方式来实现这个接口
- 在定义实现类时不明确类型参数,在创建对象时才明确类型参数
- 在定义实现类时明确类型参数
在定义实现类时不明确类型参数,在创建对象时才明确类型参数
class GenericsImpl1<K, V> implements GenericsInterface<K, V> {
@Override
public void printKV(K k, V v) {
System.out.println(k);
System.out.println(v);
}
}
在定义实现类时明确类型参数
class GenericsImpl2 implements GenericsInterface<String, Integer> {
@Override
public void printKV(String s, Integer integer) {
System.out.println(s.toUpperCase());
System.out.println(integer);
}
}
测试代码如下
public static void main(String[] args) {
GenericsInterface<Integer, String> g1 = new GenericsImpl1<>();
g1.printKV(1, "a");
GenericsInterface<String, Integer> g2 = new GenericsImpl2();
g2.printKV("a", 1);
}
受限的类型参数
有些时候可能只希望只能处理特定类型,例如只希望能处理List类型,那么就可用对类型参数进行限制。格式如下:
<类型参数1 extends 限制类型, 类型参数2 extends 限制类型...>
使用方法如下:
class Box<E extends List> {
private E e;
public E getE() {
return e;
}
public void setE(E e) {
this.e = e;
}
}
测试代码如下
public static void main(String[] args) {
Box<ArrayList> b1 = new Box<>();
// Box<HashMap> b2 = new Box<HashMap>(); 编译器会报错
}
使用泛型之后的继承关系
通配符
例如List, List会被视为两种不同的类型。如果代码中需要同时接受这两种类型,就可以用通配符来实现
通配符用<?>表示,代表不确定的类型,可以在定义参数,属性和本地变量的类型时使用(在定义返回值类型时也可以用,但是不建议用)
没有限制的通配符
没有限制的通配符就是<?>, 如List<?>,需要注意的是,使用通配符代表类型的对象会被视为Object类,只能调用Object类提供的方法。
代码实现如下。
public static void printList(List<?> list) {
for (Object o: list) {
System.out.println(o);
}
}
测试代码如下:
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<>();
list1.add(1);
list1.add(2);
printList(list1);
List<String> list2 = new ArrayList<>();
list2.add("abc");
list2.add("cba");
printList(list2);
}
运行结果
1
2
abc
cba
Process finished with exit code 0
有上限的通配符
与受限的类型参数的类似,使用格式为:<? extends 类名>,被修饰的参数必须是该类的子类,使用通配符代表类型的对象的类型会被视为该类。
代码实现如下。
public static void printNumberList(List<? extends Number> list) {
for (Number n: list) {
System.out.println(n);
}
}
测试代码如下:
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<>();
list1.add(1);
list1.add(2);
printNumberList(list1);
List<Long> list2 = new ArrayList<>();
list2.add(1l);
list2.add(2l);
printList(list2);
List<String> list3 = new ArrayList<>();
// printNumberList(list2); 编译器会报错
}
运行结果如下:
1
2
1
2
Process finished with exit code 0
有下限的通配符
与受限的类型参数的类似。不同的是使用格式为:<? super 类名>,被修饰的参数必须是该类的父类,使用通配符代表类型的对象的类型会被视为Object 类。
public static void printIntegerList(List<? super Integer> list) {
for (Object o: list) {
System.out.println(o);
}
}
测试代码如下
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<>();
list1.add(1);
list1.add(2);
printIntegerList(list1);
List<Number> list2 = new ArrayList<>();
list2.add(1);
list2.add(2);
printIntegerList(list2);
List<Long> list3 = new ArrayList<>();
// printIntegerList(list3);
}
运行结果如下
1
2
1
2
Process finished with exit code 0
带有通配符的继承关系
类型擦除
Java中的泛型是通过类型擦除实现的。
类型擦除就是把泛型对象当作其泛型类型参数的边界类型或者Object类处理。
例如, <?>会被视为Object类型;<T extends 类1>,<? extends 类1>就会被视为类1类型;<? super 类2>就会被是为类2的类型。
类型擦除的缺点
首先我们来看下面这两个类
class Box<E> {
E data;
public void setData(E data) {
this.data = data;
}
}
class MyBox extends Box<Integer> {
public void setData(Integer data) {
System.out.println("MyBox setData");
super.setData(data);
}
}
经过类型擦除之后,这两个类的会变成如下代码
class Box<Object> {
Object data;
public void setData(Object data) {
this.data = data;
}
}
class MyBox extends Box<Integer> {
public void setData(Integer data) {
System.out.println("MyBox setData");
super.setData(data);
}
}
来看下下面这段测试代码,因为我们没有在MyBox类中重写public void setData(Object data)
方法,因为多态的存在,box.setData("data")
调用的的Box类中的方法。所以可以推断出可以运行到Integer data = myBox.data
这句代码并且会有ClassCastException(因为Bridge Methods,实际上不会运行到这句代码)
public static void main(String[] args) {
MyBox myBox = new MyBox();
Box box = myBox;
box.setData("data");
Integer data = myBox.data; // 这里会报错java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer
}
为了解决这个问题,编译器在编译期间会生成一个Bridge Methods
Bridge Methods
生成的Bridge Methods如下,不过在java文件中看不到,字节码文件中才能看到。
class MyBox extends Box<Integer> {
public void setData(Object data) { // Bridge Methods
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyBox setData");
super.setData(data);
}
}
因为这个Bridge Methods的存在,在编译期间会有警告信息。(只要消除编译期间的所有warings, errors就不会出现类型转换异常)
注: test8\Test_8.java使用了未经检查或不安全的操作。
注: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。
加上-Xlint:unchecked再编译下
test8\Test_8.java:10: 警告: [unchecked] 对作为原始类型Box的成员的setData(E)的调用未经过检查
box.setData("data");
^
其中, E是类型变量:
E扩展已在类 Box中声明的Object
1 个警告
当然,这只是警告,我们还是可以强行运行这个程序,不过运行期间会报错。
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
at test8.MyBox.setData(MyBox.java:3)
at test8.Test_8.main(Test_8.java:10)
Process finished with exit code 1
可以看到错误发生在Test_8.java:10,也就是box.setData("data");
这行代码