那些年我错过的Java

最近在看面向对象设计的书,有了一些新的感悟。于是周末闲来无事,想写个小东西练练手。最近一直用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());
    }
}

注意:由于篇幅关系,可能是最重要的改进lambdajigsaw项目等将放在以后单独的文章里学习介绍。


1.语言特性增强

1.1 类型推断

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>>

1.2 字符串

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")));

1.3 异常处理

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();
}

1.4 面向对象

Java 8和9都对interface做了增强,比如允许定义defaultstatic方法,具体请看下面的例子。

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 {}

尽管如此,interfaceabstract还是有区别,因为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;
    }
}

1.5 取代Null

新加入的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));

1.6 集合类初始化

这可能是对我们日常编程影响最大的改进了,Java程序员一定知道在Java里要简单的初始化一个ListSet有多麻烦!在这种情况下,Arrays.asList成了最受欢迎的方法,甚至还有了下面的所谓“双花括号”法。

Set set = new HashSet() {{
    add("foo"); add("bar");
}};

在Java 9里问题得到了解决,我们可以自豪地像其他语言一样简洁优雅的初始化集合类了!但要注意的是:这些of工厂方法创建出来的不是普通的ArrayListHashSetHashMap等,而是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);

2.新库函数引入

Java不断引入新的、更方便的库函数,让大家的开发工作更简单。JDK不断膨胀的同时,给大家带来了便利。由于篇幅关系,这里只介绍最常使用的文件、日期和JSON这三个,货币、HTTP/2客户端等就略去了。

2.1 NIO 2

Java NIO 2加入了PathFiles,尽管看起来不是很面向对象,但用起来还算方便。具体与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.findFiles.walk得到Stream遍历目录下内容,通过Files.lines获得Stream用流式的方式遍历文件内容。

2.2 JodaTime

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")));

2.3 JSON

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"));
}

3.多语言风格融合

3.1 REPL

REPLRead-Evaluate-Print-Loop可以说是其他动态语言的标配。当想实验一些函数或用法时,直接打开解析器,随着交互就能测试自己的想法。这种学习语言的用法的方式非常方便,尤其是这种“即兴”的代码。

而在Java里要如何做呢?打开IDE,创建个文件,写上一个classmain,然后写上要实验的一堆代码,运行看结果。这已经是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

3.2 InvokeDynamic指令

作为Java程序员,可能不会太关心InvokeDynamic这种改进。因为它是提供给JVM上的语言创造者,为动态语言的实现提供更好的支持。具体来说,就是允许比较松的类型检查、延后处理等。具体请参看这里,提到了InvokeDynamic为诸如Ruby中的Duck Type提供了支持。

3.3 Nashorn JS引擎

Nashorn是Java 8中新引入的JS引擎,用于替换之前的Rhino。但是按照官方的说法,由于ECMAScript规范发展太快,Nashorn难以维护,所以准备在未来从JDK中彻底移除。


4.JVM性能

4.1 PermGen的移除

很久以前实习的时候,做过一次JVM的内存泄漏分析。当时很常见的一种内存溢出(OOM)就是PermGen不足。所谓PermGen就是永久代,里面存放着加载进来的各种类的元信息和字节码。并且这部分区域是固定大小的。所以可想而知,如果一个项目依赖好多其他第三方的代码,那就会导致PermGen空间不足,最终OOM。Java 8移除掉了PermGen,我们再也见不到这种类型的OOM错误了,取而代之的是更加灵活的Metaspace。这里提到了两者间的区别。

4.2 G1成为默认GC

在Java 7首次引入的垃圾回收器G1,在经历了几代的谨慎实验后,在Java 9中终于成为默认的GC。JVM的整个垃圾回收机制的进化都围绕着停顿时间,通过细分成多个阶段,尽可能地减少停顿时间。最先进的G1也如此,它最大限度地减少由它造成的停顿时间,二就是它可以使用非连续的空间工作,高效地在超大堆上进行回收。具体请参见基础知识《JVM垃圾回收总结》和这里。


附录:Release

引自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

  1. Automated Resource Management
  2. Timsort is used to sort collections and arrays of objects instead of merge sort
  3. Multiple exceptions handling in single catch block
  4. String in switch
  5. JDBC 4.1 support
  6. JVM Enhancements(invokedynamic instructions)
  7. Automatic Type Inference during the Generic Instance Creation(using diamond operator)
  8. Numeric literals with underscore
  9. Fork and Join Framework
  10. G1 Garbage Collector

JDK 1.8 | Code Name : Spider | Year : 2014

  1. Lambda Expression
  2. Streams
  3. Functional Interface
  4. Nashorn
  5. Performance Improvement for HashMaps with Key Collisions
  6. Parallel Array Sorting
  7. StringJoiner class
  8. Optional class
  9. Remove permanent generation
  10. Annotation on Java Types

JDK 1.9 | Code Name : Jigsaw | Year : 2017

  1. JShell: the interactive Java REPL
  2. Private interface methods
  3. Process API Updates
  4. Light-Weight JSON API
  5. Money and Currency API for representing, transporting, and performing comprehensive calculations with Money and Currency
  6. Segmented Code Cache
  7. Improve Contended Locking

你可能感兴趣的:(Java)