Java 中文官方教程 2022 版(六)(2)

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

Java 中文官方教程 2022 版(六)(1)https://developer.aliyun.com/article/1486307

有界类型参数

原文:docs.oracle.com/javase/tutorial/java/generics/bounded.html

有时候你可能想要限制可以用作参数化类型中类型参数的类型。例如,一个操作数字的方法可能只想接受Number或其子类的实例。这就是有界类型参数的用途。

要声明一个有界类型参数,列出类型参数的名称,后跟extends关键字,后跟其上界,在这个例子中是Number。请注意,在这个上下文中,extends的意思是"扩展"(如类)或"实现"(如接口)。

public class Box<T> {
    private T t;          
    public void set(T t) {
        this.t = t;
    }
    public T get() {
        return t;
    }
    public <U extends Number> void inspect(U u){
        System.out.println("T: " + t.getClass().getName());
        System.out.println("U: " + u.getClass().getName());
    }
    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<Integer>();
        integerBox.set(new Integer(10));
        integerBox.inspect("some text"); // error: this is still String!
    }
}

通过修改我们的通用方法以包含这个有界类型参数,编译现在会失败,因为我们对inspect的调用仍然包括一个String

Box.java:21: <U>inspect(U) in Box<java.lang.Integer> cannot
  be applied to (java.lang.String)
                        integerBox.inspect("10");
                                  ^
1 error

除了限制你可以用来实例化泛型类型的类型之外,有界类型参数还允许你调用边界中定义的方法:

public class NaturalNumber<T extends Integer> {
    private T n;
    public NaturalNumber(T n)  { this.n = n; }
    public boolean isEven() {
        return n.intValue() % 2 == 0;
    }
    // ...
}

isEven方法通过n调用了Integer类中定义的intValue方法。

多个边界

前面的例子说明了使用具有单个边界的类型参数,但是类型参数可以有多个边界

<T extends B1 & B2 & B3>

具有多个边界的类型变量是边界中列出的所有类型的子类型。如果边界中有一个类,它必须首先指定。例如:

Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }

如果边界A没有首先指定,你会得到一个编译时错误:

class D <T extends B & A & C> { /* ... */ }  // compile-time error

泛型方法和有界类型参数

原文:docs.oracle.com/javase/tutorial/java/generics/boundedTypeParams.html

有界类型参数是实现泛型算法的关键。考虑以下方法,该方法计算数组T[]中大于指定元素elem的元素数量。

public static <T> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e > elem)  // compiler error
            ++count;
    return count;
}

该方法的实现很简单,但它无法编译,因为大于运算符(>)仅适用于原始类型,如shortintdoublelongfloatbytechar。你不能使用>运算符来比较对象。为了解决这个问题,使用一个由Comparable接口限定的类型参数:

public interface Comparable<T> {
    public int compareTo(T o);
}

最终的代码将是:

public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.compareTo(elem) > 0)
            ++count;
    return count;
}

泛型、继承和子类型

译文:docs.oracle.com/javase/tutorial/java/generics/inheritance.html

正如你已经知道的,可以将一个类型的对象赋给另一个类型的对象,前提是这两种类型是兼容的。例如,你可以将一个 Integer 赋给一个 Object,因为 ObjectInteger 的超类型之一:

Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger;   // OK

在面向对象的术语中,这被称为“是一个”关系。由于 Integer Object 的一种,所以赋值是允许的。但是 Integer 也是 Number 的一种,所以下面的代码也是有效的:

public void someMethod(Number n) { /* ... */ }
someMethod(new Integer(10));   // OK
someMethod(new Double(10.1));   // OK

泛型也是如此。你可以执行泛型类型调用,将 Number 作为其类型参数,并且如果参数与 Number 兼容,则允许任何后续的 add 调用:

Box<Number> box = new Box<Number>();
box.add(new Integer(10));   // OK
box.add(new Double(10.1));  // OK

现在考虑以下方法:

public void boxTest(Box<Number> n) { /* ... */ }

它接受什么类型的参数?通过查看其签名,你可以看到它接受一个类型为 Box 的单个参数。但这意味着什么?你可以传入 BoxBox 吗,正如你可能期望的那样?答案是“不可以”,因为 BoxBox 不是 Box 的子类型。

这是在使用泛型进行编程时的一个常见误解,但这是一个重要的概念需要学习。

Box 不是 Box 的子类型,即使 IntegerNumber 的子类型。


**注意:**给定两个具体类型 AB(例如,NumberInteger),MyClassMyClass 没有关系,无论 AB 是否相关。MyClassMyClass 的共同父类是 Object

有关如何在类型参数相关的情况下创建两个泛型类之间类似子类型的关系的信息,请参阅通配符和子类型。


通用类和子类型

你可以通过扩展或实现来对泛型类或接口进行子类型化。一个类或接口的类型参数与另一个类或接口的类型参数之间的关系由 extendsimplements 子句确定。

Collections 类为例,ArrayList 实现了 List,而 List 扩展了 Collection。因此,ArrayListList 的子类型,ListCollection 的子类型。只要不改变类型参数,类型之间的子类型关系就会保持不变。

一个示例 Collections 层次结构

现在想象一下,我们想要定义自己的列表接口,PayloadList,它将泛型类型P的可选值与每个元素关联起来。它的声明可能如下所示:

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

以下对PayloadList的参数化是List的子类型:

  • PayloadList
  • PayloadList
  • PayloadList

一个PayloadList层次结构示例

类型推断

原文:docs.oracle.com/javase/tutorial/java/generics/genTypeInference.html

类型推断是 Java 编译器根据每个方法调用和相应声明来确定使调用适用的类型参数(或参数)的能力。推断算法确定参数的类型,以及如果可用的话,结果被分配或返回的类型。最后,推断算法尝试找到适用于所有参数的最具体类型。

为了说明最后一点,在以下示例中,推断确定传递给pick方法的第二个参数的类型为Serializable

static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());

类型推断和泛型方法

泛型方法向您介绍了类型推断,使您能够调用泛型方法,就像调用普通方法一样,而无需在尖括号之间指定类型。考虑以下示例,BoxDemo,它需要Box类:

public class BoxDemo {
  public static <U> void addBox(U u, 
      java.util.List<Box<U>> boxes) {
    Box<U> box = new Box<>();
    box.set(u);
    boxes.add(box);
  }
  public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {
    int counter = 0;
    for (Box<U> box: boxes) {
      U boxContents = box.get();
      System.out.println("Box #" + counter + " contains [" +
             boxContents.toString() + "]");
      counter++;
    }
  }
  public static void main(String[] args) {
    java.util.ArrayList<Box<Integer>> listOfIntegerBoxes =
      new java.util.ArrayList<>();
    BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
    BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
    BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
    BoxDemo.outputBoxes(listOfIntegerBoxes);
  }
}

以下是此示例的输出:

Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]

泛型方法addBox定义了一个名为U的类型参数。通常,Java 编译器可以推断泛型方法调用的类型参数。因此,在大多数情况下,您不必指定它们。例如,要调用泛型方法addBox,您可以使用类型见证指定类型参数如下:

BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);

另外,如果省略类型见证,Java 编译器会自动推断(从方法的参数中)类型参数为Integer

BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);

类型推断和泛型类的实例化

只要编译器可以从上下文中推断出类型参数,您可以用一组空类型参数(<>)替换调用泛型类构造函数所需的类型参数。这一对尖括号非正式地称为菱形。

例如,考虑以下变量声明:

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

您可以用一组空类型参数(<>)替换构造函数的参数化类型:

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

请注意,在泛型类实例化期间利用类型推断,必须使用菱形。在以下示例中,编译器生成了未经检查的转换警告,因为HashMap()构造函数引用了HashMap原始类型,而不是Map>类型:

Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning

泛型和非泛型类的泛型构造函数的类型推断

请注意,构造函数可以是泛型的(换句话说,在泛型和非泛型类中声明自己的形式类型参数)。考虑以下示例:

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

考虑类MyClass的以下实例化:

new MyClass<Integer>("")

这个语句创建了参数化类型MyClass的实例;语句明确为泛型类MyClass的形式类型参数X指定了类型Integer。请注意,这个泛型类的构造函数包含一个形式类型参数T。编译器为这个泛型类的构造函数的形式类型参数T推断了类型String(因为这个构造函数的实际参数是一个String对象)。

Java SE 7 之前的编译器能够推断泛型构造函数的实际类型参数,类似于泛型方法。然而,在 Java SE 7 及更高版本中,如果使用菱形(<>),编译器可以推断正在实例化的泛型类的实际类型参数。考虑以下例子:

MyClass<Integer> myObject = new MyClass<>("");

在这个例子中,编译器为泛型类MyClass的形式类型参数X推断了类型Integer。它为这个泛型类的构造函数的形式类型参数T推断了类型String


**注意:**需要注意的是,推断算法仅使用调用参数、目标类型和可能的明显预期返回类型来推断类型。推断算法不使用程序后面的结果。


目标类型

Java 编译器利用目标类型推断泛型方法调用的类型参数。表达式的目标类型是 Java 编译器根据表达式出现的位置所期望的数据类型。考虑声明如下的方法Collections.emptyList

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

考虑以下赋值语句:

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

这个语句期望一个List的实例;这个数据类型是目标类型。因为方法emptyList返回类型为List的值,编译器推断类型参数T必须是值String。这在 Java SE 7 和 8 中都适用。或者,您可以使用类型推断并指定T的值如下:

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

然而,在这种情况下并不是必需的。尽管在其他情况下是必需的。考虑以下方法:

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

假设您想要使用空列表调用方法processStringList。在 Java SE 7 中,以下语句不会编译:

processStringList(Collections.emptyList());

Java SE 7 编译器生成类似以下的错误消息:

List<Object> cannot be converted to List<String>

编译器需要一个类型参数T的值,因此它从值Object开始。因此,调用Collections.emptyList返回一个类型为List的值,这与方法processStringList不兼容。因此,在 Java SE 7 中,您必须如下指定类型参数的值:

processStringList(Collections.<String>emptyList());

在 Java SE 8 中,这已经不再是必需的。什么是目标类型的概念已经扩展到包括方法参数,比如方法processStringList的参数。在这种情况下,processStringList需要一个类型为List的参数。方法Collections.emptyList返回一个List的值,因此使用List的目标类型,编译器推断类型参数T的值为String。因此,在 Java SE 8 中,以下语句编译通过:

processStringList(Collections.emptyList());

查看目标类型在 Lambda 表达式中获取更多信息。

通配符

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

在泛型代码中,问号(?),称为通配符,表示未知类型。 通配符可以在各种情况下使用:作为参数、字段或局部变量的类型;有时作为返回类型(尽管更具体的编程实践更好)。 通配符永远不会用作泛型方法调用、泛型类实例创建或超类型的类型参数。

以下部分将更详细地讨论通配符,包括上界通配符、下界通配符和通配符捕获。

上界通配符

原文:docs.oracle.com/javase/tutorial/java/generics/upperBounded.html

您可以使用上界通配符来放宽对变量的限制。例如,假设您想编写一个适用于ListListList的方法;您可以通过使用上界通配符来实现这一点。

要声明上界通配符,请使用通配符字符(‘?’),后跟extends关键字,再跟其上界。请注意,在此上下文中,extends的含义是广义上的,既可以表示"extends"(如类)也可以表示"implements"(如接口)。

要编写适用于Number及其子类型(如IntegerDoubleFloat)的列表的方法,您应指定List。术语ListList更为严格,因为前者仅匹配类型为Number的列表,而后者匹配类型为Number或其任何子类的列表。

考虑以下process方法:

public static void process(List<? extends Foo> list) { /* ... */ }
• 1
• 2

上界通配符,其中Foo是任何类型,匹配FooFoo的任何子类型。process方法可以将列表元素作为类型Foo访问:

public static void process(List<? extends Foo> list) {
    for (Foo elem : list) {
        // ...
    }
}

foreach子句中,elem变量遍历列表中的每个元素。现在可以在elem上使用Foo类中定义的任何方法。

sumOfList方法返回列表中数字的总和:

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

使用Integer对象列表的以下代码打印sum = 6.0

List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));

一组Double值可以使用相同的sumOfList方法。以下代码打印sum = 7.0

List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum = " + sumOfList(ld));

无界通配符

原文:docs.oracle.com/javase/tutorial/java/generics/unboundedWildcards.html

未限定通配符类型是使用通配符字符(?)指定的,例如,List。这被称为未知类型的列表。有两种情况下未限定通配符是一个有用的方法:

  • 如果你正在编写一个可以使用Object类提供的功能来实现的方法。
  • 当代码使用泛型类中不依赖于类型参数的方法时。例如,List.sizeList.clear。事实上,Class经常被使用,因为Class中的大多数方法不依赖于T

考虑以下方法,printList

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

printList的目标是打印任何类型的列表,但它未能实现这个目标 — 它只打印Object实例的列表;它无法打印ListListList等,因为它们不是List的子类型。要编写一个通用的printList方法,使用List

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

因为对于任何具体类型AListList的子类型,所以你可以使用printList来打印任何类型的列表:

List<Integer> li = Arrays.asList(1, 2, 3);
List<String>  ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);

注意: 在本课程的示例中使用了Arrays.asList方法。这个静态工厂方法将指定的数组转换并返回一个固定大小的列表。


需要注意的是,List和List并不相同。你可以将ObjectObject的任何子类型插入List中。但你只能将null插入List中。通配符使用指南部分有关于如何确定在特定情况下应该使用什么类型的通配符的更多信息。


Java 中文官方教程 2022 版(六)(3)https://developer.aliyun.com/article/1486309

相关文章
|
4天前
|
前端开发 Java Maven
【前端学java】全网最详细的maven安装与IDEA集成教程!
【8月更文挑战第12天】全网最详细的maven安装与IDEA集成教程!
21 2
【前端学java】全网最详细的maven安装与IDEA集成教程!
|
9天前
|
存储 网络协议 Oracle
java教程
java教程【8月更文挑战第11天】
14 5
|
1月前
|
SQL 安全 Java
「滚雪球学Java」教程导航帖(更新2024.07.16)
《滚雪球学Spring Boot》是一个面向初学者的Spring Boot教程,旨在帮助读者快速入门Spring Boot开发。本专通过深入浅出的方式,将Spring Boot开发中的核心概念、基础知识、实战技巧等内容系统地讲解,同时还提供了大量实际的案例,让读者能够快速掌握实用的Spring Boot开发技能。本书的特点在于注重实践,通过实例学习的方式激发读者的学习兴趣和动力,并引导读者逐步掌握Spring Boot开发的实际应用。
42 1
「滚雪球学Java」教程导航帖(更新2024.07.16)
WXM
|
25天前
|
Oracle Java 关系型数据库
Java JDK下载安装及环境配置超详细图文教程
Java JDK下载安装及环境配置超详细图文教程
WXM
129 3
|
1月前
|
测试技术 API Android开发
《手把手教你》系列基础篇(九十七)-java+ selenium自动化测试-框架设计篇-Selenium方法的二次封装和页面基类(详解教程)
【7月更文挑战第15天】这是关于自动化测试框架中Selenium API二次封装的教程总结。教程中介绍了如何设计一个支持不同浏览器测试的页面基类(BasePage),该基类包含了对Selenium方法的二次封装,如元素的输入、点击、清除等常用操作,以减少重复代码。此外,页面基类还提供了获取页面标题和URL的方法。
44 2
|
1月前
|
Web App开发 XML Java
《手把手教你》系列基础篇(九十六)-java+ selenium自动化测试-框架之设计篇-跨浏览器(详解教程)
【7月更文挑战第14天】这篇教程介绍了如何使用Java和Selenium构建一个支持跨浏览器测试的自动化测试框架。设计的核心是通过读取配置文件来切换不同浏览器执行测试用例。配置文件中定义了浏览器类型(如Firefox、Chrome)和测试服务器的URL。代码包括一个`BrowserEngine`类,它初始化配置数据,根据配置启动指定的浏览器,并提供关闭浏览器的方法。测试脚本`TestLaunchBrowser`使用`BrowserEngine`来启动浏览器并执行测试。整个框架允许在不同浏览器上运行相同的测试,以确保兼容性和一致性。
47 3
|
1月前
|
存储 Web App开发 Java
《手把手教你》系列基础篇(九十五)-java+ selenium自动化测试-框架之设计篇-java实现自定义日志输出(详解教程)
【7月更文挑战第13天】这篇文章介绍了如何在Java中创建一个简单的自定义日志系统,以替代Log4j或logback。
136 5
|
1月前
|
Java 数据安全/隐私保护
Java无模版导出Excel 0基础教程
经常写数据导出到EXCEL,没有模板的情况下使用POI技术。以此作为记录,以后方便使用。 2 工具类 样式工具: 处理工具Java接口 水印工具 导出Excel工具类 3 测试代码 与实际复杂业务不同 在此我们只做模拟 Controller Service 4 导出测试 使用Postman进行接口测试,没接触过Postman的小伙伴可以看我这篇博客Postman导出excel文件保存为文件可以看到导出很成功,包括水印 sheet页名称自适应宽度。还有一些高亮……等功能可以直接搜索使用
Java无模版导出Excel 0基础教程
|
1月前
|
设计模式 测试技术 Python
《手把手教你》系列基础篇(九十二)-java+ selenium自动化测试-框架设计基础-POM设计模式简介(详解教程)
【7月更文挑战第10天】Page Object Model (POM)是Selenium自动化测试中的设计模式,用于提高代码的可读性和维护性。POM将每个页面表示为一个类,封装元素定位和交互操作,使得测试脚本与页面元素分离。当页面元素改变时,只需更新对应页面类,减少了脚本的重复工作和维护复杂度,有利于团队协作。POM通过创建页面对象,管理页面元素集合,将业务逻辑与元素定位解耦合,增强了代码的复用性。示例展示了不使用POM时,脚本直接混杂了元素定位和业务逻辑,而POM则能解决这一问题。
43 6
|
1月前
|
设计模式 Java 测试技术
《手把手教你》系列基础篇(九十四)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-下篇(详解教程)
【7月更文挑战第12天】在本文中,作者宏哥介绍了如何在不使用PageFactory的情况下,用Java和Selenium实现Page Object Model (POM)。文章通过一个百度首页登录的实战例子来说明。首先,创建了一个名为`BaiduHomePage1`的页面对象类,其中包含了页面元素的定位和相关操作方法。接着,创建了测试类`TestWithPOM1`,在测试类中初始化WebDriver,设置驱动路径,最大化窗口,并调用页面对象类的方法进行登录操作。这样,测试脚本保持简洁,遵循了POM模式的高可读性和可维护性原则。
27 2