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

Java8 流和Lambda表达式笔记

Java流和lambda表达式笔记

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


Lambda表达式

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

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

  • 捕获实例或静态变量是没有限制的(可认为是通过 final 类型的局部变量 this 来引用前两者)
  • 捕获的局部变量必须显式的声明为 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 属性

换句话说,如果在匿名类或 Lambda 表达式中访问的局部变量,如果不是 final 类型的话,编译器自动加上 final 修饰符。

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

JDK8 函数式接口编程(二)
https://my.oschina.net/kaishui/blog/740737

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


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


中间操作Intermediate

map()

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<SpOrder> spOrderList = spOrderMapper.selectByExample(example);
List<String> orderNos = spOrderList.stream().map(o -> o.getOrderNo()).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个数字。

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


结束操作Terminal

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

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

/**
 * Returns a {@code Collector} that accumulates elements into a
 * {@code Map} whose keys and values are the result of applying the provided
 * mapping functions to the input elements.
 *
 * <p>If the mapped
 * keys contains duplicates (according to {@link Object#equals(Object)}),
 * the value mapping function is applied to each equal element, and the
 * results are merged using the provided merging function.  The {@code Map}
 * is created by a provided supplier function.
 *
 * @implNote
 * The returned {@code Collector} is not concurrent.  For parallel stream
 * pipelines, the {@code combiner} function operates by merging the keys
 * from one map into another, which can be an expensive operation.  If it is
 * not required that results are merged into the {@code Map} in encounter
 * order, using {@link #toConcurrentMap(Function, Function, BinaryOperator, Supplier)}
 * may offer better parallel performance.
 *
 * @param <T> the type of the input elements
 * @param <K> the output type of the key mapping function
 * @param <U> the output type of the value mapping function
 * @param <M> the type of the resulting {@code Map}
 * @param keyMapper a mapping function to produce keys
 * @param valueMapper a mapping function to produce values
 * @param mergeFunction a merge function, used to resolve collisions between
 *                      values associated with the same key, as supplied
 *                      to {@link Map#merge(Object, Object, BiFunction)}
 * @param mapSupplier a function which returns a new, empty {@code Map} into
 *                    which the results will be inserted
 * @return a {@code Collector} which collects elements into a {@code Map}
 * whose keys are the result of applying a key mapping function to the input
 * elements, and whose values are the result of applying a value mapping
 * function to all input elements equal to the key and combining them
 * using the merge function
 *
 * @see #toMap(Function, Function)
 * @see #toMap(Function, Function, BinaryOperator)
 * @see #toConcurrentMap(Function, Function, BinaryOperator, Supplier)
 */
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 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));
}

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

使用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(","));

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

上一篇 Atom使用笔记

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