要讲 Stream,那就不得不先说一下它的左膀右臂 Lambda 和办法援用,你用的 Stream API 其实就是函数式的编程格调,其中的「函数」就是办法援用,「式」就是 Lambda 表达式。
Lambda 表达式
Lambda 表达式是一个匿名函数,Lambda 表达式基于数学中的 λ 演算得名,间接对应于其中的 lambda 形象,是一个匿名函数,即没有函数名的函数。Lambda 表达式能够示意闭包。
在 Java 中,Lambda 表达式的格局是像上面这样
// 无参数,无返回值
() -> log.info("Lambda")
// 有参数,有返回值
(int a, int b) -> {a+b}
复制代码
其等价于
log.info("Lambda");
private int plus(int a, int b){return a+b;}
复制代码
最常见的一个例子就是新建线程,有时候为了省事,会用上面的办法创立并启动一个线程,这是匿名外部类的写法,new Thread
须要一个 implements 自 Runnable
类型的对象实例作为参数,比拟好的形式是创立一个新类,这个类 implements Runnable
,而后 new 出这个新类的实例作为参数传给 Thread。而匿名外部类不必找对象接管,间接当做参数。
new Thread(new Runnable() {
@Override
public void run() {System.out.println("疾速新建并启动一个线程");
}
}).start();
复制代码
然而这样写是不是感觉看上去很乱、很土,而这时候,换上 Lambda 表达式就是另外一种感觉了。
new Thread(()->{System.out.println("疾速新建并启动一个线程");
}).start();
复制代码
怎么样,这样一改,霎时感觉清爽脱俗了不少,简洁优雅了不少。
Lambda 表达式简化了匿名外部类的模式,能够达到同样的成果,然而 Lambda 要优雅的多。尽管最终达到的目标是一样的,但其实外部的实现原理却不雷同。
匿名外部类在编译之后会创立一个新的匿名外部类进去,而 Lambda 是调用 JVM invokedynamic
指令实现的,并不会产生新类。
办法援用
办法援用的呈现,使得咱们能够将一个办法赋给一个变量或者作为参数传递给另外一个办法。::
双冒号作为办法援用的符号,比方上面这两行语句,援用 Integer
类的 parseInt
办法。
Function<String, Integer> s = Integer::parseInt;
Integer i = s.apply("10");
复制代码
或者上面这两行,援用 Integer
类的 compare
办法。
Comparator<Integer> comparator = Integer::compare;
int result = comparator.compare(100,10);
复制代码
再比方,上面这两行代码,同样是援用 Integer
类的 compare
办法,然而返回类型却不一样,但却都能失常执行,并正确返回。
IntBinaryOperator intBinaryOperator = Integer::compare;
int result = intBinaryOperator.applyAsInt(10,100);
复制代码
置信有的同学看到这里恐怕是上面这个状态,齐全不可理喻吗,也太轻易了吧,返回给谁都能接盘。
先别冲动,来来来,当初咱们就来解惑,解除蒙圈脸。
Q:什么样的办法能够被援用?
A:这么说吧,任何你有方法拜访到的办法都能够被援用。
Q:返回值到底是什么类型?
A:这就问到点儿上了,下面又是 Function
、又是 Comparator
、又是 IntBinaryOperator
的,看上去如同没有法则,其实不然。
返回的类型是 Java 8 专门定义的函数式接口,这类接口用 @FunctionalInterface
注解。
比方 Function
这个函数式接口的定义如下:
@FunctionalInterface
public interface Function<T, R> {R apply(T t);
}
复制代码
还有很要害的一点,你的援用办法的参数个数、类型,返回值类型要和函数式接口中的办法申明一一对应才行。
比方 Integer.parseInt
办法定义如下:
public static int parseInt(String s) throws NumberFormatException {return parseInt(s,10);
}
复制代码
首先 parseInt
办法的参数个数是 1 个,而 Function
中的 apply
办法参数个数也是 1 个,参数个数对应上了,再来,apply
办法的参数类型和返回类型是泛型类型,所以必定能和 parseInt
办法对应上。
这样一来,就能够正确的接管 Integer::parseInt
的办法援用,并能够调用 Funciton
的apply
办法,这时候,调用到的其实就是对应的 Integer.parseInt
办法了。
用这套规范套到 Integer::compare
办法上,就不难理解为什么即能够用 Comparator<Integer>
接管,又能够用 IntBinaryOperator
接管了,而且调用它们各自的办法都能正确的返回后果。
Integer.compare
办法定义如下:
public static int compare(int x, int y) {return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
复制代码
返回值类型 int
,两个参数,并且参数类型都是 int
。
而后来看 Comparator
和IntBinaryOperator
它们两个的函数式接口定义和其中对应的办法:
@FunctionalInterface
public interface Comparator<T> {int compare(T o1, T o2);
}
@FunctionalInterface
public interface IntBinaryOperator {int applyAsInt(int left, int right);
}
复制代码
对不对,都能正确的匹配上,所以后面示例中用这两个函数式接口都能失常接管。其实不止这两个,只有是在某个函数式接口中申明了这样的办法:两个参数,参数类型是 int
或者泛型,并且返回值是 int
或者泛型的,都能够完满接管。
JDK 中定义了很多函数式接口,次要在 java.util.function
包下,还有 java.util.Comparator
专门用作定制比拟器。另外,后面说的 Runnable
也是一个函数式接口。
本人入手实现一个例子
1. 定义一个函数式接口,并增加一个办法
定义了名称为 KiteFunction 的函数式接口,应用 @FunctionalInterface
注解,而后申明了具备两个参数的办法 run
,都是泛型类型,返回后果也是泛型。
还有一点很重要,函数式接口中只能申明一个可被实现的办法,你不能申明了一个 run
办法,又申明一个 start
办法,到时候编译器就不晓得用哪个接管了。而用default
关键字润饰的办法则没有影响。
@FunctionalInterface
public interface KiteFunction<T, R, S> {
/**
* 定义一个双参数的办法
* @param t
* @param s
* @return
*/
R run(T t,S s);
}
复制代码
2. 定义一个与 KiteFunction 中 run 办法对应的办法
在 FunctionTest 类中定义了办法 DateFormat
,一个将 LocalDateTime
类型格式化为字符串类型的办法。
public class FunctionTest {public static String DateFormat(LocalDateTime dateTime, String partten) {DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten);
return dateTime.format(dateTimeFormatter);
}
}
复制代码
3. 用办法援用的形式调用
失常状况下咱们间接应用 FunctionTest.DateFormat()
就能够了。
而用函数式形式,是这样的。
KiteFunction<LocalDateTime,String,String> functionDateFormat = FunctionTest::DateFormat;
String dateString = functionDateFormat.run(LocalDateTime.now(),"yyyy-MM-dd HH:mm:ss");
复制代码
而其实我能够不专门在里面定义 DateFormat
这个办法,而是像上面这样,应用匿名外部类。
public static void main(String[] args) throws Exception {String dateString = new KiteFunction<LocalDateTime, String, String>() {
@Override
public String run(LocalDateTime localDateTime, String s) {DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(s);
return localDateTime.format(dateTimeFormatter);
}
}.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");
System.out.println(dateString);
}
复制代码
后面第一个 Runnable
的例子也提到了,这样的匿名外部类能够用 Lambda 表达式的模式简写,简写后的代码如下:
public static void main(String[] args) throws Exception {KiteFunction<LocalDateTime, String, String> functionDateFormat = (LocalDateTime dateTime, String partten) -> {DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten);
return dateTime.format(dateTimeFormatter);
};
String dateString = functionDateFormat.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");
System.out.println(dateString);
}
复制代码
应用(LocalDateTime dateTime, String partten) -> {} 这样的 Lambda 表达式间接返回办法援用。
Stream API
为了说一下 Stream API 的应用,能够说是大费周章啊,知其然,也要知其所以然吗,谋求技术的态度和姿态要正确。
当然 Stream 也不只是 Lambda 表达式就厉害了,真正厉害的还是它的性能,Stream 是 Java 8 中汇合数据处理的利器,很多原本简单、须要写很多代码的办法,比方过滤、分组等操作,往往应用 Stream 就能够在一行代码搞定,当然也因为 Stream 都是链式操作,一行代码可能会调用好几个办法。
Collection
接口提供了 stream()
办法,让咱们能够在一个汇合不便的应用 Stream API 来进行各种操作。值得注意的是,咱们执行的任何操作都不会对源汇合造成影响,你能够同时在一个汇合上提取出多个 stream 进行操作。
咱们看 Stream 接口的定义,继承自 BaseStream
,机会所有的接口申明都是接管办法援用类型的参数,比方 filter
办法,接管了一个 Predicate
类型的参数,它就是一个函数式接口,罕用来作为条件比拟、筛选、过滤用,JPA
中也应用了这个函数式接口用来做查问条件拼接。
public interface Stream<T> extends BaseStream<T, Stream<T>> {Stream<T> filter(Predicate<? super T> predicate);
// 其余接口
}
复制代码
上面就来看看 Stream 罕用 API。
of
可接管一个泛型对象或可变成泛型汇合,结构一个 Stream 对象。
private static void createStream(){Stream<String> stringStream = Stream.of("a","b","c");
}
复制代码
empty
创立一个空的 Stream 对象。
concat
连贯两个 Stream,不扭转其中任何一个 Steam 对象,返回一个新的 Stream 对象。
private static void concatStream(){Stream<String> a = Stream.of("a","b","c");
Stream<String> b = Stream.of("d","e");
Stream<String> c = Stream.concat(a,b);
}
复制代码
max
个别用于求数字汇合中的最大值,或者按实体中数字类型的属性比拟,领有最大值的那个实体。它接管一个 Comparator<T>
,下面也举到这个例子了,它是一个函数式接口类型,专门用作定义两个对象之间的比拟,例如上面这个办法应用了 Integer::compareTo
这个办法援用。
private static void max(){Stream<Integer> integerStream = Stream.of(2, 2, 100, 5);
Integer max = integerStream.max(Integer::compareTo).get();
System.out.println(max);
}
复制代码
当然,咱们也能够本人定制一个 Comparator
,顺便温习一下 Lambda 表达式模式的办法援用。
private static void max(){Stream<Integer> integerStream = Stream.of(2, 2, 100, 5);
Comparator<Integer> comparator = (x, y) -> (x.intValue() < y.intValue()) ? -1 : ((x.equals(y)) ? 0 : 1);
Integer max = integerStream.max(comparator).get();
System.out.println(max);
}
复制代码
min
与 max 用法一样,只不过是求最小值。
findFirst
获取 Stream 中的第一个元素。
findAny
获取 Stream 中的某个元素,如果是串行状况下,个别都会返回第一个元素,并行状况下就不肯定了。
count
返回元素个数。
Stream<String> a = Stream.of("a", "b", "c");
long x = a.count();
复制代码
peek
建设一个通道,在这个通道中对 Stream 的每个元素执行对应的操作,对应 Consumer<T>
的函数式接口,这是一个消费者函数式接口,顾名思义,它是用来生产 Stream 元素的,比方上面这个办法,把每个元素转换成对应的大写字母并输入。
private static void peek() {Stream<String> a = Stream.of("a", "b", "c");
List<String> list = a.peek(e->System.out.println(e.toUpperCase())).collect(Collectors.toList());
}
复制代码
forEach
和 peek 办法相似,都接管一个消费者函数式接口,能够对每个元素进行对应的操作,然而和 peek 不同的是,forEach
执行之后,这个 Stream 就真的被生产掉了,之后这个 Stream 流就没有了,不能够再对它进行后续操作了,而 peek
操作完之后,还是一个可操作的 Stream 对象。
正好借着这个说一下,咱们在应用 Stream API 的时候,都是一串链式操作,这是因为很多办法,比方接下来要说到的 filter
办法等,返回值还是这个 Stream 类型的,也就是被以后办法解决过的 Stream 对象,所以 Stream API 依然能够应用。
private static void forEach() {Stream<String> a = Stream.of("a", "b", "c");
a.forEach(e->System.out.println(e.toUpperCase()));
}
复制代码
forEachOrdered
性能与 forEach
是一样的,不同的是,forEachOrdered
是有程序保障的,也就是对 Stream 中元素按插入时的程序进行生产。为什么这么说呢,当开启并行的时候,forEach
和 forEachOrdered
的成果就不一样了。
Stream<String> a = Stream.of("a", "b", "c");
a.parallel().forEach(e->System.out.println(e.toUpperCase()));
复制代码
当应用下面的代码时,输入的后果可能是 B、A、C 或者 A、C、B 或者 A、B、C,而应用上面的代码,则每次都是 A、B、C
Stream<String> a = Stream.of("a", "b", "c");
a.parallel().forEachOrdered(e->System.out.println(e.toUpperCase()));
复制代码
limit
获取前 n 条数据,相似于 MySQL 的 limit,只不过只能接管一个参数,就是数据条数。
private static void limit() {Stream<String> a = Stream.of("a", "b", "c");
a.limit(2).forEach(e->System.out.println(e));
}
复制代码
上述代码打印的后果是 a、b。
skip
跳过前 n 条数据,例如上面代码,返回后果是 c。
private static void skip() {Stream<String> a = Stream.of("a", "b", "c");
a.skip(2).forEach(e->System.out.println(e));
}
复制代码
distinct
元素去重,例如上面办法返回元素是 a、b、c,将反复的 b 只保留了一个。
private static void distinct() {Stream<String> a = Stream.of("a", "b", "c","b");
a.distinct().forEach(e->System.out.println(e));
}
复制代码
sorted
有两个重载,一个无参数,另外一个有个 Comparator
类型的参数。
无参类型的依照天然程序进行排序,只适宜比拟单纯的元素,比方数字、字母等。
private static void sorted() {Stream<String> a = Stream.of("a", "c", "b");
a.sorted().forEach(e->System.out.println(e));
}
复制代码
有参数的须要自定义排序规定,例如上面这个办法,依照第二个字母的大小程序排序,最初输入的后果是 a1、b3、c6。
private static void sortedWithComparator() {Stream<String> a = Stream.of("a1", "c6", "b3");
a.sorted((x,y)->Integer.parseInt(x.substring(1))>Integer.parseInt(y.substring(1))?1:-1).forEach(e->System.out.println(e));
}
复制代码
为了更好的阐明接下来的几个 API,我模仿了几条我的项目中常常用到的相似数据,10 条用户信息。
private static List<User> getUserData() {Random random = new Random();
List<User> users = new ArrayList<>();
for (int i = 1; i <= 10; i++) {User user = new User();
user.setUserId(i);
user.setUserName(String.format("古时的风筝 %s 号", i));
user.setAge(random.nextInt(100));
user.setGender(i % 2);
user.setPhone("18812021111");
user.setAddress("无");
users.add(user);
}
return users;
}
复制代码
filter
用于条件筛选过滤,筛选出符合条件的数据。例如上面这个办法,筛选出性别为 0,年龄大于 50 的记录。
private static void filter(){List<User> users = getUserData();
Stream<User> stream = users.stream();
stream.filter(user -> user.getGender().equals(0) && user.getAge()>50).forEach(e->System.out.println(e));
/**
* 等同于上面这种模式 匿名外部类
*/
// stream.filter(new Predicate<User>() {
// @Override
// public boolean test(User user) {// return user.getGender().equals(0) && user.getAge()>50;
// }
// }).forEach(e->System.out.println(e));
}
复制代码
map
map
办法的接口办法申明如下,承受一个 Function
函数式接口,把它翻译成映射最合适了,通过原始数据元素,映射出新的类型。
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
复制代码
而 Function
的申明是这样的,察看 apply
办法,承受一个 T 型参数,返回一个 R 型参数。用于将一个类型转换成另外一个类型正合适,这也是 map
的初衷所在,用于扭转以后元素的类型,例如将 Integer
转为 String
类型,将 DAO 实体类型,转换为 DTO 实例类型。
当然了,T 和 R 的类型也能够一样,这样的话,就和 peek
办法没什么不同了。
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
}
复制代码
例如上面这个办法,应该是业务零碎的罕用需要,将 User 转换为 API 输入的数据格式。
private static void map(){List<User> users = getUserData();
Stream<User> stream = users.stream();
List<UserDto> userDtos = stream.map(user -> dao2Dto(user)).collect(Collectors.toList());
}
private static UserDto dao2Dto(User user){UserDto dto = new UserDto();
BeanUtils.copyProperties(user, dto);
// 其余额定解决
return dto;
}
复制代码
mapToInt
将元素转换成 int 类型,在 map
办法的根底上进行封装。
mapToLong
将元素转换成 Long 类型,在 map
办法的根底上进行封装。
mapToDouble
将元素转换成 Double 类型,在 map
办法的根底上进行封装。
flatMap
这是用在一些比拟特地的场景下,当你的 Stream 是以下这几种构造的时候,须要用到 flatMap
办法,用于将原有二维构造扁平化。
Stream<String[]>
Stream<Set<String>>
Stream<List<String>>
以上这三类构造,通过 flatMap
办法,能够将后果转化为 Stream<String>
这种模式,不便之后的其余操作。
比方上面这个办法,将 List<List<User>>
扁平解决,而后再应用 map
或其余办法进行操作。
private static void flatMap(){List<User> users = getUserData();
List<User> users1 = getUserData();
List<List<User>> userList = new ArrayList<>();
userList.add(users);
userList.add(users1);
Stream<List<User>> stream = userList.stream();
List<UserDto> userDtos = stream.flatMap(subUserList->subUserList.stream()).map(user -> dao2Dto(user)).collect(Collectors.toList());
}
复制代码
flatMapToInt
用法参考 flatMap
,将元素扁平为 int 类型,在 flatMap
办法的根底上进行封装。
flatMapToLong
用法参考 flatMap
,将元素扁平为 Long 类型,在 flatMap
办法的根底上进行封装。
flatMapToDouble
用法参考 flatMap
,将元素扁平为 Double 类型,在 flatMap
办法的根底上进行封装。
collection
在进行了一系列操作之后,咱们最终的后果大多数时候并不是为了获取 Stream 类型的数据,而是要把后果变为 List、Map 这样的罕用数据结构,而 collection
就是为了实现这个目标。
就拿 map 办法的那个例子阐明,将对象类型进行转换后,最终咱们须要的后果集是一个 List<UserDto >
类型的,应用 collect
办法将 Stream 转换为咱们须要的类型。
上面是 collect
接口办法的定义:
<R, A> R collect(Collector<? super T, A, R> collector);
复制代码
上面这个例子演示了将一个简略的 Integer Stream 过滤出大于 7 的值,而后转换成 List<Integer>
汇合,用的是 Collectors.toList()
这个收集器。
private static void collect(){Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33);
List<Integer> list = integerStream.filter(s -> s.intValue()>7).collect(Collectors.toList());
}
复制代码
很多同学示意看不太懂这个 Collector
是怎么一个意思,来,咱们看上面这段代码,这是 collect
的另一个重载办法,你能够了解为它的参数是按程序执行的,这样就分明了,这就是个 ArrayList 从创立到调用 addAll
办法的一个过程。
private static void collect(){Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33);
List<Integer> list = integerStream.filter(s -> s.intValue()>7).collect(ArrayList::new, ArrayList::add,
ArrayList::addAll);
}
复制代码
咱们在自定义 Collector
的时候其实也是这个逻辑,不过咱们基本不必自定义,Collectors
曾经为咱们提供了很多拿来即用的收集器。比方咱们常常用到 Collectors.toList()
、Collectors.toSet()
、Collectors.toMap()
。另外还有比方Collectors.groupingBy()
用来分组,比方上面这个例子,依照 userId 字段分组,返回以 userId 为 key,List 为 value 的 Map,或者返回每个 key 的个数。
// 返回 userId:List<User>
Map<String,List<User>> map = user.stream().collect(Collectors.groupingBy(User::getUserId));
// 返回 userId: 每组个数
Map<String,Long> map = user.stream().collect(Collectors.groupingBy(User::getUserId,Collectors.counting()));
复制代码
toArray
collection
是返回列表、map 等,toArray
是返回数组,有两个重载,一个空参数,返回的是 Object[]
。
另一个接管一个 IntFunction<R>
类型参数。
@FunctionalInterface
public interface IntFunction<R> {
/**
* Applies this function to the given argument.
*
* @param value the function argument
* @return the function result
*/
R apply(int value);
}
复制代码
比方像上面这样应用,参数是 User[]::new
也就是 new 一个 User 数组,长度为最初的 Stream 长度。
private static void toArray() {List<User> users = getUserData();
Stream<User> stream = users.stream();
User[] userArray = stream.filter(user -> user.getGender().equals(0) && user.getAge() > 50).toArray(User[]::new);
}
复制代码
reduce
它的作用是每次计算的时候都用到上一次的计算结果,比方求和操作,前两个数的和加上第三个数的和,再加上第四个数,始终加到最初一个数地位,最初返回后果,就是 reduce
的工作过程。
private static void reduce(){Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33);
Integer sum = integerStream.reduce(0,(x,y)->x+y);
System.out.println(sum);
}
复制代码
另外 Collectors
好多办法都用到了 reduce
,比方 groupingBy
、minBy
、maxBy
等等。
并行 Stream
Stream 实质上来说就是用来做数据处理的,为了放慢处理速度,Stream API 提供了并行处理 Stream 的形式。通过 users.parallelStream()
或者users.stream().parallel()
的形式来创立并行 Stream 对象,反对的 API 和一般 Stream 简直是统一的。
并行 Stream 默认应用 ForkJoinPool
线程池,当然也反对自定义,不过个别状况下没有必要。ForkJoin 框架的分治策略与并行流解决正好符合。
尽管并行这个词听下来很厉害,但并不是所有状况应用并行流都是正确的,很多时候齐全没这个必要。
什么状况下应用或不应应用并行流操作呢?
- 必须在多核 CPU 下才应用并行 Stream,听下来如同是废话。
- 在数据量不大的状况下应用一般串行 Stream 就能够了,应用并行 Stream 对性能影响不大。
- CPU 密集型计算适宜应用并行 Stream,而 IO 密集型应用并行 Stream 反而会更慢。
- 尽管计算是并行的可能很快,但最初大多数时候还是要应用
collect
合并的,如果合并代价很大,也不适宜用并行 Stream。 - 有些操作,比方 limit、findFirst、forEachOrdered 等依赖于元素程序的操作,都不适宜用并行 Stream。
作者:古时的风筝
链接:https://juejin.cn/post/684490…
起源:稀土掘金
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。
微信公众号【程序员黄小斜】作者是前蚂蚁金服 Java 工程师,专一分享 Java 技术干货和求职成长心得,不限于 BAT 面试,算法、计算机根底、数据库、分布式、spring 全家桶、微服务、高并发、JVM、Docker 容器,ELK、大数据等。关注后回复【book】支付精选 20 本 Java 面试必备精品电子书。