今日内容
- Lambda表达式
- Stream流
教学目标
- 能够掌握Lambda表达式的标准格式与省略格式
- 能够通过集合、映射或数组方式获取流
- 能够掌握常用的流操作
- 能够将流中的内容收集到集合和数组中
第一章 函数式接口
1.1 概述
函数式接口在Java中是指:有且仅有一个抽象方法的接口。
函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。
格式
只要确保接口中有且仅有一个抽象方法即可:
修饰符 interface 接口名称 {
public abstract 返回值类型 方法名称(可选参数信息);
// 其他非抽象方法内容
}
由于接口当中抽象方法的public abstract
是可以省略的,所以定义一个函数式接口很简单:
public interface MyFunctionalInterface {
void myMethod();
}
自定义函数式接口
对于刚刚定义好的MyFunctionalInterface
函数式接口,典型使用场景就是作为方法的参数:
public class Demo07FunctionalInterface {
// 使用自定义的函数式接口作为方法参数
private static void doSomething(MyFunctionalInterface inter) {
inter.myMethod(); // 调用自定义的函数式接口方法
}
public static void main(String[] args) {
// 调用使用函数式接口的方法
doSomething(() -> System.out.println("Lambda执行啦!"));
}
}
FunctionalInterface注解
与@Override
注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解:@FunctionalInterface
。该注解可用于一个接口的定义上:
@FunctionalInterface
public interface MyFunctionalInterface {
void myMethod();
}
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
1.2 常用函数式接口
JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在java.util.function
包中被提供。下面是两个常用的函数式接口及使用示例。
Supplier接口
java.util.function.Supplier<T>
接口仅包含一个无参的方法:T get()
。用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。
说明:
1、这里的泛型T表示当我们使用该接口时,传递什么数据类型就是什么数据类型。然后我们通过这个接口中的get方法就可以获取到指定泛型的类型的对象。
举例:比如使用该接口时,指定泛型T是String类型,那么通过get()方法就可以获取String类的对象。
举例:比如使用该接口时,指定泛型T是Student类型,那么通过get()方法就可以获取Student类的对象。
2、由于Supplier属于函数式接口,我们可以使用Lambda来实现。在Lambda中可以完成Supplier接口中的get()方法体中的代码。主要完成生产某个类的对象数据的功能。即我们需要使用Lambda返回指定泛型类的对象。
3、其实Supplier接口就是用来生产某个类的对象的。
需求:指定泛型是String类型,获取String类的对象。
代码实现如下所示:
/*
需求:指定泛型是String类型,获取String类的对象。
*/
public class SupplierDemo {
public static void main(String[] args) {
//调用方法
String s = getString(() -> {
return "hello";
});
//输出获取到的String类的对象s
System.out.println("s = " + s);
}
//定义方法获取String类的对象
public static String getString(Supplier<String> lambda)//Supplier<String> 已经确定泛型T是String类型
{
return lambda.get();
}
}
说明:上述只是生产String类的对象,那么我们要生产任何类的对象,我们可以修改代码如下所示:
public class SupplierDemo01 {
public static void main(String[] args) {
//调用方法
String s = getObject(() -> {
return "hello";
});
//输出获取到的String类的对象s
System.out.println("s = " + s);
//获取Integer类的对象
Integer i = getObject(() -> {
return 123;
});
//输出
System.out.println("i = " + i);
}
//定义方法获取任意类的对象
public static <T> T getObject(Supplier<T> lambda)//这样定义会更加方便,可以获取任意类型的对象
{
return lambda.get();
}
}
问题:
有的同学会有疑问:如下述代码:
Integer i = getObject(() -> {
return 123;
});
我还不如直接定义:Integer i =123;或者int i=123;这样定义更加简单。为什么还要使用Supplier接口方式,这么麻烦。
原因:
使用Integer i =123;或者int i=123;这样定义,就把获取对象数据的方式写死了、固定了。很不灵活。
而使用Supplier接口的方式,我们可以在getObject方法中,除了执行return lambda.get();我们还可以书写其他的代码,完成其他的功能。比如再生产某个对象时,有可能我们还会结合其他的类和对象完成更多的功能。只是我们这里书写的比较简单,简化了业务代码。
Consumer接口
java.util.function.Consumer<T>
接口,是消费一个数据,其数据类型由泛型参数决定。
抽象方法:accept
Consumer
接口中包含抽象方法void accept(T t)
: 消费一个指定泛型的数据。
基本使用如:
//给你一个字符串,请按照大写的方式进行消费
import java.util.function.Consumer;
public class Demo08Consumer {
public static void main(String[] args) {
String str = "Hello World";
//1.lambda表达式标准格式
fun(str,(String s)->{
System.out.println(s.toUpperCase());
});
//2.lambda表达式简化格式
fun(str,s-> System.out.println(s.toUpperCase()));
}
/*
定义方法,使用Consumer接口作为参数
fun方法: 消费一个String类型的变量
*/
public static void fun(String s,Consumer<String> con) {
con.accept(s);
}
}
Predicate接口
有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate<T>
接口。
抽象方法:test
Predicate
接口中包含一个抽象方法:boolean test(T t)
。用于条件判断的场景:
//1.练习:判断字符串长度是否大于5
//2.练习:判断字符串是否包含"H"
public class Demo09Predicate {
private static void method(Predicate<String> predicate,String str) {
boolean veryLong = predicate.test(str);
System.out.println("字符串很长吗:" + veryLong);
}
public static void main(String[] args) {
method(s -> s.length() > 5, "HelloWorld");
}
}
条件判断的标准是传入的Lambda表达式逻辑,只要字符串长度大于5则认为很长。
Function接口
java.util.function.Function<T,R>
接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件,有进有出。
抽象方法:apply
Function
接口中最主要的抽象方法为:R apply(T t)
,根据类型T的参数获取类型R的结果。
说明:
Function接口其实是一个转换接口,具有转换功能,可以将传入的T类型数据转换为R类型数据。转换的操作由Lambda表达式来处理,就是根据类型T作为参数的数据获取R类型的数据。
使用的场景例如:将String
类型转换为Integer
类型。
“123”------>转换为123。
/*
使用的场景例如:将String类型转换为Integer类型。
"123"------>转换为123。
*/
public class FunctionDemo {
public static void main(String[] args) {
//调用method方法
/*int x = method("123", (s) -> {
return Integer.parseInt(s);
});*/
int x = method("123", s->Integer.parseInt(s));
//输出转换后的值
System.out.println("x = " + x);
}
//定义方法通过接口Function将字符串转换为整数
public static int method(String s,Function<String,Integer> lambda)
{
return lambda.apply(s);
}
}
第二章 Stream(必须掌握)
在Java 8中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。
2.1 引言
传统集合的多步遍历代码
几乎所有的集合(如Collection
接口或Map
接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如:
public class Demo10ForEach {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
for (String name : list) {
System.out.println(name);
}
}
}
这是一段非常简单的集合遍历操作:对集合中的每一个字符串都进行打印输出操作。
循环遍历的弊端
Java 8的Lambda让我们可以更加专注于做什么(What),而不是怎么做(How),这点此前已经结合内部类进行了对比说明。现在,我们仔细体会一下上例代码,可以发现:
- for循环的语法就是“怎么做”
- for循环的循环体才是“做什么”
为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。
试想一下,如果希望对集合中的元素进行筛选过滤:
- 将集合A根据条件一过滤为子集B;
- 然后再根据条件二过滤为子集C。
那怎么办?在Java 8之前的做法可能为:
这段代码中含有三个循环,每一个作用不同:
- 首先筛选所有姓张的人;
- 然后筛选名字有三个字的人;
- 最后进行对结果进行打印输出。
public class Demo11NormalFilter {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
List<String> zhangList = new ArrayList<>();
for (String name : list) {
if (name.startsWith("张")) {
zhangList.add(name);
}
}
List<String> shortList = new ArrayList<>();
for (String name : zhangList) {
if (name.length() == 3) {
shortList.add(name);
}
}
for (String name : shortList) {
System.out.println(name);
}
}
}
每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循环是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使用另一个循环从头开始。
那,Lambda的衍生物Stream能给我们带来怎样更加优雅的写法呢?
Stream的更优写法
下面来看一下借助Java 8的Stream API,什么才叫优雅:
public class Demo12StreamFilter {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
list.stream()
.filter(s -> s.startsWith("张"))
.filter(s -> s.length() == 3)
.forEach(s -> System.out.println(s));
}
}
直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤姓张、过滤长度为3、逐一打印。代码中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。
2.2 流式思想概述
注意:这里和传统IO流没有任何关系!
整体来看,流式思想类似于工厂车间的“生产流水线”。
当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个“模型”步骤方案,然后再按照方案去执行它。
这张图中展示了过滤、映射、跳过、计数等多步操作,这是一种集合元素的处理方案,而方案就是一种“函数模型”。图中的每一个方框都是一个“流”,调用指定的方法,可以从一个流模型转换为另一个流模型。而最右侧的数字3是最终结果。
这里的filter
、map
、skip
都是在对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法count
执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。
备注:“Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)。
2.3 获取流方式
java.util.stream.Stream<T>
是Java 8新加入的最常用的流接口。(这并不是一个函数式接口。)
获取一个流非常简单,有以下几种常用的方式:
- 所有的
Collection
集合都可以通过stream
默认方法获取流; Stream
接口的静态方法of
可以获取数组对应的流。
方式1 : 根据Collection获取流
首先,java.util.Collection
接口中加入了default方法stream
用来获取流,所以其所有实现类均可获取流。
import java.util.*;
import java.util.stream.Stream;
/*
获取Stream流的方式
1.Collection中 方法
Stream stream()
2.Stream接口 中静态方法
of(T...t) 向Stream中添加多个数据
*/
public class Demo13GetStream {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// ...
Stream<String> stream1 = list.stream();
Set<String> set = new HashSet<>();
// ...
Stream<String> stream2 = set.stream();
}
}
方式2: 根据数组获取流
如果使用的不是集合或映射而是数组,由于数组对象不可能添加默认方法,所以Stream
接口中提供了静态方法of
,使用很简单:
import java.util.stream.Stream;
public class Demo14GetStream {
public static void main(String[] args) {
String[] array = { "张无忌", "张翠山", "张三丰", "张一元" };
Stream<String> stream = Stream.of(array);
}
}
备注:
of
方法的参数其实是一个可变参数,所以支持数组。
2.4 常用方法
流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:
- 终结方法:返回值类型不再是
Stream
接口自身类型的方法,因此不再支持类似StringBuilder
那样的链式调用。本小节中,终结方法包括count
和forEach
方法。 - 非终结方法:返回值类型仍然是
Stream
接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为非终结方法。)
备注:本小节之外的更多方法,请自行参考API文档。
forEach : 逐一处理
虽然方法名字叫forEach
,但是与for循环中的“for-each”昵称不同,该方法并不保证元素的逐一消费动作在流中是被有序执行的。
void forEach(Consumer<? super T> action);
参数:
Consumer属于消费函数式接口,我们在使用void forEach(Consumer<? super T> action);方法的时候可以传递lambda,完成Consumer接口中的 void accept(T t);
该方法接收一个Consumer
接口函数,会将每一个流元素交给该函数进行处理。例如:
import java.util.stream.Stream;
public class Demo15StreamForEach {
public static void main(String[] args) {
Stream<String> stream = Stream.of("大娃","二娃","三娃","四娃","五娃","六娃","七娃","爷爷","蛇精","蝎子精");
//Stream<String> stream = Stream.of("张无忌", "张三丰", "周芷若");
stream.forEach((String str)->{System.out.println(str);});
}
}
在这里,lambda表达式(String str)->{System.out.println(str);}
就是一个Consumer函数式接口的示例。
filter:过滤
可以通过filter
方法将一个流转换成另一个子集流。方法声明:
Stream<T> filter(Predicate<? super T> predicate);
参数:
Predicate 表示判断函数式接口,可以使用lambda,我们在使用filter(Predicate<? super T> predicate);方法的时候可以传递lambda完成Predicate 接口中的抽象方法 boolean test(T t);
如果,满足条件,方法test(T t)返回true,那么filter(Predicate<? super T> predicate);就会将满足条件的数据放到流水线中
该接口接收一个Predicate
函数式接口参数(可以是一个Lambda)作为筛选条件。
基本使用
Stream流中的filter
方法基本使用的代码如:
public class Demo16StreamFilter {
public static void main(String[] args) {
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.filter((String s) -> {return s.startsWith("张");});
}
}
在这里通过Lambda表达式来指定了筛选的条件:必须姓张。
count:统计个数
正如旧集合Collection
当中的size
方法一样,流提供count
方法来数一数其中的元素个数:
long count();
该方法返回一个long值代表元素个数(不再像旧集合那样是int值)。基本使用:
public class Demo17StreamCount {
public static void main(String[] args) {
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.filter(s -> s.startsWith("张"));
System.out.println(result.count()); // 2
}
}
limit:取用前几个
limit
方法可以对流进行截取,只取用前n个。方法签名:
Stream<T> limit(long maxSize):获取Stream流对象中的前n个元素,返回一个新的Stream流对象
参数是一个long型,如果流水线当前长度大于参数则进行截取;否则不进行操作。基本使用:
import java.util.stream.Stream;
public class Demo18StreamLimit {
public static void main(String[] args) {
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.limit(2);
System.out.println(result.count()); // 2
}
}
skip:跳过前几个
如果希望跳过前几个元素,可以使用skip
方法获取一个截取之后的新流:
Stream<T> skip(long n): 跳过Stream流对象中的前n个元素,返回一个新的Stream流对象
如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。基本使用:
import java.util.stream.Stream;
public class Demo19StreamSkip {
public static void main(String[] args) {
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.skip(2);
System.out.println(result.count()); // 1
}
}
映射:map
如果需要将流中的元素映射到另一个流中,可以使用map
方法。方法说明:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
属于延迟方法
该接口需要一个Function
函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。
复习Function<T,R>接口
此前我们已经学习过java.util.stream.Function
函数式接口,其中唯一的抽象方法为:
R apply(T t);
这可以将一种T类型转换成为R类型,而这种转换的动作,就称为“映射”。
对于map方法的解释举例,如下所示:
基本使用
Stream流中的map
方法基本使用的代码如:
public class StreamMapDemo {
public static void main(String[] args) {
//创建老流对象存储String类型
Stream<String> strStream = Stream.of("123", "345", "567");
//使用map方法将老流转换为新流
//<R> Stream<R> map(Function<? super T, ? extends R> mapper);
//由于map方法的参数需要Function接口,Function只有一个抽象方法R apply(T t);属于函数式接口
//可以使用lambda
/*Stream<Integer> integerStream = strStream.map((s) -> {
return Integer.parseInt(s);
});*/
Stream<Integer> integerStream = strStream.map(s->Integer.parseInt(s));
//取出新流中的数据
integerStream.forEach(name->System.out.println(name));
}
}
这段代码中,map
方法的参数通过方法引用,将字符串类型转换成为了int类型(并自动装箱为Integer
类对象)。
concat:组合
如果有两个流,希望合并成为一个流,那么可以使用Stream
接口的静态方法concat
:
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b): 把参数列表中的两个Stream流对象a和b,合并成一个新的Stream流对象
备注:这是一个静态方法,与
java.lang.String
当中的concat
方法是不同的。
该方法的基本使用代码如:
import java.util.stream.Stream;
public class Demo20StreamConcat {
public static void main(String[] args) {
Stream<String> streamA = Stream.of("张无忌");
Stream<String> streamB = Stream.of("张翠山");
Stream<String> result = Stream.concat(streamA, streamB);
}
}
2.5 Stream综合案例
现在有两个ArrayList
集合存储队伍当中的多个成员姓名,要求使用Stream依次进行以下若干操作步骤:
- 第一个队伍只要名字为3个字的成员姓名;
- 第一个队伍筛选之后只要前3个人;
- 第二个队伍只要姓张的成员姓名;
- 第二个队伍筛选之后不要前2个人;
- 将两个队伍合并为一个队伍;
- 根据姓名创建
Person
对象; - 打印整个队伍的Person对象信息。
两个队伍(集合)的代码如下:
public class DemoArrayListNames {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
one.add("迪丽热巴");
one.add("宋远桥");
one.add("苏星河");
one.add("老子");
one.add("庄子");
one.add("孙子");
one.add("洪七公");
List<String> two = new ArrayList<>();
two.add("古力娜扎");
two.add("张无忌");
two.add("张三丰");
two.add("赵丽颖");
two.add("张二狗");
two.add("张天爱");
two.add("张三");
// ....
}
}
而Person
类的代码为:
public class Person {
private String name;
public Person() {}
public Person(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{name='" + name + "'}";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Stream方式
Stream流式处理代码为:
public class DemoStreamNames {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
// ...
List<String> two = new ArrayList<>();
// ...
// 第一个队伍只要名字为3个字的成员姓名;
// 第一个队伍筛选之后只要前3个人;
Stream<String> streamOne = one.stream().filter(s -> s.length() == 3).limit(3);
// 第二个队伍只要姓张的成员姓名;
// 第二个队伍筛选之后不要前2个人;
Stream<String> streamTwo = two.stream().filter(s -> s.startsWith("张")).skip(2);
// 将两个队伍合并为一个队伍;
// 根据姓名创建Person对象;
// 打印整个队伍的Person对象信息。
Stream.concat(streamOne, streamTwo).map((name)->{return new Person(name);}).forEach((p)->{System.out.println(p);});
}
}
运行效果:
Person{name='宋远桥'}
Person{name='苏星河'}
Person{name='洪七公'}
Person{name='张二狗'}
Person{name='张天爱'}
Person{name='张三'}
2.6 收集Stream结果
对流操作完成之后,如果需要将其结果进行收集,例如获取对应的集合、数组等,如何操作?
收集到集合中
Stream流提供collect
方法,其参数需要一个java.util.stream.Collector<T,A, R>
接口对象来指定收集到哪种集合中。幸运的是,java.util.stream.Collectors
类提供一些方法,可以作为Collector
接口的实例:
public static <T> Collector<T, ?, List<T>> toList()
:转换为List
集合。public static <T> Collector<T, ?, Set<T>> toSet()
:转换为Set
集合。
下面是这两个方法的基本使用代码:
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Demo15StreamCollect {
public static void main(String[] args) {
Stream<String> stream = Stream.of("10", "20", "30", "40", "50");
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
}
}
收集到数组中
Stream提供toArray
方法来将结果放到一个数组中,返回值类型是Object[]的:
Object[] toArray();
其使用场景如:
import java.util.stream.Stream;
public class Demo16StreamArray {
public static void main(String[] args) {
Stream<String> stream = Stream.of("10", "20", "30", "40", "50");
Object[] objArray = stream.toArray();
}
}