资讯专栏INFORMATION COLUMN

【从基础学 Java】泛型

huhud / 293人阅读

摘要:泛型方法泛型类中可以定义静态非静态的泛型方法。上述泛型类会被替换成下面形式一般使用第一个限定类型替换变为原始类型,没有限定类型,使用替换。

引言

在面向对象的世界里,我们如果需要一个容器来盛装对象。举个例子:一个篮子。我们可以用这个篮子装苹果,也可以用这个篮子装香蕉。基于 OOP 的思想,我们不希望为苹果和香蕉分别创建不同的篮子;同时,我们希望放进篮子里的是苹果,拿出来的还是苹果。于是,Java 程序员提出了「泛型」的概念——一种类似于 C++ 模板的技术。

早期程序员使用如下代码创建一个泛型集合:

public class ArrayList{
    private Object[] elementData;
    ...
    public Object get(int i);
    public void add(Object o);
}

我们可以看出,对与这个集合而言,取出 (get) 和放入时都没有进行类型检查。因此,如果我们不记得放入的顺序,把取出的对象进项强制类型转换,很可能出现 ClassCastException。因此,真正的泛型是可以在编译时期,对数据类型进行检查,保证安全。如下面代码所示:

ArrayList list = new ArrayList<>();

P.S. <>里的String叫做类型参数。

使用泛型,为我们提供了如下优点:

更强大的编译时期的类型检查

避免不必要的类型转换,如:

List list = new ArrayList<>(3);
String str = list.get(0);

让程序能够实现通用的算法

泛型类

泛型中使用名为泛型参数表明可以传入类或方法的对象类型,这种设计实现了类型参数化(可以把同一类型的类作为参数进行传递),如下面的代码所示:

泛型类示例

public class Pair{
    private T first;
    private T last;
    
    public Pair(){}
    public Pair(T first, T last){
        this.first = frist;
        this.last = last;
    }
    
    public T getFirst();
    public T getLast();
}

泛型方法示例

public class Util{
    // 简单的泛型方法
    public static  T getMiddle(T...a){
        return a[a.length/2];
    }
    // 带限定符的泛型方法,如果有多个限定符,使用 & 连接多个接口或超类
    public static  T min(T...a){
        // 具体实现
    }
}

注意,这里的泛型参数(type parameter)在上述示例中指的是用大写字母 T 表示的值,而泛型实参(type argument)则是指 T a 中的 a。根据惯例,泛型参数通常如下命名:

E:表示一个元素,Java 集合框架中使用最多

K:键

N:数字

T:类型

V:值

S,U,V:其它类型

原始类型(raw type)

原始类型指的是,不包括泛型参数的类型,如上述泛型类中的 Pair。我们可以通过原生类型构造对象:

Pair pair = new Pair();

同时,可以通过泛型参数构造对象:

Pair pair = new Pair<>();

但是,如果把一个通过原生类型获取的对象指向一个通过泛型参数生成的参数会报 unchecked warning,如下面的代码:

Pair pair = new Pair();
Pair pair1 = pair;
继承和子类型

在 Java 中,有继承的概念,简而言之,就是一个类型可以指向它的兼容类型,如:

Object object = new Object();
Integer integer = new Integer(20);
object = integer;

上述代码表示:Integer IS-A Object。这种概念在泛型中也适用。如下定义:

public class Box{
    public void add(T t);
}

那么一个 Box 的对象可以增加任意 Number 子类的值。但是 BoxBox 不是同一个类型。

泛型方法

泛型类中可以定义静态、非静态的泛型方法。泛型方法的语法为:<泛型参数类型列表> + 返回类型 + 泛型参数列表。

静态方法

public static  void foo(T t){
}

非静态方法

public void foo(T t){
}
类型限定

在某种情况下,我们希望方法只接受特定类型的参数,可以使用如下语法实现:

public  void inspect(U u){
    // 这里是逻辑处理
}

上述代码中,该泛型方法只接受为 Number 类型的参数。同样,也可以在泛型类上加以限制:

public class Utils{
    // 这里的 T 必须为 Number 类型
    private T t;
}

当然,也可以使用多重限制,如下面代码所示:

public class Utils{

}

P.S. 限制中的类必须放在接口的前面。

类型推断

类型推断是:编译器去推断调用方法的参数的类型的能力。
如,泛型方法中:

public  void addBox(Box box){
    // 这里是处理代码
}

不必通过 obj.addBox(box) 调用, 可以省略。

构造方法中:

// 类型推断
Map> map = new HashMap<>();

其中,构造方法中的泛型还可以这样用:

// 定义泛型类
public class Box{
    public  Box(T t){
    
    }
}
// 实例化一个对象
public class Application{
    void method(){
        Box box = new Box<>(");
    }
}
通配符

通配符 ? 表示一个未知的类型,可用于参数的类型、字段以及局部变量中,但不可用于调用泛型方法里的类型参数、泛型对象实例化以及泛型超类里。

// 可以
public void foo(Pair pair){
    // 可以
    Pair foo;
}
// 可以
private Pair pair; 
上界通配符

上界通配符表明需要最高限定的类型,下面的代码用来计算所有类型为数字的集合的总和:

public double sumList(List){
    // 这里做逻辑处理
}
无界限通配符

使用无界限通配符表示不确定的类型,以下两种情况可以使用无界限通配符:

当方法的参数可以用 Object 对象替换

方法的实现不依赖具体的类型

比如,有一个打印集合对象的方法:

// 定义一个打印集合对象列表的方法
public void printList(List list){
    for(Object obj: list){
        // 打印list
    }
}
// 调用方法
List integers = Arrays.asList(1,2,3);
List strings = Arrays.asList("A","B","C");
printList(integers);
printList(strings);

P.S. ListList 不同,List 只能插入 nullList 可以插入任何对象。

下界通配符

使用下界统配符,表明最低限度的类型,如:

public double sumList(List){
    // 这里做逻辑处理
}
通配符和子类型

在本文的继承和子类里,提到过:Box 不是 Box 的子类。在 Java 泛型中,继承关系可以通过如下图表示:

可以看出,泛型中的 extends 的确限定了上界(父类);super 的确限定了下界(子类型);? 是所有泛型的超类(类似 Object)。

泛型的继承关系(父子类型关系)可以通过下面的韦恩图解释:

我们不妨用某一泛型所占的面积表示其层次关系,面积大的在继承关系上层次高。由上图很容易看出: 的继承层次比 的继承层次高;相应地, 的继承层次比 的继承层次低。

使用泛型的场景

调用一个方法:foo(src, dest);src 看做入参,dest 看做出参,基于以下规则决定是否使用和如何使用泛型:

入参使用上界通配符:extends

出参使用下界通配符:super

入参可以用 Object 代替的,使用无边通配符

需要获取入参和出参的变量,不要使用通配符

这种原则也叫做 PECS(Producer Extends Consumer Super) 原则。

类型擦除
类型擦除确保被参数化的类型不会创建新的类,不会产生运行时的开销。

泛型擦除时,编译器做了一点小小的工作:如果该泛型参数有边界限制,替换成它的边界;否则,用 Object 替换。
上述泛型类 Pair 会被替换成下面形式:

class Pair{
    Object first;
    Object last;
    public Object getFirst(){}
    public Object getLast(){}
}

P.S. 一般使用第一个限定类型替换变为原始类型,没有限定类型,使用 Object 替换。

桥接方法

当子类继承(或实现)父类(或接口)的泛型方法时,在子类中指明了具体的类型。编译器会自动构建桥接方法(bridge method)。如:

class Node{
    private T t;
    public Node(T t){
        setT(t);
    }
    public void setT(T t){
        this.t = t;
    }
}

class MyNode extends Node{
    public MyNode(Integer i){
        super(i);
    }
    public void setT(Integer i){
        super.setT(i);
    }
}

在上述代码中,编译时期,由于泛型擦除,Node 中的方法为 setT(Object t) 而 MyNode 中的方法为 setT(Inetger i) 。签名不匹配,不再是重写,因此,编译器为 MyNode 生成如下桥接方法:

// 桥接方法
public void setT(Object i){
    setData((Integer)i);
}

public void setT(Integer i){
    super.setData(i);
}
非具体化类型 非具体化类型定义

具体类型(Reifiable Type)指的是:原始数据类型、非泛型类型、原生类型和调用不受限的通配等在运行时期,信息不会丢失的类型。
非具体类型(Non-Reifiable Type)在运行时期不能获取其所有的信息,如 JVM 无法区别 ListList 。因此,这种类型不能使用类似 instanceof 的方法。

堆污染

堆污染指的是:一个参数化类型指向一个非该参数化类型对象的过程。通常是,在程序中进行了一些操作,使编译时期发生未检查(unchecked)警告时发生。如:混用原始类型(Raw Type)和参数化类型。

使用非具体化类型做可变参数的潜在缺陷

当使用可变参数作为泛型输入参数时,会造成堆污染。如:
可以通过如下注解消除编译时期的警告:

@SafeVarargs

@SuppressWarnings({"unchecked", "varargs"})

泛型的限制

虽然泛型是如此的便利,但不免有缺点:

不能用基本类型实例化类型参数

// 编译出错
List array = new ArrayList<>();

不能通过类型参数实例化对象

public static  void foo(List list){
    // 编译出错
    E element = new E();
    list.add(element);
}

不能创建泛型变量类型的静态字段

public class Foo{
    // 编译出错
    private static T field;
}

不能使用 instanceof 来确认参数类型

public static  void foo(List list){
    // 编译出错
    if(list instanceof ArrayList){
    }
}

不能创建参数化类型数组

// 编译出错
List[] strings = new ArrayList<>[2];

不能抛出或捕获泛型类实例

// 编译出错
public class FooException extends Exception{
}

不能重载擦除后有同样方法签名的方法

public class Example{
    // 编译出错
    public void print(Set string){
    }
    public void print(Set integer){
    }
}

运行时类型查询只适用于原始类型

Varargs 警告

泛型类的静态上下文的类型变量无效

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/71063.html

相关文章

  • Java入门基础知识点总结(详细篇)

    摘要:深入理解数据库管理系统通用知识及数据库的使用与管理。为后台开发打下坚实基础。项目文档,项目规范,需求分析,数据库设计,工程构建,需求评审,配置管理,修复,项目管理等。 很多新手在学习java的时候都比较迷茫,不知道从哪里开始学起,这里就给大家整理了一份java开发学习路线,比较系统全面,可参...

    shinezejian 评论0 收藏0
  • 基础 Java】序

    摘要:本人生性愚钝,在大学期间没能好好领略等面向对象编程的魅力。现借助一些较为权威的书籍资料,将基础知识里比较重要的东西整理成文,命名从基础学。如果博文不慎侵犯了您的著作权,请联系我。 和很多大学一样,我的学校也是从 Java 、C++ 入手,教给我们面向对象 (OOP) 的思想。本人生性愚钝,在大学期间没能好好领略 Java 等面向对象编程的魅力。现借助一些较为权威的书籍资料,将 Java...

    JackJiang 评论0 收藏0
  • 作为我的的第一门语言,Java时是什么感受?

    摘要:作为技术书籍或者视频,讲解一门语言的时候都是从最底层开始讲解,底层的基础有哪些呢首先是整个,让我们对这门语言先混个脸熟,知道程序的基本结构,顺带着还会说一下注释是什么样子。 2018年新年刚过,就迷茫了,Java学不下去了,不知道从哪里学了。 那么多细节的东西,我根本记不住,看完就忘。 刚开始学习的时候热情万丈,持续不了几天就慢慢退去。 作为技术书籍或者视频,讲解一门语言的时候都是...

    isaced 评论0 收藏0
  • java篇 - 收藏集 - 掘金

    摘要:进阶多线程开发关键技术后端掘金原创文章,转载请务必将下面这段话置于文章开头处保留超链接。关于中间件入门教程后端掘金前言中间件 Java 开发人员最常犯的 10 个错误 - 后端 - 掘金一 、把数组转成ArrayList 为了将数组转换为ArrayList,开发者经常... Java 9 中的 9 个新特性 - 后端 - 掘金Java 8 发布三年多之后,即将快到2017年7月下一个版...

    OpenDigg 评论0 收藏0
  • Java字节码修改神器HiBeaver:黑掉你的SDK

    摘要:下面我们正式开始尝试小米推送,首先,找出其业务逻辑中的一个节点。因为小米推送是商业产品,这里不便于探索太多内容,但是通过这个插件可以比较方便的进行类似的研究。 前言 有时候我们在Java开发过程中可能有这样的需求:需要研究或者修改工程依赖的Jar包中的一些逻辑,查看代码运行中Jar包代码内部的取值情况(比如了解SDK与其服务器通信的请求报文加密前的情况)。 这个需求类似于Hook。 但...

    voidking 评论0 收藏0

发表评论

0条评论

huhud

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<