java 泛型

目录

一、声明泛型类型

1.1、泛型类(接口)

1.2、泛型方法

1.3、raw type(生的类型)

1.4、有界限的类型参数

1.4.1、多界限

二、类型推断

2.1、泛型构造函数的推断

2.2、目标类型

三、类型擦除

3.1、替换类型参数

3.2、在适当的出口和入口添加类型转换语句

3.3、产生桥接方法保护泛型继承中的多态

四、通配符

4.1、无界限通配符

4.2、有上界的通配符

4.3、有下界的通配置

五、继承

六、通配符使用准则

七、泛型的限制

术语:

参考


简而言之,泛型使类型能够成为参数。通过泛型可以使得运行时出错的bug在编译时就可以检测到。声明泛型时可以指定上界,在传入通配符做类型实参时可以指定上下界。编译时类型会被检查,然后擦除。

一、声明泛型类型

1.1、泛型类(接口)

一个泛型的通用形式:

class ClassName<T1, T2, ..., Tn> { /* ... */ }

T1,T2,,,Tn就是待传的泛型参数(类型形参)。而调用泛型类型(invocate generic type)时传入的参数(类型实参)不能为基本类型,比如Box<Integer> integerBox,不能换成int,此时Box<Integer>被称为参数化类型(parameterized typex)

实例化时可以显示传入类型或者由编译器自己推断:

ClassName<T1,T2,....,Tn> objectName=new ClassName<String,Integer,....,Object>();//显示参数类型参数
ClassName<T1,T2,....,Tn> objectName=new ClassName<>();//由编译器推断

注意,<>不能少,否则会被当做raw type产生警告。

注意,,声明和实例化都是要参入类型实参的,只是实例化时可以由编译器自行推断。

注意,,,不能在静态方法中使用类型形参,只能将该静态方法定义为泛型方法。

1.2、泛型方法

与泛型类相似,只是泛型方法的类型参数作用域限于在方法体中。静态和非静态泛型方法都一样,甚至可以声明泛型构造函数。泛型方法声明就是在返回类型前声明类型参数,如下面的方法所示,K和V是方法compare的类型参数。

public class Util {
    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());
    }
}

使用泛型方法时也可以显示给出类型实参或由编译器推断:

boolean same = Util.<Integer, String>compare(p1, p2);//显示传入类型参数
boolean same = Util.compare(p1, p2);//编译器推断

1.3、raw type(生的类型)

raw type是针对于泛型类型而言的,非泛型(接口)不是raw type。比如泛型类List<E>,它raw type为List,即不传入类型参数。raw type是来了向后兼容而存在的,但是将参数化类型赋值给raw type或将raw type赋值给参数化类型都会参数产生警告。

1.4、有界限的类型参数

有界限的类型参数(形参)可以限制传入的类型(实参)。不要和有界限的通配符混淆,通配符是用作实参的。通过如下形参声明有界限的类型参数:

<U extends Number>

此时U只能是U或其子类Integer、Double等。它确定了传入类型实参的上界,因此可以把U当做Number类型使用。貌似只能定义上界,也不要和有上界的通配符混淆了。。

1.4.1、多界限

可以为指定多个可传入的参数类型界限,如果有一个界限为类,那么该类列在第一个:

<T extends B1 & B2 & B3>

那么传入的类型实参必须是所有界限类型的子类型,然后可以将类型形参当做列表中的一个类型使用。

二、类型推断

如果在方法调用和相应的声明(对象实例相当于方法调用)处没有显示给出类型实参,那么编译器会自动推断类型实参的类型。推断算法会利用传入的普通参数的类型、目标类型和期待的返回参数类型来推断类型实参的类型,有可能的话,还会推断返回参数的类型。推断出来的类型实参是最具体符合所有参数的类型。

再实例化泛型类时,<>不能被省略掉,否则会被看多raw type,不会进行类型推断。

Map<String, List<String>> myMap = new HashMap<>();

2.1、泛型构造函数的推断

构造函数也可以是泛型的:

class MyClass<X> {
  <T> MyClass(T t) {
    // ...
  }
}

构造函数的类型实参只能由编译器推断:

new MyClass<Integer>("")//类的类型实参为Integer,构造函数的类型实参为String
MyClass<Integer> myObject = new MyClass<>("");//与上述一样

2.2、目标类型

编译器会根据目标类型来推断类型实参,比如下面一个例子:

static <T> List<T> emptyList();

使用如下赋值语句:

List<String> listOne = Collections.emptyList();

此时根据目标listOne的类型List<String>,那么类型实参T则为String。

要注意的是,jdk8可以从方法形参中推断类型实参了,看下面的函数:

void processStringList(List<String> stringList) {
    // process stringList
}

执行函数:

processStringList(Collections.emptyList());

如果是jdk7,它不能从processStringList的形参中获得相关信息推断emptyList的类型实参,因此默认为Object,导致cast错误。jdk8可以,因此正常运行。

三、类型擦除

编译器在编译阶段对类型进行检查,然后擦除了类型,因此不会增加运行时负担,也不会有新的类型产生。比如说List<E>即使传入不同的类型参数,得到参数化类型List<String>、List<Integer>,实际上他们仍然是同一个类的实例,只是在对应的入口、出口部分使用了类型转化。

3.1、替换类型参数

如果是无界限的类型参数,则替换为Object;如果有界限,假设界限为Number,那么将类型参数替换为Number

3.2、在适当的出口和入口添加类型转换语句

比如:

class Node<T>{
    T data;
    public void set(T d){data=d;)
    publci T get(){ return data;}
}
....
Node<String> node=new Node<>();
node.set("aaa");
node.get();
-------------------------
擦除类型:
class Node{
    Object data;
    public void set(Object d){data=d;)
    public Object get(){ return data;}
}
加上类型转化语句:
node.set((String)"aaa");
(String)node.get();

3.3、产生桥接方法保护泛型继承中的多态

再泛型类的子类中,当泛型信息被擦除后,父类原先的泛型类型被Object或界限替换,导致子类的方法原型与父类不一致,因此为了保护多态,因此会产生桥接方法。下面看个具体例子

public class Node<T> {

    public T data;

    public Node(T data) { this.data = data; }

    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node<Integer> {
    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

当Node<T>类型被擦除后,Node变为:

public class Node {

    public Object data;

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

    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

此时setData的函数原型与子类不一致了,为了保护多态,编译器会额外产生桥接方法:

class MyNode extends Node {

    // Bridge method generated by the compiler
    //
    public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }

    // ...
}

四、通配符

通配符?用来表示一个未知的类型,注意,用作类型实参!!!

4.1、无界限通配符

单独使用?,比如List<?>,表示可以为任意类型,但是任意类型都可以调用Object的方法,因此只想使用Object的方法时可以传入类型实参?。如果泛型类的方法的实现并不依赖于类型参数,也不必给出具体类型。多用于方法中,注意不要和类型形参混淆了。

public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}

仅使用Object的toString方法。

那List<?>与List<Object>的区别呢?可大了。。List<Object>可以插入Object对象,而List<?>只能插入null,因为?可以代表很多类型,并不知道具体类型,要是随便插入一个出错了怎么办?比如说:List<?> list=new ArrayList<Integer>();list.add("aaaa");因此编译器不允许这种行为,只能插入null,也就是只能用null给泛型参数实例实例对象赋值。

4.2、有上界的通配符

有上界的通配符指定了?所能代表的范围。比如List<? extends Number>中的?可以表示Number及其子类,那么List<Integer>的实例可以赋值给List<? extends Number>的引用,List中的元素可以使用Nubmer中的方法,相当于List<? extends Number>是父类,List<Integer>是子类。

public static double sumOfList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list)
        s += n.doubleValue();
    return s;
}

可以取出,不能存入。

4.3、有下界的通配置

?表示Object到下届的所有类型,比如List<? super Integer>,此时List<? super Integer>是List<Number>的父类,因为,?可以表示Number,因此List<Number>的实例赋值给List<? super Integer>的引用。

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

可以存,但不能取。

五、继承

由于类型擦除的存在,List<Number>和List<Integer>不存在继承的关系,而是同一种类型。但是List<String>、ArrayList<String>、Collection<String>之间存在继承关系:

diagram showing a sample collections hierarchy: ArrayList<String> is a subtype of List<String>, which is a subtype of Collection<String>.

如果有多个类型参数,且继承其他泛型:

interface PayloadList<E,P> extends List<E> {
  void setPayload(int index, P val);
  ...
}

此时有如下继承关系:

diagram showing an example PayLoadList hierarchy: PayloadList<String, String> is a subtype of List<String>, which is a subtype of Collection<String>. At the same level of PayloadList<String,String> is PayloadList<String, Integer> and PayloadList<String, Exceptions>.

为啥List<String>不是ArrayList<Integer>的父类呢?反正类型都擦除了。。。?

废话,逻辑都错误了,编译器肯定不让编译,否则编译器就是傻!

对于有通配符的情况,List<? extends Number>和List<Integer>此时前者是后者的“父类”,且看下图:

diagram showing that List<Integer> is a subtype of both List<? extends Integer> and List<?super Integer>. List<? extends Integer> is a subtype of List<? extends Number> which is a subtype of List<?>. List<Number> is a subtype of List<? super Number> and List>? extends Number>. List<? super Number> is a subtype of List<? super Integer> which is a subtype of List<?>.

如何理解?比如List<? extends Number>和List<Integer>,此时?表示Number及其子类,并且List中的元素通过Number接口来操作元素的,自然能够操作Integer,因此可以将后者赋值给前者,存在一个继承关系。

super又如何理解?比如List<? super Integer>和List<? super Number>,?表示Object到Integer的类型,包含Object到Number的类型,因此List<? super Number>可以赋值给List<? super Integer>的引用,存在一个继承关系。

六、通配符使用准则

举例List<E>来说。

  • 如果List<? extends Number>,那么List中元素可以当作Number或者Object访问,不能存入Number或者Object,类似于只读,但可以插入null。
  • 如果List<? super Number>,那么可以存入Number元素到List<? super Number>,类似于只写
  • 如果同时想要读和写,那么不要使用通配符。

对于第一点可以这么理解,List<? extends Number>是类型实参为Number子类的List的父类,那么List<? extends Integer>可以赋值给List<? etends Number>的引用。而List<? extends Number>没有足够的信息确定元素具体为什么类型,如果新增个Number或Double的元素,就会报错,因此编译器不允许写的存在。

对于第二点,List<? super Number>的?可以表示Number及其父类,那么List存入Number是可以的,因为Number是?本身或子类,因此可写。

七、泛型的限制

1、不能通过基本类型实例化泛型

Pair<int, char> p = new Pair<>(8, 'a');  // compile-time error

2、不能创建类型参数的参数

public static <E> void append(List<E> list) {
    E elem = new E();  // compile-time error
    list.add(elem);
}

但可以通过反射创建

public static <E> void append(List<E> list, Class<E> cls) throws Exception {
    E elem = cls.newInstance();   // OK
    list.add(elem);
}

3、静态字段的类型不能是类型参数

对于参数化类型不能使用cast和instanceof

public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<Integer>) {  // compile-time error
        // ...
    }
}
List<Integer> li = new ArrayList<>();
List<Number>  ln = (List<Number>) li;  // compile-time error

4、不能创建参数化类型的数组

List<Integer>[] arrayOfLists = new List<Integer>[2];  // compile-time error

5、不能重载方法,此方法类型被擦除后原型一致

public class Example {
    public void print(Set<String> strSet) { }
    public void print(Set<Integer> intSet) { }
}

6、不能创建、catch、throw参数化类型的方法,可以throws类型参数

术语:

Type Parameter类型形参:定义时的类型类型参数
Type Argument类型实参:传入的类型参数
parameterized type参数化类型,传入了类型实参的类型,比如List<String>。
generic type invocation:传递类型到泛型类、泛型接口、泛型方法的过程

参考

https://docs.oracle.com/javase/tutorial/java/generics/index.html

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值