当前位置 : 首页 » 文章分类 :  开发  »  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的额外信息
userBeanList.parallelStream().forEachOrdered(bean -> completeBean(bean));

中间操作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()

public static <T,K,U> Collector<T,?,Map<K,U>> toMap​(Function<? super T,? extends K> keyMapper,
                                                    Function<? super T,? extends U> valueMapper)

当有重复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重复Key处理

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

如不处理,可能报错(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(), (key1, key2) -> key2, LinkedHashMap::new));
}

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

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

上一篇 Atom使用笔记

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