Java泛型与Java泛型面试题

泛型定义

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>
    它们之间的区别?
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339

推荐阅读更多精彩内容

  • 开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收List作为形式参数,那么如果尝试...
    时待吾阅读 1,034评论 0 3
  • Java 泛型是 Java 5 引入的一个重要特性,相信大多数 Java 开发者都对此不陌生,但是泛型背后的实现原...
    JohnnyShieh阅读 2,055评论 6 37
  • 1、基本应用 Java泛型可以用在类、接口和方法上。基本使用请参考《on Java 8》. 2、类型擦除 ​ ...
    流_心阅读 306评论 0 0
  • 参数类型的好处 在 Java 引入泛型之前,泛型程序设计是用继承实现的。ArrayList 类只维护一个 Obje...
    杰哥长得帅阅读 869评论 0 3
  • 参考:java核心技术 一、Java泛型的实现方法:类型擦除 前面已经说了,Java的泛型是伪泛型。为什么说Jav...
    御前灬码夫阅读 1,051评论 0 7