Bootstrap

【Java八股文之基础篇(二十)】函数式编程之Stream流(下)

Optional

概述

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

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

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

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

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

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

使用

创建对象

Optional就好像是包装类,可以把我们的具体数据封装Optional对象内部。然后我们去使用Optional中封装好的方法操作封装进去的数据就可以非常优雅的避免空指针异常。
我们一般使用Optional静态方法ofNullable来把数据封装成一个Optional对象。无论传入的参数是否为null都不会出现问题。

 Author author = getAuthor();
        Optional<Author> authorOptional = Optional.ofNullable(author);

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

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

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

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

但是一定要注意,如果使用of的时候传入的参数必须不为null。(尝试下传入null会出现什么结果)
如果一个方法的返回值类型是Optional类型。而如果我们经判断发现某次计算得到的返回值为null,这个时候就需要把null封装成Optional对象返回。这时则可以使用Optional静态方法empty来进行封装。

Optional.empty()

所以最后你觉得哪种方式会更方便呢?ofNullable

安全消费值

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

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

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

Optional<Author> authorOptional = Optional.ofNullable(getAuthor());

        authorOptional.ifPresent(author -> System.out.println(author.getName()));

获取值

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

安全获取值

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

  • orElseGet
    获取数据并且设置数据为空时的默认值。如果数据不为空就能获取到该数据。如果为空则根据你传入的参数来创建对象作为默认值返回。
Optional<Author> authorOptional = Optional.ofNullable(getAuthor());
        Author author1 = authorOptional.orElseGet(() -> new Author());
  • orElseThrow
    获取数据,如果数据不为空就能获取到该数据。如果为空则根据你传入的参数来创建异常抛出。
Optional<Author> authorOptional = Optional.ofNullable(getAuthor());
        try {
            Author author = authorOptional.orElseThrow((Supplier<Throwable>) () -> new RuntimeException("author为空"));
            System.out.println(author.getName());
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }

过滤

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

Optional<Author> authorOptional = Optional.ofNullable(getAuthor());
        authorOptional.filter(author -> author.getAge()>100).ifPresent(author -> System.out.println(author.getName()));

判断

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

Optional<Author> authorOptional = Optional.ofNullable(getAuthor());

        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));
    }

函数式接口

概述

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

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

常见函数式接口

  • Consumer 消费接口
    根据其中抽象方法的参数列表和返回值类型知道,我们可以在方法中对传入的参数进行消费。
    在这里插入图片描述
  • Function 计算转换接口

根据其中抽象方法的参数列表和返回值类型知道,我们可以在方法中对传入的参数计算或转换,把结果返回
在这里插入图片描述

  • Predicate 判断接口
    根据其中抽象方法的参数列表和返回值类型知道,我们可以在方法中对传入的参数条件判断,返回判断结果
    在这里插入图片描述
  • Supplier 生产型接口

根据其中抽象方法的参数列表和返回值类型知道,我们可以在方法中创建对象,把创建好的对象返回
在这里插入图片描述

常用的默认方法

  • and

我们在使用Predicate接口时候可能需要进行判断条件的拼接。而and方法相当于是使用&&来拼接两个判断条件
例如:
打印作家中年龄大于17并且姓名的长度大于1的作家。

List<Author> authors = getAuthors();
        Stream<Author> authorStream = authors.stream();
        authorStream.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));//第一个右括号是and的,第二个是filter的
  • or
    我们在使用Predicate接口时候可能需要进行判断条件的拼接。而or方法相当于是使用||来拼接两个判断条件。
    例如:

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

//        打印作家中年龄大于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()));
  • negate
    Predicate接口中的方法。negate方法相当于是在判断添加前面加了个! 表示取反
    例如:
    打印作家中年龄不大于17的作家。
//        打印作家中年龄不大于17的作家。
        List<Author> authors = getAuthors();
        authors.stream()
                .filter(new Predicate<Author>() {
                    @Override
                    public boolean test(Author author) {
                        return author.getAge()>17;
                    }
                }.negate()).forEach(author -> System.out.println(author.getAge()));

方法引用

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

推荐用法

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

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

基本格式

类名或者对象名::方法名

语法详解(了解)

引用类的静态方法

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

格式

类名::方法名

使用前提

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

List<Author> authors = getAuthors();

        Stream<Author> authorStream = authors.stream();
        
        authorStream.map(author -> author.getAge())
                .map(age->String.valueOf(age));

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

优化后如下:

List<Author> authors = getAuthors();

        Stream<Author> authorStream = authors.stream();

        authorStream.map(author -> author.getAge())
                .map(String::valueOf);

引用对象的实例方法

格式

对象名::方法名

使用前提

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

例如:

List<Author> authors = getAuthors();

        Stream<Author> authorStream = authors.stream();
        StringBuilder sb = new StringBuilder();
        authorStream.map(author -> author.getName())
                .forEach(name->sb.append(name));

优化后:

List<Author> authors = getAuthors();

        Stream<Author> authorStream = authors.stream();
        StringBuilder sb = new StringBuilder();
        authorStream.map(author -> author.getName())
                .forEach(sb::append);

引用类的实例方法

格式

类名::方法名

使用前提

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

例如:

 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("123456", new UseString() {
            @Override
            public String use(String str, int start, int length) {
                return str.substring(start,length);
            }
        });

	}

优化后:

 public static void main(String[] args) {

        subAuthorName("123456", String::substring);//1

    }

构造器引用

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

格式

类名::new

使用前提

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

例如:

List<Author> authors = getAuthors();
        authors.stream()
                .map(author -> author.getName())
                .map(name->new StringBuilder(name))
                .map(sb->sb.append("-kkkk").toString())
                .forEach(str-> System.out.println(str));

优化:

 List<Author> authors = getAuthors();
        authors.stream()
                .map(author -> author.getName())
                .map(StringBuilder::new)
                .map(sb->sb.append("-kkkk").toString())
                .forEach(str-> System.out.println(str));//在作者的名字后面加上kkkk然后打印

高级用法

基本数据类型优化

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

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

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

​ 例如:mapToInt,mapToLong,mapToDouble,flatMapToInt,flatMapToDouble等。

    private static void test27() {

        List<Author> authors = getAuthors();
        authors.stream()
                .map(author -> author.getAge())
                .map(age -> age + 10)
                .filter(age->age>18)
                .map(age->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);
    }

并行流

当流中有大量元素时,我们可以使用并行流去提高操作的效率。其实并行流就是把任务分配给多个线程去完全。如果我们自己去用代码实现的话其实会非常的复杂,并且要求你对并发编程有足够的理解和认识。而如果我们使用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, ele) -> result + ele)
                .get();
        System.out.println(sum);
    }

也可以通过parallelStream直接获取并行流对象。

 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);
;