Java泛型

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。泛型的意义在于代码的复用。Java泛型使用擦除原理来实现,所以在使用泛型时,任何具体的类型信息都已被擦除。
一些常用的泛型类型变量:

E:元素(Element),多用于容器场景
K:关键字(Key)
N:数字(Number)
T:类型(Type)
V:值(Value)
X: 异常(Exception)

泛型类/泛型接口

泛型类定义示例如下:

public class GenericClass<T> {
    private T data;
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}

泛型接口定义示例如下:

public interface GenericIntercace<T> {
    T getData();
}

泛型方法

泛型方法使得该方法能够独立于类而产生变化,以下是一个基本的指导原则:无论何时,只要能够做到,就应尽量使用泛型方法。也就是说,如果可以使用泛型方法解决问题,就不应对整个类泛型化;如果可以使用泛型方法解决问题,就应使用泛型方法。
要定义泛型方法,只需将泛型参数列表置于返回值之前。示例如下:

public class GenericMethods {
    public <T> T add(T a, T b) {
        return a+b;
    }
    
    public static void main(String[] args) {
        GenericMethods genericMethods = new GenericMethods();
        genericMethods.add(1, 2);
        genericMethods.add("a", "b");
    }
}

泛型方法与可变参数

泛型方法与可变采纳数列表能够很好地共存:

public class GenericVarargs {
    public <T> List<T> toList(T... args) {
        List<T> result = new LinkedList<>();
        for (T arg: args) {
            result.add(arg);
        }
        return result;
    }

    public void main(String[] args) {
        GenericVarargs genericVarargs = new GenericVarargs();
        genericVarargs.toList(new int[]{1, 2, 3});
    }
}

泛型中限定符

Java泛型引入?作为“无界通配符”,使用无界通配符,等价于使用原生类型(Object)。<?>可以被认为是一种装饰,它是在声明:“这里在使用Java泛型,这里并不是原生类型,但在当前这种情况下,泛型参数可以持有任何类型”。

边界

边界可以在泛型的参数类型上设置限制条件。因为擦除移除了类型信息,所以可以使用无界泛型参数调用的方法只是那些可以用Object调用的方法。而如果能将这个参数限制在特定的类型范围,那么就可以用范围内的特性类型去调用这个方法。为了执行这种限制,Java泛型重用了extends关键字和super关键字。extends 指定了泛型类型的上界,super 指定了泛型类型的下界。这里上界是指父类,下界是指子类。
在介绍extends和super的使用前,先定义如下类型:

public class Parent {}
public class Child extends Parent {}
public class Son extends Child {}
public class Daughter extends Child {}

示例如下:

public void testLimit() {
    // 入参,不可以是Child的父类
    // test(Arrays.asList(new Parent[]{new Parent()}));
    // 入参,可以是Child
    test(Arrays.asList(new Child[]{new Child()}));
    // 入参,可以是Child的子类
    test(Arrays.asList(new Son[]{new Son()}));
}

public List<? super Child> test(List<? extends Child> elements) {
    // 返回值,可以是Child的父类
    // List<Parent> result = new LinkedList<>();
    // 返回值,可以是 Child
    List<Child> result = new LinkedList<>();
    // 返回值,不可以是Child的子类
    // List<Son> result = new LinkedList<>();
    if (elements == null || elements.size() <= 0) {
        return result;
    }
    elements.stream().forEach(element -> {
        result.add(element);
    });
    return result;
}

什么时候使用extends,什么时候使用super。《Effective Java》给出精炼的描述:producer-extends, consumer-super(PECS) 简单来说,从数据流来看,extends是限制数据来源(生产者),而super是限制数据流入(消费者)。

捕获转换

有一种情况特别需要使用<?>而不是原生类型。如果向一个使用<?>的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,使得这个方法可以回调并调用另一个使用这个确切类型的方法。也即"捕获转换"。(未指定的通配符类型被捕获,并被转换为确切类型)

任何基本类型都不能作为类型参数

严格意义上来说,Java并非完全面向对象语言。之所以这么说,一方面是因为在Java语言中,基本类型并非继承自Object。因为基本类型无法擦除(一旦参数,将无法还原),所以任何基本类型都不能作为类型参数。
针对基本类型的这个问题,可以使用基本类型的包装器类及 Java SE5 的自动包装机制。注意,自动包装机制仅能解决一部分问题(如自动包装机制不能应用于数组),且可能会带来性能问题。

异常中使用泛型

由于擦除的原因,将泛型应用应用于异常是非常受限的。catch语句不能捕获泛型类型的异常,因为在编译期和运行时都必须知道确切的异常类型。同样地,泛型类也不能直接或间接继承自Throwable。
但是,类型参数可以会在一个方法的throws子句中用到。这使得可以编写随检查型异常的类型而发生变化的泛型代码:

public interface IGeneric<E extends Exception> {
    void testGenericException() throws E;
}

public class GenericDemo implements IGeneric<RuntimeException> {
    @Override
    public void testGenericException() throws RuntimeException {
        throw new RuntimeException();
    }
}

擦除

Java泛型是使用擦除来实现的,这意味着在使用泛型时,任何具体的类型信息都会被擦除,唯一知道的就是此时在使用一个对象。擦除是导致泛型无法用于显式地引用运行时类型的操作。(擦除方式移除了类型信息)

迁移兼容性

为了减少潜在的关于擦除的混淆,必须清楚的认识到,擦除不是一个语言特性。擦除是Java的泛型实现中的一种折中,因为泛型不是Java语言出现时就有的组成部分,所以这种折中是必需的。
如果泛型在Java 1.0中就已经是其一部分,那么这个特性将不会使用擦除来实现———它将使用具体化,使类型参数保持为第一类实体,这样就可在类型参数上执行基于类型的语言操作和反射操作。而Java中擦除减少了泛型的泛化性。因为使用擦除实现泛型,所以泛型在Java中并不如原本设想的那么有用,但仍然有用。
在基于擦除的实现中,泛型类型是被当作第二类型处理,即不能在某些重要的上下文环境中使用的类型。泛型类型只有在静态类型期间才出现。在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。如List这样的类型将被擦除为List,而普通的类型变量在未指定边界时,将被擦除为Object。
擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用,这经常被称为“迁移兼容性”。
Java泛型不仅必须保持兼容性,即现有的代码和类文件仍然合法,并且继续保持其之前的含义,而且还要支持迁移兼容性,使得类库按照它们自己的步调变为泛型的,并且当某个类库变为泛型时,不会破坏依赖于它的代码和程序。在决定这个目标之后,擦除是唯一可行的解决方案。
例如,某个程序中有两个类库X和Y,并且Y还要使用类库Z。当使用Java 5后,为了实现迁移兼容性,每个类库和应用程序都必须与其他所有的部分是否使用了泛型无关。因此,某个特定的类库使用了泛型这样的证据必须被“擦除”。

擦除的问题

引入擦除的主要原因是,实现非泛化代码到泛化代码的转变,以及在不破坏现有类库的情况下,将泛型融入Java语言。擦除使得现有的非泛型客户端能够在不改变的情况下继续使用。这是一个崇高的动机,因为它不会突然间破坏所有现有的代码。
擦除的代码是显著的。Java泛型不能用于显式地引用运行时类型的操作之中,如转型、instanceof操作和new表达式

擦除的补偿

擦除丢失了在泛型代码中执行某些操作的能力。任何在运行时需要知道确切类型信息的操作将无法工作。针对这种情况,可以通过引入类型标签来对擦除进行补偿。这意味着,必须显式地传递类型的Class对象,以便在类型表达式中使用它。示例如下:

class Building {}
class House extends Building {}
public class ClassTypeCapture<T> {
    Class<T> kind;
    
    public ClassTypeCapture(Class<T> kind) {
        this.kind = kind;
    }
    
    public boolean isKindOf(Object arg) {
        return kind.isInstance(arg);
    }

    public static void main(String[] args) {
        ClassTypeCapture<Building> clssTypeCapture = new ClassTypeCapture<Building>(Building.class);
        clssTypeCapture.isKindOf(new Building()); // true
        clssTypeCapture.isKindOf(new House()); // true
    }
}

自限定类型

这里不展开讨论自限定类型。有兴趣的同学,可以参考《Java编程思想》一书(第十五章)。

总结

在可以使用泛型的地方,尽量使用泛型,从而实现代码的复用。在多线程场景,要谨慎使用泛型。

参考

https://www.runoob.com/java/java-generics.html Java泛型
https://www.jianshu.com/p/986f732ed2f1 Java泛型详解
《Java编程思想》(第四版) Bruce Eckel [译]陈昊鹏 P352-P432
https://www.cnblogs.com/wangbaicheng1477865665/p/OutIn.html 逆变和协变
https://www.jianshu.com/p/2bf15c5265c5 Java泛型–协变与逆变

原创不易,如果本文对您有帮助,欢迎关注我,谢谢 ~_~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值