java函数式编程

函数式接口

什么是函数式接口  

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

  语法糖:是指使用更加方便,但是原理不变的代码语法。比如说在遍历集合的时候用的for-each语法其实就是对迭代器的封装,这就是一个语法糖。从应用层面上来讲,Java中的Lambda可以被当做是匿名内部类的语法糖,但是二者在原理上不同。

函数式接口格式

  确保接口中有且仅有一个抽象方法,但是可以有默认、静态方法:

@FunctionalInterface
public interface MyFunctionInterface {

    //抽象方法
    public abstract void method();

    //默认方法
    default void method1(){

    }
    //静态方法
    static void method2(){

    }
}

  加上@FunctionalInterface注解的意思就是规范这个接口是一个函数式接口,如果这个接口违反了“接口中有且仅有一个抽象方法”这个规范,那么这个接口就会编译失败。

函数式接口的使用

  函数式接口一般可以作为方法的参数和返回值类型:

public class MyFunctionInterfaceImpl implements MyFunctionInterface{
    @Override
    public void method() {
        System.out.println("method");
    }
}
public class UseFunctionInterface {

    public static void show(MyFunctionInterface myFunctionInterface){
        myFunctionInterface.method();
    }

    public static void main(String[] args) {

        //普通掉用,传入这个接口的实现类
        show(new MyFunctionInterfaceImpl());

        //传递接口的匿名内部类
        show(new MyFunctionInterface() {
            @Override
            public void method() {
                System.out.println("method111");
            }
        });

        //作为一个函数式接口,我们可以使用Lambda表达式
        show(()-> {
            System.out.println("method22222");
        });

        //简化Lambda表达式,当Lambda表达式大括号中有且仅有一条语句可以省略大括号
        show(()->System.out.println("method3333"));


    }
}

函数式编程

  Java在兼顾面向对象特性的基础上,通过Lambda表达式与方法引用等,为开发者打开了函数式编程的大门。

Lambda延迟执行特性

  假设我们要去打印一条拼接的日志消息,我们正常情况的代码如下:

/**
 * 我们常用的方法调用
 */
public class LoggerDemo1 {

    public static void log(int level, String message){
        if (level == 1) {
            System.out.println(message);
        }
    }

    public static void main(String[] args) {
        String message1 = "hello ";
        String message2 = "world ";
        String message3 = "lambda";
        //这个方法无论传的level是什么  ‘message1+message2+message3’ 这个拼接字符串的操作都会执行
        //但是实际情况当中,当level不是1的时候,这个拼接字符串的操作是不需要进行的
        log(1,message1+message2+message3);
    }
}

  分析这个方法的执行过程,我们可以看到,无论传的level是什么 ,message1+message2+message3这个拼接字符串的操作都会执行,但是实际情况当中,当level不是1的时候,这个拼接字符串的操作是不需要执行的。

  因此我们可以做出如下优化,首先创建一个持有消息的对象,并提供拼接日志消息的方法:

/**
 * 创建一个持有消息的对象,并提供拼接日志消息的方法
 */
public class MessageHolder {

    String[] messages;

    public MessageHolder(String... messages) {
        this.messages = messages;
    }

    public String buildMessage(){
        System.out.println("start build message");
        StringBuilder res = new StringBuilder();
        for (String message : messages) {
            res.append(" ").append(message);
        }
        return res.toString();
    }
}

   将持有日志消息的MessageHolder对象当成一个参数传入,只有当 level 为1的时候,才去调用拼接字符串的方法:

/**
 *  对上一个操作的优化,
 *  我们把字符串封装到 MessageHolder 中,并在 MessageHolder 中提供一个拼接字符串的方法
 *  然后将MessageHolder当成一个参数传入,只有当 level 为1的时候,才去调用拼接字符串的方法
 */
public class LoggerDemo2 {

    public static void log(int level,MessageHolder messageHolder){
        if (level == 1) {
            System.out.println(messageHolder.buildMessage());
        }
    }

    public static void main(String[] args) {
        String message1 = "hello ";
        String message2 = "world ";
        String message3 = "lambda";
        log(1,new MessageHolder(message1,message2,message3));
    }

}

  执行后可见,只有当level传入为1的时候,才会去拼接字符串,从而增加了代码的执行效率。但是达到同样的效果下,我们可以使用Lambda对代码进行重新优化。

  首先,创建一个函数式接口,提供一个方法,返回值为拼接好的字符串:

@FunctionalInterface
public interface MessageBuilder {
    String buildMessage();
}

  将函数式接口作为参数,用户可以使用匿名内部类的方式去实现拼接字符串的方法:

/**
 * 使用函数式接口,继续对代码进行优化,可以实现Demo同样的延迟调用的结果
 *
 * 但是这种方法看起来更加简单明了,而且代码量也显著减少
 *
 */
public class LoggerDemo3 {

    public static void log(int level,MessageBuilder messageBuilder){
        if (level == 1) {
            System.out.println(messageBuilder.buildMessage());
        }
    }

    public static void main(String[] args) {
        String message1 = "hello ";
        String message2 = "world ";
        String message3 = "lambda";
        log(1, () -> message1 + message2 + message3);
    }

}

  执行效果与第二次优化相同,但是相对于第二次优化,通过函数式接口进行的优化看起来更加简单明了,而且代码量也得到了显著的减少。因此可以看到Lambda编程天生就带有延迟执行的特性。

Lambda表达式练习

函数式接口作为参数

  我们之前应该已经学过了多线程相关的知识,可以发现Runnable就是一个函数式接口,我们可以通过实现它去创建一个线程。现要求写一个启动线程的方法,将Runnable作为此方法的参数,在这个方法内部启动这个线程并打印出线程的名称。

/**
 * 练习:
 *
 * Runnable也是一个函数式接口,
 * 这里我们写个例子,写一个启动线程的方法,将Runnable当做方法的参数,在方法内部启动这个线程
 */
public class UseFunctionInterfaceAsParam {

    public static void startThread(Runnable runnable){
        new Thread(runnable).start();
    }

    public static void main(String[] args) {

        //使用匿名内部类
        startThread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " is running ");
            }
        });

        //使用Lambda表达式
        startThread(()->{
            System.out.println(Thread.currentThread().getName() + " is running ");
        });

        //简化Lambda表达式,当重写的方法只有一行代码语句的时候,可以省略大括号
        startThread(()-> System.out.println(Thread.currentThread().getName() + " is running "));
    }

}

函数式接口作为返回值

  Comparator作为一个常用的比较器,它也是一个函数式接口。在数组工具ArraysStream流以及其他有序集合中都封装了排序方法,这些排序方法都是以Comparator比较器为参数。

  现要求写一个方法返回一个比较器,然后将返回的比较器作为数组排序方法的参数传入,实现Integer类型数组的排序功能。

import java.util.Arrays;
import java.util.Comparator;

public class UseFunctionInterfaceAsResult {

/*    public static Comparator<Integer> getComparator(){
        //直接返回一个匿名内部类
        return new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1 - o2;
            }
        };
    }*/

/*    public static Comparator<Integer> getComparator(){
        //使用Lambda表达式
        return (Integer o1,Integer o2)->{
            return o1-o2;
        };
    }*/

    public static Comparator<Integer> getComparator(){
        //对Lambda表达式进行简化
        //由于compare方法的泛型已经在 getComparator 方法的返回值中已经指定,因此我们可以省略掉 参数类型
        //由于 大括号内只有一行代码,我们可以 省略掉大括号 和 return
        return (o1,o2)->o1-o2;
    }

/*    public static Comparator<Integer> getComparator(){

        //当然 idea还会提示我们使用这个方法
        // 这是因为Comparator 中已经默认已经有了一个比较int类型的方法,所以这里可以直接掉用这个方法返回
        return Comparator.comparingInt(o -> o);
    }*/

/*
    public static Comparator<Integer> getComparator(){
                //  ::的格式我们之后讲
        return Integer::compare;
    }
*/

    public static void main(String[] args) {
        Integer[] arr = {1,5,3,2,9,8,4,7,6};
        Arrays.stream(arr).forEach(System.out::println);
        Arrays.sort(arr,getComparator());
        System.out.println("=============================");
        Arrays.stream(arr).forEach(System.out::println);
    }
}

Lambda表达式省略格式

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

  • 小括号内参数的类型可以省略;

  • 如果小括号内有且仅有一个参,则小括号可以省略;

  • 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。

常用的函数式接口

  在JDK中,为我们提供了大量的函数式接口以丰富Lambda的典型使用场景,他们主要在java.util.function包下。

Supplier

  Supplier接口仅包含一个无参的方法:T get()。用来获取一个通过泛型指定类型的对象数据。下面我们定义一个方法,用来返回一个String类型的参数并打印出来。

/**
 * Supplier
 * 是一个生产型接口,指定泛型,get方法机会返回相应类型的数据
 */
public class SupplierTest {

    /**
     * 指定返回String类型数据
     */
    private static String getString(Supplier<String> supplier){
        return supplier.get();
    }

    public static void main(String[] args) {

        String hello = getString(() -> "hello");
        System.out.println(hello);

    }
}

练习:

  定义一个方法,参数使用Supplier接口,这个方法返回一个int数组中的最大值。提示:即Supplier的泛型指定为Integer类型。

/**
 * 使用Supplier接口作为方法参数,求int数组最大值
 */
public class UseSupplier {

    //定义一个方法,用于获取int数组中最大值,泛型为  Integer
    public static int getMax(Supplier<Integer> supplier){
        return supplier.get();
    }


    public static void main(String[] args) {
        int[] intArr = {1,5,6,9,8,4,2};
        //通过数组工具类Arrays将数组转换为stream,使用stream获取数组最大值,
        // stream后面会讲到,这里先接触一下
        //也可以通过遍历数组的方式去获取一个最大值,有兴趣自己实现
        int maxNum = getMax(()->Arrays.stream(intArr).max().getAsInt());

        System.out.println(maxNum);
    }
}

Consumer

  Consumer接口与Supplier接口正好相反,他是一个消费型接口,用来消费数据,数据类型同样通过泛型指定。它定义了两个接口:

  • void accept(T t):是一个抽象方法,消费一个泛型所指定类型的数据。
/**
 * Consumer
 * 它是一个消费型接口,使用accept方法消费指定类型的数据
 */
public class ConsumerAcceptTest {



    public static void handleName(String name, Consumer<String> consumer){
        consumer.accept(name);
    }

    //消费一个String类型的姓名,打印出这个姓名
    public static void main(String[] args) {

        //使用匿名内部类方式
        handleName("托塔李天王", new Consumer<String>() {
            @Override
            public void accept(String name) {
                System.out.println(name);
            }
        });

        //使用lambda写法
        handleName("托塔李天王",(String name)->{
            System.out.println(name);
        });

        //只有一个参数 一般省略类型和小括号
        handleName("托塔李天王",name->{
            System.out.println(name);
        });

        //只有一行语句 省略花括号
        handleName("托塔李天王",name-> System.out.println(name));

        //参数与处理方法入参一样 且只有一句,可以使用::语法
        handleName("托塔李天王",System.out::println);

    }

}
  • default Consumer<T> andThen(Consumer<? super T> after):如果一个方法的参数和返回值都是Consumer类型,那么这个方法的返回可以通过这个方法去接收另外一个Consumer对象,实现类似一个处理链的效果。即把多个ConsumerandThen方法串起来运行 ,我们可以看一下他的源码实现:
default Consumer<T> andThen(Consumer<? super T> after) {
    Objects.requireNonNull(after);
    //掉用本类中的accept方法处理t这个数据之后
    //继续掉用after中的accept方法去处理t这个数据
    //就相当于一个数据处理链的一个效果
    return (T t) -> { accept(t); after.accept(t); };
}

使用:

/**
 *   使用Consumer的andThen方法实现一个类似数据处理链的效果
 */
public class ConsumerAndThenTest {

    public static void method(String s, Consumer<String> consumer1,Consumer<String> consumer2){
        //使用consumer1连接consumer2
        //先执行consumer1的accept方法,再执行consumer2的accept方法
        consumer1.andThen(consumer2).accept(s);
    }

    public static void main(String[] args) {
        method("Hello",
                //转换为大写并输出
                t-> System.out.println(t.toUpperCase()),
                //转换为小写并输出
                t-> System.out.println(t.toLowerCase()));
    }
}

练习:

  现在需要实现一个组装电脑的产品线,产品零件由客户提供,然后通过这个产品线对零件按照指定的加工顺序进行组装。规定的组装过程:主板->电源->CPU->显卡->散热器->机箱

/**
 * 使用Consumer接口,
 *
 * 实现一个可以组装电脑的产品链方法,
 * 通过这个方法,可以根据客户提供的产品零件,对产品进行按一下顺序的组装
 * 主板->电源->CPU->显卡->散热器->机箱
 *
 */
public class UseConsumer {


    /**
     *
     * @param productType 产品类型
     * @param computerSupplier 提供产品零件
     * @param assemblies 组装流程
     */
    @SafeVarargs
    public static void assemblyComputer(String productType, Supplier<Consumer<String>> computerSupplier, Consumer<String>... assemblies){
        Consumer<String> computer = computerSupplier.get();
        for (Consumer<String> assembly : assemblies) {
            computer = computer.andThen(assembly);
        }
        computer.accept(productType);
    }

    public static void main(String[] args) {
        assemblyComputer("笔记本电脑",
                ()-> productType-> System.out.println("准备"+productType+"零件"),
                computer-> System.out.println("组装"+computer+"的主板"),
                computer-> System.out.println("组装"+computer+"的电源"),
                computer-> System.out.println("组装"+computer+"的CPU"),
                computer-> System.out.println("组装"+computer+"的显卡"),
                computer-> System.out.println("组装"+computer+"的机箱"));
    }
}

Predicate

  对指定数据类型的数据进行判断,并返回判断的结果,可以看到Stream中的filter方法的入参就是它。

  • boolean test(T t):对数据进行判断,返回boolean类型。
  • default Predicate<T> and(Predicate<? super T> other):默认方法,与判断,相当于&&
  • default Predicate<T> or(Predicate<? super T> other):默认方法,或判断,相当于||
  • default Predicate<T> negate():默认方法,非判断,相当于!
/**
 * Predicate 对指定数据类型的数据进行判断,并返回判断的结果
 */
public class PredicateTest {

    public static void main(String[] args) {
        //测试字符串长度是否大于6
        System.out.println(testPredicate("1234567", str -> str.length() > 6));
        //测试字符串是否  长度大于6并且包含字母a
        System.out.println(testAnd("12345a67", str -> str.length() > 6,str->str.contains("a")));
        //测试字符串是否  长度大于或者包含字母a
        System.out.println(testOr("1234567", str -> str.length() > 6,str->str.contains("a")));
        //测试字符串长度是否不大于6
        System.out.println(testNegate("1234567", str -> str.length() > 6));

    }

    /**
     * 测试test方法
     */
    public static boolean testPredicate(String str,Predicate<String> predicate){
        return predicate.test(str);
    }

    /**
     * 测试and方法
     */
    public static boolean testAnd(String str,Predicate<String> predicateLength,Predicate<String> predicateContains){
        return predicateLength.and(predicateContains).test(str);
    }

    /**
     * 测试or方法
     */
    public static boolean testOr(String str,Predicate<String> predicateLength,Predicate<String> predicateContains){
        return predicateLength.or(predicateContains).test(str);
    }

    /**
     * 测试negate方法
     */
    public static boolean testNegate(String str,Predicate<String> predicate){
        return predicate.negate().test(str);
    }


}

练习:

  找出下面一段字符串数组中名字长度为四位,并且性别为女的,将其姓名放入List集合中并输出。

/**
 * 找出下面一段字符串数组中名字长度为四位,并且性别为女的,将其姓名放入`List`集合中并输出。
 */
public class UsePredicate {

    public static void main(String[] args) {

        String[] heroArr = {"阿狸,女","布里茨,男","九尾妖狐,女","崔丝塔娜,女","拉克丝,女","艾瑞莉娅,女","马尔扎哈,男"};

        List<String> femaleHero = filter(heroArr,
                hero->hero.split(",")[0].length()==4,
                hero->"女".equals(hero.split(",")[1]));

        System.out.println(femaleHero);
    }

    public static List<String> filter(String[] heroArr, Predicate<String> predicateLength,Predicate<String> predicateSex){
        List<String> names = new ArrayList<>();
        for (String hero : heroArr) {
           if (predicateLength.and(predicateSex).test(hero)){
               names.add(hero.split(",")[0]);
           }
        }
        return names;
    }

}

Function

  java.util.function.Function<T,R>,将一个类型的数据转换为另一个类型。我们可以看到这个接口需要指定两个泛型TR,T就表示要转换的源类型,R表示要转换的目标类型。

  • R apply(T t):通过参数类型和返回类型就可以知道,我们可以通过这个方法将T类型转换成R类型。

  • default <V> Function<T, V> andThen(Function<? super R, ? extends V> after):与ConsumerandThen方法一样,将多个apply操作串起来。

  • default <V> Function<V, R> compose(Function<? super V, ? extends T> before):与andThen方法类似,不同的是该方法串起来的执行顺序是相反的。

/**
 * Function 将一个类型的数据转换为另一个类型。我们可以看到这个接口需要指定两个泛型`T`和`R`,`T`就表示要转换的源类型,`R`表示要转换的目标类型。
 */
public class FunctionTest {

    public static void main(String[] args) {
        //将String类型转换成Integer类型
        Integer integer = testApply("22", Integer::parseInt);
        System.out.println(integer);

        //将String类型转换成Integer后加10 再转换成String类型返回
        String s = testAndThen("22", strData -> Integer.parseInt(strData) + 10, String::valueOf);
        System.out.println(s);
        
        //将Integer类型转换成String后加10 再转换成Integer类型返回
        Integer integer1 = testCompose(22, Integer::parseInt, str -> str+""+10);
        System.out.println(integer1);

    }

    /**
     * 测试apply方法
     */
    public static Integer testApply(String str, Function<String,Integer> function){
        return function.apply(str);
    }

    /**
     * 测试andThen方法
     */
    public static String testAndThen(String str, Function<String,Integer> function1,Function<Integer,String> function2){
        return function1.andThen(function2).apply(str);
    }
    /**
     * 测试compose方法
     */
    public static Integer testCompose(Integer intData, Function<String,Integer> function1,Function<Integer,String> function2){
        return function1.compose(function2).apply(intData);
    }



}

练习:

  要过年了,把下面字符串中张三的年龄加一岁: “张三,22”。

/**
 * 要过年了,把下面字符串中张三的年龄加一岁:  "张三,22"
 */
public class UseFunction {


    public static void main(String[] args) {
        String zsInfo = "张三,22";
        String addAge = addAge(zsInfo,
                info -> Integer.parseInt(info.split(",")[1])+1,
                age -> zsInfo.split(",")[0] + "," + age);
        System.out.println(addAge);


    }

    public static String addAge(String info, Function<String,Integer> addAgeFunction,Function<Integer,String> splicingInfoFunction){
        return addAgeFunction.andThen(splicingInfoFunction).apply(info);
    }


}

Stream流

  在我们研究 Java Stream API 之前,让我们看看为什么需要它。假设我们要遍历一个整数列表并找出所有大于 10 的整数的总和。在 Java 8 之前,我们的方法是:

public class BeforeStartStream {

    public static void main(String[] args) {
        int sumIterator = sumIterator(StreamDataUtils.intDataList());
        System.out.println(sumIterator);
    }

    /**
     *遍历一个整数列表并找出所有大于 10 的整数的总和
     */
    private static int sumIterator(List<Integer> list) {
        Iterator<Integer> it = list.iterator();
        int sum = 0;
        while (it.hasNext()) {
            int num = it.next();
            if (num > 10) {
                sum += num;
            }
        }
        return sum;
    }
}

  上述方法存在三个主要问题:

  • 我们只想知道整数的总和,但我们还必须关心去如何进行迭代。
  • 该程序本质上是顺序的,我们无法轻松地并发执行此操作。
  • 一个简单的任务我们写了太多的代码。

  为了解决上述所有缺点,引入了 Java 8 Stream API。我们可以使用Java Stream API来实现内部迭代,这样用起来就十分容易。Stream的内部迭代提供了多种功能,例如顺序和并行执行、基于给定标准的过滤、映射等。
  
  大多数 Java 8 Stream API 方法参数都是函数式接口,因此lambda 表达式与它们配合得很好。让我们看看如何使用 Java Streams 在单行语句中编写上述逻辑。

/**
 *使用Stream 遍历一个整数列表并找出所有大于 10 的整数的总和
 */
private static int sumStream(List<Integer> list) {
    return list.stream().filter(i -> i > 10).mapToInt(i -> i).sum();
}

  首先,我们将研究Java 8 Stream API的核心概念,然后我们将通过一些示例来理解最常用的方法。

初始化数据类

  为了减少代码,封装了一个提供数据的类:

/**
 * Stream数据测试帮助类,提供演示数据
 */
public class StreamDataUtils {

    private StreamDataUtils() {}

    public static List<Integer> intDataList(){
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(6);
        list.add(13);
        list.add(18);
        list.add(20);
        list.add(2);
        list.add(7);
        return list;
    }
    public static List<String> stringDataList(){
        List<String> list = new ArrayList<>();
        list.add("雷欧");
        list.add("赛文");
        list.add("迪迦");
        list.add("泰罗");
        list.add("初代");
        return list;
    }

    public static Stream<String> stringDataStream(){
        return stringDataList().stream();
    }

    public static Stream<Integer> intDataStream(){
        return intDataList().stream();
    }


    public static List<UseStream.Employee> group1List(){
        List<UseStream.Employee> group1 = new ArrayList<>();
        group1.add(new UseStream.Employee("张三",11));
        group1.add(new UseStream.Employee("李四",12));
        group1.add(new UseStream.Employee("王五",60));
        group1.add(new UseStream.Employee("迪迦",1123651));
        group1.add(new UseStream.Employee("泰罗",321325));
        group1.add(new UseStream.Employee("雷欧",1565));
        group1.add(new UseStream.Employee("阿狸",2125));
        group1.add(new UseStream.Employee("猴子",500));
        group1.add(new UseStream.Employee("光辉",600));
        group1.add(new UseStream.Employee("小炮",700));
        group1.add(new UseStream.Employee("蚂蚱",8000));
        group1.add(new UseStream.Employee("空虚行者",900));
        return group1;
    }

    public static List<UseStream.Employee> group2List(){
        List<UseStream.Employee> group2 = new ArrayList<>();
        group2.add(new UseStream.Employee("张三",32));
        group2.add(new UseStream.Employee("机器人",23));
        group2.add(new UseStream.Employee("提莫",44));
        group2.add(new UseStream.Employee("狗剩",34));
        group2.add(new UseStream.Employee("张全蛋",32));
        group2.add(new UseStream.Employee("兔崽子",111));
        group2.add(new UseStream.Employee("李四",323));
        group2.add(new UseStream.Employee("光辉",222));
        group2.add(new UseStream.Employee("空虚行者",2323));
        group2.add(new UseStream.Employee("薇恩",23));
        group2.add(new UseStream.Employee("大头",232));
        group2.add(new UseStream.Employee("迪迦",2132132));
        return group2;
    }
}

Optional

  Java Optional 是一个可能为空的容器对象。如果容器中存在值,则它的isPresent()方法 将返回 trueget() 方法将返回该值。他是一个中断操作,它常用的方法有:

  • Optional<T> reduce(BinaryOperator<T> accumulator):计算
  • Optional<T> min(Comparator<? super T> comparator):返回最小值
  • Optional<T> max(Comparator<? super T> comparator):返回最大值
  • Optional<T> findFirst():返回第一个数据
  • Optional<T> findAny():返回任意一个数据

Java集合和 Java Stream

  集合是按照特定数据结构存存储于内存中的一组数据,而Java Stream 是对数据进行处理计算的一种数据结构。它允许以声明性方式处理数据集合,可以把Stream流看作是遍历数据集合的一个高级迭代器。Java Stream 不存储数据,我们经常会用流去对集合进行一些流水线的操作。stream就像工厂一样,只需要把集合、命令还有一些参数灌输到 流水线 中去,就可以加工成得出想要的结果。这样的流水线能大大简洁代码,减少操作。

  Java 8 Stream 内部迭代原理有助于在某些流操作中实现延迟操作。例如,过滤、映射或重复删除数据可以延迟进行,从而实现更高的性能操作。

  Java 8 Stream 支持顺序和并行处理,并行处理对于处理大型集合数据的提高性能非常有帮助。

  所有 Java Stream API 接口和类都在 java.util.stream 包中。由于我们在集合中使用原始数据类型,例如 intlong等,会有自动装箱的操作,并且这些操作可能需要很多时间,因此有针对原始类型的特定Stream类 - IntStreamLongStreamDoubleStream

常用方法

中间操作(Intermediate Operations)

  Java Stream API中会返回新的Stream对象的操作称为中间操作。大多数情况下,这些操作本质上是延迟性的,它们生成一个新的Stream元素,并将其传递到下一个操作。中间操作不会返回最终的处理结果,即直到实际需要处理结果时才会执行这些中间操作。常用的中间操作有:

  • filter(): 过滤。
  • map(): 用于映射每个元素到对应的结果。
  • flatMap(): 把Stream中的层级结构扁平化,就是将最底层元素抽出来放到一起(可将每个元素的流分割成更细的流然后返回一个新流)。
  • distinct(): 结果去重。
  • sorted(): 对流进行排序。
  • peek(): 和map()方法差不多,map()方法可以改变返回值类型,而peek无法改变返回值类型。
  • limit(): 用于获取指定数量的流。
  • skip(): 跳过stream中的前n个元素在执行。

终端操作(Terminal Operations)

  在Java 8 Stream API中,会返回结果的操作操作被称为终端操作。一旦这些方法被掉用,它就会把Stream对象消费掉,然后我们就不能使用Stream了。终端操作操作本质上是即时的,即它们会在返回结果之前,处理Stream流中的所有数据。常用的终端操作有:

  • forEach(): 循环
  • forEachOrdered(): 主要区别在并行处理上,forEach()是并行处理的,forEachOrder()是按顺序处理的
  • toArray(): 转换成数组
  • reduce(): 对数据进行计算
  • collect(): 将原来的Stream映射为一个单元素流,然后收集,Collectors.toList(),`Collectors.toMap()等
  • min(): 返回最小值
  • max(): 返回最大值
  • count(): 返回元素数量
  • anyMatch(): anyMatch表示,判断的条件里,任意一个元素成功,返回true
  • allMatch(): allMatch表示,判断条件里的元素,所有的都是,返回true
  • noneMatch(): noneMatch跟allMatch相反,判断条件里的元素,所有的都不是,返回true
  • findFirst(): 返回第一个元素
  • findAny(): 并行情况下随机返回一个元素

创建Java Stream

  我们可以通过多种方式从数组和集合创建 Java Stream

  1. 我们可以使用 Stream.of() 从相似类型的数据创建一个流。例如,我们可以从一组 nt类型或 Integer类型的对象创建 Java Stream
Stream<Integer> stream = Stream.of(1,2,3,4);
  1. Stream.of()传入数组对象创建Stream流。注意:它不支持自动装箱,因此我们无法传递原始类型数组。
int[] intArr = {1, 2, 3, 4};
//Compile time error, Type mismatch: cannot convert from Stream<int[]> to Stream<Integer>
Stream<Integer> streamWithArr1 = Stream.of(intArr);  //错误
  1. 使用集合Collectionstream() 方法或者 parallelStream() 创建。
List<Integer> myList = new ArrayList<>();
for(int i=0; i<100; i++) myList.add(i);
//顺序 stream
Stream<Integer> sequentialStream = myList.stream();
//异步 stream
Stream<Integer> parallelStream = myList.parallelStream();
  1. 使用Stream.generate()Stream.iterate()方法创建无限流。注意:无限流不设置长度会一直执行下去,此处使用limit方法设置长度
Stream<Integer> stream1 = Stream.iterate(1, x -> x + 3);
stream1.limit(100).forEach(System.out::println);

Supplier<UUID> randomUUIDSupplier = UUID::randomUUID;
Stream<UUID> infiniteStreamOfRandomUUID = Stream.generate(randomUUIDSupplier);
infiniteStreamOfRandomUUID.limit(3).forEach(System.out::println);
  1. 使用Arrays.stream()String.chars() 方法。
LongStream is = Arrays.stream(new long[]{1,2,3,4});
IntStream is2 = "abc".chars();

将Java Stream转换为集合或数组

  1. 我们可以使用Java Streamcollect() 方法从Stream中去获取一个 List, Map 或者 Set 集合。
Stream<Integer> intStream = Stream.of(1,2,3,4);
List<Integer> intList = intStream.collect(Collectors.toList());
System.out.println(intList); //prints [1, 2, 3, 4]

intStream = Stream.of(1,2,3,4); //stream is closed, so we need to create it again
Map<Integer,Integer> intMap = intStream.collect(Collectors.toMap(i -> i, i -> i+10));
System.out.println(intMap); //prints {1=11, 2=12, 3=13, 4=14}
  1. 我们可以使用StreamtoArray() 方法去创建一个数组。
Stream<Integer> stream = Stream.of(1,2,3,4);
Integer[] intArray = stream.toArray(Integer[]::new);
System.out.println(Arrays.toString(intArray)); //prints [1, 2, 3, 4]

常用方法详解

中间操作
  • Stream filter(Predicate predicate): 可以将一个流过滤转换称为另外一个流。该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。
//过滤出大于10的数字
StreamDataUtils.intDataStream().filter(i->i>10).forEach(System.out::println);
  • Stream map(Function mapper): 将流中的元素映射到另一个流中,该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。
Stream<String> stringStream = StreamDataUtils.intDataStream().map(String::valueOf);
System.out.println(stringStream);
  • Stream limit(long maxSize):延迟方法,可以对流进行截取,只取用前n个。
StreamDataUtils.stringDataStream().limit(3).forEach(System.out::println);
  • Stream skip(long n):延迟方法,可以对流进行截取,跳过前n个。如果跳过的长度大于元素个数,则返回一个空的Stream
StreamDataUtils.intDataStream().skip(2).forEach(System.out::println);
  • static Stream concat(Stream a, Stream b): 传递两个Stream,并将这两个Stream合并成一个新的Stream
Stream<? extends Serializable> newStream = Stream.concat(StreamDataUtils.stringDataStream(), StreamDataUtils.intDataStream());
newStream.forEach(System.out::println);
  • Stream sorted(): 对Stream的元素按自然顺序进行排序。
  • Stream sorted(Comparator<? super T> comparator) :自己实现Comparator,并根据自己实现的Comparator进行排序。
Stream<String> names2 = Stream.of("aBc", "d", "ef", "123456");
List<String> reverseSorted = names2.sorted(Comparator.reverseOrder()).collect(Collectors.toList());
System.out.println(reverseSorted); // [ef, d, aBc, 123456]

Stream<String> names3 = Stream.of("aBc", "d", "ef", "123456");
List<String> naturalSorted = names3.sorted().collect(Collectors.toList());
System.out.println(naturalSorted); //[123456, aBc, d, ef]
  • Stream flatMap(Function<? super T,? extends Stream<? extends R>> mapper) :我们可以通过它去创建一个Stream对象,可以看到,参数为Function接口,即将一个类型的数据转换成另一个指定类型。
Stream<List<String>> namesOriginalList = Stream.of(
        Arrays.asList("PanKaj","Mark"),
        Arrays.asList("David", "Lisa"),
        Arrays.asList("Amit","Lesson"));
//将Stream流中的 List<String> 集合合并成一个 String stream 流
Stream<String> flatStream = namesOriginalList
        .flatMap(Collection::stream);

flatStream.forEach(System.out::println);
  • Stream flatMapToDouble(Function<? super T,? extends DoubleStream> mapper) :将T类型数据转换成DoubleStream对象。
  • Stream flatMapToInt(Function<? super T,? extends IntStream> mapper) :将T类型数据转换成IntStream对象。
  • Stream flatMapToLong(Function<? super T,? extends LongStream> mapper) :将T类型数据转换成LongStream对象。
  • Stream peek(Consumer<? super T> action) :消费Stream中的元素,即可以对数据进行使用处理。
终端操作
  • void forEach(Consumer<? super T> action): 循环,该方法接收一个 Consumer 接口函数,会将每一个流元素交给该函数进行处理。java.util.function.Consumer接口是一个消费型接口。 Consumer接口中包含抽象方法void accept(T t)`,意为消费一个指定泛型的数据。
//遍历并输出
StreamDataUtils.stringDataStream().forEach(System.out::println);
  • long count(): 他是一个终结方法,与旧集合 Collection 当中的 size 方法一样,用来统计Stream中的元素个数。
System.out.println(StreamDataUtils.intDataStream().count());
  • Optional<T> reduce(BinaryOperator<T> accumulator) :对Stream流中的元素进行计算处理,并返回一个Optional
//reduce  计算所有数据和
Stream<Integer> numbers = StreamDataUtils.intDataStream();
Optional<Integer> intOptional = numbers.reduce(Integer::sum);
//67
intOptional.ifPresent(integer -> System.out.println("Multiplication = " + integer));
  • T reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner) :定义一个初始值,计算的时候同时要处理这个初始值,直接返回计算结果。
Stream<Integer> numbers2 = StreamDataUtils.intDataStream();
Integer reduce = numbers2.reduce(2, Integer::sum);
System.out.println(reduce);
  • boolean match()相关:检查是否匹配。
//anyMatch 有一个匹配就返回true
Stream<Integer> numbers3 = StreamDataUtils.intDataStream();
System.out.println("Stream contains 4? "+numbers3.anyMatch(i -> i==4));

//allMatch 全部匹配返回true
Stream<Integer> numbers4 = StreamDataUtils.intDataStream();
System.out.println("Stream contains all elements less than 10? "+numbers4.allMatch(i -> i<10));

//noneMatch 没有一个匹配 返回true
Stream<Integer> numbers5 = StreamDataUtils.intDataStream();
System.out.println("Stream doesn't contain 10? "+numbers5.noneMatch(i -> i==10));
  • Optional<T> findFirst() :返回一个Optional,找出第一个元素。
//找到第一个以D开头的名字
Stream<String> names4 = Stream.of("PanKaj","Amit","David", "Lisa" ,"Dal");
Optional<String> firstNameWithD = names4.filter(i -> i.startsWith("D")).findFirst();
//David
firstNameWithD.ifPresent(s -> System.out.println("First Name starting with D=" + s));

Stream 的缺点

  Java 8 Stream API为我们处理集合和数组提供了很大的遍历,然后也会有一些限制。

  1. 无状态 lambda 表达式:如果使用parallelStream()并且 lambda 表达式是有状态的,则可能会导致随机响应。
/**
 * 如果运行上面的程序,会得到不同的结果,
 * 因为它取决于流的迭代方式,这里没有为并行处理定义任何顺序。
 * 如果使用顺序流,则不会出现这个问题。
 */
public class StatefulParallelStream {

    public static void main(String[] args) {

        List<Integer> ss = Arrays.asList(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15);
        List<Integer> result = new ArrayList<>();

        Stream<Integer> stream = ss.parallelStream();

        stream.peek(s -> {
            synchronized (result) {
                if (result.size() < 10) {
                    result.add(s);
                }
            }
        }).forEach( e -> {});
        System.out.println(result);
    }
}
  1. 一旦 Stream 被消费,它就不能再次被使用。比如上面的例子,我都会创建一个新的Stream,而不会去重复使用。如果重复使用就会报错,可自己尝试。
  2. Stream的学习成本比较高。

方法引用

  方法引用符:双冒号 :: 为引用运算符,而它所在的表达式被称为方法引用。如果Lambda要表达的函数方案已经存在于某个方 法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者。

  创建一个函数接口,用来打印<key ,value>对:

@FunctionalInterface
public interface KeyAndValueHolder {
    void printKeyAndValue(String key,Integer value);
}
/**
 * 方法引用
 */
public class MethodQuote {

    public static void main(String[] args) {
        test1();
        test2();
    }

    //前面讲过 Consumer 用来消费数据
    private static void accept(Consumer<String> consumer){
        consumer.accept("aaa");
    }


    private static void accept(Map.Entry<String,Integer> entry, KeyAndValueHolder keyAndValueHolder){
        keyAndValueHolder.printKeyAndValue(entry.getKey(),entry.getValue());
    }

    private static void print(String key,Integer value){
        System.out.println(key+"#"+value);
    }

    /**
     * 打印一个key value对
     */
    private static void test2() {
        Map<String,Integer> map = new HashMap<>();
        map.put("mark",33);
        map.put("xu",18);
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            accept(entry,(String key,Integer value)->print(key,value));
            accept(entry,MethodQuote::print);
        }
    }

    /**
     * 打印一个字符串
     */
    public static void test1(){
        //这里我们使用System.out 对象的 println 方法去消费 aaa 这个字符串,即打印这个字符串
        accept(str-> {
            System.out.println(str);
        });

        //通过方法引用方式
        accept(System.out::println);
    }

}

  总结规律:

  当我们所掉用处理数据的方法如:System.out.println(str)print(key,value)所需的参数与函数式接口所需要的参数相同,并且中括号中只有一句代码的时候,那么就认为这个Lambda表达式是可推导的,我们就可以使用方法引用去简化写法。

  如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式——它们都 将被自动推导。

  而如果使用方法引用,也是同样可以根据上下文进行推导。 函数式接口是Lambda的基础,而方法引用是Lambda的孪生兄弟。

通过对象名引用成员方法

/**
 * 通过对象名引用
 * 函数接口使用 Consumer
 */
public class MethodRefObjectTest {

    public static class MethodRefObject{
        public void printUpperCase(String str) {
            System.out.println(str.toUpperCase());
        }
    }

    private static void printString(Consumer<String> consumer) {
        consumer.accept("Hello");
    }
    public static void main(String[] args) {
        MethodRefObject obj = new MethodRefObject();
        printString(obj::printUpperCase);
    }


}

通过类名称引用静态方法

/**
 * 通过类名称引用静态方法
 * 比如我们有一个时间工具类,工具类的方法一般都是静态的,
 * 我们需要掉用这个工具类的parse静态方法去转换时间格式
 */
public class MethodRefStaticMethodTest {


    public static void main(String[] args) {
        accept(new Date(),date -> {
            DateUtils.parse(date);
        });

        accept(new Date(),DateUtils::parse);
    }

    public static void accept(Date date,Consumer<Date> consumer){
        consumer.accept(date);
    }

    public static class DateUtils{
        private DateUtils() {
        }
        public static void parse(Date date){
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            System.out.println(simpleDateFormat.format(date));
        }

    }

}

通过super引用成员方法

  如果存在继承关系,当Lambda中需要出现super调用时,也可以使用方法引用进行替代。首先是函数式接口:

@FunctionalInterface
public interface Greetable {
    void greet();
}

  然后是父类 Human 的内容:

public class Human {
    public void sayHello() {
        System.out.println("Hello!");
    }
}

  最后是子类 Man 的内容,其中使用了Lambda的写法:

public class Man extends Human {
    @Override
    public void sayHello() {
        System.out.println("大家好,我是Man!");
    }
    //定义方法method,参数传递Greetable接口
    public void method(Greetable g){
        g.greet();
    }
    public void show(){
        //调用method方法,使用Lambda表达式
        method(()->{
        //创建Human对象,调用sayHello方法
                new Human().sayHello();
        });
        //简化Lambda
        method(()->new Human().sayHello());
        //使用super关键字代替父类对象
        method(()->super.sayHello());

        method(super::sayHello);

    }
}

通过this引用成员方法

  this代表当前对象,如果需要引用的方法就是当前类中的成员方法,那么可以使用this::成员方法的格式来使用方 法引用。

@FunctionalInterface
public interface Richable {
    void buy();
}
public class Husband {
    private void buyHouse() {
        System.out.println("买套房子");
    }
    private void marry(Richable lambda) {
        lambda.buy();
    }
    public void beHappy() {
        marry(this::buyHouse);
    }
}

类的构造器引用

  由于构造器的名称与类名完全一样,并不固定。所以构造器引用使用 类名称::new 的格式表示。

public class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
@FunctionalInterface
public interface PersonBuilder {
    Person buildPerson(String name);
}
public class ConstructorRef {
    public static void printName(String name, PersonBuilder builder) {
        System.out.println(builder.buildPerson(name).getName());
    }
    public static void main(String[] args) {
        printName("赵丽颖", Person::new);
    }
}

数组的构造器引用

  数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。

@FunctionalInterface
public interface ArrayBuilder {
    int[] buildArray(int length);
}
public class ArrayInitRef {
    private static int[] initArray(int length, ArrayBuilder builder) {
        return builder.buildArray(length);
    }
    public static void main(String[] args) {
        int[] array = initArray(10, int[]::new);
    }

}

练习

有两组员工数据:第一组数据为数据库已存在的数据,第二组数据为用户需要新增或者修改的数据

要求:

  • 比较第一组数据和第二组数据中员工的姓名,如果第一组数据中有第二组姓名相同的数据,则这批数据是修改的员工,则对数据库进行更新操作

    如果第一组数据中没有的第二组数据,说明是新增的员工,则对这批数据进行新增操作。

  • 新增或修改玩数据库之后,只返回他们的名字,展示给客户看。

/**
 * 作业:
 * 有两组Employee员工数据:
 *      第一组数据为数据库已存在的数据
 *      第二组数据为用户需要新增或者修改的数据
 *    要求:比较第一组数据和第二组数据中员工的姓名,
 *    如果第一组数据中有第二组姓名相同的数据,则这批数据是修改的员工,则对数据库进行更新操作
 *    如果第一组数据中没有的第二组数据,说明是新增的员工,则对这批数据进行新增操作
 *
 *    新增或修改玩数据库之后,只返回他们的名字,展示给客户看
 *
 */
public class UseStream {


    public static void main(String[] args) {
        List<Employee> employees1 = StreamDataUtils.group1List();
        List<Employee> employees2 = StreamDataUtils.group2List();

        //需要新增的数据
        List<Employee> needInsertEmployee =
                employees2.stream().filter(e2 -> employees1.stream().noneMatch(e1 -> Objects.equals(e1.getName(), e2.getName()))).collect(Collectors.toList());
        System.out.println("=============需要新增的数据=================");
        needInsertEmployee.forEach(System.out::println);
        List<Employee> needUpdateEmployee =
                employees2.stream().filter(e2 -> employees1.stream().anyMatch(e1 -> Objects.equals(e1.getName(), e2.getName()))).collect(Collectors.toList());
        System.out.println("=============需要修改的数据=================");
        needUpdateEmployee.forEach(System.out::println);

        System.out.println("=============需要新增的员工的姓名=================");
        needInsertEmployee.stream().map(Employee::getName).collect(Collectors.toList()).forEach(System.out::println);

        System.out.println("=============需要修改的员工的姓名=================");
        needUpdateEmployee.stream().map(Employee::getName).collect(Collectors.toList()).forEach(System.out::println);

    }



    static class Employee {

        private String name;

        private Integer age;

        public Employee() {
        }

        public Employee(String name, Integer age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Integer getAge() {
            return age;
        }

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

        @Override
        public String toString() {
            return "Employee{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值