第十五章:泛型(上)

泛型

  • 其实前几章已经零零散散提到过泛型的使用方法了,这一章我们来详细地了解一下泛型是如何使用的。
  • 为什么要有泛型?我们知道一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类型。稍微通用一点,我们可以定义某个方法传入类型是一个基类或者接口,那我们就可以传入它的导出类。那如果我现在要求这个方法能传入其他类型的接口该怎么办?要将这个接口与原来的接口再提取一个新接口吗?这样未免也太麻烦了。
  • 泛型是Java SE5的重大变化之一。泛型实现了参数化类型(我们知道参数值可以变化,此处指类型也可以像参数值一样变化)的概念。不过Java中的泛型不如其他语言中(如C++)的泛型来的纯粹,所以Java中的泛型的用处可能没你想的那么大,但在多数情况下的确可以使你的代码更加优雅(还有什么比代码优雅更重要的呢?哈哈哈)。

简单泛型

  • 有很多原因促成了泛型的出现,而最引人注目的原因,就是为了创建容器类。我们先来看看一个只能持有单个对象的类,这个类可以明确指定其持有的对象类型:
class A {}
public class Test {
    private A a ;
    public A getA() {
        return a;
    }
    public void setA(A a) {
        this.a = a;
    }
}
  • 不过这个类的重用性就不怎么样了,它无法持有其他类型的任何对象。那该怎么改呢?在Java SE5之前,我们可以让这个类直接持有Object类型的对象:
class A {}
class B {}
public class Test {
    private Object data;
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }

    public static void main(String args[]) {
        Test t = new Test();
        t.setData(new A());
        System.out.println(t.getData());
        t.setData(new B());
        System.out.println(t.getData());
    }
}
  • 恩,勉强可以接受。但是通常而言,我们只会使用容器来存储一种类型的对象。我们并不希望同样一个集合对象,可以同时放A或B。或者说我们希望在创建这个集合时就要确定它要存储的对象类型。要达到这个目的,需要使用类型参数,用尖括号括住,放在类名后面。然后在使用这个类的时候,再用实际的类型替换此类型参数。看下面的例子:
class A {}
class B {}
public class Test<T> {
    private T data;
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
    public static void main(String args[]) {
        Test<A> t = new Test<A>();
        t.setData(new A());
        System.out.println(t.getData().getClass().getSimpleName());
        //t.setData(new B());//不被允许! 指定泛型能在编译时检查类型是否对应
        //如果我们需要一个存放B的容器,我们就得重新创建一个新容器
        Test<B> t2 = new Test<B>();
        t2.setData(new B());
        System.out.println(t2.getData().getClass().getSimpleName());
        //下面四种写法在较新的JDK中都是一个意思
        Test t3 = new Test();
        Test t4 = new Test<>();
        Test t5 = new Test<Object>();
        Test<Object> t6 = new Test<Object>();
    }
}
  • 这就是Java泛型的核心概念,告诉编译器想使用什么类型,然后编译器帮你处理一切细节。 接下来是几个应用泛型的例子,用来熟悉一下泛型的使用方式。

一个元组类库

  • 元组(tuple)是指将一组对象打包存储于一个单一对象的一个容器。这个容器对象允许读取其中元素,但是不允许向其中存放新的对象(也可以成为数据传送对象信使)。通常元组可以具有任意长度,同时其对象可以是任意类型。下面是一个二维元组的样例:
public class Test<T> {
    public static void main(String args[]) {
        TwoTuple<String, Integer> tt = new TwoTuple<String, Integer>("hello", 1);
        TwoTuple<Integer, String> tt2 = new TwoTuple<Integer, String>(1, "hello");
        System.out.println(tt.a);
        System.out.println(tt2.a);
    }
}
/*class A {
    void test() {};
}*/

/*  The type parameter A is hiding the type A 注意这段警告
 *  此处的A B是参数化类名,不是明确的class A B
 *  如果你定义了A类,这里的A还是指参数名,和A类没有半毛钱关系
 *  如果你非要A和A类有关系,可以写成<A extends test.A, B> 
 */
class TwoTuple<A, B> {
    /*A a; 
     * void test() {a.test();} //编译错误 这个可以验证此A非彼A
     */
    public final A a;//final保证不会被修改
    public final B b;
    public TwoTuple(A a, B b) {
        this.a = a;
        this.b = b;
    }
}
  • 如果需要多维的元组,我们再重新定义或者继承这个二维元组进行扩展即可。

一个堆栈类

  • 前面我们提到java.util.Stack(已弃用)LinkedList都可以作为堆栈的实现,下面我们将自己定义一个堆栈类,来熟悉一下泛型的使用。
public class Test {
    public static void main(String args[]) {
        LinkedStack<String> s = new LinkedStack<String>();
        System.out.println(s.pop());
        s.push("123");
        s.push("456");
        s.push("789");
        System.out.println(s.pop());
        System.out.println(s.pop());
        System.out.println(s.pop());
        System.out.println(s.pop());
    }
}
class LinkedStack<T> {
    //注意嵌套类要重新定义泛型, 一是减少耦合度, 也许以后会提取出这个内部类
    //二是因为该类定义为static, 不能直接使用T
    //如果要使用T 可以定义成非嵌套类,同样也可以正常使用,
    //因为我们不需要在内部类中访问外围类的非静态属性或方法,所以没这个必要。
    private static class Node<V> {
        V item;
        Node<V> next;
        Node() {}
        Node(V item, Node<V> next) {
            this.item = item;
            this.next = next;
        }
        boolean end() {
            return item == null && next == null;
        }
    }
    private Node<T> top = new Node<T>();//头结点,内容为空
    public void push(T item) {
        top = new Node<T>(item, top);
    }
    public T pop() {
        T result = top.item;
        if (!top.end()) {
            top = top.next;
        }
        return result;
    }
}

RandomList

  • 做为容器的另一个例子,假设我们需要一个持有特定类型对象的列表,每次调用其上的select()方法时,它就可以随机地选取一个元素。如果我们希望以此构建一个可以应用于各种类型的对象的工具,就得使用泛型。考虑到查询速度,我们应该使用数组作为底层实现,所以我们这里用ArrayList作为一个代理容器。
import java.util.ArrayList;
import java.util.Random;

public class Test {
    public static void main(String args[]) {
        RandomList<String> rl = new RandomList<String>();
        rl.addArray("hello world haha hehe memeda".split(" "));
        for (int i = 0; i < 10; i++) {
            System.out.println(rl.select());
        }
    }
}
class RandomList<T> {
    private ArrayList<T> storage = new ArrayList<T>();
    private Random random = new Random(47);
    public void add(T item) {
        storage.add(item);
    }
    public void addArray(T[] items) {
        for (T item : items) {
            add(item);
        }
    }
    public T select() {
        return storage.get(random.nextInt(storage.size()));
    }
}

泛型接口

  • 接口也可以设置泛型,和类设置泛型没有什么区别,来看下面一个生成器(generator)的一个接口例子。
import java.util.Iterator;
import java.util.Random;

interface Generator<T> {
    T next();
}
class Coffee {
    private static int count = 1;
    private final int id = count++;//这样每次创建一个实例都会获得唯一id
    public String toString() {
        return this.getClass().getSimpleName() + " " + id;
    }
}
class Latte extends Coffee {}
class Mocha extends Coffee {}
class Cappuccino extends Coffee {}
class Americano extends Coffee {}
class Breve extends Coffee {}

class CoffeeGenerator implements 
    Generator<Coffee>, Iterable<Coffee> {
    private int size;
    private Class<?>[] types = new Class<?>[] {
            Latte.class, Mocha.class, Cappuccino.class, 
            Americano.class, Breve.class };
    private static Random random = new Random(47);
    public CoffeeGenerator() {}
    public CoffeeGenerator(int size) {
        this.size = size;
    }
    public Iterator<Coffee> iterator() {
        //返回一个匿名类
        return new Iterator<Coffee>() {
            int size = CoffeeGenerator.this.size;
            public boolean hasNext() {
                return size-- > 0;
            }
            public Coffee next() {
                return CoffeeGenerator.this.next();
            }
        };
    }
    public Coffee next() {
        try {
            return (Coffee) types[random.nextInt(types.length)].newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
public class Test {
    public static void main(String args[]) {
        CoffeeGenerator gen = new CoffeeGenerator();
        for (int i = 0; i < 5; i++) {
            System.out.println(gen.next());
        }
        for (Coffee c : new CoffeeGenerator(5)) {
            System.out.println(c);
        }
    }
}
  • 代码本身不是很难懂,只是一个应用的例子而已,这里就不做解析了。

泛型方法

  • 到目前为止,我们看到的泛型,都是应用到整个类上。但同样可以在类中包含参数化方法,而这个方法所在的类可以是泛型类,也可以不是。也就是说,是否拥有泛型方法,与其所在的类是否是泛型没有关系
  • 如果你能尽量使用泛型方法完成必要的功能,就尽量不要使用泛型类。不过对于一个static方法而言,无法访问泛型类的类型参数。所以如果static方法需要使用泛型能力,就必须使其成为泛型方法。来看一下泛型方法是如何定义的。
import java.util.ArrayList;
import java.util.List;

public class Test {
    public static <T> List<T> asList(T... ts) {
        List<T> l = new ArrayList<T>(ts.length);
        for (T t : ts) {
            l.add(t);
        }
        return l; 
    }
    public static <T> List<T> asList2(Object... ts) {
        List<T> l = new ArrayList<T>(ts.length);
        for (Object t : ts) {
            try {
                //类型擦除,这里可以不管
                List.class.getMethod("add", Object.class).invoke(l, t);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return l; 
    }
    public static void main(String args[]) {
        List<String> strl = Test.asList(new String[]{});
        //相当于 List<String> strl = Test.<String>asList(new String[]{});

        //List<Object> strl2 = Test.asList(new String[]{});//报错

        List<Object> strl3 = Test.<Object>asList(new String[]{});//显示指定泛型

        List<? extends Object> strl4 = Test.asList(new String[]{});//也可以用通配符

        //对比strl2
        List<Object> strl5 = Test.asList2(new String[]{});//相当于指定为Object
    }
}
  • 和创建对象时指定泛型一样,调用方法时也可以显式指定泛型,也可以不写。当不主动指定泛型时,如果参数类型存在泛型类型,则会指定其为参数类型。如果参数类型中没有泛型类型,则会被指定为Object。书上还有其他例子,我觉得没必要都放在这,先简单了解一下泛型方法即可。以后的例子我会尽量增加泛型方法的使用。
  • 另外匿名内部类也可以带上泛型,泛型还可以轻松构建复杂的集合。这里都略过了。

擦除的神秘之处

  • 我们先来看看下面的代码:
    System.out.println(new ArrayList<String>().getClass()
           == new ArrayList<Integer>().getClass());//true
  • 结果打印trueArrayList<String>ArrayList<Integer>很容易被认为是两种不同的类型。如果尝试将一个Integer对象放入ArrayList<String>将会失败。但是上面的程序却认为它们是相同的类型。实际上ArrayList<String>ArrayList<Integer>在运行时是相同的类型,这两种形式都被擦除成他们的原生类型ArrayList
  • 在学习类型信息的时候,还有一个方法我没有提到,就是Class.getTypeParameters,他的功能是返回一个TypeVariable的对象数组,表示有泛型声明所声明的类型参数。我们不妨改一下这个代码,看看调用这个方法后会输出什么。
import java.util.ArrayList;
import java.util.Arrays;

class A <T extends ArrayList> {//这种写法叫设置擦除边界,我觉得还不如直接定义T为ArrayList而抛弃泛型
    public void test(T t) {
        t.add(1);
    }
    /*public void test(ArrayList t) {
        t.add(1);
    }*/
}
public class Test {

    public static void main(String args[]) throws Exception {
        Class a = new ArrayList<String>().getClass();
        Class b = new ArrayList<Integer>().getClass();
        System.out.println(a == b);
        System.out.println(Arrays.toString(a.getTypeParameters()));
        System.out.println(Arrays.toString(b.getTypeParameters()));
        System.out.println(Arrays.toString(A.class.getTypeParameters()));
    }
}
---------------------
true
[E]
[E]
[T]
  • 尼玛,竟然输出了定义的泛型名E,我要这类型有何用!
  • 因此,残酷的现实是:

    在泛型代码内部,无法获得任何有关泛型参数类型的信息。

  • Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象(除非使用擦除边界,你就可以使用边界类型的一些方法(默认边界是Object))。
  • 当然存在即合理,也不是说擦除边界就一无是处,来看下面的代码:
class A <T extends ArrayList> {
    private T obj;
    public void test(T t) {
        t.add(1);
    }
    public void set(T t) {
        obj = t;
    }
    //返回类型为泛型时体现了意义,其结果会得到泛型而不是ArrayList,就不用再自行强转了
    public T get() {return obj;}
}

擦除的问题

  • 擦除的代价是显著的。泛型不能用于显式的引用运行时类型的操作之中。例如转型instanceof操作和new表达式。因为所有关于参数的类型信息都丢失了,无论何时,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来好像拥有有关参数的类型信息而已。例如:
class Test<T> {
    T var;
}
Test<Integer> f = new Test<Integer>();
  • 当你创建实例时,Test类的代码应该知道现在工作于Integer之上,而泛型语法也在强烈暗示:在整个类中的各个地方,类型T都在被替换。但是事实并非如此,无论何时,当你在编写这个类的代码时,必须提醒自己:“不,它只是一个Object”。

边界处的动作

  • 为了更加深刻理解擦除,我们有以下两个例子:
class A {
    private Object obj;
    public Object get() {
        return obj;
    }
    public void set(Object obj) {
        this.obj = obj;
    }
    public static void Test() {
        A a = new A();
        a.set("123");
        String str = (String) a.get();
    }
}
class B <T> {
    private T obj;
    public T get() {
        return obj;
    }
    public void set(T obj) {
        this.obj = obj;
    }
    public static void Test() {
        B<String> b = new B<String>();
        b.set("123");
        String str = b.get();
    }
}
  • 下面是通过反编译后获得的字节码:

  public void set(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field obj:Ljava/lang/Object;
       5: return

  public static void Test();
    Code:
       0: new           #3                  // class A
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_0
       8: aload_0
       9: ldc           #5                  // String 123
      11: invokevirtual #6                  // Method set:(Ljava/lang/Object;)V
      14: aload_0
      15: invokevirtual #7                  // Method get:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_1
      22: return
}

class B<T> {
  B();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":
()V
       4: return

  public T get();
    Code:
       0: aload_0
       1: getfield      #2                  // Field obj:Ljava/lang/Object;
       4: areturn

  public void set(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field obj:Ljava/lang/Object;
       5: return

  public static void Test();
    Code:
       0: new           #3                  // class B
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_0
       8: aload_0
       9: ldc           #5                  // String 123
      11: invokevirtual #6                  // Method set:(Ljava/lang/Object;)V
      14: aload_0
      15: invokevirtual #7                  // Method get:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_1
      22: return
}
  • 然后你会惊奇的发现,这不是一模一样吗?注意18: checkcast即使使用泛型后不必显式的书写转换代码,编译器也会自动为我们带上这个操作。这里举这个例子是想说泛型中的所有动作都发生在边界(不是擦除边界的那个边界,是指进出类的时间点)处——对传递进来的值进行额外的编译器检查,并插入对传递出去的值的转型。

擦除的补偿

  • 正如我们看到的,擦除丢失了在泛型代码中,执行某些操作的能力。任何在运行时需要知道确切类型信息的操作都将无法工作:
class A <T> {
    public void getNewA(Object arg) {
        if (arg instanceof T) {}//error
        T var = new T();//error
        T[] array = new T[10];//error
        T[] array = (T[]) new Object[10];//warning
    }
}
  • 偶尔可以绕过这些问题来编程,但是有时必须通过引入类型标签来对擦除进行补偿。这意味着你需要显式地传递你的类型的Class对象,以便你可以在类型表达式中使用它。
import java.lang.reflect.Array;

public class Test {
    public static void main(String args[]) throws Exception {
        A<Arg> a = new A<Arg>(Arg.class);
    }
}
class Arg {}
class A <T> {
    Class<?> type;
    public A (Class<?> type) {
        this.type = type;
    }
    public void getNewA(Object arg) throws InstantiationException, IllegalAccessException {
        //if (arg instanceof T) {}//error
        if (type.isInstance(arg)) {}
        //T var = new T();//error
        T var = (T) type.newInstance();//需要无参构造器,最佳解决方案是使用工厂模式
        //T[] array = new T[10];//error
        T[] array = (T[]) Array.newInstance(type, 10);//然而由于版本原因,jdk中有很多的代码不是这么写的
    }
}
  • 我们来看一个例子:
public class Test {
    public static void main(String args[]) throws Exception {
        A<String> a = new A<String>(1);
        a.put(0, "123");
        System.out.println(a.get(0));
        String [] arr = a.rep();//error 注意必须进行赋值操作,或调用函数,否则不会进行类型转换检查
        //这也正是某些集合(例如ArrayList)无法将握有的数组引用直接转换成明确数组类型的原因
    }
}
class A <T> {
    private T[] array;
    public A(int size) {
        array = (T[]) new Object[size];
    }
    public void put(int index, T item) {
        array[index] = item;
    }
    public T get(int index) {
        return array[index];
    }
    public T[] rep() {return array;}
}
  • 还记得前面说过的泛型边界吗?对传递进来的值进行额外的编译器检查,并插入对传递出去的值的转型。在A类的内部,array的类型是Object[]而不是的String[]。所以哪怕该数组下面的所有元素都是String,当我们调用rep()时,这种自动转型也依旧会造成ClassCastException。当然对于数组握有的引用将其转换成T是可以的,因为其本身就是T类型。要注意数组本身是一个特殊的类型,数组的每个元素可以是数组声明的类型的任意的子类型。
  • 因为无法正确转型,一般集合都是这么定义数组的:
class A <T> {
    private Object[] array;
    public A(int size) {
        array = new Object[size];
    }
    public void put(int index, T item) {
        array[index] = item;
    }
    public T get(int index) {
        return (T) array[index];
    }
    public T[] rep() {return (T[]) array;}
}
  • 这种写法可能会更清晰一点,这将明确得告诉我们array的真实类型是Object[]所以我们要深刻理解到,在泛型类的内部,T的意义就是其擦除边界而不是什么具体类型)。这也是ArrayList的写法,不过其调用rep()同样会造成CCE异常
  • 其实我们要认识到,创建一个数组,其实相当于调用了数组类的构造方法,这个构造方法一般传入大小作为参数。我们稍微修改一下第一个例子,这样调用rep()就不会出现异常了。
import java.lang.reflect.Array;

class A <T> {
    private T[] array;
    public A(Class<?> type, int size) {
        array = (T[]) Array.newInstance(type, size);
    }
    public void put(int index, T item) {
        array[index] = item;
    }
    public T get(int index) {
        return array[index];
    }
    public T[] rep() {return array;}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值