85 将数组赋给var
根据经验,将数组分配给var
不需要括号[]
。通过相应的显式类型定义一个int
数组可以如下所示:
int[] numbers = new int[10]; // or, less preferred int numbers[] = new int[10];
现在,尝试直觉地使用var
代替int
可能会导致以下尝试:
var[] numberArray = new int[10]; var numberArray[] = new int[10];
不幸的是,这两种方法都无法编译。解决方案要求我们从左侧拆下支架:
// Prefer var numberArray = new int[10]; // inferred as array of int, int[] numberArray[0] = 3; // works numberArray[0] = 3.2; // doesn't work numbers[0] = "3"; // doesn't work
通常的做法是在声明时初始化数组,如下所示:
// explicit type work as expected int[] numbers = {1, 2, 3};
但是,尝试使用var
将不起作用(不会编译):
// Does not compile var numberArray = {1, 2, 3}; var numberArray[] = {1, 2, 3}; var[] numberArray = {1, 2, 3};
此代码无法编译,因为右侧没有自己的类型。
86 在复合声明中使用 LVTI
复合声明允许我们声明一组相同类型的变量,而无需重复该类型。类型只指定一次,变量用逗号分隔:
// using explicit type String pending = "pending", processed = "processed", deleted = "deleted";
将String
替换为var
将导致无法编译的代码:
// Does not compile var pending = "pending", processed = "processed", deleted = "deleted";
此问题的解决方案是将复合声明转换为每行一个声明:
// using var, the inferred type is String var pending = "pending"; var processed = "processed"; var deleted = "deleted";
因此,根据经验,LVTI 不能用在复合声明中。
87 LVTI 和变量范围
干净的代码最佳实践包括为所有局部变量保留一个小范围。这是在 LVTI 存在之前就遵循的干净代码黄金规则之一。
此规则支持可读性和调试阶段。它可以加快查找错误和编写修复程序的过程。请考虑以下打破此规则的示例:
// Avoid ... var stack = new Stack<String>(); stack.push("John"); stack.push("Martin"); stack.push("Anghel"); stack.push("Christian"); // 50 lines of code that doesn't use stack // John, Martin, Anghel, Christian stack.forEach(...);
因此,前面的代码声明了一个具有四个名称的栈,包含 50 行不使用此栈的代码,并通过forEach()方法完成此栈的循环。此方法继承自java.util.Vector,将栈作为任意向量(John、Martin、Anghel、Christian循环。这是我们想要的遍历顺序。
但后来,我们决定从栈切换到ArrayDeque(原因无关紧要)。这次,forEach()方法将是由ArrayDeque类提供的方法。此方法的行为不同于Vector.forEach(),即循环将遍历后进先出(LIFO)遍历(Christian、Anghel、Martin、John之后的条目:
// Avoid ... var stack = new ArrayDeque<String>(); stack.push("John"); stack.push("Martin"); stack.push("Anghel"); stack.push("Christian"); // 50 lines of code that doesn't use stack // Christian, Anghel, Martin, John stack.forEach(...);
这不是我们的本意!我们切换到ArrayDeque是为了其他目的,而不是为了影响循环顺序。但是很难看出代码中有 bug,因为包含forEach()部分的代码部分不在我们完成修改的代码附近(代码行下面 50 行)。我们有责任提出一个解决方案,最大限度地提高快速修复这个 bug 的机会,避免一堆上下滚动来了解正在发生的事情。解决方案包括遵循我们之前调用的干净代码规则,并使用小范围的stack变量编写此代码:
// Prefer ... var stack = new Stack<String>(); stack.push("John"); stack.push("Martin"); stack.push("Anghel"); stack.push("Christian"); // John, Martin, Anghel, Christian stack.forEach(...); // 50 lines of code that doesn't use stack
现在,当我们从Stack
切换到ArrayQueue
时,我们应该更快地注意到错误并能够修复它。
88 LVTI 与三元运算符
只要写入正确,三元运算符允许我们在右侧使用不同类型的操作数。例如,以下代码将不会编译:
// Does not compile List evensOrOdds = containsEven ? List.of(10, 2, 12) : Set.of(13, 1, 11); // Does not compile Set evensOrOdds = containsEven ? List.of(10, 2, 12) : Set.of(13, 1, 11);
但是,可以通过使用正确/支持的显式类型重写代码来修复此代码:
Collection evensOrOdds = containsEven ? List.of(10, 2, 12) : Set.of(13, 1, 11); Object evensOrOdds = containsEven ? List.of(10, 2, 12) : Set.of(13, 1, 11);
对于以下代码片段,类似的尝试将失败:
// Does not compile int numberOrText = intOrString ? 2234 : "2234"; // Does not compile String numberOrText = intOrString ? 2234 : "2234";
但是,可以这样修复:
Serializable numberOrText = intOrString ? 2234 : "2234"; Object numberOrText = intOrString ? 2234 : "2234";
因此,为了在右侧有一个具有不同类型操作数的三元运算符,开发人员必须匹配支持两个条件分支的正确类型。或者,开发人员可以依赖 LVTI,如下所示(当然,这也适用于相同类型的操作数):
// inferred type, Collection<Integer> var evensOrOddsCollection = containsEven ? List.of(10, 2, 12) : Set.of(13, 1, 11); // inferred type, Serializable var numberOrText = intOrString ? 2234 : "2234";
不要从这些例子中得出结论,var
类型是在运行时推断出来的!不是的!
89 LVTI 和for
循环
使用显式类型声明简单的for
循环是一项琐碎的任务,如下所示:
// explicit type for (int i = 0; i < 5; i++) { ... }
或者,我们可以使用增强的for
循环:
List<Player> players = List.of( new Player(), new Player(), new Player()); for (Player player: players) { ... }
从 JDK10 开始,我们可以将变量的显式类型i
和player
替换为var
,如下所示:
for (var i = 0; i < 5; i++) { // i is inferred of type int ... } for (var player: players) { // i is inferred of type Player ... }
当循环数组、集合等的类型发生更改时,使用var
可能会有所帮助。例如,通过使用var
,可以在不指定显式类型的情况下循环以下array
的两个版本:
// a variable 'array' representing an int[] int[] array = { 1, 2, 3 }; // or the same variable, 'array', but representing a String[] String[] array = { "1", "2", "3" }; // depending on how 'array' is defined // 'i' will be inferred as int or as String for (var i: array) { System.out.println(i); }
90 LVTI 和流
让我们考虑以下Stream
流:
// explicit type Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5); numbers.filter(t -> t % 2 == 0).forEach(System.out::println);
使用 LVTI 代替Stream非常简单。只需将Stream替换为var,如下所示:
// using var, inferred as Stream<Integer> var numberStream = Stream.of(1, 2, 3, 4, 5); numberStream.filter(t -> t % 2 == 0).forEach(System.out::println);
下面是另一个例子:
// explicit types Stream<String> paths = Files.lines(Path.of("...")); List<File> files = paths.map(p -> new File(p)).collect(toList()); // using var // inferred as Stream<String> var pathStream = Files.lines(Path.of("")); // inferred as List<File> var fileList = pathStream.map(p -> new File(p)).collect(toList());
看起来 Java10、LVTI、Java8 和Stream
API 是一个很好的团队。
91 使用 LVTI 分解嵌套/大型表达式链
大型/嵌套表达式通常是一些代码片段,它们看起来非常令人印象深刻,令人生畏。它们通常被视为智能或智慧代码的片段。关于这是好是坏是有争议的,但最有可能的是,这种平衡倾向于有利于那些声称应该避免这种代码的人。例如,检查以下表达式:
List<Integer> ints = List.of(1, 1, 2, 3, 4, 4, 6, 2, 1, 5, 4, 5); // Avoid int result = ints.stream() .collect(Collectors.partitioningBy(i -> i % 2 == 0)) .values() .stream() .max(Comparator.comparing(List::size)) .orElse(Collections.emptyList()) .stream() .mapToInt(Integer::intValue) .sum();
这样的表达式可以是有意编写的,也可以表示一个增量过程的最终结果,该过程在时间上丰富了一个最初很小的表达式。然而,当这些表达式开始成为可读性的空白时,它们必须通过局部变量被分解成碎片。但这并不有趣,可以被认为是我们想要避免的令人筋疲力尽的工作:
List<Integer> ints = List.of(1, 1, 2, 3, 4, 4, 6, 2, 1, 5, 4, 5); // Prefer Collection<List<Integer>> evenAndOdd = ints.stream() .collect(Collectors.partitioningBy(i -> i % 2 == 0)) .values(); List<Integer> evenOrOdd = evenAndOdd.stream() .max(Comparator.comparing(List::size)) .orElse(Collections.emptyList()); int sumEvenOrOdd = evenOrOdd.stream() .mapToInt(Integer::intValue) .sum();
检查前面代码中局部变量的类型。我们有Collection>、List和int。很明显,这些显式类型需要一些时间来获取和写入。这可能是避免将此表达式拆分为碎片的一个很好的理由。然而,如果我们希望采用局部变量的样式,那么使用var类型而不是显式类型的琐碎性是很诱人的,因为它节省了通常用于获取显式类型的时间:
var intList = List.of(1, 1, 2, 3, 4, 4, 6, 2, 1, 5, 4, 5); // Prefer var evenAndOdd = intList.stream() .collect(Collectors.partitioningBy(i -> i % 2 == 0)) .values(); var evenOrOdd = evenAndOdd.stream() .max(Comparator.comparing(List::size)) .orElse(Collections.emptyList()); var sumEvenOrOdd = evenOrOdd.stream() .mapToInt(Integer::intValue) .sum();
令人惊叹的!现在,编译器的任务是推断这些局部变量的类型。我们只选择打破表达的点,用var
来划分。
92 LVTI 和方法返回值和参数类型
根据经验,LVTI 不能用作return
方法类型或参数方法类型;相反,var
类型的变量可以作为方法参数传递或存储return
方法。让我们通过几个例子来迭代这些语句:
- LVTI 不能用作以下代码不编译的方法返回类型:
// Does not compile public var fetchReport(Player player, Date timestamp) { return new Report(); }
- LVTI 不能用作方法参数类型以下代码不编译:
public Report fetchReport(var player, var timestamp) { return new Report(); }
var
类型的变量可以作为方法参数传递,也可以存储一个返回方法。下面的代码编译成功并且可以工作:
public Report checkPlayer() { var player = new Player(); var timestamp = new Date(); var report = fetchReport(player, timestamp); return report; } public Report fetchReport(Player player, Date timestamp) { return new Report(); }
93 LVTI 和匿名类
LVTI 可以用于匿名类。下面是一个匿名类的示例,该类对weighter
变量使用显式类型:
public interface Weighter { int getWeight(Player player); } Weighter weighter = new Weighter() { @Override public int getWeight(Player player) { return ...; } }; Player player = ...; int weight = weighter.getWeight(player);
现在,看看如果我们使用 LVTI 会发生什么:
var weighter = new Weighter() { @Override public int getWeight(Player player) { return ...; } };
94 LVTI 可以是最终的,也可以是有效最终的
作为一个快速提醒,从 JavaSE8 开始,一个局部类可以访问封闭块的局部变量和参数,这些变量和参数是final
或实际上是final
。一个变量或参数,其值在初始化后从未改变,实际上是最终的。
下面的代码片段表示一个有效最终变量(尝试重新分配ratio
变量将导致错误,这意味着该变量是有效最终)和两个final
变量(尝试重新分配limit
和bmi
变量将导致错误)的用例在一个错误中,这意味着这些变量是final
:
public interface Weighter { float getMarginOfError(); } float ratio = fetchRatio(); // this is effectively final var weighter = new Weighter() { @Override public float getMarginOfError() { return ratio * ...; } }; ratio = fetchRatio(); // this reassignment will cause error public float fetchRatio() { final float limit = new Random().nextFloat(); // this is final final float bmi = 0.00023f; // this is final limit = 0.002f; // this reassignment will cause error bmi = 0.25f; // this reassignment will cause error return limit * bmi / 100.12f; }
现在,让我们用var
替换显式类型。编译器将推断出这些变量(ratio
、limit
和bmi
的正确类型并保持它们的状态-ratio
将是有效最终,而limit
和bmi
是final
。尝试重新分配其中任何一个将导致特定错误:
var ratio = fetchRatio(); // this is effectively final var weighter = new Weighter() { @Override public float getMarginOfError() { return ratio * ...; } }; ratio = fetchRatio(); // this reassignment will cause error public float fetchRatio() { final var limit = new Random().nextFloat(); // this is final final var bmi = 0.00023f; // this is final limit = 0.002f; // this reassignment will cause error bmi = 0.25f; // this reassignment will cause error return limit * bmi / 100.12f; }
95 LVTI 和 Lambda
使用 LVTI 和 Lambda 的问题是无法推断具体类型。不允许使用 Lambda 和方法引用初始化器。此语句是var
限制的一部分;因此,Lambda 表达式和方法引用需要显式的目标类型。
例如,以下代码片段将不会编译:
// Does not compile // lambda expression needs an explicit target-type var incrementX = x -> x + 1; // method reference needs an explicit target-type var exceptionIAE = IllegalArgumentException::new;
由于var不能使用,所以这两段代码需要编写如下:
Function<Integer, Integer> incrementX = x -> x + 1; Supplier<IllegalArgumentException> exceptionIAE = IllegalArgumentException::new;
但是在 Lambda 的上下文中,Java11 允许我们在 Lambda 参数中使用var。例如,下面的代码在 Java11 中工作(更多详细信息可以在《JEP323:Lambda 参数的局部变量语法》中找到:
@FunctionalInterface public interface Square { int calculate(int x); } Square square = (var x) -> x * x;
但是,请记住,以下操作不起作用:
var square = (var x) -> x * x; // cannot infer
96 LVTI 和null
初始化器、实例变量和catch
块变量
LVTI 与null
初始化器、实例变量和catch
块变量有什么共同点?嗯,LVTI 不能和它们一起使用。以下尝试将失败:
- LVTI 不能与
null
初始化器一起使用:
// result in an error of type: variable initializer is 'null' var message = null; // result in: cannot use 'var' on variable without initializer var message;
LVTI 不能与实例变量(字段)一起使用:
public class Player { private var age; // error: 'var' is not allowed here private var name; // error: 'var' is not allowed here ... }
- LVTI 不能用于
catch
块变量:
try { TimeUnit.NANOSECONDS.sleep(1000); } catch (var ex) { ... }
资源尝试使用
另一方面,var
类型非常适合资源尝试使用,如下例所示:
// explicit type try (PrintWriter writer = new PrintWriter(new File("welcome.txt"))) { writer.println("Welcome message"); }
// using var try (var writer = new PrintWriter(new File("welcome.txt"))) { writer.println("Welcome message"); }
97 LVTI 和泛型类型,T
为了理解 LVTI 如何与泛型类型相结合,让我们从一个示例开始。以下方法是泛型类型T
的经典用例:
public static <T extends Number> T add(T t) { T temp = t; ... return temp; }
在这种情况下,我们可以将T
替换为var
,代码将正常工作:
public static <T extends Number> T add(T t) { var temp = t; ... return temp; }
因此,具有泛型类型的局部变量可以利用 LVTI。让我们看看其他一些示例,首先使用泛型类型T
:
public <T extends Number> T add(T t) { List<T> numberList = new ArrayList<T>(); numberList.add(t); numberList.add((T) Integer.valueOf(3)); numberList.add((T) Double.valueOf(3.9)); // error: incompatible types: String cannot be converted to T // numbers.add("5"); return numberList.get(0); }
现在,我们将List
替换为var
:
public <T extends Number> T add(T t) { var numberList = new ArrayList<T>(); numberList.add(t); numberList.add((T) Integer.valueOf(3)); numberList.add((T) Double.valueOf(3.9)); // error: incompatible types: String cannot be converted to T // numbers.add("5"); return numberList.get(0); }
注意并仔细检查ArrayList
实例化是否存在T
。不要这样做(这将被推断为ArrayList,并将忽略泛型类型
T
后面的实际类型):
var numberList = new ArrayList<>();
98 LVTI、通配符、协变和逆变
用 LVTI 替换通配符、协变和逆变是一项微妙的工作,应该在充分意识到后果的情况下完成。
LVTI 和通配符
首先,我们来讨论 LVTI 和通配符(
?
。通常的做法是将通配符与Class
关联,并编写如下内容:
// explicit type Class<?> clazz = Long.class;
在这种情况下,使用var代替Class没有问题。根据右边的类型,编译器将推断出正确的类型。在本例中,编译器将推断出Class。
但是请注意,用 LVTI 替换通配符应该小心,并且您应该意识到其后果(或副作用)。让我们看一个例子,用var替换通配符是一个错误的选择。考虑以下代码:
Collection<?> stuff = new ArrayList<>(); stuff.add("hello"); // compile time error stuff.add("world"); // compile time error
由于类型不兼容,此代码无法编译。一种非常糟糕的方法是用var替换通配符来修复此代码,如下所示:
var stuff = new ArrayList<>(); strings.add("hello"); // no error strings.add("world"); // no error
通过使用var,错误将消失,但这不是我们在编写前面的代码(存在类型不兼容错误的代码)时想到的。所以,根据经验,不要仅仅因为一些恼人的错误会神奇地消失,就用var代替Foo!试着思考一下预期的任务是什么,并相应地采取行动。例如,可能在前面的代码片段中,我们试图定义ArrayList,但由于错误,最终得到了Collection。
LVTI 和协变/逆变
用 LVTI 替换协变(
Foo
)或逆变(Foo
)是一种危险的方法,应该避免。
请查看以下代码片段:
// explicit types Class<? extends Number> intNumber = Integer.class; Class<? super FilterReader> fileReader = Reader.class;
在协变中,我们有一个上界,由
Number
类表示,而在逆变中,我们有一个下界,由FilterReader
类表示。有了这些边界(或约束),以下代码将触发特定的编译时错误:
// Does not compile // error: Class<Reader> cannot be converted // to Class<? extends Number> Class<? extends Number> intNumber = Reader.class; // error: Class<Integer> cannot be converted // to Class<? super FilterReader> Class<? super FilterReader> fileReader = Integer.class;
现在,让我们用
var
代替前面的协变和逆变:
// using var var intNumber = Integer.class; var fileReader = Reader.class;
此代码不会导致任何问题。现在,我们可以将任何类赋给这些变量,这样我们的边界/约束就消失了。这不是我们打算做的:
// this will compile just fine var intNumber = Reader.class; var fileReader = Integer.class;
所以,用
var
代替协变和逆变是个错误的选择!
总结
这是本章的最后一个问题。请看《JEP323:Lambda 参数的局部变量语法》、《JEP301:增强枚举》了解更多信息。只要您熟悉本章介绍的问题,采用这些特性应该是相当顺利的。