什么是泛型?有什么作用?
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
泛型机制的优势?
- 省去了强制转换,可以在编译时候检查类型安全,可以用在类,方法,接口上,其具有更好的可读性和安全性。
- 泛型程序设计意味着程序可以被不同类型的对象重用,即可模板化。
- 泛型类和泛型方法同时具备可重用性、类型安全和效率,这是非泛型类和非泛型方法无法具备的。泛型通常用与集合以及作用于集合的方法一起使用。
java方法泛型与通配符:
使用大写字母定义类型的类,就都是泛型中的方法泛型,其使用的大写字母只有名字上的区别而已,没有功能上的区别,但为了命名规范,就有了下面的用法。
- T (type)表示具体的一个java类型
- K V (key value) 分别代表java键值中的Key Value
- E (element) 代表Element
- ? 表示不确定的java类型(即泛型通配符)
List(方法泛型),List,List<?>(泛型通配符),List区别
List,List<?>的区别
- ArrayList al=new ArrayList();
- 方法泛型中的泛型参数对象是可在范围内任意类型中指定的,但一旦指定(即调用后),集合元素就只能是T(指定后的类型)类型
- ArrayList<?> al=new ArrayList<?>();
- 通配符的泛型对象是只读的,不可修改,因为?类型是不确定的,可以代表范围内任意类型
根据对方法泛型(T)和泛型通配符(?)的分析就可以得出它们的区别:
- 本质区别:方法泛型(T)是指定,泛型通配符(?)是代表; 指定就是确定的意思,而代表却不知道代表谁,可以代表范围内所有类型。
- List 和List<?>可进行的函数操作不一样:
- List可以进行取值赋值操作,取值的时候用传入的数据类型接受就可以了。
- List<?>不可以写入值,只能进行取值、clear、remove操作,取出后全部强转为Object类型。一般用来作为参数接受外部的集合,或者返回元素类型不具体的集合。
List,List的区别
- Object和T不同点在于,Object是一个已确定的类,并没有泛指谁,而T在未约束前是泛值,可以泛指任何类型,包括Object。
例如:
public class TestShow<T> {
public void printList(List<T> list) {
}
public void printList1(List<Object> list) {
}
public void test() {
List<Object> list1 = new ArrayList<>();
list1.add(1);
List<String> list2 = new ArrayList<>();
list2.add("舔到最后一无所有");
//约束T为Object,即可传 List<Object>类参数
TestShow<Object> obj = new TestShow<>();
obj.printList(list1);
//约束T为String,即可传 List<String>类参数
TestShow<String> s = new TestShow<>();
s.printList(list2);
//参数List<Object>可传
printList1(list1);
//参数为List<String>编译时错误,类型不匹配
printList1(list2);
}
List,List的区别
- List出现类型转换错误会在编译时抛出
- List出现类型转换错误只有运行时才抛出
public void test(){
List l1 = new ArrayList();
l1.add("区块链出师未捷身先死?");
//在运行的时候才会抛出异常
//java.lang.String cannot be cast to java.lang.Integer
List<Integer> l2 = l1;
List<Object> l3 = new ArrayList();
l1.add("区块链寒冬?");
// 在编译的时候就会抛出异常
List<Integer> l4 = l3;
}
泛型的上下界:
1.泛型中上界和下界的定义:
ArrayList<? extends E> al=new ArrayList<? extends E>();
注意:接收并不是add(添加)
- ? extends E:接收E类型或者E的子类型(即上界为E)。
- ? super E:接收E类型或者E的父类型(即下界为E)
测试代码:
public class Food {}
public class Fruits extends Food {}
public class Apple extends Fruits {}
//普通类,与其他类无继承关系
public class Wood {}
/**
* 测试具有上界或下界的泛型通配符的可接收的类型;
*/
public class Test {
Food food;
Fruits fruits;
Apple apple;
Wood wood;
Object object;
/**
* 测试具有上界的泛型通配符的可接收的类型;
*/
public void test1() {
//接收上界类型本身和其子类型时编译正确
List<? extends Fruits> lowerBound1 = new ArrayList<Fruits>();
List<? extends Fruits> lowerBound2 = new ArrayList<Apple>();
//接收上界类型的父类和超类Object时,编译出错
List<? extends Fruits> lowerBound3 = new ArrayList<Food>();
List<? extends Fruits> lowerBound4 = new ArrayList<Object>();
//接收与泛型通配符无关系时,编译出错
List<? extends Fruits> lowerBound5 = new ArrayList<Wood>();
//测试结果:具有上界的泛型通配符只能接收上界类型本身和其子类型;
}
/**
* 测试具有下界的泛型通配符的可接收的类型;
*/
public void test2(){
//接收下界类型和其父类型、超类Object时编译正确
List<? super Fruits> lowerBound1 = new ArrayList<Fruits>();
List<? super Fruits> lowerBound2 = new ArrayList<Food>();
List<? super Fruits> lowerBound4 = new ArrayList<Object>();
//接收下界类型的子类时,编译出错
List<? super Fruits> lowerBound3 = new ArrayList<Apple>();
//接收与泛型通配符无关系时,编译出错
List<? super Fruits> lowerBound5 = new ArrayList<Wood>();
//测试结果:具有下界的泛型通配符可以下界类型本身和其至超类Object的父类型;
}
}
2.上界和下界的特性:
注意:add时只能向下转型;向上转型要强转;
- 具有上界的通配符泛型只能get,不能add除null外的对象;
- 具有下界的通配符泛型可以add,但get获取对象为object类型;
测试代码:
public class Food {}
public class Fruits extends Food {}
public class Apple extends Fruits {}
//普通类,与其他类无继承关系
public class Wood {}
/**
* 测试具有上界或下界的泛型通配符的add与get功能;
*/
public class Test {
Food food;
Fruits fruits;
Apple apple;
Wood wood;p
/**
* 测试具有上界的泛型通配符的add与get功能;
*/
public void test1() {
List<? extends Fruits> upperBound = new ArrayList<Fruits>();
//编译正确,可add null
upperBound.add(null);
//编译出错;无法add null外的值
upperBound.add(apple);
//编译正确,可get,且并用上界类型对象接收
Fruits fruits = upperBound.get(1);
//测试结果:具有上界的通配符泛型;get功能完整,但不能add除null外的对象;
}
/**
* 测试具有下界的泛型通配符的add与get功能;
*/
public void test2(){
List<? super Fruits> lowerBound = new ArrayList<Fruits>();
//可get,但获取的结果全部都为Object类型;
Object object = lowerBound.get(1);
//编译正常,可以add;
lowerBound.add(fruits);
lowerBound.add(apple);
//编译出错,接收父类对象是;即向上转型要强转;
lowerBound.add(food);
//编译正确,向上转型强转
lowerBound.add((Fruits)food);
/**
* 测试结果:具有下界的通配符泛型;get功能可以,但获取的结果全部都为Object类型;
* add可以,添加类型遵守向下转型原则,向上转型必须强转;
*/
}
}
3.导致特性出现的原因
- 具有上界的通配符泛型只能get,不能add除null外的对象的原因:
由于其接收对象可以是上界类型本身或者其子类型;导致add时编译器不能识别add的对象类型,所以会出现编译错误;- 具有下界的通配符泛型可以add,但get获取对象为object类型的原因:
具有下界的泛型add时编译器同样不能识别add的对象类型,由于其接收对象可以是下界类型本身或者其所有父类型;
而object又是所有对象的超类,所有编译器会在add时自动将要储存的对象转换为object并储存起来;这也是我们get获取对象时,获取的对象都是object类型的原因。
Java泛型的类型擦除
什么是泛型的类型擦除?
Java的泛型只在编译期间有效,在运行期被删除,也就是说所有泛型参数类型在编译后都会被清除掉。
简单的说就是,类型参数只存在于编译期,在运行时,Java 的虚拟机 ( JVM ) 并不知道泛型的存在。
例子:
public class a {
//编译时异常
//a(List<String>)' clashes with 'a(List<Integer>)'; both methods have same erasure
public void a(List<String> c){}
public void a(List<Integer> d){}
/**
* 原因就是List<String>,List<Integer>在运行时发生泛型擦除,擦除后两个都是List<>,
* 就会出现方法名相同,参数列表相同,jdk就会认为这两方法是相同的一个方法,抛出异常
*/
}
类型的擦除机制的实现过程
类型擦除机制实现的具体过程:
- 首先是找到用来替换类型参数的具体类。
- 这个具体类通常是指定类型参数的上界,但当指定类型没有上界时,就会使用Object代替上界,因为object是所以类的超类。
- 同时去掉原有的类型声明,即去掉<>的内容。
- 例如类List就变成了List,方法T get()声明就变成了Object get()。
- 最后是由编译器对生成的新类进行检查并修复,生成一些桥接方法的过程。
- 这是因为在泛型擦除机制实行后可能会导致之后生成的新类缺少部分必须的方法。
例如下面的代码:
class MyTest implements Comparable<String> {
/**当类型信息被擦除之后,上述类的声明变成了class MyTest implements Comparable。
* 但是这样的话,类MyTest就会有编译错误,因为没有实现接口Comparable声明的int compareTo(Object)方法。
* 这个时候就由编译器来动态生成这个方法。*/
public int compareTo(String str) {
return 0;
}
}
部分Java泛型的类型擦除后的转化例子:
List<String>、List<T> 擦除后的类型为 List。
List<String>[]、List<T>[] 擦除后的类型为 List[]。
List<? extends E>、List<? super E> 擦除后的类型为 List<E>。
List<T extends Serialzable & Cloneable> 擦除后类型为 List<Serializable>。
Java 为什么要采用泛型类型擦除机制?有以下两个原因:
- 避免 JVM 的大换血。如果 JVM 将泛型类型延续到运行期,那么到运行期时 JVM 就需要进行大量的重构工作了,提高了运行期的效率。
- 版本兼容。 在编译期擦除可以更好地支持原生类型(Raw Type)。
Java采用泛型的类型擦除机制后带来问题:
- 泛型类并没有自己独有的Class类对象,泛型的 class对象是相同的(可用于判断泛型擦除后类型是否相等)
- 泛型数组初始化时不能声明泛型类型
- instanceof (判断函数)不允许存在泛型参数
- 静态变量是被泛型类的所有实例所共享的。
(对于声明为MyClass的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过new MyClass还是new MyClass创建的对象,都是共享一个静态变量。)- 泛型的类型参数不能用在Java异常处理的catch语句中(因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException和MyException的。对于JVM来说,它们都是 MyException类型的。也就无法执行与异常对应的catch语句。)
例1:
public static void main(String[] args) {
List<String> ls = new ArrayList<String>();
List<Integer> li = new ArrayList<Integer>();
//输出结果为true,擦除后的类型都是 List
System.out.println(ls.getClass() == li.getClass());
}
例2:
public static void main(String[] args) {
//可行
ArrayList[] arrayLists = new ArrayList[]{};
/**
* 编译错误,List<Object>[] 与 List<String>[] 擦除后相同,编译器拒绝如此声明
*/
List<Object>[] list = new List<String>[]{};
//可行
List<Object>[] list1 = new ArrayList[]{};
}
例3:
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
/**
* 编译出错,Illegal generic type for instanceof,instanceof的非法泛型类型
*instanceof作用:是判断其左边对象是否为其右边类的实例
*/
System.out.println(list instanceof List<String>);
}
弥补泛型擦除的方法
-
由于泛型类并没有自己独有的Class类对象,所以当我们必须获取其class时,可以使用java提供Type接口。
- Type 是 Java 编程语言中所有类型的公共高级接口。它们包括原始类型、参数化类型、数组类型、类型变量和基本类型。它的实现类有Class。
-
当我们拿到一个Class,用Class. getGenericInterfaces()方法得到Type[],也就是这个类实现接口的Type类型列表。
-
当我们拿到一个Class,用Class.getDeclaredFields()方法得到Field[],也就是类的属性列表,然后用Field. getGenericType()方法得到这个属性的Type类型。
-
当我们拿到一个Method,用Method. getGenericParameterTypes()方法获得Type[],也就是方法的参数类型列表。
-
- Type 是 Java 编程语言中所有类型的公共高级接口。它们包括原始类型、参数化类型、数组类型、类型变量和基本类型。它的实现类有Class。
-
用ArrayList代替数组,或者是传入类型标记
-
引入类型标签,使用动态的isInstance()代替instanceof
-
用工厂方法或模版方法创建类型实例
泛型的类型转换机制
泛型类型转换机制在两个维度实现:
- 类型参数自身的继承体系结构(指的是对于 List和List这样的情况,类型参数String是继承自Object的)
- 泛型类或接口自身的继承体系结构。(指的是 List接口继承自Collection接口)
- 相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构
- 即List是Collection 的子类型,List可以替换Collection。这种情况也适用于带有上下界的类型声明
- 当泛型类的类型声明中使用了通配符的时候, 其子类型可以在两个维度上分别展开
- 其子类型可以在两个维度上分别展开。如对Collection<? extends Number>来说,其子类型可以在Collection这个维度上展开,即List<? extends Number>和Set<? extends Number>等;也可以在Number这个层次上展开,即Collection和Collection等。如此循环下去,ArrayList和HashSet等也都算是Collection<? extends Number>的子类型。
- 如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。
泛型传递
泛型传递指的是泛型被作为参数在已实例化的不同的类中进行传递的情况,理论上来说这种关系是可以无限制层次的传递下去。但下一层的参数类型和返回值都是由上一层决定的,详细描述见《泛型传递》这篇文章。