zoukankan      html  css  js  c++  java
  • Java函数式编程

    函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。

    函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!


    Lambda表达式

    在Java程序中,我们经常遇到一大堆单方法接口,即一个接口只定义了一个方法:

    • Comparator
    • Runnable
    • Callable

    以Comparator为例,我们想要调用Arrays.sort()时,可以传入一个Comparator实例,以匿名类方式编写如下:

    String[] array = ...
    Arrays.sort(array, new Comparator<String>() {
        public int compare(String s1, String s2) {
            return s1.compareTo(s2);
        }
    });
    

    用Lambda表达式可以写为

    Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));
    

    Lambda表达式的参数和返回值均可由编译器自动推断。


    FunctionalInterface:单方法接口

    我们把只定义了单方法的接口称之为FunctionalInterface,用注解@FunctionalInterface标记。
    虽然Comparator接口有很多方法,但只有一个抽象方法int compare(T o1, T o2),其他的方法都是default方法或static方法。另外注意到boolean equals(Object obj)是Object定义的方法,不算在接口方法内。因此,Comparator也是一个FunctionalInterface。接收FunctionalInterface作为参数的时候,可以把实例化的匿名类改写为Lambda表达式,能大大简化代码。

    函数式接口的特点

    • 接口有且仅有一个抽象方法,如抽象方法compare
    • 允许定义静态非抽象方法
    • 允许定义默认defalut非抽象方法
    • 允许定义java.lang.Object中的public方法,如equals
    • FunctionInterface注解不是必须的,如果一个接口符合"函数式接口"定义,那么加不加该注解都没有影响。加上该注解能够更好地让编译器进行检查。如果编写的不是函数式接口,但是加上了@FunctionInterface,那么编译器会报错

    函数式接口是专门为lambda表达式准备的,lambda表达式是只实现接口中唯一的抽象方法的匿名实现类

    default关键字

    • default方法可以有自己的默认实现,即有方法体
    • 接口实现类可以不去实现default方法,并且可以使用default方法

    Stream:惰性计算

    这个Stream不同于java.io的InputStream和OutputStream,它代表的是任意Java对象的序列。这个Stream和List也不一样,List存储的每个元素都是已经存储在内存中的某个Java对象,而Stream输出的元素可能并没有预先存储在内存中,而是实时计算出来的。

    Stream看上去有点不好理解,但我们举个例子就明白了。

    如果我们要表示一个全体自然数的集合,显然,用List是不可能写出来的,因为自然数是无限的,内存再大也没法放到List中。但是,用Stream可以做到。

    Stream<BigInteger> naturals = createNaturalStream(); // 全体自然数
    

    因为这个streamNxN也有无限多个元素,要打印它,必须首先把无限多个元素变成有限个元素,可以用limit()方法截取前100个元素,最后用forEach()处理每个元素,这样,我们就打印出了前100个自然数的平方:

    Stream<BigInteger> naturals = createNaturalStream();
    naturals.map(n -> n.multiply(n)) // 1, 4, 9, 16, 25...
        .limit(100)
        .forEach(System.out::println);
    

    我们总结一下Stream的特点:它可以“存储”有限个或无限个元素。这里的存储打了个引号,是因为元素有可能已经全部存储在内存中,也有可能是根据需要实时计算出来的。

    Stream的另一个特点是,一个Stream可以轻易地转换为另一个Stream,而不是修改原Stream本身。

    最后,真正的计算通常发生在最后结果的获取,也就是惰性计算。

    Stream<BigInteger> naturals = createNaturalStream(); // 不计算
    Stream<BigInteger> s2 = naturals.map(BigInteger::multiply); // 不计算
    Stream<BigInteger> s3 = s2.limit(100); // 不计算
    s3.forEach(System.out::println); // 计算
    

    惰性计算的特点是:一个Stream转换为另一个Stream时,实际上只存储了转换规则,并没有任何计算发生。


    创建Stream

    (1)

    Stream<String> stream = Stream.of("A", "B", "C", "D");//虽然这种方式基本上没啥实质性用途,但测试的时候很方便。
    

    (2)

    Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
    Stream<String> stream2 = List.of("X", "Y", "Z").stream();
    stream1.forEach(System.out::println);
    stream2.forEach(System.out::println);
    

    (3)创建Stream还可以通过Stream.generate()方法,它需要传入一个Supplier对象,基于Supplier创建的Stream会不断调用Supplier.get()方法来不断产生下一个元素,这种Stream保存的不是元素,而是算法,它可以用来表示无限序列。

    Stream<String> s = Stream.generate(Supplier<String> sp);
    

    因为Java的范型不支持基本类型,所以我们无法用Stream< int>这样的类型,会发生编译错误。为了保存int,只能使用Stream< Integer>,但这样会产生频繁的装箱、拆箱操作。为了提高效率,Java标准库提供了IntStream、LongStream和DoubleStream这三种使用基本类型的Stream,它们的使用方法和范型Stream没有大的区别,设计这三个Stream的目的是提高运行效率


    map

    Stream.map()是Stream最常用的一个转换方法,它把一个Stream转换为另一个Stream。

    List.of("  Apple ", " pear ", " ORANGE", " BaNaNa ")
        .stream()
        .map(String::trim) // 去空格
        .map(String::toLowerCase) // 变小写
        .forEach(System.out::println); // 打印
    

    通过若干步map转换,可以写出逻辑简单、清晰的代码。

    flatMap

    map可以对管道流中的数据进行转换操作,但是如果管道中还有管道该如何处理?即:如何处理二维数组及二维集合类。实现一个简单的需求:将“hello”,“world”两个字符串组成的集合,元素的每一个字母打印出来。

    words.stream()
            .flatMap(w -> Arrays.stream(w.split(""))) // [h,e,l,l,o,w,o,r,l,d]
            .forEach(System.out::println);
    

    filter

    IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
                .filter(n -> n % 2 != 0)
                .forEach(System.out::println);
    

    谓词逻辑的复用

    通常情况下,filter函数中lambda表达式为一次性使用的谓词逻辑。如果我们的谓词逻辑需要被多处、多场景、多代码中使用,通常将它抽取出来单独定义到它所限定的主语实体中。
    比如:将下面的谓词逻辑定义在Employee实体class中。

    public static Predicate<Employee> ageGreaterThan70 = x -> x.getAge() >70;
    public static Predicate<Employee> genderM = x -> x.getGender().equals("M");
    
    List<Employee> filtered = employees.stream()
            .filter(Employee.ageGreaterThan70.and(Employee.genderM))
            .collect(Collectors.toList());
    
    List<Employee> filtered1 = employees.stream()
            .filter(Employee.ageGreaterThan70.or(Employee.genderM))
            .collect(Collectors.toList());
    
    List<Employee> filtered2 = employees.stream()
            .filter(Employee.ageGreaterThan70.or(Employee.genderM).negate())//negate语法(取反)
            .collect(Collectors.toList());
    

    reduce(归约)

    Stream提供的操作分为两类:转换操作(map filter)和聚合操作(reduce)

    Stream API为我们提供了Stream.reduce用来实现集合元素的归约。reduce函数有三个参数:

    • Identity标识:一个元素,它是归约操作的初始值,如果流为空,则为默认结果
    • Accumulator累加器:具有两个参数的函数:归约运算的部分结果和流的下一个元素
    • Combiner合并器(可选):当归约并行化时,或当累加器参数的类型与累加器实现的类型不匹配时,用于合并归约操作的部分结果的函数

    Integer类型归约

    reduce初始值为0,累加器可以是lambda表达式,也可以是方法引用。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
    int result = numbers
            .stream()
            .reduce(0, (subtotal, element) -> subtotal + element);
    System.out.println(result);  //21
    
    int result = numbers
            .stream()
            .reduce(0, Integer::sum);
    System.out.println(result); //21
    

    String类型归约

    不仅可以归约Integer类型,只要累加器参数类型能够匹配,可以对任何类型的集合进行归约计算。

    List<String> letters = Arrays.asList("a", "b", "c", "d", "e");
    String result = letters
            .stream()
            .reduce("", (partialString, element) -> partialString + element);
    System.out.println(result);  //abcde
    
    
    String result = letters
            .stream()
            .reduce("", String::concat);
    System.out.println(result);  //ancde
    

    复杂对象归约

    Employee e1 = new Employee(1,23,"M","Rick","Beethovan");
    Employee e2 = new Employee(2,13,"F","Martina","Hengis");
    Employee e3 = new Employee(3,43,"M","Ricky","Martin");
    Employee e4 = new Employee(4,26,"M","Jon","Lowman");
    Employee e5 = new Employee(5,19,"F","Cristine","Maria");
    Employee e6 = new Employee(6,15,"M","David","Feezor");
    Employee e7 = new Employee(7,68,"F","Melissa","Roy");
    Employee e8 = new Employee(8,79,"M","Alex","Gussin");
    Employee e9 = new Employee(9,15,"F","Neetu","Singh");
    Employee e10 = new Employee(10,45,"M","Naveen","Jain");
    
    
    List<Employee> employees = Arrays.asList(e1, e2, e3, e4, e5, e6, e7, e8, e9, e10);
    
    
    Integer total = employees.stream().map(Employee::getAge).reduce(0,Integer::sum);
    System.out.println(total); //346
    

    Combiner合并器的使用

    除了使用map函数实现类型转换后的集合归约,我们还可以用Combiner合并器来实现,这里第一次使用到了Combiner合并器。
    因为Stream流中的元素是Employee,累加器的返回值是Integer,所以二者的类型不匹配。这种情况下可以使用Combiner合并器对累加器的结果进行二次归约,相当于做了类型转换。

    Integer total3 = employees.stream()
            .reduce(0,(totalAge,emp) -> totalAge + emp.getAge(),Integer::sum); //注意这里reduce方法有三个参数
    System.out.println(total); //346
    

    计算结果和使用map进行数据类型转换的方式是一样的。

    并行流数据归约(使用合并器)

    对于大数据量的集合元素归约计算,更能体现出Stream并行流计算的威力。在进行并行流计算的时候,可能会将集合元素分成多个组计算。为了更快的将分组计算结果累加,可以使用合并器

    Integer total2 = employees
            .parallelStream()
            .map(Employee::getAge)
            .reduce(0,Integer::sum,Integer::sum);  //注意这里reduce方法有三个参数
    
    System.out.println(total); //346
    

    Stream查找和匹配元素

    不用stream,用for循环的做法:

    boolean isExistAgeThan70 = false;
    for(Employee employee:employees){
      if(employee.getAge() > 70){
        isExistAgeThan70 = true;
        break;
      }
    }
    

    使用Stream API来实现查找和匹配:

    boolean isExistAgeThan70 = employees.stream().anyMatch(Employee.ageGreaterThan70);//使用之前定义的谓词逻辑
    
    boolean isExistAgeThan72 = employees.stream().anyMatch(e -> e.getAge() > 72);//lambda表达式
    
    boolean isExistAgeThan10 = employees.stream().allMatch(e -> e.getAge() > 10);//AllMatch
    
    boolean isExistAgeLess18 = employees.stream().noneMatch(e -> e.getAge() < 18);//noneMatch
    

    总结

    Stream提供的常用操作有:

    转换操作:map(),filter(),sorted(),distinct();

    合并操作:concat(),flatMap();

    并行处理:parallel();Stream中并行操作不一定比串行快

    聚合操作:reduce(),collect(),count(),max(),min(),sum(),average();

    其他操作:allMatch(), anyMatch(), forEach()

  • 相关阅读:
    spring 之 AOP
    spring 之 动态代理
    为 NSDate 添加扩展类 判断时间
    iOS 日期相关总结
    iOS 请求出现 "Request failed: bad request (400)"
    NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802) 解决办法
    poj-3302
    辽宁省赛——杨鲁斯卡尔专场-J
    辽宁省赛——杨鲁斯卡尔专场 -F
    zzuli训练赛_05_13-D
  • 原文地址:https://www.cnblogs.com/swifthao/p/12564810.html
Copyright © 2011-2022 走看看