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

Java-流和Lambda表达式

Java流和lambda表达式笔记

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


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/


函数式接口

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


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


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


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());

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之后的其余字符串


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.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


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();

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


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

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重复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指定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选择最新的

根据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

Map<Integer, List<String>> jdk8MultiMap = beanList.stream()
    .filter(o -> o != null) // avoid throws NullPointerException
    .collect(Collectors.toMap(
        // get key
        TestBean::getAge
        // get value
        , (TestBean o) -> Lists.newArrayList(o.getName())
        // 当同一个key遇到不同value时的合并策略
        , (x, y) -> {
            x.addAll(y);
            return x;
        }
        // 当不需要明确具体的Map类型时可省略。默认就是HashMap
        , HashMap::new
    ));
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()

跟SQL中的group by语句类似,这里的groupingBy()也是按照某个属性对数据进行分组,属性相同的元素会被对应到Map的同一个key上。下列代码展示将员工按照部门进行分组:

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

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

分组后统计个数

有些时候仅仅分组是不够的。在SQL中使用group by是为了协助其他查询,比如1. 先将员工按照部门分组,2. 然后统计每个部门员工的人数。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()))); // 更下游的收集器

上一篇 Atom使用笔记

下一篇 Typora,一款好用的Markdown编辑器

阅读
评论
6,503
阅读预计28分钟
创建日期 2018-06-10
修改日期 2020-04-21
类别
标签

页面信息

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

评论