Java泛型详解

泛型是什么

泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。 Java语言引入泛型的好处是安全简单。
在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。
泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,以提高代码的重用率。

使用泛型有什么优点

如果直接说泛型的优点,可能没有那么直观,先通过一个例子来说明不使用泛型带来的问题:

public static void main(String[] args) {
    	//不适用泛型的名称集合
        List nameList = new ArrayList();
        nameList.add("Generic");
    	Integer age = new Integer(20);
        nameList.add(age); //不小心插入Integer类型数据
        for (Object obj: nameList) {
            String name = (String) obj;
            System.out.println(name);
        }
    }

这段代码能通过编译,但在运行时会抛出类型转换异常(ClassCastException):

Generic
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
	at flyweight.Client.main(Client.java:14)

这会引入潜在的问题:测试阶段没有发现,但在生产系统运行时触发这个问题。

使用泛型时:

public static void main(String[] args) {
        //使用泛型的名称集合
        List<String> nameList = new ArrayList<String>();
        nameList.add("Generic");
        Integer age = new Integer(20);
        nameList.add(age); //插入Integer类型数据时,此处编译器会提示add(java.lang.String) in List cannot be applied to (java.lang.Integer)
        for (Object obj: nameList) {
            String name = (String) obj;
            System.out.println(name);
        }
    }

使用泛型,问题可以在程序编译时期发现,避免了在运行时才出现这个安全隐患,提高代码的健壮性。

泛型的类型

泛型分为三种:泛型类、泛型接口、泛型方法。

泛型类和泛型接口

泛型类和泛型接口的作用范围是类(接口)的作用范围,即泛型声明的参数在类(接口)中都可以使用。如ArrayList、List等集合类和集合接口中的泛型。

泛型类和泛型接口的语法格式为:类名或接口 <泛型声明> {…}

// ArrayList
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
	// ...
}
// List
public interface List<E> extends Collection<E> {
	// ...
}

泛型方法

泛型声明的参数的作用范围是在方法中可见。泛型方法独立于类,类是否是泛型类没有关系。

泛型类和泛型接口的语法格式为:方法权限声明 <泛型声明> 方法返回值 方法名称 (泛型声明参数) {…}。如在Arrays.工具类中的asList静态方法:

public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
}

泛型方法的使用场景:

(1)如果static方法要使用泛型,就必须使其成为泛型方法。因为静态方法无法访问类上定义的泛型,如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。

(2)优先考虑使用泛型方法而不是泛型类或接口,泛型方法能使方法独立于类而产生变化,具有更灵活的特性。

泛型通配符" ?"

在某些情况下,当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。

public static Object getObject(String classPath) throws Exception {
    Class<?> clazz = Class.forName(classPath);
    return clazz.newInstance();
}

如上面代码,对于getObject()方法形参,客户端传入的参数是什么类型的类无法确定,这时候使用通配符"?"即可压制警告(警告信息:Class is a raw type. References to generic type Class should be parameterized)。

// 可以确认List<Object> list中的数据是Number的子类,但是无法确定是Integer还是Long类型,但是可以操作Number中的方法
public static void sumList(List<?> list) {
    int intVal = 0;
    for (Object object : list) {
        if(object instanceof Number) {
            Number number  = (Number) object;
            intVal += number.intValue();
        }
    }
    System.out.println("intVal = " + intVal);
}

对于上面的sumList方法的逻辑是统计Number数据类型的整型值总和。看起来代码没问题,但是在能够知道sumList需要处理的是Number类型的情况下,有没有一种方法可以限定形参中的泛型类型数据是Number或是及子类的数据?这样就可以避免“if(object instanceof Number)”判断,提高方法阅读性和代码健壮性。

可以使用泛型限定解决此类问题。

泛型限定

泛型限定为泛型的调用者提供更精确的编译能力。这样做,编译器可以直接检查传入的泛型是否符合限定条件。如果不符合,就不通过编译,符合,则程序可以直接放心调用。

泛型限定类别:

(1)上限,格式:"? extends E" ,可以接收E类型或者E的子类型。

(2)下限,格式:"? super E",可以接收E类型或者E的父类型。

注:只有通配符"?"的是泛型无限定,即可以传入任何类型的数据。

上限

对于上限 “? extends E” ,E是一个具体的类,通配符?代表的是E对象或者E的子类对象。在List<? extends E>中,可以操作E类型对象的方法或属性。因为上限可以确认类型是具体的E类型的对象(E或E的子类),所以可以操作数据类型为E对象的方法或属性。

但是List<? extends E>不能添加E类或者E类的子类对象,因为List<? extends E>需要的对象是一个具体范围的对象,而E类可以通过继承而可能存在无数种子类(只要E类不是final类),所以E范围不能只能知道上限,不能指定下限。

即:对于上限泛型,可以操作E类对象的相关方法或属性,不能在List<? extends E>集合中添加E类对象。

//不能在上限泛型集合中添加元素
public void extendsList(List<? extends Number> extendsList){
    //add(capture<? extends java.lang.Number>) in List cannot be applied  to(java.lang.Integer)
    extendsList.add(new Integer(10));
}
//可以操作E类对象的相关方法或属性
public void extendsList(List<? extends Number> extendsList) {
    for (Number number : extendsList) {
        System.out.println(number.intValue());
    }
}

下限

对于下限"? super E",E是具体的类,通配符?代表的是E对象或者E的父类对象。在List<? super E>中,可以操作Object中的方法,因为Object是所有的类的父类(包括接口)。但通配符?代表的是Object的子类且是其中E或E的父类某个类的对象,无法确认是哪一个类的对象。但是知道了下限,所以就可以对List<? super E>集合添加E类或E子类的对象了,因为泛型范围是可以确定的(在Object至E之间)。

即:对于上限泛型,不可以操作E类对象的相关方法或属性,可以在List<? extends E>集合中添加E类或E子类的对象。

//不可以操作E类对象的相关方法或属性
public void superList(List<? super Number> superList) {
    //Incompatible types. Required:capture of ? super Number Found: Number
    for (Number number : superList) {
        System.out.println(number.intValue());
    }
}

//可以在下限泛型集合中添加元素
public void superList(List<? super Number> superList) {
    superList.add(new Integer(10));
}

在日常中,只要记住“PECS(Producer Extends Consumer Super)原则”即可。意思为:

(1)频繁往外读取内容的,适合用上界Extends。

(2)经常往里插入的,适合用下界Super。

一篇详细解释<? extends E>和<? super E>区别的文章链接:https://www.zhihu.com/question/20400700

泛型的生命周期

Java的泛型信息只存在于编译期间,编译之后所有的泛型信息都会被擦除。

List<Integer> integerList = new ArrayList<>();
List<String> stringList = new ArrayList<>();
System.out.println(integerList.getClass() == stringList.getClass());
// 打印结果:true

这就是 Java 泛型的类型擦除造成的,因为不管是 ArrayList<Integer> 还是 ArrayList<Long>,在编译时都会被编译器擦除成了 ArrayList。

Java引入泛型是为了加强参数类型的安全性,在编译期间根据泛型声明及早发现类型转化问题,减少了类型的转换。但需要知道的是,在JDK1.5之前,是没有泛型的,Java作为一个企业级开发语言,需要考虑向下兼容,所以对于泛型,只存在于编译期间,编译成字节码后,ArrayList<Integer>或 ArrayList<Long>都会被擦除成ArrayList,这样就能保持和JDK1.5之前版本兼容。

ArrayList<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
Class listClass = list.getClass();
Method methodList = listClass.getMethod("add", Object.class);
methodList.invoke(list, new Integer(10));
for (Object obj : list) {
    System.out.println("obj = " + obj +", class = " + obj.getClass().getName());
}
/**
打印结果:
    obj = hello, class = java.lang.String
    obj = world, class = java.lang.String
    obj = java, class = java.lang.String
    obj = C++, class = java.lang.String
    obj = 10, class = java.lang.Integer
*/

反射是在程序运行时期操作类的字节码来操作对象的。通过上面的例子可以说明,泛型只存在于编译时期。

在运行期间获取泛型信息

虽然泛型类在编译的时候会进行泛型擦除,不过泛型信息还是会保存在泛型类的字节码对象中。

获取到泛型类型的要求:

(1)必须具有真实类型的存在。

(2)泛型的类型是明确的(如List<User>是明确的,List<T>是不明确的)。

abstract class Generic<T> {
	// 为了方便其他类获取,将其作为公有属性
	public Class<?> clazz;
	
	public Generic(){
		ParameterizedType pt = (ParameterizedType) 
            this.getClass().getGenericSuperclass();
		// 获取泛型参数列表,如Map<K,V>,那么返回K,V的数组
		Type[] types = pt.getActualTypeArguments();
		//因为这里只有一个泛型T,所以获取第一个元素
		clazz = (Class<?>) types[0];
	}
}

class MyGeneric extends Generic<String> {
	
}

Client:

public static void main(String[] args) {
    MyGeneric myGeneric = new MyGeneric();
    System.out.println(myGeneric.clazz);
}

打印结果:

java.lang.String

注:对 “泛型的类型是明确的” 进一步说明:

class Generic<T> {
	// 为了方便其他类获取,将其作为公有属性
	public Class<?> clazz;
	
	public Generic(){
		ParameterizedType pt = (ParameterizedType) 
            this.getClass().getGenericSuperclass();
		// 获取泛型参数列表,如Map<K,V>,那么返回K,V的数组
		Type[] types = pt.getActualTypeArguments();
		//因为这里只有一个泛型T,所以获取第一个元素
		clazz = (Class<?>) types[0];
	}	
}

Client:

public static void main(String[] args) {
    Generic<String> generic = new Generic<>();
    System.out.println(generic.clazz.getName());
}

打印结果:

Exception in thread "main" java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType
	at com.zhxj.structure.Generic.<init>(MyGenericTest.java:27)
	at com.zhxj.structure.MyGenericTest.main(MyGenericTest.java:10)

为什么会这样呢?

首先Generic<T>是一个泛型不确定的类,而类的Class在JVM中是唯一的。这样会导致这样的情况:如果在代码中同时实例化这样的两个对象:

Generic<Integer> g1 = new Generic<Integer>();
Generic<String> g1 = new Generic<String>();

那么Generic类对应的Class对象应该记录Integer还是String的泛型信息呢?这没法确定。

所以泛型的类型是明确的指的是泛型类对应的Class对象的泛型信息必须是唯一确定的。

而对于MyGeneric类,因为MyGeneric本身的泛型只能是String,所以可以唯一确定MyGeneric对应的Class对象,当然也就可以通过反射api获取到MyGeneric的泛型信息了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值