1. 简介
毫无疑问,Java 8是Java自Java 5(发布于2004年)之后的最重要的版本。这个版本包含语言、编译器、库、工具和JVM等方面的十多个新特性。
在本文中我们将学习这些新特性,并用实际的例子说明在什么场景下适合使用。
这个教程包含Java开发者经常面对的几类问题:
- 语言
- 编译器
- 库
- 工具
- 运行时(JVM)
2. Java语言的新特性
Java 8是Java的一个重大版本,有人认为,虽然这些新特性领Java开发人员十分期待,但同时也需要花不少精力去学习。在这一小节中,我们将介绍Java 8的大部分新特性。
2.1 Lambda表达式和函数式接口
Lambda表达式(也称为闭包)是Java 8中最大和最令人期待的语言改变。它允许我们将函数当成参数传递给某个方法,或者把代码本身当作数据处理:函数式开发者非常熟悉这些概念。
(parameters参数) -> expression表达式或方法体
paramaters:类似方法中的形参列表,这里的参数是函数式接口里的参数 ->:可理解为“被用于”的意思
Lambda的设计耗费了很多时间和很大的社区力量,最终找到一种折中的实现方案,可以实现简洁而紧凑的语言结构。
Lambda表达式可以引用类成员和局部变量(会将这些变量隐式得转换成final的),例如下列两个代码块的效果完全相同:
String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach(
( String e ) -> System.out.print( e + separator ) );
和
final String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach(
( String e ) -> System.out.print( e + separator ) );
匿名传统内部类与使用lambad比较
// 使用 java 7 排序(传统的使用匿名内部类)
private void sortUsingJava7(List<String> names) {
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
}
// 使用 java 8 排序(使用lambad)
private void sortUsingJava8(List<String> names) {
Collections.sort(names, (s1, s2) -> s1.compareTo(s2));
}
公共定义的函数式接口
a.功能性接口:Function<T, R>,有输入参数,有返回值,是对接收一个T类型参数,返回R类型的结果的方法的抽象,通过调用apply方法执行内容
需求:给定一个字符串,返回字符串长度
public class Main {
public static void main(String[] args) {
String str = "helloworld";
int length = testFun(str, (s) -> s.length());
System.out.println(length);
}
public static int testFun(String str, Function<String, Integer> fun) {
// 执行
Integer length = fun.apply(str);
return length;
}
}
实例方法引用
排序后输出
b.消费型接口:Consumer<T>
有输入参数,没返回值
对应的方法类型为接收一个参数,没有返回值
一般来说使用Consumer接口往往伴随着一些期望状态的改变
或者事件的发生,典型的forEach就是使用的Consumer接口
虽然没有任何的返回值,但是向控制台输出结果
Consumer 使用accept对参数执行行为
public class Main {
public static void main(String[] args) {
String str = "helloworld";
testCon(str,(s)->System.out.println(s));
}
public static void testCon(String str, Consumer<String> con) {
// 执行
con.accept(str);
}
}
c.供给型接口:Supplier<T>,无传入参数,有返回值,该接口对应的方法类型不接受参数,但是提供一个返回值,使用get()方法获得这个返回值
public class Main {
public static void main(String[] args) {
String str = "helloworld";
String resultStr = testSup(()->str);
System.out.println(resultStr);
}
public static String testSup(Supplier<String> sup) {
// 执行
String s = sup.get();
return s;
}
}
d.断言型接口:Predicate<T>
有传入参数,有返回值Boolean
该接口对应的方法为接收一个参数,返回一个Boolean类型值
多用于判断与过滤,使用test()方法执行这段行为
需求:输入字符串,判断长度是否大于0
public class Main {
public static void main(String[] args) {
String str = "helloworld";
Boolean flg = testSup(str,(s)->str.length()>0);
if (flg){
System.out.println("字符串大于0");
}else {
System.out.println("字符串小于0");
}
}
public static Boolean testSup(String str,Predicate<String> pre) {
// 执行
Boolean b = pre.test(str);
return b;
}
}
2.2 lambad表达式优点
a.极大的减少代码冗余,同时可读性也好过冗长的匿名内部类
b.与集合类批处理操作结合,实现内部迭代,并充分利用现代多核CPU进行并行计算。之前集合类的迭代都是外部的,即客户代码。而内部迭代意味着由Java类库来进行迭代,而不是客户代码
流的性能测试:https://mp.weixin.qq.com/s/-p-N-K_oY-vGtvBIKUqShA
2. Java官方库的新特性
Java 8增加了很多新的工具类(date/time类),并扩展了现存的工具类,以支持现代的并发编程、函数式编程等。
2.1 Optional
Java应用中最常见的bug就是空值异常。在Java 8之前,Google Guava引入了Optionals类来解决NullPointerException,从而避免源码被各种null检查污染,以便开发者写出更加整洁的代码。Java 8也将Optional加入了官方库。
接下来看一点使用Optional的例子:可能为空的值或者某个类型的值:
Optional< String > fullName = Optional.ofNullable( null );
System.out.println( "Full Name is set? " + fullName.isPresent() );
System.out.println( "Full Name: " + fullName.orElseGet( () -> "[none]" ) );
System.out.println( fullName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
如果Optional实例持有一个非空值,则isPresent()方法返回true,否则返回false;orElseGet()方法,Optional实例持有null,则可以接受一个lambda表达式生成的默认值;map()方法可以将现有的Opetional实例的值转换成新的值;orElse()方法与orElseGet()方法类似,但是在持有null的时候返回传入的默认值。
上述代码的输出结果如下:
Full Name is set? false
Full Name: [none]
Hey Stranger!
2.2 Streams
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
strings.forEach(s -> System.out.println(s));//全部输出
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());//经过过滤后输出
filtered.forEach(s -> System.out.println(s));
A.流中使用forEach来迭代流中的每个数据
Random random = new Random();
random.ints().limit(10).forEach(System.out :: println);
List<String> strings1 = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
long count = strings1.stream().filter(s -> s.isEmpty()).count();
B.Filter方法用于通过设置的条件过滤出元素(见上面程序)
C.limit 方法用于获取指定数量的流见下面程序(获取10个数)
Random random2 = new Random();
random2.ints().limit(10).sorted().forEach(System.out::println);
D.sorted 方法用于对流进行排序,见上面程序
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
Long count2 = strings.parallelStream().filter(string -> string.isEmpty()).count();
System.out.println("并行(parallel)程序空字符串的数量" + count2);
E.并行处理字符串(与fork/join原理一样,利用多核CPU)
List<String> strings3 = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
List<String> filtered1 = strings3.stream().filter(s -> !s.isEmpty()).collect(Collectors.toList());
System.out.println("筛选列表: " + filtered1);
//将流中的元素放置到一个列表集合中去。这个列表默认为ArrayList
String mergedString = strings3.stream().filter(s -> !s.isEmpty()).collect(Collectors.joining(","));
System.out.println("合并字符串: " + mergedString);
//joining的目的是将流中的元素全部以字符序列的方式连接到一起,可以指定连接符,甚至是结果的前后缀。
2.3 Nashorn JavaScript引擎
Java 8提供了新的Nashorn JavaScript引擎,使得我们可以在JVM上开发和运行JS应用。Nashorn JavaScript引擎是javax.script.ScriptEngine的另一个实现版本,这类Script引擎遵循相同的规则,允许Java和JavaScript交互使用,例子代码如下:
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName( "JavaScript" );
System.out.println( engine.getClass().getName() );
System.out.println( "Result:" + engine.eval( "function f() { return 1; }; f() + 1;" ) );
这个代码的输出结果如下:
jdk.nashorn.api.scripting.NashornScriptEngine
Result: 2
2.4 Base64
对Base64编码的支持已经被加入到Java 8官方库中,这样不需要使用第三方库就可以进行Base64编码,例子代码如下:
package com.javacodegeeks.java8.base64;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class Base64s {
public static void main(String[] args) {
final String text = "Base64 finally in Java 8!";
final String encoded = Base64
.getEncoder()
.encodeToString( text.getBytes( StandardCharsets.UTF_8 ) );
System.out.println( encoded );
final String decoded = new String(
Base64.getDecoder().decode( encoded ),
StandardCharsets.UTF_8 );
System.out.println( decoded );
}
}
这个例子的输出结果如下:
QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
Base64 finally in Java 8!
新的Base64API也支持URL和MINE的编码解码。
(Base64.getUrlEncoder() / Base64.getUrlDecoder(), Base64.getMimeEncoder() / Base64.getMimeDecoder())。
BASE64不是用来加密的,是BASE64编码后的字符串,例如:在上传图片时候就会将图片转成base64,之前用过的freemark生成world文档就是。全部都是由标准键盘上面的常规字符组成,这样编码后的字符串在网关之间传递不会产生UNICODE字符串不能识别或者丢失的现象。你再仔细研究下EMAIL就会发现其实EMAIL就是用base64编码过后再发送的。然后接收的时候再还原。
2.5 并行数组
Java8版本新增了很多新的方法,用于支持并行数组处理。最重要的方法是parallelSort(),可以显著加快多核机器上的数组排序。下面的例子论证了parallexXxx系列的方法:
package com.javacodegeeks.java8.parallel.arrays;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
public class ParallelArrays {
public static void main( String[] args ) {
long[] arrayOfLong = new long [ 20000 ];
Arrays.parallelSetAll( arrayOfLong,
index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
i -> System.out.print( i + " " ) );//取前10位数
System.out.println();
Arrays.parallelSort( arrayOfLong );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
i -> System.out.print( i + " " ) );//排序后取前10位数
System.out.println();
}
}
上述这些代码使用parallelSetAll()方法生成20000个随机数,然后使用parallelSort()方法进行排序。这个程序会输出乱序数组和排序数组的前10个元素。上述例子的代码输出的结果是:
Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378
Sorted: 39 220 263 268 325 607 655 678 723 793
3. 新的Java工具
Java 8提供了一些新的命令行工具,这部分会讲解一些对开发者最有用的工具。
3.1 Nashorn引擎:jjs
jjs是一个基于标准Nashorn引擎的命令行工具,可以接受js源码并执行。例如,我们写一个func.js文件,内容如下:
function f() {
return 1;
};
print( f() + 1 );
可以在命令行中执行这个命令:jjs func.js
,控制台输出结果是:
2
如果需要了解细节,可以参考官方文档。
3.2 类依赖分析器:jdeps
jdeps是一个相当棒的命令行工具,它可以展示包层级和类层级的Java类依赖关系,它以.class文件、目录或者Jar文件为输入,然后会把依赖关系输出到控制台。
我们可以利用jedps分析下Spring Framework库,为了让结果少一点,仅仅分析一个JAR文件:org.springframework.core-3.0.5.RELEASE.jar。
jdeps org.springframework.core-3.0.5.RELEASE.jar
这个命令会输出很多结果,我们仅看下其中的一部分:依赖关系按照包分组,如果在classpath上找不到依赖,则显示"not found".
org.springframework.core-3.0.5.RELEASE.jar -> C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar
org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar)
-> java.io
-> java.lang
-> java.lang.annotation
-> java.lang.ref
-> java.lang.reflect
-> java.util
-> java.util.concurrent
-> org.apache.commons.logging not found
-> org.springframework.asm not found
-> org.springframework.asm.commons not found
org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar)
-> java.lang
-> java.lang.annotation
-> java.lang.reflect
-> java.util
其他补充
Lambda的设计者们为了让现有的功能与Lambda表达式良好兼容,考虑了很多方法,于是产生了函数接口这个概念。函数接口指的是只有一个函数的接口,这样的接口可以隐式转换为Lambda表达式。java.lang.Runnable和java.util.concurrent.Callable是函数式接口的最佳例子。在实践中,函数式接口非常脆弱:只要某个开发者在该接口中添加一个函数,则该接口就不再是函数式接口进而导致编译失败。为了克服这种代码层面的脆弱性,并显式说明某个接口是函数式接口,Java 8 提供了一个特殊的注解@FunctionalInterface(Java 库中的所有相关接口都已经带有这个注解了),举个简单的函数式接口的定义:
@FunctionalInterface
public interface Functional {
void method();
}
不过有一点需要注意,默认方法和静态方法不会破坏函数式接口的定义,因此如下的代码是合法的。
@FunctionalInterface
public interface FunctionalDefaultMethods {
void method();
default void defaultMethod() {
}
}
Lambda表达式作为Java 8的最大卖点,它有潜力吸引更多的开发者加入到JVM平台,并在纯Java编程中使用函数式编程的概念。如果你需要了解更多Lambda表达式的细节,可以参考官方文档。
Date/Time API(JSR 310)
Java 8引入了新的Date-Time API(JSR 310)来改进时间、日期的处理。时间和日期的管理一直是最令Java开发者痛苦的问题。java.util.Date和后来的java.util.Calendar一直没有解决这个问题(甚至令开发者更加迷茫)。
1、本地化日期时间 API
public class Main {
public static void main(String[] args) {
Main java8tester = new Main();
java8tester.testLocalDateTime();
}
public void testLocalDateTime(){
// 获取当前的日期时间
LocalDateTime currentTime = LocalDateTime.now();
System.out.println("当前时间: " + currentTime);
LocalDate date1 = currentTime.toLocalDate();
System.out.println("date1: " + date1);
Month month = currentTime.getMonth();
int day = currentTime.getDayOfMonth();
int seconds = currentTime.getSecond();
System.out.println("月: " + month +", 日: " + day +", 秒: " + seconds);
LocalDateTime date2 = currentTime.withDayOfMonth(10).withYear(2012);
System.out.println("date2: " + date2);
// 12 december 2014
LocalDate date3 = LocalDate.of(2014, Month.DECEMBER, 12);
System.out.println("date3: " + date3);
// 22 小时 15 分钟
LocalTime date4 = LocalTime.of(22, 15);
System.out.println("date4: " + date4);
// 解析字符串
LocalTime date5 = LocalTime.parse("20:15:30");
System.out.println("date5: " + date5);
}
}
输出结果
2、使用时区的日期时间API
public class Main {
public static void main(String[] args) {
Main java8tester = new Main();
java8tester.testZonedDateTime();
}
public void testZonedDateTime(){
// 获取当前时间日期
ZonedDateTime date1 = ZonedDateTime.parse("2015-12-03T10:15:30+05:30[Asia/Shanghai]");
System.out.println("date1: " + date1);
ZoneId id = ZoneId.of("Europe/Paris");
System.out.println("ZoneId: " + id);
ZoneId currentZone = ZoneId.systemDefault();
System.out.println("当期时区: " + currentZone);
}
}
待分析: