百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

Java修炼终极指南:42. 举例说明擦除与重载

toyiye 2024-07-15 01:29 7 浏览 0 评论


在通过一个示例来讨论它们之前,让我们先分别快速了解一下擦除和重载。

擦除概述

Java在编译时使用类型擦除来强制类型约束和与旧字节码的向后兼容性。基本上,在编译时,所有的类型参数都被替换为Object(任何泛型都必须可以转换为Object)或类型边界(extends或super)。接下来,在运行时,编译器擦除的类型将被我们的类型替换。类型擦除的一个常见情况涉及泛型。

泛型类型的擦除

实际上,编译器将无界类型(如E、T、U等)用有界Object替换。这通过以下类型擦除的类示例来强制类型安全:

public class ImmutableStack<E> implements Stack<E> {  
  private final E head;  
  private final Stack<E> tail;  
  // ...


编译器应用类型擦除将E替换为Object:

public class ImmutableStack<Object> implements Stack<Object> {  
  private final Object head;  
  private final Stack<Object> tail;  
  // ...


如果E参数被绑定,则编译器使用第一个绑定类。例如,在类`class Node<T extends Comparable<T>> {...}`中,编译器会将T替换为Comparable。同样,在类`class Computation<T extends Number> {...}`中,T的所有出现都将被编译器替换为上限Number。

检查以下情况,这是方法类型擦除的经典案例:

public static <T, R extends T> List<T> listOf(T t, R r) {  
        
  List<T> list = new ArrayList<>();  
        
  list.add(t);  
  list.add(r);        
        
  return list;  
}  
// 使用这个方法  
List<Object> list = listOf(1, "one");


这是如何工作的?当我们调用`listOf(1, "one")`时,我们实际上是将两种不同的类型传递给了泛型参数T和R。编译器的类型擦除将T替换为Object。这样,我们就可以在ArrayList中插入不同的类型,代码运行得很好。

擦除和桥接方法

桥接方法是编译器为覆盖边缘情况而创建的。具体来说,当编译器遇到参数化接口的实现或参数化类的扩展时,它可能需要生成一个桥接方法(也称为合成方法),作为类型擦除阶段的一部分。例如,考虑以下参数化类:

public class Puzzle<E> {  
  public E piece;  
  public Puzzle(E piece) {  
    this.piece = piece;  
  }  
  public void setPiece(E piece) {         
    this.piece = piece;  
  }  
}


以及这个类的扩展:

public class FunPuzzle extends Puzzle<String> {  
  public FunPuzzle(String piece) {  
    super(piece);  
  }  
  @Override  
  public void setPiece(String piece) {         
    super.setPiece(piece);  
  }  
}


类型擦除将`Puzzle.setPiece(E)`修改为`Puzzle.setPiece(Object)`。这意味着`FunPuzzle.setPiece(String)`方法并没有重写`Puzzle.setPiece(Object)`方法。由于方法的签名不兼容,编译器必须通过桥接(合成)方法来适应泛型类型的多态性,以确保子类型按预期工作。让我们在代码中突出显示这个方法:

/* Decompiler 8ms, total 3470ms, lines 18 */  
package modern.challenge;  
public class FunPuzzle extends Puzzle<String> {  
   public FunPuzzle(String piece) {  
      super(piece);  
   }  
   public void setPiece(String piece) {  
      super.setPiece(piece);  
   }  
   // $FF: synthetic method  
   // $FF: bridge method  
   public void setPiece(Object var1) {  
      this.setPiece((String)var1);  
   }  
}


现在,每当你在堆栈跟踪中看到桥接方法时,你就知道它是什么以及为什么在那里。

类型擦除和堆污染

你是否见过未检查的警告?我确定你见过!这是所有Java开发人员都会遇到的问题之一。它们可能在编译时作为类型检查的结果出现,或在运行时作为类型转换或方法调用的结果出现。在这两种情况下,我们谈论的是编译器无法验证涉及某些参数化类型的操作的正确性。并非每个未检查的警告都是危险的,但确实存在我们必须考虑和处理的情况。

堆污染的一个特例是,当某个类型的参数化变量指向一个不是该类型的对象时,我们就容易遇到导致堆污染的代码。涉及varargs参数的方法的一个很好的候选者是这样的场景。检查以下代码:

public static <T> void listOf(List<T> list, T... ts) {  
  list.addAll(Arrays.asList(ts));     
}


listOf()的声明将导致此警告:可能的堆污染来自参数化varargs类型T。那么这里发生了什么?故事从编译器将形式T...参数替换为数组开始。在应用类型擦除后,T...参数变为T[],并最终变为Object[]。因此,我们为可能的堆污染打开了一扇门。但是,我们的代码只是将Object[]的元素添加到了List<Object>中,所以我们处于安全区域。

换句话说,如果你知道varargs方法的主体不容易生成特定异常(例如,ClassCastException)或在不适当的操作中使用varargs参数,那么我们可以指示编译器抑制这些警告。我们可以通过@SafeVarargs注解来实现这一点,如下所示:

@SafeVarargs  
public static <T> void listOf(List<T> list, T... ts) { … }


@SafeVarargs是一个提示,表明被注解的方法将仅在适当的操作中使用varargs形式参数。更常见但不太推荐的是使用@SuppressWarnings({"unchecked", "varargs"}),它只是简单地抑制此类警告,而不声明varargs形式参数不会在不适当的操作中使用。

现在,让我们来处理这段代码:

public static void main(String[] args) {  
  List<Integer> ints = new ArrayList<>();  
  Main.listOf(ints, 1, 2, 3);  
         
  Main.listsOfYeak(ints);  
}  
  
public static void listsOfYeak(List<Integer>... lists) {  
         
  Object[] listsAsArray = lists;      
     
  listsAsArray[0] = Arrays.asList(4, 5, 6);         
  Integer someInt = lists[0].get(0);    
     
  listsAsArray[0] = Arrays.asList("a", "b", "c");     
  Integer someIntYeak = lists[0].get(0); // ClassCastException  
}


这次,类型擦除将List<Integer>...转换为List[],这是Object[]的子类型。这允许我们进行赋值:`Object[] listsAsArray = lists;`。但是,请查看最后两行代码,其中我们创建了一个List<String>并将其存储在`listsAsArray[0]`中。在最后一行,我们尝试从`lists[0]`访问第一个Integer,这显然会导致ClassCastException。这是使用varargs的不当操作,因此不建议在这种情况下使用@SafeVarargs。我们应该认真对待以下警告:

// unchecked generic array creation for varargs parameter  
// of type java.util.List<java.lang.Integer>[]  
Main.listsOfYeak(ints);  
// Possible heap pollution from parameterized vararg  
// type java.util.List<java.lang.Integer>  
public static void listsOfYeak(List<Integer>... lists) { … }


现在,你已经熟悉了类型擦除,让我们简要地介绍一下多态重载。

多态重载概述

由于重载(也称为“即席”多态性)是面向对象编程(OOP)的核心概念,我相信你对Java方法重载很熟悉,所以我不会坚持这个概念的基本理论。

我也知道有些人不同意重载是一种多态形式,但那是另一个不会在这里讨论的话题。我们将更加实际,并跳入一系列旨在突出重载某些有趣方面的测验。更具体地说,我们将讨论类型优势。让我们处理第一个测验(wordie是一个最初为空的字符串):

static void kaboom(byte b) { wordie += "a";}    
static void kaboom(short s) { wordie += "b";}    
kaboom(1);


会发生什么?如果你回答编译器会指出找不到适合kaboom(1)的方法,那么你是对的。编译器寻找一个接受整数参数的方法,kaboom(int)。好的,这很简单!下一个:

static void kaboom(byte b) { wordie += "a";}    
static void kaboom(short s) { wordie += "b";}   
static void kaboom(long l) { wordie += "d";}     
static void kaboom(Integer i) { wordie += "i";}    
kaboom(1);


我们知道前两个kaboom()是无用的。那么kaboom(long)和kaboom(Integer)呢?你是对的,将调用kaboom(long)。如果我们删除kaboom(long),则调用kaboom(Integer)。

在原始类型重载中,编译器首先寻找一对一的匹配。如果这种尝试失败,编译器将寻找接受比当前域更广泛的原始域的重载版本(例如,对于int,它寻找int、long、float或double)。如果这也失败,编译器将检查接受装箱类型(Integer、Float等)的重载。

以下面的情况为例:

static void kaboom(Integer i) { wordie += "i";}    
static void kaboom(Long l) { wordie += "j";}    
kaboom(1);


这次,wordie将会是"i"。调用了kaboom(Integer),因为不存在kaboom(int/long/float/double)。如果我们有一个kaboom(double),则该方法将比kaboom(Integer)具有更高的优先级。

在装箱类型重载中,编译器首先寻找一对一的匹配。如果失败,编译器将不会考虑任何接受比当前域更广泛的装箱类型的重载版本(当然,更狭窄的域也被忽略)。它查找Number作为所有装箱类型的超类。如果找不到Number,编译器将向上遍历层次结构,直到达到java.lang.Object,这是道路的尽头。

现在让我们稍微复杂一点:

static void kaboom(Object... ov) { wordie += "o";}    
static void kaboom(Number n) { wordie += "p";}    
static void kaboom(Number... nv) { wordie += "q";}      
kaboom(1);


这次,哪个方法将被调用?你可能会想到kaboom(Number),对吧?至少,我的简单逻辑让我认为这是一个常识性的选择。而且它是正确的!如果我们移除kaboom(Number),编译器将调用varargs方法kaboom(Number...)。这是有道理的,因为kaboom(1)使用了一个参数,所以kaboom(Number)应该比kaboom(Number...)具有更高的优先级。但是,如果我们调用kaboom(1,2,3),逻辑就会反转,因为kaboom(Number)不再代表这次调用的有效重载,而kaboom(Number...)是正确的选择。

但是,这种逻辑之所以适用,是因为Number是所有装箱类(Integer、Double、Float等)的超类。现在考虑以下情况:

static void kaboom(Object... ov) { wordie += "o";}    
static void kaboom(File... fv) { wordie += "s";}    
kaboom(1);


这次,编译器将“绕过”kaboom(File...)并调用kaboom(Object...)。基于相同的逻辑,调用kaboom(1, 2, 3)也会调用kaboom(Object...),因为没有kaboom(Number...)。

在重载中,如果调用具有单个参数,则具有单个参数的方法比其varargs对应项具有更高的优先级。另一方面,如果调用具有多个相同类型的参数,则调用varargs方法,因为具有单个参数的方法不再适用。当调用具有单个参数但只有varargs重载可用时,将调用该方法。

这导致了以下示例:

static void kaboom(Number... nv) { wordie += "q";}    
static void kaboom(File... fv) { wordie += "s";}    
kaboom();


这次,kaboom()没有参数,编译器无法找到唯一匹配项。这意味着对kaboom()的引用是模糊的,因为两个方法都匹配(modern.challenge.Main中的kaboom(java.lang.Number...)和modern.challenge.Main中的kaboom(java.io.File...))。

在捆绑的代码中,你可以进一步探索多态重载并测试你的知识。此外,尝试挑战自己,并在等式中引入泛型。

擦除与重载

现在,基于之前的经验,检查以下代码:

void print(List<A> listOfA) {  
  System.out.println("Printing A: " + listOfA);  
}  
    
void print(List<B> listofB) {  
  System.out.println("Printing B: " + listofB);  
}


会发生什么?这是一个擦除和重载冲突的情况。类型擦除将List<A>替换为List<Object>,并将List<B>替换为List<Object>。因此,重载是不可能的,我们会得到一个错误,如“名称冲突:print(java.util.List<modern.challenge.B>)和print(java.util.List<modern.challenge.A>)具有相同的擦除。”

为了解决这个问题,我们可以向这两个方法中的一个添加一个虚拟参数:

void print(List<A> listOfA, Void... v) {  
  System.out.println("Printing A: " + listOfA);  
}


现在,我们可以对这两个方法进行相同的调用:

new Main().print(List.of(new A(), new A()));  
new Main().print(List.of(new B(), new B())); // 注意:这里实际上会编译错误,因为第二个print方法已被修改


但是请注意,上面的第二个调用实际上会导致编译错误,因为我们修改了第二个print方法以接受一个额外的Void...参数。通常,为了处理类型擦除导致的重载问题,你会通过改变方法名称、添加不同类型的参数或使用其他技术来区分它们。

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码