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

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

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 开始,我们可以将变量的显式类型iplayer替换为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 和StreamAPI 是一个很好的团队。



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变量(尝试重新分配limitbmi变量将导致错误)的用例在一个错误中,这意味着这些变量是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替换显式类型。编译器将推断出这些变量(ratiolimitbmi的正确类型并保持它们的状态-ratio将是有效最终,而limitbmifinal。尝试重新分配其中任何一个将导致特定错误:

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:增强枚举》了解更多信息。只要您熟悉本章介绍的问题,采用这些特性应该是相当顺利的。



相关文章
|
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