Java中的泛型 Generics
一.初识泛型
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。由尖括号(<>
)分隔的类型参数部分跟在类名后面。它指定类型参数(也称为类型变量)T1,T2,...和 Tn。
//语法格式
class name<T1, T2, ..., Tn> { /* ... */ }
public class Printer<T> { //占位符T代表这个类可以传入任何类型的参数 //内部也可以传入多个参数<T,K> T content; public Printer(T content) { this.content = content; } public T getContent() { return content; } public void setContent(T content) { this.content = content; } public void print() { System.out.println(content); }
}
//调用函数
public class Main {public static void main(String[] args) {//使用Integer因为Java泛型底层实现是类型擦除,只有对象才容易在运行时擦除Printer<Integer> p=new Printer<>(123);p.print();}
}
泛式的优点:
- 编译时的强类型检查
泛型要求在声明时指定实际数据类型,Java 编译器在编译时会对泛型代码做强类型检查,而以 前没有泛式的时候是在运行时进行检查的。
- 避免了类型转换
- 泛型编程可以实现通用算法
二.有界限的泛型
在实际做项目时,类型参数不用满足任何类型,因此我们对于这个类型参数可以做一些约束
- 比如传入的参数必须是某个类型的子类型就在<>里用到extends
public class Printer<T extends Car> {}//代表T必须是Car的子类型(eg:volvo)
- 当然我们也可以用接口,此时仍然用extends,而不是implments
public class Printer<T extends Run> {}//代表T必须是Run接口的实现
- 也可以把接口和类同时写在一起,此时的class必须写在interface前面,否则会报错
//注意:extends 关键字后面的第一个类型参数可以是类或接口,其他类型参数只能是接口。
public class Printer<T extends Car & Run> {}
- 相反,如果必须是父类型的话就用super
public class Printer<T super Car> {}//T是Car的父类或者Car本身
三.泛型方法Generic Method
是否拥有泛型方法,与其所在的类是否是泛型没有关系。
情景:假设我们现在有这样一个需求,需要写一个print方法去打印任意的变量
private static void print(T content){System.out.println(content);//此时会报错}
当我们写上述的方法时,会报错。那是因为T其实只不过是一个占位符而已,它在Java里面并不是一种明确的类型。那我们又该如何告诉Java我们使用的是泛型呢
解决方法: 在返回值类型前加一个<>,再把T包含在里面
private static <T> void print(T content){System.out.println(content);//此时不会报错了}
四.类型通配符 ?
那当我们碰到的类型参数是List的情况又是怎么样的呢?
import java.util.ArrayList;
import java.util.List;public class Main {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("hello");list.add("world");print(list);//此时会报错,哪怕String继承Object,但是此时List<String>这个整体并不是List<Object>子类//正是由于泛型时基于类型擦除实现的,所以,泛型类型无法向上转型。//Integer 继承了 Object;ArrayList 继承了 List;但是 List<Interger> 却并非继承了 List<Object>。//这是因为,泛型类并没有自己独有的 Class 类对象。比如:并不存在 List<Object>.class 或是 List<Interger>.class,Java 编译器会将二者都视为 List.class}private static void print(List<Object> content){System.out.println(content);//此时会报错}
}
此时可以用通配符来解决这个问题
private static void print(List<?> content){System.out.println(content);}
//用以前的老方法<T>也可以private static<T> void print(List<T> content){System.out.println(content);}
五.类型擦除
Java 泛型是使用类型擦除来实现的,使用泛型时,任何具体的类型信息都被擦除了。
那么,类型擦除做了什么呢?它做了以下工作:
把泛型中的所有类型参数替换为 Object,如果指定类型边界,则使用类型边界来替换。因此,生成的字节码仅包含普通的类,接口和方法。
擦除出现的类型声明,即去掉
<>
的内容。比如T get()
方法声明就变成了Object get()
;List<String>
就变成了List
。如有必要,插入类型转换以保持类型安全。生成桥接方法以保留扩展泛型类型中的多态性。类型擦除确保不为参数化类型创建新类;因此,泛型不会产生运行时开销。
//编译之后集合的泛型是去泛型化的
//Java中集合的泛型,是防止错误输入的,只在编译阶段有效,绕过编译就无效了
//我们可以通过方法的反射来操作,绕过编译
public class GenericsErasureTypeDemo {public static void main(String[] args) {List<Object> list1 = new ArrayList<Object>();List<String> list2 = new ArrayList<String>();System.out.println(list1.getClass());System.out.println(list2.getClass());}
}
// Output:
// class java.util.ArrayList
// class java.util.ArrayList
示例说明:
上面的例子中,虽然指定了不同的类型参数,但是 list1 和 list2 的类信息却是一样的。
这是因为:使用泛型时,任何具体的类型信息都被擦除了。这意味着:
ArrayList<Object>
和ArrayList<String>
在运行时,JVM 将它们视为同一类型。
六.桥接方法
前面泛型的类型擦除我们提到了桥接方法,那么什么是桥接方法呢?
子类在重写父类的方法时,必须保证和父类有相同的方法名称,参数列表和返回类型,那当泛型父类和泛型接口的泛型参数被擦除了,那子类的重写方法岂不是不满足重写规则了,如下图所示,显然违背了重写规则,但程序为什么还是能正常编译呢?
所以Java为了解决类型擦除和重写的冲突,Java会在编译阶段,编译器为这些继承(实现)泛型类(接口)的子类创建一个合成方法,用来保持扩展泛型类型中的多态性,个被创建出来的方法,被我们称为桥接方法。
桥接方法,顾名思义,它是父类和子类之间的一座桥梁,它是实际上的重写了父类的方法,在方法内部委托了子类的原始方法,这样就巧妙的绕过了类型擦除带来的影响