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

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

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

类型擦除和桥接方法的影响

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

有时类型擦除会导致一个你可能没有预料到的情况。以下示例展示了这种情况是如何发生的。有时编译器会在类型擦除过程中创建一个合成方法,称为桥接方法

给定以下两个类:

public class Node<T> {
    public T data;
    public Node(T data) { this.data = data; }
    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}
public class MyNode extends Node<Integer> {
    public MyNode(Integer data) { super(data); }
    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

考虑以下代码:

MyNode mn = new MyNode(5);
Node n = mn;            // A raw type - compiler throws an unchecked warning
n.setData("Hello");     // Causes a ClassCastException to be thrown.
Integer x = mn.data;  

经过类型擦除后,这段代码变成:

MyNode mn = new MyNode(5);
Node n = mn;            // A raw type - compiler throws an unchecked warning
                        // Note: This statement could instead be the following:
                        //     Node n = (Node)mn;
                        // However, the compiler doesn't generate a cast because
                        // it isn't required.
n.setData("Hello");     // Causes a ClassCastException to be thrown.
Integer x = (Integer)mn.data; 

下一节将解释为什么在 n.setData("Hello"); 语句处抛出 ClassCastException

桥接方法

当编译一个继承参数化类或实现参数化接口的类或接口时,编译器可能需要创建一个合成方法,称为桥接方法,作为类型擦除过程的一部分。通常情况下,你不需要担心桥接方法,但如果在堆栈跟踪中出现一个,你可能会感到困惑。

经过类型擦除后,NodeMyNode 类变成:

public class Node {
    public Object data;
    public Node(Object data) { this.data = data; }
    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}
public class MyNode extends Node {
    public MyNode(Integer data) { super(data); }
    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

经过类型擦除后,方法签名不匹配;Node.setData(T) 方法变成了 Node.setData(Object)。因此,MyNode.setData(Integer) 方法不会覆盖 Node.setData(Object) 方法。

为了解决这个问题并在类型擦除后保留泛型类型的多态性,Java 编译器生成一个桥接方法来确保子类型化按预期工作。

对于 MyNode 类,编译器为 setData 生成了以下桥接方法:

class MyNode extends Node {
    // Bridge method generated by the compiler
    //
    public void setData(Object data) {
        setData((Integer) data);
    }
    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
    // ...
}

桥接方法 MyNode.setData(object) 委托给原始的 MyNode.setData(Integer) 方法。因此,n.setData("Hello"); 语句调用了 MyNode.setData(Object) 方法,由于 "Hello" 无法转换为 Integer,导致抛出 ClassCastException

非可实例化类型

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

章节 类型擦除 讨论了编译器删除与类型参数和类型参数相关的信息的过程。类型擦除对于具有非可实例化类型的可变参数(也称为 varargs)方法有相关后果。有关可变参数方法的更多信息,请参见 传递信息给方法或构造函数 中的 任意数量的参数 章节。

本页涵盖以下主题:

  • 非可实例化类型
  • 堆污染
  • 具有非可实例化形式参数的可变参数方法的潜在漏洞
  • 防止具有非可实例化形式参数的可变参数方法产生警告

非可实例化类型

可实例化 类型是一种在运行时完全可用的类型信息的类型。这包括原始类型、非泛型类型、原始类型和未绑定通配符的调用。

非可实例化类型 是在编译时通过类型擦除删除了信息的类型 —— 未定义为未限定通配符的泛型类型的调用。非可实例化类型在运行时不具备所有信息。非可实例化类型的示例包括 ListList;JVM 无法在运行时区分这些类型。如 泛型的限制 所示,有一些情况下不能使用非可实例化类型:例如,在 instanceof 表达式中,或作为数组中的元素。

堆污染

堆污染 发生在参数化类型的变量引用不是该参数化类型的对象时。如果程序执行了一些操作导致在编译时产生未经检查的警告,则会出现这种情况。如果在编译时(在编译时类型检查规则的限制范围内)或在运行时无法验证涉及参数化类型的操作的正确性(例如,强制转换或方法调用),则会生成 未经检查的警告。例如,当混合使用原始类型和参数化类型,或执行未经检查的强制转换时,就会发生堆污染。

在正常情况下,当所有代码同时编译时,编译器会发出未经检查的警告,以引起您对潜在的堆污染的注意。如果您分别编译代码的各个部分,很难检测到堆污染的潜在风险。如果确保您的代码在没有警告的情况下编译通过,那么就不会发生堆污染。

具有非可实例化形式参数的可变参数方法的潜在漏洞

包含可变参数输入参数的泛型方法可能导致堆污染。

考虑以下ArrayBuilder类:

public class ArrayBuilder {
  public static <T> void addToList (List<T> listArg, T... elements) {
    for (T x : elements) {
      listArg.add(x);
    }
  }
  public static void faultyMethod(List<String>... l) {
    Object[] objectArray = l;     // Valid
    objectArray[0] = Arrays.asList(42);
    String s = l[0].get(0);       // ClassCastException thrown here
  }
}

以下示例HeapPollutionExample使用了ArrayBuiler类:

public class HeapPollutionExample {
  public static void main(String[] args) {
    List<String> stringListA = new ArrayList<String>();
    List<String> stringListB = new ArrayList<String>();
    ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");
    ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve");
    List<List<String>> listOfStringLists =
      new ArrayList<List<String>>();
    ArrayBuilder.addToList(listOfStringLists,
      stringListA, stringListB);
    ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
  }
}

编译时,ArrayBuilder.addToList方法的定义会产生以下警告:

warning: [varargs] Possible heap pollution from parameterized vararg type T

当编译器遇到可变参数方法时,它将可变参数形式参数转换为数组。然而,Java 编程语言不允许创建参数化类型的数组。在方法ArrayBuilder.addToList中,编译器将可变参数形式参数T... elements转换为形式参数T[] elements,一个数组。然而,由于类型擦除,编译器将可变参数形式参数转换为Object[] elements。因此,存在堆污染的可能性。

以下语句将可变参数形式参数l赋给Object数组objectArgs

Object[] objectArray = l;

这个语句可能会引入堆污染。一个与可变参数形式参数l的参数化类型不匹配的值可以赋给变量objectArray,从而可以赋给l。然而,在这个语句中,编译器并不生成未经检查的警告。编译器在将可变参数形式参数List... l翻译为形式参数List[] l时已经生成了警告。这个语句是有效的;变量l的类型是List[],它是Object[]的子类型。

因此,如果您将任何类型的List对象分配给objectArray数组的任何数组组件,编译器不会发出警告或错误,如下所示:

objectArray[0] = Arrays.asList(42);

这个语句将包含一个类型为Integer的对象的List对象分配给objectArray数组的第一个数组组件。

假设您使用以下语句调用ArrayBuilder.faultyMethod

ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));

在运行时,JVM 在以下语句处抛出ClassCastException

// ClassCastException thrown here
String s = l[0].get(0);

存储在变量l的第一个数组组件中的对象的类型是List,但这个语句期望的是类型为List的对象。

防止具有非可重复形式参数的可变参数方法产生警告

如果您声明一个具有参数化类型参数的可变参数方法,并确保方法体不会因为对可变参数形式参数的不当处理而抛出ClassCastException或其他类似异常,您可以通过在静态和非构造方法声明中添加以下注解来防止编译器为这些类型的可变参数方法生成警告:

@SafeVarargs

@SafeVarargs注解是方法契约的一部分;此注解断言方法的实现不会不当处理可变参数形式参数。

也可以通过在方法声明中添加以下内容来抑制此类警告,尽管这种做法不太理想:

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

然而,这种方法并不会抑制从方法调用点生成的警告。如果你对@SuppressWarnings语法不熟悉,请参见 Annotations。

泛型的限制。

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

要有效地使用 Java 泛型,您必须考虑以下限制:

  • 不能用原始类型实例化泛型类型。
  • 不能创建类型参数的实例。
  • 不能声明其类型为类型参数的静态字段。
  • 不能在参数化类型中使用强制类型转换或instanceof
  • 不能创建参数化类型的数组。
  • 不能创建、捕获或抛出参数化类型的对象。
  • 不能重载形式参数类型擦除为相同原始类型的方法。

不能用原始类型实例化泛型类型。

考虑以下参数化类型:

class Pair<K, V> {
    private K key;
    private V value;
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    // ...
}

在创建Pair对象时,您不能用原始类型替换类型参数KV

Pair<int, char> p = new Pair<>(8, 'a');  // compile-time error

您只能用非原始类型替换类型参数KV

Pair<Integer, Character> p = new Pair<>(8, 'a');

请注意,Java 编译器会将8自动装箱为Integer.valueOf(8),将'a'自动装箱为Character('a')

Pair<Integer, Character> p = new Pair<>(Integer.valueOf(8), new Character('a'));

有关自动装箱的更多信息,请参见自动装箱和拆箱中的数字和字符串课程。

不能创建类型参数的实例。

你不能创建一个类型参数的实例。例如,以下代码会导致编译时错误:

public static <E> void append(List<E> list) {
    E elem = new E();  // compile-time error
    list.add(elem);
}

作为一种解决方法,您可以通过反射创建一个类型参数的对象:

public static <E> void append(List<E> list, Class<E> cls) throws Exception {
    E elem = cls.newInstance();   // OK
    list.add(elem);
}

您可以按以下方式调用append方法:

List<String> ls = new ArrayList<>();
append(ls, String.class);

不能声明其类型为类型参数的静态字段。

类的静态字段是所有非静态对象共享的类级变量。因此,不允许类型参数的静态字段。考虑以下类:

public class MobileDevice<T> {
    private static T os;
    // ...
}

如果允许类型参数的静态字段,则以下代码将会混淆:

MobileDevice<Smartphone> phone = new MobileDevice<>();
MobileDevice<Pager> pager = new MobileDevice<>();
MobileDevice<TabletPC> pc = new MobileDevice<>();

因为静态字段osphonepagerpc共享,os的实际类型是什么?它不能同时是SmartphonePagerTabletPC。因此,您不能创建类型参数的静态字段。

不能在参数化类型中使用强制类型转换或instanceof

因为 Java 编译器会擦除泛型代码中的所有类型参数,所以无法在运行时验证泛型类型的参数化类型:

public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<Integer>) {  // compile-time error
        // ...
    }
}

传递给rtti方法的参数化类型集合为:

S = { ArrayList<Integer>, ArrayList<String> LinkedList<Character>, ... }

运行时不会跟踪类型参数,因此无法区分ArrayListArrayList之间的区别。您最多可以使用无界通配符来验证列表是否是ArrayList

public static void rtti(List<?> list) {
    if (list instanceof ArrayList<?>) {  // OK; instanceof requires a reifiable type
        // ...
    }
}

通常情况下,除非使用无界通配符进行参数化,否则不能进行参数化类型的强制转换。例如:

List<Integer> li = new ArrayList<>();
List<Number>  ln = (List<Number>) li;  // compile-time error

但是,在某些情况下,编译器知道类型参数始终有效并允许强制转换。例如:

List<String> l1 = ...;
ArrayList<String> l2 = (ArrayList<String>)l1;  // OK

不能创建参数化类型的数组。

你不能创建参数化类型的数组。例如,以下代码无法编译:

List<Integer>[] arrayOfLists = new List<Integer>[2];  // compile-time error

以下代码说明了当不同类型插入数组时会发生什么:

Object[] strings = new String[2];
strings[0] = "hi";   // OK
strings[1] = 100;    // An ArrayStoreException is thrown.

如果你尝试对一个泛型列表做同样的事情,会出现问题:

Object[] stringLists = new List<String>[2];  // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>();   // OK
stringLists[1] = new ArrayList<Integer>();  // An ArrayStoreException should be thrown,
    

如果允许参数化列表的数组,上述代码将无法抛出期望的ArrayStoreException

无法创建、捕获或抛出参数化类型的对象

一个泛型类不能直接或间接地扩展Throwable类。例如,以下类将无法编译:

// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ }    // compile-time error
// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // compile-time error

一个方法无法捕获类型参数的实例:

public static <T extends Exception, J> void execute(List<J> jobs) {
    try {
        for (J job : jobs)
            // ...
    } catch (T e) {   // compile-time error
        // ...
    }
}

但是,你可以在throws子句中使用类型参数:

class Parser<T extends Exception> {
    public void parse(File file) throws T {     // OK
        // ...
    }
}

无法重载形式参数类型擦除为相同原始类型的方法

一个类不能有两个在类型擦除后具有相同签名的重载方法。

public class Example {
    public void print(Set<String> strSet) { }
    public void print(Set<Integer> intSet) { }
}

这些重载将共享相同的类文件表示,并将生成编译时错误。

相关文章
|
9天前
|
Java 测试技术 Python
《手把手教你》系列技巧篇(二十九)-java+ selenium自动化测试- Actions的相关操作上篇(详解教程)
【4月更文挑战第21天】本文介绍了Selenium中处理特殊测试场景的方法,如鼠标悬停。Selenium的Actions类提供了鼠标悬停功能,用于模拟用户在网页元素上的悬停行为。文中通过实例展示了如何使用Actions悬停并展开下拉菜单,以及在搜索时选择自动补全的字段。代码示例包括了打开百度首页,悬停在“更多”元素上显示下拉菜单并点击“音乐”,以及在搜索框输入关键词并自动补全的过程。
33 0
|
1天前
|
Java 测试技术 Python
《手把手教你》系列技巧篇(三十六)-java+ selenium自动化测试-单选和多选按钮操作-番外篇(详解教程)
【4月更文挑战第28天】本文简要介绍了自动化测试的实战应用,通过一个在线问卷调查(&lt;https://www.sojump.com/m/2792226.aspx/&gt;)为例,展示了如何遍历并点击问卷中的选项。测试思路包括找到单选和多选按钮的共性以定位元素,然后使用for循环进行点击操作。代码设计方面,提供了Java+Selenium的示例代码,通过WebDriver实现自动答题。运行代码后,可以看到控制台输出和浏览器的相应动作。文章最后做了简单的小结,强调了本次实践是对之前单选多选操作的巩固。
9 0
|
2天前
|
Java 测试技术 项目管理
Java基础教程(22)-构建工具Maven的基本使用
【4月更文挑战第22天】Maven是Java项目管理及构建工具,简化构建、测试、打包和部署等任务。遵循约定优于配置原则,核心是`pom.xml`配置文件,用于管理依赖和项目信息。安装涉及下载、解压、配置环境变量。在IDEA中使用Maven创建项目,通过`pom.xml`添加依赖和管理版本。常用命令包括`clean`、`compile`、`test`、`package`、`install`和`deploy`。IDEA支持直接执行这些命令。
|
2天前
|
NoSQL Java 关系型数据库
Java基础教程(21)-Java连接MongoDB
【4月更文挑战第21天】MongoDB是开源的NoSQL数据库,强调高性能和灵活性。Java应用通过MongoDB Java驱动与之交互,涉及MongoClient、MongoDatabase、MongoCollection和Document等组件。连接MongoDB的步骤包括:配置连接字符串、创建MongoClient、选择数据库和集合。伪代码示例展示了如何建立连接、插入和查询数据。
|
3天前
|
存储 前端开发 测试技术
《手把手教你》系列技巧篇(三十五)-java+ selenium自动化测试-单选和多选按钮操作-下篇(详解教程)
【4月更文挑战第27天】本文介绍了使用Java+Selenium进行Web自动化测试时,如何遍历和操作多选按钮的方法。文章分为两个部分,首先是一个本地HTML页面的示例,展示了多选按钮的HTML代码和页面效果,并详细解释了遍历多选按钮的思路:找到所有多选按钮的共同点,通过定位这些元素并放入list容器中,然后使用for循环遍历并操作。 第二部分介绍了在JQueryUI网站上的实战,给出了被测网址,展示了代码设计,同样使用了findElements()方法获取所有多选按钮并存储到list中,然后遍历并进行点击操作。最后,文章对整个过程进行了小结,并推荐了作者的其他自动化测试教程资源。
11 0
|
3天前
|
Java 关系型数据库 MySQL
Java基础教程(20)-Java连接mysql数据库CURD
【4月更文挑战第19天】MySQL是流行的关系型数据库管理系统,支持SQL语法。在IDEA中加载jar包到项目类路径:右击项目,选择“Open Module Settings”,添加库文件。使用JDBC连接MySQL,首先下载JDBC驱动,然后通过`Class.forName()`加载驱动,`DriverManager.getConnection()`建立连接。执行CRUD操作,例如创建表、插入数据和查询,使用`Statement`或`PreparedStatement`,并确保正确关闭数据库资源。
|
3天前
|
设计模式 算法 Java
Java基础教程(19)-设计模式简述
【4月更文挑战第19天】设计模式是软件设计中反复使用的代码设计经验,旨在提升代码的可重用性、可扩展性和可维护性。23种模式分为创建型、结构型和行为型三类。创建型模式如工厂方法、抽象工厂、建造者、原型和单例,关注对象创建与使用的分离。结构型模式涉及对象组合,如适配器、装饰器、外观等,增强结构灵活性。行为型模式专注于对象间职责分配和算法合作,包括责任链、命令、观察者等。设计模式提供标准化解决方案,促进代码交流和复用。
|
4天前
|
前端开发 测试技术 Python
《手把手教你》系列技巧篇(三十三)-java+ selenium自动化测试-单选和多选按钮操作-上篇(详解教程)
【4月更文挑战第25天】本文介绍了自动化测试中如何处理单选和多选按钮的操作,包括它们的定义、HTML代码示例以及如何判断和操作这些元素。文章通过一个简单的HTML页面展示了单选和多选框的示例,并提供了Java+Selenium实现的代码示例,演示了如何检查单选框是否选中以及如何进行全选操作。
11 0
|
5天前
|
网络协议 Java 网络架构
Java基础教程(18)-Java中的网络编程
【4月更文挑战第18天】Java网络编程简化了底层协议处理,利用Java标准库接口进行TCP/IP通信。TCP协议提供可靠传输,常用于HTTP、SMTP等协议;UDP协议则更高效但不保证可靠性。在TCP编程中,ServerSocket用于监听客户端连接,Socket实现双进程间通信。UDP编程中,DatagramSocket处理无连接的数据报文。HTTP编程可以通过HttpURLConnection发送请求并接收响应。
|
6天前
|
前端开发 Java 测试技术
《手把手教你》系列技巧篇(三十二)-java+ selenium自动化测试-select 下拉框(详解教程)
【4月更文挑战第24天】本文介绍了在自动化测试中处理HTML下拉选择(select)的方法。使用Selenium的Select类,可以通过index、value或visible text三种方式选择选项,并提供了相应的取消选择的方法。此外,文章还提供了一个示例HTML页面(select.html)和相关代码实战,演示了如何使用Selenium进行选择和取消选择操作。最后,文章提到了现代网页中类似下拉框的新设计,如12306网站的出发地选择,并给出了相应的代码示例,展示了如何定位并选择特定选项。
16 0