美文网首页
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泛型教程导航 Java 泛型概述 Java泛型环境设置 Java泛型通用类 Java泛型类型参数命名约定 ...

  • 泛型

    与Java泛型相同,Kotlin同样提供了泛型支持。对于简单的泛型类、泛型函数的定义,Kotlin 与 Java ...

  • 第二十八课:泛型

    泛型出现之前 泛型出现之后 Java深度历险(五)——Java泛型

  • Kotlin 泛型

    说起 kotlin 的泛型,就离不开 java 的泛型,首先来看下 java 的泛型,当然比较熟悉 java 泛型...

  • java泛型中类型擦除的一些思考

    java泛型 java泛型介绍 java泛型的参数只可以代表类,不能代表个别对象。由于java泛型的类型参数之实际...

  • Java泛型

    参考:Java知识点总结(Java泛型) 自定义泛型类 自定义泛型接口 非泛型类中定义泛型方法 继承泛型类 通配符...

  • Java泛型—Java语法糖,只在编译有作用,编译后擦出泛型

    Java泛型—Java语法糖,只在编译有作用,编译后擦出泛型 在代码进入和离开的边界处,会处理泛型 Java泛型作...

  • JAVA 核心笔记 || [xxx] 泛型

    泛型 JAVA 的参数化类型 称为 泛型 泛型类的设计 Learn12.java 运行

  • 简单回顾Java泛型之-入门介绍

    什么时候开始有了Java泛型?什么是Java泛型?为什么要引入Java泛型?什么时候用到了泛型?可不可以给泛型下一...

  • java基础

    八、泛型 面试题==什么是泛型中的限定通配符和非限定通配符 ?这是另一个非常流行的Java泛型面试题。限定通配符对...

网友评论

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

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