函数式编程

函数式编程

1、Lambda 表达式

1.1、概述

Lambda 是 JDK8 中一个语法糖。他可以对某些匿名内部类的写法进行简化。它是函数式编程思想的一个重要体现。让我们不用关注是什么对象,而是更关注我们对数据进行了什么操作。

1.2、基本格式

(参数列表) -> {代码}

例一

我们在创建线程并启动时可以使用匿名内部类的写法:

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("你知道吗 我比你想象的 更想在你身边");
    }
}).start();

可以使用 Lambda 的格式对其进行修改。修改后如下:

new Thread(() -> {
    System.out.println("你知道吗 我比你想象的 更想在你身边");
}).start();
new Thread(() -> System.out.println("你知道吗 我比你想象的 更想在你身边")).start();

例二

现有方法定义如下,其中 IntBinaryOperator 是一个接口。先使用匿名内部类的写法调用该方法。

		public static int calculateNum(IntBinaryOperator operator){
        int a = 10;
        int b = 20;
        return operator.applyAsInt(a, b);
    }

    public static void main(String[] args) {
        int i = calculateNum(new IntBinaryOperator() {
            @Override
            public int applyAsInt(int left, int right) {
                return left + right;
            }
        });
        System.out.println(i);
    }

Lambda写法:

		public static void main(String[] args) {
        int i = calculateNum((int left, int right) -> {
            return left + right;
        });
        System.out.println(i);
    }
		public static void main(String[] args) {
        int i = calculateNum((left, right) -> left + right);
        System.out.println(i);
    }

例三

现有方法定义如下,其中 IntPredicate 是一个接口。先使用匿名内部类的写法调用该方法。

		public static void main(String[] args) {
        printNum(new IntPredicate() {
            @Override
            public boolean test(int value) {
                return value % 2 == 0;
            }
        });
    }

    public static void printNum(IntPredicate predicate) {
        int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        for (int i : arr) {
            if (predicate.test(i)) {
                System.out.println(i);
            }
        }
    }

Lambda写法:

		public static void main(String[] args) {
        printNum((int value) -> {
            return value % 2 == 0;
        });
    }
		public static void main(String[] args) {
        printNum(value -> value % 2 == 0);
    }

例四

现有方法定义如下,其中 Function 是一个接口。先使用匿名内部类的写法调用该方法。

		public static <R> R typeConver(Function<String, R> function) {
        String str = "1235";
        R result = function.apply(str);
        return result;
    }

    public static void main(String[] args) {
        Integer result = typeConver(new Function<String, Integer>() {
            @Override
            public Integer apply(String s) {
                return Integer.valueOf(s);
            }
        });
        System.out.println("result = " + result);
    }

Lambda写法:

		Integer result = typeConver((String s)->{
        return Integer.valueOf(s);
    });
    System.out.println(result);
		public static void main(String[] args) {
        Integer result = typeConver(s -> Integer.valueOf(s));
        System.out.println(result);
    }

例五

现有方法定义如下,其中 IntConsumer 是一个接口。先使用匿名内部类的写法调用该方法。

		public static void foreachArr(IntConsumer consumer) {
        int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        for (int i : arr) {
            consumer.accept(i);
        }
    }

    public static void main(String[] args) {
        foreachArr(new IntConsumer() {
            @Override
            public void accept(int value) {
                System.out.println("value = " + value);
            }
        });
    }

Lambda写法:

		public static void main(String[] args) {
        foreachArr((int value) -> {
            System.out.println("value = " + value);
        });
    }
		public static void main(String[] args) {
        foreachArr(value -> System.out.println("value = " + value));
    }

1.3、省略规则

  • 参数类型可以省略
  • 当方法体只有一句代码时,大括号、return 和唯一一句代码的分号可以省略
  • 当方法只有一个参数时,小括号可以省略
  • 以上这些规则都记不住也可以省略不记

2、Stream流

2.1、概述

Java8 的 Stream 使用的是函数式编程模式,如同它的名字一样,它可以被用来对集合或数组进行链状流式的操作。可以更方便的让我们对集合或数组进行操作。

2.2、案例数据准备

引入依赖

		<dependencies>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.16version>
        dependency>
    dependencies>

实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode // 用于后期的去重使用
public class Author {
    // id
    private Long id;
    // 姓名
    private String name;
    // 年龄
    private Integer age;
    // 简介
    private String intro;
    // 作品
    private List<Book> books;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode // 用于后期的去重使用
public class Book {
    //id
    private Long id;
    //书名
    private String name;
    //分类
    private String category;
    //评分
    private Integer score;
    //简介
    private String intro;
}
		private static List<Author> getAuthors() {
        // 数据初始化
        Author author = new Author(1L,"蒙多",33,"一个从菜刀中明悟哲理的祖安人",null);
        Author author2 = new Author(2L,"亚拉索",15,"狂风也追逐不上他的思考速度",null);
        Author author3 = new Author(3L,"易",14,"是这个世界在限制他的思维",null);
        Author author4 = new Author(3L,"易",14,"是这个世界在限制他的思维",null);

        // 书籍列表
        List<Book> books1 = new ArrayList<>();
        List<Book> books2 = new ArrayList<>();
        List<Book> books3 = new ArrayList<>();

        books1.add(new Book(1L,"刀的两侧是光明与黑暗","哲学,爱情",88,"用一把刀划分了爱恨"));
        books1.add(new Book(2L,"一个人不能死在同一把刀下","个人成长,爱情",99,"讲述如何从失败中明悟真理"));

        books2.add(new Book(3L,"那风吹不到的地方","哲学",85,"带你用思维去领略世界的尽头"));
        books2.add(new Book(3L,"那风吹不到的地方","哲学",85,"带你用思维去领略世界的尽头"));
        books2.add(new Book(4L,"吹或不吹","爱情,个人传记",56,"一个哲学家的恋爱观注定很难把他所在的时代理解"));

        books3.add(new Book(5L,"你的剑就是我的剑","爱情",56,"无法想象一个武者能对他的伴侣这么的宽容"));
        books3.add(new Book(6L,"风与剑","个人传记",100,"两个哲学家灵魂和肉体的碰撞会激起怎么样的火花呢?"));
        books3.add(new Book(6L,"风与剑","个人传记",100,"两个哲学家灵魂和肉体的碰撞会激起怎么样的火花呢?"));

        author.setBooks(books1);
        author2.setBooks(books2);
        author3.setBooks(books3);
        author4.setBooks(books3);

        List<Author> authorList = new ArrayList<>(Arrays.asList(author, author2, author3, author4));
        return authorList;
    }

2.3、快速入门

需求

我们可以调用 getAuthors 方法获取到作家的集合。现在需要打印所有年龄小于 18 的作家的名字,并且要注意去重。

实现

				// 打印所有年龄小于18的作家的名字,并且要注意去重
        List<Author> authors = getAuthors();
        authors.
                stream() // 把集合转换成流
                .distinct() // 先去除重复的作家
                .filter(author -> author.getAge()<18) // 筛选年龄小于18的作家
                .forEach(author -> System.out.println(author.getName())); // 遍历打印名字

2.4、创建流

单列集合: 集合对象.stream()

		List<Author> authors = getAuthors();
		Stream<Author> stream = authors.stream();

数组:Arrays.stream(数组) 或使用 Stream.of 来创建

		private static void test2() {
        Integer[] arr = {1, 2, 3, 4, 5};
        Stream<Integer> stream = Arrays.stream(arr);
        stream.distinct().filter(integer -> integer > 2)
                .forEach(integer -> System.out.println(integer));
    }

双列集合:转换成单列集合后再创建 stream 流

		private static void test3() {
        Map<String, Integer> map = new HashMap<>();
        map.put("蜡笔小新", 19);
        map.put("黑子", 17);
        map.put("日向翔阳", 16);

        Set<Map.Entry<String, Integer>> set = map.entrySet();
        Stream<Map.Entry<String, Integer>> stream = set.stream();
        stream.filter(entry -> entry.getValue() > 16)
                .forEach(entry -> System.out.println(entry.getKey() + " = " + entry.getValue()));
    }

2.5、中间操作

filter

可以对流中的元素进行条件过滤,符合过滤条件的才能继续保留在流中。

例如:

打印所有姓名长度大于1的作家的姓名

				List<Author> authors = getAuthors();
        // 打印所有姓名长度大于1的作家姓名
        authors.stream().filter(author -> author.getName().length() > 1)
                .forEach(author -> System.out.println(author.getName()));

map

可以对流中的元素进行计算或转换。

例如:

打印所有作家的姓名

		private static void test5() {
        // 打印所有作家的姓名
        List<Author> authors = getAuthors();
        authors.stream()
                .map(new Function<Author, String>() {
                    @Override
                    public String apply(Author author) {
                        return author.getName();
                    }
                })
                .forEach(s -> System.out.println(s));
    }
		private static void test5() {
        // 打印所有作家的姓名
        List<Author> authors = getAuthors();
        authors.stream()
                .map(author -> author.getName())
                .forEach(s -> System.out.println(s));
    }
		private static void test5() {
        List<Author> authors = getAuthors();

        authors.stream()
								// 将作家转换成他的年龄
                .map(author -> author.getAge())
                // 将每位作家的年龄加10
                .map(age -> age + 10)
                // 打印每位作家的年龄
                .forEach(age -> System.out.println(age));
    }

distinct

可以去除流中的重复元素。

例如:

打印所有作家的姓名,并且要求其中不能有重复元素。

		private static void test6() {
        // 打印所有作家的姓名,并且要求其中不能有重复元素
        List<Author> authors = getAuthors();
        authors.stream()
                .distinct()
                .forEach(author -> System.out.println(author.getName()));
    }

注意:distinct 方法是依赖 Object 的 equals 方法来判断是否是相同对象的。所以需要注意重写 equals 方法。

sorted

可以对流中的元素进行排序。

例如:

对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素。

写法一:

@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode // 用于后期的去重使用
public class Author implements Comparable<Author>{
    // id
    private Long id;
    // 姓名
    private String name;
    // 年龄
    private Integer age;
    // 简介
    private String intro;
    // 作品
    private List<Book> books;

    @Override
    public int compareTo(Author o) {
        return o.getAge() - this.getAge();
    }
}
		private static void test7() {
        // 对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素。
        List<Author> authors = getAuthors();
        authors.stream()
                .distinct()
                .sorted().forEach(author -> System.out.println(author.getAge()));
    }

注意:如果调用空参的 sorted() 方法,需要流中的元素实现 Comparable 接口。

写法二:

		private static void test7() {
        // 对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素。
        List<Author> authors = getAuthors();
        authors.stream()
                .distinct()
                .sorted((Author o1, Author o2) -> o2.getAge() - o1.getAge())
                .forEach(author -> System.out.println(author.getAge()));
    }

limit

可以设置流的最大长度,超出的部分将被抛弃。

例如:

对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素,然后打印其中年龄最大的两个作家的姓名。

		private static void test8() {
        // 对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素,然后打印其中年龄最大的两个作家的姓名
        List<Author> authors = getAuthors();
        authors.stream()
                .distinct()
                .sorted((Author o1, Author o2) -> o2.getAge() - o1.getAge())
                .limit(2)
                .forEach(author -> System.out.println(author.getName()));
    }

skip

跳过流中的前 n 个元素,返回剩下的元素

例如:

打印除了年龄最大的作家外的其他作家,要求不能有重复元素,并且按照年龄降序排序。

		private static void test9() {
        // 打印除了年龄最大的作家外的其他作家,要求不能有重复元素,并且按照年龄降序排序。
        List<Author> authors = getAuthors();
        authors.stream()
                .distinct()
                .sorted((Author o1, Author o2) -> o2.getAge() - o1.getAge())
                .skip(1)
                .forEach(author -> System.out.println(author.getName()));
    }

flatMap

map 只能把一个对象转换成另一个对象来作为流中的元素。而 flatMap 可以把一个对象转换成多个对象作为流中的元素。

例一:

打印所有书籍的名字,要求对重复的元素进行去重。

		private static void test10() {
        List<Author> authors = getAuthors();
        // 打印所有书籍的名字,要求对重复的元素进行去重
        authors.stream()
                // flatMap会自动展平流
                .flatMap(author -> author.getBooks().stream())
                .distinct()
                .forEach(new Consumer<Book>() {
                    @Override
                    public void accept(Book book) {
                        System.out.println(book.getName());
                    }
                });
    }

函数式编程_第1张图片

函数式编程_第2张图片

例二:

打印现有数据的所有分类。要求对分类进行去重。不能出现这种格式:哲学,爱情

		private static void test11() {
        // 打印现有数据的所有分类。要求对分类进行去重。不能出现这种格式:哲学,爱情
        List<Author> authors = getAuthors();
        authors.stream()
                .flatMap(author -> author.getBooks().stream())
                .distinct()
                .flatMap(book -> Arrays.stream(book.getCategory().split(",")))
                .distinct()
                .forEach(category -> System.out.println(category));
    }

函数式编程_第3张图片

函数式编程_第4张图片

2.6、终结操作

forEach

对流中的元素进行遍历操作,通过传入的参数去指定对遍历到的元素进行什么具体操作。

例子:

输出所有作家的名字

		private static void test12() {
        // 输出所有作家的名字
        List<Author> authors = getAuthors();
        authors.stream()
                .map(author -> author.getName())
                .distinct()
                .forEach(name -> System.out.println(name));
    }

count

可以用来获取当前流中元素的个数。

例子:

打印这些作家所出书籍的数目,注意删除重复元素。

		private static void test13() {
        // 打印这些作家所出书籍的数目,注意删除重复元素。
        List<Author> authors = getAuthors();
        long count = authors.stream()
                .flatMap(author -> author.getBooks().stream())
                .distinct()
                .count();
        System.out.println(count);
    }

max&min

可以用来获取流中的最值。

例子:

分别获取这些作家所出书籍的最高分和最低分并打印。

		private static void test14() {
        // 分别获取这些作家所出书籍的最高分和最低分并打印。
        List<Author> authors = getAuthors();
        Optional<Integer> max = authors.stream()
                .flatMap(author -> author.getBooks().stream())
                .map(book -> book.getScore())
                .max((score1, score2) -> score1 - score2);

        System.out.println("最大值:" + max.get());

        Optional<Integer> min = authors.stream()
                .flatMap(author -> author.getBooks().stream())
                .map(book -> book.getScore())
                .min((score1, score2) -> score1 - score2);
        System.out.println("最小值:" + min.get());
    }

collect

把当前流转换成一个集合。

例子:

获取一个存放所有作者名字的 List 集合。

		private static void test15() {
        // 获取一个存放所有作者名字的 List 集合
        List<Author> authors = getAuthors();
        List<String> nameList = authors.stream()
                .map(author -> author.getName())
                .collect(Collectors.toList());
        System.out.println(nameList);
    }

获取一个所有书名的 Set 集合。

		private static void test16() {
        // 获取一个所有书名的Set集合
        List<Author> authors = getAuthors();
        Set<Book> bookSet = authors.stream()
                .flatMap(author -> author.getBooks().stream())
                .collect(Collectors.toSet());
        System.out.println(bookSet);
    }

获取一个 Map 集合,map 的 key 为作者名,value 为 List

		private static void test17() {
        // 获取一个Map集合,map的key为作者名,value为List
        List<Author> authors = getAuthors();
        // 注意key是不能重复的
        Map<String, List<Book>> map = authors.stream()
                .distinct()
                .collect(Collectors.toMap(author -> author.getName(), author -> author.getBooks()));
        System.out.println(map);
    }

2.7、查找与匹配

anyMatch

可以用来判断是否有任意符合匹配条件的元素,结果为 boolean 类型。

例子:

判断是否有年龄在29岁以上的作家

		private static void test18() {
        // 判断是否有年龄在29岁以上的作家
        List<Author> authors = getAuthors();
        boolean flag = authors.stream()
                .anyMatch(author -> author.getAge() > 29);
        System.out.println(flag);
    }

allMatch

可以用来判断是否都符合匹配条件,结果为 boolean 类型。如果都符合结果为 true,否则结果为 false。

例子:

判断是否所有的作家都是成年人

		private static void test19() {
        // 判断是否所有的作家都是成年人
        List<Author> authors = getAuthors();
        boolean flag = getAuthors().stream()
                .allMatch(author -> author.getAge() >= 18);
        System.out.println(flag);
    }

noneMatch

可以判断流中的元素是否都不符合匹配条件。如果都不符合结果为 true,否则结果为 false

例子:

判断作家是否都没有超过100岁的。

		private static void test20() {
        // 判断作家是否都没有超过100岁的
        List<Author> authors = getAuthors();
        boolean flag = authors.stream()
                .noneMatch(author -> author.getAge() > 100);
        System.out.println(flag);
    }

findAny

获取流中的任意一个元素。该方法没有办法保证获取的一定是流中的第一个元素。

例子:

获取任意一个年龄大于18的作家,如果存在就输出他的名字

		private static void test21() {
        // 获取任意一个年龄大于18的作家,如果存在就输出他的名字
        List<Author> authors = getAuthors();
        Optional<Author> optionalAuthor = authors.stream()
                .filter(author -> author.getAge() > 18)
                .findAny();
        optionalAuthor.ifPresent(author -> System.out.println(author.getName()));
    }

findFirst

获取流中的第一个元素。

例子:

获取一个年龄最小的作家,并输出他的姓名。

		private static void test22() {
        // 获取一个年龄最小的作家,并输出他的姓名
        List<Author> authors = getAuthors();
        Optional<Author> optionalAuthor = authors.stream()
                .sorted((o1, o2) -> o1.getAge() - o2.getAge())
                .findFirst();
        // 如果存在,就输出作家的姓名
        optionalAuthor.ifPresent(author -> System.out.println(author.getName()));
    }

reduce归并

对流中的数据按照指定的计算方式计算出一个结果。(缩减操作)

reduce 的作用是把 stream 流中的元素给组合起来,我们可以传入一个初始值,它会按照我们的计算方式依次拿流中的元素和初始化值进行计算,计算结果再和后面的元素再进行计算。

reduce两个参数的重载形式内部的计算方式如下:

// identity相当于我们传入的初始值
T result = identity;
// 遍历stream流中的每个元素,T代表元素类型
for (T element : this stream)
	result = accumulator.apply(result, element)
return result;

其中 identity 就是我们可以通过方法参数传入的初始值,accumulator 的 apply 具体进行什么计算也是我们通过方法参数来决定的。

例子:

使用 reduce 求所有作者年龄的和

		private static void test23() {
        // 使用reduce求所有作者年龄的和
        List<Author> authors = getAuthors();
        Integer sum = authors.stream()
                .distinct()
                .map(author -> author.getAge())
                .reduce(0, (result, element) -> result + element);
        System.out.println(sum);
    }

使用 reduce 求所有作者中年龄的最大值

		private static void test24() {
        // 使用reduce求所有作者中年龄的最大值
        List<Author> authors = getAuthors();
        Integer max = authors.stream()
                .map(author -> author.getAge())
                .reduce(Integer.MIN_VALUE, (result, element) -> result < element ? element : result);
        System.out.println(max);
    }

使用 reduce 求所有作者中年龄的最小值

		private static void test25() {
        // 使用reduce求所有作者中年龄的最小值
        List<Author> authors = getAuthors();
        Integer min = authors.stream()
                .map(author -> author.getAge())
                .reduce(Integer.MAX_VALUE, (result, element) -> result > element ? element : result);
        System.out.println(min);
    }

reduce 一个参数的重载形式内部的计算方式如下:

		boolean foundAny = false;
		// 初始化值
     T result = null;
		// 遍历stream流中的每个元素
     for (T element : this stream) {
         if (!foundAny) {
             foundAny = true;
           	 // 将流中的第一个元素作为初始值
             result = element;
         }
         else
           	// 将后面的元素和初始化值进行相应的计算
             result = accumulator.apply(result, element);
     }
     return foundAny ? Optional.of(result) : Optional.empty();

如果用一个参数的重载方法去求最小值代码如下:

		private static void test26() {
        // 使用reduce求所有作者中年龄的最小值
        List<Author> authors = getAuthors();
        Optional<Integer> min = authors.stream()
                .map(author -> author.getAge())
                .reduce((result, element) -> result > element ? element : result);
        min.ifPresent(age -> System.out.println(age));
    }

2.8、注意事项

  • 惰性求值(如果没有终结操作,那么中间操作是不会得到执行的)
  • 流是一次性的(一旦一个流对象经过终结操作后,这个流就不能再被使用了)
  • 流操作不会影响原数据(我们在流中可以多数据做很多处理,但是正常情况下是不会影响原来集合中的元素的)

4、Optional

4.1、概述

我们在编写代码的时候出现最多的就是空指针异常,所以在很多情况下我们需要做各种非空的判断。

				Author author = getAuthor();
        if (author != null) {
            System.out.println(author.getName());
        }

尤其是对象中的属性还是一个对象的情况下,这种判断会更多。

而过多的判断语句会让我们的代码显得臃肿不堪。

所以在 JDK8 中引入了 Optional,养成使用 Optional 的习惯后你就可以写出更优雅的代码来避免空指针异常。

并且在很多函数式编程相关的 API 中也都用到了 Optional,如果不会使用 Optional 也会对函数式编程的学习造成影响。

4.2、使用

创建对象

Optional 就好比是包装类,可以把我们的具体数据封装到 Optional 对象内部。然后我们去使用 Optional 中封装好的方法操作封装进去的数据就可以非常优雅的避免空指针异常。

我们一般使用 Optional 的静态方法 ofNullable 来把数据封装成一个 Optional 对象。无论传入的参数是否为 null 都不会出现问题。

		public static void main(String[] args) {
        Author author = getAuthor();
        Optional<Author> authorOptional = Optional.ofNullable(author);
        authorOptional.ifPresent(author1 -> System.out.println(author1.getName()));
    }

    public static Author getAuthor() {
        Author author = new Author(1L, "蒙多", 33, "一个从菜刀中明悟哲理的祖安人", null);
        return null;
    }

你可能会觉得还要加一行代码来封装数据比较麻烦。但是如果改造下 getAuthor 方法,让其的返回值就是封装好的 Optional 的话,我们在使用时就会方便很多。

		public static void main(String[] args) {
        Optional<Author> authorOptional = getAuthorOptional();
        authorOptional.ifPresent(author -> System.out.println(author.getName()));
    }

    public static Optional<Author> getAuthorOptional() {
        Author author = new Author(1L, "蒙多", 33, "一个从菜刀中明悟哲理的祖安人", null);
        return Optional.ofNullable(author);
    }

而且在实际开发中我们的数据很多是从数据库获取的。Mybatis 从 3.5 版本开始也已经支持 Optional 了。我们可以直接把 dao 方法的返回值类型定义成 Optional 类型,MyBastis 会自己把数据封装成 Optional 对象返回,封装的过程也不需要我们自己操作。

如果你确定一个对象不是空的则可以使用 Optional 的静态方法 of 来把数据封装成 Optional 对象。

				Author author = new Author();
        Optional<Author> authorOptional = Optional.of(author);

但是一定要注意,如果使用 of 方法那么传入的参数必须不为 null。(尝试下传入 null 会出现什么结果)

函数式编程_第5张图片

如果一个方法的返回值类型是 Optional 类型,并且经过判断发现某次计算得到的返回值为 null,这个时候就需要把 null 封装成Optional 对象返回。这时则可以使用 Optional 的静态方法 empty 来进行封装。

		public static Optional<Author> getAuthorOptional() {
        Author author = new Author(1L, "蒙多", 33, "一个从菜刀中明悟哲理的祖安人", null);
        return author == null ? Optional.empty() : Optional.of(author);
    }

安全消费值

我们获取到一个 Optional 对象后肯定需要对其中的数据进行使用。这时候我们就可以使用其 ifPresent 方法来消费其中的值。

这个方法会判断其内封装的数据是否为空,不为空时才会执行具体的消费代码。这样使用起来就更加安全了。

例如,以下写法就优雅的避免了空指针异常。

		public static void main(String[] args) {
        Optional<Author> authorOptional = getAuthorOptional();
        authorOptional.ifPresent(author -> System.out.println(author.getName()));
    }

    public static Optional<Author> getAuthorOptional() {
        Author author = new Author(1L, "蒙多", 33, "一个从菜刀中明悟哲理的祖安人", null);
        return Optional.ofNullable(null);
    }

获取值

如果我们想获取值进行处理可以使用 get 方法,但是不推荐。因为当 Optional 内部的数据为空的时候会出现异常。

函数式编程_第6张图片

安全获取值

如果我们期望安全的获取值,那么不推荐使用 get 方法,而是使用 Optional 提供的以下方法:

1、orElseGet

获取数据并且设置当数据为空时的默认值。如果数据不为空就能获取到该数据,如果为空则根据你传入的参数来创建对象作为默认值返回。

		public static void main(String[] args) {
        Optional<Author> authorOptional = getAuthorOptional();
        // 当获取的值为空时,这里可以设置要返回的默认值
        Author author = authorOptional.orElseGet(() -> new Author(2L, "小明", 23, "小明学函数式编程", null));
        System.out.println(author.getName());
    }

函数式编程_第7张图片

2、orElseThrow

获取数据,如果数据不为空就能获取到该数据。如果为空则根据你传入的参数来创建异常并抛出。

		public static void main(String[] args) {
        Optional<Author> authorOptional = getAuthorOptional();
        try {
            authorOptional.orElseThrow(() -> new RuntimeException("数据为null!"));
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

函数式编程_第8张图片

过滤

我们可以使用 filter 方法对数据进行过滤。如果原本是有数据的,但是不符合判断,也会变成一个无数据的 Optional 对象。

函数式编程_第9张图片

		private static void testFilter() {
        Optional<Author> authorOptional = getAuthorOptional();
        authorOptional.filter(author -> author.getAge() > 88)
                .ifPresent(author -> System.out.println(author.getName()));
    }

    public static Optional<Author> getAuthorOptional() {
        Author author = new Author(1L, "蒙多", 33, "一个从菜刀中明悟哲理的祖安人", null);
        return Optional.ofNullable(author);
    }

判断

我们可以使用 isPresent 方法进行是否存在数据的判断。如果为空则返回值为 false,如果不为空则返回值为 true。但是这种方式并不能体现 Optional 的好处,所以更推荐使用 ifPresent 方法。

		private static void testIsPresent() {
        Optional<Author> authorOptional = getAuthorOptional();
        if (authorOptional.isPresent()) {
            System.out.println(authorOptional.get().getName());
        }
    }

数据转换

Optional 还提供了 map 可以让我们对数据进行转换,并且转换得到的数据也还是被 Optional 包装好的,保证了我们的使用安全。

例如:我们想获取作家的书籍集合。

		private static void testMap() {
        Optional<Author> authorOptional = getAuthorOptional();
        Optional<List<Book>> optionalBooks = authorOptional.map(author -> author.getBooks());
        optionalBooks.ifPresent(books -> System.out.println(books));
    }

		public static Optional<Author> getAuthorOptional() {
        Author author = new Author(1L, "蒙多", 33, "一个从菜刀中明悟哲理的祖安人", null);
        //书籍列表
        List<Book> books1 = new ArrayList<>();
        books1.add(new Book(1L, "刀的两侧是光明与黑暗", "哲学,爱情", 88, "用一把刀划分了爱恨"));
        books1.add(new Book(2L, "一个人不能死在同一把刀下", "个人成长,爱情", 99, "讲述如何从失败中明悟真理"));
        author.setBooks(books1);
        return Optional.ofNullable(author);
    }

5、函数式接口

5.1、概述

只有一个抽象方法的接口我们称之为函数式接口。

JDK 的函数式接口都加上了 @FunctionalInterface 注解进行标识。但是无论是否加上该注解,只要接口中只有一个抽象方法,那么该接口就是一个函数式接口。

5.2、常见函数式接口

Consumer 消费接口

根据其中抽象方法的参数列表和返回值类型知道,我们可以在方法中对传入的参数进行消费。

函数式编程_第10张图片

Function 计算转换接口

根据其中抽象方法的参数列表和返回值类型知道,我们可以在方法中对传入的参数进行计算或转换,并把结果返回

函数式编程_第11张图片

Predicate 判断接口

根据其中抽象方法的参数列表和返回值类型知道,我们可以在方法中对传入的参数进行条件判断,并返回判断结果

函数式编程_第12张图片

Supplier 生产型接口

根据其中抽象方法的参数列表和返回值类型知道,我们可以在方法中创建对象,并把创建好的对象返回

函数式编程_第13张图片

5.3、常用的默认方法

and

我们在使用 Predicate 接口时可能需要进行判断条件的拼接,而 and 方法相当于是使用 && 来拼接两个判断条件。

例如:

打印作家中年龄大于 17 并且姓名的长度大于 1 的作家。

		private static void testAnd() {
        // 打印作家中年龄大于 17 并且姓名的长度大于 1 的作家
        List<Author> authors = getAuthors();
        authors.stream()
                .filter(new Predicate<Author>() {
                    @Override
                    public boolean test(Author author) {
                        return author.getAge() > 17;
                    }
                }.and(new Predicate<Author>() {
                    @Override
                    public boolean test(Author author) {
                        return author.getName().length() > 1;
                    }
                })).forEach(author -> System.out.println(author.getAge() + " -- " + author.getName()));
    }

or

我们在使用 Predicate 接口时可能需要进行判断条件的拼接,而 or 方法相当于是使用 || 来拼接两个判断条件。

例如:

打印作家中年龄大于 17 或者姓名的长度小于 2 的作家。

		private static void testOr() {
        // 打印作家中年龄大于 17 或者姓名的长度小于 2 的作家
        List<Author> authors = getAuthors();
        authors.stream()
                .filter(new Predicate<Author>() {
                    @Override
                    public boolean test(Author author) {
                        return author.getAge() > 17;
                    }
                }.or(new Predicate<Author>() {
                    @Override
                    public boolean test(Author author) {
                        return author.getName().length() < 2;
                    }
                }))
                .forEach(author -> System.out.println(author.getName() + " -- " + author.getAge()));
    }

negate

Predicate 接口中的方法。negate 方法相当于是在判断条件后面加了个 ! 表示取反

例如:

打印作家中年龄不大于 17 的作家。

		private static void testNegate() {
        List<Author> authors = getAuthors();
        // 打印作家中年龄不大于17的作家
        authors.stream()
                .filter(new Predicate<Author>() {
                    @Override
                    public boolean test(Author author) {
                        return author.getAge() > 17;
                    }
                }.negate())
                .forEach(author -> System.out.println(author.getAge()));
    }

6、方法引用

我们在使用 lambda 时,如果方法体中只有一个方法调用的话(包括构造方法),我们就可以使用方法引用来进一步简化代码。

6.1、推荐用法

我们在使用 lambda 时不需要考虑什么时候用方法引用,用哪种方法引用,方法引用的格式是什么。我们只需要在写完 lambda 方法后发现方法体只有一行代码,并且是方法的调用时就可以使用快捷键尝试是否能够转换成方法引用即可。

当我们方法引用使用得多了慢慢的也就可以直接写出方法引用了。

6.2、基本格式

类名或对象名::方法名

6.3、语法详解

引用类的静态方法

其实就是引用类的静态方法

格式:

类名::方法名

使用前提:

如果我们在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了某个类的静态方法,并且我们把要重写的抽象方法中所有的参数都按照顺序传入了这个静态方法中,这个时候我们就可以引用类的静态方法。

例如:

如下代码就可以使用方法引用进行简化:

		private static void test111() {
        List<Author> authors = getAuthors();
        Stream<Author> authorStream = authors.stream();
        authorStream
                .map(new Function<Author, Integer>() {
                    /**
                     * getAge()不是静态方法
                     * @param author 重写的抽象方法中的参数
                     * @return
                     */
                    @Override
                    public Integer apply(Author author) {
                        // 方法体中只有一行代码
                        return author.getAge();
                    }
                })
                .map(new Function<Integer, String>() {
                    /**
                     * String.valueOf(age)是静态方法
                     * @param age 重写的抽象方法中的参数
                     * @return
                     */
                    @Override
                    public String apply(Integer age) {
                        // 方法体中只有一行代码
                        return String.valueOf(age);
                    }
                });
    }

注意,如果我们所重写的方法是没有参数的,调用的方法也是没有参数的那么也相当于符合以上规则。

优化后如下:

		private static void test111() {
        List<Author> authors = getAuthors();
        Stream<Author> authorStream = authors.stream();
        authorStream
                .map(author -> author.getAge())
                .map(String::valueOf);
    }

引用对象的实例方法

格式:

对象名::方法名

使用前提:

如果我们在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了某个对象的成员方法,并且我们把要重写的抽象方法中所有的参数都按照顺序传入了这个成员方法中,这个时候我们就可以引用对象的实例方法。

例如:

		private static void test222() {
        List<Author> authors = getAuthors();
        Stream<Author> authorStream = authors.stream();
        StringBuilder sb = new StringBuilder();
        authorStream
                .map(new Function<Author, String>() {
                    /**
                     * getName()是author对象的成员方法
                     * @param author 重写的抽象方法中的参数
                     * @return
                     */
                    @Override
                    public String apply(Author author) {
                        // 方法体中只有一行代码
                        return author.getName();
                    }
                })
                .forEach(new Consumer<String>() {
                    /**
                     * append()是string对象的成员方法
                     * @param name 重写的抽象方法中的参数
                     */
                    @Override
                    public void accept(String name) {
                        // 方法体中只有一行代码
                        sb.append(name);
                    }
                });
    }

优化后:

		private static void test222() {
        List<Author> authors = getAuthors();
        Stream<Author> authorStream = authors.stream();
        StringBuilder sb = new StringBuilder();
        authorStream
                .map(author -> author.getName())
                .forEach(sb::append);
    }

引用类的实例方法

格式:

类名::方法名

使用前提:

如果我们在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了第一个参数的成员方法,并且我们把要重写的抽象方法中剩余的所有参数都按照顺序传入了这个成员方法中,这个时候我们就可以引用类的实例方法。

例如:

public class MethodDemo {

    interface UseString {
        // 抽象方法
        String use(String str, int start, int length);
    }

    public static String subAuthorName(String str, UseString useString) {
        int start = 0;
        int length = 1;
        // 调用了接口的抽象方法
        return useString.use(str, start, length);
    }

    public static void main(String[] args) {
        subAuthorName("三更草堂", new UseString() {
            @Override
            public String use(String str, int start, int length) {
                // 调用了String对象的成员方法,并且把剩余的参数都传入了这个成员方法中
                return str.substring(start, length);
            }
        });
    }

}

优化后:

		public static void main(String[] args) {
        // 调用了String对象的成员方法,并且把剩余的参数都传入了这个成员方法中
        subAuthorName("三更草堂", String::substring);
    }

构造器引用

如果方法体中的一行代码是构造器的话就可以使用构造器引用。

格式:

类名::new

使用前提:

如果我们在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了某个类的构造方法,并且我们把要重写的抽象方法中的所有参数都按照顺序传入了这个构造方法中,这个时候我们就可以引用构造器。

例如:

		private static void test333() {
        List<Author> authors = getAuthors();
        authors.stream()
                .map(author -> author.getName())
                .map(new Function<String, StringBuilder>() {
                    @Override
                    public StringBuilder apply(String name) {
                        // 调用了StringBuilder的构造方法
                        return new StringBuilder(name);
                    }
                })
                .map(sb -> sb.append("-三更").toString())
                .forEach(str -> System.out.println(str));
    }

优化后:

		private static void test333() {
        List<Author> authors = getAuthors();
        authors.stream()
                .map(author -> author.getName())
                .map(StringBuilder::new)
                .map(sb -> sb.append("-三更").toString())
                .forEach(System.out::println);
    }

7、高级用法

7.1、基本数据类型优化

我们之前用到的很多 Stream 的方法由于都使用了泛型,所以涉及到的参数和返回值都是引用数据类型。

即使我们操作的是整数或小数,但是实际用的都是他们的包装类。JDK5 中引入的自动装箱和自动拆箱让我们在使用对应的包装类时就好像在使用基本数据类型一样方便。但是你一定要知道装箱和拆箱肯定是要消耗时间的。虽然这个时间消耗很低,但是在大量数据不断的重复装箱拆箱的时候,你就不能无视这个时间消耗了。

所以为了让我们能够对这部分的时间消耗进行优化,Stream 还提供了很多专门针对基本数据类型的方法。

例如:mapToInt、mapToLong、mapToDouble、flatMapToInt、flatMapToDouble 等。

		private static void test27() {
        List<Author> authors = getAuthors();
        authors.stream()
                .map(author -> author.getAge())
                .map(new Function<Integer, Integer>() {
                    // 这里就涉及到了装箱、拆箱
                    @Override
                    public Integer apply(Integer age) {
                        return age + 10;
                    }
                })
                .filter(age -> age > 18)
                .map(new Function<Integer, Integer>() {
                    // 这里就涉及到了装箱、拆箱
                    @Override
                    public Integer apply(Integer age) {
                        return age + 2;
                    }
                })
                .forEach(System.out::println);

        // 优化后:

        authors.stream()
                .mapToInt(author -> author.getAge())
                .map(age -> age + 10)
                .filter(age -> age > 18)
                .map(age -> age + 2)
                .forEach(System.out::println);
    }

7.2、并行流

当流中有大量元素时,我们可以使用并行流去提高操作的效率。其实并行流就是把任务分配给多个线程去完成。如果我们自己去用代码实现的话其实会非常复杂,并且要求你对并发编程有足够的理解和认识。而如果我们使用 Stream 的话,只需要修改一个方法的调用就可以使用并行流来帮助我们实现,从而提高效率。

parallel 方法可以把串行流转换成并行流:

		private static void test28() {
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Integer sum = stream.parallel()
                .peek(new Consumer<Integer>() {
                    @Override
                    public void accept(Integer num) {
                        System.out.println(num + Thread.currentThread().getName());
                    }
                })
                .filter(num -> num > 5)
                .reduce((result, element) -> result + element)
                .get();
        System.out.println(sum);
    }

也可以通过 parallelStream 方法直接获取并行流对象:

		private static void test28() {
        List<Author> authors = getAuthors();
        authors.parallelStream()
                .map(author -> author.getAge())
                .map(age -> age + 10)
                .filter(age -> age > 18)
                .map(age -> age + 2)
                .forEach(System.out::println);
    }

你可能感兴趣的:(JavaSE,java,stream,函数式编程,lambda表达式,stream流)