Java 中文官方教程 2022 版(四十六)(1)

简介: Java 中文官方教程 2022 版(四十六)

定义简单的通用类型

原文:docs.oracle.com/javase/tutorial/extra/generics/simple.html

这里是包java.util中接口ListIterator的定义的一个小节选:

public interface List <E> {
    void add(E x);
    Iterator<E> iterator();
}
public interface Iterator<E> {
    E next();
    boolean hasNext();
}

这段代码应该都很熟悉,除了尖括号中的内容。那些是接口ListIterator形式类型参数的声明。

类型参数可以在通用声明中的几乎任何地方使用,就像你会使用普通类型一样(尽管有一些重要的限制;请参阅细则部分)。

在介绍中,我们看到了List调用,比如List。在调用(通常称为参数化类型)中,所有形式类型参数(在本例中为E)的所有出现都被实际类型参数(在本例中为Integer)替换。

你可能会想象List代表了List的一个版本,其中E已经被Integer统一替换:

public interface IntegerList {
    void add(Integer x);
    Iterator<Integer> iterator();
}

这种直觉可能有所帮助,但也是误导的。

这很有帮助,因为参数化类型List确实有看起来像这个展开的方法。

这是误导的,因为通用的声明实际上从未以这种方式展开。代码中没有多个副本——不在源代码中,也不在二进制代码中,也不在磁盘上,也不在内存中。如果您是 C++程序员,您会明白这与 C++模板非常不同。

通用类型声明只编译一次,并转换为单个类文件,就像普通类或接口声明一样。

类型参数类似于方法或构造函数中使用的普通参数。就像方法有描述其操作的值种类的形式值参数一样,通用声明有形式类型参数。当调用方法时,实际参数被替换为形式参数,并且方法体被评估。当调用通用声明时,实际类型参数被替换为形式类型参数。

关于命名约定的说明。我们建议您为形式类型参数使用简洁(如果可能的话是单个字符)但富有启发性的名称。最好避免在这些名称中使用小写字符,这样可以轻松区分形式类型参数和普通类和接口。许多容器类型使用E,表示元素,就像上面的例子一样。我们将在后面的例子中看到一些额外的约定。

泛型和子类型

原文:docs.oracle.com/javase/tutorial/extra/generics/subtype.html

让我们测试一下你对泛型的理解。以下代码片段是否合法?

List<String> ls = new ArrayList<String>(); // 1
List<Object> lo = ls; // 2 

第 1 行肯定是合法的。问题的棘手部分在于第 2 行。这归结为一个问题:StringList是否是ObjectList。大多数人本能地回答:“当然!”

好吧,看看接下来的几行:

lo.add(new Object()); // 3
String s = ls.get(0); // 4: Attempts to assign an Object to a String!

在这里,我们给lslo取了别名。通过别名lo访问String的列表ls,我们可以向其中插入任意对象。结果ls不再只包含String,当我们尝试从中取出东西时,会得到一个不愉快的惊喜。

Java 编译器当然会阻止这种情况发生。第 2 行将导致编译时错误。

一般来说,如果FooBar的子类型(子类或子接口),而G是某个泛型类型声明,那么G不是G的子类型。这可能是你需要了解的关于泛型最困难的事情,因为它违背了我们根深蒂固的直觉。

我们不应该假设集合不会改变。我们的直觉可能会让我们认为这些东西是不可变的。

例如,如果机动车管理部门向人口普查局提供驾驶员名单,这似乎是合理的。我们认为ListList,假设DriverPerson的子类型。实际上,传递的是驾驶员注册表的副本。否则,人口普查局可能会将不是驾驶员的新人加入列表,从而破坏了机动车管理部门的记录。

为了应对这种情况,考虑更灵活的泛型类型是很有用的。到目前为止,我们看到的规则相当严格。

通配符

原文:docs.oracle.com/javase/tutorial/extra/generics/wildcards.html

考虑编写一个打印集合中所有元素的例程的问题。以下是你可能在语言的旧版本(即 5.0 版本之前)中编写的方式:

void printCollection(Collection c) {
    Iterator i = c.iterator();
    for (k = 0; k < c.size(); k++) {
        System.out.println(i.next());
    }
}

这是一个使用泛型(和新的for循环语法)编写的天真尝试:

void printCollection(Collection<Object> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

问题在于这个新版本比旧版本要不实用得多。而旧代码可以使用任何类型的集合作为参数调用,新代码只接受Collection,正如我们刚刚证明的,这是所有种类集合的超类型!

那么所有种类的集合的超类型是什么呢?它被写作Collection(读作"未知类型的集合"),即元素类型匹配任何内容的集合。它被称为通配符类型,原因显而易见。我们可以这样写:

void printCollection(Collection<?> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

现在,我们可以使用任何类型的集合调用它。请注意,在printCollection()内部,我们仍然可以从c中读取元素并将它们赋予类型Object。这总是安全的,因为无论集合的实际类型是什么,它都包含对象。但是向其中添加任意对象是不安全的:

Collection<?> c = new ArrayList<String>();
c.add(new Object()); // Compile time error

由于我们不知道c的元素类型代表什么,我们无法向其添加对象。add()方法接受类型为E的参数,即集合的元素类型。当实际类型参数为?时,它代表某种未知类型。我们传递给add的任何参数都必须是这种未知类型的子类型。由于我们不知道那种类型是什么,我们无法传递任何内容。唯一的例外是null,它是每种类型的成员。

另一方面,对于给定的List,我们可以调用get()并利用结果。结果类型是一个未知类型,但我们始终知道它是一个对象。因此,将get()的结果分配给类型为Object的变量或将其作为期望类型为Object的参数传递是安全的。

有界通配符

考虑一个简单的绘图应用程序,可以绘制矩形和圆形等形状。为了在程序中表示这些形状,你可以定义一个类层次结构,如下所示:

public abstract class Shape {
    public abstract void draw(Canvas c);
}
public class Circle extends Shape {
    private int x, y, radius;
    public void draw(Canvas c) {
        ...
    }
}
public class Rectangle extends Shape {
    private int x, y, width, height;
    public void draw(Canvas c) {
        ...
    }
}

这些类可以在画布上绘制:

public class Canvas {
    public void draw(Shape s) {
        s.draw(this);
   }
}

任何绘图通常会包含许多形状。假设它们被表示为一个列表,那么在Canvas中有一个绘制它们所有的方法会很方便:

public void drawAll(List<Shape> shapes) {
    for (Shape s: shapes) {
        s.draw(this);
   }
}

现在,类型规则表明drawAll()只能在Shape的列表上调用:例如,不能在List上调用。这很不幸,因为该方法所做的只是从列表中读取形状,所以它同样可以在List上调用。我们真正想要的是该方法接受任何种类的形状列表:

public void drawAll(List<? extends Shape> shapes) {
    ...
}

这里有一个很小但非常重要的区别:我们用List替换了类型List。现在drawAll()将接受任何Shape的子类列表,因此我们现在可以在List上调用它。

List是有界通配符的一个例子。?代表一个未知类型,就像我们之前看到的通配符一样。然而,在这种情况下,我们知道这个未知类型实际上是Shape的子类型。(注意:它可以是Shape本身,或者某个子类;它不一定要直接扩展Shape。)我们说Shape是通配符的上界。

使用通配符灵活性的代价通常是很高的。这个代价是在方法体中写入shapes现在是非法的。例如,下面的写法是不允许的:

public void addRectangle(List<? extends Shape> shapes) {
    // *Compile-time error!*
    shapes.add(0, new Rectangle());
}

你应该能够弄清楚为什么上面的代码是不允许的。shapes.add()的第二个参数的类型是? **extends** Shape– 一个未知的Shape子类型。由于我们不知道它的类型,我们也不知道它是否是Rectangle的超类型;它可能是也可能不是这样的超类型,因此在这里传递Rectangle是不安全的。

有界通配符正是处理 DMV 将其数据传递给人口普查局的例子所需的。我们的例子假设数据由从名称(表示为字符串)到人员(由Person或其子类型,如Driver表示的引用类型)的映射表示。Map是一个接受两个类型参数的泛型类型的例子,表示映射的键和值。

再次注意正式类型参数的命名约定–K代表键,V代表值。

public class Census {
    public static void addRegistry(Map<String, ? extends Person> registry) {
}
...
Map<String, Driver> allDrivers = ... ;
Census.addRegistry(allDrivers);

通用方法

原文:docs.oracle.com/javase/tutorial/extra/generics/methods.html

考虑编写一个方法,该方法接受一个对象数组和一个集合,并将数组中的所有对象放入集合中。以下是第一次尝试:

static void fromArrayToCollection(Object[] a, Collection<?> c) {
    for (Object o : a) { 
        c.add(o); // *compile-time error*
    }
}

到目前为止,您已经学会了避免初学者的错误,即尝试将Collection用作集合参数的类型。您可能已经意识到,使用Collection也行不通。请记住,您不能将对象随意放入未知类型的集合中。

处理这些问题的方法是使用通用方法。就像类型声明一样,方法声明也可以是通用的—即,由一个或多个类型参数参数化。

static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
    for (T o : a) {
        c.add(o); // *Correct*
    }
}

我们可以使用任何元素类型为数组元素类型的超类型的任何类型的集合调用此方法。

Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();
// *T inferred to be Object*
fromArrayToCollection(oa, co); 
String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();
// *T inferred to be String*
fromArrayToCollection(sa, cs);
// *T inferred to be Object*
fromArrayToCollection(sa, co);
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<Number>();
// *T inferred to be Number*
fromArrayToCollection(ia, cn);
// *T inferred to be Number*
fromArrayToCollection(fa, cn);
// *T inferred to be Number*
fromArrayToCollection(na, cn);
// *T inferred to be Object*
fromArrayToCollection(na, co);
// *compile-time error*
fromArrayToCollection(na, cs);

请注意,我们不必向通用方法传递实际类型参数。编译器根据实际参数的类型为我们推断类型参数。它通常会推断使调用类型正确的最具体的类型参数。

一个问题是:何时应该使用通用方法,何时应该使用通配符类型?为了理解答案,让我们来看看Collection库中的一些方法。

interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

我们可以在这里使用通用方法:

interface Collection<E> {
    public <T> boolean containsAll(Collection<T> c);
    public <T extends E> boolean addAll(Collection<T> c);
    // *Hey, type variables can have bounds too!*
}

然而,在containsAll和addAll中,类型参数T仅使用一次。返回类型不依赖于类型参数,方法的任何其他参数也不依赖于类型参数(在这种情况下,只有一个参数)。这告诉我们,类型参数用于多态性;它的唯一效果是允许在不同的调用站点使用各种实际参数类型。如果是这种情况,应该使用通配符。通配符旨在支持灵活的子类型化,这正是我们要表达的。

通用方法允许使用类型参数来表示方法的一个或多个参数以及/或其返回类型之间的依赖关系。如果没有这样的依赖关系,则不应使用通用方法。

可以同时使用通用方法和通配符。以下是Collections.copy()方法的使用方法:

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

注意两个参数类型之间的依赖关系。从源列表src复制的任何对象必须可分配给目标列表dst的元素类型T。因此,src的元素类型可以是T的任何子类型—我们不关心是哪种类型。copy的签名使用类型参数表达依赖关系,但对第二个参数的元素类型使用通配符。

我们可以以另一种方式编写此方法的签名,而根本不使用通配符:

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

这是可以的,但是第一个类型参数既用于dst的类型,也用于第二个类型参数S的边界,而S本身只在src的类型中使用了一次—没有其他地方依赖于它。这表明我们可以用通配符替换S。使用通配符比声明显式类型参数更清晰、更简洁,因此在可能的情况下应优先使用通配符。

通配符还有一个优点,就是它们可以在方法签名之外使用,比如字段、局部变量和数组的类型。这里是一个例子。

回到我们的形状绘制问题,假设我们想要保留绘制请求的历史记录。我们可以在Shape类内部的静态变量中维护历史记录,并让drawAll()将其传入的参数存储到历史字段中。

static List<List<? extends Shape>> 
    history = new ArrayList<List<? extends Shape>>();
public void drawAll(List<? extends Shape> shapes) {
    history.addLast(shapes);
    for (Shape s: shapes) {
        s.draw(this);
    }
}

最后,让我们再次注意一下用于类型参数的命名约定。我们在没有更具体的类型来区分时使用T表示类型。这在泛型方法中经常发生。如果有多个类型参数,我们可能会使用字母来区分T在字母表中的邻居,比如S。如果泛型方法出现在泛型类中,最好避免在方法和类的类型参数中使用相同的名称,以避免混淆。嵌套泛型类也适用相同的规则。

与遗留代码互操作

原文:docs.oracle.com/javase/tutorial/extra/generics/legacy.html

到目前为止,我们所有的例子都假设了一个理想化的世界,在这个世界中,每个人都在使用支持泛型的最新版本的 Java 编程语言。

然而,现实情况并非如此。数百万行代码是用早期版本的语言编写的,它们不会一夜之间全部转换。

稍后,在将遗留代码转换为使用泛型部分,我们将解决将旧代码转换为使用泛型的问题。在本节中,我们将专注于一个更简单的问题:如何使遗留代码和通用代码互操作?这个问题有两个部分:在通用代码中使用遗留代码和在遗留代码中使用通用代码。

在通用代码中使用遗留代码

如何在使用自己的代码时使用旧代码,同时仍享受泛型的好处?

举个例子,假设你想使用包 com.Example.widgets。Example.com 的人们推出了一个用于库存控制的系统,其亮点如下所示:

package com.Example.widgets;
public interface Part {...}
public class Inventory {
    /**
     * Adds a new Assembly to the inventory database.
     * The assembly is given the name name, and 
     * consists of a set parts specified by parts. 
     * All elements of the collection parts
     * must support the Part interface.
     **/ 
    public static void addAssembly(String name, Collection parts) {...}
    public static Assembly getAssembly(String name) {...}
}
public interface Assembly {
    // *Returns a collection of Parts*
    Collection getParts();
}

现在,你想添加新代码,使用上面的 API。最好确保你总是使用正确的参数调用 addAssembly() - 也就是说,你传入的集合确实是 Part 的 Collection。当然,泛型正是为此而设计的:

package com.mycompany.inventory;
import com.Example.widgets.*;
public class Blade implements Part {
    ...
}
public class Guillotine implements Part {
}
public class Main {
    public static void main(String[] args) {
        Collection<Part> c = new ArrayList<Part>();
        c.add(new Guillotine()) ;
        c.add(new Blade());
        Inventory.addAssembly("thingee", c);
        Collection<Part> k = Inventory.getAssembly("thingee").getParts();
    }
}

当我们调用 addAssembly 时,它期望第二个参数是 Collection 类型。实际参数是 Collection 类型。这样也能工作,但为什么呢?毕竟,大多数集合不包含 Part 对象,因此一般情况下,编译器无法知道 Collection 类型引用的是什么类型的集合。

在适当的通用代码中,Collection 总是伴随着一个类型参数。当像 Collection 这样的通用类型在没有类型参数的情况下使用时,它被称为原始类型。

大多数人的第一反应是 Collection 真的意味着 Collection。然而,正如我们之前看到的,将 Collection 传递到需要 Collection 的地方是不安全的。更准确地说,类型 Collection 表示某种未知类型的集合,就像 Collection 一样。

但等等,这也不对!考虑一下对 getParts() 的调用,它返回一个 Collection。然后将其赋给 k,它是一个 Collection。如果调用的结果是 Collection,那么赋值就会出错。

实际上,这种赋值是合法的,但会生成一个未经检查的警告。这个警告是必要的,因为事实上编译器无法保证其正确性。我们无法检查 getAssembly() 中的遗留代码,以确保返回的确实是一个 Part 集合。代码中使用的类型是 Collection,可以合法地向这样的集合中插入各种对象。

那么,这不应该是一个错误吗?从理论上讲,是的;但从实际上讲,如果通用代码要调用遗留代码,这必须被允许。这取决于你,程序员,要确信在这种情况下,赋值是安全的,因为getAssembly()的契约规定它返回一个Part的集合,即使类型签名没有显示这一点。

因此,原始类型非常类似于通配符类型,但它们的类型检查不那么严格。这是一个故意的设计决定,允许泛型与现有的遗留代码进行交互。

从通用代码调用遗留代码本质上是危险的;一旦将通用代码与非通用遗留代码混合,通常提供的所有通用类型系统的安全保证都将无效。然而,你仍然比根本不使用泛型要好。至少你知道你这边的代码是一致的。

目前,非泛型代码比泛型代码要多得多,而且不可避免地会出现必须混合使用它们的情况。

如果你发现必须混合使用遗留代码和通用代码,请密切关注未经检查的警告。仔细考虑如何证明引发警告的代码的安全性。

如果你仍然犯了错误,导致警告的代码确实不安全,会发生什么?让我们看看这种情况。在这个过程中,我们将深入了解编译器的工作原理。

Java 中文官方教程 2022 版(四十六)(2)https://developer.aliyun.com/article/1488455


相关文章
|
17天前
|
Java 开发者 UED
【实战宝典】Java异常处理大师级教程:throws关键字,让异常声明成为你的专属标签!
【实战宝典】Java异常处理大师级教程:throws关键字,让异常声明成为你的专属标签!
31 3
|
29天前
|
前端开发 Java Maven
【前端学java】全网最详细的maven安装与IDEA集成教程!
【8月更文挑战第12天】全网最详细的maven安装与IDEA集成教程!
60 2
【前端学java】全网最详细的maven安装与IDEA集成教程!
|
18天前
|
Java 开发者
Java多线程教程:使用ReentrantLock实现高级锁功能
Java多线程教程:使用ReentrantLock实现高级锁功能
21 1
|
1月前
|
存储 网络协议 Oracle
java教程
java教程【8月更文挑战第11天】
23 5
|
2月前
|
SQL 安全 Java
「滚雪球学Java」教程导航帖(更新2024.07.16)
《滚雪球学Spring Boot》是一个面向初学者的Spring Boot教程,旨在帮助读者快速入门Spring Boot开发。本专通过深入浅出的方式,将Spring Boot开发中的核心概念、基础知识、实战技巧等内容系统地讲解,同时还提供了大量实际的案例,让读者能够快速掌握实用的Spring Boot开发技能。本书的特点在于注重实践,通过实例学习的方式激发读者的学习兴趣和动力,并引导读者逐步掌握Spring Boot开发的实际应用。
60 1
「滚雪球学Java」教程导航帖(更新2024.07.16)
|
16天前
|
Java API
Java与Lua互相调用简单教程
【8月更文挑战第29天】在软件开发中,Java以其强大的稳定性和广泛的生态系统著称,而Lua则因其轻量级、灵活和嵌入式的特点在脚本编写、游戏开发等领域大放异彩。将两者结合使用,可以充分利用Java的底层能力和Lua的快速开发优势。本文将通过一个简单的教程,介绍如何在Java程序中嵌入并执行Lua脚本,以及如何在Lua中调用Java方法。
17 0
WXM
|
2月前
|
Oracle Java 关系型数据库
Java JDK下载安装及环境配置超详细图文教程
Java JDK下载安装及环境配置超详细图文教程
WXM
242 3
|
2月前
|
测试技术 API Android开发
《手把手教你》系列基础篇(九十七)-java+ selenium自动化测试-框架设计篇-Selenium方法的二次封装和页面基类(详解教程)
【7月更文挑战第15天】这是关于自动化测试框架中Selenium API二次封装的教程总结。教程中介绍了如何设计一个支持不同浏览器测试的页面基类(BasePage),该基类包含了对Selenium方法的二次封装,如元素的输入、点击、清除等常用操作,以减少重复代码。此外,页面基类还提供了获取页面标题和URL的方法。
62 2
|
2月前
|
Web App开发 XML Java
《手把手教你》系列基础篇(九十六)-java+ selenium自动化测试-框架之设计篇-跨浏览器(详解教程)
【7月更文挑战第14天】这篇教程介绍了如何使用Java和Selenium构建一个支持跨浏览器测试的自动化测试框架。设计的核心是通过读取配置文件来切换不同浏览器执行测试用例。配置文件中定义了浏览器类型(如Firefox、Chrome)和测试服务器的URL。代码包括一个`BrowserEngine`类,它初始化配置数据,根据配置启动指定的浏览器,并提供关闭浏览器的方法。测试脚本`TestLaunchBrowser`使用`BrowserEngine`来启动浏览器并执行测试。整个框架允许在不同浏览器上运行相同的测试,以确保兼容性和一致性。
63 3
|
2月前
|
存储 Web App开发 Java
《手把手教你》系列基础篇(九十五)-java+ selenium自动化测试-框架之设计篇-java实现自定义日志输出(详解教程)
【7月更文挑战第13天】这篇文章介绍了如何在Java中创建一个简单的自定义日志系统,以替代Log4j或logback。
245 5