qiezijiajia

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

先看一段代码的输出

public static void main(String[] args){

        List<String> list1=new ArrayList<String>();
        List<Integer> list2=new ArrayList<Integer>();

        System.out.println(list1.getClass()==list2.getClass());
    }

结果为true。

为什么? 这里两个List均用到泛型,而泛型在编译的时候通过类型擦除已经没有了类型一说。所有两个class是相等的。

泛型是什么?

泛型一种较为准确的说法就是为了参数化类型,或者说可以将类型当做参数传递给一个类或者方法。

怎么理解参数化?

public class Cache {
    Object value;

    public Object getValue() {
        return value;
    }

    public void setValue(Object value) {
        this.value = value;
    }

}

假设 Cache 能够存取任何类型的值,于是,我们可以这样使用它。

Cache cache = new Cache();
cache.setValue(134);
int value = (int) cache.getValue();
cache.setValue("hello");
String value1 = (String) cache.getValue();

使用起来比较麻烦,每次使用均要做强制转换;

但是如果使用范围呢?前面讲到泛型可以将类型当做一个参数传递给一个类或者方法;这时在定义类的时候,就加上一个参数传递,即<T>

public class Cache<T> {
    T value;

    public Object getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }

}

这时我们定义了一个泛型的类,将value的类型也参数化了,使用时候把类型传递过去就行了

Cache<String> cache1 = new Cache<String>();
cache1.setValue("123");
String value2 = cache1.getValue();

Cache<Integer> cache2 = new Cache<Integer>();
cache2.setValue(456);
int value3 = cache2.getValue();

使用的时候将类型String,或者Integer传递过去就好了,这样就不会出现强制转换的错误了。

但是一旦定义好了类型后,如果类型不匹配,编译器就不会通过了。如下,List定义的String类型,但是传递的缺失Integer类型。

所以:

  1. 与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨。
  2. 当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。所以说,它是一种类型安全检测机制,一定程度上提高了软件的安全性防止出现低级的失误。
  3. 泛型提高了程序代码的可读性,不必要等到运行的时候才去强制转换,在定义或者实例化阶段,因为 Cache<String> 这个类型显化的效果,程序员能够一目了然猜测出代码要操作的数据类型。

那有没有办法绕过泛型类型检测机制?可以,我们可以看到List的add方法源码

里面还是一个泛型,也就是说add方法并没有指定具体的类型,那么我们可以通过反射的方式调用add方法,如下:

List<String> list1=new ArrayList<String>();
        Method method = list1.getClass().getDeclaredMethod("add",Object.class);
        method.invoke(list1,123);
        method.invoke(list1,123.98);
        for ( Object o: list1){
            System.out.println(o);
        }

输出结果:

123
123.98

int和double类型均可以输出。

泛型类或者泛型方法中,不接受 8 种基本数据类型。

所以,你没有办法进行这样的编码。

List<int> li = new ArrayList<>();
List<boolean> li = new ArrayList<>();

需要使用它们对应的包装类。

List<Integer> li = new ArrayList<>();
List<Boolean> li1 = new ArrayList<>();

对泛型方法的困惑

public <T> T test(T t){
    return null;
}

有的同学可能对于连续的两个 T 感到困惑,其实 <T> 是为了说明类型参数,是声明,而后面的不带尖括号的 T 是方法的返回值类型。 
你可以相像一下,如果 test() 这样被调用

test("123");

那么实际上相当于

public String test(String t);

Java 不能创建具体类型的泛型数组

这句话可能难以理解,代码说明。

List<Integer>[] li2 = new ArrayList<Integer>[];
List<Boolean> li3 = new ArrayList<Boolean>[];

这两行代码是无法在编译器中编译通过的。原因还是类型擦除带来的影响。

List 和 List 在 jvm 中等同于 List ,所有的类型信息都被擦除,程序也无法分辨一个数组中的元素类型具体是 List 类型还是 List 类型。

但是,

List<?>[] li3 = new ArrayList<?>[10];
li3[1] = new ArrayList<String>();
List<?> v = li3[1];

借助于无限定通配符却可以,前面讲过  代表未知类型,所以它涉及的操作都基本上与类型无关,因此 jvm 不需要针对它对类型作判断,因此它能编译通过,但是,只提供了数组中的元素因为通配符原因,它只能读,不能写。比如,上面的 v 这个局部变量,它只能进行 get() 操作,不能进行 add() 操作,这个在前面通配符的内容小节中已经讲过。

 

除了用 <T> 表示泛型外,还有 <?> 这种形式。 被称为通配符。

可能有同学会想,已经有了 <T> 的形式了,为什么还要引进 <?> 这样的概念呢?

class Base{}

class Sub extends Base{}

Sub sub = new Sub();
Base base = sub;            

上面代码显示,Base 是 Sub 的父类,它们之间是继承关系,所以 Sub 的实例可以给一个 Base 引用赋值,那么


List<Sub> lsub = new ArrayList<>();
List<Base> lbase = lsub;

最后一行代码成立吗?编译会通过吗?

答案是否定的。

编译器不会让它通过的。Sub 是 Base 的子类,不代表 List<Sub> 和 List<Base> 有继承关系。

但是,在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。

所以,通配符的出现是为了指定泛型中的类型范围

通配符有 3 种形式。

  1. <?> 被称作无限定的通配符。
  2. <? extends T> 被称作有上限的通配符。
  3. <? super T> 被称作有下限的通配符。

 

 

 

类型擦除

泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。

这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除

通俗地讲,泛型类和普通类在 java 虚拟机内是没有什么特别的地方。回顾文章开始时的那段代码

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();

System.out.println(l1.getClass() == l2.getClass());

打印的结果为 true 是因为 List<String> 和 List<Integer> 在 jvm 中的 Class 都是 List.class。

泛型信息被擦除了。

可能同学会问,那么类型 String 和 Integer 怎么办?

答案是泛型转译。

public class Erasure <T>{
    T object;

    public Erasure(T object) {
        this.object = object;
    }

}

Erasure 是一个泛型类,我们查看它在运行时的状态信息可以通过反射。

Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());

打印的结果是

erasure class is:com.frank.test.Erasure

Class 的类型仍然是 Erasure 并不是 Erasure<T> 这种形式,那我们再看看泛型类中 T 的类型在 jvm 中是什么具体类型。

Field[] fs = eclz.getDeclaredFields();
for ( Field f:fs) {
    System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
}

打印结果是

Field name object type:java.lang.Object

那我们可不可以说,泛型类被类型擦除后,相应的类型就被替换成 Object 类型呢?

这种说法,不完全正确。

我们更改一下代码。

public class Erasure <T extends String>{
//  public class Erasure <T>{
    T object;

    public Erasure(T object) {
        this.object = object;
    }

}

现在再看测试结果:

Field name object type:java.lang.String

我们现在可以下结论了,在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T> 则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String> 则类型参数就被替换成类型上限。

所以,在反射中。

public class Erasure <T>{
    T object;

    public Erasure(T object) {
        this.object = object;
    }

    public void add(T object){

    }

}

add() 这个方法对应的 Method 的签名应该是 Object.class。

Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());

Method[] methods = eclz.getDeclaredMethods();
for ( Method m:methods ){
    System.out.println(" method:"+m.toString());
}

打印结果是

 method:public void com.frank.test.Erasure.add(java.lang.Object)

也就是说,如果你要在反射中找到 add 对应的 Method,你应该调用 getDeclaredMethod("add",Object.class) 否则程序会报错,提示没有这么一个方法,原因就是类型擦除的时候,T 被替换成 Object 类型了。即在反射中这么调用

Method tt1 = erasure.getClass().getDeclaredMethod("add",Object.class);
如果把Object变为String,则出现下面错误

Exception in thread "main" java.lang.NoSuchMethodException: test.Erasure.add(java.lang.String)

 

想不出错呢? 那就把Erasure限定上限为String类型

Erasure <T extends String>

 

posted on 2017-08-23 14:55  qiezijiajia  阅读(184)  评论(0编辑  收藏  举报