1. Lambda 表达式
作用:把函数作为方法的参数,让代码变得更加简洁和紧凑。
语法格式:
// 第一种(parameters) -> expression/* parameters 表示参数 expression 表示表达式*/// 第二种(parameters) -> {statements;}/* statements 表示 Java 程序代码 */
这里的格式可以和 if 表达式加不加括号的情况来对照理解。当 if 判断条件下只有一行代码的时候可以不用加大括号,如果是有多行代码就必须要加上大括号。
然后,我们结合几个例子来看看 Lambda 表达式的一些特征:
// 1. 无参数,有返回值 => 5() -> 5// 2. 一个参数,有返回值 => 2 * xx -> 2 * x// 3. 两个参数,并且指定 int 类型,返回两者之和(int x, int y) -> x + y// 4. 一个参数,调用 print 方法输出(String s) -> System.out.print(s)
从上面的例子里简单归纳下 Lambda 表达式的特征:
- 可选的类型声明:不需要声明参数类型,编译器可以统一识别参数
- 可选的参数小括号:一个参数不需要用小括号,多个参数必须要
- 可选的大括号:这一点上面提到了,跟
if情况一样 - 可选的返回关键字:如果主体只有一个表达式、返回值不需要加
return关键字,如果加上了大括号就必须用return返回
简单熟悉了 Lambda 表达式的语法格式和特性之后,我们来看看怎么实现一个 Lambda 表达式的例子。
// 省略类和依赖等@Testpublic void test1() throws InterruptedException {// Runnable runnable = () -> log.info("start a new thread...");Thread thread = new Thread(() -> log.info("start a new thread..."));thread.start();thread.join();}
上面我们使用 JUnit 测试来用 Lambda 表达式实现了启动一个线程,然后输出一行日志。
如果光是看这个可能不是很明显,我们再使用 Java 8 以前的方式来重新实现这个功能对比下代码。
// 省略类和依赖等@Testpublic void test2() throws InterruptedException {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {log.info("start a new thread...");}});thread.start();thread.join();}
可以看到,两种方法的主要区别在于子线程的实现部分。Java 8 之前需要明确声明重写接口的具体方法,然后在方法体里面写业务代码;而使用 Lambda 表达式之后,我们就只需要关注方法具体的实现,而不用显示指定重写某个方法。
以上就是 Lambda 表达式最简单的表现形式,就是这么简单。
看到这里,是不是觉得:就这?就这? 别慌,还有新特性可以跟 Lambda 表达式发生化学反应的,一旦结合起来就像是得到了哆啦 A 梦的百宝袋。
2. 方法引用
作用:通过方法的名字来指向一个方法,让代码更加紧凑。
语法格式:类名和方法之前使用 :: 来连接,如下:
/*** 方法引用测试类*/@Slf4jpublic class MethodTest {/*** 方法引用 new 一个实例* 需要借助 Supplier 函数式接口来实现*/@Testpublic void test1() {UserService userService = UserService.create(UserService::new);}/*** 方法引用调用实例方法* 需要借助 Consumer 函数式接口来实现*/@Testpublic void test2() {UserService userService = UserService.create(UserService::new);;List<UserService> list = Arrays.asList(userService);list.forEach(UserService::checked);}}@Slf4jclass UserService {/*** 创建一个 UserService* @param supplier* @return*/public static UserService create(Supplier<UserService> supplier) {log.info("created a new UserService...");return supplier.get();}/*** user checked* @return*/public void checked() {log.info("UserService checked....");}}
这个方法引用可能不是很好理解,因为它要借助下面才会提到的函数式接口来实现。
上面的例子只用到了 Supplier 和 Consumer 函数式接口,前者表示供应型接口,只提供对象给别人使用;后者表示消费型接口,只消费别人提供的对象。
方法引用本质上就是在函数式接口基础之上对方法调用的一种简化方式,使用 :: 来作为连接符。
3. 函数式接口
作用:更好的支持 Lambda 表达式,可以实现更加复杂的函数式编程。
一般使用 @FunctionalInterface 注解在接口上标注,表示这个接口式函数式接口。
我们重点来看下面 4 种函数式接口,都是 Java 8 新增的,另外一些大多都是根据这 4 种类型来衍生的。
其他的诸如上面提到的 Runnable 等接口虽然也是,但是在 Java 8 之前就已经存在了,而且上面也用 Lambda 表达式演示过了,没有太多花样。
Supplier 供应型接口:无参数,有返回值
@FunctionalInterfacepublic interface Supplier<T> {T get();}
Consumer 消费型接口:有参数,无返回值
@FunctionalInterfacepublic interface Consumer<T> {void accept(T t);}
Function 功能型接口:有参数,有返回值
@FunctionalInterfacepublic interface Function<T, R> {R apply(T t);}
Predicate 断言型接口:有参数,有返回值(布尔类型),一般用于判断、预测
@FunctionalInterfacepublic interface Predicate<T> {boolean test(T t);}
有了这些函数式接口,我们就可以配合上 Lambda 表达式去实现函数式编程——也就是把函数表达式当作参数交给程序去执行。
先看几个简单的例子吧。
@Testpublic void test3() {// Consumer 输出字符串Consumer<String> consumer = System.out::println;consumer.accept("Hotstrip");// Supplier 生成一个字符串Supplier<String> supplier = () -> "new String";log.info(supplier.get());// Function 计算数字 * 2Function<Integer, Integer> function = x -> x * 2;log.info("function result is: {}", function.apply(5));// Predicate 断言字符串长度Predicate<String> predicate = s -> s.length() > 5;log.info("predicate result is: {}", predicate.test("Hello World"));}
从上面的例子可以看出函数式接口是如何使用的——需要用合适的函数式接口去接收一个 Lambda 表达式实现的方法体,然后调用接口实现的方法。
虽然从代码表现上似乎没有原来那种写法方便和易懂,那是因为一般也不会这样去使用,这里只是为了单纯介绍这几种函数式接口该怎么用。
在 Java 8 里面函数式接口都已经和常用的类完美结合了,使用起来不会像上面那样觉得既不简洁又难以理解。
再看个稍微复杂点的例子吧。
假如我们现在需要对这几个姓名借助 Comparator 接口来排序:Hotstrip Stormzhang Allen Zhang TomKeeper 。
先简单梳理下逻辑:
- 构造需要排序的数据,字符串数组或者字符串集合
- 自定义排序算法,使用
Comparator接口,这个接口也是函数式接口哦 - 对数据进行排序,输出结果
我们先使用 Java 8 以前的方式来实现:
@Testpublic void test3() {String[] names = new String[] {"Hotstrip", "Stormzhang", "Allen Zhang", "TomKeeper"};// 实现排序算法Comparator<String> comparator = new Comparator<String>() {@Overridepublic int compare(String s1, String s2) {return s1.compareTo(s2);}};// 对数据进行排序Arrays.sort(names, comparator);// 输出结果for (String name : names) {System.out.println(name);}}
再使用 Java 8 的 Lambda 表达式和函数式接口来实现:
@Testpublic void test4() {List<String> list = Arrays.asList("Hotstrip", "Stormzhang", "Allen Zhang", "TomKeeper");// 自定义排序算法,并且对数据排序list.sort((s1, s2) -> s1.compareTo(s2));// 甚至还可以简化成方法引用方式// list.sort(String::compareTo);// 输出结果list.forEach(System.out::println);}
对比上面的代码,自定义排序都是去实现接口的方法,不同的是 Java 8 以前需要实现类,重写方法体;而 Java 8 之后就可以借助函数式接口来使用 Lambda 表达式实现方法体。虽然本质上还是一样的原理,但是在代码编写上的确是简洁了很多。
再加上方法引用的特性,还可以让代码变得更加紧凑。虽然这样的写法一开始很难理解,但是它的表现行为只有有限的几种,多用几次也就能熟稔于心了。
最最重要的是,不管代码在表现上是不是变得更加简洁,本质上的原理依然没有变化,可以算是新的语法糖。
从上面的一些例子上看,不难发现 Lambda 表达式和方法引用,以及函数式接口都不是独立存在的,往往是经常会一起使用,用简洁的代码实现复杂的业务逻辑。 当然,还包括下面提到的 Stream API 也是会经常一起来搭配使用的。
4. Stream API
作用:把需要处理的数据当作一种流,流在管道中传输,并且可以在管道的节点上处理数据,比如过滤、排序、聚合等操作。
Stream API 操作的数据流可以来源于集合、数组、IO 等,一般的使用方式是使用 Collection 接口类里面新增的 default 方法 sream 和 parallelStream,分别代表串行流何并行流。
default Stream<E> stream() {return StreamSupport.stream(spliterator(), false);}default Stream<E> parallelStream() {return StreamSupport.stream(spliterator(), true);}
这里的 default 方法也是 Java 8 的新特性之一,跟其他不带 default 方法的区别就是作用在接口类里面,可以有具体的方法实现。
我们重点关注串行流,主要是熟悉它对应的管道节点处理 API 方法。
forEach
在集合里面,最常用的就是遍历元素了。在 Stream API 里面也有对应的遍历操作,也就是 forEach。
该 API 方法在 Java 8 里 Iterable 接口中添加了 default forEach 方法。
default void forEach(Consumer<? super T> action) {Objects.requireNonNull(action);for (T t : this) {action.accept(t);}}
从源代码上看,使用了函数式接口作为方法的入参,底层还是使用了 for 循环。
使用上也很简单,在上面的排序的例子上就已经使用 forEach 输出过集合里面的元素了。
map
map 方法用于映射每个元素到对应的结果,以下代码片段使用 map 输出了元素对应的平方数:
@Testpublic void test6() {List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);// 获取对应的平方数List<Integer> squaresList = numbers.stream().map( i -> i*i).collect(Collectors.toList());squaresList.forEach(System.out::println);}
这个例子先是利用数组工具类生成了一个 List 集合对象,然后把集合对象转换成流,调用 map 方法去处理每个元素,最后使用 Colletors 把结果收集起来。
先不用去管 Collectors 收集器,我们后面会介绍。
我们看 map 方法的写法,很明显是一个 Lambda 表达式,目的也很明确,直接对元素 i 赋值为 i 的平方数。
从代码实现上看,map 方法本身也是 Stream 接口里面的方法。
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
这里需要注意的是 map 方法返回的依然是一个流,也就是说调用了 map 方法之后还可以继续调用其他 Stream API。
filter
filter 方法是 Stream 里面的过滤器,目的是过滤出需要的元素,源代码跟上面的 map 差不多。
Stream<T> filter(Predicate<? super T> predicate);
本质上还是借助函数式接口,结合 Lambda 表达式来自定义方法的实现,一起来看个例子:
@Testpublic void test7() {List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");// 获取空字符串的数量long count = strings.stream().filter(String::isEmpty).count();log.info("count: {}", count);}
这个例子很简单,从集合里面过滤出字符串为空的元素,并且统计出具体的个数。利用来函数式接口,Lambda 表达式,以及方法引用(调用 String 类里面判断字符串是否为空的方法)。
Collectors
Collectors 是 Stream API 里面的收集器,里面的方法可以把流转换成集合或者聚合元素。
很多时候我们对集合元素进行一系列处理之后,一般会返回一个新的集合,这时候就需要用到 Collectors 收集器了。
我们看两个例子:
@Testpublic void test8() {List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());log.info("筛选列表: {}", filtered.toString());String mergedString = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(", "));log.info("合并字符串: {}", mergedString);}
这两个例子很简单,第一个我们上面见过,就是对集合按照条件筛选之后使用 Collectors 收集器返回一个新的集合,然后输出这个新集合的元素。
第二个例子就是使用 Collectors 收集器返回一个合并后的字符串,使用指定的分隔符把每个元素拼接起来。
统计
最后我们介绍下 Stream API 里面的统计操作。
比如我们现在需要对一系列的数字求和、求最大值、最小值、平均数等操作,如果使用常规的方式一般会定义多个方法去分别处理,但是 Stream API 里面提供了这样的统计方法。
@Testpublic void test9() {List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);IntSummaryStatistics stats = numbers.stream().mapToInt((x) -> x).summaryStatistics();log.info("列表中最大的数: {}", stats.getMax());log.info("列表中最小的数: {}", stats.getMin());log.info("所有数之和: {}", stats.getSum());log.info("平均数: {}", stats.getAverage());}
其他
其他还有一些 Stream API,比如说 limit、sorted 方法等,都是可以用在流里面处理的方法。
5. Optional
Optional 类是一个可以为 null 的容器对象。如果值存在则 isPresent 方法会返回 true,调用 get 方法会返回该对象。Optional 是个容器:它可以保存类型T的值,或者仅仅保存 null。Optional 提供很多有用的方法,这样我们就不用显式进行空值检测。Optional 类的引入很好的解决空指针异常。
ofNullable 和 of
这两个方法都会返回一个 Optional 对象,区别就是 of 方法会检测入参是否为 null,如果是的话就会抛出空指针异常,这也是在使用中需要注意的点。
而 ofNullable 方法检测到入参是 null 之后会调用 empty 方法专门返回一个空的 Optional 对象,这样可以保证后续调用 Optional 其他方法也不会导致空指针异常。
get
简单来说,Optional 就是把我们需要的对象又包裹了一层,所以当我们需要取到真正的对象时需要调用它的 get 方法。
// Optional.ofNullable - 允许传递为 null 参数Optional<Integer> option = Optional.ofNullable(new Integer(10));int num = option.get();
但是在实际使用的时候,我们一般会先确定 Optional 里面的对象不为空才会获取它,因为如果包裹的对象是 null 的话依然会抛出空指针异常,这时候就会用到其他的方法。
这些其他的方法本质上也是会调用 get 方法的,无非是加上了一些判断条件或者提供默认值等方式。
orElse 和 orElseGet
这两个方法是属于同一种类型的,作用是:如果存在该值就返回该值,如果不存在就根据入参来处理。
两者的区别就是入参的类型不同,orElse 的参数是一个具体的类型,orElseGet 的参数是一个供应式函数式接口 Supplier,支持 Lambda 表达式。
// Optional.ofNullable - 允许传递为 null 参数Optional<Integer> option = Optional.ofNullable(null);int num = option.orElse(new Integer(10));
isPresent 和 ifPresent
这两个都是用来判断 Optional 里面对象是否为空的方法,区别也很明显。
isPresent 是只判断里面的对象是否为空,返回值是 true 或者 false。
ifPresent 的入参是一个消费型的函数式接口 Comsumer,如果里面的对象不为空,就会执行 Comsumer 的 accept 方法消费。
@Testpublic void test10 () {Optional<UserInfo> optional = Optional.empty();optional.ifPresent(userInfo -> {// do action});}
6. 新日期时间 API
Java 8 里面新增了日期时间 API,我们都知道日期和时间 API 在 JDK 里面已经存在很久了,但是原有的 API 存在一些问题,而新的日期时间 API 就是为此而生的。
在原来的日期时间 API 里面主要有 3 个问题:
- 非线程安全:
java.util.Date是非线程安全的,所有的日期类都是可变的,这是 Java 日期类最大的问题之一。 - 设计不太合理:Java 的日期/时间类的定义并不一致,在
java.util和java.sql的包中都有日期类,此外用于格式化和解析的类在java.text包中定义。java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,将其纳入java.sql包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。 - 对时区支持不友好:日期类并不提供国际化,没有时区支持,因此 Java 引入了
java.util.Calendar和java.util.TimeZone类,但他们同样存在上述所有的问题。
在 Java 8 里面 **java.time** 包下提供了很多新的 API。
以下为两个比较重要的 API:
- Local (本地) : 简化了日期时间的处理,没有时区的问题。
- Zoned (时区) :通过制定的时区处理日期时间。
新的 java.time 包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。
LocalDate、 LocalTime、LocalDateTime 日期和时间
LocalDate 类表示一个具体的日期,但不包含具体时间,也不包含时区信息。可以通过 LocalDate 的静态方法of() 创建一个实例,LocalDate 也包含一些方法用来获取年份,月份,天,星期几等:
@Testpublic void test1() {// 初始化一个日期:2021-04-04LocalDate localDate = LocalDate.of(2021, 4, 4);// 年份:2021int year = localDate.getYear();// 月份:JANUARYMonth month = localDate.getMonth();// 月份中的第几天:4int dayOfMonth = localDate.getDayOfMonth();// 一周的第几天:WEDNESDAYDayOfWeek dayOfWeek = localDate.getDayOfWeek();// 月份的天数:31int length = localDate.lengthOfMonth();// 是否为闰年:falseboolean leapYear = localDate.isLeapYear();// 调用静态方法now()来获取当前日期LocalDate now = LocalDate.now();}
LocalTime 和 LocalDate 类似,他们之间的区别在于 LocalDate 不包含具体时间,而 LocalTime 包含具体时间,例如:
@Testpublic void test2() {// 初始化一个时间:17:23:52LocalTime localTime = LocalTime.of(17, 23, 52);// 时:17int hour = localTime.getHour();// 分:23int minute = localTime.getMinute();// 秒:52int second = localTime.getSecond();}
LocalDateTime 类是 LocalDate 和 LocalTime 的结合体,可以通过 of() 方法直接创建,也可以调用 LocalDate 的 atTime() 方法或 LocalTime 的 atDate()方法将 LocalDate 或 LocalTime 合并成一个LocalDateTime:
@Testpublic void test3() {LocalDateTime ldt1 = LocalDateTime.of(2017, Month.JANUARY, 4, 17, 23, 52);log.info("ldt1: {}", ldt1);LocalDate localDate = LocalDate.of(2017, Month.JANUARY, 4);LocalTime localTime = LocalTime.of(17, 23, 52);LocalDateTime ldt2 = localDate.atTime(localTime);log.info("ldt2: {}", ldt2);}
Instant 时刻(时间戳)
Instant 用于表示一个时间戳,它与我们常使用的 System.currentTimeMillis() 有些类似,不过 Instant 可以精确到纳秒(Nano-Second),System.currentTimeMillis() 方法只精确到毫秒(Milli-Second)。
如果查看 Instant 源码,发现它的内部使用了两个常量,seconds 表示从 1970-01-01 00:00:00 开始到现在的秒数,nanos 表示纳秒部分(nanos 的值不会超过 999,999,999)。
Instant除了使用 now() 方法创建外,还可以通过 ofEpochSecond 方法创建:
@Testpublic void test4() {// ofEpochSecond()方法的第一个参数为秒,第二个参数为纳秒,下面的代码表示从1970-01-01 00:00:00开始后两分钟的10万纳秒的时刻Instant instant = Instant.ofEpochSecond(120, 100000);log.info("instat: {}", instant);}
Duration、Period 时间段
Duration 的内部实现与 Instant 类似,也是包含两部分:seconds 表示秒,nanos 表示纳秒。
两者的区别是 Instant 用于表示一个时间戳(或者说是一个时间点),而 Duration 表示一个时间段,所以 Duration 类中不包含 now() 静态方法。可以通过 Duration.between() 方法创建 Duration 对象:
@Testpublic void test5() {LocalDateTime from = LocalDateTime.of(2017, Month.JANUARY, 5, 10, 7, 0); // 2017-01-05 10:07:00LocalDateTime to = LocalDateTime.of(2017, Month.FEBRUARY, 5, 10, 7, 0); // 2017-02-05 10:07:00Duration duration = Duration.between(from, to); // 表示从 2017-01-05 10:07:00 到 2017-02-05 10:07:00 这段时间long days = duration.toDays(); // 这段时间的总天数long hours = duration.toHours(); // 这段时间的小时数long minutes = duration.toMinutes(); // 这段时间的分钟数long seconds = duration.getSeconds(); // 这段时间的秒数long milliSeconds = duration.toMillis(); // 这段时间的毫秒数long nanoSeconds = duration.toNanos(); // 这段时间的纳秒数log.info("days: {}...hours: {}...minutes: {}...seconds: {}...milliSeconds: {}...nanoSeconds: {}",days, hours, minutes, seconds, milliSeconds, nanoSeconds);// Duration对象还可以通过of()方法创建,该方法接受一个时间段长度,和一个时间单位作为参数:Duration duration1 = Duration.of(5, ChronoUnit.DAYS); // 5天Duration duration2 = Duration.of(1000, ChronoUnit.MILLIS); // 1000毫秒log.info("duration1: {}...duration2: {}", duration1, duration2);}
Period 在概念上和 Duration 类似,区别在于 Period 是以年月日来衡量一个时间段。
比如 2 年3 个月 6 天:
Period period1 = Period.of(2, 3, 6);
Period 对象也可以通过 between() 方法创建,值得注意的是,由于 Period 是以年月日衡量时间段,所以between() 方法只能接收 LocalDate 类型的参数:
// 2017-01-05 到 2017-02-05 这段时间Period period2 = Period.between(LocalDate.of(2017, 1, 5),LocalDate.of(2017, 2, 5));
ZoneId 时区
Java 8 中的时区操作被很大程度上简化了,新的时区类 java.time.ZoneId 是原有的 java.util.TimeZone 类的替代品。ZoneId 对象可以通过 ZoneId.of() 方法创建,也可以通过 ZoneId.systemDefault() 获取系统默认时区:
ZoneId shanghaiZoneId = ZoneId.of("Asia/Shanghai");ZoneId systemZoneId = ZoneId.systemDefault();
有了 ZoneId,我们就可以将一个 LocalDate、LocalTime 或 LocalDateTime 对象转化为 ZonedDateTime 对象:
ZoneId shanghaiZoneId = ZoneId.of("Asia/Shanghai");LocalDateTime localDateTime = LocalDateTime.now();ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, shanghaiZoneId);
其他
Java 8 中的日期/时间类都是不可变的,这是为了保证线程安全。当然,新的日期/时间类也提供了方法用于创建对象的可变版本,比如增加一天或者减少一天:
LocalDate date = LocalDate.of(2017, 1, 5); // 2017-01-05LocalDate date1 = date.withYear(2016); // 修改为 2016-01-05LocalDate date2 = date.withMonth(2); // 修改为 2017-02-05LocalDate date3 = date.withDayOfMonth(1); // 修改为 2017-01-01LocalDate date4 = date.plusYears(1); // 增加一年 2018-01-05LocalDate date5 = date.minusMonths(2); // 减少两个月 2016-11-05LocalDate date6 = date.plus(5, ChronoUnit.DAYS); // 增加5天 2017-01-10
新的日期API中提供了一个 DateTimeFormatter 类用于处理日期格式化操作,它被包含在 java.time.format 包中,Java 8 的日期类有一个 format() 方法用于将日期格式化为字符串,该方法接收一个 DateTimeFormatter 类型参数:
// 日期时间转换成字符串LocalDateTime dateTime = LocalDateTime.now();String strDate1 = dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); // 2017-01-05
同样,日期类也支持将一个字符串解析成一个日期对象,例如:
// 字符串转换成日期时间String strDate2 = "2017-01-05";LocalDate date = LocalDate.parse(strDate2, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
OK,以上就是 Java 8 的新特性了,总体上来讲有 4 部分:
- 支持 Lambda 表达式来实现函数式编程
- 提供 Stream API
- Optional 来有效防止空指针
- 新的日期时间 API
