使用Java 10的var类型推断的几个注意点!

不加选择地应用var可能会让代码不容易理解,因为模糊了类型这个概念,而人类是依据类型分类进行逻辑思考的,这样就使事情变得更糟,如果使用得当,var可以帮助改进良好的代码,使其更短更清晰,同时不会影响可理解性。

使用var需要通过减少混乱来改进代码,从而使更重要的信息脱颖而出。

本地类型推断功能背后的主要前提非常简单。使用新的保留类型名称'var'替换声明中的显式类型,并推断其类型。所以我们可以替换原来:

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

为:

var outputStream = new ByteArrayOutputStream();

Java现在允许动态类型了吗?绝对不

所有类型推断都在编译时发生,显式类型由编译器烘焙到字节代码中。在运行时,Java与以往一样静态。鉴于使用非常简单,本备忘单将集中在本地类型推断的最重要方面 - 它的实际用途。当您应该使用显式类型以及何时应该考虑类型推断时,它将提供指导。

由于想要编写这个备忘单,Oracle的JDK工程师Stuart Marks写了一篇完美的文章,给出了编码原理和使用类型推理的指导,我将它们浓缩成一张备忘单:

原则

1.阅读代码>编写代码

无论是花10分钟还是10天写一行代码,你几乎肯定会在未来的许多年里阅读它。如果代码清晰,简洁,并且最重要的是包含了解其目的的所有必要信息,那么代码将来只能维护和理解。目标是最大化可理解性。

2.本地推理应明确代码

尽可能多地将信息烘焙到代码中,以避免读者必须查看代码库的不同部分,以便了解正在发生的事情。这可以通过方法或变量命名。

3.代码可读性不应该依赖于IDE

IDE可以很棒。我的意思是真的很棒!它们可以使开发人员的开发更高效或更准确。代码必须易读且易于理解,而不依赖于IDE。通常,代码在IDE外部读取。或者,IDE可能会为读者提供多少信息。代码应该是自我暴露的。它应该是可以理解的,无需工具的帮助。

决定权在你

是否为变量提供显式类型或让Java编译器为自己解决问题的选择是一种权衡。一方面,你想减少杂乱,样板,仪式。另一方面,您不希望损害代码的可理解性。类型声明不是向读者传达信息的唯一方式。其他方法包括变量的名称和初始化表达式。

方法

1.选择提供有用信息的变量名称

一般来说,这是一种很好的做法,但在var的上下文中它更为重要。在var声明中,可以使用变量的名称来传达有关变量含义和用法的信息。用var替换显式类型通常应该伴随着改进变量名。有时,在其名称中对变量的类型进行编码可能很有用。例如:

List<Customer> x = dbconn.executeQuery(query);
 var custList = dbconn.executeQuery(query);

2.最小化局部变量的作用域

当变量的作用域很大时会发生此问题:这意味着变量声明与其用法之间有许多代码行。随着代码的维护,对类型的更改等可能最终会产生不同的行为。例如,从List移动​​到Set可能看起来没问题,但是您的代码是否依赖于稍后在同一范围内的排序?虽然类型总是静态设置,但使用相同接口的实现中的细微差别可能会让您失望。应该更改代码以减少局部变量的作用域,然后用var声明它们,而不是简单地避免在这些情况下使用var。请考虑以下代码:

var items = new HashSet<Item>(...);
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) { ... }

此代码现在有一个bug,因为集合没有定义的迭代顺序。但是,程序员可能会立即修复此错误,因为items变量的使用与其声明相邻。现在,假设此代码是大型方法的一部分,而items变量的作用域相应较大:

var items = new HashSet<Item>(...);
// ... 100 lines of code ...
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) { ... }

这个bug现在变得更难以追踪,因为该行试图将一个项目添加到集合的末尾并不足够接近类型声明以使该bug明显。

3.初始化程序为Reader提供足够的信息时,请考虑Var

局部变量通常用构造函数初始化。正在构造的类的名称通常作为左侧的显式类型显得累赘重复,如果类型名称很长,则使用var可以提供简洁而不会丢失信息:

ByteArrayOutputStream  outputStream  =  new  ByteArrayOutputStream();
 var  outputStream  =  new  ByteArrayOutputStream();

4.使用Var分解具有局部变量的链接或嵌套表达式

看看采用字符串集合并查找最常出现的字符串的代码。这可能如下所示:

return strings.stream()
              .collect(groupingBy(s -> s, counting()))
              .entrySet()
              .stream()
              .max(Map.Entry.comparingByValue())
              .map(Map.Entry::getKey);

此代码是正确的,但在多个语句中更易读。拆分语句的问题如下所示:

Map<String, Long> freqMap = strings.stream()
                                   .collect(groupingBy(s -> s, counting()));
Optional<Map.Entry<String, Long>> maxEntryOpt = freqMap.entrySet()
                                                       .stream()
                                                   .max(Map.Entry.comparingByValue());
return maxEntryOpt.map(Map.Entry::getKey);

但是原作者可能拒绝这样做,因为显式打字看起来非常混乱,分散了重要的代码。使用var允许我们更自然地表达代码,而无需支付明确声明中间变量类型的成本:

var freqMap = strings.stream()
                     .collect(groupingBy(s -> s, counting()));
var maxEntryOpt = freqMap.entrySet()
                         .stream()
                         .max(Map.Entry.comparingByValue());
return maxEntryOpt.map(Map.Entry::getKey);

有人可能合法地更喜欢第一个片段及其单个长链方法调用。但是,在某些情况下,最好分解长方法链。

5.不要担心使用局部变量导致“编程接口”太多

Java编程中常见的习惯用法是构造具体类型的实例,但要将其分配给接口类型的变量。例如:

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

但是,如果使用var,则推断出具体类型而不是接口:

// Inferred type of list is ArrayList<String>.
 var list = new ArrayList<String>();

使用list变量的代码现在可以形成对具体实现的依赖性。如果变量的初始化程序将来要更改,这可能会导致其推断类型发生更改,从而导致在使用该变量的后续代码中发生错误或错误。

当遵守准则2时这不是问题,因为局部变量的范围很小,可能影响后续代码的具体实现的“泄漏”的风险是有限的。

6.使用泛型时要小心

var和泛类型功能允许您在可以从已存在的信息派生时省略显式类型信息。但是,如果一起使用,它们可能最终会省略编译器正确缩小您希望推断的类型所需的所有有用信息。

PriorityQueue<Item> itemQueue = new PriorityQueue<Item>();
PriorityQueue<Item> itemQueue = new PriorityQueue<>();
 var itemQueue = new PriorityQueue<Item>();
 
//欢迎加入Java高级架构进阶Qqun:963944895;免费分享Java架构学习资料、面试题、编程书籍
//危险:推断为PriorityQueue <Object>
 var itemQueue = new PriorityQueue<>();

泛型方法也成功地使用了类型推断,程序员很少提供显式类型参数。如果没有提供足够类型信息的实际方法参数,则泛型方法的推断依赖于目标类型。在var声明中,没有目标类型,因此可能会出现与diamond类似的问题。例如:

// DANGEROUS: infers as List<Object>
 var list = List.of();

使用泛型方法时,可以通过构造函数或方法的实际参数提供其他类型的信息,从而允许推断出预期的类型。这确实增加了额外的间接级别,但仍然是可预测的。从而:

// OK: itemQueue infers as PriorityQueue<String>
Comparator<String> comp = ... ;
 var itemQueue = new PriorityQueue<>(comp);

7.使用Var与文字Literals时要小心

使用带文字的var不太可能提供许多优点,因为类型名称通常很短。但是,var有时很有用,例如,对齐变量名称。

布尔值,字符,长字符串和字符串等文字没有问题。从这些文字推断出的类型是精确的,因此,var的含义是明确的。当初始值设定项是数值时,尤其是整数文字时,应特别小心。如果左侧有显式类型,则数值可以静默加宽或缩小为int以外的类型。对于var,该值将被推断为int,这可能是无意的。

// ORIGINAL
boolean ready = true;
char ch = '\ufffd';
long sum = 0L;
String label = "wombat";
byte flags = 0;
short mask = 0x7fff;
long base = 17;
 var ready = true;
 var ch    = '\ufffd';
 var sum   = 0L;
 var label = "wombat";
//危险:全部推断为int
//欢迎加入Java高级架构进阶Qqun:963944895;免费分享Java架构学习资料、面试题、编程书籍
 var flags = 0;
 var mask = 0x7fff;
 var base = 17;

写在最后

点关注,不迷路;【Java_苏先生】持续更新Java相关技术及咨询文章

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券