1. 什么是泛型?
Java泛型(Generics)是JDK5中引入的一种参数化类型特性。在定义类、接口或方法时使用类型参数,以便在使用时指定具体的数据类型。
参数化类型: 把类型当参数一样传递。
Person<T>:"T"为类型参数,整体为泛型类型。
Person<Student>:"Student"为实际类型参数,整体为参数化类型ParameterizedType。
2. 为什么要使用泛型?
为什么要使用泛型,泛型的优点是什么?
2.1 代码更灵活,可以复用
首先一点,对于同样一段代码,由于传入参数不同,必须重写方法。也就是需要传入多种数据类型,执行相同逻辑的代码。
public int add(int a, int b){
return a + b;
}
public float add(float a, float b){
return a + b;
}
public long add(long a, long b){
return a + b;
}
...
对于不同数据类型的加法需要写不同方法去实现,这时可以使用泛型:
public static <T extends Number> T add(T a, T b) {
if (a instanceof Integer) {
return (T) Integer.valueOf(a.intValue() + b.intValue());
} else if (a instanceof Double) {
return (T) Double.valueOf(a.doubleValue() + b.doubleValue());
} else if (a instanceof Float) {
return (T) Float.valueOf(a.floatValue() + b.floatValue());
} else if (a instanceof Long) {
return (T) Long.valueOf(a.longValue() + b.longValue());
} else if (a instanceof Short) {
return (T) Short.valueOf((short) (a.shortValue() + b.shortValue()));//在Java中,当你进行整数运算时,如果操作数的类型是 short 或 byte,结果会自动提升为 int。
} else if (a instanceof Byte) {
return (T) Byte.valueOf((byte)(a.byteValue() + b.byteValue()));
} else {
throw new IllegalArgumentException("不支持的数据类型");
}
}
2.2 消除强转,使代码更简洁
未使用泛型时,需要进行强转:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
使用泛型时,不需要类型强转:
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);
2.3 将类型转换错误提前到编译期
使用泛型,编译时进行更强大的类型检查,若编译期没有警告,运行期就不会出现ClassCastException。
List list = new ArrayList();
list.add("hello");
list.add("你好");
list.add(1);
for(int i = 0; i < list.size(); i++){
String s = (String) list.get(i);
sout(s);
}
上述代码没有使用泛型,在编译期不会报错,但是运行期会出现ClassCastException
。
在声明集合时指定了类型,如果尝试添加其他类型的元素,编译器会报错,将类型转换错误提前到编译期,这样会使代码更加健壮。
3. 泛型使用的三种情况
3.1 泛型类
创建一个泛型类型声明,引入了类型变量T,在类中的任何位置都可以使用:
public class Box<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
要从代码中引用泛型Box类,必须执行泛型类型调用,如:Box<Integer> integerBox;
注:
1. 泛型类型变量不能使用基本数据类型。(类型擦除下文会介绍)
ArrayList<int> arrays = new ArrayList<>();会报错。
擦除后变成Object,而Object没法存int。2.不能使用instanceof运算符。
擦除后只剩下原始数据类型,泛型信息不存在了。
常用的类型形参名称:
- E - Element(Java 集合框架广泛使用)
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
3.2 泛型接口
定义方法同泛型类。
3.3 泛型方法
泛型方法中的类型形参的范围仅限于声明它的方法,允许使用静态和非静态方法,以及泛型类构造函数。
类型形参部分必须出现在方法的返回类之前。
public class Util {
//静态方法
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {//可定义多个类型参数
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}
调用此方法的完整语法如下:
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util. <Integer, String> compare(p1, p2);
通常,已明确提供该类型时,可以省略,允许你将泛型方法作为普通方法调用,而无需在尖括号之间指定类型,编译器将推断所需的类型,此功能称为 type inference (类型推断) :
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);
4. 通配符 & 类型变量的限定
通配符分类:
通配符:?,表示未知类型。
Q:通配符与泛型有什么关系?
A:使用通配符的目的就是为了灵活转型,通配符让泛型转型更灵活,限制类型实参的类型,使用有界类型形参。
public <T extends Person> void getPerson(T t){
sout(t.getClass().getName());
}
4.1 is-a关系
在具体介绍通配符之前,首先了解一下is-a关系:
只要类型兼容,就可以将一种类型的对象分配给另一种类型的对象:
Object o = new Object();
Integer i = new Integer(1);
o = i;
在面向对象中这是一种“ 是一个(is a)”关系。由于Integer is a(是一个)Object,因此允许赋值。
同理,泛型也是如此:
Box<Number> box = new Box<Number>();
box.add(new Integer(1));
box.add(new Double(1.1));
4.2 上界通配符
上界通配符:<? extends Person>,extends后跟着上界,该类型是继承(或实现)Person类(或接口)的。
上界通配符的副作用:
使用上界通配符可以取元素,但不可以存放元素。
Room<? extends Person> studentRoom = teacherRoom;
studentRoom.set(new Sudent());//报错,无法放任何元素,也不可以取
studentRoom.set(new Teacher());//报错,无法放任何元素
studentRoom.set(null);//true,可以放null
studentRoom.get();//也是不可以的
Person person = studentRoom.get();//true
Object o = student.get();//true
Q:为什么使用上界通配符,不能存放元素了?
A: 字节码中是使用标记指定泛型类型,<? extends Person>这个类型也是使用标记表示,事实上并不知道他是什么类型,他可以是Person或Person子类型中任何一个,既无法与Sudent的标记对应,也无法与Teacher的标记对应,所以不能放任何元素。
注:通过反射,什么元素都能放,该方法自己用用就行。
4.3 下界通配符
下界通配符:<? super Person>,super后跟着下界,Person类是该类型的基类。
使用下界通配符,可以存放元素,但不可以取元素:
Room<? super Person> studentRoom = new Room<Creature>;
studentRoom.set(new Sudent());//true
studentRoom.set(new Teacher());//true
studentRoom.set(new Creature());//不可以
Person person = studentRoom.get();//不可以,限定的是下界
Object o = student.get();//true
4.4 非限定通配符
Room<?>: 非限定通配符,是一个泛型类型,等价于Room<? extends Object>。
注:非限定通配符既不能读也不能写。
Q:List<?>与List有什么区别?
A:List不进行类型安全检查,List<?>进行类型安全检查。
4.5 多重边界
Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }
如果边界中含有类,必须第一个指定,如上述代码中A,否则会编译出错。
注:有界形参除了限制类型外,还允许你调用边界中定义的方法。
4.6 Java泛型的PECS原则
PECS:Producer extends Consumer super。
以集合为例:
- 如果只需要从集合中获得类型T,使用 <? extends T> 通配符。
- 如果只需要将类型T放入集合中,使用 <? super T> 通配符。
- 如果既要获取又要放置元素,则不使用任何通配符。
5. 在Java中如何处理泛型&类型擦除
5.1 Java泛型的原理
private static void test1() {
ArrayList<Apple> apples = new ArrayList<>();
ArrayList<Banana> bananas = new ArrayList<>();
System.out.println(apples.getClass() == bananas.getClass());//true
}
上述代码中打印信息居然为true,为什么呢?
以ArrayList中的set方法为例,查看生成的字节码文件,如下图所示:
注:使用 javac 命令编译源文件,使用 javap -c 命令查看生成的字节码。
从中可以发现,泛型E在字节码文件中被擦除为Object,为什么会这样呢?
Q: Java泛型的原理是什么?什么是泛型擦除机制? 泛型是JDK5后引入的,兼容性怎么样,使用是否有影响?
A: Java的泛型是JDK5引入的新特性,虚拟机是不支持泛型的,为了向下兼容,Java实现的其实是一种伪泛型机制,只是在编译器进行处理的时候去做类型转换等操作,但是字节码还是之前的字节码,Java在在编译期擦出了所有的泛型信息,这样Java就不需要产生新的类型到字节码,在Java运行时根本不存在泛型信息。
接着看如下代码:
public class Banana<T extends Banana> extends Fruit<T>{
@Override
public void set(T t) {
super.set(t);
}
}
查看字节码文件
Q:字节码中为什么会有两个set方法呢?
A: 为了保证继承的多态性,自动生成了一个桥方法。在调用第二个set方法时,先判断是否是Banana,然后强转。
5.2 Java编译器具体是如何擦除泛型的?
1. 检查泛型类型,获取目标类型。
2. 擦除类型变量,并替换为限定类型.
- 如果泛型类型的类型变量没有限定(<T>),则用 Object 作为原始类型。
- 如果有限定(<T extends Person>),则用 Person 作为原始类型。
- 如果多个限定,则使用第一个边界作为原始类。
3. 在必要时插入类型转换以保证类型安全。
4. 生成桥方法以在扩展时保持多态性。
5.3 泛型擦除残留
查看Banana类的class文件:
class文件中泛型仍为T,这是泛型擦除的残留。这里看到的其实是签名而已,保留了定义的格式,对分析字节码有好处的。这个信息存在类的常量池中。
泛型类中独有的标记,普通类没有,JDK5才加入,标记了定义时的成员签名。
5.4 泛型与反射
Q:泛型被擦除了,为什么还与反射有关?
A:擦除后,在类的常量池中保留了泛型信息,还能拿到它的信息。
public static void main(String[] args) throws NoSuchFieldException {
test2();
}
ArrayList<String> arrayList;
private static void test2() throws NoSuchFieldException {
Field field = MyClass.class.getDeclaredField("arrayList");
System.out.println(field.getGenericType());//java.util.ArrayList<java.lang.String>
ParameterizedType type = (ParameterizedType) field.getGenericType();
System.out.println(type);//java.util.ArrayList<java.lang.String>
}
6. 有关泛型的几个小问题
6.1 泛型在静态方法和静态类中的问题
Q:为什么上述图片中泛型会报错?
A: 静态域或方法中不能引用类型变量。泛型参数的实例化需要在定义泛型类型对象时指定。而静态成员是不需要使用对象来调用的,所以不需要创建对象,自然就无法确定泛型参数是什么。
这里注意,泛型方法可以,因为这个T并不是Test<T>中的T,以下代码是正确的:
public static <T> T test(T t){
return t;
}
6.2 泛型类型中的方法冲突
Q:为什么会报错
A: 擦除后两个方法一样了。
6.3 无法创建泛型实例
无法创建一个类型参数的实例,因为类型不确定,下面代码会引起编译时错误。
public static <E> void test(List<E> list){
E elem = new E();//是错误的
}
通配符从不用作泛型方法调用,泛型类实例创建或超类型的类型实参。
但是,给出Class类型,可以通过反射创建一个参数化类型的实例:
public static <E> void test(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance();//正确
}
6.4 没有泛型数组
数组的协变: 如果A的父类是B,则A[]的父类是B[]。
注:不可以创建泛型数组,因为数组是协变的,擦除后就没法满足数组协变的原则。
注:无论A和B两个类是否相关,MyClass<A>和MyClass<B>都没任何关系,他们的公共父对象是Object。
6.5 泛型与异常相关问题
泛型类不能extend ‘java.lang.Throwable’:
不能捕获泛型类对象:
但是可以这样写:
public <T extends Exception> void test(T t) throws T {
try{
} catch (Exception t1){
throw t;
}
}
6.6 类型和子类型
只要不改变类型实参就会在类型之间保留子类型关系。