在本文中, 我将通过示例介绍新的 Java SE 10 特性 --"var" 类型. 你将学习如何在代码中正确使用它, 以及在什么情况下不能使用它.
介绍
Java 10 引入了一个闪亮的新功能: 局部变量类型推断. 对于局部变量, 现在可以使用特殊的保留类型名称 "var" 代替实际类型, 如下所示:
var name = "Mohamed Taman"; 提供这个特性是为了增强 Java 语言, 并将类型推断扩展到局部变量的声明上. 这样可以减少板代码, 同时仍然保留 Java 的编译时类型检查.
由于编译器需要通过检查赋值等式右侧 (RHS) 来推断 var 的实际类型, 因此在某些情况下, 这个特性具有局限性. 我会在稍后提到这个问题. 现在, 让我们来看一些简单的例子吧.
在开始演示代码之前, 你需要一个 IDE 来体验这些新特性. 现在有很多可选择的 IDE, 所以你可以在它们当中选择你喜欢的能够支持 Java SE 10 的 IDE, 比如 Apache NetBeans 9,IntelliJ IDEA 2018 或最新版本的 Eclipse.
就个人而言, 我更喜欢使用交互式的编程工具, 可以快速学习 Java 语言语法, 了解新的 Java API 及其特性, 甚至用来进行复杂代码的原型设计. 这与枯燥的编辑, 编译和执行代码的繁琐过程不太一样:
写一个完整的程序; 编译并修复错误; 运行程序; 弄清楚它有什么问题; 修改; 重复这个过程. 除了 IDE 之外, 现在还可以使用从 Java SE 9 以就随 ava SE JDK 一起发布的 JShell.
什么是 JShell
现在, Java 有了自己的 REPL(Read-Evaluate-Print-Loop)实现 JShell(Java Shell), 作为交互式的编程环境. 那么, 它有什么神奇的地方? JShell 提供了一个快速友好的环境, 让你能够快速探索, 发现和试验 Java 语言特性及其丰富的库. 在 JShell 中, 你可以一次输入一个程序元素, 并可以立即看到结果, 然后根据需要对代码做出调整. 因此, JShell 用它的 Read-Evaluate-Print 循环取代了编辑, 编译和执行的繁琐过程. 在 JShell 中, 你不需要编写完整的程序, 只需要编写 JShell 命令和 Java 代码片段即可.
当你输入代码段时, JShell 会立即读取, 执行并打印结果, 然后准备好执行下一个代码片段. 因此, JShell 的即时反馈可以让你保持注意力, 提高你的效率, 并加快学习和软件开发过程.
对 JShell 的介绍就到此为止(InfoQ 最近对这个工具进行过全面介绍). 为了深入了解 JShell 的功能, 我录制了一套视频教程 "Hands-on Java 10 Programming with JShell", 可以帮助你掌握 JShell, 可以从 Packt 或 Udemy 访问这些教程.
现在, 让我们通过一些简单的示例 (使用 JShell) 来了解这个新的 var 类型能做些什么.
必备软件 为了能用上 JShell, 我假设你安装了 Java SE 或 JDK 10+, 并且 JDK 的 bin 目录已经加入到系统路径中. 如果还没有安装, 可以在这里下载 JDK 10 + 最新版本.
启动 JShell 会话
在 Windows 上, 打开命令提示符, 输入 jshell 并按回车键. 在 Linux 上, 打开一个 shell 窗口, 输入 jshell 并按回车键. 在 macOS(以前称为 OS X)上, 打开终端窗口, 输入 "jshell" 并按回车键. 这个命令会启动一个新的 JShell 会话, 并显示这个消息:
| Welcome to JShell -- Version 10.0.1 | For an introduction type: /help intro jshell>
使用 "var" 类型 现在你已经安装了 JDK 10, 现在让我们开始玩 JShell. 我们直接跳到终端, 通过示例来了解 var 类型. 只需在 jshell 提示符下输入我接下来要介绍的每个代码片段, 我会把结果留给你作为练习. 如果你稍微有瞄过一两眼在代码, 你会注意到它们看起来好像是错的, 因为当中没有分号. 你可以试试看, 看看能不能运行.
简单的类型推理 这是 var 类型的基本用法, 在下面的示例中, 编译器可以将 RHS 推断为 String 字面量:
- var name = "Mohamed Taman"
- var lastName = str.substring(8)
- System.out.println("Value:"+lastName +",and type is:"+ lastName.getClass().getTypeName())
这里不需要分号, 因为 JShell 是一个交互式环境. 只有当同一行代码有多个语句或一个类型声明或方法声明中有多个语句时才需要分号, 你将在后面的示例中看到.
var 类型和继承 在使用 var 时, 多态仍然有效. 在继承的世界中, var 类型的子类型可以像平常一样赋值给超类型的 var 类型, 如下所示:
import javax.swing.* var password = new JPasswordField("Password text") String.valueOf(password.getPassword()) // // 将密码的字符数组转换成字符串 var textField = new JTextField("Hello text") textField = password textField.getText()
但不能将超类型 var 赋值给子类型 var, 如下所示:
password = textField
这是因为 JPasswordField 是 JTextField 的子类.
var 和编译时安全性 如果出现错误的赋值操作会怎样? 不兼容的变量类型不能相互赋值. 一旦编译器推断出实际类型的 var, 就不能将错误的值赋值给它, 如下所示:
var number = 10 number = "InfoQ"
这里发生了什么? 编译器将 "var number = 10" 替换为 "int number = 10", 所以仍然可以保证安全性.
var 与集合和泛型 现在让我们来看看 var 与集合和泛型一起使用时如何进行类型推断. 我们先从集合开始. 在下面的情况中, 编译器可以推断出集合元素的类型是什么:
var list = List.of(10);
这里没有必要进行类型转换, 因为编译器已经推断出正确的元素类型为 int.
int i = list.get(0); // 等效于: var i = list.get(0);
下面的情况就不一样了, 编译器只会将其作为对象集合 (而不是整数), 因为在使用菱形运算符时, Java 需要 LHS(左侧) 的类型来推断 RHS 的类型:
var list2 = new ArrayList<>(); list2.add(10); list2 int i = list2.get(0) // 编译错误 int i = (int) list2.get(0) // 需要进行转换, 获得 int
对于泛型, 最好在 RHS 使用特定类型(而不是菱形运算符), 如下所示:
- var list3 = new ArrayList<Integer>(); list3.add(10); System.out.println(list3)
- int i = list3.get(0)
for 循环中的 var 类型 让我们先来看看基于索引的 For 循环:
for (var x = 1; x <= 5; x++) { var m = x * 2; // 等效于: int m = x * 2; System.out.println(m); }
下面是在 For Each 循环中:
var list = Arrays.asList(1,2,3,4,5,6,7,8,9,10) for (var item : list) { var m = item + 2; System.out.println(m); }
现在我有一个问题, var 是否适用于 Java 8 Stream? 让我们看看下面的例子:
var list = List.of(1, 2, 3, 4, 5, 6, 7) var stream = list.stream() stream.filter(x -> x % 2 == 0).forEach(System.out::println)
var 类型和三元运算符 那么三元运算符呢?
var x = 1> 0 ? 10 : -10 int i = x
现在, 如果在三元运算符的 RHS 中使用不同类型的操作数会怎样? 让我们来看看:
- var x = 1> 0 ? 10 : "Less than zero"; System.out.println(x.getClass()) //Integer
- var x = 1 <0 ? 10 : "Less than zero"; System.out.println(x.getClass()) // String
这两个例子是否可以说明 var 的类型是在运行时决定的? 绝对不是! 让我们以旧方式实现同样的逻辑:
Serializable x = 1 < 0 ? 10 : "Less than zero"; System.out.println(x.getClass())
Serializable 是其中两个操作数最具兼容性和最专的有类型(最不专有的类型是 java.lang.Object).
String 和 Integer 都实现了 Serializable.Integer 从 int 自动装箱. 换句话说, Serializable 是两个操作数的 LUB(最小上限). 所以, 这表明往前数第三个例子中的 var 类型也是 Serializable.
让我们转到另一个主题: 将 var 类型传给方法.
var 类型与方法 我们先声明一个名为 squareOf 的方法, 这个方法的参数为 BigDecimal 类型, 并返回参数的平方, 如下所示:
BigDecimal squareOf(BigDecimal number) { var result= number.multiply(number); return result; } var number = new BigDecimal("2.5") number = squareOf(number)
现在让我们看看它如何与泛型一起使用. 我们声明一个名为 toIntgerList 的方法, 参数类型为 List(泛型类型), 并使用 Streams API 返回一个整数列表, 如下所示:
- <T extends Number> List<Integer> toIntgerList(List<T> numbers) {
- var integers = numbers.stream()
- .map(Number::intValue)
- .collect(Collectors.toList());
- return integers;
- }
- var numbers = List.of(1.1, 2.2, 3.3, 4.4, 5.5)
- var integers = toIntgerList(numbers)
var 类型与匿名类 最后, 让我们看一下 var 和匿名类. 我们通过实现 Runnable 接口来使用线程, 如下所示:
- <T extends Number> List<Integer> toIntgerList(List<T> numbers) {
- var integers = numbers.stream()
- .map(Number::intValue)
- .collect(Collectors.toList());
- return integers;
- }
- var numbers = List.of(1.1, 2.2, 3.3, 4.4, 5.5)
- var integers = toIntgerList(numbers)
到目前为止, 我已经介绍了 Java 10 的新特性 --"var" 类型, 它减少了样板编码, 同时保持了 Java 的编译时类型检查. 我还通过实例说明了可以用它做些什么. 接下来, 你将了解 var 类型的局限性以及不能将它用在哪些地方.
- var message = "running..." //effectively final
- var runner = new Runnable(){
- @Override
- public void run() {
- System.out.println(message);
- }}
- runner.run()
"var" 的局限性 接下来, 你将看一些示例, 以便了解 var 类型功能无法做到的事情.
jshell 提示符将会告诉你代码出了什么问题, 你可以利用这些交互式的即时反馈.
应该要进行初始化 第一个也是最简单的原则就是不允许没有初始值的变量.
var name;
你将得到一个编译错误, 因为编译器无法推断这个局部变量 x 的类型.
不允许复合声明 尝试运行这行代码:
var x = 1, y = 3, z = 4
你将得到一个错误消息: 复合声明中不允许使用'var'.
不支持确定性赋值(Definite Assignment) 尝试创建一个名为 testVar 的方法, 如下所示, 将下面的代码复制并粘贴到 JShell 中:
void testVar(boolean b) { var x; if (b) { x = 1; } else { x = 2; } System.out.println(x); }
方法不会被创建, 而是会抛出编译错误. 因为没有设置初始值, 所以不能使用'var'. null 赋值 不允许进行 null 赋值, 如下所示:
var name = null;
这将抛出异常 "variable initializer is'null'". 因为 null 不是一个类型.
与 Lambda 一起使用 另一个例子, 没有 Lambda 初始化器. 这与菱形操作符那个示例一样, RHS 需要依赖 LHS 的类型推断.
var runnable = () -> {}
将抛出异常:"lambda expression needs an explicit target-type".
var 和方法引用 没有方法引用初始值, 类似于 Lambda 和菱形运算符示例:
var abs = BigDecimal::abs
将抛出异常:"method reference needs an explicit target-type".
var 和数组初始化 并非所有数组初始化都有效, 让我们看看什么时候 var 与 [] 不起作用:
var numbers[] = new int[]{2, 4, 6}
以下也不起作用:
var numbers = {2, 4, 6}
抛出的错误是: "array initializer needs an explicit target-type".
就像上一个例子一样, var 和 [] 不能同时用在 LHS 一边:
var numbers[] = {2, 4, 6}
错误: 'var' is not allowed as an element type of an array.
只有以下数组初始化是有效的:
var numbers = new int[]{2, 4, 6} var number = numbers[1] number = number + 3
不允许使用 var 字段
class Clazz { private var name; }
不允许使用 var 方法参数
void doAwesomeStuffHere(var salary){}
不能将 var 作为方法返回类型
var getAwesomeStuff(){ return salary; }
catch 子句中不能使用 var
try { Files.readAllBytes(Paths.get("c:\temp\temp.txt")); } catch (var e) {}
在编译时 var 类型究竟发生了什么? "var" 实际上只是一个语法糖, 并且它不会在编译的字节码中引入任何新的结构, 在运行期间, JVM 也没有为它们提供任何特殊的指令.
结论
在这篇文章中, 我介绍了 "var" 类型是什么以及它如何减少样板编码, 同时保持 Java 的编译时类型检查.
然后, 你了解了新的 JShell 工具, 即 Java 的 REPL 实现, 它可以帮助你快速学习 Java 语言, 并探索新的 Java API 及其功能. 你还可以使用 JShell 对复杂代码进行原型设计, 而不是重复编辑, 编译和执行的传统繁琐流程.
最后, 你了解了所有 var 类型的功能和限制, 例如什么时候可以和不可以使用 var. 写这篇文章很有意思, 所以我希望你喜欢它并能给你带来帮助.