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

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

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

擦除和转换

public String loophole(Integer x) {
    List<String> ys = new LinkedList<String>();
    List xs = ys;
    xs.add(x); // *Compile-time unchecked warning*
    return ys.iterator().next();
}

在这里,我们给字符串列表和普通旧列表取了别名。我们将一个Integer插入列表,并尝试提取一个String。这显然是错误的。如果我们忽略警告并尝试执行此代码,它将在我们尝试使用错误类型的地方失败。在运行时,此代码的行为如下:

public String loophole(Integer x) {
    List ys = new LinkedList;
    List xs = ys;
    xs.add(x); 
    return(String) ys.iterator().next(); // *run time error*
}

当我们从列表中提取一个元素,并尝试将其强制转换为String以将其视为字符串时,我们将收到ClassCastException。与loophole()的泛型版本发生的事情完全相同。

这是因为,泛型是由 Java 编译器实现的一种称为擦除的前端转换。你(几乎)可以将其视为源到源的转换,其中loophole()的泛型版本转换为非泛型版本。

因此,即使存在未经检查的警告,Java 虚拟机的类型安全性和完整性也永远不会受到威胁

基本上,擦除会消除(或擦除)所有泛型类型信息。所有尖括号之间的类型信息都被丢弃,因此,例如,像List这样的参数化类型被转换为List。所有类型变量的剩余用法都被替换为类型变量的上界(通常为Object)。并且,每当生成的代码不符合类型时,都会插入到适当类型的强制转换,就像loophole的最后一行一样。

擦除的全部细节超出了本教程的范围,但我们刚刚给出的简单描述并不离谱。了解一些关于这个是很有好处的,特别是如果您想要做一些更复杂的事情,比如将现有 API 转换为使用泛型(参见将旧代码转换为使用泛型部分),或者只是想了解为什么事情是这样的。

在旧代码中使用泛型代码

现在让我们考虑相反的情况。想象一下,Example.com 选择将他们的 API 转换为使用泛型,但是一些客户端还没有这样做。现在代码看起来像这样:

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<Part> parts) {...}
    public static Assembly getAssembly(String name) {...}
}
public interface Assembly {
    // *Returns a collection of Parts*
    Collection<Part> getParts();
}

客户端代码如下:

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 c = new ArrayList();
        c.add(new Guillotine()) ;
        c.add(new Blade());
        // *1: unchecked warning*
        Inventory.addAssembly("thingee", c);
        Collection k = Inventory.getAssembly("thingee").getParts();
    }
}

客户端代码是在引入泛型之前编写的,但它使用了com.Example.widgets包和集合库,两者都使用了泛型类型。客户端代码中所有泛型类型声明的使用都是原始类型。

第 1 行生成了一个未经检查的警告,因为一个原始Collection被传递到一个期望Part集合的Collection位置,编译器无法确保原始Collection确实是Part集合。

作为一种替代方案,您可以使用源 1.4 标志编译客户端代码,确保不会生成任何警告。然而,在这种情况下,您将无法使用 JDK 5.0 引入的任何新语言特性。


细则

译文:docs.oracle.com/javase/tutorial/extra/generics/fineprint.html

一个泛型类被所有调用共享

以下代码片段打印什么?

List <String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());

你可能会倾向于说false,但你会错。它打印true,因为泛型类的所有实例具有相同的运行时类,而不考虑它们的实际类型参数。

实际上,使类成为泛型的是它对所有可能的类型参数具有相同行为的事实;同一个类可以被视为具有许多不同的类型。

因此,类的静态变量和方法也被所有实例共享。这就是为什么在静态方法或初始化程序中引用类型声明的类型参数,或在静态变量的声明或初始化程序中引用类型参数是非法的原因。

强制转换和 InstanceOf

泛型类被所有实例共享的事实的另一个含义是,通常没有意义询问一个实例是否是泛型类型的特定调用的实例:

Collection cs = new ArrayList<String>();
// *Illegal.*
if (cs instanceof Collection<String>) { ... }

同样,像这样的强制转换

// *Unchecked warning,*
Collection<String> cstr = (Collection<String>) cs;

给出一个未经检查的警告,因为这不是运行时系统会为你检查的内容。

类型变量也是如此

// *Unchecked warning.* 
<T> T badCast(T t, Object o) {
    return (T) o;
}

类型变量在运行时不存在。这意味着它们在时间和空间上都没有性能开销,这很好。不幸的是,这也意味着你不能可靠地在强制转换中使用它们。

数组

除非是(无界)通配符类型,否则数组对象的组件类型可能不是类型变量或参数化类型。你可以声明元素类型为类型变量或参数化类型的数组类型,但不能声明数组对象

这确实很烦人。这个限制是必要的,以避免出现这样的情况:

// *Not really allowed.*
List<String>[] lsa = new List<String>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// *Unsound, but passes run time store check*
oa[1] = li;
// *Run-time error: ClassCastException.*
String s = lsa[1].get(0);

如果允许参数化类型的数组,前面的例子将在没有任何未经检查警告的情况下编译,并在运行时失败。我们将类型安全作为泛型的主要设计目标。特别是,该语言被设计为保证如果整个应用程序使用javac -source 1.5编译时没有未经检查的警告,那么它是类型安全的

然而,你仍然可以使用通配符数组。前面代码的以下变体放弃了使用数组对象和元素类型为参数化的数组类型。因此,我们必须显式转换才能从数组中获取String

// *OK, array of unbounded wildcard type.*
List<?>[] lsa = new List<?>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// *Correct.*
oa[1] = li;
// *Run time error, but cast is explicit.*
String s = (String) lsa[1].get(0);

在下一个变体中,会导致编译时错误,我们避免创建元素类型为参数化的数组对象,但仍然使用具有参数化元素类型的数组类型。

// *Error.*
List<String>[] lsa = new List<?>[10];

同样,尝试创建元素类型为类型变量的数组对象会导致编译时错误:

<T> T[] makeArray(T t) {
    return new T[100]; // *Error.*
}

由于类型变量在运行时不存在,无法确定实际的数组类型。

解决这类限制的方法是使用类字面量作为运行时类型标记,如下一节所述,类字面量作为运行时类型标记。

类字面量作为运行时类型标记

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

JDK 5.0 中的一个变化是类java.lang.Class是泛型的。这是一个有趣的例子,使用泛型性来做除了容器类之外的事情。

现在Class有一个类型参数T,你可能会问,T代表什么?它代表Class对象所代表的类型。

例如,String.class的类型是ClassSerializable.class的类型是Class。这可以用来提高反射代码的类型安全性。

特别是,由于Class中的newInstance()方法现在返回一个T,在通过反射创建对象时可以获得更精确的类型。

例如,假设你需要编写一个实用方法,执行数据库查询,给定一个 SQL 字符串,并返回与该查询匹配的数据库中的对象集合。

一种方法是显式传递一个工厂对象,在调用点编写代码如下:

interface Factory<T> { T make();} 
public <T> Collection<T> select(Factory<T> factory, String statement) { 
    Collection<T> result = new ArrayList<T>(); 
    /* *Run sql query using jdbc* */  
    for (/* *Iterate over jdbc results.* */) { 
        T item = factory.make();
        /* *Use reflection and set all of item's 
         * fields from sql results.* 
         */ 
        result.add(item); 
    } 
    return result; 
}

你可以这样调用

select(new Factory<EmpInfo>(){ 
    public EmpInfo make() {
        return new EmpInfo();
    }}, "selection string");

或者你可以声明一个类EmpInfoFactory来支持Factory接口

class EmpInfoFactory implements Factory<EmpInfo> {
    ...
    public EmpInfo make() { 
        return new EmpInfo();
    }
}

并调用它

select(getMyEmpInfoFactory(), "selection string");

这种解决方案的缺点是需要:

  • 使用冗长的匿名工厂类在调用点,或者
  • 为每种使用的类型声明一个工厂类,并在调用点传递一个工厂实例,这有点不自然。

将类字面量作为工厂对象是很自然的,然后可以通过反射来使用。今天(没有泛型的情况下)代码可能会这样写:

Collection emps = sqlUtility.select(EmpInfo.class, "select * from emps");
...
public static Collection select(Class c, String sqlStatement) { 
    Collection result = new ArrayList();
    /* *Run sql query using jdbc.* */
    for (/* *Iterate over jdbc results.* */ ) { 
        Object item = c.newInstance(); 
        /* *Use reflection and set all of item's
         * fields from sql results.* 
         */  
        result.add(item); 
    } 
    return result; 
}

然而,这不会给我们提供我们想要的精确类型的集合。现在Class是泛型的,我们可以改为写如下代码:

Collection<EmpInfo> 
    emps = sqlUtility.select(EmpInfo.class, "select * from emps");
...
public static <T> Collection<T> select(Class<T> c, String sqlStatement) { 
    Collection<T> result = new ArrayList<T>();
    /* *Run sql query using jdbc.* */
    for (/* *Iterate over jdbc results.* */ ) { 
        T item = c.newInstance(); 
        /* *Use reflection and set all of item's
         * fields from sql results.* 
         */  
        result.add(item);
    } 
    return result; 
} 

上面的代码以类型安全的方式给出了我们想要的精确类型的集合。

使用类字面量作为运行时类型标记的技术是一个非常有用的技巧。这是一个在新的用于操作注解的 API 中广泛使用的习语。

通配符更有趣

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

在本节中,我们将考虑通配符的一些更高级用法。我们已经看到了几个示例,在从数据结构中读取时有界通配符是有用的。现在考虑相反的情况,一个只写数据结构。接口Sink是这种类型的一个简单示例。

interface Sink<T> {
    flush(T t);
}

我们可以想象使用它,如下面的代码所示。方法writeAll()旨在将集合coll的所有元素刷新到接收器snk中,并返回最后一个刷新的元素。

public static <T> T writeAll(Collection<T> coll, Sink<T> snk) {
    T last;
    for (T t : coll) {
        last = t;
        snk.flush(last);
    }
    return last;
}
...
Sink<Object> s;
Collection<String> cs;
String str = writeAll(cs, s); // *Illegal call.*

如所写,对writeAll()的调用是非法的,因为无法推断出有效的类型参数;StringObject都不适合T的类型,因为Collection元素和Sink元素必须是相同类型。

我们可以通过修改writeAll()的签名来修复此错误,如下所示,使用通配符。

public static <T> T writeAll(Collection<? extends T>, Sink<T>) {...}
...
// *Call is OK, but wrong return type.* 
String str = writeAll(cs, s);

现在调用是合法的,但赋值是错误的,因为推断的返回类型是Object,因为Ts的元素类型匹配,而sObject

解决方案是使用我们尚未看到的一种有界通配符形式:带有下界的通配符。语法? **super** T表示一个未知类型,它是T的超类型(或T本身;请记住超类型关系是自反的)。这是我们一直在使用的有界通配符的对偶,我们使用? **extends** T来表示一个未知类型,它是T的子类型。

public static <T> T writeAll(Collection<T> coll, Sink<? super T> snk) {
    ...
}
String str = writeAll(cs, s); // *Yes!* 

使用这种语法,调用是合法的,并且推断的类型是String,如所需。

现在让我们转向一个更现实的例子。java.util.TreeSet表示一个按顺序排列的类型为E的元素树。构造TreeSet的一种方法是将Comparator对象传递给构造函数。该比较器将用于根据所需的顺序对TreeSet的元素进行排序。

TreeSet(Comparator<E> c) 

Comparator接口本质上是:

interface Comparator<T> {
    int compare(T fst, T snd);
}

假设我们想创建一个TreeSet并传入一个合适的比较器,我们需要传递一个可以比较StringComparator。这可以通过Comparator来完成,但Comparator同样有效。但是,我们将无法在Comparator上调用上面给出的构造函数。我们可以使用下界通配符来获得所需的灵活性:

TreeSet(Comparator<? super E> c) 

此代码允许使用任何适用的比较器。

作为使用下界通配符的最后一个示例,让我们看看方法Collections.max(),它返回作为参数传递给它的集合中的最大元素。现在,为了让max()起作用,传入的集合的所有元素都必须实现Comparable。此外,它们都必须可以相互比较。

对这种方法签名的泛型化的第一次尝试产生了:

public static <T extends Comparable<T>> T max(Collection<T> coll)

换句话说,该方法接受一个可与自身比较的某种类型T的集合,并返回该类型的一个元素。然而,这段代码实际上过于限制性。要了解原因,考虑一种可与任意对象进行比较的类型:

class Foo implements Comparable<Object> {
    ...
}
Collection<Foo> cf = ... ;
Collections.max(cf); // *Should work.*

cf的每个元素都可以与cf中的其他元素进行比较,因为每个这样的元素都是Foo,而Foo可以与任何对象进行比较,特别是与另一个Foo进行比较。然而,使用上面的签名,我们发现该调用被拒绝了。推断的类型必须是Foo,但Foo并没有实现Comparable

T自身比较并不是必需的。所需的是T与其超类型之一进行比较。这给了我们:

public static <T extends Comparable<? super T>> 
        T max(Collection<T> coll)

请注意,Collections.max()的实际签名更为复杂。我们将在下一节将遗留代码转换为使用泛型中回到这一点。这种推理几乎适用于任何旨在适用于任意类型的Comparable的用法:您总是希望使用Comparable

一般来说,如果您的 API 只使用类型参数T作为参数,那么它的使用应该利用下界通配符(? **super** T)。相反,如果 API 只返回T,那么使用上界通配符(? **extends** T)将为客户端提供更大的灵活性。

通配符捕获

现在应该很清楚了,鉴于:

Set<?> unknownSet = new HashSet<String>();
...
/* Add an element  t to a Set s. */ 
public static <T> void addToSet(Set<T> s, T t) {
    ...
}

下面的调用是非法的。

addToSet(unknownSet, "abc"); // *Illegal.*

实际传递的集合是字符串集合并不重要;重要的是作为参数传递的表达式是未知类型的集合,不能保证是字符串集合,或者是特定类型的集合。

现在,考虑以下代码:

class Collections {
    ...
    <T> public static Set<T> unmodifiableSet(Set<T> set) {
        ...
    }
}
...
Set<?> s = Collections.unmodifiableSet(unknownSet); // *This works! Why?*

看起来这不应该被允许;然而,看着这个具体的调用,允许它是完全安全的。毕竟,unmodifiableSet()对于任何类型的Set都有效,无论其元素类型是什么。

由于这种情况相对频繁出现,有一条特殊规则允许在非常具体的情况下使用这样的代码,其中可以证明代码是安全的。这条规则称为通配符捕获,允许编译器将通配符的未知类型推断为泛型方法的类型参数。

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

相关文章
|
2天前
|
Web App开发 JavaScript 前端开发
《手把手教你》系列技巧篇(三十九)-java+ selenium自动化测试-JavaScript的调用执行-上篇(详解教程)
【5月更文挑战第3天】本文介绍了如何在Web自动化测试中使用JavaScript执行器(JavascriptExecutor)来完成Selenium API无法处理的任务。首先,需要将WebDriver转换为JavascriptExecutor对象,然后通过executeScript方法执行JavaScript代码。示例用法包括设置JS代码字符串并调用executeScript。文章提供了两个实战场景:一是当时间插件限制输入时,用JS去除元素的readonly属性;二是处理需滚动才能显示的元素,利用JS滚动页面。还给出了一个滚动到底部的代码示例,并提供了详细步骤和解释。
31 10
|
2天前
|
Java 测试技术 Python
《手把手教你》系列技巧篇(三十六)-java+ selenium自动化测试-单选和多选按钮操作-番外篇(详解教程)
【4月更文挑战第28天】本文简要介绍了自动化测试的实战应用,通过一个在线问卷调查(&lt;https://www.sojump.com/m/2792226.aspx/&gt;)为例,展示了如何遍历并点击问卷中的选项。测试思路包括找到单选和多选按钮的共性以定位元素,然后使用for循环进行点击操作。代码设计方面,提供了Java+Selenium的示例代码,通过WebDriver实现自动答题。运行代码后,可以看到控制台输出和浏览器的相应动作。文章最后做了简单的小结,强调了本次实践是对之前单选多选操作的巩固。
25 0
|
23小时前
|
JavaScript Java 测试技术
《手把手教你》系列技巧篇(四十六)-java+ selenium自动化测试-web页面定位toast-下篇(详解教程)
【5月更文挑战第10天】本文介绍了使用Java和Selenium进行Web自动化测试的实践,以安居客网站为例。最后,提到了在浏览器开发者工具中调试和观察页面元素的方法。
11 2
|
1天前
|
算法 Java Python
保姆级Java入门练习教程,附代码讲解,小白零基础入门必备
保姆级Java入门练习教程,附代码讲解,小白零基础入门必备
|
1天前
|
Web App开发 JavaScript 测试技术
《手把手教你》系列技巧篇(四十五)-java+ selenium自动化测试-web页面定位toast-上篇(详解教程)
【5月更文挑战第9天】本文介绍了在Appium中处理App自动化测试中遇到的Toast元素定位的方法。Toast在Web UI测试中也常见,通常作为轻量级反馈短暂显示。文章提供了两种定位Toast元素的技巧.
10 0
|
2天前
|
Web App开发 缓存 前端开发
《手把手教你》系列技巧篇(四十四)-java+ selenium自动化测试-处理https 安全问题或者非信任站点-下篇(详解教程)
【5月更文挑战第8天】这篇文档介绍了如何在IE、Chrome和Firefox浏览器中处理不信任证书的问题。作者北京-宏哥分享了如何通过编程方式跳过浏览器的证书警告,直接访问不受信任的HTTPS网站。文章分为几个部分,首先简要介绍了问题背景,然后详细讲解了在Chrome浏览器中的两种方法,包括代码设计和运行效果,并给出了其他浏览器的相关信息和参考资料。最后,作者总结了处理此类问题的一些通用技巧。
16 2
|
2天前
|
Java Android开发
【Java开发指南 | 第十八篇】Eclipse安装教程
【Java开发指南 | 第十八篇】Eclipse安装教程
11 2
|
2天前
|
Web App开发 JavaScript 前端开发
《手把手教你》系列技巧篇(四十三)-java+ selenium自动化测试-处理https 安全问题或者非信任站点-上篇(详解教程)
【5月更文挑战第7天】本文介绍了如何在Java+Selenium自动化测试中处理浏览器对不信任证书的处理方法,特别是针对IE、Chrome和Firefox浏览器。在某些情况下,访问HTTPS网站时会遇到证书不可信的警告,但可以通过编程方式跳过这些警告。
13 1
|
2天前
|
前端开发 Java 测试技术
《手把手教你》系列技巧篇(四十二)-java+ selenium自动化测试 - 处理iframe -下篇(详解教程)
【5月更文挑战第6天】本文介绍了如何使用Selenium处理含有iframe的网页。作者首先解释了iframe是什么,即HTML中的一个框架,用于在一个页面中嵌入另一个页面。接着,通过一个实战例子展示了在QQ邮箱登录页面中,由于输入框存在于iframe内,导致直接定位元素失败。作者提供了三种方法来处理这种情况:1)通过id或name属性切换到iframe;2)使用webElement对象切换;3)通过索引切换。最后,给出了相应的Java代码示例,并提醒读者根据iframe的实际情况选择合适的方法进行切换和元素定位。
10 0
|
2天前
|
前端开发 测试技术 Python
《手把手教你》系列技巧篇(四十一)-java+ selenium自动化测试 - 处理iframe -上篇(详解教程)
【5月更文挑战第5天】本文介绍了HTML中的`iframe`标签,它用于在网页中嵌套其他网页。`iframe`常用于加载外部内容或网站的某个部分,以实现页面美观。文章还讲述了使用Selenium自动化测试时如何处理`iframe`,通过`switchTo().frame()`方法进入`iframe`,完成相应操作,然后使用`switchTo().defaultContent()`返回主窗口。此外,文章提供了一个包含`iframe`的HTML代码示例,并给出了一个简单的自动化测试代码实战,演示了如何在`iframe`中输入文本。
17 3