Java基础--泛型

14年做开发,至今已有近4年了。越来越多地在进行需求开发,对于之前的一些概念性的东西或者说少用到的东西渐渐的有些遗忘了。打算从本篇开始把一些基础性的东西一点一点拾起来,工程量可能会很大,包括数据结构、基础算法、java基础等等的知识。一点点的进步日积月累也会有巨大的收获,好了,就从现在开始吧。

泛型是什么?

Java 泛型的参数只可以代表类,不能代表个别对象。由于 Java 泛型的类型参数之实际类型在编译时会被消除,所以无法在运行时得知其类型参数的类型。Java 编译器在编译泛型时会自动加入类型转换的编码,故运行速度不会因为使用泛型而加快。Java 允许对个别泛型的类型参数进行约束,包括以下两种形式(假设 T 是泛型的类型参数,C 是一般类、泛类,或是泛型的类型参数):T 实现接口 I 。T 是 C ,或继承自 C 。一个泛型类不能实现Throwable接口。(以上定义来自百度百科)

Java中泛型是在JDK1.5之后引入,在1.5之前如果要实现泛型类的功能,就只能使用Object来进行强制转型。比如下面的例子:

public class A {
    private Object b;

    public void setB(Object b) {
        this.b = b;
    }

    public Object getB() {
        return b;
    }
}
//mainMethod
A a = new A();
a.setB("String");
String b1 = (String)a.getB();
Integer b2 = (Integer)a.getB();

可以看到使用Object的一个问题就是需要频繁地使用强制转型,而编译器在编译期间,不会发现上述代码的错误,在运行阶段,应用程序会crash,并在最后一行报ClassCastException即类型转换异常。由上可以看出,若使用Object来实现上述功能,需要时刻谨慎使用类型转换,以防在运行阶段强转失败。我们将上述代码转换为泛型来实现,如下:

public class A<T> {
    private T b;

    public void setB(T b) {
        this.b = b;
    }

    public T getB() {
        return b;
    }
}
//mainMethod
A a = new A();
a.setB("String");
String b1 = a.getB();
Integer b2 = a.getB();

上述代码编译器会直接报错,错误的类型。泛型的好处可见一斑,运用泛型可以减少强制转换的出现,解决一部分编译期间出现的问题。

泛型擦除

在我们的开发过程中,泛型使用可谓是无处不在,最常使用的ListMap、等等都用到了泛型,在Android中我们使用到RecyclerViewAdapter时,也会用到泛型。

我们来看一个简单的例子:

public static void main(String[] args) {
    List strList = new ArrayList<>();
    List intList = new ArrayList<>();

    System.out.println("strList type is "+strList.getClass());
    System.out.println("intList type is "+intList.getClass());
}

这里简单打印出两个List的类型信息:

strList type is class java.util.ArrayList
intList type is class java.util.ArrayList

可以看到与我们预想的类型信息不一致,不应该打印ArrayListArrayList吗?造成上面结果的原因便是Java中泛型使用的类型擦除问题。

所谓类型擦除,指的是,在编译期间,使用泛型所加上的泛型参数(如上例子中的String、Integer)都会被编译器去掉,这一过程就叫做泛型擦除。

反省擦除后的类型会被重置为Object,因此,泛型的参数类型不可以是基本类型,例如使用List就是不合法的。

泛型通配符?、extends、super

java泛型通配符分为限定通配符和非限定通配符,限定通配符有两种,非限定通配符就是限定了泛型类型必须是T以及T的子类,以此来限定泛型上边界;则限定了泛型类型必须是T以及T的父类,以此来限定泛型的下边界;则不指定泛型类型,任意类型皆可。下面我们来看下具体用法:

public class TestGeneric {

    public static void main(String[] args) {
        List strList = new ArrayList<>();
        List intList = new ArrayList<>();
        strList.add("sss");
        strList.add("ddd");
        strList.add("aaa");
        intList.add(1);
        intList.add(2);
        intList.add(3);

        printFirst(strList);
        printFirst(intList);
    }

    public static void printFirst(List list) {
        if(list.size() > 0) {
            System.out.println(list.get(0));
            list.add(list.get(0));
        }
    }
}

上述我们使用了两个List,使用同一方法打印出各自的第一个元素,并将第一个元素放入队列,将上述代码放入编译器里,很明显的会发现最后一行list.add(list.get(0));会报错,用一句总结来概括就是一旦形参中使用了?通配符,那么除了写入null以外,不可以調用任何和泛型参数有关的方法,当然和泛型参数无关的方法是可以调用的。那么如果我们把printFirst方法的参数改掉,改成printFirst(List list),你又会发现原先的编译错误不见了,程序也能正常执行,很神奇,不过如果在有泛型的类使用时,没有使用泛型,则会有警告出现。

我们来看看限定通配符的使用:

static class Creature{}
static class Animal extends Creature {}
static class Dog extends Animal{}

public static void main(String[] args) {

    List animals = new ArrayList<>();
    animals = new ArrayList();//编译成功
    animals = new ArrayList();//编译成功
    animals = new ArrayList();//编译失败

    animals.add(new Dog());//编译错误
    animals.add(new Animal());//编译错误
    animals.add(new Creature());//编译错误

    Creature creature = animals.get(0);//编译成功
    Animal animal = animals.get(0);//编译成功
    Dog dog = animals.get(0);//编译失败
}

从这几行代码的编译状况可以看出,使用extends限定的泛型类,使用其泛型方法会出错,因为编译器不知道具体方法接收的参数类型(可能是Dog或者Animal或者是Animal的其他子类),调用就会出现编译错误。在后面可以看到所有初始化为Animal及其子类的都通过编译检查。而在get方法中获取到的肯定是Animal及其子类,那么被当做Animal的父类Creature也是可以的。

static class Creature{}
static class Animal extends Creature {}
static class Dog extends Animal{}

public static void main(String[] args) {

    Listsuper Animal> animals = new ArrayList<>();
    animals = new ArrayList();//编译失败
    animals = new ArrayList();//编译成功
    animals = new ArrayList();//编译成功

    animals.add(new Dog());//编译成功
    animals.add(new Animal());//编译成功
    animals.add(new Creature());//编译错误

    Creature creature = animals.get(0);//编译失败
    Animal animal = animals.get(0);//编译失败
    Dog dog = animals.get(0);//编译失败
}

使用super关键字的结果跟刚才恰好相反,所有Animal及其父类作为泛型参数的初始化都编译成功。在调用add方法时由于泛型参数是Animal或者其父类(可以是Animal、Creature或者Creature的父类),因此Animal及其子类可以添加成功(Animal和Dog肯定是T,但是Creature不一定)。在get方法中也是同样的道理,向下转型必须使用强制转换,因此编译出错。

总结一下就是:

  1. extends 可用于的返回类型限定,不能用于参数类型限定。
  2. super 可用于参数类型限定,不能用于返回类型限定。
  3. 带有 super 超类型限定的通配符可以向泛型对易用写入,带有 extends 子类型限定的通配符可以向泛型对象读取。

泛型参数类型获取

在之前泛型擦除中我们已经看到了,在编译过程中,编译器将泛型参数的类型信息去除掉,那么我们怎么能在使用时获取到泛型的参数类型呢?

public static void main(String[] args) {
    Bean bean = new Bean(){};
    Type genericType = bean.getClass().getGenericSuperclass();
    Type firstGenericType = ((ParameterizedType)(bean.getClass().getGenericSuperclass())).getActualTypeArguments()[0];
    System.out.println("genericType "+genericType+"\n firstGenericType "+firstGenericType);
}

public class Bean<T,E> {
}

输出结果如下:

genericType com.xylitolZ.generic.Bean<com.xylitolZ.generic.TestGeneric$Dog, com.xylitolZ.generic.TestGeneric$Animal>
firstGenericType class com.xylitolZ.generic.TestGeneric$Dog

需要注意的是,在初始化的时候我们使用的是new Bean(){}来生成了一个匿名内部类继承Bean,才可以使用下面的方法,否则调用会出错。使用getGenericSuperclass获取从父类继承的泛型参数信息,getGenericInterfaceclass获取从接口中继承的泛型参数。

常见的泛型问题

请说说下面代码片段中注释行执行结果和原因?
DynamicArray ints = new DynamicArray<>();
DynamicArray numbers = ints; 
Integer a = 200;
numbers.add(a);        //这三行add现象?
numbers.add((Number)a);
numbers.add((Object)a);
public void copyTo(DynamicArraysuper E> dest){    
    for(int i=0; i//这行add现象?    
    }
}

三个add方法全部报错,因为numbers泛型类型是Number及Number子类,可能是Integer或者Float等等,因此直接调用add方法添加Integer类型以及Number类型都需要向下转型,未做转型,则会报错。后面的add不会报错,因为super通配符修饰属于向上转型,不需要做强制转型,因此不会报错

请说说下面代码片段中注释行执行结果和原因?
Vector x1 = new Vector();//正确
Vector x2 = new Vector();//编译错误
Vectorsuper Integer> y1 = new Vector();//正确
Vectorsuper Integer> y2 = new Vector();//编译错误

原因参考通配符一节

下面程序合法吗?
class Beansuper Student> { //TODO }

编译时报错,因为 Java 类型参数限定只有 extends 形式,没有 super 形式。(Super在非方法中相当于没有限定)

下面程序有什么问题?该如何修复?
public class Test {      
    public static void main(String[] args) throws Exception{          
        List listInteger = new ArrayList();
        printCollection(listInteger);         
    }       
    public static void printCollection(Collection collection) {         
        for(Object obj:collection){              
            System.out.println(obj);          
        }        
    }  
} 
  

语句printCollection(listInteger); 编译报错,因为泛型的参数是没有继承关系的。修复方式就是使用 ?通配符,printCollection(Collection collection),因为在方法printCollection(Collection collection)中不可以出现与参数类型有关的方法,譬如collection.add(),因为程序调用这个方法的时候传入的参数不知道是什么类型的,但是可以调用与参数类型无关的方法,譬如collection.size()

请解释下面程序片段的执行情况及原因?
public class Test{    
    public static  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.add(10, 20);        
        int t5 = Test.add(100, 22.2);        
        Number t6 = Test.add(121, 22.2);
    }
}

t0 编译直接报错,add 的两个参数一个是 Integer,一个是 Float,所以取同一父类的最小级为 Number,故 T 为 Number 类型,而 t0 类型为 int,所以类型错误。t1 执行赋值成功,add 的两个参数都是 Integer,所以 T 为 Integer 类型。t2 执行赋值成功,add 的两个参数一个是 Integer,一个是 Float,所以取同一父类的最小级为 Number,故 T 为 Number 类型。 t3 执行赋值成功,add 的两个参数一个是 Integer,一个是 Float,所以取同一父类的最小级为 Object,故 T 为 Object 类型。t4 执行赋值成功,add 指定了泛型类型为 Integer,所以只能 add 为 Integer 类型或者其子类的参数。t5编译直接报错,add 指定了泛型类型为 Integer,所以只能 add 为 Integer 类型或者其子类的参数,不能为 Float。 t6 执行赋值成功,add 指定了泛型类型为 Number,所以只能 add 为 Number 类型或者其子类的参数,Integer 和 Float 均为其子类,所以可以 add 成功。 t0、t1、t2、t3 其实演示了调用泛型方法不指定泛型的几种情况,t4、t5、t6 演示了调用泛型方法指定泛型的情况。 在调用泛型方法的时可以指定泛型,也可以不指定泛型;在不指定泛型时泛型变量的类型为该方法中的几种类型的同一个父类的最小级(直到 Object),在指定泛型时该方法中的几种类型必须是该泛型实例类型或者其子类。切记,java 编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,再进行编译的。

下面两个方法有什么区别?为什么?
public static  T get1(T t1, T t2) {      
    if(t1.compareTo(t2) >= 0);    
    return t1;  
}  
public static  T get2(T t1, T t2) {    
    if(t1.compareTo(t2) >= 0);      
    return t1;  
}

get1 方法直接编译错误,因为编译器在编译前首先进行了泛型检查和泛型擦除才编译,所以等到真正编译时 T 由于没有类型限定自动擦除为 Object 类型,所以只能调用 Object 的方法,而 Object 没有 compareTo 方法。get2 方法添加了泛型类型限定可以正常使用,因为限定类型为 Comparable 接口,其存在 compareTo 方法,所以 t1、t2 擦除后被强转成功。所以类型限定在泛型类、泛型接口和泛型方法中都可以使用,不过不管该限定是类还是接口都使用 extends 和 & 符号,如果限定类型既有接口也有类则类必须只有一个且放在首位,如果泛型类型变量有多个限定则原始类型就用第一个边界的类型变量来替换。

什么是 Java 泛型中的限定通配符和非限定通配符?有什么区别?

限定通配符对类型进行限制,泛型中有两种限定通配符,一种是 来保证泛型类型必须是 T 的子类来设定泛型类型的上边界,另一种是 来保证泛型类型必须是 T 的父类来设定类型的下边界,泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。非限定通配符 表示可以用任意泛型类型来替代,可以在某种意义上来说是泛型向上转型的语法格式,因为 ListList不存在继承关系。

简单说说ListList原始类型之间的区别?

主要区别有两点。

  • 原始类型和带泛型参数类型 之间的主要区别是在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查,通过使用 Object 作为类型可以告知编译器该方法可以接受任何类型的对象(比如 String 或 Integer)。
  • 我们可以把任何带参数的类型传递给原始类型 List,但却不能把 List 传递给接受 List 的方法,因为会产生编译错误。
简单说说ListList类型之间的区别

这道题跟上一道题看起来很像,实质上却完全不同。List

List listOfAnyType;List listOfObject = new ArrayList();
List listOfString = new ArrayList();
List listOfInteger = new ArrayList();
listOfAnyType = listOfString; //legal
listOfAnyType = listOfInteger; //legal
listOfObjectType = (List) listOfString; //compiler error 
  

所以通配符形式都可以用类型参数的形式来替代,通配符能做的用类型参数都能做。 通配符形式可以减少类型参数,形式上往往更为简单,可读性也更好,所以能用通配符的就用通配符。 如果类型参数之间有依赖关系或者返回值依赖类型参数或者需要写操作则只能用类型参数。

ListList 之间有什么区别?

有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符,这两个 List 的声明都是限定通配符的例子,List

public static super T>> void sort(List list)
public static  void sort(List list, Comparatorsuper T> c)
public static  void copy(Listsuper T> dest, List src)
public static  T max(Collection coll, Comparatorsuper T> comp)
说说有什么区别?

答:它们用的地方不一样, 用于定义类型参数,声明了一个类型参数 T,可放在泛型类定义中类名后面、接口后面、泛型方法返回值前面。

public void addAll(Bean c)public  void addAll(Bean c) 
说说ListList的关系和区别?

这两个东西没有关系只有区别。因为也许很多人认为 String 是 Object 的子类,所以List应当可以用在需要 List的地方,但是事实并非如此,泛型类型之间不具备泛型参数类型的继承关系,所以ListList没有关系,无法转换。

总结

以上就是本篇文章的所有内容了,关于泛型的使用不仅仅只有这些东西,我们留待以后讨论。

欢迎来我的个人博客与我交流~

上述代码有部分来自网络,如有侵权,请联系我删除。enjoy~

你可能感兴趣的:(Java知识体系)