java8 - Lambda集合操作

遍历集合

在java8之前的版本中遍历一个集合常用下面这两种方法

private static void list() {
    for (int i = 0; i < fruits.size(); i++) {
        System.out.println(fruits.get(i));
    }
}
private static void list1() {
    for (String fruit : fruits) {
        System.out.println(fruit);
    }
}

第一种方法引入了中间变量,只有需要在对某个特定的位置元素执行某个操作时候,循环变量的引入才是有意义的
第二种方式也叫做增强型的for循环,是通过iterator迭代器实现的
在java8中我们有了更加简洁的方法用于遍历集合,java8中的Iterable接口被增强了,现在该接口有了一个forEach方法用来实现内部遍历器

private static void list4() {
    fruits.forEach(new Consumer<String>() {
        @Override
        public void accept(String s) {
            System.out.println(s);
        }
    });
}

在java中不推荐使用匿名类,取而代之的是Lambda表达式作为参数

fruits.forEach((final String fruit) -> System.out.println(fruit));

forEach方法本省是一个高阶函数,因为它接受了一个Lambda表达式作为参数,Lambda表达式是一个函数,在Lambda表达式的左边代表了集合的元素,右边是对该元素具体的操作,forEach之所以被称为内部遍历器,原因在于一旦它开始执行了,那么遍历操作就不能够被轻易中断。

通过java编译器类型推到特性,Lambda表达式能够被进一步简化如下

fruits.forEach((fruit) -> System.out.println(fruit));

如果Lambda坐标只有一个变量的时候,括号是可以省略的

fruits.forEach(fruit -> System.out.println(fruit));

我们还可以使用方法引用

fruits.forEach(System.out::println);

当一个方法接收函数式接口作为参数的时候,可以传入Lambda表达式进行调用,方法引用时省略了参数信息.这是因为Java编译器在为该方法引用生成实例时候,会进行类型推到自动将集合中的元素作为参数传到方法中

集合转换

使用Lambda将一个集合通过计算转换成另一种集合是常用的操作
java8之前的方法如下

private static void test1() {
    final List<String> uppercaseFruit = new ArrayList<>();
    for (String fruit : fruits) {
        uppercaseFruit.add(fruit.toUpperCase());
    }
}

以上代码使用了外部遍历器,我们要尽可能的将外部遍历器转化为内部遍历器

private static void test2() {
    final List<String> uppercaseFruit = new ArrayList<>();
    fruits.forEach(fruit -> uppercaseFruit.add(fruit.toUpperCase()));
}

上面的代码并不简洁,我们使用Stream API进行进一步简化

private static void test3() {
    final List<String> uppercaseFruit =  fruits.stream().map(fruit -> fruit.toUpperCase()).collect(Collectors.toList());
}

Stream API则有一种一气呵成的感觉,map首先转化集合元素,利用collect(Collectors.toList())转化成一个新集合

过滤筛选元素

当需要打印出集合中某个字母开头的元素时候,java8以前如下

public static void pickName(
    final List<String> names, final String startingLetter) {
    String foundName = null;
    for(String name : names) {
        if(name.startsWith(startingLetter)) {
            foundName = name;
            break;
        }
    }
    System.out.print(String.format("A name starting with %s: ", startingLetter));
    if(foundName != null) {
        System.out.println(foundName);
    } else {
        System.out.println("No name found");
    }
}

代码非常的冗长,如果维护这样的代码首先看懂这段代码就要花费很长的时间,而且在使用初始值为null的foundName之前我们还要使用条件进行判断,我们使用Lambda表达式重写这段代码对比一下

public static void pickName(
    final List<String> names, final String startingLetter) {
    final Optional<String> foundName = names.stream()
        .filter(name ->name.startsWith(startingLetter))
        .findFirst();
    System.out.println(String.format("A name starting with %s: %s", startingLetter, foundName.orElse("No name found")));
}

以上代码出现了几个新概念: 在调用filter后,调用了findFirst方法,这个方法返回的对象类型时Optional。关于这个Optional,可以将它理解成一个可能存在,也可能不存在的结果。这样的话,就可以避免对返回结果进行空检查了。对于结果是否真的存在,可以使用isPresent()方法进行判断,而get()方法用于尝试对结果的获取。当结果不存在时,我们也可以使用orElse()来指定一个替代结果,正如上面使用的那样。

另外,当结果存在时,通过使用ifPresent方法也可以运行某一段代码,运行的代码可以通过Lambda表达式声明:

foundName.ifPresent(name -> System.out.println("Hello " + name));

重用Lambda表达式

如果需要对不止一个集合进行Lambda操作时候

如下情况

final long countFriendsStartN = friends.stream().filter(name -> name.startsWith("N")).count();
final long countComradesStartN = comrades.stream().filter(name -> name.startsWith("N")).count();
final long countEditorsStartN = editors.stream().filter(name -> name.startsWith("N")).count();

所以Lambda表达式需要被重用,我们利用变量来存储Lambda表达式

看下面的例子

final Predicate<String> startsWithN = name -> name.startsWith("N");
final long countFriendsStartN = friends.stream().filter(startsWithN).count();
final long countComradesStartN = comrades.stream().filter(startsWithN).count();
final long countEditorsStartN = editors.stream().filter(startsWithN).count();

如果上面的lambda表达式”N”需要根据情况有所改变的话,我们使用下面的高阶函数

public static Predicate<String> checkIfStartsWith(final String letter) {
    return name -> name.startsWith(letter);
}

通过letter作为参数,在不同的场景下作为不同的filter因素

缩小作用域

实际上,使用static来实现以上的高阶函数并不是一个好主意。可以将作用域缩小一些:

final Function<String, Predicate<String>> startsWithLetter =
    (String letter) -> {
        Predicate<String> checkStartsWith = (String name) -> name.startsWith(letter);
        return checkStartsWith;
    };

startsWithLetter变量代表的是一个Lambda表达式,该表达式接受一个String作为参数,返回另外一个Lambda表达式。这也就是它的类型Function>所代表的意义。

目前来看,使用这种方式让代码更加复杂了,但是将它简化之后就成了下面这个样子:

final Function<String, Predicate<String>> startsWithLetter = (String letter) -> (String name) -> name.startsWith(letter);

还可以通过省略参数类型进行进一步的简化:

final Function<String, Predicate<String>> startsWithLetter = letter -> name -> name.startsWith(letter);

乍一看也许觉得上面的形式太复杂,其实不然,你只是需要时间来适应这种简练的表达方式。

那么,我们需要实现的代码就可以这样写了:

final long countFriendsStartN = friends.stream().filter(startsWithLetter.apply("N")).count();
final long countFriendsStartB = friends.stream().filter(startsWithLetter.apply("B")).count();

集合规约

和前面的种种操作不同,对于集合的归约(Collection Reduction),元素与元素不再是独立的,它们会通过某种归约操作联系在一起。
比如得到名字集合的总字符数,就是一种典型的求和归约。可以实现如下:

System.out.println("Total number of characters in all names " +
        list.stream().mapToInt(name -> name.length()).sum());

元素连接

java8以前我们使用for循环连接一个集合中所有的元素

for(String name : friends) {
    System.out.print(name + ", ");
}
System.out.println();

上述代码的问题是,在最后一个名字后面也出现了讨人厌的逗号!为了修复这个问题:

for(int i = 0; i < friends.size() - 1; i++) {
    System.out.print(friends.get(i) + ", ");
}
if(friends.size() > 0)
    System.out.println(friends.get(friends.size() - 1));

我们使用Stream API来实现上面的代码
使用reduce方法也能够完成对于集合元素的连接操作,毕竟集合元素的连接也是一种归约。只不过,正如前面看到的那样,reduce方法太过于底层了。针对这个问题,Stream类型还定义了一个collect方法用来完成一些常见的归约操作:

System.out.println(friends.stream().map(String::toUpperCase).collect(Collectors.joining(", ")));

可见collect方法并不自己完成归约操作,它会将归约操作委托给一个具体的Collector,而Collectors类型则是一个工具类,其中定义了许多常见的归约操作,比如上述的joining Collector。

Collect和Collector类

上面的代码简单的提到了Collector的使用,接下来详细讲解Collect及Collector操作

对于一个people集合,我们要从中筛选出年龄大于20的人组成一个新的集合

不使用Collect方法时候我们可以这样写

List<Person> olderThan20 = new ArrayList<>();
        people.stream().filter(person -> person.getAge() > 20).forEach(person -> olderThan20.add(person));

上面的代码中forEach(person -> olderThan20.add(person))这一段仍然属于命令式代码风格,并且声明了可变的List代码难以并行化

Collect方法的使用需要以下几种信息
1.创建这个目标容器可以使用ArrayList::new
2.将元素添加到这个容器使用ArrayList::add
3.考虑到可能的并行处理,合并多个小容器成为目标容器

使用collect方法如下

List<Person> olderThan20 = people.stream().filter(person -> person.getAge() > 20).collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

这里用到的collect函数完整的声明如下

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

第一个参数表示如何创建目标容器,它需要一个Supplier类型的函数式接口

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

实际上根据定义的特点,类型的构造函数也是可以被传入的,即上面的ArrayList::new。

第二参数用于表示如何收集元素到容器,它需要接受一个BiConsumer类型的函数式接口

@FunctionalInterface
public interface BiConsumer<T, U> {
    void accept(T t, U u);
}

第三个参数表示如何合并多个目标容器,这是考虑在多线程环境下每个线程会拥有一个小容器,当每个线程完成了自己的小容器那么就要将所有小容器合并为一个大容器

所以上面的代码意图更明显,也利于并行操作

利用collector可以进一步简化上面的代码

List<Person> olderThan20 = people.stream().filter(person -> person.getAge() > 20).collect(Collectors.toList());

Collectors工具类中还提供了需多方法比如toSet,toMap,joinning等

collectors还有一个非常重要的方法Collectors.groupingBy
下面介绍Collectors.groupingBy的使用:

Map<Integer, List<Person>> peopleByAge = people.stream()
    .collect(Collectors.groupingBy(Person::getAge));
System.out.println("People grouped by age: " + peopleByAge);

上述代码很简短,可是实现的功能却并不简单。当使用传统的命令式风格进行编码时,代码量估计是上述代码量的5倍左右,还不包括为了让程序能够并行运行,需要添加的那部分代码。

groupingBy方法接受一个函数式接口作为分类器(Classifier),用来实现分类的逻辑。正如以上的getAge方法,该方法的每一个返回值都会被作为一个分类,也就是得到的结果Map中的一个Key。

如果每个分类是由一个Key和该Key对应的分类结果组成的,那么对于分类结果实际上还可以进一步使用进行各种操作来得到该结果的一种变型,看上去有点难以理解,举一个例子就简单了:

Map<Integer, List<String>> nameOfPeopleByAge = people.stream()
    .collect(Collectors.groupingBy(Person::getAge, mapping(Person::getName, Collectors.toList())));
System.out.println("People grouped by age: " + nameOfPeopleByAge);

groupingBy方法接受的两个参数:

Function

final Comparator<Person> byAge = Comparator.comparing(Person::getAge);
        Map<Character, Optional<Person>> oldest = people.stream().collect(Collectors.groupingBy(people -> people.getName().charAt(0), Collectors.reducing(BinaryOperator.maxBy(byAge))));

参考http://blog.csdn.net/dm_vincent/article/details/40340291
参考http://blog.csdn.net/dm_vincent/article/details/40391387

    原文作者:光与热
    原文地址: https://blog.csdn.net/JavaMoo/article/details/78856561
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞