Java8--Lambda 表达式、Stream 和时间 API
Java10 已经在 2018 年发布了,Java 也步入了小步迭代的阶段。 现如今很多实际的项目中,因为 Java9 模块化变动太大,用到 Java7 和 Java8 的还是比较多,Java7 中令人印象深刻的自然是新的 I/O 操作 API 了,Java8 中则有非常重要的新特性 --Lambda 和 Stream,自从 Java8 出来之后,本人就一直在使用,下面是一个 Java8 的总结,也是一个经验的分享。
# 一、Lambda
Java没有Lambda之前,我们只能羡慕的看着Ruby或JavaScript在语法层面玩出花儿,Java天下没有闭包这一特性,广大程序员苦其久矣。
闭包我们应该都不陌生,其他语言中多有见到,在Java中,Lambda表达式一般用来简化表示一种特殊的接口:函数式接口。
所谓函数式,其实就是只定义了一个方法的接口,在Java8中用@FunctionalInterface
来修饰。
看一下我们熟悉的Runnable
,Java8的源码如下:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
平常我们把一个匿名Runnable
对象传给Thread
,如:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
}).run();
用Lambda的话:
new Thread(() -> {
System.out.println("hello");
}).run();
因为这里方法体只有一句代码,那么可以更简洁一些:
new Thread(() -> System.out.println("hello")).run();
# 1、Lambda基本结构
一个参数:
Consumer<String> consumer = event -> syso("hello "+ event)
二个参数:
Comparator<String> comp = ( first, second ) -> { return first.length() > second.length(); }
无参数:
Runable runable = () -> { syso("hello"); }
有时,可不用return,java会自动推断类型:
Comparator<String> comp = ( first, second ) -> { first.length() > second.length(); }
# 2、通用的函数式接口
为了方便,Java中定义了许多通用的函数式接口,其实以前我们在Guava中见到过这些类:
Predicate
传入一个参数,返回一个bool结果, 方法为boolean test(T t)BiPredicate
传入两个参数,返回一个bool结果, 方法为boolean test(T t, U u)Consumer
传入一个参数,无返回值,纯消费。 方法为void accept(T t)BiConsumer
传入两个参数,无返回值,纯消费。 方法为void accept(T t, U u)Function
传入一个参数,返回一个结果,方法为R apply(T t)BiFunction<T, U, R>
入两个参,返回一个值:R apply(T t, U u)
Supplier
无参数传入,返回一个结果,方法为T get()UnaryOperator
一元操作符, 继承Function,传入参数的类型和返回类型相同。BinaryOperator
二元操作符, 传入的两个参数的类型和返回类型相同, 继承BiFunction
# 3、方法引用操作符
先看一个例子:
button.setOnAction(event -> System.out.println(event))
//等价于:
button.setOnAction(System.out::println)
可以看到一个新的操作符::
,在Java8中它是方法引用操作符。
先定义一个类,便于下面举例使用:
private static class TUser {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
方法引用操作符主要是四种用法:
- 对象
::
实例方法,如
TUser tUser = new TUser();
Supplier<String> supplier = tUser::getName
//tUser::getName = tUser -> tUser.getName
- 类
::
静态方法,如
button.setOnAction(System.out::println)
//或
Math::pow = (x, y) -> Math.pow(x, y)
- 类
::
实例方法,如
this::equals
//或
Function<TUser, String> function = TUser::getName;
//TUser::getName = tUser -> tUser.getName
需要注意对象::
实例方法和类::
实例方法的不同
- 构造器引用
int[]::new = x -> new int[x]
//或
List<Object> list = new ArrayList<Object>();
list.stream().toArray(String[]::new);
# 4、接口中的默认方法
语言本身语法的改进,会带来效率方面的很大提升(羡慕其他JVM语言如Kotlin的语法糖)。例如Java5的泛型,Java7的try-cath-resource,Java8中提供了接口默认方法的特性,可以在接口中定义方法,更利于模块化。可以和Ruby的Mixin对照参考一下。
需要用default关键字,直接用JDK中的例子来看吧,拿我们熟悉的java.util.List接口来说,就有了新的接口默认方法:
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
# 5、LambdaMetafactory
LambdaMetafactory可以生成函数映射来替代反射,理论上任何的公开方法都可以使用函数映射,而且他的强大之处在于他的性能远远超过了反射调用。
在fastjson2中,LambdaMetafactory得到了大量使用,可以参考:fastjson2为什么这么快? (opens new window)
我们来看一下函数映射怎么替代反射:
@Test
void direct() {
String toBeTrimmed = " text with spaces ";
System.out.println(toBeTrimmed.trim());
Supplier<String> trimSupplier = toBeTrimmed::trim;
System.out.println(trimSupplier.get());
Function<String, String> trimFunc = String::trim;
System.out.println(trimFunc.apply(toBeTrimmed));
}
@Test
void reflection() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
String toBeTrimmed = " text with spaces ";
Method reflectionMethod = String.class.getMethod("trim");
Object invoke = reflectionMethod.invoke(toBeTrimmed);
System.out.println(invoke);
}
@Test
void methodHandle() throws Throwable {
String toBeTrimmed = " text with spaces ";
Lookup lookup = MethodHandles.lookup();
MethodType mt = MethodType.methodType(String.class);
MethodHandle mh = lookup.findVirtual(String.class, "trim", mt);
Object invoke = mh.invoke(toBeTrimmed);
System.out.println(invoke);
}
@Test
void lambdametaFactory1() throws Throwable {
String toBeTrimmed = " text with spaces ";
Lookup lookup = MethodHandles.lookup();
MethodType mt = MethodType.methodType(String.class);
MethodHandle mh = lookup.findVirtual(String.class, "trim", mt);
CallSite callSite = LambdaMetafactory.metafactory(lookup, "get", MethodType.methodType(Supplier.class, String.class),
MethodType.methodType(Object.class), mh, MethodType.methodType(String.class));
Supplier<String> lambda = (Supplier<String>) callSite.getTarget().bindTo(toBeTrimmed).invoke();
System.out.println(lambda.get());
}
@Test
void lambdametaFactory2() throws Throwable {
String toBeTrimmed = " text with spaces ";
Lookup lookup = MethodHandles.lookup();
MethodType mt = MethodType.methodType(String.class);
MethodHandle mh = lookup.findVirtual(String.class, "trim", mt);
CallSite callSite = LambdaMetafactory.metafactory(lookup, "apply", MethodType.methodType(Function.class),
MethodType.methodType(Object.class, Object.class), mh, MethodType.methodType(String.class, String.class));
Function<String, String> trimFunc = (Function<String, String>) callSite.getTarget().invokeExact();
System.out.println(trimFunc.apply(toBeTrimmed));
}
上面代码分别演示了直接调用、反射、方法句柄、函数映射的使用,从功能上来说,他们得到的结果是一致的。不同的在于,函数映射的性能和直接调用近似,而反射和方法句柄调用的性能则较差。
# 二、Stream API
假如我们要从一个集合中进行元素的筛选,一般来说会用到for循环和新建集合对象获得结果。
在Java8中新增了Stream API,先来体验一下他是如何筛选和统计的:
List<String> words = new ArrayList<>();
...
long count = words.stream().filter(w -> w.length() > 10).count();
Java8之后所有的集合对象都有stream()
方法,他会返回一个Stream
对象,Stream
对象和集合的主要区别是:
- Stream自己不存储元素
- Stream操作符不会改变源对象,它会返回一个持有结果的新Stream
- Stream可能是延迟执行的。等到需要结果的时候才执行
如果要获得一个并行的Stream
,可以使用words.parallelStream()
# 1、创建Stream
创建主要有这么几种方式:
Stream.of(arr)
of函数接受一个值或一个数组Stream.empty()
一个空的流Stream.generate(() -> "Echo")
按照函数逻辑生成一个值Stream.generate(Math::random)
同上Stream.iterate(BigInteger.ZERO,n -> n.add(BigInteger.ONE))
无限序列。第一个参数是初始值,第二个参数是对于前一个值进行的操作Stream<String> lines = Files.lines(path)
有的函数直接返回一个Stream
# 2、filter、map和flatMap方法
说一下Stream
的几个主要方法。
filter
在前面已经体验过了,来看看map
方法,他获得了流的一个副本:
Stream<String> lowercaseWords = words.map(String::toLowerCase)
Stream<Character> firstChars = words.map(s -> s.charAt(0))
flatMap
将多个Stream
合并为一个Stream
:我们假定characterStream(String s)
方法返回一个Stream<Character>
,用flatMap:
Stream<Character> letters = words.flatMap(w -> characterStream(w))
# 3、提取子流和组合流
直接看例子:
words.stream().limit(100) //limit()返回一个包含n个元素的新流
words.stream().skip(5) //会丢弃掉前面的n个元素
Stream.concat(words1, words2) //把两个流连接起来
Stream.iterate(1, n -> n + 1).peek(e -> System.out.println(e)).limit(20).toArray(); //peek函数是一个中间操作,一般用于调试比较多
# 4、有状态的转换
之前介绍的流转换都是无状态的,结果不依赖之前的元素。
看一下有状态的转换,也就是元素之间有依赖关系的转换:
Stream.of(arr).distinct()
words.sorted(Comparator.comparing(String:length).reversed())
# 5、简单的聚合方法
聚合方法一般都是终止操作,看代码:
words.max(String::compareToIgnoreCase)
words.filter(...).findFirst()
words.filter(...).findAny()
list.stream().anyMatch(s -> s.startsWith(""))
list.stream().noneMatch(s -> s.startsWith(""))
# 6、Optional类型
Optional主要用来解决空值的问题,直接看例子:
Optional<Integer> num = Optional.of(100);
num.ifPresent(v -> System.out.println(v)); //不为null,才调用里面的方法
num = num.map(v -> v = 19);
System.out.println(num.get());
Optional<String> str = Optional.empty();
System.out.println(str.orElse("123")); //默认值
str.orElseGet(() -> "");
Optional.ofNullable(""); //如果入参是null,返回empty()
Optional<Double> num1 = Optional.of(12.3);
Optional<String> str1 = num1.flatMap((x) -> Optional.ofNullable(x + "")); //转换类型
感觉Optional中最常用的就是orElse()方法了。
# 7、reduce聚合函数
在大数据处理中,MapReduce是比较经典的思想了,来自于lisp语言的map和reduce函数。
前面讲过map函数,主要是用来分发并获得副本,下面来看看reduce聚合函数:
Stream<Integer> values = Stream.of(1, 2, 3, 4);
Optional<Integer> sum = values.reduce(0, (x, y) -> x + y);
values.reduce(Integer::sum);
reduce的第一个参数是初始值,第二个参数是BinaryOperator接口,接口中的方法要有两个入参,这两个入参很有意思。第一个参数是结果值,第二个参数是当前循环值,什么意思呢?我们看到reduce入参的方法体有一个计算x+y,这是一个返回值,这个返回值在下一次循环会变成方法的第一个入参。
reduce的处理逻辑相当于下面的代码:
T result = null;
for (T element : this stream) {
result = accumulator.apply(result, element);
}
return Optional.of(result);
//accumulator = reduce入参的BinaryOperator
上述reduce方法的第一个参数可以省略,那么初始值将会是Stream的第一个元素。
Stream<Integer> values = Stream.of(1, 2, 3, 4);
Optional<Integer> sum = values.reduce((x, y) -> x + y);
values.reduce(Integer::sum);
reduce还有更复杂的用法:
List<String> words = new ArrayList<>();
words.add("a");
words.add("bed");
words.add("c");
int num = words.stream().reduce(0, (x, y) -> x + y.length(), Integer::sum);
System.out.println(num); //=5
可以看到这个一个统计集合中所有元素总字数的一段代码。这个是reduce的重载函数,有三个入参。第一个参数是初始的result;第二个参数是一个BiFunction,用来做累计结果操作;第三个参数是一个BinaryOperator,合并两个累积结果。他的接口方法参数有两个:prevResult, nextResul,分别是前一次操作的结果和下一次操作的结果。
# 8、收集结果
Stream是一个流,如果要想把他转换为我们熟悉的集合,可以这样做:
Stream strem = ..;
stream.collect(Collectors.toList()); //转换为List集合
stream.collect(Collectors.toSet()); //转换为Set集合
stream.collect(Collectors.toCollection(TreeSet::new)); //转换为TreeSet
如果要收集到Map中:
Stream<TUser> values = Lists.newArrayList(TUser.getInstance()).stream();
values.collect(Collectors.toMap(TUser::getId, TUser::getName));
values.collect(Collectors.toMap(TUser::getId, Function.identity())); //Function.identity()表示元素本身
//如果多个元素拥有相同的键,那么收集方法会抛出一个异常,所以我们需要定义第三个参数用于指定map中相同键的value的合并策略:
Stream<TUser> values = Stream.of(new TUser(1L, "a"), new TUser(2L, "b"), new TUser(1L, "c"));
Map<Long, String> collect = values
.collect(Collectors.toMap(TUser::getId, TUser::getName, (t1, t2) -> t1 + "-" + t2));
collect.forEach((k, v) -> System.out.println(v));
此外,还可以进行字符串连接操作:
stream.collect(Collectors.joining())
stream.collect(Collectors.joining(","))
stream.map(Object::toString).collect(Collectors.joining(","))
还可以收集一些我们常用的计算方式:
IntSummaryStatistics summaryStatistics = stream.collect(Collectors.summarizingInt(String::length)); //里面包含总和、平均值、最大值和最小值
summaryStatistics.getAverage();
summaryStatistics.getMax();
//还有Collectors.summingInt系列方法,只求总值
list.stream().collect(Collectors.summingInt(String::length));
//其实等同于下面的聚合操作
list.stream().mapToInt(String::length).sum();
求最大值、最小值:
Optional<TUser> max = list.stream().collect(Collectors.maxBy(Comparator.comparing(TUser::getId)));
// 等同于下面的聚合操作
Optional<TUser> max = list.stream().max(Comparator.comparing(TUser::getId));
Optional<TUser> min = list.stream().collect(Collectors.minBy(Comparator.comparing(TUser::getId)));
# 9、分组和分片
先看分组:
Stream<TUser> values = Stream.of(new TUser(1L, "a"), new TUser(2L, "b"), new TUser(1L, "c"));
Map<Long, List<TUser>> collect = values.collect(Collectors.groupingBy(TUser::getId));
Map<Long, Set<TUser>> collect2 = values.collect(Collectors.groupingBy(TUser::getId, Collectors.toSet()));
看一些比较高级的功能:
Map<Long, Long> collect3 = values.collect(Collectors.groupingBy(TUser::getId,Collectors.counting()));
Map<Long, LongSummaryStatistics> collect4 = values.collect(Collectors.groupingBy(TUser::getId,Collectors.summarizingLong(TUser::getId)));
Map<Long, String> collect5 = values.collect(Collectors.groupingBy(TUser::getId,Collectors.mapping(TUser::getName, Collectors.joining(","))));
Collectors
还有很多有用的方法,感兴趣可以自己试验一下。
分片的例子:
Map<Boolean, List<TUser>> collect5 = values.collect(Collectors.partitioningBy(t -> t.getName().endsWith("&")));
# 10、原始类型流
看一下int的原始流,一叶知秋:
IntStream stream = IntStream.of(1, 2, 3, 4, 5);
stream = Arrays.stream(new int[] { 1, 2, 3 }, 0, 2);
IntStream.range(0, 10).mapToObj(e -> String.valueOf(e)).collect(Collectors.toList()); // 不包括上限
IntStream.rangeClosed(0, 10).mapToObj(e -> String.valueOf(e)).collect(Collectors.toList()); // 包括上限
IntStream.range(0, 100).boxed(); // 原始类型流转换为一个对象流
# 11、并行流
并行流就是会并行处理的流。例如,当计算stream.map(fun)
时,流可以被分为n段,每一段都会被并发处理,然后再按顺序将结果组合起来:
words.parallelStream();
stream.parallel();
要调整并发值:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "16");
// 默认为Runtime.getRuntime().availableProcessors()
# 三、时间API
在Java8之前,Java中对于日期的操作是不够那么方面的。比如对日期进行加减操作、对日期的格式化、对日期的大小进行判断等。所以会出现Joda Time工具包来帮我们解决问题。
Java8之后,就不需要再引入第三方工具包来处理日期了。
刚开始的时候,看着新的API还是非常不适应的,习惯了之后就会爱上他了。
# 1、对日期进行加减
Java8中提供了LocalDate
、LocalTime
和LocalDateTime
分别表示日期、时间和日期时间。LocalDate
的格式如2018-05-22
,LocalTime的格式如15:49:50.494
,那么LocalDateTime
就是他们的结合体了。
直接看LocalDateTime
的例子吧:
LocalDateTime dateTime = LocalDateTime.now();
// 三天之后
dateTime.plusDays(3);
// 三月之前
dateTime.minusMonths(3);
// 星期几
dateTime.getDayOfWeek();
// 月份中的第几天
dateTime.getDayOfMonth();
// 获得秒值
dateTime.getSecond();
// 获得今天的开始时间
dateTime.with(LocalTime.MIN);
// 获得今天的结束时间
dateTime.with(LocalTime.MAX);
plus
系列方法用于相加,minus
系列方法用于相减,get
系列方法用于获得值,with
系列方法用于获得处理后的副本。
# 2、日期的格式化
原来会用到SimpleDateFormat
类,但他是线程不安全的。
看新的线程安全的API:
LocalDate.parse("2005-12-03", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
# 3、对两个日期的判断和运算
判断两个日期:
dateTime.isAfter(other);
dateTime.isBefore(other);
dateTime.equels(other);
计算两个日期之间相差的情况:
LocalDate date = LocalDate.now();
Period period = Period.between(date, date.plusWeeks(2));
System.out.println(period.getYears() + " " + period.getDays()); //0 14
除了Period
之外,还有一个Duration
也可以用于计算两个日期的差值。他们的区别是:当处理带时区的时间时,请使用Period
,因为会涉及到夏令时问题
# 4、带时区的日期
语言对时区的支持,主要体现在UTC时间的转换上。这里需要注意一下,时区的名词有UTC和GMT,简单理解他们其实表示一个意思。
比如现在我想知道纽约现在是几点,一种方式是:
ZoneId america = ZoneId.of("America/New_York");
LocalDateTime localtDateAndTime = LocalDateTime.now();
ZonedDateTime dateAndTimeInNewYork = ZonedDateTime.of(localtDateAndTime, america);
System.out.println("Current date and time in a particular timezone : " + dateAndTimeInNewYork);
// Output :
// Current date and time in a particular timezone : 2018-05-22T16:39:47.474-05:00[America/New_York]
ZonedDateTime用于处理带时区的日期。
这里的ZoneId一定要写对,否则会抛异常。
还有另一种方式,用时区偏移量也可以实现时区时间转换:
LocalDateTime datetime = LocalDateTime.now();
ZoneOffset offset = ZoneOffset.of("-05:00");
OffsetDateTime date = OffsetDateTime.of(datetime, offset);
System.out.println("Date and Time with timezone offset in Java : " + date);
// Output :
// Date and Time with timezone offset in Java : 2018-05-22T16:39:47.474-05:00
这种方式对机器友好,第一种方式对人类友好。
# 5、与旧API的相互操作
Java8中用Instant
表示时间轴上的一个点,和原先的Date
很像。
下面是Date
、Instant
和LocalDateTime
之间的相互转换:
Date date = new Date();
Instant instant = date.toInstant();
LocalDateTime.ofInstant(instant,ZoneId.systemDefault());
instant.atZone(ZoneId.systemDefault()).toLocalDateTime();
instant.atZone(ZoneId.systemDefault()).toLocalDate();
ZoneId zoneId = ZoneId.systemDefault();
LocalDate localDate = LocalDate.now();
// LocalDate必须先转为LocalDateTime(通过atStartOfDay方法),才可以toInstant
ZonedDateTime zdt = localDate.atStartOfDay().atZone(zoneId);
// zdt = localDate.atStartOfDay(zoneId); 简写
Date date = Date.from(zdt.toInstant());
LocalDate或LocalDateTime转换为时间戳,都要先转换为Instant:
Instant instant = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant();
instant = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant();
Long milli = instant.toEpochMilli();
LocalDateTime time2 =LocalDateTime.ofEpochSecond(timestamp/1000,0,ZoneOffset.ofHours(8));
时间戳转换为LocalDateTime,同理,需要从Instant中来:
LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault());
// 有了localDateTime之后可以获得LocalDate
localDateTime.toLocalDate();
# 6、时间调整之TemporalAdjusters
比如我要获取这个月的最后一天,可以这么做:
LocalDate date = LocalDate.of(2019,1, 1);
LocalDate lastDay = date.with(TemporalAdjusters.lastDayOfMonth());
System.out.println(lastDay);
这里出现了TemporalAdjusters类,它包含的方法如下:
TemporalAdjusters其实是TemporalAdjuster的工具类,而TemporalAdjuster则是一个函数接口,可以执行复杂的时间操作。
比如要获取某天之后的工作日:
public static void main(String[] args) {
LocalDate localDate = LocalDate.of(2019, 7, 8);
TemporalAdjuster temporalAdjuster = NEXT_WORKING_DAY;
LocalDate result = localDate.with(temporalAdjuster);
}
static TemporalAdjuster NEXT_WORKING_DAY = TemporalAdjusters.ofDateAdjuster(date -> {
DayOfWeek dayOfWeek = date.getDayOfWeek();
int daysToAdd;
if (dayOfWeek == DayOfWeek.FRIDAY)
daysToAdd = 3;
else if (dayOfWeek == DayOfWeek.SATURDAY)
daysToAdd = 2;
else
daysToAdd = 1;
return date.plusDays(daysToAdd);
});
# 总结
Java 8是一个重要的版本,Lambda表达式、Stream给Java带来了现代语言的一些特性,编程的效率得到很大提升。
掌握Java 8中的核心功能变为了我们的必修课,希望此文对你有所帮助!
祝你变得更强!