Java进阶 - 泛型(面试题)持续更新.....

3,688 阅读14分钟

最近学习了泛型的一些进阶知识,因为很多开源框架里面都涉及到了泛型,所以想要成为Android老司机这些东西也都是必不可少的,关于泛型的官方文档和例子,如果想了解的话可以看我的上一篇博客Java泛型进阶(中文文档:泛型类、泛型方法、有界类型参数、泛型与继承、类型推断、通配符、类型擦除、泛型的限制),这里面是针对于官方提供的泛型文档将里面里写常用的知识进行了整理。

今天这篇博客主要是根据泛型的知识针对一些面试题进行整理,因为泛型在面试中也经常被问到,废话不多说,开锤。

1.首先说下什么是泛型,以及使用泛型有哪些优点?

泛型是JDK 1.5引入的新特性,那么Java之所以引入它我认为主要有三个作用

  • ①.类型检查,它将运行时类型转换的ClassCastException通过泛型提前到编译时期。
  • ②.避免类型强转。
  • ③.泛型可以泛型算法,增加代码的复用性。

①.类型检查提前:需求是需要提供一个装有Integer类型的List,并求和

//不使用泛型
ArrayList list = new ArrayList();
list.add(1);     
list.add("2");    //这里模拟写错的情况,或者这个list是由别人提供的
//编译可以通过但是运行报出ClassCastException: String cannot be cast to Integer
int sum = (int) list.get(0) + (int) list.get(1);


//使用泛型
ArrayList<Integer> list2 = new ArrayList<>();
list2.add(1);
list2.add("2");     //编译报错: (参数不匹配; String无法转换为Integer)
int sum2 = (int) list2.get(0) + (int) list2.get(1);

②.避免类型强转:需求是字符串存取

List list = new ArrayList();
list.add("str");        //装箱
//拆箱,不使用泛型 需要强转
String s = (String) list.get(0);


List<String> list2 = new ArrayList<>();
list2.add("str");      //装箱
//拆箱,使用泛型 不需要强转,存什么取什么
String s1 = list2.get(0);

③.通过泛型可以增加代码的复用性:需求提供方法来计算两个float类型或者int类型的和

int   x  = 1;
int   y  = 2;
float f1 = 0.3F;
float f2 = 0.4F;

//不使用泛型方法时,计算分别需要调用重载的两个方法来分别计算不同类型的和
int   intSum   = getIntSum(x, y);
float floatSum = getFloatSum(f1, f2);


//使用泛型方法,调用同一个方法即可
try {
    int   intSum1   = getNumSum(x, y);
    float floatSum1 = getNumSum(f1, f2);
}catch(Exception e){
    e.printStackTrace();
}

//不使用泛型,求int类型的和
public static int getIntSum(int i1, int i2) {
    return i1 + i2;
}
//不使用泛型,求float类型的和
public static float getFloatSum(float f1, float f2) {
    return f1 + f2;
}


//使用泛型的求和方法
public static <T extends Number> T getNumSum(T t1, T t2) throws InterruptedException {

    //这里不能直接返回t1 + t2,因为泛型擦除后是Number类型,Number类型不能使用+运算符
    //这里也不能强转(T)(t1.doubleValue() + t2.doubleValue()),因为基础数据类型不能转成引用类型
    //如果强转的话只能这样Double d = t1.doubleValue() + t2.doubleValue(); return (T)d;
    T result;
    if (t1 instanceof Integer) {
        result = (T) (Integer.valueOf(t1.intValue() + t2.intValue()));
    } else if (t1 instanceof Long) {
        result = (T) (Long.valueOf(t1.longValue() + t2.longValue()));
    } else if (t1 instanceof Float) {
        result = (T) (Float.valueOf(t1.floatValue() + t2.floatValue()));
    } else if (t1 instanceof Double) {
        result = (T) (Double.valueOf(t1.doubleValue() + t2.doubleValue()));
    } else {
        throw new InterruptedException();
    }

    return result;
}

2.泛型有哪几种?

Java的泛型有3种:


  • ①.泛型方法  private <K,V> boolean compare(Pair<K,V> p1,Pair<K,V> p2){}
  • ②.泛型类   class xxx<T1,T2,...,Tn>
  • ③.泛型接口  interface xxx<T1,T2,...,Tn>

①.android/三方类库中的泛型方法

//1.findViewById方法
@Override
public <T extends View> T findViewById(@IdRes int id) {
    return getDelegate().findViewById(id);
}

②.android/三方类库中的泛型类

//Java中Collection是所有集合的父类,JDK1.5之后使用泛型
public interface Collection<E> extends Iterable<E> {
    Iterator<E> iterator();
    <T> T[] toArray(T[] var1);
    boolean add(E var1);

   //......
}

③.android/三方类库中的泛型接口


//Retrofit的call 使用RxJava的Observable接收 Observable的泛型

public interface ObservableSource<T> {
    void subscribe(@NonNull Observer<? super T> observer);
}

public abstract class Observable<T> implements ObservableSource<T> {
        //...............
}

3.泛型中的一些基本概念

  • 原始类型:原始类型就是缺少类型变量的泛型,class Phone<T> //Phone p = new Phone();这个Phone就是原始类型
  • 菱形运算符:ArrayList<String> l = new ArrayList<>();//“<>”就是,也称为钻石运算符,是JDK1.7引入的,通过类型推断得知泛型类型且让代码更易读,但它不能用于匿名的内部类。
  • 类型推断:类型推断就是通过一些信息判断,我们可以像使用正常方法或正常初始化实例一样去使用泛型。
//类型推断是JDK1.7之后推出的,而泛型是1.5推出的,所以在1.7之前使用泛型都必须写完成的泛型.
//例如ArrayList<String> = new ArrayList<String>();
class Book<T> {
    public <U> void readBook(U book){}
}
//完整的创建泛型类
Book<String> book = new Book<String>();
//类型判断之后可以这样写,不需要再创建实例的时候再去参数化泛型
Book<String> book1 = new Book<>();

//完整的使用泛型方法
book.<String>readBook("123");
//类型推断之后可以这样写
book.readBook("123");

  • 泛型继承:有两种方式。
interface Generic<T>{
    <U> void run(U u);
}
//1.方式1,如果继承/实现泛型接口/类,父类的泛型没有参数化,则子类也必须在类上声明泛型
class Generic1<T> implements Generic<T>{
    //3.如果父类中有泛型方法需要实现,则泛型自动继承
    @Override
    public <U> void run(U u) {}
}    
//2.方式2,如果继承/实现泛型接口/类,父类的泛型已经参数化,则子类就不用声明了
class Generic2 implements Generic<String>{
    @Override
    public <U> void run(U u) {}
}
  • 非限定通配符:也叫无边界通配符,这个就是ArrayList<?>,要注意的是,如果使用了无边界通配符,那么那么只能使用泛型类中任何依赖泛型参数的方法。
public void test(ArrayList<?> list){
    // 因为add的参数是 T 所以不能使用,因为不知道add的类型是什么
    list.add("123");
    list.size();
    list.add(null);
    list.get(0);
    list.contains(2);

    for (Object o : list) {
        System.out.println(o);
    }
}

4.泛型中的限定机制是什么

限定机制其实就是通过限定,让泛型更加有意义,如果只是单单为一个类声明泛型<T>的话,其实它就相当于是Object,只有加入限定的话才会让泛型更具有意义,比如计算方法的泛型,我们可以限定为<? extends Number>这样的话就可以保证不会把字符串传入我们的计算方法。

限定类型变量

  • 它的意思就是我们常用的ArrayList<String> list; 直接将泛型用一个确定的对象限定死。

限定类型参数(注意它和通配符限定不是一回事,作用于泛型类,泛型接口,泛型方法)

  • 单一限制:<U extends Number>
  • 多种限制:<U extends A&B&C>

限定类型变量是使用extends对泛型类、泛型接口或者泛型方法声明传入的泛型类型变量进行限制。 extends支持通过&符号指定多个限制条件,但是多个限制条件必须是一个类和多个接口,而且类必须写在前面,因为Java是单继承多实现,例如

<T extends ArrayList&Comparable>那么传入的类型必须是ArrayList和他的子类,并且要实现Comparable接口

通配符限定(作用于参数类型、字段类型、局部变量类型、返回值类型)

  • 为什么要有通配符这个概念?
public void setData(ArrayList<Number> data){
    // do sth
} 

//虽然Integer继承自Number,但是 XXX<Integer> 与 XXX<Number>一毛钱关系也没有
ArrayList<Number> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();
setData(list1);             //succcess
setData(list2);             //error

//所以引出了通配符的概念。
public <T> void setData2(ArrayList<? extends Number> data){
    // do sth
}
setData2(list1);             //succcess
setData2(list2);             //succcess

5.泛型通配符的PEC(和谐)S原则

  • Producer extends原则:当只想从容器中获取元素,请把这个容器看成生产者,使用<? extends T>
  • Consumer super 原则:当只想操作容器中的元素,请把这个容器看成消费者,使用<? super T>
  • 通过上图的关系编写代码,验证通配符限定的使用以及约束和局限
static class Student {}
static class Boy extends Student {}
static class Girl extends Student {}
static class XiaoMing extends Boy {}
static class XiaoQiang extends Boy {}
static class XiaoHong extends Girl {}

public static void useExtends(GenericType<? extends Boy> student) {}

public static void useSuper(GenericType<? super Boy> student) {}
  • <? extends T>限定参数类型的上界:参数类型必须是T或T的子类型
  • <? super T> 限定参数类型的下界:参数类型必须是T或T的超类型
  • <? extends T> 只能取,且只能取T和T的直接父类型。(排除Object)
  • <? super T> 只能存,且只能存T和他的所有子类型(排除Object)
  • 其实通过反射也可以强制突破限制进行存和取,但是要注意进行类型判断。
  • 在定义上发现其实它的取和存操作有点违背上面的通配符限定装入的类型,但你只要记住一点就行,通配符限定后,需要类型擦除,那么编译器会去推断你这个限定的具体类型,JVM不允许你存入或取出模棱两可的类型。

6.泛型约束和局限性

  • 不能实例化类型变量(因为泛型只存在于编译期,而泛型的具体类型在运行期才可以知道,所以无法初始化)
  • 静态域或者方法里不能引用类型变量(因为静态不依赖类的实例化,而泛型的类型确认需要类实例化才可得知)
  • 实例化泛型类型,必须是引用类型,因为类型擦除后就变成了Object
  • 不能使用instanceof关键字判断泛型的类型(语法规定编译不能通过)
  • 不管泛型传入的类型是什么,泛型类编译后getClass().getName()得到的都是泛型类的原始类型,这句话很绕,但很好理解,就是下面代码的GenericTest<T>不管T传入的是什么,那么最终编译的GenericTest的实例都是 new GenericTest().
  • 可以声明泛型数组但是不能实例化(因为类型擦除后,引用类型数组就变成了Object [])
  • 泛型类不能extends Exception/Throwable 原因
  • 不能捕获泛型类对象 原因
  • 不能使用泛型定义重载方法(因为形参类型擦除后,他们就都变成了原始类型)
static class GenericTest<T> {

    public T data;
    //6.可以声明泛型数组,但是不能实例化,编译会报错
    public GenericTest<String> [];
    
    //2.静态域或者方法里不能引用类型变量
    //private static T instance;    //error
    //static{
    //    T staticField;            //error
    //}
    //2.但是静态方法 本身是泛型方法就行(因为它是在调用的时候才确定泛型,他的泛型不依赖于类的实例化)
    //private static <T> T getInstance(){}

    public GenericTest() {
        //1.不能实例化类型变量,因为泛型在使用的时候才能确定泛型的具体类型
        //data = new T();           //error
    }
    
    public static void main(String[] args) {
        //3.实例化泛型类型,必须是引用类型
        //GenericTest<double> g1 = new GenericTest<>();       //error
        GenericTest<Double> g2 = new GenericTest<>();       //success
    }
    
    //7.泛型类不能extends Exception/Throwable
    //private class Problem<T> extends Exception;

    //8.不能捕获泛型类对象
    //public <T extends Throwable> void doWork(T x){
    //  try{
    //
    //  }catch(T x){(如果需要捕获只能把这里的T变为Throwable,然后将T throw出去)
    //            //do sth;
    //  }
    //}
}    

7.类型参数和类型变量的不同

  • 如果泛型中声明的是K、V、E...等不明确的类型就叫做类型参数,如果是传入的具体的对象则称为类型变量
  • Book<T>:这是类型参数
  • Book<String>:这是类型变量

8. List<?> 和 List <Object>的区别

  • 区别是List<?>使用了无限定通配符,不可以使用和泛型相关的方法,因为不明确泛型的类型是什么,而List<Objet>则可以操作因为明确了泛型的类型是Object,而Object是所有类的超类。

9.Array中可以使用泛型吗

  • 不可以,虚拟机本身的实现就不支持泛型数组。因为数组是协变,擦除后就没法满足数组协变的原则。

10. List 和 List <Object>的区别

  • List属于原始类型,它不会进行安全类型检查,也不存在泛型类型的限制。
  • List<Object>泛型为Object,它会进行安全类型检查,而且受泛型的限制。

11.泛型擦除机制?

  • Java的泛型是JDK5新引入的特性,为了向下兼容,虚拟机其实是不支持泛型,所以Java实现的是一种 伪泛型机制,也就是说Java在编译期擦除了所有的泛型信息,这样Java就不需要产生新的类型到字节码, 所有的泛型类型最终都是一种原始类型,在Java运行时根本就不存在泛型信息。

12.Java编译器具体是如何擦除泛型的

    1. 检查泛型类型,获取目标类型
    1. 擦除类型变量,并替换为限定类型 如果泛型类型的类型变量没有限定(),则用Object作为原始类型 如果有限定(),则用XClass作为原始类型 如果有多个限定(T extends XClass1&XClass2),则使用第一个边界XClass1作为原始类
    1. 在必要时插入类型转换以保持类型安全
    1. 生成桥方法以在扩展时保持多态性

13.为什么泛型被擦除但是仍然可以通过反射拿到

因为在将.java编译成.class时,虚拟机在擦除泛型时会去检查泛型的限定类型变量限定类型参数

例如:

class Person<T>                     //擦出后就是 class Person
class Student<T extends Number>     //擦除后就是 class Student<Number>
class Teacher<String>               //擦除后就是 class Teacher<String>

我们也可以通过反编译来查看下具体的字节码文件:

  • 反编译查看字节码,我们会发现它在class的元数据区,来记录泛型的签名信息。

再举个例子:

使用常用的Gson,反序列化拿到我们想要的数据类型。

//创建一个Response<Data> 数据
Response<Data> response = new Response<>(new Data(), 1, "12345");

//转换为字符串
String json = new Gson().toJson(response);

//再转换为Bean对象:注意 fromJson传入的是Response.class
Response<Data> response1 = new Gson().fromJson(json, Response.class);
System.out.println(response1.getData().getClass()+"");
  • 这里虽然可以转换成功,但是打印data报错:ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to com.xxx.xxx.bean.Data
  • 为什么呢,因为调用fromJson的时候,告诉API我要一个Response.class的返回类型,但是并没有指定泛型类型,所以它默认将data声明为LinkedTreeMap

然后我们使用Gson提供的TypeToken来进行转换发现可以了:

Type type = new TypeToken<Response<Data>>() {
}.getType();

Response<Data> response2 = new Gson().fromJson(json, type);
System.out.println(response2.getData().getClass() + "");

嗯?我们知道TypeToken中是通过反射拿到反序列化Bean对象的泛型类型,那不是说泛型会被擦除吗?那他是怎么拿到的? 仔细看:

Type type = new TypeToken<Response<Data>>() {
}.getType();
  • 这其实是一个内部类,那内部类相当于什么?他就相当于你重新声明了一个类继承自TypeToken,且声明类他的泛型类型。

  • 而且我们尝试创建TypeToken,然后再getType,这样是不行了,因为它的构造方法被Protected关键字保护,所以你只能通过内部类的方式,我记得Gson,早期的版本这个类好像是抽象的,原理也是一样,它就是想让你通过创建内部类/继承它的方式来声明所需要的反序列化Bean对象的实际泛型类型。

14.泛型在一些源码或框架中的使用

  • Collections.copy();方法:
//通过上下界通配符限定,src用来取,而des只能存,这样保证src不能添加修改,而dex获取只能添加
public static <T> void copy(List<? super T> des, List<? extends T> src) {
    //dosth
}

本文使用 mdnice 排版