Java泛型深度解析以及面试题

Java的泛型是在JDK5引入的新特性,它提供了编译时类型安全检测机制,将代码的检测提前到了编译阶段,增加了代码的安全性,并且大大的增加了代码的复用性。为了与之前的JDK兼容,其实Java中的泛型是一种伪泛型,经过编译后会有类型擦除机制,保证了和之前的版本兼容。本篇我们就系统的对泛型进行一个分析,并对常见的面试题进行解答。

泛型知识部分

1 什么是泛型?使用泛型的好处是什么?

  • Java泛型是在JDK5引入的新特性,它提供了编译时类型安全检测机制。该机制允许程序员在编译时检测到非法的类型,泛型的本质是参数类型。
  • 使用泛型的好处:
    • 1 泛型可以增强编译时错误检测,减少因类型问题引发的运行时异常。
    • 2 泛型可以避免类型转换。
    • 3 泛型可以泛型算法,增加代码复用性。

2 Java中泛型的分类?

Java中的泛型分为三种,泛型类,泛型接口,泛型方法。

  • 泛型类:它的定义格式是class name,其中一种很常见的做法是定义我们的JavaBean,比如接口返回的json数据,需要和后台定义一个固定的数据返回格式,如下, 返回一个对象中包含了code和一个data, data是一个对象,我们不能固定它是什么类型,这时候就用T泛型来代替,大大增加了代码的复用性。
    public class Result {
        private T data;
        private int code;
    
        public T getData() {
            return data;
        }
    
        public void setData(T data) {
            this.data = data;
        }
    
        public int getCode() {
            return code;
        }
    
        public void setCode(int code) {
            this.code = code;
        }
    }
    
  • 泛型接口和泛型类使用相似。
  • 泛型方法: 它的定义是[public] [static] 返回值类型 方法名(T 参数列表)。只有在前面加这种的才能算是泛型方法,比如上面的setData方法虽然有泛型,但是不能算泛型方法。

3 泛型常见的类型参数

  • K 键
  • V 值
  • N 数字
  • T 类型
  • E 元素
  • S, U, V 等,泛型声明的多个类型

4 泛型的实现

  • 泛型类或者接口的实现要么是继续声明泛型类型,要么指明实际类型参数,如下:
    public class Generic {
    
    }
    
    class AA extends Generic {
    
    }
    
    class BB extends Generic {
    
    }
    

5 钻石运算符Diamond

  • JDK7以下版本需要 Box box = new Box();
  • JDK7及以上版本 Box integerBox1 = new Box<>();

6 泛型中的类型名词

  • 原始类型:缺少实际类型变量的泛型就是一个原始类型
    class Box{} 
    Box b = new Box(); //这个Box就是Box的原始类型 
    
  • 泛型类型Person整个就是泛型类型。
  • 类型参数Person中的T就类型参数。
  • 参数化类型Person整个成为参数化类型(ParameterizedType)
  • 实际类型参数Person中的Man就称之为实际类型参数。

7 受限的类型参数是什么?为什么提供受限的类型参数?

  • 它的作用是对泛型变量的范围作出限制, 格式:
    单一限制:
    多种限制:
  • 多种限制的时候,类必须写在第一个。

8 类型推断

  • 写了下面这段代码来解释类型推断, 类型推断是Java编译器查看每个方法调用和相应声明以确定使调用适用的类型参数的能力。推断算法确定参数的类型,以及确定结果是否被分配或返回的类型(如果有)。最后,推断算法尝试找到与所有参数一起使用的最具体的类型。因为String和ArrayList的共同类型是Serializable,所以最终推断出了Serializable类型。
    //public final class String implements java.io.Serializable, Comparable, CharSequence
    //class ArrayList extends AbstractList  implements List, RandomAccess, Cloneable, java.io.Serializable
    public static void main(String[] args) {
        Serializable serializable = get("1", new ArrayList<>());
    }
    
    private static  T get(T t, T t2) {
        return t2;
    }
    

9 通配符

  • 通配符用?标识,分为受限制的通配符和不受限制的通配符。
    通配符 在框架中使用更多,它使代码更加灵活。比如ListList是没有任何关系的。如果我们将print方法中参数列表部分的List声明为List list, 那么编译是不会通过的,但是如果我们将List定义为List list 或者List list,那么在编译的时候就不会报错了。
    	public static void main(String[] args) {
            List list = new ArrayList<>();
            print(list);
        }
    
        //通配符
        // private static void print(List list){
        private static void print(List list){
            System.out.println(list);
        }
    
    • 受限制的通配符:语法为,它可以扩大兼容的范围在XXX以及它的子类范围。
      比如上面例子中print中如果改为List,虽然它能存储Integer和Double等类型的元素,但是作为参数传递的时候,它只能接受List这一种类型。如果声明为List list就不一样了,相当于扩大了类型的范围,使得代码更加的灵活,代码复用性更高。和extends一样,只不过extends是限定了上限,而super是限定了下限。
    • 非受限制的通配符:不适用关键字extends或者super。比如上面print参数列表声明为List list也可以解决问题。?代表了未知类型。所以所有的类型都可以理解为List的子类。它的使用场景一般是泛型类中的方法不依赖于类型参数的时候,比如list.size(), 遍历集合等,这样的话并不关心List中元素的具体类型。后面会有面试题单独对它和其它的类型作比较。

10 Java泛型的PECS原则(Producter extends Customer super)

  • 如果你只需要从集合中获得类型T , 使用通配符

  • 如果你只需要将类型T放到集合中, 使用通配符

  • 如果你既要获取又要放置元素,则不使用任何通配符。例如List
    PECS原则增加了API的灵活性,在集合框架中经常见到它的影子。比如Collections.copy方法:

    public static  void copy(List dest, List src) {
    	...
    }
    
  • 桥接方法 父类类型擦拭后变为了Object,子类这里是Integer, 不匹配,其实编译后字节码中这里会生成一个桥接方法。

    public class MyNode extends Node {
        public MyNode(Integer data) { super(data); }
    
        public void setData(Integer data) {
            System.out.println("MyNode.setData");
            super.setData(data);
        }
    }    
    

11 类型擦除

  • 类型擦除作用:因为Java中的泛型实在JDK1.5之后新加的特性,为了兼容性,在虚拟机中运行时是不存在泛型的,所以Java泛型是一种伪泛型,类型擦除就保证了泛型不在运行时候出现。
  • 场景:编译器会把泛型类型中所有的类型参数替换为它们的上(下)限,如果没有对类型参数做出限制,那么就替换为Object类型。因此,编译出的字节码仅仅包含了常规类,接口和方法。
    • 在必要时插入类型转换以保持类型安全。
    • 生成桥方法以在扩展泛型时保持多态性
  • Bridge Methods 桥方法
    当编译一个扩展参数化类的类,或一个实现了参数化接口的接口时,编译器有可能因此要创建一个合成方法,名为桥方法。它是类型擦除过程中的一部分。下面对桥方法代码验证一下:
    public class Node {
        T t;
    
        public Node(T t) {
            this.t = t;
        }
    
        public void set(T t) {
            this.t = t;
        }
    }
    
    class MyNode extends Node {
    
        public MyNode(String s) {
            super(s);
        }
    
        @Override
        public void set(String s) {
            super.set(s);
    }	
    
    • 上面Node是一个泛型类型,没有声明上下限,所以在类型擦除后会变为Object类型。而MyNode类已经声明了实际类型参数为String类型,这样在调用父类set方法的时候就会出现不匹配的情况,所以虚拟机在编译的时候为我们生成了一个桥方法,我们通过javap -c MyNode.class查看字节码文件,看到确实为我们生成了一个桥方法。
      Java泛型深度解析以及面试题_第1张图片

面试题部分

  • 1 Java泛型的原理?什么是泛型擦除机制?

    • Java的泛型是JDK5新引入的特性,为了向下兼容,虚拟机其实是不支持泛型,所以Java实现的是一种伪泛型机制,也就是说Java在编译期擦除了所有的泛型信息,这样Java就不需要产生新的类型到字节码,所有的泛型类型最终都是一种原始类型,在Java运行时根本就不存在泛型信息。
    • 类型擦除其实在类常量池中保存了泛型信息,运行时还能拿到信息,比如Gson的TypeToken的使用。
    • 泛型算法实现的关键:利用受限类型参数。
  • 2 Java编译器具体是如何擦除泛型的

    • 1 检查泛型类型,获取目标类型
    • 2 擦除类型变量,并替换为限定类型
      如果泛型类型的类型变量没有限定(),则用Object作为原始类型
      如果有限定(),则用XClass作为原始类型
      如果有多个限定(T extends XClass1&XClass2),则使用第一个边界XClass1作为原始类
    • 3 在必要时插入类型转换以保持类型安全
    • 4生成桥方法以在扩展时保持多态性
  • 3 Array数组中可以用泛型吗? 不能,简单的来讲是因为如果可以创建泛型数组,泛型擦除会导致编译能通过,但是运行时会出现异常。所以如果禁止创建泛型数组,就可以避免此类问题。

  • 4 你可以把List传递给一个接受List参数的方法吗?
    不能,虽然String是Object的子类,但是ListList没有任何关系

     ArrayList arrayList1=new ArrayList();
     ArrayList arrayList1=new ArrayList();
      
       
  • 5 Java中List和原始类型List之间的区别?

    • 原始类型和带参数类型之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查,通过使用Object作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String或Integer。这道题的考察点在于对泛型中原始类型的正确理解。
    • 它们之间的第二点区别是,你可以把任何带参数的泛型类型传递给接受原始类型List的方法,但却不能把List传递给接受List的方法,因为会产生编译错误。
    • 6 Java中Set与Set到底区别在哪?

      • Set可以支持添加数据,并且不会进行类型检查,但是Set 是支持类型检查的。
    • 7 Java中ListList之间的区别是什么?

      • 虽然他们都会进行类型检查,实质上却完全不同。List 是一个未知类型的List,而List其实是任意类型的List。你可以把List, List赋值给List,却不能把List赋值给List
      • 8 Java泛型PESC原则是什么? 什么是泛型中的限定通配符和非限定通配符?

        • 如果你只需要从集合中获得类型T , 使用通配符
        • 如果你只需要将类型T放到集合中, 使用通配符
        • 如果你既要获取又要放置元素,则不使用任何通配符。例如List
          PECS原则增加了API的灵活性,在集合框架中经常见到它的影子。比如Collections.copy方法:
        public static  void copy(List dest, List src) {
        	...
        }
        
        • 非限定通配符既不能存也不能取, 一般使用非限定通配符只有一个目的,就是为了灵活的转型。其实List 等于 List
      • 9 如何阻止Java中的类型未检查的警告?

        • 如果你把泛型和原始类型混合起来使用,例如下列代码,Java 5的javac编译器会产生类型未检查的警告,例如List rawList = new ArrayList(),注意: xxx.java使用了未检查或称为不安全的操作,这种警告可以使用@SuppressWarnings(“unchecked”)注解来屏蔽。
      • 10 C++模板和java泛型之间有何不同?

        • java泛型实现根植于“类型消除”这一概念。当源代码被转换为Java虚拟机字节码时,这种技术会消除参数化类型。有了Java泛型,我们可以做的事情也并没有真正改变多少;他只是让代码变得漂亮些。鉴于此,Java泛型有时也被称为“语法糖”。

        • 这和 C++模板截然不同。在 C++中,模板本质上就是一套宏指令集,只是换了个名头,编译器会针对每种类型创建一份模板代码的副本。

        • 由于架构设计上的差异,Java泛型和C++模板有很多不同点:
          C++模板可以使用int等基本数据类型。Java则不行,必须转而使用Integer。
          在Java中,可以将模板的参数类型限定为某种特定类型。
          在C++中,类型参数可以实例化,但java不支持。
          在Java中,类型参数不能用于静态方法(?)和变量,因为它们会被不同类型参数指定的实例共享。在C++,这些类时不同的,因此类型参数可以用于静态方法和静态变量。
          在Java中,不管类型参数是什么,所有的实例变量都是同一类型。类型参数会在运行时被抹去。在C++中,类型参数不同,实例变量也不同。

      • 11 泛型使用注意事项

        为了高效使用Java泛型,你必须考虑一下下面的约束条件:

        • 1 无法利用原始类型来创建泛型 int
        • 2 无法创建类型参数的实例 可以通过反射 E e = new E();
        • 3 无法创建参数化类型的静态变量 public static T t
        • 4 无法对参数化类型使用转换或者instanceof关键字 可以使用if (list instanceof ArrayList)
        • 5 无法创建参数化类型的数组 List[] arrayOfLists = new List[2]; // compile-time error
        • 6 无法创建、捕获或是抛出参数化类型对象
        • 7 当一个方法的所有重载方法的形参类型擦除后,如果它们具有了相同的原始类型,那么此方法不可重载。
      • 你可能感兴趣的:(java)