Java 编程问题:四、类型推断

简介: Java 编程问题:四、类型推断

本章包括 21 个涉及 JEP286 或 Java 局部变量类型推断LVTI)的问题,也称为var类型。这些问题经过精心设计,以揭示最佳实践和使用var时所涉及的常见错误。到本章结束时,您将了解到将var推向生产所需的所有知识。



问题


使用以下问题来测试您的类型推断编程能力。我强烈建议您在使用解决方案和下载示例程序之前,先尝试一下每个问题:


简单var示例:编写一个程序,举例说明类型推断(var)在代码可读性方面的正确用法。


将var与原始类型结合使用:编写一个程序,举例说明将var与 Java 原始类型(int、long、float、double结合使用。


使用var和隐式类型转换来维持代码的可维护性:编写一个程序,举例说明var和隐式类型转换如何维持代码的可维护性。


显式向下转换或更好地避免var:编写一个程序,举例说明var和显式向下转换的组合,并解释为什么要避免var。


如果被调用的名称没有包含足够的人性化类型信息,请避免使用var:请举例说明应避免使用var,因为它与被调用的名称的组合会导致人性化信息的丢失。


结合 LVTI 和面向接口编程技术:编写一个程序,通过面向接口编程技术来举例说明var的用法。


结合 LVTI 和菱形运算符:编写一个程序,举例说明var和菱形运算符的用法。


使用var分配数组:编写一个将数组分配给var的程序。


在复合声明中使用 LVTI:解释并举例说明 LVTI 在复合声明中的用法。


LVTI 和变量范围:解释并举例说明为什么 LVTI 应该尽可能地缩小变量的范围。


LVTI 和三元运算符:编写几个代码片段,举例说明 LVTI 和三元运算符组合的优点。


LVTI 和for循环:写几个例子来举例说明 LVTI 在for循环中的用法。


LVTI 和流:编写几个代码片段,举例说明 LVTI 和 Java 流的用法。


使用 LVTI 分解嵌套的/大的表达式链:编写一个程序,举例说明如何使用 LVTI 分解嵌套的/大的表达式链。


LVTI 和方法返回和参数类型:编写几个代码片段,举例说明 LVTI 和 Java 方法在返回和参数类型方面的用法。


LVTI 和匿名类:编写几个代码片段,举例说明 LVTI 在匿名类中的用法。


LVTI 可以是final和有效的final:写几个代码片段,举例说明 LVTI 如何用于final和有效的final变量。


LVTI 和 Lambda:通过几个代码片段解释如何将 LVTI 与 Lambda 表达式结合使用。


LVTI 和null初始化器、实例变量和catch块变量:举例说明如何将 LVTI 与null初始化器、实例变量和catch块结合使用。


LVTI 和泛型类型T:编写几个代码片段,举例说明如何将 LVTI 与泛型类型结合使用。


LVTI、通配符、协变和逆变:编写几个代码片段,举例说明如何将 LVTI 与通配符、协变和逆变结合使用。




解决方案


以下各节介绍上述问题的解决方案。记住,通常没有一个正确的方法来解决一个特定的问题。另外,请记住,这里显示的解释仅包括解决问题所需的最有趣和最重要的细节。您可以下载示例解决方案以查看更多详细信息并尝试程序



78 简单var示例


从版本 10 开始,Java 附带了 JEP286 或 JavaLVTI,也称为var类型。


var标识符不是 Java 关键字,而是保留类型名。


这是一个 100% 编译特性,在字节码、运行时或性能方面没有任何副作用。简而言之,LVTI 应用于局部变量,其工作方式如下:编译器检查右侧并推断出实类型(如果右侧是一个初始化器,则使用该类型)。


此功能可确保编译时安全。这意味着我们不能编译一个试图实现错误赋值的应用。如果编译器已经推断出var的具体/实际类型,我们只能赋值该类型的值。


LVTI 有很多好处;例如,它减少了代码的冗长,减少了冗余和样板代码。此外,LVTI 可以减少编写代码所花的时间,特别是在涉及大量声明的情况下,如下所示:


// without var
Map<Boolean, List<Integer>> evenAndOddMap...
// with var
var evenAndOddMap = ...

一个有争议的优点是代码可读性。一些声音支持使用var会降低代码可读性,而另一些声音则支持相反的观点。根据用例的不同,它可能需要在可读性上进行权衡,但事实是,通常情况下,我们非常关注字段(实例变量)的有意义的名称,而忽略了局部变量的名称。例如,让我们考虑以下方法:

public Object fetchTransferableData(String data)
    throws UnsupportedFlavorException, IOException {
  StringSelection ss = new StringSelection(data);
  DataFlavor[] df = ss.getTransferDataFlavors();
  Object obj = ss.getTransferData(df[0]);
  return obj;
}



这是一个简短的方法;它有一个有意义的名称和一个干净的实现。但是检查局部变量的名称。它们的名称大大减少(它们只是快捷方式),但这不是问题,因为左侧提供了足够的信息,我们可以很容易地理解每个局部变量的类型。现在,让我们使用 LVTI 编写以下代码:

public Object fetchTransferableData(String data)
    throws UnsupportedFlavorException, IOException {
  var ss = new StringSelection(data);
  var df = ss.getTransferDataFlavors();
  var obj = ss.getTransferData(df[0]);
  return obj;
}

显然,代码的可读性降低了,因为现在很难推断出局部变量的类型。如下面的屏幕截图所示,编译器在推断正确的类型方面没有问题,但是对于人类来说,这要困难得多:


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FcuV0SJX-1657077646421)(img/95cb6f1d-d16c-4a89-b68b-50281887697b.png)]

这个问题的解决方案是在依赖 LVTI 时为局部变量提供一个有意义的名称。例如,如果提供了局部变量的名称,代码可以恢复可读性,如下所示:


public Object fetchTransferableData(String data)
    throws UnsupportedFlavorException, IOException {
  var stringSelection = new StringSelection(data);
  var dataFlavorsArray = stringSelection.getTransferDataFlavors();
  var obj = stringSelection.getTransferData(dataFlavorsArray[0]);
  return obj;
}

然而,可读性问题也是由这样一个事实引起的:通常,我们倾向于将类型视为主要信息,将变量名视为次要信息,而这应该是相反的。

让我们再看两个例子来执行上述语句。使用集合(例如,List)的方法如下:

// Avoid
public List<Player> fetchPlayersByTournament(String tournament) {
  var t = tournamentRepository.findByName(tournament);
  var p = t.getPlayers();
  return p;
}
// Prefer
public List<Player> fetchPlayersByTournament(String tournament) {
  var tournamentName = tournamentRepository.findByName(tournament);
  var playerList = tournamentName.getPlayers();
  return playerList;
}

为局部变量提供有意义的名称并不意味着陷入过度命名技术。

例如,通过简单地重复类型名来避免命名变量:

// Avoid
var fileCacheImageOutputStream​ 
  = new FileCacheImageOutputStream​(..., ...);
// Prefer
var outputStream​ = new FileCacheImageOutputStream​(..., ...);
// Or
var outputStreamOfFoo​ = new FileCacheImageOutputStream​(..., ...);




79 对原始类型使用var


将 LVTI 与原始类型(intlongfloatdouble)一起使用的问题是,预期类型和推断类型可能不同。显然,这会导致代码中的混乱和意外行为。


这种情况下的犯罪方是var类型使用的隐式类型转换

例如,让我们考虑以下两个依赖显式原始类型的声明:

boolean valid = true; // this is of type boolean
char c = 'c';         // this is of type char


现在,让我们用 LVTI 替换显式原始类型:

var valid = true; // inferred as boolean
var c = 'c';      // inferred as char


很好!到目前为止没有问题!现在,让我们看看另一组基于显式原始类型的声明:

int intNumber = 10;       // this is of type int
long longNumber = 10;     // this is of type long
float floatNumber = 10;   // this is of type float, 10.0
double doubleNumber = 10; // this is of type double, 10.0


让我们按照第一个示例中的逻辑,用 LVTI 替换显式原始类型:

// Avoid
var intNumber = 10;    // inferred as int
var longNumber = 10;   // inferred as int
var floatNumber = 10;  // inferred as int
var doubleNumber = 10; // inferred as int

根据以下屏幕截图,所有四个变量都被推断为整数:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JJrzLR4d-1657077646422)(img/ec6aa8f3-a41c-4cc5-bff2-9d08083bcc91.png)]

这个问题的解决方案包括使用显式 Java 字面值:


// Prefer
var intNumber = 10;     // inferred as int
var longNumber = 10L;   // inferred as long
var floatNumber = 10F;  // inferred as float, 10.0
var doubleNumber = 10D; // inferred as double, 10.0


最后,让我们考虑一个带小数的数字的情况,如下所示:

var floatNumber = 10.5; // inferred as double


变量名表明10.5float,但实际上是推断为double。因此,即使是带小数的数字(尤其是带小数的数字),也建议使用字面值

var floatNumber = 10.5F; // inferred as float





80 使用var隐式类型转换来维持代码的可维护性


在上一节中,“将var与原始类型结合使用”,我们看到将var隐式类型转换结合使用会产生实际问题。但在某些情况下,这种组合可能是有利的,并维持代码的可维护性。


让我们考虑以下场景,我们需要编写一个方法,该方法位于名为ShoppingAddicted的外部 API 的两个现有方法之间(通过推断,这些方法可以是两个 Web 服务、端点等)。有一种方法专门用于返回给定购物车的最佳价格。基本上,这种方法需要一堆产品,并查询不同的在线商店,以获取最佳价格。

结果价格返回为int。此方法的存根如下所示:


public static int fetchBestPrice(String[] products) {
  float realprice = 399.99F; // code to query the prices in stores
  int price = (int) realprice;
  return price;
}


另一种方法将价格作为int接收并执行支付。如果支付成功,则返回true

public static boolean debitCard(int amount) {
  return true;
}


现在,通过对该代码进行编程,我们的方法将充当客户端,如下所示(客户可以决定购买哪些商品,我们的代码将为他们返回最佳价格并相应地借记卡):

// Avoid
public static boolean purchaseCart(long customerId) {
  int price = ShoppingAddicted.fetchBestPrice(new String[0]);
  boolean paid = ShoppingAddicted.debitCard(price);
  return paid;
}


但是过了一段时间,ShoppingAddictedAPI 的拥有者意识到他们通过将实际价格转换成int来赔钱(例如,实际价格是 399.99,但在int形式中,它是 399.0,这意味着损失 99 美分)。因此,他们决定放弃这种做法,将实际价格返回为float

public static float fetchBestPrice(String[] products) {
  float realprice = 399.99F; // code to query the prices in stores
  return realprice;
}

因为返回的价格是float,所以debitCard()也会更新:

public static boolean debitCard(float amount) {
  return true;
}



但是,一旦我们升级到新版本的ShoppingAddictedAPI,代码将失败,并有可能从float到int异常的有损转换。这是正常的,因为我们的代码需要int。由于我们的代码不能很好地容忍这些修改,因此需要相应地修改代码。


然而,如果我们已经预见到这种情况,并且使用了var而不是int,那么由于隐式类型转换,代码将不会出现问题:


// Prefer
public static boolean purchaseCart(long customerId) {
  var price = ShoppingAddicted.fetchBestPrice(new String[0]);
  var paid = ShoppingAddicted.debitCard(price);
  return paid;
}




81 显式向下转换或更好地避免var


在“将var与原始类型结合使用”一节中,我们讨论了将字面值与原始类型结合使用(int、long、float和double来避免隐式类型转换带来的问题。但并非所有 Java 原始类型都可以利用字面值。在这种情况下,最好的方法是避免使用var。但让我们看看为什么!


检查以下关于byteshort变量的声明:

byte byteNumber = 25;     // this is of type byte
short shortNumber = 1463; // this is of type short


如果我们用var替换显式类型,那么推断的类型将是int

var byteNumber = 25;    // inferred as int
var shortNumber = 1463; // inferred as int


不幸的是,这两种基本类型没有可用的字面值。帮助编译器推断正确类型的唯一方法是依赖显式向下转换:

var byteNumber = (byte) 25;     // inferred as byte
var shortNumber = (short) 1463; // inferred as short

虽然这段代码编译成功并按预期工作,但我们不能说使用var比使用显式类型带来了任何价值。因此,在这种情况下,最好避免var和显式的向下转型。



82 如果被调用的名称没有包含足够的类型信息,请避免使用var


好吧,var不是一颗银弹,这个问题将再次凸显这一点。以下代码片段可以使用显式类型或var编写,而不会丢失信息:

// using explicit types
MemoryCacheImageInputStream is =
  new MemoryCacheImageInputStream(...);
JavaCompiler jc = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fm = compiler.getStandardFileManager(...);

因此,将前面的代码片段迁移到var将产生以下代码(通过从右侧目视检查被调用的名称来选择变量名称):

// using var
var inputStream = new MemoryCacheImageInputStream(...);
var compiler = ToolProvider.getSystemJavaCompiler();
var fileManager = compiler.getStandardFileManager(...);


同样的情况也会发生在过度命名的边界上:

// using var
var inputStreamOfCachedImages = new MemoryCacheImageInputStream(...);
var javaCompiler = ToolProvider.getSystemJavaCompiler();
var standardFileManager = compiler.getStandardFileManager(...);


因此,前面的代码在选择变量的名称和可读性时不会引起任何问题。所谓的名称包含了足够的信息,让人类对var感到舒服。

但让我们考虑以下代码片段:

// Avoid
public File fetchBinContent() {
  return new File(...);
}
// called from another place
// notice the variable name, bin
var bin = fetchBinContent();


对于人类来说,如果不检查名称fetchBinContent()的返回类型,就很难推断出名称返回的类型。根据经验,在这种情况下,解决方案应该避免var并依赖显式类型,因为右侧没有足够的信息让我们为变量选择合适的名称并获得可读性很高的代码:

// called from another place
// now the left-hand side contains enough information
File bin = fetchBinContent();

因此,如果将var与被调用的名称组合使用导致清晰度损失,则最好避免使用var。忽略此语句可能会导致混淆,并会增加理解和/或扩展代码所需的时间。


考虑另一个基于java.nio.channels.Selector类的例子。此类公开了一个名为open()的static方法,该方法返回一个新打开的Selector。但是,如果我们在一个用var声明的变量中捕获这个返回值,我们很可能会认为这个方法可能返回一个boolean,表示打开当前选择器的成功。使用var而不考虑可能的清晰度损失会产生这些问题。像这样的一些问题和代码将成为一个真正的痛苦。



83 LVTI 与面向接口编程技术相结合


Java 最佳实践鼓励我们将代码绑定到抽象。换句话说,我们需要依赖于面向接口编程的技术。


这种技术非常适合于集合声明。例如,建议声明ArrayList如下:

List<String> players = new ArrayList<>();


我们也应该避免这样的事情:

ArrayList<String> players = new ArrayList<>();


通过遵循第一个示例,代码实例化了ArrayList类(或HashSet、HashMap等),但声明了一个List类型的变量(或Set、Map等)。由于List、Set、Map以及更多的都是接口(或契约),因此很容易用List(Set和Map的其他实现来替换实例化,而无需对代码进行后续修改。

不幸的是,LVTI 不能利用面向接口编程技术。换句话说,当我们使用var时,推断的类型是具体的实现,而不是合同。例如,将List<String>替换为var将导致推断类型ArrayList<String>:

// inferred as ArrayList<String>
var playerList = new ArrayList<String>();


然而,有一些解释支持这种行为:


   LVTI 在局部级别(局部变量)起作用,其中面向接口编程技术的的使用少于方法参数/返回类型或字段类型。

   由于局部变量的作用域很小,因此切换到另一个实现所引起的修改也应该很小。切换实现对检测和修复代码的影响应该很小。

   LVTI 将右侧的代码视为一个用于推断实际类型的初始化器。如果将来要修改这个初始化器,那么推断的类型可能不同,这将导致使用此变量的代码出现问题。



84 LVTI 和菱形运算符相结合


根据经验,如果右侧不存在推断预期类型所需的信息,则 LVTI 与菱形运算符结合可能会导致意外的推断类型。


在 JDK7 之前,即 Coin 项目,List<String>将声明如下:

List<String> players = new ArrayList<String>();


基本上,前面的示例显式指定泛型类的实例化参数类型。从 JDK7 开始,Coin 项目引入了菱形操作符,可以推断泛型类实例化参数类型,如下所示:

List<String> players = new ArrayList<>();


现在,如果我们从 LVTI 的角度来考虑这个例子,我们将得到以下结果:

var playerList = new ArrayList<>();


但是现在推断出的类型是什么呢?好吧,我们有一个问题,因为推断的类型将是ArrayList<Object>,而不是ArrayList<String>。解释很明显:推断预期类型(String所需的信息不存在(注意,右侧没有提到String类型)。这指示 LVTI 推断出最广泛适用的类型,在本例中是Object。


但是如果ArrayList<Object>不是我们的意图,那么我们需要一个解决这个问题的方法。解决方案是提供推断预期类型所需的信息,如下所示:

var playerList = new ArrayList<String>();


现在,推断的类型是ArrayList<String>。也可以间接推断类型。请参见以下示例:

var playerStack = new ArrayDeque<String>();
// inferred as ArrayList<String>
var playerList = new ArrayList<>(playerStack);

也可以通过以下方式间接推断:

Player p1 = new Player();
Player p2 = new Player();
var listOfPlayer = List.of(p1, p2); // inferred as List<Player>
// Don't do this!
var listOfPlayer = new ArrayList<>(); // inferred as ArrayList<Object>
listOfPlayer.add(p1);
listOfPlayer.add(p2);
相关文章
|
8天前
|
算法 Java
【编程基础知识】Java打印九九乘法表
本文介绍了在Java中实现九九乘法表的三种方法:嵌套循环、数组和流控制。通过代码示例、流程图和表格对比,帮助读者深入理解每种方法的优缺点,提升编程技能。
31 2
|
8天前
|
存储 Java
【编程基础知识】 分析学生成绩:用Java二维数组存储与输出
本文介绍如何使用Java二维数组存储和处理多个学生的各科成绩,包括成绩的输入、存储及格式化输出,适合初学者实践Java基础知识。
37 1
|
8天前
|
Java 开发者
【编程进阶知识】《Java 文件复制魔法:FileReader/FileWriter 的奇妙之旅》
本文深入探讨了如何使用 Java 中的 FileReader 和 FileWriter 进行文件复制操作,包括按字符和字符数组复制。通过详细讲解、代码示例和流程图,帮助读者掌握这一重要技能,提升 Java 编程能力。适合初学者和进阶开发者阅读。
110 61
|
8天前
|
存储 Java
【编程基础知识】《Java 起航指南:配置 Java 环境变量的秘籍与奥秘》
本文详细介绍了如何配置 Java 环境变量及其重要性,通过具体步骤、代码示例和流程图,帮助初学者轻松掌握 Java 环境变量的设置,为 Java 编程打下坚实基础。关键词:Java、环境变量、配置方法、编程基础。
116 57
|
4天前
|
安全 Java UED
Java中的多线程编程:从基础到实践
本文深入探讨了Java中的多线程编程,包括线程的创建、生命周期管理以及同步机制。通过实例展示了如何使用Thread类和Runnable接口来创建线程,讨论了线程安全问题及解决策略,如使用synchronized关键字和ReentrantLock类。文章还涵盖了线程间通信的方式,包括wait()、notify()和notifyAll()方法,以及如何避免死锁。此外,还介绍了高级并发工具如CountDownLatch和CyclicBarrier的使用方法。通过综合运用这些技术,可以有效提高多线程程序的性能和可靠性。
|
4天前
|
缓存 Java UED
Java中的多线程编程:从基础到实践
【10月更文挑战第13天】 Java作为一门跨平台的编程语言,其强大的多线程能力一直是其核心优势之一。本文将从最基础的概念讲起,逐步深入探讨Java多线程的实现方式及其应用场景,通过实例讲解帮助读者更好地理解和应用这一技术。
22 3
|
4天前
|
Java 开发者
在Java编程中,正确的命名规范不仅能提升代码的可读性和可维护性,还能有效避免命名冲突。
【10月更文挑战第13天】在Java编程中,正确的命名规范不仅能提升代码的可读性和可维护性,还能有效避免命名冲突。本文将带你深入了解Java命名规则,包括标识符的基本规则、变量和方法的命名方式、常量的命名习惯以及如何避免关键字冲突,通过实例解析,助你写出更规范、优雅的代码。
26 3
|
4天前
|
Java 程序员
在Java编程中,关键字不仅是简单的词汇,更是赋予代码强大功能的“魔法咒语”。
【10月更文挑战第13天】在Java编程中,关键字不仅是简单的词汇,更是赋予代码强大功能的“魔法咒语”。本文介绍了Java关键字的基本概念及其重要性,并通过定义类和对象、控制流程、访问修饰符等示例,展示了关键字的实际应用。掌握这些关键字,是成为优秀Java程序员的基础。
12 3
|
4天前
|
Java 程序员 编译器
在Java编程中,保留字(如class、int、for等)是具有特定语法意义的预定义词汇,被语言本身占用,不能用作变量名、方法名或类名。
在Java编程中,保留字(如class、int、for等)是具有特定语法意义的预定义词汇,被语言本身占用,不能用作变量名、方法名或类名。本文通过示例详细解析了保留字的定义、作用及与自定义标识符的区别,帮助开发者避免因误用保留字而导致的编译错误,确保代码的正确性和可读性。
16 3
|
4天前
|
算法 Java
在Java编程中,关键字和保留字是基础且重要的组成部分,正确理解和使用它们
【10月更文挑战第13天】在Java编程中,关键字和保留字是基础且重要的组成部分。正确理解和使用它们,如class、int、for、while等,不仅能够避免语法错误,还能提升代码的可读性和执行效率。本指南将通过解答常见问题,帮助你掌握Java关键字的正确使用方法,以及如何避免误用保留字,使你的代码更加高效流畅。
20 3