美文网首页Java读书笔记_IT
读《Java 8 函数式编程》

读《Java 8 函数式编程》

作者: maxzhao_ | 来源:发表于2019-01-28 20:32 被阅读0次

    前言

    • 以下内容带有“【摘】”字样的段落,均来自"Java 8 Lambda, Richard Warburton著(O‘Reilly , 2015)"。
    • 我自己写的代码均使用Junit @Test,实体均为内部类,这里的@Data是lombok插件哦。
    • 本文内容章节随书的章节而定,并不是所有的章节都有做笔记,所以部分章节可能没有出现。

    简介

    每个人对函数式编程的理解不尽相同。但其核心是:在思考问题时,使用不可变值和函
    数,函数对一个值进行处理,映射成另一个值。
    不同的语言社区往往对各自语言中的特性孤芳自赏。现在谈 Java 程序员如何定义函数式编程还为时尚早,但是,这根本不重要!我们关心的是如何写出好代码,而不是符合函数式
    编程风格的代码。
    本书将重点放在函数式编程的实用性上,包括可以被大多数程序员理解和使用的技术,帮助他们写出易读、易维护的代码。【摘】

    Lambda 表达式

    Lambda 表达式的几种形式

    • unnable noArguments = () -> System.out.println("Hello World");
    • ActionListener oneArgument = event -> System.out.println("button clicked");
    • Runnable multiStatement = () -> {- System.out.print("Hello");System.out.println(" World");};
    • BinaryOperator<Long> add = (x, y) -> x + y;
    • BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;

    Lambda 表达式的类型依赖于上下文环境,是由编译器
    推断出来的。目标类型也不是一个全新的概念。如final String[] array = { "hello", "world" };Java 中初始化数组时,数组的类型就是根据上下文推断出来的。另一个常见的例子是 null ,只有将 null 赋值给一个变量,才能知道它的类型。

    这里的示例为什么要用final呢?我理解为让我们有一个良好的编程习惯,常量用final修饰。

    看到BinaryOperator就涉及到我的知识盲区了,下面附带一个测试代码。
    介绍:表示对同一类型的两个操作数的操作,产生与操作数相同类型的结果。 对于操作数和结果都是相同类型的情况,这是BiFunction专业化

        @Test
        public void add() {
            BinaryOperator<Integer> add = (x, y) -> x + y;
            System.out.println(add.apply(1, 2));
        }
    
        @Test
        public void string() {
            BinaryOperator<String> add = (x, y) -> x + "===" + y;
            System.out.println(add.apply("1", "2"));
            System.out.println(add.apply("1", "1"));
        }
    
        @Test
        public void minBy() {
            BinaryOperator<Integer> add = BinaryOperator.minBy(Comparator.naturalOrder());
            System.out.println(add.apply(1, 2));
        }
    
        @Test
        public void maxBy() {
            BinaryOperator<Integer> add = BinaryOperator.maxBy(Comparator.naturalOrder());
            System.out.println(add.apply(1, 2));
        }
    
        @Test
        public void maxBy2() {
            @Data
            class User {
                private String name;
            }
            User user1 = new User();
            user1.setName("123");
            User user4 = new User();
            user4.setName("1234");
            BinaryOperator<User> maxBy2 = BinaryOperator.maxBy(Comparator.comparing(User::getName));
            System.out.println(maxBy2.apply(user1, user4));
        }
    

    2.3 引用值 , 而不是变量

    Java 8 可以引用非 final 变量,但是该变量在既成事实上必须是
    final 。虽然无需将变量声明为 final ,但在 Lambda 表达式中,也无法用作非终态变量。如果坚持用作非终态变量,编译器就会报错。
    比如:

            String str = "final";
            // str = str.substring(1);如果不加当前操作,下面Lambda不会报错,但是如果对当前字符串进行操作,下面Lambda无法通过编译。
            BinaryOperator<String> finalAdd = (x, y) -> x + str + y;
            System.out.println(add.apply("1", "2"));
            // 如果必须要进行操作,则可以在操作之后替换该变量`String finalStr = str;`,这样就额可以通过编译。
    

    2.4 函数接口

    函数接口是只有一个抽象方法的接口,用作 Lambda 表达式的类型。

    Java中重要的函数接口

    接口 参数 返回类型 示例
    Predicate<T> T boolean 这张唱片已经发行了吗
    Consumer<T> T void 输出一个值
    Function<T,R> T R 获得 Artist 对象的名字
    Supplier<T> None T 工厂方法
    UnaryOperator<T> T T 逻辑非 (!)
    BinaryOperator<T> (T, T) T 求两个数的乘积 (*)

    好吧,这都是知识盲区,既然见到了不得不学习一番。

    • Predicate<T> 即对t进行断言,返回true或者false。
            @Data
            class Employee {
                Employee(Integer id, Integer age, String fName) {
                    this.id = id;
                    this.age = age;
                    this.firstName = fName;
                }
    
                private Integer id;
                private Integer age;
                private String firstName;
            }
            Predicate<Employee> isAgeMoreThan = p -> p.getAge() > 18;
            Predicate<Employee> isjack = p -> p.getAge() > 18 && p.getFirstName().equalsIgnoreCase("jack");
            Predicate<Employee> isJon = p -> p.getAge() > 21 && p.getFirstName().equalsIgnoreCase("jon");
            Employee e2 = new Employee(2, 13, "Martina");
            Employee e3 = new Employee(3, 43, "Jack");
            Employee e4 = new Employee(4, 26, "Jon");
            System.out.println(isAgeMoreThan.test(e3));
            System.out.println(isAgeMoreThan.test(e2));
            System.out.println(isjack.test(e2));
            System.out.println(isJon.test(e4));
    
    • Consumer<T> 表示接受单个输入参数并且不返回结果的操作。 与大多数其他功能界面不同, Consumer预计将通过副作用进行操作。本人不成熟的见解:当前类中的处理进行封装,更有利于调用,通过副作用进行实现。
    // JDK1.8实现
            @Data
            class User {
                private String name;
                User(String name) {
                    this.name = name;
                }
            }
            User user = new User("max");
            Consumer<User> consumer = user1 -> user1.setName(user1.getName() + " Zhao");
            //  Consumer<User> consumer = user1 ->{ user1.setName(user1.getName() + " Zhao");}可以加上括号做更复杂的操作。
            consumer.accept(user);
            System.out.println(user);
    // JDK1.8之前的实现,需要调用下面方法
            public String accept(User user){
                user1.setName(user1.getName() + " Zhao");   
            }
    
    • Function<T,R> 表示接受一个参数并产生结果的函数。
            Function<Integer, Integer> name = e -> e * 2;
            Function<Integer, Integer> square = e -> e * e;
            int value = name.andThen(square).apply(3);//36
            int value2 = name.compose(square).apply(3);//18
            int value3 = name.apply(3);//6
            //返回一个执行了apply()方法之后只会返回输入参数的函数对象
            Object identity = Function.identity().apply("MaxZhao");//MaxZhao
    
    • Supplier<T> 是一个提供结果的函数接口,每次调用get()方法的时候才会创建对象。并且每次调用创建的对象都不一样
    public class SupplierTest {
        @Test
        public void test() {
            Supplier<String> supplier = () -> "字符串";
            String str = supplier.get();
            System.out.println("str = " + str);
    
            //创建Supplier容器,此时并不会调用对象的构造方法,即不会创建对象
            Supplier<User> user = User::new;
            //调用get()方法,此时会调用对象的构造方法,即获得到真正对象
            user.get();
            Supplier<User> user2 = () -> {
                User user1 = new User();
                //
                return user1;
            };
            Supplier<User> user3 = () -> {
                User user1 = new User() {
                    @Override
                    public void come(User t) {
                        System.out.println("User = " + t);
                    }
                };
                return user1;
            };
        }
    }
    @Data
    class User {
        private String name;
        public void come(User t) {
            System.out.println(t.getName() + " is come");
        }
    }
    
    • UnaryOperator<T> Operator其实就是Function,函数有时候也叫作算子。算子在Java8中接口描述更像是函数的补充,和上面的很多类型映射型函数类似。它包含UnaryOperator和BinaryOperator。分别对应单元算子和二元算子。
            Function<Integer, Integer> name = e -> e * 2;
            Function<Integer, Integer> square = e -> e * e;
            int value = name.andThen(square).apply(3);//36
            System.out.println("andThen value=" + value);
            int value2 = name.compose(square).apply(3);//18
            System.out.println("compose value2=" + value2);
            int value3 = name.apply(3);//6
            System.out.println("compose value2=" + value3);
            //返回一个执行了apply()方法之后只会返回输入参数的函数对象
            Object identity = Function.identity().apply("MaxZhao");//MaxZhao
            System.out.println(identity);
    
    • BinaryOperator<T> 上面也介绍到了,部分通用接口部分介绍中也体现了,这里就不讲了。

    2.5 类型推断

    某些情况下,用户需要手动指明类型,建议大家根据自己或项目组的习惯,采用让代码最便于阅读的方法。有时省略类型信息可以减少干扰,更易弄清状况;而有时却需要类型信息帮助理解代码。经验证发现,一开始类型信息是有用的,但随后可以只在真正需要时才加上类型信息。下面将介绍一些简单的规则,来帮助确认是否需要手动声明参数类型。【摘】
    下面是一些例子:

    Map<String, Integer> oldWordCounts = new HashMap<String, Integer>();
    Map<String, Integer> diamondWordCounts = new HashMap<>(); 
    
    useHashmap(new HashMap<>());
    private void useHashmap(Map<String, String> values);
    

    Java 7 中程序员可省略构造函数的泛型类型,Java 8 更进一步,程序员可省略 Lambda 表达式中的所有参数类型。再强调一次,这并不是魔法, javac 根据 Lambda 表达式上下文信息就能推断出参数的正确类型。程序依然要经过类型检查来保证运行的安全性,但不用再显式声明类型罢了。这就是所谓的类型推断。【摘】

    3 流——我最喜欢,不仅使处理易读,更提升了速度(并行)

    流使程序员得以站在更高的抽象层次上对集合进行操作。【摘】

    3.1 从外部迭代到内部迭代

    传统的迭代方式都是为循环操作,每次迭代集合类时,都需要写很多样板代码。将
    for 循环改造成并行方式运行也很麻烦,需要修改每个 for 循环才能实现。
    外部迭代Iterator:然而,外部迭代也有问题。首先,它很难抽象出本章稍后提及的不同操作;此外,它从本质上来讲是一种串行化操作。总体来看,使用 for 循环会将行为和方法混为一谈。

    另一种方法就是内部迭代stream()方法的调用,它和 iterator() 的作用一样。该方法不是返回一个控制迭代的 Iterator 对象,而是返回内部迭代中的相应接口: Stream
    首先对比一下for、iterator、streamUser的操作:

            List<User> userList = new ArrayList<>();
            for (User user : userList) {
                // **
            }
            Iterator<User> userIterator = userList.iterator();
            User user;
            while (userIterator.hasNext()) {
                user = userIterator.next();
                // **
            }
            userList.stream().filter(user1 -> {
                // **
            }).count();
    

    Stream 是用函数式编程方式在集合类上进行复杂操作的工具。【摘】

    3.2 实现机制

    filtercount两种操作是否意味着需要两次循环?事实上,类库设计精妙,只需对艺术家列表迭代一次。
    count 这样最终会从 Stream 产生值的方法叫作及早求值方法
    Stream最终都会有终止操作;

    3.3 常用的流操作

    下面所有的userList为:List<User> userList = Arrays.asList(new User("Jack"), new User("Jon"));

    3.3.1 collect(toList())

    collect(Collectors.toList())方法由 Stream 里的值生成一个列表,是一个及早求值操作。

    List<String> collected = Stream.of("a", "b", "c").collect(Collectors.toList());
    System.out.println(Objects.equals(Arrays.asList("a", "b", "c"), collected));//true
    

    3.3.2 map,可以接受Function参数

    如果有一个函数可以将一种类型的值转换成另外一种类型, map 操作就可以使用该函数,将一个流中的值转换成一个新的流。

    userList.stream().map(user -> user.getName().length()).collect(Collectors.toList());//[4, 3]
    // 等价与
    userList.stream().map(user -> {return user.getName().length();}).collect(Collectors.toList());//[4, 3]
    

    3.3.3 filter,可以接受Predicate参数

    userList.stream().filter(user -> user.getName().equalsIgnoreCase("Jon")).collect(Collectors.toList());//[StreamTest.User(name=Jon)]
    

    3.3.4 flatMap

    flatMap 方 法 可 用 Stream 替 换 值, 然 后 将 多 个 Stream 连 接 成 一 个 Stream
    (如图 3-7 所示)。【摘】

    List<Integer> together = Stream.of(asList(1, 2), asList(3, 4))
    .flatMap(numbers -> numbers.stream())
    .collect(toList());
    assertEquals(asList(1, 2, 3, 4), together);
    

    【摘】

    3.3.5 max 和 min

    userList.stream().min(Comparator.comparing(user -> user.getName().length())).get();//StreamTest.User(name=Jon)
    

    max``min返回的是Optional,也是一个新特性,可以取代三元运算符。

    3.3.7 reduce

    reduce 操作可以实现从一组值中生成一个值。

    Stream.of(1, 2, 3).reduce(0, (acc, element) -> acc + element);//6
    

    3.3.8 整合操作

    Stream.of(1, 2, 3).map(item -> item * item).filter(item -> item > 4).collect(Collectors.toList());//[9]                                                                      
    

    3.6 高阶函数

    高阶函数是指接受另外一个函
    数作为参数,或返回一个函数的函数。高阶函数不难辨认:看函数签名就够了。如果函数的参数列表里包含函数接口,或该函数返回一个函数接口,那么该函数就是高阶函数。【摘】

    3.10 进阶练习

    1. 只用 reduce 和 Lambda 表达式写出实现 Stream 上的 map 操作的代码,如果不想返回
      Stream ,可以返回一个 List 。
    // reduce实现map
    private <T, R> Stream<R> map(Stream<T> stream, Function<T, R> fun) {
        return stream.reduce(new ArrayList<R>().stream(),  // Stream<R>是reduce参数的的U
                (u, t) -> Stream.concat(u, Stream.of(fun.apply(t))),
                (a, b) -> Stream.concat(a, b));
    }
    // 测试方法
    // Artist(String name, String nation)
    @Test
    public void test23() {
        System.out.println(
            map(Stream.of(
                    new Artist("a", "aaa"),
                    new Artist("b", "bbb"),
                    new Artist("c", "ccc")
            ), t -> t.getNation()).collect(Collectors.toList())
        ); // [aaa,bbb,ccc]
    }
    
    1. 只用 reduce 和 Lambda 表达式写出实现 Stream 上的 filter 操作的代码,如果不想返回
      Stream ,可以返回一个 List 。
    // reduce实现filter
    private <T> Stream<T> filter(Stream<T> stream, Predicate<T> pre) {
        return stream.reduce(new ArrayList<T>().stream(),   // Stream<T>是reduce的U
                (u, t) -> {
                    if (pre.test(t))
                        return Stream.concat(u, Stream.of(t));
                    return u;
                },
                (a, b) -> Stream.concat(a, b)
        );
    }
    // 测试方法
    @Test
    public void test24() {
        System.out.println(
                filter(Stream.of(
                        new Artist("a", "aaa"),
                        new Artist("b", "bb"),
                        new Artist("c", "c")
                ), t -> t.getNation().length() >= 2).collect(Collectors.toList())
        );
    } // [Artist("a", "aaa"), Artist("b", "bb")]
    

    上面来自这里

    4 类库

    有点复杂,建议直接看书,总结不出来。

    4.10 Optional

    上面提到过,Optional 是为核心类库新设计的一个数据类型,用来替换 null 值。

    5 高级集合类和收集器

    5.1 方法引用

    例如下面两种语法结果相同。

    user->user.getName()
    user::getName
    

    例如创建对象

    User::new
    

    更复杂的还是看书。

    5.2 元素顺序

    另外一个尚未提及的关于集合类的内容是流中的元素以何种顺序排列。读者可能知道,一些集合类型中的元素是按顺序排列的,比如 List ;而另一些则是无序的,比如 HashSet 。
    增加了流操作后,顺序问题变得更加复杂。
    直观上看,流是有序的,因为流中的元素都是按顺序处理的。这种顺序称为出现顺序。出现顺序的定义依赖于数据源和对流的操作。
    在一个有序集合中创建一个流时,流中的元素就按出现顺序排列,因此,List集合代码总是可以通过。【摘】
    如果集合本身就是无序的,由此生成的流也是无序的。 HashSet 就是一种无序的集合,因此不能保证程序每次都通过。【摘】
    这 会 带 来 一 些 意 想 不 到 的 结 果, 比 如 使 用 并 行 流 时, forEach 方 法 不 能 保 证 元 素 是按顺序处理的(第 6 章会详细讨论这些内容)。如果需要保证按顺序处理,应该使用forEachOrdered 方法,它是你的朋友。【摘】

    5.3 使用收集器

    前面我们使用过 collect(toList()) ,在流中生成列表。显然, List 是能想到的从流中生成的最自然的数据结构,但是有时人们还希望从流生成其他值,比如 Map 或 Set ,或者你希望定制一个类将你想要的东西抽象出来。

    5.3.1 转换成其他集合

    list转Map

    System.out.println(Stream.of(new User("jdck")).collect(Collectors.toMap(User::getName, Function.identity())));
    System.out.println(Stream.of(new User("jdck")).collect(Collectors.toMap(User::getName, User::getName)));
    

    5.3.3 数据分块

    通过一个例子,很好理解

    System.out.println(Stream.of(new User("jdck")).collect(partitioningBy(t -> t.getName().equalsIgnoreCase("jdck"))));
    System.out.println(Stream.of(new User("jdck")).collect(partitioningBy(t -> t.getName().equalsIgnoreCase("join"))));
    //{false=[], true=[User(name=jdck)]}
    //{false=[User(name=jdck)], true=[]}
    

    5.3.4 数据分组

    分组是分块的子集(可能想等),下面有个例子:

    System.out.println(Stream.of(new User("jdck")).collect(groupingBy(t -> t.getName().equalsIgnoreCase("jdck"))));
    System.out.println(Stream.of(new User("jdck")).collect(groupingBy(t -> t.getName().equalsIgnoreCase("join"))));
    // {true=[User(name=jdck)]}
    // {false=[User(name=jdck)]}
    

    读者可能知道 SQL 中的 group by 操作,我们的方法是和这类似的一个概念,只不过在 Stream 类库中实现了而已。

    5.3.5 字符串

    字符串的joining方法如下:

    System.out.println(userList.stream().map(User::getName).collect(Collectors.joining(",", "[", "]")));//[Jack,Jon]
    System.out.println(userList.stream().map(User::getName).collect(Collectors.joining(",")));//Jack,Jon
    System.out.println(userList.stream().map(User::getName).collect(Collectors.joining()));//JackJon
    

    这里使用 map 操作提取出艺术家的姓名,然后使用 Collectors.joining 收集流中的值,该方法可以方便地从一个流得到一个字符串,允许用户提供分隔符(用以分隔元素)、前缀和后缀。
    下面这个结果是报错的:

    List<Integer> a = Arrays.asList(1, 2, 3);
    List<Integer> a2 = Arrays.asList(4, 5, 6);
    a.addAll(a2);
    

    调试之后发现,Arrays.asList()返回类型是Array$ArrayList@
    new ArrayList()<> 返回类型是ArrayList@

    6 数据并行化——高潮来了

    6.1 并行和并发

    并发和并行不是一个概念!
    并发是两个任务共享时间段,并行则是两个任务在同一时间发生,比如运行在多核 CPU上。

    并行化是指为缩短任务执行时间,将一个任务分解成几部分,然后并行执行。这和顺序执行的任务量是一样的,区别就像用更多的马来拉车,花费的时间自然减少了。实际上,和顺序执行相比,并行化执行任务时,CPU 承载的工作量更大。【摘】(简直废话)

    6.2 为什么并行化如此重要

    硬件越来越给力。

    6.3 并行化流操作

    下面两个操作都可以实现并行:

    userList.stream().parallel().map(User::getName).collect(Collectors.toList());
    userList.parallelStream().map(User::getName).collect(Collectors.toList());
    

    并不是并行速度就快,要看运行时的环境。在一个四核电脑上,如果有 10 张专辑,串行化代码的速度是并行化代码速度的 8 倍;如果将专辑数量增至 100 张,串行化和并行化速度相当;如果将专辑数量增值 10 000 张,则并行化代码的速度是串行化代码速度的 2.5 倍。

    6.4 模拟系统

    pass:书上说的很详细,这里只是把我不了解的拿出来。

    // 使用蒙特卡洛模拟法并行化模拟掷骰子事件
    public Map<Integer, Double> parallelDiceRolls() {
    double fraction = 1.0 / N;
    return IntStream.range(0, N)
    .parallel()
    .mapToObj(twoDiceThrows())
    .collect(groupingBy(side -> side,
    summingDouble(n -> fraction)));
    }
    

    上面的完全看不懂的样子,查一下API。
    IntStream.range(0, 100),是生成[0-100)的区间为1的stream的流。

    6.6 性能

    在前面我简要提及了影响并行流是否比串行流快的一些因素,现在让我们仔细看看它们。理解哪些能工作、哪些不能工作,能帮助在如何使用、什么时候使用并行流这一问题上做出明智的决策。影响并行流性能的主要因素有 5 个,依次分析如下。【摘】

    • 数据大小
    输入数据的大小会影响并行化处理对性能的提升。将问题分解之后并行化处理,再将结果合并会带来额外的开销。因此只有数据足够大、每个数据处理管道花费的时间足够多时,并行化处理才有意义。6.3 节讨论过。
    • 源数据结构
    每个管道的操作都基于一些初始数据源,通常是集合。将不同的数据源分割相对容易,这里的开销影响了在管道中并行处理数据时到底能带来多少性能上的提升。
    • 装箱
    处理基本类型比处理装箱类型要快。
    • 核的数量
    极端情况下,只有一个核,因此完全没必要并行化。显然,拥有的核越多,获得潜在性能提升的幅度就越大。在实践中,核的数量不单指你的机器上有多少核,更是指运行时你的机器能使用多少核。这也就是说同时运行的其他进程,或者线程关联性(强制线程在某些核或 CPU 上运行)会影响性能。
    • 单元处理开销
    比如数据大小,这是一场并行执行花费时间和分解合并操作开销之间的战争。花在流中每个元素身上的时间越长,并行操作带来的性能提升越明显。【摘】

    我们可以根据性能的好坏,将核心类库提供的通用数据结构分成以下 3 组。
    • 性能好
    ArrayList 、数组或 IntStream.range ,这些数据结构支持随机读取,也就是说它们能轻而易举地被任意分解。
    • 性能一般
    HashSet 、 TreeSet ,这些数据结构不易公平地被分解,但是大多数时候分解是可能的。
    • 性能差
    有些数据结构难于分解,比如,可能要花 O(N) 的时间复杂度来分解问题。其中包括LinkedList ,对半分解太难了。还有 Streams.iterate 和 BufferedReader.lines ,它们长度未知,因此很难预测该在哪里分解。【摘】

    相关文章

      网友评论

        本文标题:读《Java 8 函数式编程》

        本文链接:https://www.haomeiwen.com/subject/ljzojqtx.html