java8学习笔记(一):FunctionalInterface与Stream

387 阅读9分钟

引言

最近在工作中用到java8的Stream流式操作很多,因而对相关概念与实操作简单归纳与总结:

函数式接口

函数式接口是java8新加入特性,为配合lambda表达式而生。lambda表达式与匿名表达式异同可参考 时光隧道  。判断一个接口是否为函数式接口特别简单,只需满足一个条件即可:一个接口有且仅有一个函数(接口默认static及default方法除外)! 即使该接口未被@FunctionalInterface注解标记,但仍为一个函数式接口

我们来看一个示例:

 Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("hello the thread.");
    }
});
thread.start();

使用lambda表达式创建方式为:

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

从示例可以看出,使用lambda表达式配合函数式接口可以大大精简代码,那么函数式接口有什么用呢?

使用lambda表达式调用该函数,将接口方法的实现封装到具体方法,实际也是实现且创建一个接口对象,将方法作为接口实现。更加抽象,只能看到接口方法内部实现。

那么,我们可将常见的使用方式归纳为:

方法引用通过::将方法隶属和方法自身连接起来,如:ClassName :: methodName
1. 静态方法   (args) -> ClassName.staticMethod(args)   转换成   ClassName::staticMethod
2. 实例方法   (args) -> args.instanceMethod()   转换成   ClassName::instanceMethod
3. 外部的实例方法   (args) -> ext.instanceMethod(args)   转换成   ext::instanceMethod(args)

我们来看一个完整的示例:

@FieldDefaults(level = AccessLevel.PRIVATE)
@ToString
public class Kid {

    Integer age;
    
    String name;

    public Kid(int age, String name) {
        this.age = age;
        this.name = name;
    }
    public Integer getAge() {return age;}
    public void setAge(Integer age) {this.age = age;}
    public String getName() {return name;}
    public void setName(String name) {this.name = name;}
}

public class MethodReference {

    public static void main(String[] args) {
        List<Kid> kids = Arrays.asList(
                new Kid(10, "奶酪1"),
                new Kid(8, "奶酪3"),
                new Kid(11, "奶酪2")
        );

        // 采用lambda表达式排序
        kids.sort((Kid a, Kid b) -> compare(a.getAge(), b.getAge()));
        kids.forEach(System.out::println);
        // 采用方法引用排序
        kids.sort(Comparator.comparing(Kid::getName));
        kids.forEach(System.out::println);
    }

}

输出如下:

Kid(age=8, name=奶酪3),Kid(age=10, name=奶酪1),Kid(age=11, name=奶酪2)
Kid(age=10, name=奶酪1),Kid(age=11, name=奶酪2),Kid(age=8, name=奶酪3)


java8新增的java.util.function包,为我们提供了诸多的函数式接口,归纳如下:

函数式接口名 参数类型 接口说明
Supplier Supplier< T > 提供T对象(例如工厂),不接收值
Consumer Consumer< T > 接收T对象,不返回值
Predicate Predicate< T > 接收T对象并返回boolean
Function Function< T, R > 接收T对象,返回R对象
UnaryOperator UnaryOperator< T > 接收T对象,返回T对象
BiConsumer BiConsumer<T, U> 接收T对象和U对象,不返回值
BiPredicate BiPredicate<T, U> 接收T对象和U对象,返回boolean
BiFunction BiFunction<T, U, R> 接收T对象和U对象,返回R对象
BinaryOperator BinaryOperator< T > 接收两个T对象,返回T对象

函数式接口须配合Stream才能发挥其最大的用处,接下来我们对Steam接口方法常见使用方式进行举例。

Stream浅析

何为Stream ?

流(Stream)是Java API的新成员,它允许以声明性的方式处理数据集合(类似于数据库查询语句).暂且理解为遍历数据集的高级迭代器.

我们可以用一图来归纳Stream的通用场景:

steam通用示意图
对Stream的使用就是实现一个filter-map-reduce过程,产生一个最终结果,或者导致一个副作用(side effect), 整个流过程,即为过滤数据过程,每次流转换会生成新的流,流之间互不影响,常见的流可归结如下:

  • intermediate(中间流 [可一个或多个])
    map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
  • terminal(终结流 [只可有一个])
    forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
  • short-circuiting(短路 [可随时终止])
    anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

我们现在开始慢慢解剖Stream常用API,完成以后你可能可以基本掌握Stream的用法!

Stream filter(Predicate<? super T> predicate);

filter:简而言之,按指定条件过滤,保留符合条件的对象,数据流程如下:

filter.image
示例:

final List<Cat> catList = initList();
// 1. 过滤年龄小于等于3的猫咪
List<Cat> ageLessThanThreeList = catList.parallelStream().filter(cat -> cat.getAge().compareTo(3) <= 0).collect(toCollection(LinkedList::new));
ageLessThanThreeList.parallelStream().forEach(System.err::println);

result:

Cat{age=2, cnName='奶酪', enName='cheese', isMale=true}
Cat{age=3, cnName='曾三妹', enName='sisterThree', isMale=false}
Cat{age=2, cnName='奶油', enName='cream', isMale=true}

Stream map(Function<? super T, ? extends R> mapper);

map:对于Stream中包含的元素使用给定的转换函数进行转换操作,新生成的Stream只包含转换生成的元素。该方法有三个类型的变种,依次为:mapToInt、mapToLong、mapToDouble。比如,mapToInt意为新生成的Stream中的元素类型皆为int。多种变形方法,可以免除自动装箱/拆箱的额外消耗;通过流程图,我们一探究竟:

示例:

// 2. 将英文名称全部大写
List<String> upperCaseEnNameList = catList.parallelStream().map(cat -> cat.getEnName().toUpperCase()).collect(Collectors.toList());
upperCaseEnNameList.parallelStream().forEach(enName -> {
    System.err.print(enName + " ");
});

result:

SISTERTHREE SEVENTH CHEESE CREAM

IntStream mapToInt(ToIntFunction<? super T> mapper);

// 3. 对年龄求和
int sumAge = catList.parallelStream().mapToInt(Cat::getAge).sum();
System.err.println(sumAge);

result:

11

flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)

flatMap:主要用于将层级结构扁平化,就是将最底层元素抽出来放到一起,最终产生的Stream里面已经没有List了,都是最底层的元素。我们通过流程图能更加直观理解流程:

flatMap1.image
不好理解吗?那我们换张更加清晰明了的示意图:
flatMap2.image
通过示例,就能更好的理解了:

// 4. 取出所有的int元素,并汇聚为list
List<List<Integer>> flatMapList = flatMapInitList();
List<Integer> numberList = flatMapList.parallelStream().flatMap(itemList -> itemList.parallelStream()).collect(toList());
numberList.parallelStream().forEach(item -> {
    System.err.print(item.toString() + " ");
});

result:

6 5 8 7 3 4 2 1

Stream distinct();

distinct:滤重 , 数据流程图为:

distinct.image

// 5. 过滤去重
List<Integer> dupIntegerLit = dupIntegerInitList();
List<Integer> duplicateIntegerList = dupIntegerLit.parallelStream().distinct().collect(toList());
duplicateIntegerList.parallelStream().forEach(System.out::print);
System.out.println();

result :

231

Stream sorted();

源码表述为,sorted according to natural order, 对流中数据按照自然顺序排序。我们来看一个例子:

// 6. sorted 按年龄正序排列
List<Integer> orderAgeList = catList.parallelStream().map(Cat::getAge).sorted().collect(toList());
orderAgeList.stream().forEach(System.out::print);
System.out.println("attention:并行流会影响排序,需要特别注意!并行流结果为:");
orderAgeList.parallelStream().forEach(System.out::print);

result:

2234
attention:并行流会影响排序,需要特别注意!并行流结果为:
3422

Stream sorted(Comparator<? super T> comparator);

源码中描述为:sorted according to the provided {@code Comparator},意味着可以按照Comparator的排列规则,正序或倒序排列。并且,入参为Comparator,那么便可使用Comparator类中的多种方法,提升程序的扩展性。我们来看一个简单的例子:

/* sorted(Comparator<? super T> comparator) 按年龄倒序排列
   若是按照自然顺序排列,则这种写法与.map(Cat::getAge).sorted()效果一模一样
*/
List<Cat> orderedAgeOfCatList = catList.parallelStream().sorted(Comparator.comparing(Cat::getAge).reversed()).collect(toList());
/*
    等效于:
    List<Cat> orderedAgeOfCatList = catList.parallelStream().sorted(Collections.reverseOrder(Comparator.comparing(Cat::getAge))).collect(toList());
    即:Comparator.comparing(Cat::getAge).reversed() = Collections.reverseOrder(Comparator.comparing(Cat::getAge))
 */
orderedAgeOfCatList.stream().forEach(cat -> {
    System.err.println(cat.toString());
});

result:

Cat{age=4, cnName='七夕', enName='seventh', isMale=true}
Cat{age=3, cnName='曾三妹', enName='sisterThree', isMale=false}
Cat{age=2, cnName='奶油', enName='cream', isMale=true}
Cat{age=2, cnName='奶酪', enName='cheese', isMale=true}

** 对上面的排序我们使用如下方式有异曲同工之妙**:

// 使用java8 list.sort(Comparator<? super E> c)
catList.sort(Comparator.comparing(Cat::getAge).thenComparing(Cat::getCnName).reversed());
catList.stream().forEachOrdered(System.out::println);
/*
    总结:
        1. 无参的sorted()是对某一项的排序,默认返回natural order,返回的是Stream<T>
        2. 带参的sorted(Comparator<? super T> comparator)可对实体类中的某项排序,默认natural order,可通过reversed()方法倒序,返回的是实体类的集合.
        3. 亦可使用java8 新的排序方式 list.sort(Comparator<? super E> c),配合Comparator进行排序
 */

result:

Cat{age=4, cnName='七夕', enName='seventh', isMale=true}
Cat{age=3, cnName='曾三妹', enName='sisterThree', isMale=false}
Cat{age=2, cnName='奶酪', enName='cheese', isMale=true}
Cat{age=2, cnName='奶油', enName='cream', isMale=true}

Stream limit(long maxSize);

limit方法很容易理解,即返回多少个符合条件的对象。简单连接下流程图即可:

limit.image

List<Cat> limitCatList = catList.stream().limit(2).collect(toList());
limitCatList.stream().forEach(cat -> {
    System.err.println(cat.toString());
});

result:

Cat{age=4, cnName='七夕', enName='seventh', isMale=true}
Cat{age=3, cnName='曾三妹', enName='sisterThree', isMale=false}

Stream skip(long n);

skip即跳过指定个数,取其后的数据。也很容易理解,示意图如下:

skip.image

List<Cat> skipCatList = catList.stream().skip(1).collect(toList());
skipCatList.parallelStream().forEach(cat -> {
    System.err.println(cat.toString());
});

result:

Cat{age=2, cnName='奶酪', enName='cheese', isMale=true}
Cat{age=2, cnName='奶油', enName='cream', isMale=true}
Cat{age=3, cnName='曾三妹', enName='sisterThree', isMale=false}

Optional min(Comparator<? super T> comparator);

求集合中的最小值,返回值为Optional,我们来看例子:

Cat minAgeOfCat = catList.parallelStream().min(Comparator.comparing(Cat::getAge)).get();
System.err.println(minAgeOfCat.toString());

result:

Cat{age=2, cnName='奶酪', enName='cheese', isMale=true}

Optional max(Comparator<? super T> comparator);

求集合中的最大值,很方便理解,我们接着看例子:

Cat maxAgeOfCat = catList.parallelStream().max(Comparator.comparing(Cat::getAge)).get();
System.err.println(maxAgeOfCat.toString());

result:

Cat{age=4, cnName='七夕', enName='seventh', isMale=true}

long count();

求集合中元素的数量,与list.size()有异曲同工之妙。

long count = catList.parallelStream().count();
System.err.println(count);

result:

4

boolean anyMatch(Predicate<? super T> predicate);

根据规则,若集合中有匹配该规则的元素,则直接结束流程,并返回true;若集合遍历结束还没有找到匹配的元素,则返回false;

boolean isAnyMatch = catList.parallelStream().anyMatch(cat -> cat.getCnName().contains("奶"));
System.err.println(isAnyMatch);

result:

true

Optional findAny();

亦是根据规则进行匹配,找到符合条件的则返回该元素;

Cat findAnyOfCat = catList.parallelStream().filter(cat -> cat.getCnName().contains("奶")).findAny().get();
System.err.println(findAnyOfCat.toString());

result:

Cat{age=2, cnName='奶油', enName='cream', isMale=true}
很多时候anyMatch与findAny可以相互替换,使用方式类似

boolean allMatch(Predicate<? super T> predicate);

判断某个集合是否全部符合某个规则,若是,则返回true;反之则返回false;

boolean isAllMatch = catList.parallelStream().allMatch(cat -> Objects.equals(cat.getCnName(), "奶酪"));
System.err.println(isAllMatch);

result:

false

关键代码如下:

private static Map<Integer, Integer> initMap() {
    return new HashMap<Integer, Integer>() {{
        put(1, 10);
        put(3, 12);
        put(2, 11);
        put(4, 13);
    }};
}
private static void readMeFirst() {
    /*
        1.Stream 流的常规操作可归类如下:
        . intermediate(中间流 [可一个或多个])
            map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
        . terminal(终结流 [只可有一个])
            forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
        . short-circuiting(短路 [可随时终止])
            anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
     */
}
private static List<Integer> dupIntegerInitList() {
    return new ArrayList<Integer>() {{
        add(1);
        add(1);
        add(2);
        add(3);
    }};
}
private static List<Cat> dupCatInitList() {
    return new ArrayList<Cat>() {{
        add(new Cat(1, "刘啵啵儿", "liuboboer", true));
        add(new Cat(1, "刘啵啵儿", "liuboboer", true));
    }};
}
private static List<Cat> initList() {
    return new ArrayList<Cat>() {{
        add(new Cat(2, "奶油", "cream", true));
        add(new Cat(2, "奶酪", "cheese", true));
        add(new Cat(3, "曾三妹", "sisterThree", false));
        add(new Cat(4, "七夕", "seventh", true));
    }};
}
private static List<List<Integer>> flatMapInitList() {
    return new ArrayList<List<Integer>>() {{
        add(Arrays.asList(1, 2, 3));
        add(Arrays.asList(4, 5));
        add(Arrays.asList(6, 7, 8));
    }};
}
class Cat {

    /** * 年龄 */
    private Integer age;

    /** * 中文名称 */
    private String cnName;

    /** * 英文名称 */
    private String enName;

    /** * 是否为男性 */
    private Boolean isMale;

    public Integer getAge() { return age;}

    public void setAge(Integer age) { this.age = age;}

    public String getCnName() { return cnName;}

    public void setCnName(String cnName) { this.cnName = cnName;}

    public String getEnName() { return enName;}

    public void setEnName(String enName) { this.enName = enName;}

    public Boolean getMale() { return isMale;}

    public void setMale(Boolean male) { isMale = male;}

    public Cat() {}

    public Cat(Integer age, String cnName, String enName, Boolean isMale) {
        this.age = age;
        this.cnName = cnName;
        this.enName = enName;
        this.isMale = isMale;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "age=" + age +
                ", cnName='" + cnName + '\'' +
                ", enName='" + enName + '\'' +
                ", isMale=" + isMale +
                '}';
    }
}