zoukankan      html  css  js  c++  java
  • Java 泛型(转)

    先看一段代码的输出

    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>
  • 相关阅读:
    Java中的equals方法和==的区别
    C语言学习笔记--单向链表Markdown版本
    Tamias Blog's
    nginx图片服务器
    Nginx安装解决报错
    Detour框架注入样本无法正常启动(0x000007b)
    结构体指针中包含结构体指针
    Linux开发准备:Ubuntu14.04+Samba+MobaXterm+Source Insight 4.0
    在Visual Studio中将dll以资源的形式嵌入exe中
    MACD的价值不在于“金叉死叉”而在于背离
  • 原文地址:https://www.cnblogs.com/dpains/p/7418173.html
Copyright © 2011-2022 走看看