在本文中,我们将深入研究Java 10中引入的局部变量类型推断的新特性。我们将讨论使用局部变量类型推断的范围和限制。
此功能是作为JEP:286(JDK增强提案)的一部分提出的。该提案通过支持对局部变量声明和初始化的类型推断以增强Java语言。
有关Java 10发行版的完整概述,请参阅Java 10新特性。
Java 10:局部变量类型推断
使用Java 10,您可以使用var
局部变量而不是类型名称(Manifest Type)。这是通过称为局部变量类型推断的新特征完成的。
但首先,什么是类型推断?
类型推断是指Java编译器具有查看每个方法调用和相应声明的能力,以确定调用所适用的类型参数。类型推断不是Java编程特有的。
对于使用初始化程序的局部变量声明,我们现在可以使用保留类型名称“var”而不是清单类型。我们来看几个例子。
var list = new ArrayList<String>(); // infers ArrayList<String>
var stream = list.stream(); // infers Stream<String>
清单类型:声明的每个变量的类型的显式标识称为清单类型。例如,如果变量“actors”将存储Actor的List,那么它的类型List 是清单类型,并且必须在Java 10之前声明它(如下所述):
List<Actor> actors = List.of(new Actor()); // Pre Java 10
var actors = List.of(new Actor()); // Java 10 onwards
局部变量类型推断如何工作?
解析var语句时,编译器查看声明语句的右侧,即初始化器,并从右侧(RHS)表达式推断出类型。
好吧,这是否意味着现在Java是一种动态类型的语言?并不是,它仍然是一种静态类型的语言。我们来看一个读取文件的代码片段。
private static void readFile() throws IOException {
var fileName = "Sample.txt";
var line = "";
var fileReader = new FileReader(fileName);
var bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
bufferedReader.close();
}
现在,让我们看一下从IntelliJ IDEA反编译器中获取的反编译代码。
private static void readFile() throws IOException {
String fileName = "Sample.txt";
String line = "";
FileReader fileReader = new FileReader(fileName);
BufferedReader bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
bufferedReader.close();
}
这里编译器正确地从右侧表达式推断出变量的类型,并将其添加到字节码中。
var是保留类型名称
var不是关键字,它是保留类型名称。这是什么意思呢?
- 我们可以创建一个名为“var”的变量。
var var = 5; // syntactically correct
// var is the name of the variable
- 允许使用“var”作为方法名称。
public static void var() { // syntactically correct
}
- 允许使用“var”作为包名。
package var; // syntactically correct
- “var”不能用作类或接口的名称。
class var{ } // Compile Error
LocalTypeInference.java:45: error: 'var' not allowed here
class var{
^
as of release 10, 'var' is a restricted local variable type and cannot be used for type declarations
1 error
interface var{ } // Compile Error
局部变量类型推断使用场景
局部类型推断只能在以下场景中使用:
- 仅限于具有初始化程序的局部变量
- 增强的for循环的索引
- for循环中声名的局部变量
让我们来看看这些场景的示例:
var numbers = List.of(1, 2, 3, 4, 5); // inferred value ArrayList<String>
// Index of Enhanced For Loop
for (var number : numbers) {
System.out.println(number);
}
// Local variable declared in a loop
for (var i = 0; i < numbers.size(); i++) {
System.out.println(numbers.get(i));
}
局部变量类型推理限制
使用var有一定的局限性,让我们来看看其中的一部分。
- 没有初始化,不能对变量使用’var’
如果没有initailizer,那么编译器将无法推断出类型。
var x;
LocalTypeInference.java:37: error: cannot infer type for local variable x
var x;
^
(cannot use 'var' on variable without initializer)
1 error
- 不能用于多变量定义
var x = 5, y = 10;
LocalTypeInference.java:41: error: 'var' is not allowed in a compound declaration
var x = 5, y = 10;
^
1 error
- Null不能用作var的初始化器
Null不是类型,因此编译器无法推断RHS表达式的类型。
var author = null; // Null cannot be inferred to a type
LocalTypeInference.java:47: error: cannot infer type for local variable author
var author = null;
^
(variable initializer is 'null')
1 error
- 不能有额外的数组维度括号
var actorArr[] = new Actor[10];
LocalTypeInference.java:52: error: 'var' is not allowed as an element type of an array
var actorArr[] = new Actor[10];
^
1 error
- 具有lambda,方法引用和数组初始值设定项的多语句表达式将触发错误
对于Lambda表达式,方法引用和数组初始化器的类型推断,编译器依赖于左侧表达式或传递表达式的方法的参数定义,而var使用RHS,这将形成循环推断,因此编译器生成编译时错误。
var min = (a, b) -> a < b ? a : b;
LocalTypeInference.java:59: error: cannot infer type for local variable min
var min = (a, b) -> a < b ? a : b;
^
(lambda expression needs an explicit target-type)
1 error
var minimum = Math::min;
LocalTypeInference.java:65: error: cannot infer type for local variable minimum
var minimum = Math::min;
^
(method reference needs an explicit target-type)
1 error
var nums = {1,2,3,4,5};
LocalTypeInference.java:71: error: cannot infer type for local variable nums
var nums = {1,2,3,4,5};
^
(array initializer needs an explicit target-type)
1 error
具有局部变量类型推断的泛型
Java具有泛型的类型推断,除此之外,它还必须为任何泛型语句执行类型擦除。在使用泛型的局部类型引用时,应该理解一些边界情况。
类型擦除:为了实现泛型,Java编译器会使用类型擦除。如果类型参数是无界的,则用绑定的泛化类型或Object替换泛型类型中的所有类型参数。
让我们通过泛型来讨论var的一些用例:
var map1 = new HashMap(); // Inferred as HashMap
var map2 = new HashMap<>(); // Inferred as HashMap<Object, Object>
map1 - 编译器将该map推断为HashMap,不带任何泛型类型。
map2 - 菱形运算符依赖于LHS进行类型推断,这里编译器无法推断LHS,因此它推断map2具有可以表示HashMap的上限或超类型。这导致map2被推断为HashMap。
匿名类类型
无法命名匿名类类型,但它们很容易理解——它们只是类。允许变量具有匿名类类型使声明局部类的单例实例十分简单。我们来看一个例子:
var runnable = new Runnable() {
@Override
public void run() {
var numbers = List.of(5, 4, 3, 2, 1);
for (var number : numbers) {
System.out.println(number);
}
}
};
runThread(runnable);
不可表达的类型
无法推断到特定类型的表达式称为Non Nototable Type。对于捕获变量类型,交集类型或匿名类类型,可以称为这种类型。让我们看看一个非可表达类型是如何用于局部变量类型推断:
var map3 = new HashMap<>() { // anonymous class
int someVar;
};
这里,当菱形运算符与匿名类类型一起使用时,编译器无法将RHS表达式推断为任何特定类型。这导致形成不可表示的类型。
首先,编译器将通过使用HashMap <>的超类型来获得可表示的类型,即HashMap
// Special Case Non Denotable Type
var person = new Object() {
class Name {
String firstName;
String lastName;
public Name(String firstName, String lastName) {
super();
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
}
Name name;
Actor actor;
public String displayName() {
return name.getFirstName() + " " + name.lastName;
}
};
person.name = person.new Name("Rakesh", "Kumar");
System.out.println(person.displayName());
关于var的一些有趣故事
有一个可供选择的局部类型推断的关键字列表的调查。以下是向社区用户提供的语法选项列表:
- var x = expr only(如C#)
- var,及不可变局部的val (如Scala,Kotlin)
- var,及不可变局部的let(如Swift)
- auto x = expr(如C ++)
- const x = expr(已经是保留字)
- final x = expr(已经是保留字)
- let x = expr
- def x = expr(如Groovy)
- x:= expr(如Go)
调查结果:
调查选择的百分比:
使用次优的选择(var)的理由
- 虽然var是次优的选择,人们对它的感觉很好,几乎没有人讨厌它。而其他选择则不是这种情况。
- C#体验。C#社区发现该关键字对于类似Java的语言来说是合理的。
- 一些读者发现var / val是如此相似,以至于他们可以忽略差异,并且对不可变和可变变量使用不同的关键字会很烦人。
- 大多数局部变量实际上是final的,并且用另一个形式来惩罚不变性并不是JEP的意图。
局部变量类型推断的好处
- 它改善了开发人员的体验
- 它减少了代码形式
- 它减少了样板代码
- 提高代码清晰度
原文地址:Java 10: Local Variable Type Inference written by Rakesh Kumar
完整代码:Github