美文网首页
Java泛型与Java泛型面试题

Java泛型与Java泛型面试题

作者: JeckZyang | 来源:发表于2020-04-26 12:57 被阅读0次
    泛型定义

    Java泛型(generics)是JDK5中引入的一个新特性, 泛型提供了编译时期的类型安全检查机制, 这机制允许程序员在编译时监测到非法的类型. 泛型的本质是不确定的类型(参数类型), 也就是说所操作的数据类型被指定为一个参数类型,这个参数泛型不会存在于JVM虚拟机,所以说在java中泛型其实是一种伪泛型.

    为什么使用泛型?
    • 可以增强编译时错误监测,减少因类型问题引发的运行时的异常
      (\color{red}{ClassCastException}),具有更强的类型检查.
    • 可以避免类型转换.
    • 方法的形参中使用泛型,增加程序的复用性.
    public class Generics{
      public static void main(String[] args){
        gMethod01() 
        gMethod02()
      }
    
      static void gMethod01{
        List list = new ArrayList();//没有使用泛型
        list.add("hello");
        String str = list.get(0);// 需要强转(向下转型)
      }
    
      static void gMethod02{
        List<String> list = new ArrayList<>();//使用了泛型
        list.add("hello");
        String str = list.get(0);//不需要强转
      }
    }
    
    泛型的种类
    • 泛型类

      泛型类格式
      public class calssName<G1,G2,G3...>{}

    • 泛型接口

      泛型接口格式
      public interface IName<G1,G2,G3...>{}

    • 泛型方法

      泛型方法格式
      private <K,V> boolean gMethod(K k1,V v1) {};
      使用泛型方法
      MethodUtil.<K,V> gMethod(k1,v1)

      /**
      泛型类(泛型接口 interface className<G1,G2,G3>
      class calssName<G1,G2,G3....>
    
      泛型通常使用的字母(来自官方建议),
      E - Element(Java Collections Framework广泛使用)
      K - Key
      N - Number
      T - Type
      V - value
      S,U,V ......
      */
      interface Generics<E,K,T,S,U,V>{ //泛型接口
    
      }
      interface Generics<T>{ //泛型接口
      }
      //泛型接口的使用
      /*
        这种是不确定泛型T的类型, 必须在类名后面添加声明泛型T
      */
      class GenericsImpl<T> implements Generics<T>{ /
    
      }
      /*
        这种是已经确定了泛型T的类型为String, 类前面不要加上泛型的声明.
      */
      class GenericsImpl2 implements Generics<String>{ 
    
      }
    
      public class Box<T>{  //泛型类
        private T t;
        public void setT(T t){  //注意这不是泛型方法.
          this.t = t;  
        }
    
        public T getT(){  // 注意这不是泛型方法
          return t;
        }
    
        public void test1(List<String> lists){  // 注意这不是泛型方法
        }
        
        public void test2(List<?> list){ //注意这不是泛型方法, 通配符"?" 后面会后介绍
        }  
    
        public <T> void testT(T t){ // 泛型方法 (此T 非类上面的T)
    
        }
      }
    
    类型参数&类型实参

    Box<T> 中的T 为类型参数
    Box<String> 中的String为类型实参

    The Diamond钻石运算符也叫菱形运算符 \color{red}{<>}

    JDK7以下版本
    Box<String> strBox = new Box<String>();
    JDK7及以上版本
    Box<String> strBox = new Box<>(); // The Diamond(菱形) 类型推断
    类型推断在本文后面有解释

    原始类型

    缺少实际类型变量的泛型就是一个原始类型 后面会介绍原始类型和普通类型的区别.

     public Box<T>{
        public static void main(String[] args){
           Box box = new Box(); //这个Box 就是Box <T> 的原始类型
           ArrayList list = new ArrayList();// 这个ArrayList 就是 ArrayList<E>的原始类型
        }
     }
    
    受限的类型参数

    对泛型变量的范围作出限制
    单一限制: <N extends Number>
    多种限制: <N extends A & B & C....>
    extends表达的含义: 这里指的是广义上的"扩展",兼有"类继承" 和 "接口实现" 之意
    多种限制下的格式语法要求:如果上限类似是一个类, 必须第一个位置标出,否则编译失败(\color{red}{因为java是单继承})

    单一限制

    public class Box<T>{
      private  T t;
      public setT(T t){
        this.t = t;
      }
      public T getT(){
        return t;
      }
      public <U extends Number> void inspect1(U u){
        System.out.println("T: " + t.getClass().getName());
        System.out.println("U: "+ u.getClass().getName)
      }
      public <U> void inspect2(U u){
        System.out.println("T: " + t.getClass().getName());
        System.out.println("U: "+ u.getClass().getName)
      }
    
       public static void main(String[] args){
          Box<Integer> integerBox1 = new Box<>();
          integerBox1.set(new Integer(10));
          integerBox1.inspect1(10L);//succes
          integerBox1. inspect1("some text") //编译失败 err 
    
          integerBox1. inspect2("some text") //编译成功 ok
      }
    }
    

    多重限制

     public class Test{
       static class A{
       }
       static class A1{
       }
       static interface B{
       }
       static interface C{
       }
       static class D<T extends B & A & C>{ // err 编译失败, 
       }
    
       static class D2<T extends A & A1 & B & C>{//err 编译失败,因为java是单继承
       }
       /*
       具有多个限定的类型变量是范围中列出的所有类型的子类型.
       范围中列出的子类型, 最多只能有一个类,并且这个类必须在第一个位置, 
       否则编译失败. 
       */
       static class D1<T extends A & B & C>{ // OK 编译成功
       
       }
     }
    
    为什么使用受限类型?

    因为使用受限类型,在方法或者是类中泛型实例可以使用受限类里面的公有方法. 从而可以达到代码复用.

    class Fruits{
      public <T> boolean isFruits(T t){
          if (t instanceof Fruits){
              return true;
          }
          return false;
      }
    }
    
    class Apple extends Fruits{
    
    }
    
    class Orange extends Fruits{
    
    }
    
    public class Test{
      // 计算水果有多少个?
      /*
        方法的实现很简单,但是不能编译, 因为isFruits方法仅适用于水果类型,
        要解决此问题, 请使用T extends Fruits 来限定类型参数
      */
      public static <T> int countFruits1(T[] fruitArray){
          int count = 0;
          for(T e: fruitArray) {
              if (e.isFruits(e)){ //err 编译失败
                  count ++ ;
              }
          }
          return count;
      }
    //使用了限定类型参数, 就能使用Fruits类里面的isFruits方法.
      public static <T extends Fruits> int countFruits2(T[] fruitArray){
          int count = 0;
          for(T e: fruitArray) {
              if (e.isFruits(e)){ //ok 编译成功
                  count ++ ;
              }
          }
          return count;
      }
    
      public static void main(String[] args){
          /*
            把参数类型的检测,提前到编译时期,减少运行报异常
          */
          String[] sts = new String[10];
          countFruits1(sts);
    
          countFruits2(sts);//err 编译失败,因为参数限定了为水果类.
    
          Apple[] apples = new Apple[10];
    
          countFruits1(apples);// ok
          countFruits2(apples) ;//ok
      }
    }    
    
    泛型的类型关系(继承与子类型)

    Integer 继承 Number, 但是 Box<Number> 不等于 Box<Integer>
    Box<Number> 与 Box<Integer> 的父类是Object.

    ArrayList 继承 List , List 继承 Collection, 而 List<String> 是等于 ArrayList<String>;例如: List<String> lists = new ArrayList<>();

    泛型类型关系示例

      class Box<T>{
      
      }
      public class Test{
        public static void main(String[] args){
            Object someObject  = new Integer(10);
            someObject = someInteger;// 第一组ok
            
            /*
            由于Integer是一种Object,因此允许分配,当时Integer也是Number的一种, 
            因此下面的代码也是有效的. 
            */ 
            someMethod(new Integer(10)); //ok
            someMethod(new Double(10.0))//ok
          
            /*
              给定两种具体类型A 和 B(例如Number 和Integer),无论A和B是否相    
              关,MyClass<A> 与MyClass<B> 的公共父对象都是Object.
              其实在jvm 里面是没有泛型的, 泛型其实是伪泛型,在jvm里面要进行泛型
              的擦除,虚拟机是不知道泛型的类型的. 大部分全都擦除为Object类型.
              例如 Box<Integer>.class 其实与Box<String>.class 是相等的. 
              Box<String> box = new Box<>();
              Box<Integer> box1 = new Box<>();
              if (box.getClass() == box1.getClass()){
                  System.out.println("true");
              }
            */
              boxTest(new Box<Integer>());//err 编译失败
        }
    
        public static void someMethod(Number n){
        }
      
        //Box<Integer> 与Box<Number> 没有任何关系
        public static void boxTest(Box<Number>){
        }
      }
    

    泛型类型推断

    理解编译器是如何利用目标类型来推算泛型变量的值 ?
    类型推断是Java编译器查看每个方法调用和相应声明以确定适用的类型参数的能力.
    推断算法确定参数的类型,以及确定结果是否被分配或返回类型(如果有).最后,
    推断算法尝试找到所有仪器适用的具体类型.

    当我们没有确定某个泛型的类型的时候, 虚拟机就会去类型推断

       //这是没有确定泛型类型的时候
      Serializable s1 = pick("d",new ArrayList<String>());
      //这是我们已经确定的泛型类型,虚拟机就不会进行类型推断
      Serializable s2 = Test.<Serializable>pick("d",new ArrayList<String>);
    

    目标类型有: 变量声明; 赋值; 返回语句; 数组初始化器; 方法或构造函数初始
    Lambda表达主体; 条件表达式; 转换表达式.

    来自官方例子

    public class Test{
        static <T> T pick(T t1,T t2){
          return t1
        }
        public static void main(String[] args){
            /*
              1, String 
                      public final class String implements java.io.Serializable,.....
              2,ArrayList<E> 
                    public class ArrayList<E> extends AbstractList<E> implements 
                  List<E>,RandomAccess,Cloneable,java.io.Serializable
    
              从上面分析看出, 推算到Serializable 是他们共有的一个类 . 所以推断出
              是Serializable
            */
            Serializable s = pick("d",new ArrayList<String>());
        }
    }
    
    Java泛型 PECS(\color{red}{Producer} \color{red}{extends} \color{red}{Consumer} \color{red}{super})的原则

    为何要PECS原则?

    为了提升API的灵活性.

    PECS原则总结

    如果要从集合中读取类型T的数据,(\color{red}{可读权限})并且不能写入,可以使用 (? extends) 通配符.(Producer Extends)
    如果要从集合中写入类型T的数据,并且不需要读取(\color{red}{可写权限}),可以使用 (? super) 通配符.(Consumer Super)
    如果既要存又要取,那么就不要使用任何通配符.

    注意:
    如果我用反射可以绕过上面的权限吗? 好像在的jdk1.6之后(待考证), 是不能调用的.编译不会报错,但是运行会报\color{red}{UnsupportedOperationException }异常

    通配符( \color{red}{?} )

    泛型中的问号"?" 叫"通配符"
    通配符有两种,受上下控制的通配符和不受控制的通配符.

    通配符的适用范围:

    • 参数类型
    • 字段类型
    • 局部变量类型
    • 返回值类型.(注意: 访问一个具体类型的值较好)
    上限通配符
    public class Test{
         public static void main(String[] args){
            List<Integer> integerList = Arrays.asList(1,2,3);
            /*
               err 编译失败 因为在上面泛型的类型关系的时候,
                虽然Integer 继承 Number但是List<Integer>
                与List<Number>没有任何关系.所以不能调用sumOfList方法.      
                如果对integerList进行求和运算,需要重新写一个sumOfIntegerList方法,
              不能使用方法重载, 因为泛型被擦除了.
                这时候通配符"?" 就出来. 把下面的方法改造一下(? extends Number)
             */
            sumOfList(integerList); // 编译失败
             sumOfList1(integerList); // 编译成功  
             List<Double> doubleList = Arrays.asList(1.1,2.2,3.3);
              sumOfList1(doubleList); // 编译成功  
        }
      
    /*
      要编写在Number类型的列表和Number的子类型(如Integer,Double和Float) 
    上工作的方法,一般会指定List<? extends Number>; List<Number>比
      List<? extends Number> 更具有局限性,因为前者只匹配Number类型的列表,
    而后者匹配Number 类型的列表或其任何子类.
    */
      public static double sumOfList1(List<? extends Number> list){
            // extends 叫上限,只可读,不能写入. 上面PECS 有介绍.
            list.add(1); // 这种是编译报错的
            /*
                注意: 如果我用反射可以调用吗? 最新版的jdk 是不能调用的. 
                会报 UnsupportedOperationException
            */
          //反射代码
          /*Class<?> clazz = list.getClass();
          Method addMethod = class.getMethod("add",java.lang.Object.class);
          addMethod.setAccessible(true);
          addMethod.invoke(list,10);  
          System.out.println(list.toString());*/
    
          double s = 0.0;
            for (Number number : list){
                s += number.doubleValue();
            }
            return s;
        }
    
       public static double sumOfList(List<Number> list){
            double s = 0.0;
            for (Number number : list){
                s += number.doubleValue();
            }
            return s;
        }
    }
    
    下限通配符
    //CS Consumer消费者 list理解为消费者 添加数据.
    public static double addNumber(List<? super Integer> list){
        //PECS原则的 PE(Producer extends )原则
        //当只想从集合获取元素,把这个集合看成生产者,使用<? extends T>.
        //PESC原则CS(Consumer super)原则
         // 当你想增加元素到集合中, 把这个集合看成消费者, 请使用<? super T>.
        Integer tmp = list.get(0) //编译失败, 违背了PECS原则. 
        for(int i = 1;i <= 10){
          list.add(i);//编译成功. 
        }
    }
    

    上限和下限在Collections源码中的使用.

    Collections.java-->
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        //code....
    }
    
    不受限的通配符
        //泛型退化掉了, 不能使用List中任何依赖类型参数T 的方法,只能使用List里
        //面的方法. 
           public static void printList1(List<?> list){
            list.add("sss");//编译错误.
            list.add(1); //编译失败
            list.size();  //编译成功
            list.add(null);//编译成功
            list.get(2);    //编译成功
            list.contains(2); //编译成功
        }
    

    不受限的通配符,主要是用在类型的检测和匹配上面.

    泛型擦除

    JVM虚拟机, 是不支持泛型的. 在虚拟机里面运行的时候, 是没有泛型了. 在C++ 里面有temple(泛型) , kotlin 也是伪泛型.
    为什么java会使用伪泛型,因为在jdk5之前是没有泛型, 主要是为了向下兼容,才引入了伪泛型.

    功能: 保证了泛型不在运行时出现
    类型消除应用的场所:
    编译器会把泛型类型中所有的类型参数替换为它们的上(下)限,如果没有对类型参数做出限制, 那么就替换为Object类型. 因此,编译出的字节码仅仅包含了常规类,接口 和方法.
    在必要时编译器会插入类型转换以保持类型安全.
    编译器生成桥方法以及在扩展泛型时保持多态性.

    Brdge Methods 桥方法

    当编译一个扩展参数化类的类,或一个实现了测试化接口的接口是, 编译器有可能因此要创建一个合成方法, 名为桥方法. 它是类型擦除过程中的一部分.

    /*
      如果类型参数不受限制, 则将通用类型中的所有类型参数替换为其边界(上下限)或Object.
      因此, 产生的字节码仅包含普通的类, 接口和方法. 
      必要时插入类型转换,以保持类型安全.
      生成桥接方法以在扩展的泛型类型中保留多肽.
      类型擦除可确保不会为参数化类型创建新的类,因此, 泛型不会产生运行是开销.  */
    public class TypeErasure{
        static class Pair<T>{
            private T value;
            public T getValue(){
                return value;
            }
            public void setValue(T value){
                this.value = value;
            }
        }
    
        public static void main(String[] args){
            Pair<String> pair = new Pair<>();
            pair.setValue("myString");
            System.out.println("pair: "+ pair.getValue());
        }
    }
    

    下面是TypeErasure.class 字节码. 看到setValue(Ljava/lang/Object;)V 说明 泛型T , 已经擦除成Object了.


    11.png

    下面是泛型擦除.如果有extends, 一般会把T擦除成第一个泛型实参.

    interface ITypeE{
        void inType();
    }
    
    class TypeE implements ITypeE{
        @Override
        public void inType() {
    
        }
    }
    
    
    public class TypeErasure<T extends ITypeE> {
        private T iTpeE ;
    
        public T getT(){
            return iTpeE;
        }
    
        public void setT(T iTpeE){
            this.iTpeE = iTpeE;
        }
    
        public static void main(String[] args) {
            TypeErasure<ITypeE> typeETypeErasure = new TypeErasure<>();
            typeETypeErasure.setT(new TypeE());
            ITypeE t = typeETypeErasure.getT();
            System.out.println(t.getClass());
        }
    
    }
    

    如果有extends, 一般会把T擦除成第一个泛型实参.


    image.png

    下面是擦除编译器使用桥方法的实例.

    File:ITypeE.java
    public interface ITypeE<T> {
        void inType(T t);
    }
    
    File:TypeE.java
    public class TypeE implements ITypeE<Integer>{
    
        @Override
        public void inType(Integer integer) {
    
        }
    }
    
    

    TypeE.class,在编译扩展参数化类或实现参数化接口的类或接口时,作为类型擦除过程的一部分,编译器可能需要插件一个称为桥接方法的综合方法,. 你通常不必担心桥接方法.如果在堆栈跟踪的时候, 可能会出现疑惑.

    在字节码中, 桥接方法会调用当前方法, 在下图的第39行.} image.png
    方法擦除带来的问题.

    在普通方法中,不能重写equals(T value)方法,因为T 会把类型擦除成Object类型,

    public class TypeE<T>{
         public boolean equals(T t) { //err 编译失败
         }
    }
    

    思考:
    泛型无法使用原始类型来创建泛型
    无法创建类型参数的实例
    无法创建参数化类型的静态变量
    无法对参数化类型使用转换或者instanceof关键字
    无法创建参数化类型的数组(\color{red} {因为数组是支持协变的,而泛型不支持斜变,所以无法支持创建参数化类型的数组})
    无法创建,捕获或者是抛出参数化类型对象
    当一个方法的所有重载方法的形参类型擦除后,如果他们是具有相同的原始类型,那么次方法不可重载.

    面试中遇到问题(思考...)

    数组(Array)中可以用泛型吗?

    你可以把List<String>传递给一个接收List<Object>参数的方法吗?ArrayList<String> arrayList = new ArrayList<Object>(); ArrayList<Object> arrayList = new ArrayList<String>();

    Java中Set与Set<?>到底区别在哪里 ?.(Java中List<?> 和原始的List 的区别?)

    Java中List<?> 和List<Object>之间的区别?
    List<?>是一个未知类型的List,而List<Object>其实一个任意类型的List. 你可以把List<String>,List<Integer> 赋值给List<?>, 却不能把List<String> 赋值给List<Object>

    Java中的泛型是什么? 使用泛型的好处是什么?
    泛型是一种参数化类型的机制.
    好处:
    1,代码类型检测提前
    2,代码复用.
    ....

    泛型是怎么工作, 泛型如何擦除?

    什么是泛型中的限定通配符,和非限定通配符?

    List<? extends T> 和 List<? super T>之间有什么区别?

    泛型类型变量能不能是基本数据类型?为什么?

    ArrayList<String> arrayList = new ArrayList<String>();
    if(arrList instanceof ArrayList<String>)
    if(arrayList instancesof ArrayList<?>)中那个if可以运行,为什么?

    C++ 模板和java泛型之间有何不同?
    C++ 里面会使用宏指令,它会替换成模板代码.
    java是伪泛型.

    最后来个面试附加题

    • Plate
    • Plate<Object>
    • Plate<?>
    • Plate<T>
    • Plate<? extends T>
    • Plate<? supter T>
      它们之间的区别?

    相关文章

      网友评论

          本文标题:Java泛型与Java泛型面试题

          本文链接:https://www.haomeiwen.com/subject/tvudwhtx.html