【从基础学 Java】泛型

引言

在面向对象的世界里,咱们若是须要一个容器来盛装对象。举个例子:一个篮子。咱们能够用这个篮子装苹果,也能够用这个篮子装香蕉。基于 OOP 的思想,咱们不但愿为苹果和香蕉分别建立不一样的篮子;同时,咱们但愿放进篮子里的是苹果,拿出来的仍是苹果。因而,Java 程序员提出了「泛型」的概念——一种相似于 C++ 模板的技术。java

早期程序员使用以下代码建立一个泛型集合:程序员

public class ArrayList{
    private Object[] elementData;
    ...
    public Object get(int i);
    public void add(Object o);
}

咱们能够看出,对与这个集合而言,取出 (get) 和放入时都没有进行类型检查。所以,若是咱们不记得放入的顺序,把取出的对象进项强制类型转换,极可能出现 ClassCastException。所以,真正的泛型是能够在编译时期,对数据类型进行检查,保证安全。以下面代码所示:算法

ArrayList<String> list = new ArrayList<>();

P.S. <>里的String叫作类型参数。segmentfault

使用泛型,为咱们提供了以下优势:数组

  • 更强大的编译时期的类型检查
  • 避免没必要要的类型转换,如:
List<String> list = new ArrayList<>(3);
String str = list.get(0);
  • 让程序可以实现通用的算法

泛型类

泛型中使用名为泛型参数代表能够传入类或方法的对象类型,这种设计实现了类型参数化(能够把同一类型的类做为参数进行传递),以下面的代码所示:安全

泛型类示例框架

public class Pair<T>{
    private T first;
    private T last;
    
    public Pair(){}
    public Pair(T first, T last){
        this.first = frist;
        this.last = last;
    }
    
    public T getFirst();
    public T getLast();
}

泛型方法示例this

public class Util{
    // 简单的泛型方法
    public static <T> T getMiddle(T...a){
        return a[a.length/2];
    }
    // 带限定符的泛型方法,若是有多个限定符,使用 & 链接多个接口或超类
    public static <T extends Comparable> T min(T...a){
        // 具体实现
    }
}

注意,这里的泛型参数(type parameter)在上述示例中指的是用大写字母 T 表示的值,而泛型实参(type argument)则是指 T a 中的 a。根据惯例,泛型参数一般以下命名:spa

  • E:表示一个元素,Java 集合框架中使用最多
  • K:键
  • N:数字
  • T:类型
  • V:值
  • S,U,V:其它类型

原始类型(raw type)

原始类型指的是,不包括泛型参数的类型,如上述泛型类中的 Pair。咱们能够经过原生类型构造对象:设计

Pair pair = new Pair();

同时,能够经过泛型参数构造对象:

Pair<String> pair = new Pair<>();

可是,若是把一个经过原生类型获取的对象指向一个经过泛型参数生成的参数会报 unchecked warning,以下面的代码:

Pair pair = new Pair();
Pair<String> pair1 = pair;

继承和子类型

在 Java 中,有继承的概念,简而言之,就是一个类型能够指向它的兼容类型,如:

Object object = new Object();
Integer integer = new Integer(20);
object = integer;

上述代码表示:Integer IS-A Object。这种概念在泛型中也适用。以下定义:

public class Box<T extends Number>{
    public void add(T t);
}

那么一个 Box 的对象能够增长任意 Number 子类的值。可是 Box<Double>Box<Integer> 不是同一个类型。

泛型方法

泛型类中能够定义静态、非静态的泛型方法。泛型方法的语法为:<泛型参数类型列表> + 返回类型 + 泛型参数列表。

  • 静态方法
public static <T> void foo(T t){
}
  • 非静态方法
public void foo(T t){
}

类型限定

在某种状况下,咱们但愿方法只接受特定类型的参数,可使用以下语法实现:

public <U extends Number> void inspect(U u){
    // 这里是逻辑处理
}

上述代码中,该泛型方法只接受为 Number 类型的参数。一样,也能够在泛型类上加以限制:

public class Utils<T extends Number>{
    // 这里的 T 必须为 Number 类型
    private T t;
}

固然,也可使用多重限制,以下面代码所示:

public class Utils<T extends A & B & C>{

}

P.S. 限制中的类必须放在接口的前面。

类型推断

类型推断是:编译器去推断调用方法的参数的类型的能力。
如,泛型方法中:

public <U> void addBox(Box<U> box){
    // 这里是处理代码
}

没必要经过 obj.<U>addBox(box) 调用,<U> 能够省略。

构造方法中:

// 类型推断
Map<String,List<String>> map = new HashMap<>();

其中,构造方法中的泛型还能够这样用:

// 定义泛型类
public class Box<X>{
    public <X> Box(T t){
    
    }
}
// 实例化一个对象
public class Application{
    void method(){
        Box<Integer> box = new Box<>(");
    }
}

通配符

通配符 ? 表示一个未知的类型,可用于参数的类型、字段以及局部变量中,但不可用于调用泛型方法里的类型参数、泛型对象实例化以及泛型超类里。

// 能够
public void foo(Pair<? extends Number> pair){
    // 能够
    Pair<? super Integer> foo;
}
// 能够
private Pair<? super Integer> pair;

上界通配符

上界通配符代表须要最高限定的类型,下面的代码用来计算全部类型为数字的集合的总和:

public double sumList(List<? extends Number>){
    // 这里作逻辑处理
}

无界限通配符

使用无界限通配符表示不肯定的类型,如下两种状况可使用无界限通配符:

  • 当方法的参数能够用 Object 对象替换
  • 方法的实现不依赖具体的类型

好比,有一个打印集合对象的方法:

// 定义一个打印集合对象列表的方法
public void printList(List<?> list){
    for(Object obj: list){
        // 打印list
    }
}
// 调用方法
List<Integer> integers = Arrays.asList(1,2,3);
List<String> strings = Arrays.asList("A","B","C");
printList(integers);
printList(strings);

P.S. List<?>List<Object> 不一样,List<?> 只能插入 nullList<Object> 能够插入任何对象。

下界通配符

使用下界统配符,代表最低限度的类型,如:

public double sumList(List<? super Duble>){
    // 这里作逻辑处理
}

通配符和子类型

在本文的继承和子类里,提到过:Box<Double> 不是 Box<Number> 的子类。在 Java 泛型中,继承关系能够经过以下图表示:

泛型的继承关系

能够看出,泛型中的 extends 的确限定了上界(父类);super 的确限定了下界(子类型);? 是全部泛型的超类(相似 Object)。

泛型的继承关系(父子类型关系)能够经过下面的韦恩图解释:

泛型的继承关系-韦恩图

咱们不妨用某一泛型所占的面积表示其层次关系,面积大的在继承关系上层次高。由上图很容易看出:<? super Integer> 的继承层次比 <? super Number> 的继承层次高;相应地,<? extends Integer> 的继承层次比 <? extends Number> 的继承层次低。

使用泛型的场景

调用一个方法:foo(src, dest);src 看作入参,dest 看作出参,基于如下规则决定是否使用和如何使用泛型:

  • 入参使用上界通配符:extends
  • 出参使用下界通配符:super
  • 入参能够用 Object 代替的,使用无边通配符
  • 须要获取入参和出参的变量,不要使用通配符

这种原则也叫作 PECS(Producer Extends Consumer Super) 原则。

类型擦除

类型擦除确保被参数化的类型不会建立新的类,不会产生运行时的开销。

泛型擦除时,编译器作了一点小小的工做:若是该泛型参数有边界限制,替换成它的边界;不然,用 Object 替换。
上述泛型类 Pair<T> 会被替换成下面形式:

class Pair{
    Object first;
    Object last;
    public Object getFirst(){}
    public Object getLast(){}
}

P.S. 通常使用第一个限定类型替换变为原始类型,没有限定类型,使用 Object 替换。

桥接方法

当子类继承(或实现)父类(或接口)的泛型方法时,在子类中指明了具体的类型。编译器会自动构建桥接方法(bridge method)。如:

class Node<T>{
    private T t;
    public Node(T t){
        setT(t);
    }
    public void setT(T t){
        this.t = t;
    }
}

class MyNode extends Node<Integer>{
    public MyNode(Integer i){
        super(i);
    }
    public void setT(Integer i){
        super.setT(i);
    }
}

在上述代码中,编译时期,因为泛型擦除,Node 中的方法为 setT(Object t) 而 MyNode 中的方法为 setT(Inetger i) 。签名不匹配,再也不是重写,所以,编译器为 MyNode 生成以下桥接方法:

// 桥接方法
public void setT(Object i){
    setData((Integer)i);
}

public void setT(Integer i){
    super.setData(i);
}

非具体化类型

非具体化类型定义

具体类型(Reifiable Type)指的是:原始数据类型、非泛型类型、原生类型和调用不受限的通配等在运行时期,信息不会丢失的类型。
非具体类型(Non-Reifiable Type)在运行时期不能获取其全部的信息,如 JVM 没法区别 List<String>List<Integer> 。所以,这种类型不能使用相似 instanceof 的方法。

堆污染

堆污染指的是:一个参数化类型指向一个非该参数化类型对象的过程。一般是,在程序中进行了一些操做,使编译时期发生未检查(unchecked)警告时发生。如:混用原始类型(Raw Type)和参数化类型。

使用非具体化类型作可变参数的潜在缺陷

当使用可变参数做为泛型输入参数时,会形成堆污染。如:
能够经过以下注解消除编译时期的警告:

  • @SafeVarargs
  • @SuppressWarnings({"unchecked", "varargs"})

泛型的限制

虽然泛型是如此的便利,但难免有缺点:

  • 不能用基本类型实例化类型参数
// 编译出错
List<int, int> array = new ArrayList<>();
  • 不能经过类型参数实例化对象
public static <E> void foo(List<E> list){
    // 编译出错
    E element = new E();
    list.add(element);
}
  • 不能建立泛型变量类型的静态字段
public class Foo<T>{
    // 编译出错
    private static T field;
}
  • 不能使用 instanceof 来确认参数类型
public static <E> void foo(List<E> list){
    // 编译出错
    if(list instanceof ArrayList<Integer>){
    }
}
  • 不能建立参数化类型数组
// 编译出错
List<String>[] strings = new ArrayList<>[2];
  • 不能抛出或捕获泛型类实例
// 编译出错
public class FooException<T> extends Exception{
}
  • 不能重载擦除后有一样方法签名的方法
public class Example{
    // 编译出错
    public void print(Set<String> string){
    }
    public void print(Set<Integer> integer){
    }
}
  • 运行时类型查询只适用于原始类型
  • Varargs 警告
  • 泛型类的静态上下文的类型变量无效
相关文章
相关标签/搜索