译注:但见新人笑,哪闻旧人哭。在大家都在兴致勃勃的讨论Java 8的时候,那个早被遗忘的Java 7,或许你从来都没有记得它的好。
Java 8的发布也有一个月了,我相信现在大家都在探索JDK 8中的新特性。但是,在你彻底开始钻研Java 8之前,最好先来回顾下Java 7有哪些新特性。如果你还记得的话,Java 6是没有增加任何特性的,只是JVM的一些改动以及性能的提升,不过JDK 7倒是增加了不少有助于提升开发效率的很给力的特性。我现在写这篇文章的目的是什么呢?为什么别人都在讨论Java 8的时候,我却还在聊Java1.7的事?因为我认为并不是所有的Java开发人员都很清楚JDK 7中的改动,还有什么时候比新版本发布的时候更适合介绍上一版本的特性的呢?我还很少看见有开发人员在代码中使用自动资源管理(ARM),尽管IDE的辅助工具都已经支持这个特性了。不过确实看到有人在用string的switch功能以及<>在做类型推导,另外,也很少有人知道fork-join框架,或者在一个catch块里捕获多个异常,或者在数值型字面量中使用下划线。因此我借这个机会来写一篇简短的摘要,回顾一下这些能方便我们日常开发工作的改动。NIO以及新的文件接口,还有很多API层面的改动也同样值得关注。我相信和Java 8的lambda表达式结合起来后,写出来的代码肯定会更加简洁。
类型推导
JDK 1.7引入一个新的操作符<>,也被称作钻石操作符,它使得构造方法也可以进行类型推导 。在Java 7之前,类型推导只对方法可用,正如Joshua Bloch在Effiective Java第二版中所预言 的那样,现在终于在构造方法中实现了。在这之前,你得在对象创建表达式的左右两边同时指定类型,现在你只需要在左边指定就可以了,就像下面这样。
JDK 7之前
Map<String, List<String>> employeeRecords = new HashMap<String, List<String>>(); List<Integer> primes = new ArrayList<Integer>();
JDK 7
Map<String, List<String>> employeeRecords = new HashMap<>(); List<Integer> primes = new ArrayList<>();
在Java 7中可以少敲些代码了,尤其是在使用集合的时候,因为那里大量用到了泛型。点击这里了解更多关于Java钻石操作符的信息。(译注:原文没提供链接啊)
在switch中支持String
在JDK 7之前 ,只有整型才能用作switch-case语句的选择因子。在JDK7中,你可以将String用作选择因子了。比如:
String state = "NEW"; switch (day) { case "NEW": System.out.println("Order is in NEW state"); break; case "CANCELED": System.out.println("Order is Cancelled"); break; case "REPLACE": System.out.println("Order is replaced successfully"); break; case "FILLED": System.out.println("Order is filled"); break; default: System.out.println("Invalid"); }
比较的时候会用到String的equals和hashCode()方法,因此这个比较是大小写敏感的。在switch中使用String的好处是,和直接用if-else相比 ,编译器可以生成更高效的代码。更详细的说明请点击这里。
自动资源管理(Automatic Resource Management)
在JDK 7之前,我们需要使用一个finally块,来确保资源确实被释放掉,不管try块是完成了还是中断了。比如说读取文件或者输入流的时候,我们需要在finally块中关闭它们,这样会导致很多的样板代码,就像下面这样:
public static void main(String args[]) { FileInputStream fin = null; BufferedReader br = null; try { fin = new FileInputStream("info.xml"); br = new BufferedReader(new InputStreamReader(fin)); if (br.ready()) { String line1 = br.readLine(); 09 System.out.println(line1); } } catch (FileNotFoundException ex) { System.out.println("Info.xml is not found"); } catch (IOException ex) { System.out.println("Can't read the file"); } finally { try { if (fin != null) fin.close(); if (br != null) br.close(); } catch (IOException ie) { System.out.println("Failed to close files"); } } }
看下这段代码 ,是不是很多样板代码?
而在Java 7里面,你可以使用try-with-resource的特性来自动关闭资源,只要是实现了AutoClosable和Cloaeable接口的都可以,Stream, File, Socket,数据库连接等都已经实现了。JDK 7引入了try-with-resource语句,来确保每个资源在语句结束后都会调用AutoCLosable接口的close()方法进行关闭。下面是Java 7中的一段示例代码,它看起来可是简洁多了:
public static void main(String args[]) { try (FileInputStream fin = new FileInputStream("info.xml"); BufferedReader br = new BufferedReader(new InputStreamReader(fin));) { if (br.ready()) { String line1 = br.readLine(); System.out.println(line1); } } catch (FileNotFoundException ex) { System.out.println("Info.xml is not found"); } catch (IOException ex) { System.out.println("Can't read the file"); } }
由于Java负责关闭那些打开的资源比如文件和流这种,因此文件描述符泄露的事情应该不会再发生了,应该也不会再看到文件描述符错误的提示了。甚至JDBC 4.1都已经开始支持了AutoClosable了。
Fork Join框架
Fork/join框架是ExecutorService接口的实现,它使得你可以充分利用现代服务器多处理器带来的好处。这个框架是为了那些能递归地拆分成更小任务的工作而设计的。它的目标是去压榨处理器的能力以提升程序的性能。就像别的ExecutorService的实现一样,fork/join框架也是把任务分发给线程池中的多个线程。它的不同之处在于它使用的是一种工作窃取算法(work-stealing algorithm),这和生产者消费者的算法有很大的不同。已经处理完任务的工作线程可以从别的繁忙的线程那里窃取一些任务来执行。fork/join框架的核心是ForkJoinPool类,它继承自AbstractExecutorService。ForkJoinPool类实现了核心的工作窃取算法,可以执行ForkJoinTask进程。你可以把代码封装在一个ForkJoinTask的子类里,比如RecursiveTask或者RecursiveAction。更多信息就参考这里。
数值字面量中使用下划线
JDK 7中,你可以在数值字面量中使用'_'来提升可读性。这对在源代码中使用了大数字的人来说尤其有用,例如在金融或者计算领域中。比方说这么写,
int billion = 1_000_000_000; // 10^9 long creditCardNumber = 1234_4567_8901_2345L; //16 digit number long ssn = 777_99_8888L; double pi = 3.1415_9265; float pif = 3.14_15_92_65f;
你可以在合适的位置插入下划线使得它可读性更强,比如说一个很大的数字可以每隔三位放一个下划线,对于信用卡卡号而言,通常是16位长度,你可以每隔4个数字就放一个下划线,就如它们在卡片上所显示的那样。顺便说一句,要记住,你不能在小数后面,或者数字的开始和结束的地方放下划线。比如说,下面的数值字面量就是不正确的,因为它们错误地使用了下划线:
double pi = 3._1415_9265; // underscore just after decimal point long creditcardNum = 1234_4567_8901_2345_L; //underscore at the end of number long ssn = _777_99_8888L; //undersocre at the beginning
你可以读下我的这篇文章了解更多的一些关于下划线使用的例子。
在一个catch块中捕获多个异常
JDK 7中,单个catch块可以处理多个异常类型。
比如说在JDK 7之前,如果你想捕获两种类型的异常你得需要两个catch块,尽管两个的处理逻辑都是一样的:
try { ...... } catch(ClassNotFoundException ex) { ex.printStackTrace(); } catch(SQLException ex) { ex.printStackTrace(); }
而在JDK 7中,你只须使用一个catch块就搞定了,异常类型用‘|’进行分隔:
try { ...... } catch(ClassNotFoundException|SQLException ex) { ex.printStackTrace(); }
顺便说一句,这种用法是不包括异常的子类型的。比如说,下面这个多个异常的捕获语句就会抛出编译错误:
try { ...... } catch (FileNotFoundException | IOException ex) { ex.printStackTrace(); }
这是因为FileNotFoundException是IOException 的子类,在编译的时候会抛出下面的错误: java.io.FileNotFoundException is a subclass of alternative java.io.IOException at Test.main(Test.java:18)。
了解更多请点击这里。
使用"ob"前缀的二进制字面量
JDK7中,对于整型类型(byte, short, int 和long)来说,你可以用'0b'前缀来表明这是一个二进制的字面量,就像C/C++中那样。在这之前,你只能使用8进制(前缀'0')或者16进制(前缀是'0x'或者‘0X')的字面量。
int mask = 0b01010000101;
这样写好处更明显:
int binary = 0B0101_0000_1010_0010_1101_0000_1010_0010;
8.Java NIO 2
Java SE 7中引入了java.nio.file包,以及相关的java.nio.file.attibute包,全面支持了文件IO以及对默认文件系统的访问。它同时还引入了Path 类,你可以用它来代表操作系统中的任意一个路径。新的文件系统API兼容老的版本,并且提供了几个 非常实用的方法,可以用来检查,删除,拷贝和移动文件。比如,你可以在Java中判断一个文件是否是隐藏文件。你还可以在Java中创建软链接和硬链接。JDK 7的新的文件API还能够使用通配符来进行文件的搜索。你还可以用它来监测某个目录 是否有变动。我推荐你看下它的官方文档来了解更多的一些有意思的特性。
G1垃圾回收器
JDK7中引入了一个新的垃圾回收器,G1,它是Garbage First的缩写。G1回收器优先回收垃圾最多的区域。为了实现这个策略它把堆分成了多个区域,就好比Java 7之前分成三个区域那样(新生代,老生代和持久代)。G1回收器是一个可预测的回收器,同时对那些内存密集型的程序它还能保证较高的吞吐量。
重抛异常的改进
Java SE 7的编译器和之前的版本相比,在重新抛出异常这块进行了更精确的分析。这使得你在方法声明的throws子句中可以指定更精确的异常类型。在JDK 7之前,重抛的异常的类型被认为是catch参数中的指定的异常类型。比如说,如果你的try块中抛出了一个ParseException以及一个IOException,为了捕获所有的异常,然后重新抛出来,你会去捕获Exception类型的异常,并且声明你的方法抛出的异常类型是Exception。这种方式有点不太精确,因为你实际抛出的是一个通用的Exception类型,而调用你的方法的语句需要去捕获这个通用的异常。看一下Java 1.7前的这段异常处理的代码可能你会更明白些:
public void obscure() throws Exception{ try { new FileInputStream("abc.txt").read(); new SimpleDateFormat("ddMMyyyy").parse("12-03-2014"); } catch (Exception ex) { System.out.println("Caught exception: " + ex.getMessage()); throw ex; } }
JDK 7以后你就可以在方法的throws子句中明确的指定异常类型了。精确的异常重抛指的是,如果你在catch块中重新抛出异常,实际真正抛出的异常类型会是:
你的try块抛出的异常
还没有被前面的catch块处理过,并且
catch的参数类型是Exception的某个子类。
这使得异常重抛变得更精确。你可以更准确的知道方法抛出的是何种异常,因此你可以更好的处理它们,就像下面这段代码这样:
public void precise() throws ParseException, IOException { try { new FileInputStream("abc.txt").read(); new SimpleDateFormat("ddMMyyyy").parse("12-03-2014"); } catch (Exception ex) { System.out.println("Caught exception: " + ex.getMessage()); throw ex; } }
Java SE 7的编译器允许你在preciese() 方法声明的throws子句中指定ParseException和IOException类型,这是因为你抛出的异常是声明的这些异常类型的父类,比如这里我们抛出的是java.lang.Exception,它是所有受检查异常的父类。有的地方你会看到catch参数中带final关键字,不过这个不再是强制的了。
这些就是JDK 7中所有你应该回顾的内容了。这些新特性对写出整洁的代码以及提升开发效率非常有用。有了Java 8中的lambda表达式,Java中的代码整洁之道则又上了一个新的里程碑。如果你认为我这篇文章中漏掉了Java 1.7任何有用的特性,请记得提醒我。
P.S. 如果你喜欢读书的话,那你也一定会喜欢Packet Publication的这本Java 7 New features Cookbook。