Java 泛型

泛型(generics)是Java 1.5 中引入的特性。泛型的引入使得代码的灵活性和复用性得以增强,对于容器类的作用更为明显。

泛型可以加在类、接口、方法之上。如下所示:

public class Generic1<T> {
    T t;
    List<T> list;

    //表示返回值为K,参数类型为K
    public <K> K test(K e) {
        return e;
    }
}

泛型类型参数以<>定义,括号内可以定义多个泛型,如<K,V>。

泛型的类型参数只能是对象类型(包括自定义类),不能是简单类型。定义了泛型后,就可以在原来使用具体类型的地方以泛型代替。注意泛型添加的位置,如果是类上的泛型,添加在类名之后;如果是方法上的泛型,添加在修饰符之后,返回值之前。

定义了泛型之后,我们就可以使用了。

public class Generic1<T> {
    T t;

    public <K> K test(K e) {
        return e;
    }

    public static void main(String[] args) {
        Generic1<String> g = new Generic1<>();
        System.out.println(g.test(2));
        System.out.println(g.test("2"));
    }
}

可以看到,我们只需要在使用的时候指定具体的类型即可。我们可以给 test 方法传递任意类型的参数,在没有泛型前,我们只能用方法重载实现。


类型上界

在上面的例子中,我们可以给类传递任何泛型参数。

如果我们有这样一个需求,传递的参数要是某个类的子类。

比如现在有一个类,表示将传进来的水果制成果汁,那传进来的类只能是某种水果,而不能是其它东西。extends 可以实现这样的效果。

extends 关键字指定泛型类型的上界,表示该类型必须是继承某个类,或者实现某个接口,也可以是这个类或接口本身。

示例如下:

public class Generic1<T extends List<String>> {
    T t;
    List<T> list;

    public <K extends Number> K test(K e) {
        return e;
    }

    public static void main(String[] args) {
        Generic1<ArrayList<String>> g = new Generic1<>();
        System.out.println(g.test((byte) 2));
        System.out.println(g.test(2));
        System.out.println(2L);
        System.out.println(g.test(2.0f));
        System.out.println(g.test(2.0));
        //无法编译,提示参数类型错误
        //System.out.println(g.test("hello"));
    }
}

在这个例子中, Generic1 类上的泛型参数只能接受 List 或 List 的子类,传递给 test( ) 方法的只能是 Number 类型的数据。

当没有指定泛型继承的类型或接口时,默认为 extends Object,此时任何类型都可以作为参数传入。

注意:对于 ? extends 的通配符限定泛型,我们无法向里面添加元素(只可以添加null),只能读取其中的元素。

类型下界

super 指定泛型类型的下界,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object。

示例如下:

class Fruit {}

class Apple extends Fruit {}

class Banna extends Fruit {}

class FujiApple extends Apple {}

public class Generic2 {
    public static void test(List<? super FujiApple> list) {
        list.add(new FujiApple());
        //list.add(new Apple());编译错误
    }
}

我们可以向 list 中通过 add( ) 方法添加 FujiApple 类,但却不能添加 Apple( ) 类。事实上我们只能添加 FujiApple 及其子类,而不能添加它的任意超类。

正确的用法应该是这样的:

public class Generic2 {
    public static void test(List<? super FujiApple> list) {
        list.add(new FujiApple());
        //list.add(new Apple());编译错误
        System.out.println(list);
    }

    public static void main(String[] args) {
        List<? super FujiApple> list = new ArrayList<Apple>();
        List<? super FujiApple> list1 = new ArrayList<Fruit>();
        //编译错误
        //List<? super FujiApple> list2 = new ArrayList<Banana>();
        test(list);
        test(list1);
    }
}

没错,就是多态,super 提供了多态支持。

注意:对于 ?super 的通配符限定泛型,我们可以读取其中的元素,但读取出来的元素会变为 Object 类型。


通配符(Wildcards)

?叫做通配符,表示任意类型,上面的例子中已经出现了。它与类型参数T的不同点如下:(Java 泛型通配符和类型限定

  • T 只有extends一种限定方式,<T extends List>是合法的,<T super List>是不合法的
  • ?有extends与super两种限定方式,即<? extends List> 与<? super List>都是合法的
  • T 用于泛型类和泛型方法的定义。?用于泛型方法的调用和形参,即下面的用法是不合法的
public class Generic1<? extends List<String> {
    public <? extends List> void test(String t) {
    }
}
  • T 可用于多重限定,如 <T extends A & B>,通配符 ?不能进行多重限定

PECS法则

生产者(Producer)使用 extends,消费者(Consumer)使用 super。

如果需要读取 T 类型的元素,需要声明成 <? extends T>,例如 List<? extends Apple>,此时不能往列表中添加元素。

如果需要添加 T 类型的元素,需要声明成 <?super T>,例如 List<? super Apple>,此时可以向其中添加 Apple 及其子类。从其中取元素的时候,要注意取出元素的类型是 Object。

如果需要同时添加和使用,不使用泛型通配符。


泛型与数组

不能创建泛型数组,下面的语句是无法编译通过的

ArrayList<String>[] genericArray = new ArrayList<String>[10];

数组是协变的,即如果A ≤ B,则 f(A) ≤ f(B),举个例子:

Number[] i = new Integer[10];

因为Integer是Number的子类,所以我们可以将Integer类型数组赋给Number类型数组的引用变量。我们自然会想到,泛型是否也可以这样?如下所示:

ArrayList<Number> list = new ArrayList<Integer>();

事实上,上面这句是无法编译通过的。很显然,泛型不是协变的,泛型具有无关性。正确的使用方法如下:

ArrayList<? extends Number> list = new ArrayList<Integer>();

静态成员与静态方法

无法通过类上的泛型定义类的泛型静态成员变量和静态方法。

例如,下面的写法是错误的

public class Generic3<T extends List> {
    private static T t;
    public static T void test(String t) {}
}

类的静态变量与静态方法是该类所有示例共享的,如果有两个实例具体化了不同的参数类型,那此时静态变量和静态方法的泛型到底是哪一个呢?因此才有这个限制。但你可以在静态方法上加泛型,如下:

public static <T> void f(T t) { }

泛型擦除(Type Erasure)

Java中的泛型擦除是指在编译后的字节码文件中类型信息被擦除,变为原生类型(raw type),因此在运行期,ArrayList<Integer> 与 ArrayList<String> 就是同一个类。

实际上 Java 泛型的擦除并不是对所有使用泛型的地方都会擦除的,部分地方会保留泛型信息。泛型技术相当于 Java 语言的一颗语法糖,这种实现泛型的方法称为伪泛型(参考深入理解Java虚拟机第二版)

在泛型类被类型擦除的时候,如果类型参数部分没有指定上限,如 <T> 会被转译成普通的 Object 类型,如果指定了上限,则类型参数被替换成类型上限。

例如,下面的例子在编译期无法通过

public class Generic4 {
    public void test(ArrayList<Integer> list) {
    }

    public void test(ArrayList<String> list) {
    }
}

ArrayList<Integer> 与 ArrayList<String> 编译后都被擦除了,变成了原生类型 ArrayList。

(注:深入理解Java虚拟机(第二版)所说加返回值后,javac 可以编译通过,经测试,在Java 1.8 下无法编译通过)

我们可以借助Java的Type接口获取泛型(Java中的Type详解)。

看下面一个例子:

class FF<K, V> {}

public class Generic4<K extends Integer, V extends String> extends FF<String, Integer> {

    public static void main(String[] args) throws NoSuchFieldException, SecurityException {
        Generic4<Integer, String> instance = new Generic4<>();
        System.out.println(Arrays.toString(instance.getClass().getTypeParameters()));
        System.out.println(instance.getClass().getGenericSuperclass());

        System.out.println(Arrays.toString(Generic4.class.getTypeParameters()));
        System.out.println(Generic4.class.getGenericSuperclass().getTypeName());

        System.out.println("-----------------------------------------------");

        Map<Integer, String> map = new HashMap<Integer, String>();
        Map<Integer, String> map1 = new HashMap<Integer, String>() {
        };
        ParameterizedType paraType1 = (ParameterizedType) map.getClass().getGenericSuperclass();
        Type[] type1 = paraType1.getActualTypeArguments();
        System.out.println(Arrays.toString(type1));
        ParameterizedType paraType2 = (ParameterizedType) map1.getClass().getGenericSuperclass();
        Type[] type2 = paraType2.getActualTypeArguments();
        System.out.println(Arrays.toString(type2));

        System.out.println("-----------------------------------------------");

        FF<String, Integer> ff = new FF<>();
        System.out.println(ff.getClass().getGenericSuperclass());
    }
}

输出结果:

[K, V]
com.test.FF<java.lang.String, java.lang.Integer>
[K, V]
com.test.FF<java.lang.String, java.lang.Integer>
-----------------------------------------------
[K, V]
[class java.lang.Integer, class java.lang.String]
-----------------------------------------------
class java.lang.Object

泛型的几点结论:

  • 如果通过 new 创建了类或者直接通过类似 FF.class 的形式(我们无法使用 FF<String,Integer>.class 这样的形式),我们并不能因此获得实际的类型变量,通过反射只能得到占位符的形式。这么做是要避免在创建泛型实例时而创建新的类,从而避免运行时的过度消耗
  • 如果继承了类A或实现了接口B,并且具体化了A或B中的泛型,那么可以获得A或B中的实际的类型变量
  • 对于成员变量,我们只能得到与类上的泛型声明相同的结果
  • 对于方法声明中的泛型,如果没有指定上限,通过反射返回的是 Object 类型,否则返回的是我们指定的上限
  • 对于方法参数中和方法内部的泛型(方法内部的泛型可以借助匿名内部类间接获取泛型),我们无法获得它的实际的变量类型,只能得到占位符的形式

泛型与序列化

当序列化一个泛型类,然后反序列化时,会丧失原有的类型信息。示例如下:

class Serial<T> implements Serializable {
    ArrayList<T> list = new ArrayList<>();

    public void f(T t) {
        list.add(t);
    }
}

public class Generic5 {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, SecurityException {
        Serial<String> serial = new Serial<>();
        serial.f("hello");
        //序列化对象
        FileOutputStream out = new FileOutputStream("e:/ToSerial.txt");
        ObjectOutputStream objectToOut = new ObjectOutputStream(out);
        objectToOut.writeObject(serial);
        //反序列化对象
        ObjectInputStream objectToRead = new ObjectInputStream(new FileInputStream("e:/ToSerial.txt"));
        Serial<Float> restore = (Serial) objectToRead.readObject();

        restore.f(2.0f);
        System.out.println(restore.list);

        objectToOut.close();
        objectToRead.close();
    }
}

输出结果:

[hello, 2.0]

从结果可以看到,反序列化后我们可以将浮点数加入到原本是 String 类型的 list 中,说明反序列化后原有的类型限制消失了。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐