Java泛型详解
最近在重新整理以前学过的知识,查漏补缺,发现很多忘记了,所以觉得应该好好记下来。
一,泛型的本质
泛型的本质是参数化类型,为类型指定一个参数,在使用/调用时由传入的参数确定类型;泛型可以使用在类、接口、方法中,称为泛型类、泛型接口、泛型方法。
泛型是JDK5引进的新特性,提供了编译时类型安全检测机制,使得程序员能在编译时检测出非法的类型。
二,泛型的好处
讨论泛型的好处前,我们先说说没有泛型前的做法,以便对比理解。
a,泛型之前
引入泛型之前,通用程序的设计利用继承实现,例如ArrayList类通过维护一个Object类型的数组来达到通用的目的。
public class beforeGeneric {
/*
此ArrayList仅为了表示泛型前的通用设计思路,不具实用性
*/
static class ArrayList{
private Object[] data=new Object[100];
private int current=0;
public Object get(int index){
return data[index];
}
public void add(Object obj){
data[current++]=obj;
}
}
public static void main(String[] args) {
//如果我们打算创建一个用来储存String类型的容器
ArrayList arrayList = new ArrayList();
//你可以随便插入任意类型的数据
arrayList.add(new Integer(111));
arrayList.add("zzz");
//可以编译通过,然后运行报错ClassCastException
String res= (String) arrayList.get(0);
//暴漏两个问题:
// 1,必须手动强转类型;
// 2,无法约束插入数据的类型,插入Integer类型数据没有问题,取出时发生了错误
}
}
为了优化解决这些问题引入泛型
- 必须手动强转类型;
- 无法约束插入数据的类型;
b,泛型之后
引入泛型后,这种潜在的错误可以在编译时检测出来,而不是在运行时出错
public class GenericType {
public static void main(String[] args) {
ArrayList<String> list=new ArrayList<>();
//不能插入Integer类型数据,编译不通过
//list.add(new Integer(111));
list.add("zzz");
//不需要手动强转
String str=list.get(0);
System.out.println(str);
}
}
对比之后,我们不难发现,泛型有更好的安全性,可读性。
三,泛型的使用
用<>可以在类、接口、方法定义时声明泛型
a,泛型类
访问修饰符+class+类名+< T >{}
public class GenericClass<T> {
private T value;
public T getValue(){
return this.value;
}
public void setValue(T value){
this.value=value;
}
}
实例化方式:
public static void main(String[] args) {
GenericClass<String> strGeneric = new GenericClass<>();
GenericClass<Integer> intGeneric = new GenericClass<>();
strGeneric.setValue("zzz");
intGeneric.setValue(Integer.valueOf(111));
System.out.println("strGeneric:"+strGeneric.getValue());
System.out.println("intGeneric:"+intGeneric.getValue());
//输出
//strGeneric:zzz
//intGeneric:111
}
不指定类型亦可:
public static void main(String[] args) {
GenericClass genericClass=new GenericClass();
genericClass.setValue("zzz");
System.out.println(genericClass.getValue());
//zzz
}
}
b,泛型接口
访问修饰符+interface+接口名+< T >{}
public interface GenericInterface<T> {
T getValue();
void setValue(T value);
}
实现接口
//未指明接口泛型参数时,应该在类名后声明泛型
public class GenericImpl1<T> implements GenericInterface<T>{
private T value;
@Override
public T getValue() {
return this.value;
}
@Override
public void setValue(T value) {
this.value=value;
}
}
//指明接口泛型参数时,应该在实现方法时替换泛型参数为实际类型
public class GenericImpl2 implements GenericInterface<String>{
private String value;
@Override
public String getValue() {
return this.value;
}
@Override
public void setValue(String value) {
this.value=value;
}
}
c,泛型方法
访问修饰符 + < T > + 返回类型 + 方法名 + (参数类型 + 参数名) {}
//方法上声明的泛型只能在方法的内部使用
public <E> E GenericMethod(Class<E> clazz) throws IllegalAccessException, InstantiationException {
E res = clazz.newInstance();
return res;
}
d,泛型通配符
<?>表示参数可以是任意类型(无界)
<? extends father>表示参数必须得是father的子类(有上界)
<? super son>表示参数必须得是son的父类(有下界)
四,泛型的原理
类型擦除
在编译期间,所有泛型信息会被擦除;编译成字节码文件后,所有泛型类型参数会被替换成固定的类型,如< T >声明则会替换成Object类型,< T extends A >声明则会被替换成A类型
如:
public class GenericClass<T> {
private T value;
public T getValue(){
return this.value;
}
public void setValue(T value){
this.value=value;
}
}
编译后的字节码中类的定义会变成类似如下(理解一下意思)
public class GenericClass {
private Object value;
public Object getValue(){
return this.value;
}
public void setValue(Object value){
this.value=value;
}
}
类型擦除的验证
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
ArrayList<String> list=new ArrayList<>();
list.add("zzz");
list.getClass()
.getMethod("add",Object.class)
.invoke(list,Integer.valueOf(111));
for(Object obj:list){
System.out.println(obj);
}
//可以输出zzz 111
}
说明虽然我们不能在编译前给ArrayList随便插入,但是在编译后运行时泛型类型参数已经被擦除了,我们可以突破编译时类型安全检测随便插入。
五,注意事项
1,引用传递要“门当户对”
ArrayList<String> arrayList1=new ArrayList<Object>();//编译错误
ArrayList<Object> arrayList1=new ArrayList<String>();//编译错误
2,泛型类型变量不可以是基本数据类型
3,运行时类型查询
if( arrayList instanceof ArrayList<String>) //不行
if( arrayList instanceof ArrayList) //行
if( arrayList instanceof ArrayList<?>) //行
4,在静态方法和静态变量中的泛型
public class Test2<T> {
public static T one; //编译错误
public static T show(T one){ //编译错误
return null;
}
}
静态方法和静态变量属于类,在类加载时就被一起加载到内存了,但是泛型在类实例化时才确定,所以不行;而且静态变量作为共享的变量,不确定类型比较不安全
下面的情况注意区分
public class Test2<T> {
public static <T> T show(T one){//这是正确的
return null;
}
}