Java的泛型

这篇具有很好参考价值的文章主要介绍了Java的泛型。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

泛型是我们需要的程序设计手段。使用泛型机制编写的程序代码要比那些杂乱地使用 Object 变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。

至少在表面上看来,泛型很像 C++ 中的模板。与 Java —样,在 C++ 中,模板也是最先被添加到语言中支持强类型集合的。但是,多年之后人们发现模板还有其他的用武之地。学习完本章的内容可以发现 Java 中的泛型在程序中也有新的用途。

为什么要使用泛型程序设计

泛型程序设计(Generic programming)意味着编写的代码可以被很多不同类型的对象所重用。例如,我们并不希望为聚集 String 和 File 对象分别设计不同的类。实际上,也不需要这样做,因为一个 ArrayList 类可以聚集任何类型的对象。这是一个泛型程序设计的实例。

实际上,在 Java 增加泛型类之前已经有一个 ArrayList 类。下面来研究泛型程序设计的机制是如何演变的,另外还会讲解这对于用户和实现者来说意味着什么。

类型参数的好处

在 Java 中增加范型类之前,泛型程序设计是用继承实现的。实现时使用通用类型(如 Object 或 Comparable 接口),在使用时进行强制类型转换。

泛型对于集合类尤其有用,ArrayList 就是一个无处不在的集合类。ArrayList 类维护一个 Object 类型的数组(Object 类是所有类的父类):

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

这种方法有两个问题。

  • 当获取一个值时,必须进行强制类型转换。
  • 此外,这里没有错误检査。可以向数组列表中添加任何类的对象。对于 files.add(new File("..."); 这个调用,编译和运行都不会出错。然而在其他地方,如果将 get() 的结果强制类型转换为 String 类型, 就会产生一个错误。
ArrayList files = new ArrayList();
String filename = (String) files.get(0);

泛型提供了一个更好的解决方案:类型参数(type parameters)。ArrayList 类有一个类型参数用来指示元素的类型:ArrayList<String> files = new ArrayList<String>();这使得代码具有更好的可读性。人们一看就知道这个数组列表中包含的是 String 对象。

在 Java7 及以后的版本中,构造函数中可以省略泛型类型:ArrayList<String> files = new ArrayList<>();省略的类型可以从变量的类型推断得出。

编译器也可以很好地利用这个信息。当调用 get() 方法的时候,不需要进行强制类型转换,编译器就知道返回值类型为 String,而不是 Object:String filename = files.get(0);

编译器还知道 ArrayList 中 add() 方法有一个类型为 String 的参数。现在, 编译器可以进行检査,避免插入错误类型的对象。例如下面的代码是无法通过编译的。这将比使用 Object 类型的参数安全一些,出现编译错误比类在运行时出现类的强制类型转换异常要好得多。

files,add(new File("...")); // can only add String objects to an ArrayList<String>

类型参数的魅力在于:使得程序具有更好的可读性和安全性。

谁想成为泛型程序员

使用像 ArrayList 这样的泛型类很容易。大多数 Java 程序员都使用 ArrayList 这样的类型,就好像它们已经构建在语言之中,像 String[] 数组一样。(当然, 数组列表比数组要好一些,因为数组列表可以自动扩容。)

但是,实现一个泛型类并没有那么容易。对于类型参数,使用这段代码的程序员可能想要内置(plugin)所有的类。他们希望在没有过多的限制以及混乱的错误消息的状态下,做所有的事情。因此,一个泛型程序员的任务就是预测出所用类的未来可能有的所有用途。

这一任务难到什么程度呢?下面是标准类库的设计者们肯定产生争议的一个典型问题。ArrayList 类的 addAll() 方法用来添加另一个集合的全部元素。程序员可能想要将 ArrayList 中的所有元素添加到 ArrayList 中去(Manager 类是 Employee 类的子类)。然而,反过来就不行了。如何设计才能只允许前一个调用,而不允许后一个调用呢?Java 语言的设计者发明了一个具有独创性的新概念,通配符类型(wildcard type),它解决了这个问题。通配符类型非常抽象,然而,它们能让库的构建者编写出尽可能灵活的方法。


泛型程序设计划分为 3 个能力级别。基本级别是,仅仅使用泛型类:典型的是像 ArrayList 这样的集合,不必考虑它们的工作方式与原因。大多数应用程序员将会停留在这一级别上,直到出现了什么问题。当把不同的泛型类混合在一起时,或是在与对类型参数一无所知的遗留的代码进行衔接时,可能会看到含混不清的错误消息。如果这样的话,就需要系统地学习 Java 泛型来解决这些问题,而不要胡乱地猜测。当然,最终可能想要实现自己的泛型类与泛型方法。

应用程序员很可能不喜欢编写太多的泛型代码。JDK 开发人员已经做出了很大的努力,为所有的集合类提供了类型参数。凭经验来说,那些原本涉及许多来自通用类型(如 Object 或 Comparable 接口)的强制类型转换的代码一定会因使用类型参数而受益。

本章介绍实现自己的泛型代码需要了解的各种知识。希望大多数读者可以利用这些知识解决一些疑难问题,并满足对于参数化集合类的内部工作方式的好奇心。

泛型类

泛型类(generic class)就是具有一个或多个类型参数的类。

本章使用一个简单的 Pair 类作为例子。

public class Pair<T> {
    private T first;
    private T second;
    
    public Pair() { first = null ; second = null ; }
    public Pair(T first, T second) { this.first = first; this.second = second; }
    
    public T getFirst() { return first; }
    public T getSecond() { return second; }
    
    public void setFirst(T newValue) { first = newValue; }
    public void setSecond(T newValue) { second = newValue; }
}

Pair 类引入了一个类型参数 T,用尖括号(< >)括起来,并放在类名的后面。

泛型类可以有多个类型参数。如果有多个类型变量,多个类型变量之间用 “,”逗号分隔。例如,可以定义 Pair 类,其中第一个域和第二个域使用不同的类型:public class Pair<T, U> { ... }

类定义中的类型参数指定方法的返回类型以及域和局部变量的类型。例如,private T first;


用具体的类型替换类型参数就可以实例化泛型类型,例如:Pair 可以将结果想象成带有构造器的普通类:

  • Pair()
  • Pair(String, String)

和方法:

  • String getFirst()、String getSecond()
  • void setFirst(String)、void setSecond(String)

换句话说,泛型类可看作普通类的工厂。

泛型方法

前面已经介绍了如何定义一个泛型类。实际上,还可以定义一个带有类型参数的简单方法。

class ArrayAlg {
    public static <T> T getMiddle(T... a) {
        return a[a.length / 2];
    }
}

这个方法是在普通类中定义的,而不是在泛型类中定义的。然而,这是一个泛型方法,可以从尖括号和类型参数看出这一点。注意,类型参数放在修饰符(这里是 public static)的后面,返回类型的前面。


泛型方法可以定义在普通类中,也可以定义在泛型类中。

当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:

String middle = ArrayAlg.<String>getMiddle("]ohn", "Q.", "Public");

在这种情况(实际也是大多数情况)下,方法调用中可以省略 类型参数。编译器有足够的信息能够推断出所调用的方法。它用 names 的类型(即 String[])与泛型类型 T[] 进行匹配并推断出 T 一定是 String。也就是说,可以调用

String middle = ArrayAlg.getMiddle("]ohn", "Q.", "Public");

类型参数的限定

有时,类或方法需要对类型参数加以约束。下面是一个典型的例子。我们要计算数组中的最小元素:

class ArrayAIg {
    // almost correct
    public static <T> T min(T[] a) {
        if (a == null || a.length == 0) {
            return null;
        }
        T smallest = a[0];
        for (int i = 1; i < a.length; i++) {
            if (smallest.compareTo(a[i]) > 0) smallest = a[i];
        }
        return smallest;
    }
}

但是,这里有一个问题。请看一下 min() 方法的代码内部。变量 smallest 类型为 T,这意味着它可以是任何一个类的对象。怎么才能确信 T 所属的类有 compareTo() 方法呢?

解决这个问题的方案是将 T 限制为实现了 Comparable 接口(只含一个 compareTo() 方法的标准接口)的类。可以通过对类型参数 T 设置限定(bound)实现这一点:

public static <T extends Comparable> T min(T[] a) {}

现在,泛型的 min() 方法只能被实现了 Comparable 接口的类(如 String、LocalDate 等)的数组调用。

T extends 绑定类型表示 T 应该是绑定类型的子类型(subtype)。T 和绑定类型可以是类,也可以是接口。


一个类型参数或通配符可以有多个限定,多个限定之间用 “ &” 分隔,例如:T extends Comparable & Serializable。在Java的限定中,可以根据需要拥有多个接口限定,但至多有一个类限定。如果用一个类作为限定,它必须放在限定列表中的第一个位置。

// ok
T extends Object & Comparable & Serializable
// error
T extends Comparable & Serializable & Object

泛型代码和虚拟机

类型擦除

类型擦除是Java泛型实现的一种方式。

类型擦除指的是:在编译时,将泛型类型擦除成其原始类型。

虚拟机没有泛型类型对象,所有对象都属于普通类。无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。类型参数用第一个限定的类型来替换,如果没有给定限定就用 Object 替换。例如:

  • 类 Pair 中的类型参数没有显式的限定,因此,原始类型用 Object 替换 T。
  • 类 Interval<T extends Comparable & Serializable> 中第一个限定的类型为 Comparable,因此,原始类型用 Comparable 替换 T。

Pair 的原始类型如下所示。结果是一个普通的类,就好像泛型引入 Java 语言之前已经实现的那样。在程序中可以包含不同类型的 Pair,例如,Pair 或 Pair。而擦除类型后就变成原始的 Pair 类型了。

public class Pair {
    private Object first;
    private Object second;
    
    public Pair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }
    
    public Object getFirst() { return first; }
    public Object getSecond() { return second; }
    
    public void setFirst(Object newValue) { first = newValue; }
    public void setSecond(Object newValue) { second = newValue; }
}

翻译泛型表达式

当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。例如,下面这个语句序列:

Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();

擦除 getFirst() 方法的返回类型后,getFirst() 方法将返回 Object 类型。编译器自动插入 Employee 的强制类型转换。也就是说,编译器把这个方法调用翻译为两条虚拟机指令:

  • 对 Pair#getFirst() 原始方法的调用。
  • 将返回的 Object 类型强制转换为 Employee 类型。

当存取一个泛型域时也要插入强制类型转换。假设 Pair 类的 first 域和 second 域都是公有的,表达式:Employee buddy = buddies.first;也会在结果字节码中插入强制类型转换。

翻译泛型方法

类型擦除也会出现在泛型方法中。程序员通常认为下述的泛型方法 public static <T extends Comparable> T min(T[] a)是一个完整的方法族,而擦除类型之后,只剩下一个方法:public static Comparable min(Comparable[] a)

注意,类型参数 T 已经被擦除了,只留下了限定类型 Comparable。


类型擦除带来了两个复杂问题。看一看下面这个示例:

class DateInterval extends Pair<LocalDate> {
    public void setSecond(LocalDate second) {
        if (second.compareTo(getFirst()) >= 0) {
            super.setSecond(second);
        }
    }
    ...
}

一个日期区间是一对 LocalDate 对象,并且需要覆盖这个方法来确保第二个值永远不小于第一个值。这个类擦除后变成

// after erasure
class DateInterval extends Pair {
    public void setSecond(LocalDate second) { ... }
    ...
}

令人感到奇怪的是,存在另一个从 Pair 继承的 setSecond() 方法,即 public void setSecond(Object second)这显然是一个不同的方法,因为它有一个不同类型的参数 Object,而不是 LocalDate。然而,不应该不一样。考虑下面的语句序列:

DateInterval interval = new DateInterval(...);
Pair<LocalDate> pair = interval; // OK assignment to superclass
pair.setSecond(aDate);

这里,希望对 setSecond() 的调用具有多态性,并调用最合适的那个方法。由于 pair 引用 Datelnterval 对象,所以应该调用 Datelnterval.setSecond()。问题在于类型擦除与多态发生了冲突。要解决这个问题,就需要编译器在 Datelnterval 类中生成一个桥方法(bridge method):

public void setSecond(Object second) { setSecond((Date) second); }

有关泛型的事实

需要记住有关 Java 泛型转换的事实:

  • 虚拟机中没有泛型,只有普通的类和方法。
  • 所有的类型参数都用它们的限定类型替换。
  • 桥方法被合成来保持多态。
  • 为保持类型安全性,必要时插入强制类型转换。

类 A 是类 B 的子类,但是 G 和 G 不具有继承关系,二者是并列关系。

public static void printBuddies(Pair<Employee> p) { ... }

// Manager 类是 Employee 类的子类
Pair<Manager> pair = new Pair<>();
// error(固定的泛型类型系统的局限,通配符类型解决了这个问题)
printBuddies(pair);

泛型一般有三种使用方式:泛型类、泛型方法、泛型接口。

// 泛型类
public class Pair<T>
// 实例化泛型类
Pair<String> pair = new Pair<>();
// 继承泛型类,指定类型
class DateInterval extends Pair<LocalDate>

// 泛型方法
public static <T> T getMiddle(T... a)

// 泛型接口
public interface Generator<T>
// 实现泛型接口,指定类型
class GeneratorImpl implements Generator<String>

通配符类型

固定的泛型类型系统使用起来并没有那么令人愉快,类型系统的研究人员知道这一点已经有一段时间了。Java 的设计者发明了一种巧妙的(仍然是安全的)“解决方案”:通配符类型。下面几小节会介绍如何处理通配符。

通配符概念

通配符类型中,允许类型参数变化。例如,通配符类型 Pair<? extends Employee> 表示任何泛型 Pair 类型,它的类型参数是 Employee 的子类,如 Pair,但不是 Pair

假设要编写一个打印雇员对的方法,像这样:

public static void printBuddies(Pair<Employee> p) {
    Employee first = p.getFirst();
    Employee second = p.getSecond();
    System.out.println(first.getName() + " and " + second.getName() + " are buddies.");
}

正如前面讲到的,不能将 Pair 传递给这个方法,这一点很受限制。解决的方法很简单:使用通配符类型:

public static void printBuddies(Pair<? extends Employee> p)

类型 Pair 是 Pair<? extends Employee> 的子类型(如图 8-3 所示)。

Java的泛型


使用通配符会通过 Pair<? extends Employee> 的引用破坏 Pair 吗?

Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair<? extends Employee> wildcardBuddies = managerBuddies; // OK
wildcardBuddies.setFirst(lowlyEmployee); // compile-time error

这可能不会引起破坏。对 setFirst() 的调用有一个类型错误。要了解其中的缘由,请仔细看一看类型 Pair<? extends Employee>。其方法似乎是这样的:

? extends Employee getFirst()
void setFirst(? extends Employee)

这样将不可能调用 setFirst() 方法。编译器只知道需要某个 Employee 的子类型,但不知道具体是什么类型。它拒绝传递任何特定的类型。毕竟 ? 不能用来匹配。

使用 getFirst() 就不存在这个问题:将 getFirst() 的返回值赋给一个 Employee 的引用完全合法。这就是引入有限定的通配符的关键之处。现在已经有办法区分安全的访问器方法和不安全的更改器方法了。

通配符的超类型限定

通配符限定与类型参数限定十分类似,但是,通配符限定还有一个附加的能力,即可以指定一个超类型限定(supertype bound),如下所示:? super Manager。

这个通配符限制为 Manager 的所有超类型。(已有的 super 关键字十分准确地描述了这种联系)

参考资料

《Java核心技术卷一:基础知识》(第10版)第 8 章:泛型程序设计文章来源地址https://www.toymoban.com/news/detail-480755.html

到了这里,关于Java的泛型的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Java 中的泛型(两万字超全详解)

    博主将用 CSDN 记录 Java 后端开发学习之路上的经验,并将自己整理的编程经验和知识分享出来,希望能帮助到有需要的小伙伴。 博主也希望和一直在坚持努力学习的小伙伴们共勉!唯有努力钻研,多思考勤动手,方能在编程道路上行至所向。 由于博主技术知识有限,博文中

    2024年02月04日
    浏览(48)
  • Java 中的泛型是什么,它有什么作用?(十五)

    Java中的泛型是一种类型参数化机制,它使代码更具可读性、可重用性和稳健性。在Java中,通过使用泛型,可以将类型作为参数传递给类或方法,并在编译时执行类型检查,从而避免许多运行时错误。 泛型的基础 Java泛型的基础概念是类型变量、类型参数和类型边界。 类型变

    2024年02月03日
    浏览(38)
  • 第8章-第1节-Java中的泛型(参数化类型)

    1、泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。什么是泛型?为什么要使用泛型? 2、概念:在类声明体中用到了类型参数。 3、泛型类只支持类类型,不支持基本数据类型(如int),但可以用包装类(如Integer ) 泛型标识 含义 T Type 类

    2024年01月23日
    浏览(45)
  • TypeScript中的泛型(泛型函数、接口、类、泛型约束)

    一、泛型函数 TypeScript 泛型是一种可以使代码具有更高的可重用性和泛化能力的特性 。通过泛型,我们可以定义一种通用的类型或函数,使其能够应对多种类型的输入。泛型在类、函数、接口等多种场景下都可以使用。 具体来说,在定义泛型函数时,我们可以使用来表示一

    2024年02月11日
    浏览(45)
  • 【TypeScript】TypeScript中的泛型

    定义一个函数或类时,有些情况下无法确定其中要使用的具体类型(返回值、参数、属性的类型不能确定),此时泛型便能够发挥作用。 举个例子: 上例中,test函数有一个参数类型不确定,但是能确定的时其返回值的类型和参数的类型是相同的,由于类型不确定所以参数和

    2024年02月09日
    浏览(47)
  • 第8章 泛型程序设计

    泛型程序设计(generi c programming)意味着编写的代码可以对多种不同类型的对象重 用。 它们会让你的程序更易读,也更安全。 集合中没有使用泛型时: 集合中使用泛型时: Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。即,把不安全的

    2024年02月07日
    浏览(26)
  • C++中的泛型详细讲解

    它是一种泛化的编程方式,其实现原理为程序员编写一个函数/类的代码示例,让编译器去填补出不同的函数实现。允许您延迟编写类或方法中的编程元素的数据类型的规范,直到实际在程序中使用它的时候。换句话说,泛型允许您编写一个可以与任何数据类型一起工作的类或

    2024年02月12日
    浏览(53)
  • Kotlin中的泛型理解与应用

    泛型是一种在编程中用于增加代码的灵活性和重用性的概念。它允许我们编写可以在多种类型上工作的通用代码,而不需要为每种类型都编写不同的代码。 在Kotlin中,泛型可以应用于类、函数和接口等。下面是一些关于泛型在Kotlin中的理解和示例。 1、泛型类 泛型类是指可以

    2024年02月07日
    浏览(39)
  • python中的泛型使用TypeVar

    PEP484的作者希望借助typing模块引入类型提示,不改动语言的其它部分。通过精巧的元编程技术,让类支持[]运算不成问题。但是方括号内的T变量必须在某处定义,否则要大范围改动python解释器才能让泛型支持特殊的[]表示法。 鉴于此,我们增加了typing.TypeVar构造函数,把变量

    2024年02月04日
    浏览(51)
  • .NET Framework中自带的泛型委托Func

    Func是.NET Framework中自带的泛型委托,可以接收一个或多个输入参数,并且有返回值,和Action类似,.NET基类库也提供了多达16个输入参数的Func委托,输出参数只有1个。 .NET Framework为我们提供了多达16个参数的Func委托定义,对于常见的开发场景已经完全够用。 如下图, 注意:

    2024年02月05日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包