最近在看面向对象设计的书,有了一些新的感悟。于是周末闲来无事,想写个小东西练练手。最近一直用Python,所以想回归一下更面向对象、更“静态”一些的Java。正研究怎么升级到Java 9尝尝鲜,结果发现再有80天Java 11都要发布了!真是山中方一日,世上已千年。才不关注Java没多久,已经快成版本帝了。索性直接上了Java 10,把最近落下的都补上。在这个夏天,补上那些年我错过的Java。
在开始之前要做的当然就是升级到最新版本,这是在Linux(Mint)上通过PPA的方式安装最新版本Java的命令:
$ sudo add-apt-repository ppa:linuxuprising/java
$ sudo apt update
$ sudo apt install oracle-java10-installer
$ sudo apt install oracle-java10-set-default
本文所有例子都可以通过测试类main
方法跑起来,只额外封装了一个打印到控制台的println
,编译运行javac Test.java && java Test
即可。当然,也可以玩一下Java 9提供的JShell,通过/set feedback verbose
在每条命令后能够看到更多信息。
public class Test {
public static final void main(String[] args) {
//...
}
private static void println(Object obj) {
System.out.println(obj.toString());
}
}
注意:由于篇幅关系,可能是最重要的改进lambda
、jigsaw
项目等将放在以后单独的文章里学习介绍。
Java很恼人的一点就是泛型,的确强制声明类型更加严谨和安全。然而在构建复杂数据类型时,代码会变得非常丑陋。Java 7试图改善这一问题,允许我们省略具体类型而让编译器自己去推断,还很形象地为其取名叫做Diamond Operator(钻石操作符)。
Map> employeeGroupByAge = new HashMap<>();
Java 10中又进一步地让编译器变得更加聪明,引入了var关键字,让编译器自动推断本地变量的类型。
var employeeGroupByAge = new HashMap<>();
employeeGroupByAge.put(25, List.of("Allen", "Hank"));
employeeGroupByAge.put(30, List.of("Carter"));
println(employeeGroupByAge); //{25=[Allen, Hank], 30=[Carter]}
从下面JShell的输出可以看出,Java已经足够聪明了,能够推断出甚至比人为声明更为精细的类型。
jshell> var list = List.of(1, 2.0, "3");
list ==> [1, 2.0, 3]
| modified variable list : List>>
| update overwrote variable list : List>>
Java 7中很实用的一个特性就是Switch也支持字符串了,之前我们只能通过大段的if-then-else来实现。而且据说Switch更利于编译器做优化,具体为什么还要研究一下。
switch (type) {
case "Hello": println("Hi!"); break;
case "World": println("Wow!"); break;
default: println("Bye!"); break;
}
此外一个小的改进就是String的Join,听起来好像Java太落后了…… 其实改进后依然与Python甚至Apache lang中提供的工具方法有不小的差距。
println(new StringJoiner("+", "prefix+", "+suffix").add("a").add("b").add("c"));
println(String.join("+", "prefix", "a", "b", "c", "suffix"));
println(String.join("+", List.of("prefix", "a", "b", "c", "suffix")));
Java 7中提供了几个非常实用的异常处理的语法特性,比如自动资源管理。第一次见到这个特性还是在大概十年前初学C#时,印象里好像是use关键字。Java沿用了try关键字,取名叫try-with-resources语句。但基本思想上是一样的,都是为了省去finally里无聊的资源关闭代码。适用于各种常见的资源,比如Connection、File、Stream等。
try (FileInputStream f = new FileInputStream("test.txt")) {
//...
}
Socket s1 = ...;
File f1 = ...;
try (s1; f1) {
//...
}
那其他资源或者我们自己的资源类呢?其实这些资源类都实现了java.lang.AutoCloseable接口,所以我们也可以实现它来获得自动关闭的能力。最近Java 9中又进一步简化,可以在try中传入多个final变量,甚至可以是不同的资源类型,只要它们实现了AutoCloseable接口即可。
在一个Catch中可以捕获多种异常,提供统一的处理和恢复逻辑,从而减少重复代码。
try {
//...
} catch (FileNotFoundException | IOException ex) {
ex.printStackTrace();
}
Java 8和9都对interface
做了增强,比如允许定义default
和static
方法,具体请看下面的例子。
Shape square = new Square();
square.show();
Shape.hello();
interface Shape {
default void show() {
println("Show default");
}
static void hello() {
println("Hello");
}
}
class Square implements Shape {}
尽管如此,interface
和abstract
还是有区别,因为interface
中的方法和变量只能是public
的,而abstract
却能指定任何访问权限。也就是说default
并不是用来在接口中定义供各个子类共享的工具方法的,接口依然是contract
契约,它取代的是在抽象类中实现接口中的某个方法,再由子类去实现其他方法的麻烦。这种情况下使用新的default
语法是可以减少中间抽象类一层的,在有必要时再重构加上。而且因为接口里没有私有的成员变量,默认方法实现会受到很大限制。在写上面例子时就没有找到一个很好的用例。
Java 9中又继续对接口增强,允许了private
方法,刚说过接口的契约作用,感觉不是很习惯。下面是网上找到的例子,用意就是default
方法有很多逻辑的话,可以拆分成多个私有方法。
public interface ReportGeneratorJava9 {
void generateReport(String reportData, String schema);
private String getReportDataFromDB() {
println("Reading the data from DB ....");
//...
}
private String getReportDataFromFile() {
println("Reading the data from FileSystem ....");
//...
}
private String getReportDataFromCache() {
System.out.println("Reading the data from Cache ....");
//...
}
default String getReportData(String reportSource) throws Exception {
String reportData = null;
if (reportSource.equalsIgnoreCase("DB")) {
reportData = getReportDataFromDB();
} else if (reportSource.equalsIgnoreCase("File")) {
reportData = getReportDataFromFile();
} else if (reportSource.equalsIgnoreCase("Cache")) {
reportData = getReportDataFromCache();
}
return reportData;
}
}
新加入的Optional非常实用,相当于为原来的对象又套了一层,避免使用null
。目前在实际项目中还没有见过Optional
的具体应用,总不能所有对象都套一层吧?一个比较明显的应用场景就是对入参的检查和默认赋值。
Optional hello = Optional.of("hello");
println(hello.get());
Optional nameOrNull1 = Optional.ofNullable(null);
println(nameOrNull1.isPresent());
println(nameOrNull1.orElse("Default: hello"));
println(nameOrNull1.orElseGet(() -> "Get default: hello"));
try {
nameOrNull1.orElseThrow(IllegalArgumentException::new);
} catch (Exception e) {
e.printStackTrace();
}
Optional nameOrNull2 = Optional.ofNullable("world");
nameOrNull2.ifPresent(name -> println("Name is here: " + name));
这可能是对我们日常编程影响最大的改进了,Java程序员一定知道在Java里要简单的初始化一个List
或Set
有多麻烦!在这种情况下,Arrays.asList
成了最受欢迎的方法,甚至还有了下面的所谓“双花括号”法。
Set set = new HashSet() {{
add("foo"); add("bar");
}};
在Java 9里问题得到了解决,我们可以自豪地像其他语言一样简洁优雅的初始化集合类了!但要注意的是:这些of
工厂方法创建出来的不是普通的ArrayList
,HashSet
和HashMap
等,而是Java 9新加的不可变的内部类。一旦我们在初始化后继续添加,就会发生异常。
List list = List.of("foo", "bar", "foobar");
Set set = Set.of(1, 5, 3, 9);
List<int[]> listOfArray = List.of(new int[]{1, 2, 3});
Map map = Map.of("foo", 1, "bar", 2);
Java不断引入新的、更方便的库函数,让大家的开发工作更简单。JDK不断膨胀的同时,给大家带来了便利。由于篇幅关系,这里只介绍最常使用的文件、日期和JSON这三个,货币、HTTP/2客户端等就略去了。
Java NIO 2加入了Path
和Files
,尽管看起来不是很面向对象,但用起来还算方便。具体与NIO 1的区别还不是很了解,据说是对文件链接、元数据管理等支持更好。
Path path = Paths.get("aaa.txt");
Files.exists(p);
Files.notExists(p);
Files.isRegularFile(p);
Files.isWritable(p);
Files.isExecutable(p);
Files.createFile(p);
Files.createDirectory(p);
Files.delete(p);
Files.copy(p, Paths.get("bbb.txt"));
Files.move(p, Paths.get("bbb.txt"));
byte[] bytes = Files.readAllBytes(path);
List lines = Files.readAllLines(path);
此外,还可以通过Files.find
和Files.walk
得到Stream
遍历目录下内容,通过Files.lines
获得Stream
用流式的方式遍历文件内容。
JDK 8引入了开源的Joda,下面是代码示例,展示了基本的日期获取、加减运算以及格式化等功能。
import java.time.*;
import java.time.format.*;
LocalDateTime dt1 = LocalDateTime.now();
println(dt1);
LocalDateTime dt2 = dt1.plusHours(1);
int days = Period.between(dt1.toLocalDate(), dt2.toLocalDate()).getDays();
println("Period (days): " + days);
long mins = Duration.between(dt1.toLocalTime(), dt2.toLocalTime()).toMinutes();
println("Duration (minutes): " + mins);
println(dt1.format(DateTimeFormatter.ofPattern("yyyy/MM/dd")));
JSON已经无处不在,从组件间的通信协议,到数据在UI或命令行的可视化。很多年前,jackson
和Google的Gson
是很火的两个。现在JDK有了自己的JSON解析器,但要注意JSON库只包含在了JavaEE而不是SE。如果像本文一样要在SE中使用的话,请参考这里。简单来说,就是下载单独的Jar包后,编译时指定-cp
。
//javac -cp ./javax.json-1.0.jar Test.java && java -cp ".:./javax.json-1.0.jar" Test
import javax.json.*;
String json = "{\"data\": [{\"name\": \"hello\"}, {\"name\": \"world\"}]}";
JsonReader reader = Json.createReader(new StringReader(json));
JsonObject data = reader.readObject();
JsonArray array = data.getJsonArray("data");
for (JsonObject obj : array.getValuesAs(JsonObject.class)) {
println("JSON name:" + obj.getString("name"));
}
REPL
即 Read-Evaluate-Print-Loop
可以说是其他动态语言的标配。当想实验一些函数或用法时,直接打开解析器,随着交互就能测试自己的想法。这种学习语言的用法的方式非常方便,尤其是这种“即兴”的代码。
而在Java里要如何做呢?打开IDE,创建个文件,写上一个class
和main
,然后写上要实验的一堆代码,运行看结果。这已经是Java程序员的日常了,相信很多人都有一个临时的工作区,里面有个叫Test
的类放着一堆代码。可想而知,Java的这种方式有多低效。有些IDE于是提供了这种“即兴”代码的快速编写测试功能。
而现在Java也有了自己的REPL
,用起来让你忘记了自己是在写Java,还以为是Python呢!现在就打开命令行,进入jshell
,比如我们想要实验JDK中新的Joda日期库:
cdai@cdai-ThinkPad-P50 ~/Temp $ jshell
| Welcome to JShell -- Version 10.0.2
| For an introduction type: /help intro
jshell> import java.time.LocalDateTime;
jshell> var dt = LocalDateTime.now();
dt ==> 2018-08-25T13:00:19.117641
jshell> var dt2 = dt.plusDays(1);
dt2 ==> 2018-08-26T13:00:19.117641
作为Java程序员,可能不会太关心InvokeDynamic这种改进。因为它是提供给JVM上的语言创造者,为动态语言的实现提供更好的支持。具体来说,就是允许比较松的类型检查、延后处理等。具体请参看这里,提到了InvokeDynamic为诸如Ruby中的Duck Type提供了支持。
Nashorn是Java 8中新引入的JS引擎,用于替换之前的Rhino。但是按照官方的说法,由于ECMAScript规范发展太快,Nashorn难以维护,所以准备在未来从JDK中彻底移除。
很久以前实习的时候,做过一次JVM的内存泄漏分析。当时很常见的一种内存溢出(OOM)就是PermGen不足。所谓PermGen就是永久代,里面存放着加载进来的各种类的元信息和字节码。并且这部分区域是固定大小的。所以可想而知,如果一个项目依赖好多其他第三方的代码,那就会导致PermGen空间不足,最终OOM。Java 8移除掉了PermGen,我们再也见不到这种类型的OOM错误了,取而代之的是更加灵活的Metaspace。这里提到了两者间的区别。
在Java 7首次引入的垃圾回收器G1,在经历了几代的谨慎实验后,在Java 9中终于成为默认的GC。JVM的整个垃圾回收机制的进化都围绕着停顿时间,通过细分成多个阶段,尽可能地减少停顿时间。最先进的G1也如此,它最大限度地减少由它造成的停顿时间,二就是它可以使用非连续的空间工作,高效地在超大堆上进行回收。具体请参见基础知识《JVM垃圾回收总结》和这里。
引自Quora问答回帖:https://www.quora.com/What-are-the-differences-between-Java-6-Java-7-Java-8-and-Java-9
JDK 1.7 | Code Name : Dolphin | Year : 2011
JDK 1.8 | Code Name : Spider | Year : 2014
JDK 1.9 | Code Name : Jigsaw | Year : 2017