Java泛型与Java泛型面试题

泛型定义

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

为什么使用泛型?
  • 可以增强编译时错误监测,减少因类型问题引发的运行时的异常
    (),具有更强的类型检查.
  • 可以避免类型转换.
  • 方法的形参中使用泛型,增加程序的复用性.
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 list = new ArrayList<>();//使用了泛型
    list.add("hello");
    String str = list.get(0);//不需要强转
  }
}
泛型的种类
  • 泛型类

    泛型类格式
    public class calssName{}

  • 泛型接口

    泛型接口格式
    public interface IName{}

  • 泛型方法

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

  /**
  泛型类(泛型接口 interface className
  class calssName

  泛型通常使用的字母(来自官方建议),
  E - Element(Java Collections Framework广泛使用)
  K - Key
  N - Number
  T - Type
  V - value
  S,U,V ......
  */
  interface Generics{ //泛型接口

  }
  interface Generics{ //泛型接口
  }
  //泛型接口的使用
  /*
    这种是不确定泛型T的类型, 必须在类名后面添加声明泛型T
  */
  class GenericsImpl implements Generics{ /

  }
  /*
    这种是已经确定了泛型T的类型为String, 类前面不要加上泛型的声明.
  */
  class GenericsImpl2 implements Generics{ 

  }

  public class Box{  //泛型类
    private T t;
    public void setT(T t){  //注意这不是泛型方法.
      this.t = t;  
    }

    public T getT(){  // 注意这不是泛型方法
      return t;
    }

    public void test1(List lists){  // 注意这不是泛型方法
    }
    
    public void test2(List list){ //注意这不是泛型方法, 通配符"?" 后面会后介绍
    }  

    public  void testT(T t){ // 泛型方法 (此T 非类上面的T)

    }
  }
类型参数&类型实参

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

The Diamond钻石运算符也叫菱形运算符

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

原始类型

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

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

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

单一限制

public class Box{
  private  T t;
  public setT(T t){
    this.t = t;
  }
  public T getT(){
    return t;
  }
  public  void inspect1(U u){
    System.out.println("T: " + t.getClass().getName());
    System.out.println("U: "+ u.getClass().getName)
  }
  public  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 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{ // err 编译失败, 
   }

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

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

class Fruits{
  public  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  int countFruits1(T[] fruitArray){
      int count = 0;
      for(T e: fruitArray) {
          if (e.isFruits(e)){ //err 编译失败
              count ++ ;
          }
      }
      return count;
  }
//使用了限定类型参数, 就能使用Fruits类里面的isFruits方法.
  public static  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 不等于 Box
Box 与 Box 的父类是Object.

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

泛型类型关系示例

  class Box{
  
  }
  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 与MyClass 的公共父对象都是Object.
          其实在jvm 里面是没有泛型的, 泛型其实是伪泛型,在jvm里面要进行泛型
          的擦除,虚拟机是不知道泛型的类型的. 大部分全都擦除为Object类型.
          例如 Box.class 其实与Box.class 是相等的. 
          Box box = new Box<>();
          Box box1 = new Box<>();
          if (box.getClass() == box1.getClass()){
              System.out.println("true");
          }
        */
          boxTest(new Box());//err 编译失败
    }

    public static void someMethod(Number n){
    }
  
    //Box 与Box 没有任何关系
    public static void boxTest(Box){
    }
  }

泛型类型推断

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

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

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

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

来自官方例子

public class Test{
    static  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 
                public class ArrayList extends AbstractList implements 
              List,RandomAccess,Cloneable,java.io.Serializable

          从上面分析看出, 推算到Serializable 是他们共有的一个类 . 所以推断出
          是Serializable
        */
        Serializable s = pick("d",new ArrayList());
    }
}
Java泛型 PECS( )的原则

为何要PECS原则?

为了提升API的灵活性.

PECS原则总结

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

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

通配符( )

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

通配符的适用范围:

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

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

Collections.java-->
public static  void copy(List dest, List 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{
        private T value;
        public T getValue(){
            return value;
        }
        public void setValue(T value){
            this.value = value;
        }
    }

    public static void main(String[] args){
        Pair 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 {
    private T iTpeE ;

    public T getT(){
        return iTpeE;
    }

    public void setT(T iTpeE){
        this.iTpeE = iTpeE;
    }

    public static void main(String[] args) {
        TypeErasure 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 {
    void inType(T t);
}
File:TypeE.java
public class TypeE implements ITypeE{

    @Override
    public void inType(Integer integer) {

    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

List 和 List之间有什么区别?

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

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

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

最后来个面试附加题

  • Plate
  • Plate
  • Plate
  • Plate
  • Plate
  • Plate
    它们之间的区别?
  • 你可能感兴趣的:(Java泛型与Java泛型面试题)