Java泛型(Generics)

参考:http://docs.oracle.com/javase/tutorial/java/generics/index.html

为什么要使用泛型?

  • 更强更严格的编译期间类型检查
  • 淘汰类型造型
没有泛型的话,下面的代码需要使用造型,否则类型检查会失败

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
如果有泛型,则不需要造型

List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);   // no cast

  • 实现使用泛型的算法,相同的代码可以运用于不同类型的集合

1. 泛型类型
泛型类型是一个使用类型作为参数的类或者接口,可以像下面这样定义一个泛型类,<>紧随类名之后,里面放一些类型参数也叫类型变量,类体中的代码可以使用类型参数作为它的类型,当使用这个类时,用实际的类型替换类型参数,类中使用这个类型参数的变量就会变成该类型。
class name<T1, T2, ..., Tn> { /* ... */ }
1.1 泛型类实例:
public class Box<T> {
    // T stands for "Type"
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }
}
类型变量(实例中的T)可以在类体内当做类型来使用,类型变量可以是主类型以外的任何类型:任何的类、任何接口、任何类型的数组、甚至其他类型变量。

1.2 类型参数命名惯例
按照惯例,类型参数的名字是单个大写字母,常用类型参数名如下:
  • E - Element (广泛使用在Java的集合框架)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types
1.3 调用及实例化泛型
使用上例中的泛型类 Box,需要用一个实际的类型替换T,相当于调用了一个参数为T的泛型转换函数:
Box<Integer> integerBox;
这个语句并没有真的产生一个对象,更普通类一样,要使用new关键字创建对象,注意new之后的类型也要用<Integer>指定具体类型:
Box<Integer> integerBox = new Box<Integer>();
但是在Java SE 7之后,new之后的类型参数可以忽略,但是<>还是要保留:
Box<Integer> integerBox = new Box<>();

1.4 多个类型参数
在一个泛型中可以使用多个类型参数,如下例
public interface Pair<K, V> {
    public K getKey();
    public V getValue();
}

public class OrderedPair<K, V> implements Pair<K, V> {

    private K key;
    private V value;

    public OrderedPair(K key, V value) {
	this.key = key;
	this.value = value;
    }

    public K getKey()	{ return key; }
    public V getValue() { return value; }
}
使用这个泛型类时要提供多个对应的类型替换其中的类型参数:
Pair<String, Integer> p1 = new OrderddPair<String, Integer>("Even", 8); //Java SE 7之后,可以省略new后面的类型参数 new OrderedPair<>("Even", 8);
Pair<String, String> p2 = new OrderedPair<String, String>("hello", "world");

也可以用一个参数化类型(比如List<String>)替换泛型中的类型参数:
OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));


2. 原类型
2.1 原类型是指不用任何实际类型代替类型参数的类型,比如Box的例子
public class Box<T> {
    public void set(T t) { /* ... */ }
    // ...
}
创建一个参数化类型需要将T替换成一个实际的类型比如Integer
Box<Integer> intBox = new Box<>();
但是原类型就不需要替换T,也不需要<>:
Box rawBox = new Box();
所以Box就是泛型类Box<T>的原类型,但是那些并非泛型的普通类就不是原类型。

为什么有原类型?很多的库包括集合库里面含有大量的没有实现泛型的遗留代码,引入原类型是为了向后兼容,将一个参数化类型赋给一个原类型参数是允许的:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox; //没问题
但是如果将一个原类型赋给参数化类型就会有warning:
Box rawBox = new Box();
Box<Integer> intBox = rawBox; //Warning: unchecked conversion
使用原类型调用泛型方法也会有warning:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox; //可以的
rawBox.set(8); //waring: unchecked invocation to set(T)

记住原类型只是为了向后兼容,应尽量避免使用。

2.2 Unchecked Error Messages
我们在使用比较老的API操作原类型时,可能会遇到下列错误:
Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint : unchecked for details

比如下例:
public class WarningDemo {
    public static void main(String[] args){
        Box<Integer> bi;
        bi = createBox();  //unchecked error
    }

    static Box createBox(){
        return new Box();
    }
}

3. 泛型方法
拥有自己的类型参数的方法就是泛型方法,跟定义泛型类相似,但是泛型方法的类型参数被限制在方法的定义范围内,意思是只能在方法内部使用。
定义泛型方法的语法为在方法返回类型前加一个<>,类型参数放置在里面。
public class Util {
    // Generic static method
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
}

public class Pair<K, V> {

    private K key;
    private V value;

    // Generic constructor
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    // Generic methods
    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
    public K getKey()   { return key; }
    public V getValue() { return value; }
}
使用方法如下:
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2); //这里明确给出了泛型方法的参数类型,但是其实可以省略,编译器会自动查找并添加正确的类型参数,可以用这个代替:boolean same = Util.compare(p1, p2); 省略参数类型的特性叫做类型推断(type inference),它可以让我们像普通方法一样调用泛型方法。


4. 参数类型限制
在使用泛型时,有时想要限制使用的类型种类,比如一个操作数字的泛型方法只接受Number对象或者其子类对象,这种需求可以通过参数类型限制实现。
实现参数限制,先列出参数类型的名字,后面紧跟extends关键字,然后指定一个类型上限,下例中是Number。注意,这里的extends既指类中的extends又指接口中的implements。
这个例子中,泛型方法inspect将类型限定为Number,如果在代码中使用Number之外的类型作为参数,编译器就会报错。
public class Box<T> {

    private T t;          

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }

    public <U <strong>extends Number</strong>> void inspect(U u){
        System.out.println("T: " + t.getClass().getName());
        System.out.println("U: " + u.getClass().getName());
    }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<Integer>();
        integerBox.set(new Integer(10));
        integerBox.inspect("some text"); // <strong>error: this is still String! 编译器报错</strong>
    }
}

参数限制允许你调用上限类型(就是一个类)中定义的方法,如下例,参数类型被限制为Integer(或者任何Integer的子类),那我们就可以直接调用Integer里的方法intValue():
public class NaturalNumber<T extends Integer> {

    private T n;

    public NaturalNumber(T n)  { this.n = n; }

    public boolean isEven() {
        return n.intValue() % 2 == 0;
    }

    // ...
}

多重限制
我们也可以将类型参数限制在多个类型上,比如<T extends B1 & B2 & B3>。
如果参数类型限制中有一个类,那必须将这个类放在第一个位置,否则无法编译通过:
Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }

class D <T extends A & B & C> { /* ... */ } //如果换成class D<T extends B & A & C> { /* ... */ },则编译器会报错

泛型方法中的参数类型限制:
参数类型限制是实现泛型算法的关键,下例中的方法计算出数组T[]中大于elem的元素个数。如果不使用参数限制将无法编译,因为大于运算符'>'只能用于主类型中
public static <T> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e > elem)  // compiler error
            ++count;
    return count;
}
比较对象不能使用‘>',我们应该使用对象的compareTo()方法,这样的话我们需要将泛型类型限制为泛型接口Comparable<T>,修改代码如下
public static <T <strong>extends Comparable<T></strong>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.<strong>compareTo</strong>(elem) > 0)
            ++count;
    return count;
}


5. 泛型、继承以及子类型
在Java中,如果类是兼容的(一个类继承自另一个类),那么我们可以把其中一个类对象赋值给另一个类句柄,比如Integer对象可以赋值给Object句柄,因为Integer本身也是一个Object,这个在面向对象中叫“is a”关系:Integer “is a” Object。在泛型中也类似:
Box<Number> box = new Box<Number>();
box.add(new Integer(10));   // OK
box.add(new Double(10.1));  // OK

现在有如下的一个泛型方法,思考下它可以接受哪些类型?
public void boxTest(Box<Number> n) { /* ... */ }
从下图中可以看出,这个方法只接受类型为Box<Number>的参数,其他的如Box<Integer>其实并非Box<Number>的子类,不满足“is a”原则。


对于两个实际的类型A、B(比如Number和Integer),尽管A和B可能是父子关系(“is a”关系),但是泛型类MyClass<A>和MyClass<B>半毛钱关系都没有,他们的共同祖先是最顶级的Object。


5.1 泛型类及子类型
泛型类或接口也可以继承和实现,他们的类型参数之间的关系由extends和implements关键字决定。以Collections相关类为例,ArrayList<E>实现了List<E>接口,List<E>实现了Collection<E>,所以ArrayList<String>是List<String>的子类,List<String>呢又是Collection<String>的子类,只要不改变类型参数,这些类之间的父子关系就不会变。


现在我们自己定义一个List接口,PayloadList,这个接口会为每个list元素配置一个泛型为P的可选值,声明如下:
interface PayloadList<E,P> extends List<E> {
  void setPayload(int index, P val);
  ...
}
那么下面这些类型都是List<String>的子类
PayloadList<String,String>
PayloadList<String,Integer>
PayloadList<String,Exception>



6. 类型推断
类型推断是Java编译器的一种功能,通过检查方法调用和声明来推断出合适的类型参数,推断算法会推断方法参数的类型已经返回值类型(如果存在的话),通过分析代码中的信息最终找到一个适合所有参数的最合适的类型。如下例中,类型推断方法pick的类型应为Serializable:
static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());

6.1 泛型方法中的类型推断
public class BoxDemo {

  public static <U> void addBox(U u, 
      java.util.List<Box<U>> boxes) {
    Box<U> box = new Box<>();
    box.set(u);
    boxes.add(box);
  }

  public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {
    int counter = 0;
    for (Box<U> box: boxes) {
      U boxContents = box.get();
      System.out.println("Box #" + counter + " contains [" +
             boxContents.toString() + "]");
      counter++;
    }
  }

  public static void main(String[] args) {
    java.util.ArrayList<Box<Integer>> listOfIntegerBoxes =
      new java.util.ArrayList<>();
    BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);  //正常的调用方法,不用推断
    BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);  //去掉<Integer>,但是编译器可以推断出addBox()方法的类型为<Integer>
    BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes); //同上
    BoxDemo.outputBoxes(listOfIntegerBoxes); //同样,listOfIntegerBoxes中已经知道类型为<Integer>,编译器推断出outputBoxes()方法类型也是<Integer>
  }
}

6.2 泛型类实例化中的类型推断
在new一个泛型对象是,我们可以用空的<>来调用构建器,编译器可以根据上下文推断出要new的对象的参数类型:
Map<String, List<String>> myMap = new HashMap<String, List<String>>();
//可以用下面的语句代替
Map<String, List<String>> myMap = new HashMap<>();
但是创建泛型类对象时<>不能省,否则会包unchecked conversion警告:
Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning


6.3 泛型类非泛型类的泛型构建器中的类型推断
类(泛型或者非泛型)的构建器也可以使泛型的,下例中类和它的构造器使用了不同的类型参数,这是允许的
class MyClass<X> {
  <T> MyClass(T t) {
    // ...
  }
}
new一个对象是可以这样:
new MyClass<Integer>("")
注意这个new语句指明了类中的类型参数为Integer,构建器中的参数类型没有明确指定,编译器推断出类型为String。

6.4 目标类型
目标类型是编译器根据表达式所在位置推断出的数据类型,比如Collections.emptyList()方法:
static <T> List<T> emptyList();
List<String> listOne = Collections.emptyList(); //这个语句的目的是获取List<String>实例,String就是目标类型,编译器据此推断出List<T>的类型是List<String>
但是在SE7的有些情形中,编译器不能推断出类型,比如下面的函数使用了List<String>类型参数
void processStringList(List<String> stringList) {
    // process stringList
}
我们想通过如下语句调用这个方法,但是这样的话SE 7会得到编译错误:List<Object> cannot be converted to List<String>,SE 8可以编译通过:
processStringList(Collections.emptyList());
SE 7中需要明确指定emptyList()返回的参数类型:
processStringList(Collections.<String>emptyList());


7. 通配符
泛型中,问号(?)叫做通配符,表示一个未知的类型。通配符可以用于很多情形:参数、域、本地变量的类型,也可用于返回类型。但是不能作为泛型方法调用、泛型类对象创建以及超类型的类型参数使用。

7.1 上边界通配符
通过使用上边界通配符,我们可以让变量(泛型)适用更多类型。比如要写一个方法,它的参数可以是List<Integer>、List<Double>、List<Number>,这种情况就可以通过上边界通配符实现。语法如下,通配符(?)跟extends关键字,后面紧跟上边界类。
<? extends [upper bound]>
比如一个方法以类型为Number及其子类的List为参数,这个参数可以写成List<? extends Number>,这样List,Integer>, List<Double>, List<Number>等都是合法的参数。如果写成List<Number>,则只有List<Number>才合法。

7.2 无边界通配符
没有边界的通配符语法像这样:List<?>,叫做类型未知List。无边界通配符有两种用处:
  • 想写一个可以用Object类里的功能实现的方法
  • 代码里用到不使用类型参数的泛型类中的方法,比如List.size()或者List.clear(),Class<?>被经常用到,因为Class<T>里的大多数方法都不使用T
下面这个方法printList的目的是打印出任何类型的List,但是这样写不能达到目的,这个方法只能打印出一个Object对象列表List<Object>,不能打印比如List<Integer>, List<Number>等,因为它们不是List<Object>的子类(它们的父类是Object)
public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}
想达到打印任何类型List的目的,就要使用无边界通配符List<?>,因为对任何的实际类型A,List<A>都是List<?>的子类型
public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}


7.3 下边界通配符
与上边界通配符类似,可以通过关键字super来定义下边界通配符:<? super [lower bound]>。上边界和下边界不能同时设置!
实例:写一个使用类型为Integer的List为参数的方法,如果想提高灵活性,允许这个方法接受List<Integer>, List<Number>, List<Object>等,类型为Integer或者其父类型的List,这种情况可以通过下边界通配符实现
public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

7.4 通配符和子类型化
前文提到过,类型参数之间有关系(父子)的泛型类或接口之间并没有相应的父子关系。但是通过子类型化,我们可以在泛型类或接口之间建立父子关系
List<B> lb = new ArrayList<>();
List<A> la = lb;   // compile-time error,因为List<A>和List<B>之间是没有关系的,他们共同的父类是List<?>
可通过定义上边界的方法创建有父子关系的类
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; //List<? extends Integer>是List<? extends Number>的子类
完全的关系图如下:


7.5 通配符匹配(Wildcard Capture)和帮助者方法
编译器有时可以通过代码上下文推断出通配符的确定类型,叫做通配符匹配。
一般情况下我们不必管通配符匹配,除非遇到包含“capture of”的错误,下例在编译时会产生这样的error:
import java.util.List;
public class WildcardError {
    void foo(List<?> i) {
        i.set(0, i.get(0));
    }
}
原因是从编译器的角度看,方法foo的参数List<?> i是一个类型为Object的List。编译器认为你传了一个错误的类型给参数,泛型就是用来防止这种情况的,使用泛型可以进行跟强的类型检查。

那么我们怎么解决这种错误呢?我们可以通过一个私有的帮助者方法(helper method)来识别通配符,这个解决方法是通过类型推断识别出正确的类型:
public class WildcardFixed {
    void foo(List<?> i) {
        fooHelper(i);
    }

    // Helper method created so that the wildcard can be captured
    // through type inference.
    <strong>private <T> void fooHelper(List<T> l) {
        l.set(0, l.get(0));
    }</strong>
}

现在看一个更复杂的例子:
import java.util.List;

public class WildcardErrorBad {

    void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
      Number temp = l1.get(0);
      l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
                            // got a CAP#2 extends Number;
                            // same bound, but different types
      l2.set(0, temp);	    // expected a CAP#1 extends Number,
                            // got a Number
    }
}
这个例子中,代码里有一些不安全的操作。比如像下面这样调用swapFirst方法,虽然List<Integer>和List<Double>都满足List<? extends Number>的条件,但是将Integer对象插入一个Double类型的List显示是不合法的。
List<Integer> li = Arrays.asList(1, 2, 3);
List<Double>  ld = Arrays.asList(10.10, 20.20, 30.30);
swapFirst(li, ld);
这种情况是不能通过helper方法来解决的,因为这个代码本身就是错误的。


7.6 通配符使用原则
在泛型中,什么时候使用上边界,什么时候使用下边界有时非常confuse,我们可以通过以下原则来决定:
  • “in”变量一般用上边界通配符,使用extends关键字
  • “out”变量用下边界通配符,使用super关键字
  • 如果“in”变量可以被类中的方法访问,则定义为无边界通配符
  • 既是“in”又是“out”的变量,不使用通配符
定义一下“in”和“out”变量:
“in”变量:为代码提供数据的变量,比如copy(src, dest)中的src变量
“out”变量:获取数据以便在其他地方使用,比如copy(src,dest)中的dest变量


8. 类型清除
Java引入泛型以进行更严格的编译期类型检查,为实现泛型,Java编译器在如下情况下会使用类型清除。类型清除保证运行时不会为参数化类型生成新的类,这样泛型机制就不会带来多余的运行时开销:
  • 如果类型参数是无边界的,编译器会用Object取代所有泛型类型参数,如果是有边界的,则用边界类取代所有泛型类型参数,这种情况下,字节码就只包含普通的类、接口以及方法了
  • 必要时插入类型造型,保护类型安全
  • 自动生成桥方法,以保留扩展泛型类型的多态性
8.1 泛型类型的清除
如果类型参数是无边界的,编译器会用Object取代所有泛型类型参数,如果是有边界的,则用边界类取代所有泛型类型参数。
下例中泛型是无边界的,所以编译时这些泛型类型都会被替换成Object
public class Node<T> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) }
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}
无边界的泛型类型T被替换成Object,如下
public class Node {

    private Object data;
    private Node next;

    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Object getData() { return data; }
    // ...
}

如果泛型类型有边界,则替换成边界类/接口
public class Node<T extends Comparable<T>> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}
泛型类型T被替换成Comparable
public class Node {

    private Comparable data;
    private Node next;

    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Comparable getData() { return data; }
    // ...
}

8.2 泛型方法的类型清除
下例中T是无边界的,编译时会被替换成Object
// Counts the number of occurrences of elem in anArray.
//
public static <T> int count(T[] anArray, T elem) {
    int cnt = 0;
    for (T e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}
T被替换成Object类
public static int count(Object[] anArray, Object elem) {
    int cnt = 0;
    for (Object e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}
如以下的继承关系
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
我们可以写一个泛型方法画不同的形状
public static <T extends Shape> void draw(T shape) { /* ... */ }
方法中的T会被替换成边界类Shape
public static void draw(Shape shape) { /* ... */ }






















































  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值