面试重灾区-泛型攻克

目录介绍

  • 1.使用泛型的意义
  • 1.1 例一
  • 1.2 例二
  • 2.泛型擦除
  • 3.使用泛型带来的问题
  • 4.泛型的通配符super和extends

泛型能够说是面试中的重灾区了,一直以来你们对于泛型的认识可能并非很是的清晰,在泛型的使用上可能就更疑惑了,这篇文章将带你们攻克这一知识点,泛型这块其实仍是要你们多敲敲,看看什么状况下泛型是会报错的什么状况下不会,这样才能真正的了解泛型。

1. 使用泛型的意义

1.泛型的创造者让泛型的使用者能够在使用泛型的时候根据传入泛型类型的不一样而使用对应类型的API。java

2.使用泛型能够解决没必要要的类型转换错误。android

针对第一点:举系统用到泛型的两个例子:程序员

例子一:findViewById()面试

@Nullable
public <T extends View> T findViewById(@IdRes int id) {
    return getWindow().findViewById(id);
}
复制代码

这个方法应该是咱们平常开发中最经常使用的方法了,能够看到根据咱们传入的id最终解析找到返回与之对应的一个View。 层层深刻最终找到源头:数组

protected <T extends View> T findViewTraversal(@IdRes int id) {
    if (id == mID) {
        return (T) this;
    }
    return null;
}
复制代码

调用时:bash

TextView textView= findViewById(R.id.title);
textView.setText("123");

ImageView imageView= findViewById(R.id.image);
imageView.setImageResource(R.drawable.ic_smoll_android);
复制代码

能够看到咱们并无显示的获取或者建立对应的TextView或者ImageView实例就直接获取到了对应的类型从而调用到了对应类型的API,好比TextView的setText方法和ImageView的setImageResource方法。app

这里的泛型创造者也就是谷歌写View的这个程序员,泛型的使用者天然就是咱们自身调用findViewById这个方法的人了,因为这是一个泛型方法,咱们最终不用new TextView就能够获取对应的TextView实例,由于泛型的创造者已经帮咱们将新建的逻辑写好了,咱们只须要根据须要用不用的View类型去接收就能够获取到对应类型的实例,这无疑是至关方便的。 这就是根据咱们传入类型不一样而获取调用到对应类型API的一个最好的例子。性能

例子二:系统的Comparable接口ui

public interface Comparable<T> {
    public int compareTo(T o);
}
复制代码
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
...
    public native int compareTo(String anotherString);

}
复制代码
public final class Integer extends Number implements Comparable<Integer> {
...
    public int compareTo(Integer anotherInteger) {
        return compare(this.value, anotherInteger.value);
    }

}
复制代码

能够看到String和Integer类实现Comparable接口后经过传入不一样的泛型类型String,Integer从而在复写compareTo方法时即可以调用到传入的类型String,Integer的API。this

第一点的总结:

  1. 咱们使用泛型时判断是否须要使用泛型最简单的方法其实就是判断当前这个类中的方法是否须要在返回值中使用泛型,若是须要则可使用泛型,反则不须要。
  2. 能够看到例子二实际上是违背了我上边所说的第一点的,可是若是你仔细看就会发现这个用法其实最终也是基于整体原则: 使用泛型的时候根据传入的不一样类型而使用对应类型的API 而去使用的,因此也是知足这个原则的。
  3. 若是你的类型肯定的话是彻底不须要使用泛型的。

针对第二点:

List list = new ArrayList();
list.add("123");
int a = (int) list.get(0);//须要作类型强转,在运行时,ClassCastException
复制代码

能够看到若是没有泛型在运行时有可能会出现这样没必要要的类型转换错误,使用泛型之后在编译时就将类型约束好了从而解决了这种问题的产生。

2.泛型擦除

Java中的泛型类型在代码运行时会被擦除掉(通常状况下会被擦成Object类型,若是使用了上限通配符的话会被擦成extends右边的类型,如T extends View则最终会被擦成View类型),也就是说泛型只在编译期起做用。

Class c1 = new ArrayList<Integer>().getClass();
Class c2 = new ArrayList<String>().getClass();
System.out.println(c1 == c2); true
复制代码

能够看到最后结果是true的,由于泛型类型在运行时都会被擦除掉,也就是说其实c1和c2是由相同的class字节码文件加载出来的,他们是相同的Class,new ArrayList()和new ArrayList()最后都会被擦成new ArrayList()。

为何要将泛型类型在运行时擦除?

最主要仍是出于兼容性的考虑,泛型是JDK1.5之后才引入的,为了兼容以前的JDK版本因此在运行时将泛型类型都擦掉以保证和以前JDK版本的java字节码相同。

public class Test<T> {
    private T b;
    public void setB(T b) {
        this.b = b;
    }
    public T getB() {
        return b;
    }
}


Test<Integer> a=new Test<Integer>();
a.setB(1);
int b=a.getB();//不须要作类型强转,自动完成
复制代码
//定义处已经被擦除成Object,没法进行强转,不知道强转成什么public T getB();
   Code:
      0: aload_0
      1: getfield      #23 // Field b:Ljava/lang/Object;
      4: areturn
//调用处利用checkcast进行强转
L5 {
            aload1
            invokevirtual com/ljj/A getB()Ljava.lang.Object);
            checkcast java/lang/Integer
            invokevirtual java/lang/Integer intValue(()I);
            istore2
    }
复制代码

能够看到若是使用了泛型,在运行期间是会自动进行类型强转的而不用咱们主动去调用类型强转。

Java 泛型类、泛型接口、泛型方法有什么区别?

  1. 泛型类是在实例化类的对象时才能肯定的类型,其定义譬如 class Test {},在实例化该类时必须指明泛型 T 的具体类型。

  2. 泛型接口与泛型类同样,其定义譬如 interface Generator { E dunc(E e); }。

  3. 泛型方法是独立的,它能够不依附与你当前类中的泛型,当前类没有泛型也可使用泛型方法使用的时候将泛型类型定义到方法返回值的左边,以后就可使用了。

<T> void func(T val) {}
<T> T func(Fruit val) {}
public static <T> void func(T val) {}
复制代码

下边举一个例子

public class Test{
    public static <T> T add(T x, T y){
        return y;
    }

    public static void main(String[] args) {

        int t0 = Test.add(10, 20.8);
        int t1 = Test.add(10, 20);    
        Number t2 = Test.add(100, 22.2);

        Object t3 = Test.add(121, "abc");
        int t4 = Test.<Integer>add(10, 20);
        int t5 = Test.<Integer>add(100, 22.2);
        Number t6 = Test.<Number>add(121, 22.2);

    }
}
复制代码
  1. t0 编译直接报错,add 的两个参数一个是 Integer,一个是 Float,因此取同一父类的最小级为 Number,故 T 为 Number 类型,而 t0 类型为 int,因此类型错误。
  2. t1 执行赋值成功,add 的两个参数都是 Integer,因此 T 为 Integer 类型。
  3. t2 执行赋值成功,add 的两个参数一个是 Integer,一个是 Float,因此取同一父类的最小级为 Number,故 T 为 Number 类型。
  4. t3 执行赋值成功,add 的两个参数一个是 Integer,一个是 Float,因此取同一父类的最小级为 Object,故 T 为 Object 类型。
  5. t4 执行赋值成功,add 指定了泛型类型为 Integer,因此只能 add 为 Integer 类型或者其子类的参数。
  6. t5 编译直接报错,add 指定了泛型类型为 Integer,因此只能 add 为 Integer 类型或者其子类的参数,不能为 Float。
  7. t6 执行赋值成功,add 指定了泛型类型为 Number,因此只能 add 为 Number 类型或者其子类的参数,Integer 和 Float 均为其子类,因此能够 add 成功。

数组不支持泛型

Fruit<String>[] i=new Fruit<String>[10]; //Errot
Fruit<?>[] i=new Fruit<?>[10]; /能够经过,可是没有意义
复制代码

再看一个例子

//Part1
List<Object> list=new ArrayList<String>();//Error
list.add("123");

//Part2
Object[] objects=new Long[10];
objects[0]="123"; //Runtime 异常
复制代码

上面 Part 1 编译出错,Part 2 编译 OK,运行出错。 由于 List 和 ArrayList 没有继承关系,而 Java 的数组是在运行时类型检查的。

3.使用泛型带来的问题

1.自动拆箱装箱带来的性能损耗

泛型不支持传入基本数据类型,只支持引用数据类型,例如咱们直接使用new ArrayList()是不合法的,由于类型擦除后会替换成Object(若是经过extends设置了上限,则替换成上限类型),int显然没法替换成Object,因此泛型参数必须是引用类型。

2.泛型类型没法当作真实类型去使用

因此下列方法都是错误的

static <T> void test(T t){ //所有ERROR
    T newInstance=new T();  
    T[] array=new T[0]; 
    Class c=T.class;
    List<T> list=new ArrayList<T>();
    if(list instanceof List<String>){}  
}
复制代码

3.泛型会自动将类型进行强转,类型转换时也会有性能的开销。

如何经过反射获取泛型类型?

既然泛型类型在运行时会被擦除那么咱们怎么获取到泛型类型呢?

其实在泛型擦除时并不会将全部的泛型类型都擦除掉,它只会擦除运行时的泛型类型,编译时类中定义的泛型类型是不会被擦除的,对应的泛型类型会被保存在Signature中。 咱们若是想获取对应对象中的泛型类型只需将动态建立的对象改成匿名内部类便可获取,由于内部类实在编译时建立的,泛型类型是会保存下来的。 对应API getGeneric...都是获取泛型类型的。 下边举两个例子:

List<Integer> list = new ArrayList<>();
list.getClass().getGenericSuperclass(); //获取不到泛型信息
List<Integer> list1 = new ArrayList() {};
list1.getClass().getGenericSuperclass(); //能够获取到泛型信息
复制代码
  1. 能够看到第一个list因为是在运行时建立的对象因此因为泛型擦除是没法获取泛型信息的,由于运行时对象本质是方法的调用(真正调用了之后才会建立),在运行时建立的对象是没有办法经过反射获取其中的类型的。
  2. 第二个是能够获取的,由于后边加了{},这就使得这个list成为了一个匿名内部类且父类是List,子类是能够调用父类的构造方法的,加了以后这个list1就不是运行时建立的对象了而是编译时建立的,因此是能够获取泛型类型的。

下边以一道题为例:

public class Demo {

        public static void main(String[] args) throws Exception {

                ParameterizedType type = (ParameterizedType) Bar.class.getGenericSuperclass();
                System.out.println(type.getActualTypeArguments()[0]);

                ParameterizedType fieldType = (ParameterizedType) Foo.class.getField("children").getGenericType();
                System.out.println(fieldType.getActualTypeArguments()[0]);

                ParameterizedType paramType = (ParameterizedType) Foo.class.getMethod("foo", List.class).getGenericParameterTypes()[0];
                System.out.println(paramType.getActualTypeArguments()[0])
    
                System.out.println(Foo.class.getTypeParameters()[0].getBounds()[0]);
        }

        class Foo<T extends CharSequence> {

                public List<Bar> children = new ArrayList<Bar>();

                public List<StringBuilder> foo(List<String> foo) {return null}

                public void bar(List<? extends String> param) {}
         }

        class Bar extends Foo<String> {}
}
复制代码

运行结果以下。

class java.lang.String

class Demo$Bar

class java.lang.String

interface java.lang.CharSequence

经过上面例子会发现泛型类型的每个类型参数都被保留了,并且在运行期能够经过反射机制获取到,由于泛型的擦除机制实际上擦除的是除结构化信息外的全部东西(结构化信息指与类结构相关的信息,而不是与程序执行流程有关的,即与类及其字段和方法的类型参数相关的元数据都会被保留下来经过反射获取到)。

4.泛型的通配符super和extends

因为泛型不是协变的,它不支持继承,好比在使用 List< Number> 的地方不能传递 List< Integer>,因此引入了通配符去解决对应的问题,能够理解通配符是对泛型功能的扩展和加强。

  • extends 上限通配符 能够接收extend后的类型及子类

  • super 下限通配符 能够接收super后的类型及父类

向下边这种写法都是被禁止的

List<Number> list=new ArrayList<Integer>() //Error

List<Object> list;
List<String> strlist=new ArrayList<>();
list=strlist;							   //Error
复制代码

首先明确两个概念:

形参和实参

  • 形参 Type parameter

public class Shop< T>的那个T

表示我要建立一个 Shop 类,它的内部会用到一个统一的类型,这个类型姑且称他为 T 。

  • 实参 Type argument

其它地方尖括号里的全是 Type argument,好比 Shop < Apple> appleShop;的 Apple ;

表示「那个统一代号,在这里的类型我决定是这个」

其实用大白话来讲形参就是定义泛型的地方,实参是传入具体泛型类型的地方。

下边以几个例子来看看用法

Vector<? extends Number> x1 = new Vector<Integer>();    //正确
Vector<? extends Number> x2 = new Vector<String>();    //编译错误
Vector<? super Integer> y1 = new Vector<Number>();    //正确
Vector<? super Integer> y2 = new Vector<Byte>();    //编译错误
复制代码
  1. x1使用了上限统配符,因此能够接收Interger类型。
  2. x2中的String类型并不属于Number及其子类,因此接收失败报错。
  3. y1使用了下限统配符,因此能够接收Number类型。
  4. y2中的Byte类型并不属于Integer及其父类,因此接收失败报错。
List<? extends Fruit> list = new ArrayList<>();
list.add(new Apple());//Error
list.get(0);//不报错

List<? super Fruit> list2 = new ArrayList<>();
list2.add(new Apple());//不报错
list2.get(0);//Error
复制代码

当咱们使用上限通配符时对应方法参数中使用到泛型的方法将都没法调用,由于咱们不能肯定具体传入的是哪一种类型。

当咱们使用下限通配符时对应方法返回值中使用到泛型的方法将都没法调用,由于咱们不能肯定具体返回的是哪一种类型。

这时咱们就有疑问了,既然使用通配符后对应的对象都处于报废状态,那么这东西有啥用? 其实 ? 以及通配符一般都是用在方法参数中的,好比:

extends的例子

public int getTotalWeight(List<Fruit> list) {
    float totalWeight = 0;
    for (int i=0;i<list.size();i++) {
        Fruit fruit=list.get(i);
        totalWeight += fruit.getWeight();
    }
    return totalWeight;
}

List<Apple> listApple = new ArrayList<>();
listApple.add(new Apple());

List<Banana> listBanana = new ArrayList<>();
listBanana.add(new Banana());
int totalPrice=getTotalWeight(listApple)+getTotalWeight(listBanana);//编译报错
复制代码

这种状况是错误的,由于泛型不支持继承,咱们是没法直接传入的。但咱们只要修改一下便可

public int getTotalWeight(List<? extends Fruit> list) {
    float totalWeight = 0;
    for (int i=0;i<list.size();i++) {
        Fruit fruit=list.get(i);
        totalWeight += fruit.getWeight();
    }
    return totalWeight;
}
复制代码

这种状况就OK了,由于这表明着咱们传入的类型是可知的,上限通配符extends能够接受extends右边的类型及其子类。

super的例子

定义一个方法去添加自身

public class Apple extends Fruit{

    void addMeToList(List<Apple> list){
        list.add(this);
    }
}
复制代码
List<Fruit> fruits = new ArrayList<>();
Apple apple=new Apple();
apple.addMeToList(fruits);//报错
复制代码

能够看到当调用方法时会出现报错,由于泛型不支持继承。咱们修改一下

public class Apple implements Fruit{

    void addMeToList(List<? super Apple> list){
        list.add(this);
    }
}
复制代码

这样调用时就不会出现任何问题了。由于下限通配符能够接收super类型后的父类,天然Apple的父类Fruit是确定能够接收的。

进一步加深理解和认知

< T extends E> 和 <? extends E> 有什么区别?

它们用的地方不同,< T extends E>只能用于形参(也就是泛型定义的时候),<? extends E>只能用于实参(也就是传入具体泛型类型的时候)。 好比:

public void addAll(Bean<? extends E> bean;
public <T extends E> void addAll(Bean<T> bean;
复制代码

下面程序合法吗?

class Bean<T super Student> { //TODO }
复制代码

编译时报错,由于super只能用做实参不能用于形参,extends实参形参均可以

下面两个方法有什么区别?为何?

public static <T> T get1(T t1, T t2) {
    if(t1.compareTo(t2) >= 0);
    return t1;
}

public static <T extends Comparable> T get2(T t1, T t2){
    if(t1.compareTo(t2) >= 0);
    return t1;
}
复制代码
  1. get1 方法直接编译错误,由于编译器在编译前首先进行了泛型检查和泛型擦除才编译,因此等到真正编译时 T 因为没有类型限定自动擦除为 Object 类型,因此只能调用 Object 的方法,而 Object 没有 compareTo 方法。
  2. get2 方法添加了泛型类型限定能够正常使用,由于限定类型为 Comparable 接口,其存在 compareTo 方法,因此 t一、t2 擦除后被强转成功。因此类型限定在泛型类、泛型接口和泛型方法中均可以使用,不过无论该限定是类仍是接口都使用 extends 和 & 符号,若是限定类型既有接口也有类则类必须只有一个且放在首位,若是泛型类型变量有多个限定则原始类型就用第一个边界的类型变量来替换。

总结通配符

  1. extends 方法参数中用到了泛型的方法都失效,返回值返回泛型的能够调用。
  2. super 方法参数中用到了泛型的方法能够调用,返回值返回泛型的都失效。 最直观的例子就是List集合中使用了 <? extend 类型> 后对应的add方法都没法调用,get方法能够调用。super与之相反,List集合中使用了 <? super 类型> 后对应的get方法能够调用,add方法都没法调用。
相关文章
相关标签/搜索