当前位置 : 首页 » 文章分类 :  开发  »  Java-Stream流和Lambda表达式

Java-Stream流和Lambda表达式

Java 8 流和 lambda 表达式笔记

Java 8 中的 Streams API 详解
https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/index.html


Lambda表达式

Java 中能够使用 Lambda 表达式的依据是必须有相应的函数接口(函数接口,是指内部只有一个抽象方法的接口)

Java8中lambda表达式无法抛出受检异常

Java中的异常分为两类,受检查异常(Checked Exception)跟非受检异常(UnChecked Exception)。

受检异常表示代码中必须显示的处理该异常,比如try-catch或者在方法声明上加入throws关键字把异常向上抛出。
如果方法中抛出受检异常,则方法上必须加 thorows 异常类型 声明;如果方法中抛出非受检异常,则不必加throows声明。
非受检异常也被称作运行时异常,就像我们常用的RuntimeException,表示代码中可以不进行处理,直白点说就是Java认为这是人为导致的问题,也是可以人为的在编写代码阶段就避免掉的问题。

由Error跟RuntimeException派生出来的类都是非受检异常,也就是运行时异常。其他的异常则问受检异常,主要就是IOException及其子类。

// 抛出非受检异常,编译通过
Stream.of("a", "b", "c").forEach(str -> {
    throw new RuntimeException();
});

// 抛出受检异常,编译失败
Stream.of("a", "b", "c").forEach(str -> {
    throw new IOException();
});

第一个之所以编译通过就是因为它抛出的是非受检异常,只有运行中才会处理,而编译器在编译阶段并不关心,因此没有问题。
第二段代码抛出的是IOException,它是一个受检异常,也就是说编译器强制要求开发人员在编译阶段就要显示的处理该异常,不能留给运行阶段,因此编译不通过。

再比如下面的代码

package concurrent_test;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class SimpleDateFormatConcurrentTest {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static String format(Date date) {
        return sdf.format(date);
    }

    public static Date parse(String dateStr) throws ParseException {
        return sdf.parse(dateStr);
    }

    public static void main(String[] args) throws InterruptedException, ParseException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i=0; i < 50; i++) {
            executorService.execute(() -> {
              // 无法通过编译,提示 Unhandled Exception: java.text.ParseException
                System.out.println(parse(format(new Date())));
            });
        }
        executorService.shutdown();
        executorService.awaitTermination(10, TimeUnit.SECONDS);
    }
}

parse 方法上会提示 Unhandled Exception: java.text.ParseException ,因为 parse 方法抛出的 ParseException 是一个受检异常,必须捕获或者继续上抛。在main方法上抛出此异常并不能解决问题,因为 execute() 中的lambda 表达式其实是实现了 Runnable 接口,表达式内容就是 Runnable 接口的 run() 方法的内容,而run方法是没有抛出异常的。
要解决这个问题只能try catch住这个异常处理掉。

Java8中lambda表达式无法抛出受检异常的问题
https://www.jianshu.com/p/a3855f880ff2


lambda表达式(内部类)中只能访问final局部变量

把可能会捕获外部变量的 Lambda 表达式称为闭包,那么 Java 8 的 Lambda 可以捕获什么变量呢?

  • 捕获实例或静态变量是没有限制的(可认为是通过 final 类型的局部变量 this 来引用前两者) private final Class this$0;
  • 捕获的局部变量必须显式的声明为 final 或实际效果的的 final 类型

回顾一下我们在 Java 8 之前,匿名类中如果要访问局部变量的话,那个局部变量必须显式的声明为 final
在 Java 8 下,即使局部变量未声明为 final 类型,一旦在匿名类中访问了一下就被强型加上了 final 属性,所以后面就无法再次给 version 赋值了。
因此,Java 8 的 Lambda 表达式访问局部变量时虽然没有硬性规定要被声明为 final,但实质上是和 Java 7 一样的。
总之一个局部变量如果要在 Java 7/8 的匿名类或是 Java 8 的 Lambda 表达式中访问,那么这个局部变量必须是 final 的,即使没有 final 饰它也是 final 类型。

注意,并不是 Lambda 开始访问时那个局部变量才变为 final:

String version = "1.8";
version = "1.7"; //编译无法通过,因为version是final变量
foo(() -> System.out.println(version) ); //这行让编译器决定给 version 加上 final 属性

提示: variable used in lambda expression should be final or effectively final
换句话说,如果在匿名类或 Lambda 表达式中访问的局部变量,如果不是 final 类型的话,编译器自动加上 final 修饰符。

为什么 Lambda 表达式(匿名类) 不能访问非 final 的局部变量呢?
这和 “内部类(或匿名类)只能访问外部的final变量” 是同一个道理
因为实例变量存在堆中,而局部变量是在栈上分配,Lambda 表达(匿名类) 会在另一个线程中执行。如果在线程中要直接访问一个局部变量,可能线程执行时该局部变量已经被销毁了,而 final 类型的局部变量在 Lambda 表达式(匿名类) 中其实是局部变量的一个拷贝。

Java 8 Lambda 捕获外部变量
https://unmi.cc/java-8-lambda-capture-outer-variables/


函数式接口

函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

Marker(标记)类型的接口是一种没有方法或属性声明的接口,简单地说,marker 接口是空接口。相似地,函数式接口是只包含一个抽象方法声明的接口。

java.lang.Runnable 就是一种函数式接口,在 Runnable 接口中只声明了一个方法 void run(),相似地,ActionListener 接口也是一种函数式接口,我们使用匿名内部类来实例化函数式接口的对象,有了 Lambda 表达式,这一方式可以得到简化。

每个 Lambda 表达式都能隐式地赋值给函数式接口,例如,我们可以通过 Lambda 表达式创建 Runnable 接口的引用。

Runnable r = () -> System.out.println("hello world");

当不指明函数式接口时,编译器会自动解释这种转化:

new Thread(
   () -> System.out.println("hello world")
).start();

因此,在上面的代码中,编译器会自动推断:根据线程类的构造函数签名 public Thread(Runnable r) { },将该 Lambda 表达式赋给 Runnable 接口。

深入浅出 Java 8 Lambda 表达式
http://blog.oneapm.com/apm-tech/226.html

接口的静态方法和默认方法

Java 8 允许在接口中加入具体方法。接口中的具体方法有两种,default方法和static方法,identity()就是Function接口的一个静态方法。
Function.identity()返回一个输出跟输入一样的Lambda表达式对象,等价于形如t -> t形式的Lambda表达式。

方法引用

诸如String::length的语法形式叫做方法引用(method references),这种语法用来替代某些特定形式Lambda表达式。如果Lambda表达式的全部内容就是调用一个已有的方法,那么可以用方法引用来替代Lambda表达式。方法引用可以细分为四类:

方法引用类别 举例
引用静态方法 Integer::sum
引用某个对象的方法 list::add
引用某个类的方法 String::length
引用构造方法 HashMap::new

Java Stream API进阶篇
http://www.cnblogs.com/CarpenterLee/p/6550212.html

No compile-time declaration for the method reference is found

有如下代码

Long userId = null;
Map dataMap = new HashMap();
dataMap.put("user_id", Optional.ofNullable(userId).map(Long::toString).orElse("1100"));

Idea中会提示:No compile-time declaration for the method reference is found
无法通过编译,编译器提示:

Error:(445, 63) java: 不兼容的类型: 无法推断类型变量 U
(参数不匹配; 方法引用无效
对toString的引用不明确
java.lang.Long 中的方法 toString(long) 和 java.lang.Long 中的方法 toString() 都匹配)

原因提示的很清楚,方法引用 Long::toString 不明确,Long 类中的两个同名方法都匹配。
可以改用String类的valueOf方法,这样写:

dataMap.put("user_id", Optional.ofNullable(userId).map(String::valueOf).orElse("1100"));

A tale of method reference ambiguity
https://engineering.shoutapp.io/a-tale-of-method-reference-ambiguity-e51d3c5d9b00


常用函数式接口

java.util.function 包中定义了几十个常用的函数接口

Function<T,R>

接受一个输入参数,返回一个结果。

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

Predicate<T>

接受一个输入参数,返回一个布尔值结果。

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

BiFunction<T, U, R>

根据 Data1VO 和 Data2DO 生成 Data3DO

public static BiFunction<Data1VO, Data2DO, Data3DO> convertToData3DO =
        (Data1VO, Data2DO) -> {
            Data3DO data3VO = new Data3DO();
            // data1VO 中的数据
            data3VO.setPersonNumber(data1VO.getPersonId());
            data3VO.setRealName(data1VO.getName());
            // data2VO 中的数据
            data3VO.setArchiveId(data2VO.getId());
            data3VO.setArchiveCover(data2VO.getImageId());
            data3VO.setUpdateTime(new Date());
            return data3VO;
        };

Stream 接口

public interface Stream<T>
extends BaseStream<T,Stream<T>> {

}

接口 Stream 位于 java.base 模块中,java.util.stream 包中,继承自 BaseStream 接口。
BaseStream 接口还有三个子接口 IntStream, LongStream, DoubleStream,对应三种基本类型int, long, double,Stream 对应所有剩余类型的stream视图

Java Stream API入门篇
http://www.cnblogs.com/CarpenterLee/p/6545321.html

Java Stream API进阶篇
http://www.cnblogs.com/CarpenterLee/p/6550212.html

Java 8系列之Stream的基本语法详解
https://blog.csdn.net/io_field/article/details/54971761

JDK8 Stream 从入门到装逼(三)
https://my.oschina.net/kaishui/blog/743383


Stream反转列表

注意,反转不是排序,所以使用 sort 方法是不正确的,因为它排序,而不是反转

首先要搞清楚, Stream 是不存储任何元素的,Stream 只是对集合进行一系列操作。
因此,如果不将元素存储在某个中间集合中,则无法以相反的顺序迭代元素。
那么,使用 Stream 实现反转列表的一个思路就是把元素收集到一个支持 反向输出的集合类中,例如 双端队列 Deque<>

@Test
public void testReverseList() {
    Stream.of("1", "2", "20", "3", "masikkk")
            .collect(Collectors.toCollection(ArrayDeque::new)) // or LinkedList
            .descendingIterator()
            .forEachRemaining(System.out::println);
}

结果

masikkk
3
20
2
1

Java 8 stream reverse order
https://stackoverflow.com/questions/24010109/java-8-stream-reverse-order


generate() 生成单元素无限序列

public static<T> Stream<T> generate(Supplier<T> s)

@Test
public void testGenerate() {
    Stream.generate(() -> 1).limit(10).forEach(e -> System.out.print(e + ", "));
    System.out.println();
    Stream.generate(() -> "测").limit(10).forEach(e -> System.out.print(e + ", "));
}

结果
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
测, 测, 测, 测, 测, 测, 测, 测, 测, 测,


iterate() 生成无限序列

public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)

@Test
public void testIterate() {
    Stream.iterate(1, n -> n + 1).limit(10).forEach(e -> System.out.print(e + ", "));
    System.out.println();
    Stream.iterate(1, n -> n * 2).limit(10).forEach(e -> System.out.print(e + ", "));
}

结果
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
1, 2, 4, 8, 16, 32, 64, 128, 256, 512,


parallelStream()

parallelStream 其实就是一个并行执行的流,它通过默认的 ForkJoinPool, 可能提高你的多线程任务的速度。

// 根据条件对List进行过滤,保留name等于nickname的
userBeanList = userBeanList.parallelStream().filter(bean -> {
    String name = myservice.queryName(bean.getId());
    if (StringUtils.isNotBlank(name) && name.equals(bean.getNickName())) {
        return true;
    }
    return false;
}).collect(Collectors.toList());

// 填充Bean的额外信息,保持List有序
userBeanList.parallelStream().forEachOrdered(bean -> completeBean(bean));

深入浅出parallelStream
https://blog.csdn.net/u011001723/article/details/52794455

修改并行流的线程数

Java 8 引入了并行流(Parallel Streams),流会自动通过一个通用的 Fork/Join 池并行地执行,这个通用 Fork/Join 池可以使用 ForkJoinPool.commonPool() 来访问。

ForkJoinPool 中的 common 线程池的大小可以通过 system 参数 java.util.concurrent.ForkJoinPool.common.parallelism 配置,如果不配置默认是 Runtime.getRuntime().availableProcessors() - 1 即机器cpu核数-1。

我们使用默认的并行流时,并发线程数会受到 cpu 核数的限制,如果想提高并发度,可以:
一、配置 system 参数 java.util.concurrent.ForkJoinPool.common.parallelism, 修改并发流默认使用的 ForkJoinPool 线程池核心线程个数。

二、自定义并发流使用的线程池来替换默认的 ForkJoinPool
自定义指定线程数的 ForkJoinPool, 并通过 submit(Runnable task) 方法提交执行。

@Test
public void testParallelStream() {
    // 默认并行流线程池 ForkJoinPool 线程数是cpu核数-1
    IntStream.range(1, 30).parallel().forEach(i -> System.out.println(Thread.currentThread().getName()));

    // 自定义10个线程的线程池
    ForkJoinPool forkJoinPool = new ForkJoinPool(10);
    forkJoinPool.submit(() -> IntStream.range(1, 30).parallel().forEach(
            i -> System.out.println(Thread.currentThread().getName())));
}

并行流可正常distinct()操作

Will parallel stream work fine with distinct operation?
https://stackoverflow.com/questions/53645037/will-parallel-stream-work-fine-with-distinct-operation

并行流与 distinct() 可正常搭配,但需要等全部并行流执行完。

IntStream.of(1, 1, 2, 2, 3, 3, 3, 4, 4)
        .parallel()
        .distinct()
        .forEach(System.out::println);

结果是 1,2,3,4


anyMatch()

boolean anyMatch(Predicate<? super T> predicate);
任意元素匹配 predicate 条件,返回true

allMatch()

boolean allMatch(Predicate<? super T> predicate);
所有元素匹配 predicate 条件,返回true


peek()

Stream<T> peek(Consumer<? super T> action)
peek 接收一个没有返回值的 λ 表达式,可以做一些输出、元素处理等,经过 peek 后 stream 中的元素类型不变。
map 接收一个有返回值的 λ 表达式,之后 Stream 的泛型类型将转换为 map 参数 λ 表达式返回的类型,经过 map 后 stream 中的元素类型改表了。

Stream.of("one", "two", "three", "four")
    .filter(e -> e.length() > 3)
    .peek(e -> System.out.println("Filtered value: " + e))
    .map(String::toUpperCase)
    .peek(e -> System.out.println("Mapped value: " + e))
    .collect(Collectors.toList());

findFirst()

获取第一个学生的名字

String name  = users.stream().findFirst().map(User::getName).orElse(null);

findAny()

获取任意学生的名字

String name  = users.stream().findAny().map(User::getName).orElse(null);

map()

<R> Stream<R> map(Function<? super T,? extends R> mapper)
map方法将对于Stream中包含的元素使用给定的转换函数进行转换操作,新生成的Stream只包含转换生成的元素。

方法原型为:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
作用是返回一个对当前所有元素执行执行mapper之后的结果组成的Stream。直观的说,就是对每个元素按照某种操作进行转换,转换前后Stream中元素的个数不会改变,但元素的类型取决于转换之后的类型。

示例:

Stream.of("a", "b", "hello")
        .map(item -> item.toUpperCase())
        .forEach(System.out::println);

传给map中Lambda表达式,接受了String类型的参数,返回值也是String类型,在转换行数中,将字母全部改为大写。

从Order集合中取出所有订单的id组成集合:

List<Order> orderList = orderMapper.selectByExample(example);
List<String> orderNos = orderList.stream().map(o -> o.getOrderNo()).collect(Collectors.toList());

map也可以多次使用,例如先把字符串转换为枚举,非空过滤,再获取枚举对应的数字值

List<String> enumStrList = Lists.newArrayList("red", "green", "blue");
List<Byte> enumByteList = enumStrList.stream().map(ColorEnum::valueOf).filter(Objects::nonNull).map(ColorEnum::getCode).collect(Collectors.toList());

map传入的Lambda表达式必须是Function实例,参数可以为任意类型,而其返回值也是任性类型,javac会根据实际情景自行推断。

为了提高处理效率,官方已封装好了,三种变形:mapToDouble,mapToInt,mapToLong。

IntStream mapToInt(ToIntFunction<? super T> mapper);
LongStream mapToLong(ToLongFunction<? super T> mapper);
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);

其实很好理解,如果想将原Stream中的数据类型,转换为double,int或者是long是可以调用相对应的方法。


flatMap()

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
作用是对每个元素执行 mapper 指定的操作,并用所有 mapper 返回的 Stream 中的元素组成一个新的 Stream 作为最终返回结果。说起来太拗口,通俗的讲 flatMap() 的作用就相当于把原 stream 中的所有元素都”摊平”之后组成的Stream,转换前后元素的个数和类型都可能会改变。

例如:

Stream<List<Integer>> stream = Stream.of(Arrays.asList(1,2), Arrays.asList(3, 4, 5));
stream.flatMap(list -> list.stream()).forEach(i -> System.out.println(i));

上述代码中,原来的 stream 中有两个元素,分别是两个 List<Integer>, 执行 flatMap() 之后,将每个 List 都“摊平”成了一个个的数字,所以会新产生一个由5个数字组成的 Stream, 所以最终将输出1~5这5个数字。

例如,把一个超大的 userIds List 分割 为长度 100 的一组 list, 对每组调用一个批量查询服务,最后 flatmap 成一个List

List<List<Long>> partition = Lists.partition(userIds, 100);
List<UserBean> collect = partition.parallelStream().
  flatMap(list -> someService.getBeansBatch(list).stream()).
  collect(Collectors.toList());

例如,把一个user ids列表为分100个一组,分别对100个调用服务

Lists.partition(userIds, 100).forEach(userIdList -> getSomeInfos(userIdList, resultMap));

java中也预定义了3个到int,long,double的flatMap

IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper);
LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper);
DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper);

filter()

函数原型为:
Stream<T> filter(Predicate<? super T> predicate)
作用是返回一个只包含满足predicate条件元素的Stream

// 保留长度等于3的字符串
Stream<String> stream= Stream.of("I", "love", "you", "too");
stream.filter(str -> str.length()==3)
    .forEach(str -> System.out.println(str));

上述代码将输出为长度等于3的字符串you和too。

collect后才起作用

只filter不collect是不起作用的

userInviteeBeanList.stream().filter(invitee -> {
    InviterRelationBean inviter = inviterFinder.findInviter(invitee.getUserId());
    if (inviter != null && inviter.getUserId() != null && inviter.getUserId().equals(userId)) {
        return true;
    }
    return false;
});

正确的写法如下,可以直接collect到原集合中

userInviteeBeanList = userInviteeBeanList.stream().filter(invitee -> {
    InviterRelationBean inviter = inviterFinder.findInviter(invitee.getUserId());
    if (inviter != null && inviter.getUserId() != null && inviter.getUserId().equals(userId)) {
        return true;
    }
    return false;
}).collect(Collectors.toList());

Objects::nonNull 过滤null元素

list.stream.filter(x -> x!=null).collect(Collectors.toList());
list.stream.filter(Objects::nonNull).collect(Collectors.toList());


distinct()

Stream<T> distinct()
作用是返回一个去除重复元素之后的Stream

Stream<String> stream= Stream.of("I", "love", "you", "too", "too");
stream.distinct().forEach(str -> System.out.println(str));

上述代码会输出去掉一个too之后的其余字符串

可以通过 distinct().count() 判断元素是否全都相同

System.out.println("\n" + Stream.of("1", "2", "masikkk", "test", "1", "masikkk").distinct().count());

sorted()

sorted() 默认使用自然序(字典序)排序, 其中的元素必须实现 Comparable 接口

默认自然排序(字典序)

下面代码以自然序(字典序)排序一个list
list.stream().sorted()

Comparator.reverseOrder() 自然序逆序

下面代码以自然序(字典序)的逆序排序一个list
list.stream().sorted(Comparator.reverseOrder())

Comparator.comparing() 单字段正序排序

按照创建时间升序排序,默认就是升序,即正序

List<User> userList = getUserList();
List<UserBean> userBeanList = userList.stream()
        .sorted(Comparator.comparing(UserBean::getCreateTime))
        .collect(Collectors.toList());

Comparator.comparing() 单字段倒叙排序

按创建时间倒叙排序:

sorted(Comparator.comparing(UserBean::getCreateTime).reversed())

或者

sorted(Comparator.comparing(UserBean::getCreateTime, Comparator.reverseOrder()))

Comparator.comparing().thenComparing() 多字段排序

从JDK 8开始,我们就可以把多个Comparator链在一起(chain together)去建造更复杂的比较逻辑
按照创建时间倒叙排序,创建时间相同的按照id倒叙排序,下面这样写是错误的

List<User> userList = getUserList();
List<UserBean> userBeanList = userList.stream()
        .sorted(Comparator.comparing(UserBean::getCreateTime, Comparator.reverseOrder())
                .thenComparing(UserBean::getId, Comparator.reverseOrder()))
        .collect(Collectors.toList());

注意,下面的写法是错误的:

sorted(Comparator.comparing(UserBean::getCreateTime).reversed().thenComparing(UserBean::getId()).reversed())

如果非要用 reversed() 写的话,正确的应该写法是:

sorted(Comparator.comparing(UserBean::getCreateTime).thenComparing(UserBean::getId()).reversed())

因为每次 reversed() 都是对之前所有排序字段的逆序
个人更推荐在每个字段上指定排序方式的写法,更加清晰。

多字段排序一个升序一个降序

按seq1升序排序,按seq2降序排序

sorted(Comparator.comparing(AClass::getSeq1).thenComparing(AClass::getSeq2, Comparator.reverseOrder()))

Java stream sort 2 variables ascending/desending
https://stackoverflow.com/questions/30382453/java-stream-sort-2-variables-ascending-desending

JAVA8 Stream之Sort排序comparing 和thenComparing
https://blog.csdn.net/YoungLee16/article/details/83547713


forEach()

方法签名:
void forEach(Consumer<? super E> action)
作用是对容器中的每个元素执行action指定的动作,也就是对元素进行遍历。

// 使用Stream.forEach()迭代
Stream<String> stream = Stream.of("I", "love", "you", "too");
stream.forEach(str -> System.out.println(str));

由于forEach()是结束方法,上述代码会立即执行,输出所有字符串。

Java8 foreach()中return相当于continue

使用foreach()处理集合时不能使用break和continue这两个方法,也就是说不能按照普通的for循环遍历集合时那样根据条件来中止遍历。
在foreach()的lambda表达式中使用return时相当于for循环中的continue,即跳过当前循环,并不会使整个方法返回。

为什么呢?
因为lambda表达式本来就相当于一个方法,return只是从当前lambda表达式表示的方法中退出。在我们看来就像是跳过了此次循环。

List<String> list = Arrays.asList("123", "45634", "7892", "abch", "sdfhrthj", "mvkd");
list.stream().forEach(e ->{
    if(e.length() >= 5){
        return;
    }
    System.out.println(e);
});

输出结果为:
123
7892
abch
mvkd

在Java8的foreach()中使用return/break/continue
https://blog.csdn.net/lmy86263/article/details/51057733

Move to next item using Java 8 foreach loop in stream
https://stackoverflow.com/questions/32654929/move-to-next-item-using-java-8-foreach-loop-in-stream

foreach遍历Map

//============Java8之前的方式==========
Map<String, Integer> items = new HashMap<>();
items.put("A", 10);
items.put("B", 20);
items.put("C", 30);
items.put("D", 40);
items.put("E", 50);
items.put("F", 60);
for (Map.Entry<String, Integer> entry : items.entrySet()) {
    System.out.println("Item : " + entry.getKey() + " Count : " + entry.getValue());
}
//============forEach + Lambda表达式==========
Map<String, Integer> items = new HashMap<>();
items.put("A", 10);
items.put("B", 20);
items.put("C", 30);
items.put("D", 40);
items.put("E", 50);
items.put("F", 60);
items.forEach((k,v)->System.out.println("Item : " + k + " Count : " + v));
items.forEach((k,v)->{
    System.out.println("Item : " + k + " Count : " + v);
    if("E".equals(k)){
        System.out.println("Hello E");
    }
});

Java8 Map转List

public static void main(String[] args) {
    Map<String,String> map=new HashMap<>();
    map.put("1","AAAA");
    map.put("2","BBBB");
    map.put("3","CCCC");
    map.put("4","DDDD");
    map.put("5","EEEE");
    List<Object> list= map.entrySet().stream().map(et ->et.getKey()+"_"+et.getValue()).collect(Collectors.toList());
    list.forEach(obj-> System.out.println(obj));
}

foreach遍历List

List<String> items = new ArrayList<>();
items.add("A");
items.add("B");
items.add("C");
items.add("D");
items.add("E");

for(String item : items){
    System.out.println(item);
}
//============forEach + Lambda表达式==========
List<String> items = new ArrayList<>();
items.add("A");
items.add("B");
items.add("C");
items.add("D");
items.add("E");
//输出:A,B,C,D,E
items.forEach(item->System.out.println(item));
//输出 : C
items.forEach(item->{
    if("C".equals(item)){
        System.out.println(item);
    }
});

IntStream

IntStream.range()

public static IntStream range(int startInclusive, int endExclusive)
返回一个从 startInclusive (包括) 到 endExclusive (不包括)的间隔为 1 的整数流
等价于

for (int i = startInclusive; i < endExclusive ; i++) {
   ...
}

使用示例:

// IntStream.range 测试
@Test
public void testIntStreamRange() {
    // 生成一系列整数并使用
    IntStream.range(1, 100).forEach(i -> System.out.print(i + ","));

    // 重复N次,当循环用
    IntStream.range(0, 10).forEach(i -> System.out.println("第" + i + "次循环"));

    // 生成数组
    int[] array = new int[100];
    array = IntStream.range(0, 100).toArray();
    System.out.println(Arrays.toString(array));
}

When should I use IntStream.range in Java?
https://stackoverflow.com/questions/38998514/when-should-i-use-intstream-range-in-java

IntStream.rangeClosed()

返回 1 到 10 左闭右闭区间间隔为 1 的整数序列

IntStream.rangeClosed(1, 10).forEach(i -> {
    }
);

IntStream.mapToObj()

<U> Stream<U> mapToObj(IntFunction<? extends U> mapper);
将一个 int 整数转换为对象类型 U

List<UserDO> userDos = IntStream.range(0, 30).mapToObj(i -> UserDO.builder()
        .code(codePrefix + i)
        .name("userNmme")
        .build()).collect(Collectors.toList());

sum()求和

List<PageView> pageViews = pageViewMapper.selectByExample(example);
Long sum = pageViews.stream().mapToLong(PageView::getCount).sum();

max()/min() 最大最小值

Integer maxNumber = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
                    .max(Comparator.comparing(Integer::valueOf)).get();
Integer minNumber = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
                    .min(Comparator.comparing(Integer::valueOf)).get();
// 找name最长的user
Optional<User> userWithLongestName = users.stream()
            .max(Comparator.comparing(user -> user.getName().length()));

// usersList 中列表长度的最大值
List<List<User>> usersList = Lists.newArrayList();
int maxLength = recalls.stream().mapToInt(List::size).max().orElse(0);

LeetCode.1431.Kids With the Greatest Number of Candies 拥有最多糖果的孩子

max()/min() 的错误用法

int a = Stream.of(2,1,4,5,3).max(Integer::max).get();
System.out.println(a); // 2

int b = Stream.of(-2,-1,-4,-5,-3).max(Integer::max).get();
System.out.println(b); // -3

int c = Stream.of(2,1,4,5,3).min(Integer::min).get();
System.out.println(c); // 3

int d = Stream.of(-2,-1,-4,-5,-3).min(Integer::min).get();
System.out.println(d); // -2

上述代码的错误在于,应该给 max/min 传入一个比较器,而不是求两两最大、最小值的函数。
比较器 Comparator 是个函数式接口 int compare(T o1, T o2); 当比较 int 时也是两个 int 入参,一个 int 返回值,和 max/min 的函数签名相同,所以没有语法错误。

int compare(int o1, int o2); 的正确实现应该是 o1 减 o2 为正值时表示 o1 大于 o2
第一个例子中全部元素都是正数,所以 Integer::max 总是正值,是一个 o1 总是大于 o2 的比较器,所以第一个元素 2 被认为是最大的。
第二个例子中全部元素都是负数,所以 Integer::max 总是负值,是一个 o1 总是小于 o2 的比较器,所以最后一个元素 -3 被认为是最大的。
第三个例子中全部元素都是正数,所以 Integer::min 总数正值,是一个 o1 总是大于 o2 的比较器,所以最后一个元素 3 被人是最小的。
第三个例子中全部元素都是负数,所以 Integer::min 总数负值,是一个 o1 总是小于 o2 的比较器,所以第一个元素 -2 被人是最小的。


reduce()

T reduce(T identity, BinaryOperator<T> accumulator);

identity 默认值或初始值。如果缺少identity参数,则没有默认值或初始值,并且它返回 optional
BinaryOperator 函数式接口,对两个 T 类型的操作数进行操作,返回一个 T 类型的结果。

https://www.cnblogs.com/gaohanghang/p/12390233.html


collect()

方法原型:

<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);
<R, A> R collect(Collector<? super T, A, R> collector);

三参数的collect方法调用起来太麻烦,所以还有个简化的单参数重载方法,其参数Collector就是对这三个参数的简单封装。
Collectors工具类可通过静态方法生成各种常用的Collector。举例来说,如果要将Stream规约成List可以通过如下两种方式实现:

// 将Stream规约成List
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);// 方式1
List<String> list = stream.collect(Collectors.toList());// 方式2
System.out.println(list);

将Stream转换成List或Set是比较常见的操作,所以Collectors工具类已经为我们提供了对应的收集器,通过如下代码即可完成:

// 将Stream转换成List或Set
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(Collectors.toList()); // (1)
Set<String> set = stream.collect(Collectors.toSet()); // (2)
Map<String, Integer> map = stream.collect(Collectors.toMap(
    Function.identity(), //如何生成key, t->t
    String::length)); //如何生成value,长度

上述代码能够满足大部分需求,但由于返回结果是接口类型,我们并不知道类库实际选择的容器类型是什么,有时候我们可能会想要人为指定容器的实际类型,这个需求可通过Collectors.toCollection(Supplier<C> collectionFactory)方法完成。

// 使用toCollection()指定规约容器的类型
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// (3)
HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4)

上述代码(3)处指定规约结果是ArrayList,而(4)处指定规约结果为HashSet。


Collectors

toList() 与 collect(Collectors.toList())

java 16 新增 Stream.toList()
当 userList 为空时
userList.stream().collect(Collectors.toList()) 每次返回的是一个新的 空list 对象。
userList.stream().toList() 返回的都是相同的 空list 对象。

正常使用没有影响,但如果 SneakYAML 对数据进行 yaml 序列化时,如果 空list 都是同一个对象,会导致产生 &id001 和 *id001 锚点和引用,需要注意。


Collectors.toMap()

Collectors.toMap() 源码:

public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper) {
    return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}

public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper,
                                BinaryOperator<U> mergeFunction) {
    return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}

public static <T, K, U, M extends Map<K, U>>
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
                            Function<? super T, ? extends U> valueMapper,
                            BinaryOperator<U> mergeFunction,
                            Supplier<M> mapSupplier) {
    BiConsumer<M, T> accumulator
            = (map, element) -> map.merge(keyMapper.apply(element),
                                          valueMapper.apply(element), mergeFunction);
    return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}

当有重复 key 时会抛出 IllegalStateException 异常

使用Java8的Stream把List转为Map

public Map<Long, Account> getIdAccountMap(List<Account> accounts) {
    return accounts.stream().collect(Collectors.toMap(Account::getId, account -> account));
}

account -> account 是一个返回本身的 lambda 表达式,其实还可以使用 Function 接口中的一个默认方法代替,使整个方法更简洁优雅:

public Map<Long, Account> getIdAccountMap(List<Account> accounts) {
    return accounts.stream().collect(Collectors.toMap(Account::getId, Function.identity()));
}

其中 Function.identity() 就是遍历 beanList 时对应的当前 TestBean 对象,可以简单的认为就是循环遍历时的当前元素 this。

Collectors.toMap() 指定Map类型

public Map<String, Account> getNameAccountMap(List<Account> accounts) {
    return accounts.stream().collect(Collectors.toMap(Account::getUsername, Function.identity(), (v1, v2) -> v2, LinkedHashMap::new));
}

默认使用的是 HashMap,可以指定需要的 Map 类型,比如收集为 LinkedHashMap 后可以实现按 插入/访问 顺序排序,收集为 TreeMap 后可实现按 key 排序。

Collectors.toMap() 重复value任取其一

public Map<String, Account> getNameAccountMap(List<Account> accounts) {
    return accounts.stream().collect(Collectors.toMap(Account::getUsername, Function.identity(), (v1, v2) -> v2));
}

如不处理,可能报错 java.lang.IllegalStateException: Duplicate key, 因为 name 是有可能重复的。
toMap 有个重载方法,可以传入一个合并的函数来解决 key 冲突问题。这里只是简单的使用后者覆盖前者来解决 key 重复问题。

Collectors.toMap() 重复value选择最新的

根据user ids批量查询用户标签,找到每个user id和其最新标签的Map映射

List<UserTag> userTagList = myMapper.queryUserTagsByUserIds(userIds);
Map<Long, UserTag> userIdUserTagMap = userTagList.stream().collect(Collectors
        .toMap(UserTag::getUserId, Function.identity(),
                (v1, v2) -> v1.getCreateTime().after(v2.getCreateTime()) ? v1 : v2));

Collectors.toMap() 重复value构成List

年龄 -> 年龄相同的人的姓名 List

Map<Integer, List<String>> jdk8MultiMap = beanList.stream().collect(Collectors.toMap(
        // 年龄作为 key
        PeopleBean::getAge,
        // 姓名 List 作为 value
        bean -> Lists.newArrayList(bean.getName()),
        // 当同一个 key 遇到不同 value 时的合并策略,注意这里的类型已经是 List 了
        (List<String> v1List, List<String> v2List) -> {
            v1List.addAll(v2List);
            return v1List;
        }));
import com.google.common.collect.Lists;

Map<Long, String> userIdNameMap = userList.stream().collect(Collectors.toMap(
  User::getId,
  Lists::newArrayList,
  (List<String> oldList, List<String> newList) -> {
      oldList.addAll(newList);
      return oldList;
  }));

使用java8将list转为map
https://zacard.net/2016/03/17/java8-list-to-map/


Collectors.joining(“,”) 转逗号分隔字符串

转为逗号分隔字符串

List<Long> ids = Arrays.asList(1,2,3,4);
String str = ids.stream().map(Object::toString).collect(Collectors.joining(",")); // "1,2,3,4"

JDK8 Collectors 使用篇(四)
https://my.oschina.net/kaishui/blog/754915

Collectors.counting()

统计个数


Collectors.collectingAndThen()

TreeSet多字段去重

name 和 nickname 都相同时认为重复,重写 TreeSet 构造方法的比较器 Comparator 即可利用 TreeSet 实现自动去重

Map<Long, List<User>> userIdUserMap = Maps.newHashMap();

// 向userIdUserMap中写入数据

// name和nickname都相同时认为重复,去重
userIdUserMap.forEach((userId, userList) -> {
    userList = userList.stream().collect(Collectors
            .collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(user -> user.getName() + user.getNickName()))),
                    ArrayList::new));
    userIdUserMap.put(userId, userList);
});

java8特性–list集合根据多个字段去重
https://blog.csdn.net/zh15732621679/article/details/80483617

TreeSet排序和多字段去重

resultList 是从多个表查询后 addAll 组合的结果,需要排序和去重。
利用 TreeSet 集合的去重和排序特性:

// 排序与去重
resultList = resultList.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(
        (UserBean user1, UserBean user2) -> {
            // 当两个user姓名和性别都相同时才认为是重复的
            if (user1.getUserName().equals(user2.getUserName()) && user1.getGender().equals(user2.getGender())) {
                return 0;
            } else {
                // 按更新时间倒序排序
                return user2.getUpdateTime().compareTo(user1.getUpdateTime());
            }
    })), ArrayList::new));

Collectors.groupingBy()

public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier);
public static <T, K, A, D> Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream);
public static <T, K, D, A, M extends Map<K, D>> Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier, Supplier<M> mapFactory, Collector<? super T, A, D> downstream);

跟 SQL 中的 group by 语句类似,Java Stream 中的 groupingBy() 收集器也是按照某个属性对数据进行分组,结果是个 Map, 相同 key 的元素构成 List

分组后构成List

将员工按照部门进行分组:

// Group employees by department
Map<Department, List<Employee>> byDept = employees.stream()
            .collect(Collectors.groupingBy(Employee::getDepartment));

以上只是分组的最基本用法。

分组后统计个数

有些时候仅仅分组是不够的。在 SQL 中使用 group by 是为了协助其他查询,比如先将员工按照部门分组,然后统计每个部门员工的人数。
Java 类库设计者也考虑到了这种情况,增强版的 groupingBy() 能够满足这种需求。增强版的 groupingBy() 允许我们对元素分组之后再执行某种运算,比如求和、计数、平均值、类型转换等。这种先将元素分组的收集器叫做上游收集器,之后执行其他运算的收集器叫做下游收集器(downstream Collector)。

使用下游收集器统计每个部门的人数

Map<Department, Long> totalByDept = employees.stream()
                    .collect(Collectors.groupingBy(Employee::getDepartment, // 上游收集器
                                                   Collectors.counting())); // 下游收集器

分组后做map

下游收集器还可以包含更下游的收集器,考虑将员工按照部门分组的场景,如果我们想得到每个员工的名字(字符串),而不是一个个 Employee 对象,可通过如下方式做到:
按照部门对员工分组放到一个 map 中,同部门的员工取 name 放到 list 中

Map<Department, List<String>> byDept = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment, // 上游收集器
                        Collectors.mapping(Employee::getName, // 下游收集器
                                Collectors.toList()))); // 更下游的收集器

分组到按key有序的TreeMap

dataVOList 按 time 字段的日期分组,并按日期字典序倒序排序,同一日期的放入 List, List 中的值取 DataVO 的 name 字段

TreeMap<String, List<String>> dateGroupMap = dataVOList.stream()
        .filter(dataVO -> Objects.nonNull(dataVO.getTime()))
        .collect(Collectors.groupingBy(dataVO -> DateUtils.format("yyyy-MM-dd", dataVO.getSnapshotTime()),
                () -> new TreeMap<String, List<String>>(Comparator.reverseOrder()),
                Collectors.mapping(DataVO::getName, Collectors.toList())));

上一篇 Atom 使用笔记

下一篇 Typora使用笔记

阅读
评论
9.1k
阅读预计41分钟
创建日期 2018-06-10
修改日期 2023-12-03
类别
标签
目录
  1. Lambda表达式
    1. Java8中lambda表达式无法抛出受检异常
    2. lambda表达式(内部类)中只能访问final局部变量
  2. 函数式接口
    1. 接口的静态方法和默认方法
    2. 方法引用
      1. No compile-time declaration for the method reference is found
    3. 常用函数式接口
      1. Function<T,R>
      2. Predicate<T>
      3. BiFunction<T, U, R>
  3. Stream 接口
    1. Stream反转列表
    2. generate() 生成单元素无限序列
    3. iterate() 生成无限序列
    4. parallelStream()
      1. 修改并行流的线程数
      2. 并行流可正常distinct()操作
    5. anyMatch()
    6. allMatch()
    7. peek()
    8. findFirst()
    9. findAny()
    10. map()
    11. flatMap()
    12. filter()
      1. collect后才起作用
      2. Objects::nonNull 过滤null元素
    13. distinct()
    14. sorted()
      1. 默认自然排序(字典序)
      2. Comparator.reverseOrder() 自然序逆序
      3. Comparator.comparing() 单字段正序排序
      4. Comparator.comparing() 单字段倒叙排序
      5. Comparator.comparing().thenComparing() 多字段排序
      6. 多字段排序一个升序一个降序
    15. forEach()
      1. Java8 foreach()中return相当于continue
      2. foreach遍历Map
      3. Java8 Map转List
      4. foreach遍历List
    16. IntStream
      1. IntStream.range()
      2. IntStream.rangeClosed()
      3. IntStream.mapToObj()
    17. sum()求和
    18. max()/min() 最大最小值
      1. max()/min() 的错误用法
    19. reduce()
    20. collect()
    21. Collectors
      1. toList() 与 collect(Collectors.toList())
      2. Collectors.toMap()
        1. 使用Java8的Stream把List转为Map
        2. Collectors.toMap() 指定Map类型
        3. Collectors.toMap() 重复value任取其一
        4. Collectors.toMap() 重复value选择最新的
        5. Collectors.toMap() 重复value构成List
      3. Collectors.joining(“,”) 转逗号分隔字符串
      4. Collectors.counting()
      5. Collectors.collectingAndThen()
        1. TreeSet多字段去重
        2. TreeSet排序和多字段去重
      6. Collectors.groupingBy()
        1. 分组后构成List
        2. 分组后统计个数
        3. 分组后做map
        4. 分组到按key有序的TreeMap

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论