函数式接口
什么是函数式接口
函数式接口在Java
中的表示是:有且仅有一个抽象方法的接口。他是适用于函数式编程场景的接口。在Java
中,函数式编程的体现就是Lambda
,所以函数式接口就是可以适用于Lambda
使用的接口。只有确保接口中有且仅有一个抽象方法,Java
中的Lambda
才能顺利的进行推导。
语法糖:是指使用更加方便,但是原理不变的代码语法。比如说在遍历集合的时候用的for-each
语法其实就是对迭代器的封装,这就是一个语法糖。从应用层面上来讲,Java
中的Lambda
可以被当做是匿名内部类的语法糖,但是二者在原理上不同。
函数式接口格式
确保接口中有且仅有一个抽象方法,但是可以有默认、静态方法:
@FunctionalInterface
public interface MyFunctionInterface {
//抽象方法
public abstract void method();
//默认方法
default void method1(){
}
//静态方法
static void method2(){
}
}
加上@FunctionalInterface
注解的意思就是规范这个接口是一个函数式接口,如果这个接口违反了“接口中有且仅有一个抽象方法”
这个规范,那么这个接口就会编译失败。
函数式接口的使用
函数式接口一般可以作为方法的参数和返回值类型:
public class MyFunctionInterfaceImpl implements MyFunctionInterface{
@Override
public void method() {
System.out.println("method");
}
}
public class UseFunctionInterface {
public static void show(MyFunctionInterface myFunctionInterface){
myFunctionInterface.method();
}
public static void main(String[] args) {
//普通掉用,传入这个接口的实现类
show(new MyFunctionInterfaceImpl());
//传递接口的匿名内部类
show(new MyFunctionInterface() {
@Override
public void method() {
System.out.println("method111");
}
});
//作为一个函数式接口,我们可以使用Lambda表达式
show(()-> {
System.out.println("method22222");
});
//简化Lambda表达式,当Lambda表达式大括号中有且仅有一条语句可以省略大括号
show(()->System.out.println("method3333"));
}
}
函数式编程
Java在兼顾面向对象特性的基础上,通过Lambda表达式与方法引用等,为开发者打开了函数式编程的大门。
Lambda延迟执行特性
假设我们要去打印一条拼接的日志消息,我们正常情况的代码如下:
/**
* 我们常用的方法调用
*/
public class LoggerDemo1 {
public static void log(int level, String message){
if (level == 1) {
System.out.println(message);
}
}
public static void main(String[] args) {
String message1 = "hello ";
String message2 = "world ";
String message3 = "lambda";
//这个方法无论传的level是什么 ‘message1+message2+message3’ 这个拼接字符串的操作都会执行
//但是实际情况当中,当level不是1的时候,这个拼接字符串的操作是不需要进行的
log(1,message1+message2+message3);
}
}
分析这个方法的执行过程,我们可以看到,无论传的level是什么 ,message1+message2+message3
这个拼接字符串的操作都会执行,但是实际情况当中,当level不是1的时候,这个拼接字符串的操作是不需要执行的。
因此我们可以做出如下优化,首先创建一个持有消息的对象,并提供拼接日志消息的方法:
/**
* 创建一个持有消息的对象,并提供拼接日志消息的方法
*/
public class MessageHolder {
String[] messages;
public MessageHolder(String... messages) {
this.messages = messages;
}
public String buildMessage(){
System.out.println("start build message");
StringBuilder res = new StringBuilder();
for (String message : messages) {
res.append(" ").append(message);
}
return res.toString();
}
}
将持有日志消息的MessageHolder
对象当成一个参数传入,只有当 level 为1的时候,才去调用拼接字符串的方法:
/**
* 对上一个操作的优化,
* 我们把字符串封装到 MessageHolder 中,并在 MessageHolder 中提供一个拼接字符串的方法
* 然后将MessageHolder当成一个参数传入,只有当 level 为1的时候,才去调用拼接字符串的方法
*/
public class LoggerDemo2 {
public static void log(int level,MessageHolder messageHolder){
if (level == 1) {
System.out.println(messageHolder.buildMessage());
}
}
public static void main(String[] args) {
String message1 = "hello ";
String message2 = "world ";
String message3 = "lambda";
log(1,new MessageHolder(message1,message2,message3));
}
}
执行后可见,只有当level
传入为1
的时候,才会去拼接字符串,从而增加了代码的执行效率。但是达到同样的效果下,我们可以使用Lambda
对代码进行重新优化。
首先,创建一个函数式接口,提供一个方法,返回值为拼接好的字符串:
@FunctionalInterface
public interface MessageBuilder {
String buildMessage();
}
将函数式接口作为参数,用户可以使用匿名内部类的方式去实现拼接字符串的方法:
/**
* 使用函数式接口,继续对代码进行优化,可以实现Demo同样的延迟调用的结果
*
* 但是这种方法看起来更加简单明了,而且代码量也显著减少
*
*/
public class LoggerDemo3 {
public static void log(int level,MessageBuilder messageBuilder){
if (level == 1) {
System.out.println(messageBuilder.buildMessage());
}
}
public static void main(String[] args) {
String message1 = "hello ";
String message2 = "world ";
String message3 = "lambda";
log(1, () -> message1 + message2 + message3);
}
}
执行效果与第二次优化相同,但是相对于第二次优化,通过函数式接口进行的优化看起来更加简单明了,而且代码量也得到了显著的减少。因此可以看到Lambda
编程天生就带有延迟执行的特性。
Lambda表达式练习
函数式接口作为参数
我们之前应该已经学过了多线程相关的知识,可以发现Runnable
就是一个函数式接口,我们可以通过实现它去创建一个线程。现要求写一个启动线程的方法,将Runnable
作为此方法的参数,在这个方法内部启动这个线程并打印出线程的名称。
/**
* 练习:
*
* Runnable也是一个函数式接口,
* 这里我们写个例子,写一个启动线程的方法,将Runnable当做方法的参数,在方法内部启动这个线程
*/
public class UseFunctionInterfaceAsParam {
public static void startThread(Runnable runnable){
new Thread(runnable).start();
}
public static void main(String[] args) {
//使用匿名内部类
startThread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running ");
}
});
//使用Lambda表达式
startThread(()->{
System.out.println(Thread.currentThread().getName() + " is running ");
});
//简化Lambda表达式,当重写的方法只有一行代码语句的时候,可以省略大括号
startThread(()-> System.out.println(Thread.currentThread().getName() + " is running "));
}
}
函数式接口作为返回值
Comparator
作为一个常用的比较器,它也是一个函数式接口。在数组工具Arrays
、Stream
流以及其他有序集合中都封装了排序方法,这些排序方法都是以Comparator
比较器为参数。
现要求写一个方法返回一个比较器,然后将返回的比较器作为数组排序方法的参数传入,实现Integer
类型数组的排序功能。
import java.util.Arrays;
import java.util.Comparator;
public class UseFunctionInterfaceAsResult {
/* public static Comparator<Integer> getComparator(){
//直接返回一个匿名内部类
return new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
};
}*/
/* public static Comparator<Integer> getComparator(){
//使用Lambda表达式
return (Integer o1,Integer o2)->{
return o1-o2;
};
}*/
public static Comparator<Integer> getComparator(){
//对Lambda表达式进行简化
//由于compare方法的泛型已经在 getComparator 方法的返回值中已经指定,因此我们可以省略掉 参数类型
//由于 大括号内只有一行代码,我们可以 省略掉大括号 和 return
return (o1,o2)->o1-o2;
}
/* public static Comparator<Integer> getComparator(){
//当然 idea还会提示我们使用这个方法
// 这是因为Comparator 中已经默认已经有了一个比较int类型的方法,所以这里可以直接掉用这个方法返回
return Comparator.comparingInt(o -> o);
}*/
/*
public static Comparator<Integer> getComparator(){
// ::的格式我们之后讲
return Integer::compare;
}
*/
public static void main(String[] args) {
Integer[] arr = {1,5,3,2,9,8,4,7,6};
Arrays.stream(arr).forEach(System.out::println);
Arrays.sort(arr,getComparator());
System.out.println("=============================");
Arrays.stream(arr).forEach(System.out::println);
}
}
Lambda表达式省略格式
在Lambda标准格式的基础上,使用省略写法的规则为:
-
小括号内参数的类型可以省略;
-
如果小括号内有且仅有一个参,则小括号可以省略;
-
如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。
常用的函数式接口
在JDK中,为我们提供了大量的函数式接口以丰富Lambda的典型使用场景,他们主要在java.util.function
包下。
Supplier
Supplier
接口仅包含一个无参的方法:T get()
。用来获取一个通过泛型指定类型的对象数据。下面我们定义一个方法,用来返回一个String
类型的参数并打印出来。
/**
* Supplier
* 是一个生产型接口,指定泛型,get方法机会返回相应类型的数据
*/
public class SupplierTest {
/**
* 指定返回String类型数据
*/
private static String getString(Supplier<String> supplier){
return supplier.get();
}
public static void main(String[] args) {
String hello = getString(() -> "hello");
System.out.println(hello);
}
}
练习:
定义一个方法,参数使用Supplier
接口,这个方法返回一个int
数组中的最大值。提示:即Supplier
的泛型指定为Integer
类型。
/**
* 使用Supplier接口作为方法参数,求int数组最大值
*/
public class UseSupplier {
//定义一个方法,用于获取int数组中最大值,泛型为 Integer
public static int getMax(Supplier<Integer> supplier){
return supplier.get();
}
public static void main(String[] args) {
int[] intArr = {1,5,6,9,8,4,2};
//通过数组工具类Arrays将数组转换为stream,使用stream获取数组最大值,
// stream后面会讲到,这里先接触一下
//也可以通过遍历数组的方式去获取一个最大值,有兴趣自己实现
int maxNum = getMax(()->Arrays.stream(intArr).max().getAsInt());
System.out.println(maxNum);
}
}
Consumer
Consumer
接口与Supplier
接口正好相反,他是一个消费型接口,用来消费数据,数据类型同样通过泛型指定。它定义了两个接口:
void accept(T t)
:是一个抽象方法,消费一个泛型所指定类型的数据。
/**
* Consumer
* 它是一个消费型接口,使用accept方法消费指定类型的数据
*/
public class ConsumerAcceptTest {
public static void handleName(String name, Consumer<String> consumer){
consumer.accept(name);
}
//消费一个String类型的姓名,打印出这个姓名
public static void main(String[] args) {
//使用匿名内部类方式
handleName("托塔李天王", new Consumer<String>() {
@Override
public void accept(String name) {
System.out.println(name);
}
});
//使用lambda写法
handleName("托塔李天王",(String name)->{
System.out.println(name);
});
//只有一个参数 一般省略类型和小括号
handleName("托塔李天王",name->{
System.out.println(name);
});
//只有一行语句 省略花括号
handleName("托塔李天王",name-> System.out.println(name));
//参数与处理方法入参一样 且只有一句,可以使用::语法
handleName("托塔李天王",System.out::println);
}
}
default Consumer<T> andThen(Consumer<? super T> after)
:如果一个方法的参数和返回值都是Consumer
类型,那么这个方法的返回可以通过这个方法去接收另外一个Consumer
对象,实现类似一个处理链的效果。即把多个Consumer
的andThen
方法串起来运行 ,我们可以看一下他的源码实现:
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
//掉用本类中的accept方法处理t这个数据之后
//继续掉用after中的accept方法去处理t这个数据
//就相当于一个数据处理链的一个效果
return (T t) -> { accept(t); after.accept(t); };
}
使用:
/**
* 使用Consumer的andThen方法实现一个类似数据处理链的效果
*/
public class ConsumerAndThenTest {
public static void method(String s, Consumer<String> consumer1,Consumer<String> consumer2){
//使用consumer1连接consumer2
//先执行consumer1的accept方法,再执行consumer2的accept方法
consumer1.andThen(consumer2).accept(s);
}
public static void main(String[] args) {
method("Hello",
//转换为大写并输出
t-> System.out.println(t.toUpperCase()),
//转换为小写并输出
t-> System.out.println(t.toLowerCase()));
}
}
练习:
现在需要实现一个组装电脑的产品线,产品零件由客户提供,然后通过这个产品线对零件按照指定的加工顺序进行组装。规定的组装过程:主板->电源->CPU->显卡->散热器->机箱
。
/**
* 使用Consumer接口,
*
* 实现一个可以组装电脑的产品链方法,
* 通过这个方法,可以根据客户提供的产品零件,对产品进行按一下顺序的组装
* 主板->电源->CPU->显卡->散热器->机箱
*
*/
public class UseConsumer {
/**
*
* @param productType 产品类型
* @param computerSupplier 提供产品零件
* @param assemblies 组装流程
*/
@SafeVarargs
public static void assemblyComputer(String productType, Supplier<Consumer<String>> computerSupplier, Consumer<String>... assemblies){
Consumer<String> computer = computerSupplier.get();
for (Consumer<String> assembly : assemblies) {
computer = computer.andThen(assembly);
}
computer.accept(productType);
}
public static void main(String[] args) {
assemblyComputer("笔记本电脑",
()-> productType-> System.out.println("准备"+productType+"零件"),
computer-> System.out.println("组装"+computer+"的主板"),
computer-> System.out.println("组装"+computer+"的电源"),
computer-> System.out.println("组装"+computer+"的CPU"),
computer-> System.out.println("组装"+computer+"的显卡"),
computer-> System.out.println("组装"+computer+"的机箱"));
}
}
Predicate
对指定数据类型的数据进行判断,并返回判断的结果,可以看到Stream
中的filter
方法的入参就是它。
boolean test(T t)
:对数据进行判断,返回boolean类型。default Predicate<T> and(Predicate<? super T> other)
:默认方法,与判断,相当于&&
。default Predicate<T> or(Predicate<? super T> other)
:默认方法,或判断,相当于||
。default Predicate<T> negate()
:默认方法,非判断,相当于!
。
/**
* Predicate 对指定数据类型的数据进行判断,并返回判断的结果
*/
public class PredicateTest {
public static void main(String[] args) {
//测试字符串长度是否大于6
System.out.println(testPredicate("1234567", str -> str.length() > 6));
//测试字符串是否 长度大于6并且包含字母a
System.out.println(testAnd("12345a67", str -> str.length() > 6,str->str.contains("a")));
//测试字符串是否 长度大于或者包含字母a
System.out.println(testOr("1234567", str -> str.length() > 6,str->str.contains("a")));
//测试字符串长度是否不大于6
System.out.println(testNegate("1234567", str -> str.length() > 6));
}
/**
* 测试test方法
*/
public static boolean testPredicate(String str,Predicate<String> predicate){
return predicate.test(str);
}
/**
* 测试and方法
*/
public static boolean testAnd(String str,Predicate<String> predicateLength,Predicate<String> predicateContains){
return predicateLength.and(predicateContains).test(str);
}
/**
* 测试or方法
*/
public static boolean testOr(String str,Predicate<String> predicateLength,Predicate<String> predicateContains){
return predicateLength.or(predicateContains).test(str);
}
/**
* 测试negate方法
*/
public static boolean testNegate(String str,Predicate<String> predicate){
return predicate.negate().test(str);
}
}
练习:
找出下面一段字符串数组中名字长度为四位,并且性别为女的,将其姓名放入List
集合中并输出。
/**
* 找出下面一段字符串数组中名字长度为四位,并且性别为女的,将其姓名放入`List`集合中并输出。
*/
public class UsePredicate {
public static void main(String[] args) {
String[] heroArr = {"阿狸,女","布里茨,男","九尾妖狐,女","崔丝塔娜,女","拉克丝,女","艾瑞莉娅,女","马尔扎哈,男"};
List<String> femaleHero = filter(heroArr,
hero->hero.split(",")[0].length()==4,
hero->"女".equals(hero.split(",")[1]));
System.out.println(femaleHero);
}
public static List<String> filter(String[] heroArr, Predicate<String> predicateLength,Predicate<String> predicateSex){
List<String> names = new ArrayList<>();
for (String hero : heroArr) {
if (predicateLength.and(predicateSex).test(hero)){
names.add(hero.split(",")[0]);
}
}
return names;
}
}
Function
java.util.function.Function<T,R>
,将一个类型的数据转换为另一个类型。我们可以看到这个接口需要指定两个泛型T
和R
,T
就表示要转换的源类型,R
表示要转换的目标类型。
-
R apply(T t)
:通过参数类型和返回类型就可以知道,我们可以通过这个方法将T
类型转换成R
类型。 -
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
:与Consumer
的andThen
方法一样,将多个apply
操作串起来。 -
default <V> Function<V, R> compose(Function<? super V, ? extends T> before)
:与andThen
方法类似,不同的是该方法串起来的执行顺序是相反的。
/**
* Function 将一个类型的数据转换为另一个类型。我们可以看到这个接口需要指定两个泛型`T`和`R`,`T`就表示要转换的源类型,`R`表示要转换的目标类型。
*/
public class FunctionTest {
public static void main(String[] args) {
//将String类型转换成Integer类型
Integer integer = testApply("22", Integer::parseInt);
System.out.println(integer);
//将String类型转换成Integer后加10 再转换成String类型返回
String s = testAndThen("22", strData -> Integer.parseInt(strData) + 10, String::valueOf);
System.out.println(s);
//将Integer类型转换成String后加10 再转换成Integer类型返回
Integer integer1 = testCompose(22, Integer::parseInt, str -> str+""+10);
System.out.println(integer1);
}
/**
* 测试apply方法
*/
public static Integer testApply(String str, Function<String,Integer> function){
return function.apply(str);
}
/**
* 测试andThen方法
*/
public static String testAndThen(String str, Function<String,Integer> function1,Function<Integer,String> function2){
return function1.andThen(function2).apply(str);
}
/**
* 测试compose方法
*/
public static Integer testCompose(Integer intData, Function<String,Integer> function1,Function<Integer,String> function2){
return function1.compose(function2).apply(intData);
}
}
练习:
要过年了,把下面字符串中张三的年龄加一岁: “张三,22”。
/**
* 要过年了,把下面字符串中张三的年龄加一岁: "张三,22"
*/
public class UseFunction {
public static void main(String[] args) {
String zsInfo = "张三,22";
String addAge = addAge(zsInfo,
info -> Integer.parseInt(info.split(",")[1])+1,
age -> zsInfo.split(",")[0] + "," + age);
System.out.println(addAge);
}
public static String addAge(String info, Function<String,Integer> addAgeFunction,Function<Integer,String> splicingInfoFunction){
return addAgeFunction.andThen(splicingInfoFunction).apply(info);
}
}
Stream流
在我们研究 Java Stream API
之前,让我们看看为什么需要它。假设我们要遍历一个整数列表并找出所有大于 10 的整数的总和。在 Java 8 之前,我们的方法是:
public class BeforeStartStream {
public static void main(String[] args) {
int sumIterator = sumIterator(StreamDataUtils.intDataList());
System.out.println(sumIterator);
}
/**
*遍历一个整数列表并找出所有大于 10 的整数的总和
*/
private static int sumIterator(List<Integer> list) {
Iterator<Integer> it = list.iterator();
int sum = 0;
while (it.hasNext()) {
int num = it.next();
if (num > 10) {
sum += num;
}
}
return sum;
}
}
上述方法存在三个主要问题:
- 我们只想知道整数的总和,但我们还必须关心去如何进行迭代。
- 该程序本质上是顺序的,我们无法轻松地并发执行此操作。
- 一个简单的任务我们写了太多的代码。
为了解决上述所有缺点,引入了 Java 8 Stream API
。我们可以使用Java Stream API
来实现内部迭代,这样用起来就十分容易。Stream
的内部迭代提供了多种功能,例如顺序和并行执行、基于给定标准的过滤、映射等。
大多数 Java 8 Stream API
方法参数都是函数式接口,因此lambda
表达式与它们配合得很好。让我们看看如何使用 Java Streams
在单行语句中编写上述逻辑。
/**
*使用Stream 遍历一个整数列表并找出所有大于 10 的整数的总和
*/
private static int sumStream(List<Integer> list) {
return list.stream().filter(i -> i > 10).mapToInt(i -> i).sum();
}
首先,我们将研究Java 8 Stream API
的核心概念,然后我们将通过一些示例来理解最常用的方法。
初始化数据类
为了减少代码,封装了一个提供数据的类:
/**
* Stream数据测试帮助类,提供演示数据
*/
public class StreamDataUtils {
private StreamDataUtils() {}
public static List<Integer> intDataList(){
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(6);
list.add(13);
list.add(18);
list.add(20);
list.add(2);
list.add(7);
return list;
}
public static List<String> stringDataList(){
List<String> list = new ArrayList<>();
list.add("雷欧");
list.add("赛文");
list.add("迪迦");
list.add("泰罗");
list.add("初代");
return list;
}
public static Stream<String> stringDataStream(){
return stringDataList().stream();
}
public static Stream<Integer> intDataStream(){
return intDataList().stream();
}
public static List<UseStream.Employee> group1List(){
List<UseStream.Employee> group1 = new ArrayList<>();
group1.add(new UseStream.Employee("张三",11));
group1.add(new UseStream.Employee("李四",12));
group1.add(new UseStream.Employee("王五",60));
group1.add(new UseStream.Employee("迪迦",1123651));
group1.add(new UseStream.Employee("泰罗",321325));
group1.add(new UseStream.Employee("雷欧",1565));
group1.add(new UseStream.Employee("阿狸",2125));
group1.add(new UseStream.Employee("猴子",500));
group1.add(new UseStream.Employee("光辉",600));
group1.add(new UseStream.Employee("小炮",700));
group1.add(new UseStream.Employee("蚂蚱",8000));
group1.add(new UseStream.Employee("空虚行者",900));
return group1;
}
public static List<UseStream.Employee> group2List(){
List<UseStream.Employee> group2 = new ArrayList<>();
group2.add(new UseStream.Employee("张三",32));
group2.add(new UseStream.Employee("机器人",23));
group2.add(new UseStream.Employee("提莫",44));
group2.add(new UseStream.Employee("狗剩",34));
group2.add(new UseStream.Employee("张全蛋",32));
group2.add(new UseStream.Employee("兔崽子",111));
group2.add(new UseStream.Employee("李四",323));
group2.add(new UseStream.Employee("光辉",222));
group2.add(new UseStream.Employee("空虚行者",2323));
group2.add(new UseStream.Employee("薇恩",23));
group2.add(new UseStream.Employee("大头",232));
group2.add(new UseStream.Employee("迪迦",2132132));
return group2;
}
}
Optional
Java Optional
是一个可能为空的容器对象。如果容器中存在值,则它的isPresent()
方法 将返回 true
,get()
方法将返回该值。他是一个中断操作
,它常用的方法有:
Optional<T> reduce(BinaryOperator<T> accumulator)
:计算Optional<T> min(Comparator<? super T> comparator)
:返回最小值Optional<T> max(Comparator<? super T> comparator)
:返回最大值Optional<T> findFirst()
:返回第一个数据Optional<T> findAny()
:返回任意一个数据
Java集合和 Java Stream
集合是按照特定数据结构存存储于内存中的一组数据,而Java Stream
是对数据进行处理计算的一种数据结构。它允许以声明性方式处理数据集合,可以把Stream流看作是遍历数据集合的一个高级迭代器。Java Stream
不存储数据,我们经常会用流去对集合进行一些流水线的操作。stream就像工厂一样,只需要把集合、命令还有一些参数灌输到 流水线 中去,就可以加工成得出想要的结果。这样的流水线能大大简洁代码,减少操作。
Java 8 Stream
内部迭代原理有助于在某些流操作中实现延迟操作。例如,过滤、映射或重复删除数据可以延迟进行,从而实现更高的性能操作。
Java 8 Stream
支持顺序和并行处理,并行处理对于处理大型集合数据的提高性能非常有帮助。
所有 Java Stream API
接口和类都在 java.util.stream
包中。由于我们在集合中使用原始数据类型,例如 int
、long
等,会有自动装箱的操作,并且这些操作可能需要很多时间,因此有针对原始类型的特定Stream
类 - IntStream
、LongStream
和 DoubleStream
。
常用方法
中间操作(Intermediate Operations)
Java Stream API
中会返回新的Stream
对象的操作称为中间操作。大多数情况下,这些操作本质上是延迟性的,它们生成一个新的Stream
元素,并将其传递到下一个操作。中间操作不会返回最终的处理结果,即直到实际需要处理结果时才会执行这些中间操作。常用的中间操作有:
filter()
: 过滤。map()
: 用于映射每个元素到对应的结果。flatMap()
: 把Stream中的层级结构扁平化,就是将最底层元素抽出来放到一起(可将每个元素的流分割成更细的流然后返回一个新流)。distinct()
: 结果去重。sorted()
: 对流进行排序。peek()
: 和map()
方法差不多,map()
方法可以改变返回值类型,而peek无法改变返回值类型。limit()
: 用于获取指定数量的流。skip()
: 跳过stream中的前n个元素在执行。
终端操作(Terminal Operations)
在Java 8 Stream API
中,会返回结果的操作操作被称为终端操作。一旦这些方法被掉用,它就会把Stream
对象消费掉,然后我们就不能使用Stream
了。终端操作操作本质上是即时的,即它们会在返回结果之前,处理Stream
流中的所有数据。常用的终端操作有:
forEach()
: 循环forEachOrdered()
: 主要区别在并行处理上,forEach()
是并行处理的,forEachOrder()
是按顺序处理的toArray()
: 转换成数组reduce()
: 对数据进行计算collect()
: 将原来的Stream映射为一个单元素流,然后收集,Collectors.toList()
,`Collectors.toMap()等min()
: 返回最小值max()
: 返回最大值count()
: 返回元素数量anyMatch()
: anyMatch表示,判断的条件里,任意一个元素成功,返回trueallMatch()
: allMatch表示,判断条件里的元素,所有的都是,返回truenoneMatch()
: noneMatch跟allMatch相反,判断条件里的元素,所有的都不是,返回truefindFirst()
: 返回第一个元素findAny()
: 并行情况下随机返回一个元素
创建Java Stream
我们可以通过多种方式从数组和集合创建 Java Stream
:
- 我们可以使用
Stream.of()
从相似类型的数据创建一个流。例如,我们可以从一组nt
类型或Integer
类型的对象创建Java Stream
。
Stream<Integer> stream = Stream.of(1,2,3,4);
- 在
Stream.of()
传入数组对象创建Stream
流。注意:它不支持自动装箱,因此我们无法传递原始类型数组。
int[] intArr = {1, 2, 3, 4};
//Compile time error, Type mismatch: cannot convert from Stream<int[]> to Stream<Integer>
Stream<Integer> streamWithArr1 = Stream.of(intArr); //错误
- 使用集合
Collection
的stream()
方法或者parallelStream()
创建。
List<Integer> myList = new ArrayList<>();
for(int i=0; i<100; i++) myList.add(i);
//顺序 stream
Stream<Integer> sequentialStream = myList.stream();
//异步 stream
Stream<Integer> parallelStream = myList.parallelStream();
- 使用
Stream.generate()
和Stream.iterate()
方法创建无限流。注意:无限流不设置长度会一直执行下去,此处使用limit方法设置长度
Stream<Integer> stream1 = Stream.iterate(1, x -> x + 3);
stream1.limit(100).forEach(System.out::println);
Supplier<UUID> randomUUIDSupplier = UUID::randomUUID;
Stream<UUID> infiniteStreamOfRandomUUID = Stream.generate(randomUUIDSupplier);
infiniteStreamOfRandomUUID.limit(3).forEach(System.out::println);
- 使用
Arrays.stream()
和String.chars()
方法。
LongStream is = Arrays.stream(new long[]{1,2,3,4});
IntStream is2 = "abc".chars();
将Java Stream转换为集合或数组
- 我们可以使用
Java Stream
的collect()
方法从Stream中去获取一个List
,Map
或者Set
集合。
Stream<Integer> intStream = Stream.of(1,2,3,4);
List<Integer> intList = intStream.collect(Collectors.toList());
System.out.println(intList); //prints [1, 2, 3, 4]
intStream = Stream.of(1,2,3,4); //stream is closed, so we need to create it again
Map<Integer,Integer> intMap = intStream.collect(Collectors.toMap(i -> i, i -> i+10));
System.out.println(intMap); //prints {1=11, 2=12, 3=13, 4=14}
- 我们可以使用
Stream
的toArray()
方法去创建一个数组。
Stream<Integer> stream = Stream.of(1,2,3,4);
Integer[] intArray = stream.toArray(Integer[]::new);
System.out.println(Arrays.toString(intArray)); //prints [1, 2, 3, 4]
常用方法详解
中间操作
Stream filter(Predicate predicate)
: 可以将一个流过滤转换称为另外一个流。该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。
//过滤出大于10的数字
StreamDataUtils.intDataStream().filter(i->i>10).forEach(System.out::println);
Stream map(Function mapper)
: 将流中的元素映射到另一个流中,该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。
Stream<String> stringStream = StreamDataUtils.intDataStream().map(String::valueOf);
System.out.println(stringStream);
Stream limit(long maxSize)
:延迟方法,可以对流进行截取,只取用前n个。
StreamDataUtils.stringDataStream().limit(3).forEach(System.out::println);
Stream skip(long n)
:延迟方法,可以对流进行截取,跳过前n个。如果跳过的长度大于元素个数,则返回一个空的Stream
。
StreamDataUtils.intDataStream().skip(2).forEach(System.out::println);
static Stream concat(Stream a, Stream b)
: 传递两个Stream
,并将这两个Stream
合并成一个新的Stream
。
Stream<? extends Serializable> newStream = Stream.concat(StreamDataUtils.stringDataStream(), StreamDataUtils.intDataStream());
newStream.forEach(System.out::println);
Stream sorted()
: 对Stream
的元素按自然顺序进行排序。Stream sorted(Comparator<? super T> comparator)
:自己实现Comparator
,并根据自己实现的Comparator
进行排序。
Stream<String> names2 = Stream.of("aBc", "d", "ef", "123456");
List<String> reverseSorted = names2.sorted(Comparator.reverseOrder()).collect(Collectors.toList());
System.out.println(reverseSorted); // [ef, d, aBc, 123456]
Stream<String> names3 = Stream.of("aBc", "d", "ef", "123456");
List<String> naturalSorted = names3.sorted().collect(Collectors.toList());
System.out.println(naturalSorted); //[123456, aBc, d, ef]
Stream flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
:我们可以通过它去创建一个Stream对象,可以看到,参数为Function
接口,即将一个类型的数据转换成另一个指定类型。
Stream<List<String>> namesOriginalList = Stream.of(
Arrays.asList("PanKaj","Mark"),
Arrays.asList("David", "Lisa"),
Arrays.asList("Amit","Lesson"));
//将Stream流中的 List<String> 集合合并成一个 String stream 流
Stream<String> flatStream = namesOriginalList
.flatMap(Collection::stream);
flatStream.forEach(System.out::println);
Stream flatMapToDouble(Function<? super T,? extends DoubleStream> mapper)
:将T
类型数据转换成DoubleStream
对象。Stream flatMapToInt(Function<? super T,? extends IntStream> mapper)
:将T
类型数据转换成IntStream
对象。Stream flatMapToLong(Function<? super T,? extends LongStream> mapper)
:将T
类型数据转换成LongStream
对象。Stream peek(Consumer<? super T> action)
:消费Stream
中的元素,即可以对数据进行使用处理。
终端操作
void forEach(Consumer<? super T> action): 循环,该方法接收一个
Consumer接口函数,会将每一个流元素交给该函数进行处理。
java.util.function.Consumer接口是一个消费型接口。 Consumer接口中包含抽象方法
void accept(T t)`,意为消费一个指定泛型的数据。
//遍历并输出
StreamDataUtils.stringDataStream().forEach(System.out::println);
long count()
: 他是一个终结方法,与旧集合 Collection 当中的 size 方法一样,用来统计Stream
中的元素个数。
System.out.println(StreamDataUtils.intDataStream().count());
Optional<T> reduce(BinaryOperator<T> accumulator)
:对Stream
流中的元素进行计算处理,并返回一个Optional
。
//reduce 计算所有数据和
Stream<Integer> numbers = StreamDataUtils.intDataStream();
Optional<Integer> intOptional = numbers.reduce(Integer::sum);
//67
intOptional.ifPresent(integer -> System.out.println("Multiplication = " + integer));
T reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
:定义一个初始值,计算的时候同时要处理这个初始值,直接返回计算结果。
Stream<Integer> numbers2 = StreamDataUtils.intDataStream();
Integer reduce = numbers2.reduce(2, Integer::sum);
System.out.println(reduce);
boolean match()相关
:检查是否匹配。
//anyMatch 有一个匹配就返回true
Stream<Integer> numbers3 = StreamDataUtils.intDataStream();
System.out.println("Stream contains 4? "+numbers3.anyMatch(i -> i==4));
//allMatch 全部匹配返回true
Stream<Integer> numbers4 = StreamDataUtils.intDataStream();
System.out.println("Stream contains all elements less than 10? "+numbers4.allMatch(i -> i<10));
//noneMatch 没有一个匹配 返回true
Stream<Integer> numbers5 = StreamDataUtils.intDataStream();
System.out.println("Stream doesn't contain 10? "+numbers5.noneMatch(i -> i==10));
Optional<T> findFirst()
:返回一个Optional
,找出第一个元素。
//找到第一个以D开头的名字
Stream<String> names4 = Stream.of("PanKaj","Amit","David", "Lisa" ,"Dal");
Optional<String> firstNameWithD = names4.filter(i -> i.startsWith("D")).findFirst();
//David
firstNameWithD.ifPresent(s -> System.out.println("First Name starting with D=" + s));
Stream 的缺点
Java 8 Stream API
为我们处理集合和数组提供了很大的遍历,然后也会有一些限制。
- 无状态 lambda 表达式:如果使用
parallelStream()
并且lambda
表达式是有状态的,则可能会导致随机响应。
/**
* 如果运行上面的程序,会得到不同的结果,
* 因为它取决于流的迭代方式,这里没有为并行处理定义任何顺序。
* 如果使用顺序流,则不会出现这个问题。
*/
public class StatefulParallelStream {
public static void main(String[] args) {
List<Integer> ss = Arrays.asList(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15);
List<Integer> result = new ArrayList<>();
Stream<Integer> stream = ss.parallelStream();
stream.peek(s -> {
synchronized (result) {
if (result.size() < 10) {
result.add(s);
}
}
}).forEach( e -> {});
System.out.println(result);
}
}
- 一旦
Stream
被消费,它就不能再次被使用。比如上面的例子,我都会创建一个新的Stream
,而不会去重复使用。如果重复使用就会报错,可自己尝试。 Stream
的学习成本比较高。
方法引用
方法引用符:双冒号 ::
为引用运算符,而它所在的表达式被称为方法引用。如果Lambda
要表达的函数方案已经存在于某个方 法的实现中,那么则可以通过双冒号来引用该方法作为Lambda
的替代者。
创建一个函数接口,用来打印<key ,value>对:
@FunctionalInterface
public interface KeyAndValueHolder {
void printKeyAndValue(String key,Integer value);
}
/**
* 方法引用
*/
public class MethodQuote {
public static void main(String[] args) {
test1();
test2();
}
//前面讲过 Consumer 用来消费数据
private static void accept(Consumer<String> consumer){
consumer.accept("aaa");
}
private static void accept(Map.Entry<String,Integer> entry, KeyAndValueHolder keyAndValueHolder){
keyAndValueHolder.printKeyAndValue(entry.getKey(),entry.getValue());
}
private static void print(String key,Integer value){
System.out.println(key+"#"+value);
}
/**
* 打印一个key value对
*/
private static void test2() {
Map<String,Integer> map = new HashMap<>();
map.put("mark",33);
map.put("xu",18);
for (Map.Entry<String, Integer> entry : map.entrySet()) {
accept(entry,(String key,Integer value)->print(key,value));
accept(entry,MethodQuote::print);
}
}
/**
* 打印一个字符串
*/
public static void test1(){
//这里我们使用System.out 对象的 println 方法去消费 aaa 这个字符串,即打印这个字符串
accept(str-> {
System.out.println(str);
});
//通过方法引用方式
accept(System.out::println);
}
}
总结规律:
当我们所掉用处理数据的方法如:System.out.println(str)
和print(key,value)
所需的参数与函数式接口所需要的参数相同,并且中括号中只有一句代码的时候,那么就认为这个Lambda
表达式是可推导的,我们就可以使用方法引用去简化写法。
如果使用Lambda
,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式——它们都 将被自动推导。
而如果使用方法引用,也是同样可以根据上下文进行推导。 函数式接口是Lambda
的基础,而方法引用是Lambda
的孪生兄弟。
通过对象名引用成员方法
/**
* 通过对象名引用
* 函数接口使用 Consumer
*/
public class MethodRefObjectTest {
public static class MethodRefObject{
public void printUpperCase(String str) {
System.out.println(str.toUpperCase());
}
}
private static void printString(Consumer<String> consumer) {
consumer.accept("Hello");
}
public static void main(String[] args) {
MethodRefObject obj = new MethodRefObject();
printString(obj::printUpperCase);
}
}
通过类名称引用静态方法
/**
* 通过类名称引用静态方法
* 比如我们有一个时间工具类,工具类的方法一般都是静态的,
* 我们需要掉用这个工具类的parse静态方法去转换时间格式
*/
public class MethodRefStaticMethodTest {
public static void main(String[] args) {
accept(new Date(),date -> {
DateUtils.parse(date);
});
accept(new Date(),DateUtils::parse);
}
public static void accept(Date date,Consumer<Date> consumer){
consumer.accept(date);
}
public static class DateUtils{
private DateUtils() {
}
public static void parse(Date date){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(simpleDateFormat.format(date));
}
}
}
通过super引用成员方法
如果存在继承关系,当Lambda
中需要出现super
调用时,也可以使用方法引用进行替代。首先是函数式接口:
@FunctionalInterface
public interface Greetable {
void greet();
}
然后是父类 Human 的内容:
public class Human {
public void sayHello() {
System.out.println("Hello!");
}
}
最后是子类 Man 的内容,其中使用了Lambda的写法:
public class Man extends Human {
@Override
public void sayHello() {
System.out.println("大家好,我是Man!");
}
//定义方法method,参数传递Greetable接口
public void method(Greetable g){
g.greet();
}
public void show(){
//调用method方法,使用Lambda表达式
method(()->{
//创建Human对象,调用sayHello方法
new Human().sayHello();
});
//简化Lambda
method(()->new Human().sayHello());
//使用super关键字代替父类对象
method(()->super.sayHello());
method(super::sayHello);
}
}
通过this引用成员方法
this
代表当前对象,如果需要引用的方法就是当前类中的成员方法,那么可以使用this::成员方法
的格式来使用方 法引用。
@FunctionalInterface
public interface Richable {
void buy();
}
public class Husband {
private void buyHouse() {
System.out.println("买套房子");
}
private void marry(Richable lambda) {
lambda.buy();
}
public void beHappy() {
marry(this::buyHouse);
}
}
类的构造器引用
由于构造器的名称与类名完全一样,并不固定。所以构造器引用使用 类名称::new 的格式表示。
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@FunctionalInterface
public interface PersonBuilder {
Person buildPerson(String name);
}
public class ConstructorRef {
public static void printName(String name, PersonBuilder builder) {
System.out.println(builder.buildPerson(name).getName());
}
public static void main(String[] args) {
printName("赵丽颖", Person::new);
}
}
数组的构造器引用
数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。
@FunctionalInterface
public interface ArrayBuilder {
int[] buildArray(int length);
}
public class ArrayInitRef {
private static int[] initArray(int length, ArrayBuilder builder) {
return builder.buildArray(length);
}
public static void main(String[] args) {
int[] array = initArray(10, int[]::new);
}
}
练习
有两组员工数据:第一组数据为数据库已存在的数据,第二组数据为用户需要新增或者修改的数据
要求:
-
比较第一组数据和第二组数据中员工的姓名,如果第一组数据中有第二组姓名相同的数据,则这批数据是修改的员工,则对数据库进行更新操作
如果第一组数据中没有的第二组数据,说明是新增的员工,则对这批数据进行新增操作。
-
新增或修改玩数据库之后,只返回他们的名字,展示给客户看。
/**
* 作业:
* 有两组Employee员工数据:
* 第一组数据为数据库已存在的数据
* 第二组数据为用户需要新增或者修改的数据
* 要求:比较第一组数据和第二组数据中员工的姓名,
* 如果第一组数据中有第二组姓名相同的数据,则这批数据是修改的员工,则对数据库进行更新操作
* 如果第一组数据中没有的第二组数据,说明是新增的员工,则对这批数据进行新增操作
*
* 新增或修改玩数据库之后,只返回他们的名字,展示给客户看
*
*/
public class UseStream {
public static void main(String[] args) {
List<Employee> employees1 = StreamDataUtils.group1List();
List<Employee> employees2 = StreamDataUtils.group2List();
//需要新增的数据
List<Employee> needInsertEmployee =
employees2.stream().filter(e2 -> employees1.stream().noneMatch(e1 -> Objects.equals(e1.getName(), e2.getName()))).collect(Collectors.toList());
System.out.println("=============需要新增的数据=================");
needInsertEmployee.forEach(System.out::println);
List<Employee> needUpdateEmployee =
employees2.stream().filter(e2 -> employees1.stream().anyMatch(e1 -> Objects.equals(e1.getName(), e2.getName()))).collect(Collectors.toList());
System.out.println("=============需要修改的数据=================");
needUpdateEmployee.forEach(System.out::println);
System.out.println("=============需要新增的员工的姓名=================");
needInsertEmployee.stream().map(Employee::getName).collect(Collectors.toList()).forEach(System.out::println);
System.out.println("=============需要修改的员工的姓名=================");
needUpdateEmployee.stream().map(Employee::getName).collect(Collectors.toList()).forEach(System.out::println);
}
static class Employee {
private String name;
private Integer age;
public Employee() {
}
public Employee(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}