Lambda表达式
Lambda表达式(也称为闭包)是Java 8中最大和最令人期待的语言改变。它允许我们将函数当成参数传递给某个方法,或者把代码本身当作数据处理:函数式开发者非常熟悉这些概念。
很多JVM平台上的语言(Groovy、Scala等)从诞生之日就支持Lambda表达式,但是在Java8之前,很多开发者没有选择,只能使用匿名内部类代替Lambda表达式。
测试代码:
package com.redistext.newfeatures;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.function.Consumer;
/**
* @author: maorui
* @description: TODO
* @date: 2021/10/29 10:31
* @description: V1.0
*/
public class Java8Test {
public static void main(String[] args) {
List<String> stringList = Arrays.asList("5", "3", "2", "7");
//非Lambda表达式,正序排序
stringList.sort(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
System.out.println(stringList);
//非Lambda表达式,遍历
stringList.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
/**
* 简单的Lambda表达式可由逗号分隔的参数列表、->符号和语句块组成
*/
//Lambda表达式,正序排序
Arrays.asList( "3", "8", "2", "1").sort( ( e1, e2 ) -> e1.compareTo( e2 ) );
//Lambda表达式,遍历
Arrays.asList( "3", "8", "2" ).forEach( e -> System.out.print( e + "," ) );
}
}
测试结果:
函数式接口
函数式接口指的是只有一个函数的接口,这样的接口可以隐式转换为Lambda表达式。java.lang.Runnable和java.util.concurrent.Callable是函数式接口的最佳例子。在实践中,函数式接口非常脆弱:只要某个开发者在该接口中添加一个函数,则该接口就不再是函数式接口进而导致编译失败。为了克服这种代码层面的脆弱性,并显式说明某个接口是函数式接口,Java 8 提供了一个特殊的注解@FunctionalInterface(Java 库中的所有相关接口都已经带有这个注解了)。
测试代码:
package com.redistext.newfeatures;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.function.Consumer;
/**
* @author: maorui
* @description: TODO
* @date: 2021/10/29 10:31
* @description: V1.0
*/
public class Java8Test {
public static void main(String[] args) {
Java8Test tester = new Java8Test();
// 类型声明
MathOperation addition = (int a, int b) -> a + b;
// 不用类型声明
MathOperation subtraction = (a, b) -> a - b;
// 大括号中的返回语句
MathOperation multiplication = (int a, int b) -> {
return a * b;
};
// 没有大括号及返回语句
MathOperation division = (int a, int b) -> a / b;
System.out.println("10 + 5 = " + tester.operate(10, 5, addition));
System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction));
System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication));
System.out.println("10 / 5 = " + tester.operate(10, 5, division));
// 不用括号
GreetingService greetService1 = message ->
System.out.println("Hello " + message);
// 用括号
GreetingService greetService2 = (message) ->
System.out.println("Hello " + message);
greetService1.sayMessage("Runoob");
greetService2.sayMessage("Google");
}
@FunctionalInterface
interface MathOperation {
int operation(int a, int b);
//不过有一点需要注意,默认方法和静态方法不会破坏函数式接口的定义,因此如下的代码是合法的。
static String staticMethod(){
return "staticMethod";
}
default String defaultMethod(){
return "defaultMethod";
}
}
@FunctionalInterface
interface GreetingService {
void sayMessage(String message);
}
private int operate(int a, int b, MathOperation mathOperation) {
return mathOperation.operation(a, b);
}
}
测试结果:
接口的默认方法和静态方法
上面的测试代码中用到了接口的默认方法和静态方法,这也是java8的新特性。
在Java8之前,接口中只能包含抽象方法。那么这有什么样弊端呢?比如,想再Collection接口中添加一个spliterator抽象方法,那么也就意味着之前所有实现Collection接口的实现类,都要重新实现spliterator这个方法才行。而接口的默认方法就是为了解决接口的修改与接口实现类不兼容的问题,作为代码向前兼容的一个方法。
流Stream
Java8中有一种新的数据处理方式,那就是流Stream,结合lambda表达式能够更加简洁高效的处理数据。Stream使用一种类似于SQL语句从数据库查询数据的直观方式,对数据进行如筛选、排序以及聚合等多种操作。
什么是Stream
Stream将要处理的元素集合看作一种流,在流的过程中,借助Stream API对流中的元素进行操作,比如:筛选、排序、聚合等。
Stream可以由数组或集合创建,对流的操作分为两种:
中间操作:每次返回一个新的流,可以有多个。
终端操作:每个流只能进行一次终端操作,终端操作结束后流无法再次使用。终端操作会产生一个新的集合或值。
另外,Stream有几个特性:
stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果。
stream不会改变数据源,通常情况下会产生一个新的集合或一个值。
stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行。
Stream的生成方式
1.接口Collection中和Arrays:
Collection.stream();
Collection.parallelStream(); //相较于串行流,并行流能够大大提升执行效率
Arrays.stream(T array);
stream和parallelStream的简单区分: stream是顺序流,由主线程按顺序对流执行操作,而parallelStream是并行流,内部以多线程并行执行的方式对流进行操作,但前提是流中的数据处理没有顺序要求。
2.Stream中的静态方法:
Stream.of();
generate(Supplier s);
iterate(T seed, UnaryOperator f);
empty();
3.其他方法:
Random.ints();
BitSet.stream();
Pattern.splitAsStream(java.lang.CharSequence);
JarFile.stream();
BufferedReader.lines();
示例:
package com.redistext.stream;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
/**
* @author: maorui
* @description: TODO
* @date: 2021/11/6 10:16
* @description: V1.0
*/
public class StreamTest {
public static void main(String[] args) {
//1.使用Collection中的方法和Arrays
String[] strArr = new String[]{"a", "b", "c", "k", "e", "d"};
List<String> list = Arrays.asList(strArr);
Stream<String> stream = list.stream();
Stream<String> stream5 = list.parallelStream();
Stream<String> stream1 = Arrays.stream(strArr);
//2. 使用Stream中提供的静态方法
Stream<String> stream2 = Stream.of(strArr);
Stream<Double> stream3 = Stream.generate(Math::random);
Stream<Object> stream4 = Stream.empty();
Stream.iterate(1, i -> i++);
}
}
Stream的操作
常见的Stream操作有这样几种:
Intermediate(中间操作):中间操作是指对流中数据元素做出相应转换或操作后依然返回为一个流Stream,仍然可以供下一次流操作使用。常用的有:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip。
Termial(结束操作):是指最终对Stream做出聚合操作,输出结果。
中间操作
filter:对Stream中元素进行过滤。
过滤元素为空的字符串:
long count = stream.filter(str -> str.isEmpty()).count();
map:对Stream中元素按照指定规则映射成另一个元素。
将每一个元素都添加字符串“_map”:
stream.map(str -> str + "_map").forEach(System.out::println);
concat:对流进行合并操作。
concat方法将两个Stream连接在一起,合成一个Stream。若两个输入的Stream都时排序的,则新Stream也是排序的;若输入的Stream中任何一个是并行的,则新的Stream也是并行的;若关闭新的Stream时,原两个输入的Stream都将执行关闭处理。
Stream.concat(Stream.of(1, 2, 3), Stream.of(4, 5, 6)). forEach(System.out::println);
distinct:对流进行去重操作。
去除流中重复的元素:
Stream<String> stream = Stream.of("a", "a", "b", "c");
stream.distinct().forEach(System.out::println);输出结果:
a
b
c
limit:限制流中元素的个数。
截取流中前两个元素:
Stream<String> stream = Stream.of("a", "a", "b", "c");
stream.limit(2).forEach(System.out::println);输出结果:
a
a
skip:跳过流中前几个元素。
丢掉流中前两个元素:
Stream<String> stream = Stream.of("a", "a", "b", "c");
stream.skip(2).forEach(System.out::println);
输出结果:
b
c
sorted:对流中元素进行排序,可以通过sorted(Comparator<? super T> comparator)自定义比较规则。
Stream<Integer> stream = Stream.of(3, 2, 1);
stream.sorted(Integer::compareTo).forEach(System.out::println);
输出结果:
1
2
3
match:检查流中元素是否匹配指定的匹配规则。
Stream 有三个 match 方法,从语义上说:
allMatch:Stream 中全部元素符合传入的 predicate,返回 true;
anyMatch:Stream 中只要有一个元素符合传入的 predicate,返回 true;
noneMatch:Stream 中没有一个元素符合传入的 predicate,返回 true。
如检查Stream中每个元素是否都大于5:
Stream<Integer> stream = Stream.of(3, 2, 1);
boolean match = stream.allMatch(integer -> integer > 5);
System.out.println(match);
输出结果:
false
结束操作
count:统计Stream中元素的个数。
long count = stream.filter(str -> str.isEmpty()).count();
max/min:找出流中最大或者最小的元素。
Stream<Integer> stream = Stream.of(3, 2, 1);
System.out.println(stream.max(Integer::compareTo).get());输出结果:
3
forEach:forEach方法前面已经用了好多次,其用于遍历Stream中的所元素,避免了使用for循环,让代码更简洁,逻辑更清晰。
Stream.of(5, 4, 3, 2, 1)
.sorted()
.forEach(System.out::println);
// 打印结果
// 1,2,3,4,5
Optional
为了解决空指针异常,在Java8之前需要使用if-else这样的语句去防止空指针异常,而在Java8就可以使用Optional来解决。Optional可以理解成一个数据容器,甚至可以封装null,并且如果值存在调用isPresent()方法会返回true。为了能够理解Optional。先来看一个例子:
public class OptionalTest {
private String getUserName(User user) {
return user.getUserName();
}
class User {
private String userName;
public User(String userName) {
this.userName = userName;
}
public String getUserName() {
return userName;
}
}
}
事实上,getUserName方法对输入参数并没有进行判断是否为null,因此,该方法是不安全的。如果在Java8之前,要避免可能存在的空指针异常的话就需要使用if-else
进行逻辑处理,getUserName会改变如下:
private String getUserName(User user) {
if (user != null) {
return user.getUserName();
}
return null;
}
这是十分繁琐的一段代码。而如果使用Optional则会要精简很多:
private String getUserName(User user) {
Optional<User> userOptional = Optional.ofNullable(user);
return userOptional.map(User::getUserName).orElse(null);
}
Java8之前的if-else的逻辑判断,这是一种命令式编程的方式,而使用Optional更像是一种函数式编程,关注于最后的结果,而中间的处理过程交给JDK内部实现。
创建Optional
- Optional.empty():通过静态工厂方法Optional.empty,创建一个空的Optional对象;
- Optional.of(T value):如果value为null的话,立即抛出NullPointerException;
- Optional.ofNullable(T value):使用静态工厂方法Optional.ofNullable,你可以创建一个允许null值的Optional对象。
实例代码:
//创建Optional
Optional<Object> optional = Optional.empty();
Optional<Object> optional1 = Optional.ofNullable(null);
Optional<String> optional2 = Optional.of(null);
常用方法
1. boolean equals(Object obj):判断其他对象是否等于 Optional;
2. Optional<T> filter(Predicate<? super <T> predicate):如果值存在,并且这个值匹配给定的 predicate,返回一个Optional用以描述这个值,否则返回一个空的Optional;
3. <U> Optional<U> flatMap(Function<? super T,Optional<U>> mapper):如果值存在,返回基于Optional包含的映射方法的值,否则返回一个空的Optional;
4. T get():如果在这个Optional中包含这个值,返回值,否则抛出异常:NoSuchElementException;
5. int hashCode():返回存在值的哈希码,如果值不存在 返回 0;
6. void ifPresent(Consumer<? super T> consumer):如果值存在则使用该值调用 consumer , 否则不做任何事情;
7. boolean isPresent():如果值存在则方法会返回true,否则返回 false;
8. <U>Optional<U> map(Function<? super T,? extends U> mapper):如果存在该值,提供的映射方法,如果返回非null,返回一个Optional描述结果;
9. T orElse(T other):如果存在该值,返回值, 否则返回 other;
10. T orElseGet(Supplier<? extends T> other):如果存在该值,返回值, 否则触发 other,并返回 other 调用的结果;
11. <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier):如果存在该值,返回包含的值,否则抛出由 Supplier 继承的异常;
12. String toString():返回一个Optional的非空字符串,用来调试
Date/time API的改进
在Java8之前的版本中,日期时间API存在很多的问题,比如:
- 线程安全问题:java.util.Date是非线程安全的,所有的日期类都是可变的;
- 设计很差:在java.util和java.sql的包中都有日期类,此外,用于格式化和解析的类在java.text包中也有定义。而每个包将其合并在一起,也是不合理的;
- 时区处理麻烦:日期类不提供国际化,没有时区支持,因此Java中引入了java.util.Calendar和Java.util.TimeZone类;
针对这些问题,Java8重新设计了日期时间相关的API,Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。在java.util.time包中常用的几个类有:
- 它通过指定一个时区,然后就可以获取到当前的时刻,日期与时间。Clock可以替换System.currentTimeMillis()与TimeZone.getDefault()
- Instant:一个instant对象表示时间轴上的一个时间点,Instant.now()方法会返回当前的瞬时点(格林威治时间);
- Duration:用于表示两个瞬时点相差的时间量;
- LocalDate:一个带有年份,月份和天数的日期,可以使用静态方法now或者of方法进行创建;
- LocalTime:表示一天中的某个时间,同样可以使用now和of进行创建;
- LocalDateTime:兼有日期和时间;
- ZonedDateTime:通过设置时间的id来创建一个带时区的时间;
- DateTimeFormatter:日期格式化类,提供了多种预定义的标准格式;
public class TimeTest {
public static void main(String[] args) {
Clock clock = Clock.systemUTC();
Instant instant = clock.instant();
System.out.println(instant.toString());LocalDate localDate = LocalDate.now();
System.out.println(localDate.toString());LocalTime localTime = LocalTime.now();
System.out.println(localTime.toString());LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime.toString());ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
System.out.println(zonedDateTime.toString());
}
}
输出结果为:
2018-04-14T12:50:27.437Z
2018-04-14
20:50:27.646
2018-04-14T20:50:27.646
2018-04-14T20:50:27.647+08:00[Asia/Shanghai]
参考文档:
掘金