Kahn's blogs

Java 7 and Java 8特性你有用过吗?一

2017/09/27

前言 Hello Java

刚入行的时候公司还在用1.4,当时要做一个PC客户端的房间清洁程序的界面需要用的AWT一些皮肤组件,就申请公司全面升级到1.5,后来入了android的坑后升级到1.6。在用1.6的时候公司还是有一些服务器安装的是1.4or1.5。感觉只有刚接触1.6的时候学习了1.6的新语法和类,一直都没有再去看java新的特性。android始终比java社区慢了一部,眼瞅着9就出来,这些年,我们还在用jdk7,而且使用的还是1.6学习的语法。在这里记录一下java7,8的特性,以做备忘。能有新的东西还是用起来。Java一直在变,而我们却原地踏步,这样不行。

Java语言与Java平台

跨平台

刚开始接触Java的时候,Java就是Java还能是什么别的?后来才知道JVM上还能跑别的语言的编译后的中间文件。众所周知Java是以跨平台起家的,后来又因J2EE称霸了语言界一段时间,使用率高居榜首。

Java语言执行过程 .java—javac—>.class—-类加载器—->(转换后的.class—-解释器—–>可执行代码—-JIT编译器—->二进制机器码)

在不同的操作系统平台上由于JVM的特殊实现不同,就实现了跨平台运行,同一份class文件,可以run all。说下转换后的.class,一些系统级框架,比如spring,会在特殊时期修改调整我们编译出的.class文件形成新的.class,这项技术现在在android开发中也被大量使用。

我工作过的一家公司就是因为跨平台的特性而选择Java做为开发语言的。这家公司做的是KTV点歌系统,android刚出的时候,公司就想做移动点歌。我进去就是负责android端和服务端开发。做服务端的时候做了一个服务程序(Jar运行的程序),来接收一个ktv所有的移动端长连接,进行点歌操作。这台服务器是Linux的。后来为了给小型酒吧提供这种服务,用android盒子作为服务器。之前的服务端程序(Jar包)直接平移到android盒子上,试了一下,能用。

做这个程序也是经历了Java版本升级的阵痛。刚开始这个服务端使用的是Java的io包api,后来,由于很多急促的短连接(有个业务需要即连即断)造成并发问题,线程执行过慢,重构了整个项目,使用了Java的nio,当时真是累的够呛。效果也是显著的。

跨语言

一直沉醉在Java语言的世界里,直到Android Studio出来之后才知道,哦,原来JVM还可以运行别的语言。区别就在编译后的.class文件上。整个Java生态有两个重要的规范。JSL和VMSpec,一个是Java语言的规范,一个是JVM虚拟机的规范。而在Java 7之后,VMSpec已经不引用JSL的任何内容了,这给了别的语言更多的机会。Java不在独得皇上恩宠。

Java 7

语法特性

  1. switch终于支持String类型的常量了,毕竟String也是常量。
  2. 天天撸二进制的哥哥可以用二进制来表示int型了。还支持下划线分割。
    • int i = 0b1110_1001;
    • long x = 223414_234234_234234L
  3. 异常语法

    • 多捕获

      1
      2
      3
      try() {
      } catch(XxxException e | YyyException e) {
      }

      可以把同一类的,需要统一处理异常在一个代码块里捕获统一处理

    • final重抛

      1
      2
      3
      4
      try() {
      } catch(final Exception e) {
      throw e;
      }

      这样做,不管你捕获到的异常类型是什么,重新抛出的都是真实的异常对象,而不是被掩盖后的Exception类型。

  4. try-with-resources
    java有很多套路代码,而且不写会粗大事,try-with-resources就是为了解决资源关闭的套路而来。

    1
    2
    3
    4
    5
    6
    try (InputStream inputStream = new FileInputStream("")){
    inputStream.read();
    } catch (IOException e1) {
    }

    这么撸一下,就不用写关闭代码了。只有实现java.lang.AutoCloseable接口的资源才能这么干,java7重写了Closeable接口,Closeable继承自AutoCloseable。需要注意的是,如果用try-with-resources,就不要使用匿名写法申请资源了。

    1
    2
    3
    4
    5
    try (ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(""))){
    inputStream.read();
    } catch (IOException e1) {
    }

    上面这段代码如果创建ObjectInputStream失败的话,new出来的FileInputStream资源对象就不会被关闭了

    1
    2
    3
    4
    5
    6
    try (FileInputStream fis = new FileInputStream(""); ObjectInputStream inputStream = new ObjectInputStream(fis)){
    inputStream.read();
    } catch (IOException e1) {
    }

    分开撸最稳妥了。
    最操蛋的是,如果想在android上面用,最低支持API19。

新的API

  1. 新的文件操作 java.nio.file 新增的api
  2. Path: java 7对文件系统的描述采用了新的机制Path,为兼容以前版本,Path和File可以互转。Path只描述路径,运行时才会和物理路径绑定在一起。Paths为Path的帮助类。这也是Java的传统了,一般api类名后加个s就是相关工具类。与Path一起新增了很多工具类。都在java.nio.file下。
  3. Files: 这个工具类配合Path使用简直6的很,对文件的复制,移动,遍历目录树都差不多是一键式的。详细情况请查阅API.
  4. 文件特性和WatchService: 新的文件操作可以获取更多操作系统相关的信息。比如最近更新时间等等,以前根本获取不到。还有WatchService,可以监控文件的改变,简直是还在用轮训扫描文件变化的小伙伴的福音。还有方便的FileChannel
  5. NIO: 既然包名都带有nio字样了,肯定也提供的非阻塞的io操作。java.nio在java 1.4就添加在了java api当中,但是据我所知用的人并不多。这是一个很牛逼的东西。先说下网络套接字的nio:在我刚开始写一个后端KTV点歌服务程序的时候,首先想到的是模仿聊天室写。网上搜到的例子全是阻塞式的。大概的代码样式应该是

    1
    2
    3
    4
    5
    Serversocket server = ...
    while(true) {
    Socket socket = server.accept();
    ThreadPool.execute(new MySocketThread(socket));
    }

    主线程死循环无限接收socket链接,然后启动线程处理单个长连接。要启动的线程数量不说,基本没有什么把控可讲。并发量低,对于短促的数据连接,吞吐量也是低的令人发指。这些都不是我发现nio的契机。当这个服务程序上线后,客户反馈,有的pad连接上后点不了歌,过不久后台服务程序就死了。简直见了鬼了,在网络特差的部分Linux服务器上,阻塞式的socket会阻塞在read上无法响应,设置超时也没用。后来加入心跳机制,自己做超时,检测到无响应的socket就close。更是活见过鬼,close也无法让那个阻塞的read抛出异常。当时简直快要崩溃了,说好的close就抛异常呢?后来公司同事做C++的问我,你们java没选择器,通道这些东西吗?然后各种百度,才发现nio。然而资料非常少,也是磕磕绊绊才把程序从io转到nio上。刚接触nio一堆buffer类都能让人弄崩溃。回归正题:Java 7带来了新的文件nio,AsynchronousFileChannel。如果先用相关nio,首先想到的应该是去翻看java.nio包下的东西。channel是个好东西!

  6. 详细的说下AsynchronousFileChannel,AsynchronousServerSocketChannel,AsynchronousSocketChannel这三个东东。三个东西都差不多,都是自带并发的数据处理通道。见名知意,第一个是处理文件的,后两个一个处理serversocket,一个处理socket。重点说下AsynchronousFileChannel,因为其他两个都差不多。

    • 这玩意自带线程池(注意:会和其他通道共用),也可以自己传入线程池。
    • 这玩意的读写都是非阻塞的,会交给线程池自动处理。所以,它的read,write方法不会阻塞主线程。翻看api,每个read,write方法都有两个重载方法,

      1
      2
      3
      4
      5
      abstract Future<Integer> read(ByteBuffer dst, long position)
      Reads a sequence of bytes from this channel into the given buffer, starting at the given file position.
      abstract <A> void read(ByteBuffer dst, long position, A attachment, CompletionHandler<Integer,? super A> handler)
      Reads a sequence of bytes from this channel into the given buffer, starting at the given file position.

      第一个read返回值为Future,第二个多了两个参数

    • 先说第一个,Future,未来。掌握这个就掌握了未来。这玩意跟线程池中的那一套未来机制是一样的。我们一般使用线程处理问题后,回归主线程分为两种方式,一种是Future,一种是callback。在android中大量使用callback机制。最近领导让优化app启动速度,Future起到了不可磨灭的贡献,但是实现方式不一样。回到这里,我们知道,read后并不阻塞,而是启动了线程去读取文件。这时主线程会往下执行。那么,文件什么时候读好呢?看下Future里面的方法,他是个接口需要我们实现。里面有一个isDone(),返回true的时候,就说明任务已经完成。在主线程里,如果我们必须要用到read后的执行结果了,可以去使用Future对象的isDone()方法查询是否已完成,完成就可以使用Future对象中的get()来获取结果,没有完成再处理别的逻辑,或者wait!。这种是典型的nio非阻塞写法。

      在app启动优化中,我使用这种方式。我们知道,本来放在启动中执行的代码,就是非常重要的初始化,而且前后依赖关系非常强,后面的代码依赖前面代码的执行结果,而且这些代码非要在主线程中执行。
      在主线程中必须执行的代码,还依赖一个可以在线程中执行的任务的结果时,我们就可以采用这种Future未来式的方案。启动线程和主线程并发执行任务,当主线程遇到必要结果时,则等待线程处理结果。
      在android没有这样的api,使用的是CountDown方式

    • Callback就是经常用的回调方式。但是无法与主线程通信。在AsynchronousFileChannel中,CompletionHandler接口就是这个回调,它有成功和失败两个方法。

    • AsynchronousServerSocketChannel,AsynchronousSocketChannel和上面的处理方式差不多。
  7. java.nio.channels包中还很有多好用的东西,用到的时候再去看也不晚。有网络相关操作的,先去库里八一八。

  8. 新的并发组件,可以去java.util.concurrent中看一下。里面大部分还都是1.5就已经加入了的。如果有并发需求,建议还是把java.util.concurrent库中的类全部熟悉一遍绝对可以事半功倍,concurrent库算是java里面比较高端的库了,简单调几个熟悉的说一下。

    • Future就是这个库里面
    • 我们熟悉的Executor套件,也都是这个库里面的。我曾经自己写过线程池,原因就是不知道它。可见知识空白多么可怕。
    • ConcurrentMap接口的实现类,都是自带并发安全的数据类。在并发环境下可以直接使用,不要再写synchronized来互斥读写了。
    • BlockingQueue:自带阻塞的生产者/消费者模式队列。
    • fock/join模型ForkJoinTask:可以拆分一些互相不依赖结果的任务并发执行,全部执行完毕后,合并所有并发的结果再继续执行。app启动明显就是这样的。从Application的oncreate执行为开始,到第一个界面的oncreate执行为结束。这个开始和结束中间执行的代码,如果是互相不依赖结果,并且不是必须在主线程中执行的代码,可以拆分任务,使用fock/join模型并发执行。在主线程中,还有一种拆分场景:我们知道,主线程执行都是由Looper死循环取出message来执行的,当其中一个message执行时间过长就会出现ANR,如果把一个执行时间为3s(当然在android中不太可能在主线程执行一个3s的方法,这里只是举个栗子)的方法,拆分成10个执行时间为300ms的任务分别扔到Looper中执行,是不是可以降低ANR的几率?分析过EventBus的代码,它就是这么干的。fock/join参考文章
    • java.util.concurrent和其子包还有很多好玩的东西,这里不一一复述了。
  9. 新的反射api,方法句柄MethodHandler,这是java 7新加入的包,java.lang.invoke

    • 和反射机制不同,方法句柄是JVM层面的调用,而反射是java api。
    • 经查询网络资料,在有预热(编译模式和解释模式:区别为有无JIT参与。现在虚拟机都是混合模式)情况下,JIT干预编译,大批量调用下,反射比MethodHandler更快。
    • 在无预热情况下,MethodHandler更快,和普通调用相差不大。
    • 看起来是个鸡肋,但是,我觉得java新加入这样一个工具包不会毫无用处吧?最起码套路代码比反射要简单许多。
  10. 新的字节码命令:invokedynamic,这玩意可以延迟决定调用哪个方法,这是虚拟机层面的。java代码层面无法触发,ASM这样的能修改字节码的类库已经都有支持了。
  11. 新的垃圾回收器G1,可以干预部分细节的垃圾回收期,例如:GC时所有线程暂定的时间和GC频率。

总结

这都快Java 9了,java7都没了解过,想想其实也是有点尴尬。因为从事android开发,对java原生的迭代并不敏感。话说,android的java原生支持更新也太慢了。写这篇博文的时候,迎来好消息,Android Studio迎来了3.0的更新

Support for Java 8 libraries and Java 8 language features (without the Jack compiler).

看到这个真是略感开心。下周要紧急的看Java8相关的东西了。

说回这篇博文有关的Java 7,给我的最大感受就是JVM越来越不是Java的了。就像开篇讲的那样,Java的规范和JVM规范彻底分开,在看看那JVM相关语言的海盗图,可怕,再不学几门JVM语言就老了。既然是Anroid开发,就从Kotlin开始吧!这篇博文还有很多不足之处,有空再回来添加,修改吧。

下片博文撸下Java8的东西,记录一下。