Lambda与Stream流


前言:

JDK8以后出现了函数式编程Lambda表达式与Stream流的运用,使用它们能简化代码,同时使得代码看上去更加高大上,一起来学习一下吧~

一. Lambda表达式

1. 函数式编程思想概述

在数学中函数就是有输入量 , 输出量的一套计算方案, 也就是拿什么东西做什么事情. 相对而言, 面向对象过分强调”必须通过对象的形式来做事情”, 而函数式 思想则尽量忽略面向对象的复杂语法, 更加强调做什么,而不是以什么形式做

面向对象的思想 :

做一件事情, 找一个能解决这个事情的对象, 调用对象的方法, 完成事情.

函数式编程思想 :

只要能获取到结果, 谁去做的, 怎么做的都不重要. 重视的是结果, 不重视过程

2. 冗余的Runnable代码

当需要启动一个线程去完成任务时, 通常会通过java.lang.Runnable接口来定义任务内容, 并使用java.lang.Thread类来启动该线程. 代码如下 :

public static RunnableTest {
    public static void main(String[] args) {
        Runnable task = new Runnable() {
            @Override
            public void run() {//重写抽象方法
                System.out.println("多线程任务执行!");
            }
        };
        new Thread(task).start();//启动线程

        //体验Lambda表达式
        new Thread(() -> System.out.println(Thread.currentThread().getName()+"执行了")).start();
    }

本着面向对象的思想, 先创建一个Runnable接口的匿名内部类对象, 来指定任务内容, 在将其交给一个线程来启动

代码分析

对于Runnable的匿名内部类用法, 可以分析出几点内容 :

  • Thread类需要Runnbale接口作为参数, 其中抽象run方法是用来指定线程任务内容的核心.
  • 为了指定run的方法体, 不得不需要Runnable接口的实现类
  • 为了省去定义一个RunnableImpl实现类的麻烦, 不得不用匿名内部类
  • 必须覆盖重写抽象run方法, 所以方法名称, 方法参数, 方法返回值不得不再重写一遍, 且不能写错.
  • 而实际上, 似乎只有方法体才是关键所在
3. 编程思想转换
3.1 做什么, 而不是怎么做

我们真的希望创建一个匿名内部类对象吗 ? 不, 我们只是为了做这件事情 而不得不创建一个对象, 我们真正希望做的是 : 将run方法体内的代码传递给Thread类知晓.

传递一段代码———这才是我们真正的目的, 而创建对象只是受限于面向对象语法而不得不采取的一种手段方式, 那有没有更简单的办法呢 ? 如果我们将关注点从 “ 怎么做 “ 回归到” 做什么 “的本质上, 就会发现只要能够更好地达到目的, 过程与形式其实并不重要.

JDK8以后, Java中加入了==Lambda表达式==的重量级新特性, 为我们打开了新世界的大门

3.2 体验Lambda的更优写法

借助Java 8 的全新语法, 上述Runnable接口的匿名内部类写法可以通过更简单的Lambda表达式来达到等效 :

public class LambdaRunnable {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("多线程任务执行!")).start();//启动线程
    }
}

这段代码和刚才执行的效果是完全一样的, 可以在JDK8以上的版本下编译通过, 从代码语义可以看出, 我们启动了一个新线程, 而线程任务的内容以一种更加简洁的形式被指定

这让我们不再有不得不创建接口对象的束缚, 不再有抽象方法覆盖重写的负担 !

3.3 Lambda表达式标准格式

由三部分组成:

  • 一些参数
  • 一个箭头
  • 一段代码

格式:

( 参数类型 参数名称 ) -> { 
    方法体;
    return 返回值;
}

解释说明格式:

  1. 小括号中的参数和之前方法的参数写法一样,可以写任意个参数,如果多个参数,要使用逗号隔开。
  2. ->是一个运算符,表示指向性动作。
  3. 大括号中的方法体以及return返回值的写法和之前方法的大括号中的写法一样。

Lambda表达式是函数式编程思想

函数式编程: 可推导, 就是可省略

因为在Thread构造方法中需要Runnable类型的参数, 所以可以省略new Runnable

因为Runnable中只有一个抽象方法run, 所以重写的必然是这个run方法, 所以可以省略run方法的声明部分.

匿名内部类与Lambda对比 :

//匿名内部类
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("多线程执行!");
    }
}).start();

----------------------------------------------
    //Lambda
new Thread(() -> System.out.println("多线程任务执行!")).start();

仔细分析该代码中, Runnable 接口只有一个run 方法的定义:

  • public abstract void run(); 即制定了一种做事情的方案(其实就是一个方法):

    • 无参数:不需要任何条件即可执行该方案。
    • 无返回值:该方案不产生任何结果。
    • 代码块(方法体):该方案的具体执行步骤。

同样的语义体现在Lambda 语法中,要更加简单

Lambda :

  • 前面的一对小括号即run 方法的参数(无),代表不需要任何条件;
  • 中间的一个箭头代表将前面的参数传递给后面的代码;
  • 后面的输出语句即业务逻辑代码。
3.4 参数和返回值

下面演示java.util.Comparator接口的使用场景代码, 其中抽象方法定义为 :

  • public abstract int compare(T o1, T o2);

当需要对一个对象数组进行排序时, Arrays.sort方法需要一个Comparator接口实例来指定排序的规则, 假设有一个Person类, 含有String nameint age两个成员变量 :

public class Person {
    private String name;
    private int age;
    //省略构造器, toString和get set方法
}

//使用传统代码对Person[] 数组进行排序

public class Demo05Comparator {
    public static void main(String[] args) {
    // 本来年龄乱序的对象数组
    Person[] array = { new Person("古力娜扎", 19), new Person("迪丽热巴",
18), new Person("马尔扎哈", 20) };
    // 匿名内部类
    Comparator<Person> comp = new Comparator<Person>() {
        @Override
        public int compare(Person o1, Person o2) {
            return o1.getAge() - o2.getAge();
        }
    };
    Arrays.sort(array, comp); // 第二个参数为排序规则,即Comparator接口实例
    for (Person person : array) {
        System.out.println(person);
        }
    }
}

只有参数和方法体才是关键, 其他都是多余的

Lambda写法 :

public class Demo06ComparatorLambda {
    public static void main(String[] args) {
        Person[] array = {
            new Person("古力娜扎", 19),
            new Person("迪丽热巴", 18),
            new Person("马尔扎哈", 20) };
        Arrays.sort(array, (Person a, Person b) -> {
            return a.getAge() - b.getAge();
        });

        for (Person person : array) {
            System.out.println(person);
        }
    }
}
3. 5 Lambda表达式省略格式

省略规则 :

在Lambda标准格式的基础上, 使用省略写法的规则为 :

  1. 小括号内参数的类型可以省略
  2. 如果小括号内有且仅有一个参, 则小括号可以省略
  3. 如果大括号内有且仅有一个语句, 则无论是否有返回值, 都可以省略大括号, return关键字及语句分号

原则就是 : 可推导出来的就可以省略

Lambda强调的是”做什么”而不是”怎么做”, 所以凡是可以推导得出的信息都可以省略, 例如上例还可以使用Lambda的省略写法.

//Runnable接口简化:
() -> System.out.println("多线程任务执行!");
//Comparator接口简化:
Arrays.sort(array, (a, b)) -> a.getAge() - b.getAge());
4. Lambda的前提条件
  1. 使用Lambda必须具有接口, 且要求接口中有且仅有一个抽象方法, 无论是JDK内置的Runnable, Comparator接口还是自定义的接口, 只有当接口中的抽象方法存在且唯一时, 才可以使用Lambda.
  2. 使用Lambda必须具有接口作为方法参数, 也就是方法的参数或局部变量必须为Lambda对应的接口类型, 才能使用Lambda作为该接口的实例.
  3. 必须支持上下文推导,要能够推导出来Lambda表达式表示的是哪个接口中的内容。 可以使用接口当做参数,然后传递Lambda表达式(常用) 也可以将Lambda表达式赋值给一个接口类型的变量。

有且仅有一个抽象方法的接口, 称为函数式接口.

public class Demo05BeforeLambda {
    //使用接口当做参数
    public static void method(MyInterface m) {//m = s -> System.out.println(s)
        m.printStr("HELLO");
    }

    public static void main(String[] args) {
        //使用接口当做参数,然后传递Lambda表达式。
        //method(s -> System.out.println(s));

        //使用匿名内部类方式创建对象
        /*
        MyInterface m = new MyInterface() {
            @Override
            public void printStr(String str) {
                System.out.println(str);
            }
        };
        */

        MyInterface m = str -> System.out.println(str);
        m.printStr("Hello");
    }
}

二. 函数式接口

1. 概述

函数式接口在Java中是指, 有且仅有一个抽象方法的接口.

函数式接口, 适用于函数式编程场景的接口, 而Java中的函数式编程体现就是Lambda, 所以函数式接口就是可以适用于Lambda使用的接口, 只有确保接口中有且仅有一个抽象方法, Java中的Lambda才能顺利的进行推导.

格式 :

只要确保接口中有且仅有一个抽象方法即可 :

修饰符 interface 接口名称 { 
    public abstract 返回值类型 方法名 (参数列表);
    //其他非抽象方法的内容
}

由于接口当中的抽象方法public abstract 是可以省略的, 所以定义一个函数式接口很简单,

public interface MyFunctionalInterface { 
    void myMethod();
}
2. 自定义函数式接口

对于刚刚定义好的MyFunctionalInterface函数式接口, 典型的引用场景就是作为方法的参数 :

public class FunctionalInterfaceTest {
    //使用自定义的函数式接口作为方法参数
    private static void doSomething (MyFunctionalInterface inter) {
        inter.myMethod();//调用自定义的函数式接口方法
    }

    public static void main(String[] args) {
        //调用使用函数式接口的方法
        doSomething (() -> System.out.println("Lambda执行啦"));
    }
}
3. 接口注解

@Override注解的作用类似, Java8中专门为函数式接口引入了一个新的注解: @FunctionalInterface. 该注解可用于一个接口的定义上 :

@FunctionalInterface
public interface MyFunctionalInterface {
    void myMethod();
}

一旦使用该注解来定义接口, 编译器会强制检查该接口是否确实有且仅有一个抽象方法, 否则会报错. 不过, 就算不使用它, 只要满足函数式接口的定义, 这仍然是一个函数式接口, 使用起来是一样的.

4. 常用的函数式接口

JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景, 他们主要在java.util.function包中被提供, 以下介绍两个常用的函数式接口.

  1. Consumer接口
  2. Predicate接口
4.1 Consumer接口

JDK8的时候,提供java.util.function包,这个包下有大量的函数式接口。

其中有一个接口叫做Consumer,这个接口可以看成一个消费者,可以去消费(使用)一个数据。

抽象方法:

void accept(T t): 对参数t进行使用

public class Demo01Consumer {
    //定义方法,使用函数式接口Consumer当做方法参数
    public static void method(Consumer<String> c) {
        //调用accept方法,消费使用一个数据。
        c.accept("hello");
    }

    public static void main(String[] args) {
        //调用method方法
        method(new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        });

        //调用method方法,传递Lambda表达式。
        method(s -> System.out.println(s));
    }
}
4.2 Predicate接口

有时候我们需要对某种类型的数据进行判断, 从而得到一个boolean值结果, 这时候可以使用java.util.function.Predicate接口.

这个函数式接口可以对一个数据进行判断,判断是否符合要求抽象方法:

| 修饰符 | 返回值 | 方法名 | 参数列表 | 作用 | | ————— | ——- | —— | ——– | ———— | | public abstract | boolean | test | ( T t ) | 用于条件判断 |

判断参数t是否符合规则,如果符合规则返回true。

//1. 练习: 判断字符串长度是否大于5
//2. 练习: 判断字符串是否包含"H"
public class PredicateTest {
    private static void method(Predicate<String> predicate, String str) {
        boolean veryLong = predicate.test(str);
        System.out.println("字符串很长吗" + veryLong);
    }

    public static void main(String[] args) {
        method(s -> s.length() > 5, "HelloWorld");
    }
}

条件判断的标准是传入的Lambda表达式逻辑, 只要字符串长度大于5就认为很长.


三. Stream流

Java8中, 得益于Lambda所带来的函数式编程, 引入了一个全新的Stream概念, 用于解决已有集合类库既有的弊端.

1. Stream流的初体验
/*
    要求:
        1. 首先筛选所有姓张的人;
        2. 然后筛选名字有三个字的人;
        3. 最后进行对结果进行打印输出。
 */
@SuppressWarnings("all")
public class Demo01PrintList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张强");
        list.add("张三丰");
        //1. 首先筛选所有姓张的人;
        List<String> zhangList = new ArrayList<>();
        for (String s : list) {
            if(s.startsWith("张")) {
                zhangList.add(s);
            }
        }

        //2. 然后筛选名字有三个字的人;
        List<String> threeList = new ArrayList<>();
        for (String s : zhangList) {
            if (s.length() == 3) {
                threeList.add(s);
            }
        }

        //3. 最后进行对结果进行打印输出。
        for (String s : threeList) {
            System.out.println(s);
        }
        System.out.println("=====================================");
        //Stream流的初体验
        list.stream()
            .filter(s -> s.startsWith("张"))
            .filter(s -> s.length() == 3)
            .forEach(s -> System.out.println(s));
    }
}
2. 获取流的方式
2.1 单列集合获取流的方式

在Java中,Stream表示流,Stream是一个接口类型,后面我们使用的流都是Stream这个接口的实现类。获取Stream流的两种方式:

  1. 通过Collection中的方法获取流(单列集合获取流的方式)
  2. 通过数组获取流

如果要通过单列集合获取流,那么可以调用集合的stream方法

Stream stream():获取集合对应的流

Stream stream = list.stream();

public class Demo02CollectionGetStream {
    public static void main(String[] args) {
        //创建单列集合
        List<String> list = new ArrayList<>();
        //向集合添加元素
        list.add("aa");
        list.add("bb");
        list.add("cc");
        //调用集合的stream方法,获取流
        Stream<String> stream = list.stream();

        //将流调用toArray方法转成数组,再借助数组的工具类将数组的内容打印。
        System.out.println(Arrays.toString(stream.toArray()));
    }
}
2.2 通过数组获取流

如果要通过数组获取Stream流,有两种方式 :

  1. 通过Stream中的静态方法of获取(记住)
    static Stream of(T... values): 根据数组或多个元素获取流。
  2. 通过Arrays工具类中的静态方法stream获取
    static Stream stream(T[] array): 根据数组获取流。
public class Demo03ArrayGetStream {
    public static void main(String[] args) {
        //1. 通过Stream中的静态方法of获取
        String[] strArr = {"aa", "bb", "cc"};
        //static <T> Stream<T> of(T... values): 根据数组或多个元素获取流。
        //Stream<String> stream = Stream.of(strArr);
        Stream<String> stream = Stream.of("hello", "java", "world");
        //将流进行输出
        System.out.println(Arrays.toString(stream.toArray()));


        //2. 通过Arrays工具类中的静态方法stream获取
        Stream<String> stream2 = Arrays.stream(strArr);
        //将流输出
        System.out.println(Arrays.toString(stream2.toArray()));//[aa, bb, cc]
    }
}
3. Stream中的方法

这些方法可以被分成两种:

  • 终结方法:返回值类型不再是Stream 接口自身类型的方法,因此不再支持类似StringBuilder 那样的链式调 用。本小节中,终结方法包括countforEach 方法。
  • 非终结方法:返回值类型仍然是Stream 接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为非终结方法。)

以下方法, 方法种类为终结的方法不再支持链式调用.

修饰符 返回值 方法名 参数列表 作用 方法种类
public void forEach ( Consumer action ) 逐一处理 终结
public Stream filter (Predicate predicate ) 过滤 终结
public long count ( ) 统计个数 函数拼接
public Stream limit ( long maxSize ) 取用前几个 函数拼接
public Stream skip ( long n ) 跳过前几个 函数拼接
public static Stream concat ( Stream a, Stream b ) 组合 函数拼接
3.1 forEach / forEachOrdered

forEach与增强 for不同, 该方法并不能保证元素的逐一消费动作在流中是被有效执行的

该方法接收一个Consumer接口函数, 会将每一个流元素交给该函数进行处理.


public class Drafts2 {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        set.add("捣乱黄");
        set.add("捣乱靛");
        set.add("捣乱灰");
        set.add("捣乱红");
        set.add("捣乱王");
        set.stream().forEach((str) -> System.out.println(str));//lambda表达式
    }
}
捣乱靛
捣乱王
捣乱黄
捣乱红
捣乱灰
3.2 filter 过滤

可以通过filter方法将一个流转换成另一个子集流.

该接口接收一个Predicate函数式接口, 作为筛选条件.

public class Draft3 {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("孙悟空", "哪吒", "邋遢大王", "捣乱黄", "捣乱绿", "捣乱黑");
          stream
                .filter((str) -> str.length() == 3)
                .forEach((str) -> System.out.println(str));
    }
}
孙悟空
捣乱黄
捣乱绿
捣乱黑
3.3 统计个数

流中提供了count方法来数一数其中元素的个数, 该方法是终结方法.

public class Drafts4 {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap();
        map.put("张三", 23);
        map.put("李四", 25);
        map.put("王五", 25);
        map.put("赵六", 26);

        Set<Map.Entry<String, Integer>> set = map.entrySet();
        Stream<Map.Entry<String, Integer>> stream = set.stream();

        /* Stream<Map.Entry<String, Integer>> result =

            stream        //可以省略数据类型
              .filter((Map.Entry<String, Integer> e) -> e.getValue() == 25);

        System.out.println(result.count());
`       */

        System.out.println(stream.filter((Map.Entry<String, Integer> e) -> e.getValue() == 25).count());
    }
}
3.4 取用 / 跳过

limit方法可以对流进行截取, 得到只取用前n个元素的流

skip方法可以跳过前几个元素, 得到截取后的流

public class Drafts5 {
    public static void main(String[] args) {
        Stream<Object> stream = Stream.of("捣乱王", "捣乱黑", "捣乱绿", "捣乱红");
        stream.skip(1).limit(2).forEach((s) -> System.out.println(s));
    }
}
捣乱黑
捣乱绿
3.5 concat 组合

如果有两个流, 希望合为一个流, 那么可以使用Stream接口的==静态方法==concat

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b):: 把参数列表中的 两个Stream流对象a和b,合并成一个新的Stream流对象

public class Drafts6 {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("捣乱王");
        Stream<String> stream1 = Stream.of("捣乱绿");
        Stream.concat(stream, stream1).forEach((s) -> System.out.println(s));
    }
}
捣乱王
捣乱绿
Stream 综合案例

现在有两个ArrayList 集合存储队伍当中的多个成员姓名,要求使用传统的for循环(或增强for循环)依次进行以下 若干操作步骤:

  1. 第一个队伍只要名字为3个字的成员姓名;
  2. 第一个队伍筛选之后只要前3个人;
  3. 第二个队伍只要姓张的成员姓名;
  4. 第二个队伍筛选之后不要前2个人;
  5. 将两个队伍合并为一个队伍;
  6. 打印整个队伍的姓名信息。

两个队伍(集合)的代码如下:

public class Demo21ArrayListNames {
    public static void main(String[] args) {
        List<String> one = new ArrayList<>();
            one.add("迪丽热巴");
            one.add("宋远桥");
            one.add("苏星河");
            one.add("老子");
            one.add("庄子");
            one.add("孙子");
            one.add("洪七公");

            List<String> two = new ArrayList<>();
            two.add("古力娜扎");
            two.add("张无忌");
            two.add("张三丰");
            two.add("赵丽颖");
            two.add("张二狗");
            two.add("张天爱");
            two.add("张三");
            // ....

        Stream<String> streamOne = one.stream().filter(s->s.length()==3).limit(3);
        Stream<String> streamTwo = two.stream().filter(s ->s.startsWith("张")).skip(2);
        Stream.concat(streamOne, streamTwo).forEach(s -> System.out.println(s));
    }
}

文章作者: jackey
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 jackey !
评论
  目录