Java【Stream/MethodReference】
一、Stream流
java8
之后,得益于Lambda
所带来的函数式编程,引入了一个全新的Stream
概念,用于解决已有集合类库既有的弊端。
1.1 概述
传统集合的多步遍历
几乎所有的集合(Collection
接口或Map
接口等)都支持直接或间接的遍历操作。
循环遍历的弊端
Java8
的Lambda
让我们更加专注于做什么,而不是怎么做。为什么要使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。
传统的集合遍历
public class Demo01List {
public static void main(String[] args) {
// 创建一个List集合,存储姓名
List<String> list = new ArrayList<>();
list.add("迪丽热巴");
list.add("玛尔扎哈");
list.add("赵无极");
list.add("赵晓");
list.add("赵匡胤");
// 对集合中的元素进行过滤,只要"赵"开头的元素,并存储到一个新的集合中
List<String> listA = new ArrayList<>();
for (String s : list) {
if(s.startsWith("赵")){
listA.add(s);
}
}
// 对listA集合进行过滤,取出姓名长度为2的人,并存储到一个新的集合中
List<String> listB = new ArrayList<>();
for (String s : listA) {
if(s.length()==2) {
listB.add(s);
}
}
// 遍历listB集合
for (String s : listB) {
System.out.println(s);
}
}
}
Stream的更优写法
使用Stream
流的方式遍历集合,对集合中的数据进行遍历。Stream
流是1.8之后出现的,关注的是做什么而不是怎么做。
public class Demo02Stream {
public static void main(String[] args) {
// 创建一个List集合,存储姓名
List<String> list = new ArrayList<>();
list.add("迪丽热巴");
list.add("玛尔扎哈");
list.add("赵无极");
list.add("赵晓");
list.add("赵匡胤");
// 对集合中的元素进行过滤,只要"赵"开头的元素,并存储到一个新的集合中
// 对listA集合进行过滤,取出姓名长度为2的人,并存储到一个新的集合中
// 遍历listB集合
list.stream()
.filter(name->name.startsWith("赵"))
.filter(name-> name.length()==2)
.forEach(name-> System.out.println(name));
}
}
1.2 流式思想
整体来看,流式思想类似于工厂车间的”生产线“,流遵循了做什么而非怎么做的原则。流表面上看上去和集合很类似,都可以让我们转换和获取数据,但是它们之间存在着显著的差异:
-
流并不存储其元素
-
流的操作不会修改其数据源
-
**流的操作是尽可能惰性执行的。**这意味直至需要其结果时,操作才会执行。
当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们首先应该拼接好一个”模型“步骤方案,然后按照方案去执行。
上图展示了过滤、映射、跳过、计数等多步操作,这是一个集合元素的处理方案,而方案就是一种函数模型。图中的每一个方框都是一个”流“,调用指定的方法,可以从一个流模型转换成另一个流模型,而数字3是最终结果。
需要注意的是,上面的filter
、map
、skip
都是在对函数模型进行操作,集合元素并没有真正被处理,只有当终结方法count
方法执行的时候,整个模型才会按照指定策略执行操作,这也是Lambda
的延迟执行特点的体现。
使用流的三个基本步骤:获取一个数据源(source
)->数据转换->执行操作获取想要的结果。每次转换原有的Stream对象不改变,返回一个新的Stream
对象(允许多次转换),这就允许其操作像链条一样排列,变成一个管道。
1.3 获取流
java.util.stream.Stream<T>
是Java 8 新加入的最常用的流接口。(这并不是一个函数式接口)
获取一个流非常简单,有以下几种常用方式:
- 所有的
Collection
集合都可以通过steam
默认方法获取流 Stream
接口的静态方法of
可以获取数组对应的流。
根据Collection获取流
// 把集合转换为Stream流
List<String> list = new ArrayList<>();
Stream<String> stream = list.stream();
Set<String> set = new HashSet<>();
Stream<String> stream2 = set.stream();
根据Map获取流
Map<String,String> map = new HashMap<>();
// 获取键,存储到一个Set集合中
Set<String> keySet = map.keySet();
Stream<String> stream3 = keySet.stream();
// 获取值,存储到一个Collection集合中
Collection<String> value = map.values();
Stream<String> stream4 = value.stream();
// 获取键值对(EntrySet)
Set<Map.Entry<String,String>> entries = map.entrySet();
Stream<Map.Entry<String,String>> stream5 = entries.stream();
根据数据获取流
// 把数组转换为Stream流
Stream<Integer> stream6 = Stream.of(1,2,3,4,5);
// 可变参数传递数组
Integer[] arr = {1, 2, 3, 4, 5};
Stream<Integer> stream7 = Stream.of(arr);
1.4 常用方法
流模型的操作很丰富,这里介绍一些常用的API,具体可以分为两类:
延迟方法:返回值仍然是Stream
类型,支持链式调用。
终结方法:返回值不再是Stream
类型
逐一处理:forEach
forEach
:用来遍历流中的数据
// 获取一个Stream流
Stream<String> stream = Stream.of("迪丽热巴", "玛尔扎哈", "打铁赵晓");
// 使用Stream流中的forEach方法对流进行遍历
stream.forEach(name -> System.out.println(name));// 简化写法
过滤:filter
Stream
流中常用方法filter
:用于对流中的数据进行过滤 Stream<T> filter(Predicate<? super T> predicate)
filter
方法的参数Predicate
是一个函数式接口,所以可以传递Lambda
表达式,对元素进行过滤。
注意:泛型通配符的概念
泛型的上限限定: ? extends E 代表使用的泛型只能是E的子类/本身
泛型的下限限定: ? super E 代表使用的泛型只能是E的父类/本身
// 创建一个Stream流
Stream<String> stream = Stream.of("赵无极", "赵晓", "张三丰", "张无忌", "张全蛋");
// 对Stream中的元素进行过滤,只要姓赵的人
Stream<String> stream2 = stream.filter(name -> name.startsWith("赵"));
// 遍历流中的数据
stream2.forEach(name -> System.out.println(name));
/*
Stream 流属于管道流,只能被消费(使用)一次
第一个Stream流调用完毕方法,数据就会转到下一个Stream中
第一个Stream流使用完毕,就会自动关闭,此时再调用它的方法,会报错
java.lang.IllegalStateException: stream has already been operated upon or closed
*/
// 遍历Stream流,报错
stream.forEach(name -> System.out.println(name));
映射:map
如果要将流中的元素映射到另一个流中,可以使用map
方法: <R> Stream<R> map(Function<? super T,? extends R> mapper)
,该接口需要一个Function函数式接口参数,可以将流中的T类型数据,转换成另一种R类型数据的流。
// 获取一个Stream类型的流
Stream<String> stream = Stream.of("1", "2", "3", "4", "5");
// 使用Stream流中的map方法,将字符串类型的整数映射为Integer类型
Stream<Integer> stream2 = stream.map(s -> Integer.parseInt(s));
// 遍历stream2
stream2.forEach(i-> System.out.println(i));
统计个数:count
Stream
流中的常用方法count
:用于统计Stream
流中元素的个数
// 获取一个流
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.add(7);
Stream<Integer> stream = list.stream();
// 调用count方法
long count = stream.count();
System.out.println(count);
取用前几个:limit
Stream
流中的常用方法limit: 用于截取流中的元素 。可以对流中的数据元素进行截取,只取前n个。
Stream<T> limit(long maxSize)
limit
方法是一个延迟方法,对流中的元素进行截取返回一个新的流。
// 获取一个新的流
Stream<String> stream = Stream.of("美羊羊", "喜羊羊", "懒羊羊", "灰太狼", "红太狼");
// 使用limit方法截取前三个元素
Stream<String> stream2 = stream.limit(3);
// 遍历stream2
stream2.forEach(name-> System.out.println(name));
跳过前几个:skip
Stream
流中的常用方法:skip
用于跳过元素。 如果希望跳过前几个元素,可以使用skip
方法来截取获得一个新的流 Stream<T> skip(long n)
。如果流的长度大于n,则会返回一个截取后的新流,否则会返回一个长度为0的空流。
// 获取一个新的流
Stream<String> stream = Stream.of("美羊羊", "喜羊羊", "懒羊羊", "灰太狼", "红太狼");
// 使用skip方法截跳过三个元素
Stream<String> stream2 = stream.skip(3);
// 遍历stream2
stream2.forEach(name-> System.out.println(name));
组合:concat
concate
:用于把流组合到一起static <T> Stream<T> concat(Stream<? extends T> a,Stream<? extends T> b)
// 创建一个Stream流
Stream<String> stream = Stream.of("黄通", "任登宝", "郭子瑞", "徐超", "戴维斯");
// 获取一个Stream流
String[] arr = {"赵无极", "赵敏", "赵雷", "赵晓"};
Stream<String> stream2 = Stream.of(arr);
// 使用concate方法组合两个Stream
Stream<String> concate = Stream.concat(stream,stream2);
// 遍历流
concate.forEach(name-> System.out.println(name));
1.5 练习:集合元素处理(传统方式)
要求:
有两个ArrayList
集合存储队伍当中的多个成员姓名,要求使用传统的for循环(或增强for循环)依次进行以下步骤:
1.第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
2.第一个队伍筛选之后只要前3个人;存储到一个新集合中。
3.第二个队伍只要姓德的成员姓名;存储到一个新集合中。
4.第二个队伍帅选之后不要前两个人;存储到一个新集合中。
5.将两个队伍合并为一个队伍:存储到一个新集合中。
6.根据姓名创建Person对象;存储到一个新集合中。
7.打印整个队伍的Person对象信息。
// 第一个队伍
ArrayList<String> one = new ArrayList<>();
one.add("迪丽热巴");
one.add("玛尔扎哈");
one.add("宋");
one.add("赵匡胤");
one.add("赵无极");
one.add("赵大晓");
one.add("老子");
one.add("星宿老怪");
one.add("任登宝");
one.add("赵雷");
// 1.第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
ArrayList<String> one1 = new ArrayList<>();
for (String name : one) {
if(name.length() == 3) {
// System.out.println(name);
one1.add(name);
}
}
// 2.第一个队伍筛选之后只要前3个人;存储到一个新集合中。
ArrayList<String> one2 = new ArrayList<>();
for (int i = 0; i < one1.size(); i++) {
if(i<3) {
String name = one1.get(i);
// System.out.println(name);
one2.add(name);
}
}
// 第二个队伍
ArrayList<String> two = new ArrayList<>();
two.add("德罗巴");
two.add("德布劳内");
two.add("德约科维奇");
two.add("姆巴佩");
two.add("C罗");
two.add("德马科斯");
two.add("德尚");
two.add("尼古拉斯凯奇");
two.add("布莱恩特");
// 3.第二个队伍只要姓德的成员姓名;存储到一个新集合中。
ArrayList<String> two1 = new ArrayList<>();
for (String name : two) {
if(name.startsWith("德")){
two1.add(name);
}
}
// 4.第二个队伍帅选之后不要前两个人;存储到一个新集合中。
ArrayList<String> two2 = new ArrayList<>();
for (int i = 0; i < two1.size(); i++) {
if(i>=2) {
String name = two1.get(i);
two2.add(name);
}
}
// 5.将两个队伍合并为一个队伍:存储到一个新集合中。
ArrayList<String> list = new ArrayList<>();
list.addAll(one2);
list.addAll(two2);
// 6.根据姓名创建Person对象;存储到一个新集合中。
ArrayList<Person> list_person = new ArrayList<>();
for (String name : list) {
list_person.add(new Person(name));
}
// 7.打印整个队伍的Person对象信息。
for (Person person : list_person) {
System.out.println(person);
}
1.6 练习:集合元素处理(Stream方式)
使用Stream
流依次进行上题的步骤
// 第一个队伍
ArrayList<String> one = new ArrayList<>();
one.add("迪丽热巴");
one.add("玛尔扎哈");
one.add("宋");
one.add("赵匡胤");
one.add("赵无极");
one.add("赵大晓");
one.add("老子");
one.add("星宿老怪");
one.add("任登宝");
one.add("赵雷");
// 1.第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
// 2.第一个队伍筛选之后只要前3个人;存储到一个新集合中。
Stream<String> stream1 = one.stream().filter(name->name.length()==3).limit(3);
// 第二个队伍
ArrayList<String> two = new ArrayList<>();
two.add("德罗巴");
two.add("德布劳内");
two.add("德约科维奇");
two.add("姆巴佩");
two.add("C罗");
two.add("德马科斯");
two.add("德尚");
two.add("尼古拉斯凯奇");
two.add("布莱恩特");
// 3.第二个队伍只要姓德的成员姓名;存储到一个新集合中。
// 4.第二个队伍帅选之后不要前两个人;存储到一个新集合中。
Stream<String> stream2 = two.stream().filter(name->name.startsWith("德")).skip(2);
// 5.将两个队伍合并为一个队伍:存储到一个新集合中。
// 6.根据姓名创建Person对象;存储到一个新集合中。
// 7.打印整个队伍的Person对象信息。
Stream.concat(stream1,stream2).map(name->new Person(name)).forEach(person -> System.out.println(person));
二、方法引用
方法引用是一种特殊类型的 Lambda
表达式。它们通常用于通过引用现有方法来创建简单的 Lambda
表达式。
2.1 冗余的Lambda场景及使用方法引用改进
首先要定义一个函数式接口Printable
,然后定义方法并调用,参数该传递函数式接口。
// 定义一个方法,参数传递Printable接口,对字符串进行打印
public static void printString(Printable p) {
p.print("HelloWorld");
}
public static void main(String[] args) {
// 调用printString方法
printString((s)->{
System.out.println(s);
});
/*
分析:
Lambda表达式的目的,打印参数传递的字符串
把参数s传递给System.out对象,调用out对象的println方法对字符串进行了输出
注意:
1、System.out对象已经存在
2、println方法也是已经存在的
所以我们可以使用方法引用来优化Lambda表达式
*/
printString(System.out::println);
}
2.2 方法引用符
双冒号::
引用运算符,所在的表达式被称为方法引用
。如果Lambda
要表达的函数方法已经存在于某个方法的实现中,那么就可以通过方法引用来简化。
语义分析
- Lambda表达式写法:
s->System.out.println(s);
拿到参数s之后再经Lambda
之手传递给System.out.println
方法来处理。 - 方法引用写法:
System.out::println
直接让System.out
中的println
方法来取代Lambda
。
注意:Lambda
中传递的参数一定是方法引用中的那个方法可以接受的类型,否则会抛出异常
推导与省略
函数式接口是Lambda
的基础,而方法引用是Lambda
的进一步优化。使用这两种方式,都是可以根据“可推导就是可省略”的原则,无需指定参数类型,无需指定的重载形式,被自动推导出来。
2.3 通过对象名引用成员方法
除了函数式接口Printable
,创建一个类,类中定义了成员方法。通过对象名引用成员方法,使用前提是对象名是已经存在的,成员方法也是已经存在的。
public class MethodRerObject {
// 定义一个成员方法printUpperCaseString
public void printUpperCaseString(String str){
System.out.println(str.toUpperCase());
}
}
public class Demo01ObjectMethodReference {
// 定义一个方法,方法的参数传递Printable接口
public static void printString(Printable p) {
p.print("Hello");
}
public static void main(String[] args) {
// 调用printString方法
printString((s)->{
// 创建对象
MethodRerObject mro = new MethodRerObject();
// 调用成员方法打印输出
mro.printUpperCaseString(s);
});
/*
使用方法引用优化Lambda
对象时已经存在的MethodRerObject
成员方法也是已经存在的printUpperCaseString方法
所以可以方法引用
*/
// 先创建对象
MethodRerObject obj = new MethodRerObject();
printString(obj::printUpperCaseString);
}
}
2.4 通过类名引用静态方法
通过类名引用静态方法:类已经存在,静态成员方法也已经存在。
@FunctionalInterface
public interface Calcable {
int calcAbs(int number);
}
public class Demo01StaticMethodReference {
// 定义一个方法,方法的参数传递一个整数和一个函数式接口
public static int method(int number, Calcable c) {
return c.calcAbs(number);
}
public static void main(String[] args) {
// 调用method
int number = method(-10,(n) -> {
return Math.abs(n);
});
System.out.println(number);
/*
使用方法引用优化:Math类是存在的,静态方法abs方法也是存在的
*/
int number2 = method(-101,Math::abs);
System.out.println(number2);
}
}
2.5 通过super引用成员方法
通过super
引用成员方法:super
已经存在,父类的成员方法已经存在
public class Man extends Human {
// 子类重写父类的成员方法
@Override
public void sayHello() {
System.out.println("Hello 我是Man!");
}
// 定义一个方法参数传递函数式接口
public void method(Greetable g) {
g.greet();
}
public void show() {
调用method方法,方法的参数Greetable是一个函数式接口,可传递Lambda表达式
//method(()->{
// // 创建父类对象
// Human h = new Human();
// // 调用父类的sayHello方法
// h.sayHello();
//});
// 使用关键字super优化Lambda
method(()->{super.sayHello();});
// 使用方法引用:super已经存在,父类的成员方法sayHello已经存在
method(super::sayHello);
}
public static void main(String[] args) {
new Man().show();//Hello 我是Human!
}
}
2.6 通过this引用成员方法
this
使用方法引用来引用成员方法:this
是已经存在的,成员方法是已经存在的
@FunctionalInterface
public interface Richable {
void buy();
}
public class Husband {
// 定义一个买房子的方法
public void buyHouse() {
System.out.println("北京二环内买一套四合院!");
}
// 定义一个结婚的方法,参数传递Richable接口
public void marry(Richable r) {
r.buy();
}
// 定义一个非常高兴的方法
public void soHappy(){
// 调用结婚的方法
//marry(()->{
// // 使用this调用本类的成员方法
// this.buyHouse();
//});
// 使用方法引用优化:this是已经存在的,成员方法buyHouse是已经存在的
marry(this::buyHouse);
}
public static void main(String[] args) {
new Husband().soHappy();
}
}
2.7 类的构造器引用
构造方法使用方法引用:类已知,构造方法已知
// 自己事先写个标准的Person类和函数式接口PersonBuilder
public class Demo {
// 定义一个方法,传递String类型的姓名和函数式接口
public static void printName(String name, PersonBuilder pb) {
Person person = pb.buildPerson(name);
System.out.println(person.getName());
}
public static void main(String[] args) {
// 调用printName方法
printName("赵晓",(name)->{
return new Person(name);
});
/*
使用方法引用优化:Person已知,构造方法new Person(String name)已知
*/
printName("赵晓", Person::new);
}
}
2.8 数组的构造器引用
数组构造器使用方法引用:已知创建的是int[]
,数组的长度已知,创建方法new
已知
@FunctionalInterface
public interface ArrayBuilder {
int[] buildArray(int length);
}
public class Demo {
// 定义一个方法,参数传递一个整数和一个函数式接口,返回一个int类型数组
public static int[] createArray(int length, ArrayBuilder ab){
return ab.buildArray(length);
}
public static void main(String[] args) {
// 调用createArray方法
int[] array = createArray(10,(len)->{
return new int[len];
});
System.out.println(array.length);
// 使用方法引用优化:已知创建的是int[],数组的长度已知,创建方法new已知
int[] arr = createArray(20,int[]::new);
System.out.println(arr.length);
}
}