likes
comments
collection
share

Java函数式编程(一):为什么要关心Java8

作者站长头像
站长
· 阅读数 32

为什么Java一直在变

  • 日新月异的计算应用背景:多核和处理大型数据集(大数据)
  • 改进的压力:函数式比命令式更适应新的体系架构
  • Java 8的核心新特性:Lambda(匿名函数)、流、默认方法
  • 为什么应该关心Java8:因为Java8所做出的改进,比以往任意一个版本都要深远,并且让编程变得容易。
  • 平常的CPU都是多核心的,但是实际上只使用了一个核心,其他的核心都浪费掉了,而使用Java8可以方便的使用这些功能。
  • 使用Java8的新特性,可以写出更加简洁的代码,见名知意。
  • 在Java8中,接口支持默认方法、把代码传递给方法的简洁方式(方法引用、Lambda),其根本原因就是为了支持Streams
  • 什么是方法引用:方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用会创建函数式接口的一个实例。方法引用的本质就是:引用方法。
  • 函数式编程提供了一种新的方式,其简洁的表达了行为参数化。代码本身的含义就可以轻易的阅读处理,就像读一本漫画书一样轻松。
  • 它提供了更多的编程工具和概念,能以更快,更重要的是能以更为简洁、更易于维护的方式解决新的或现有的编程问题。

引用方法

使用场景

  • 我们用Lambda表达式来实现匿名方法,但是有些情况下,我们使用Lambda表达式仅仅是调用一些已经存在的方法,除了调用动作之外,没有其他任何多余的动作,在这种情况下,我们更倾向于通过方法名来调用它,而Lambda表达式可以帮助我们实现这一要求,它使得Lambda在调用那些已经拥有方法名的代码更加简洁、更容易理解。方法引用可以理解为Lambda表达式的另外一种表现形式。

方法引用的分类

Java函数式编程(一):为什么要关心Java8

方法引用使用举例

静态方法引用

  • 类名::静态方法
  • Person类
@Data
public class Person {

    private String  name;

    private Integer age;

    public static int compareByAge(Person a, Person b) {
        return a.age.compareTo(b.age);
    }
}

class PersonAgeComparator implements Comparator<Person> {
    public int compare(Person a, Person b) {
        return a.getAge().compareTo(b.getAge());
    }
}

  • 现假设,一个部门有30人,把他们放在一个数组中,并按照年龄排序,可以自顶一个比较器,如上
public class Client {
    public static void main(String[] args) {
        Random random = new Random();
        Person[] personList = new Person[30];
        for (int i = 0; i < 30; i++) {
            Person person = new Person();
            person.setName(String.valueOf(i));
            person.setAge(random.nextInt(40));
            personList[i] = person;
        }
        Arrays.sort(personList, new PersonAgeComparator());
        for (Person person : personList) {
            System.out.println(person);
        }
    }
}
  • Arrays.sort的声明为:public static void sort(T[] a, Comparator < ? super T > c),比较器参数Comparator为一个函数式接口,利用上一节Lambda表达式所学知识,可以改写为以下代码

public class Client {
    public static void main(String[] args) {
        Random random = new Random();
        Person[] personList = new Person[30];
        for (int i = 0; i < 30; i++) {
            Person person = new Person();
            person.setName(String.valueOf(i));
            person.setAge(random.nextInt(40));
            personList[i] = person;
        }
        Arrays.sort(personList, (a, b) -> {
            return a.getAge().compareTo(b.getAge());
        });
        for (Person person : personList) {
            System.out.println(person);
        }
    }
}
  • 然而,你会发现,Perdon类中已经有了一个静态方法的比较器:compareByAge,因此,我们改用Person类已经提供的比较器
 public class Client {
    public static void main(String[] args) {
        Random random = new Random();
        Person[] personList = new Person[30];
        for (int i = 0; i < 30; i++) {
            Person person = new Person();
            person.setName(String.valueOf(i));
            person.setAge(random.nextInt(40));
            personList[i] = person;
        }
        Arrays.sort(personList, (a,b) -> Person.compareByAge(a,b));
        for (Person person : personList) {
            System.out.println(person);
        }
    }
}
  • 以上代码,因为Lambda表达式调用了一个已经存在的静态方法,根据我们第2节表格中的语法,上面的代码可以最终改写成静态方法引用
public class Client {
    public static void main(String[] args) {
        Random random = new Random();
        Person[] personList = new Person[30];
        for (int i = 0; i < 30; i++) {
            Person person = new Person();
            person.setName(String.valueOf(i));
            person.setAge(random.nextInt(40));
            personList[i] = person;
        }
        Arrays.sort(personList, Person::compareByAge);
        for (Person person : personList) {
            System.out.println(person);
        }
    }
}

实例方法引用

  • 实例::非静态方法
  • 实例方法引用,顾名思义就是调用已经存在的实例的方法,与静态方法引用不同的是类要先实例化,静态方法引用类无需实例化,直接用类名去调用。
public class TestInstanceReference {
    public static void main(String[] args) {
        Person person = new Person();
        person.setName("icanci");
        person.setAge(18);
        Supplier<String> supplier = () -> person.getName();
        System.out.println(supplier.get());
        Supplier<String> supplier2 = person::getName;
        System.out.println(supplier2.get());
    }
}

对象方法引用

  • 实例::静态方法
  • 若Lambda参数列表中的第一个参数是实例方法的参数调用者,而第二个参数是实例方法的参数时,可以使用对象方法引用。String的equals()方法
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}
public class StringTest {
    public static void main(String[] args) {
        BiPredicate<String, String> bp = (x, y) -> x.equals(y);
        BiPredicate<String, String> bp1 = String::equals;
        boolean test = bp1.test("xy", "xx");
        System.out.println(test);
    }
}
  • BiPredicate的test()方法接受两个参数,x和y,具体实现为x.equals(y),满足Lambda参数列表中的第一个参数是实例方法的参数调用者,而第二个参数是实例方法的参数,因此可以使用对象方法引用。

构造方法的引用

  • 类名::new
  • 注意:需要调用的构造器的参数列表要与函数式接口中抽象方法的参数列表保持一致
  • 如:要获取一个空的Person列表
public class Client2 {
    public static void main(String[] args) {
        Supplier<List<Person>> supplier = () -> new ArrayList<Person>();
        List<Person> personList = supplier.get();
        Supplier<List<Person>> supplier2 = ArrayList<Person>::new;
        List<Person> personList1 = supplier2.get();
    }
}

Java中的函数

  • 编程语言中的函数一词通常是指方法,尤其是静态方法;这是在数学函数,也就是没有副作用的函数之外的新含义。在Java 8谈到函数时,这两种用法几乎是一致的。
  • Java 8中新增了函数——值的一种新形式
  • Java可以操作任何值,编程语言的整个目的就在于操作值,如果按照历史上编程语言的传统,这些值称为一等值。编程语言中的其他结构也许有助于我们表示值的结构,但在程序执行期间不能传递,因而是二等值。
  • 前面所说的值是Java中的一等值,但其他很多Java概念(如方法和类等)则是二等值。用方法来定义类很不错,类还可以实例化来产生值,但方法和类本身都不是值。这又有什么关系呢?还真有,人们发现,在运行时传递方法能将方法变成一等公民。
  • 方法和Lambda作为一等值
  • Scala和Groovy等语言证明,让方法等概念称为一等值可以扩充程序员的工具库,让编程变得更加容易。其也成为Stream的基础。
  • 函数参见上文:引用方法
  • 方法引用:把这个方法作为值。与用对象引用传递对象类似(对象引用是用new创建的),在Java 8里写下File::isHidden的时候,你就创建了一个方法引用,你同样可以传递它,参见以下案例
public class FileTest {
    public static void main(String[] args) {
        File[] files = new File(".").listFiles(new FileFilter() {
            @Override
            public boolean accept(File file) {
                return file.isHidden();
            }
        });
    }
}
public class FileTest {
    public static void main(String[] args) {
        File[] files = new File(".").listFiles(File::isHidden);
    }
}
  • Lambda 匿名函数 ,后续会讲
  • 使用这些概念的程序为函数式编程风格,这句话的意思是“编写把函数作为一等值来传递的程序”
  • 传递代码的例子
  • 有个Apple类和获取Apple的Client
@Data
public class Apple {
    private String color;
    private int    weight;
}
public class AppleClient {
    public static List<Apple> getApples() {
        List<Apple> apples = new ArrayList<>();

        Apple apple1 = new Apple();
        apple1.setColor("red");
        apple1.setWeight(180);

        Apple apple2 = new Apple();
        apple2.setColor("red");
        apple2.setWeight(150);

        Apple apple3 = new Apple();
        apple3.setColor("green");
        apple3.setWeight(160);

        apples.add(apple1);
        apples.add(apple2);
        apples.add(apple3);

        return apples;
    }
}

  • 筛选绿色苹果
public class Test1 {
    public static void main(String[] args) {
        System.out.println(filterGreenApples(AppleClient.getApples()));
    }

    public static List<Apple> filterGreenApples(List<Apple> apples) {
        List<Apple> arrayList = new ArrayList<>();
        for (Apple apple : apples) {
            if ("green".equals(apple.getColor())) {
                arrayList.add(apple);
            }
        }
        return arrayList;
    }
}
// [Apple(color=green, weight=160)]
  • 筛选超过150g的苹果
public class Test1 {
    public static void main(String[] args) {
        System.out.println(filterHeavyApples(AppleClient.getApples()));
    }

    public static List<Apple> filterHeavyApples(List<Apple> apples) {
        List<Apple> arrayList = new ArrayList<>();
        for (Apple apple : apples) {
            if (apple.getWeight() > 150) {
                arrayList.add(apple);
            }
        }
        return arrayList;
    }
}
// [Apple(color=red, weight=180), Apple(color=green, weight=160)]
  • 上述的方法只有一行不同,其他都是一样的,现在将其抽取出来,如下
@Data
public class Apple {
    private String color;
    private int    weight;

    public static boolean isHeavyApple(Apple apple) {
        return apple.getWeight() > 150;
    }

    public static boolean isGreenApple(Apple apple) {
        return "green".equals(apple.getColor());
    }
}
public class Test2 {
    public static void main(String[] args) {
        // 是不是非常简单了
        System.out.println(filterApples(AppleClient.getApples(), Apple::isGreenApple));
        System.out.println(filterApples(AppleClient.getApples(), Apple::isHeavyApple));
    }

    public static List<Apple> filterApples(List<Apple> apples, Predicate<Apple> predicate) {
        List<Apple> arrayList = new ArrayList<>();
        for (Apple apple : apples) {
            if (predicate.test(apple)) {
                arrayList.add(apple);
            }
        }
        return arrayList;
    }
}
public interface Predicate<T> {
    boolean test(T t);
}
  • 这只是初步了解这种爽快,后续会深入学习。
  • 从传递方法到Lambda
public class Test3 {
    public static void main(String[] args) {
        System.out.println(filterApples(AppleClient.getApples(), (Apple a) -> "green".equals(a.getColor())));
        System.out.println(filterApples(AppleClient.getApples(), (Apple a) -> a.getWeight() > 150));
    }
    public static List<Apple> filterApples(List<Apple> apples, Predicate<Apple> predicate) {
        List<Apple> arrayList = new ArrayList<>();
        for (Apple apple : apples) {
            if (predicate.test(apple)) {
                arrayList.add(apple);
            }
        }
        return arrayList;
    }
}
  • 如上,这已经不需要为只用1次的方法做定义;代码更加干净、更清晰,不需要去找自己传递了什么代码。
  • 如果不考虑并行,Stream已经结束了。

流处理

  • Java 8在java.util.stream中添加了一个Stream API;
  • Stream就是一系列T类型的项目。可以把它看成一种比较花哨的迭代器。
  • Stream API的很多方法可以链接起来形成一个复杂的流水线。
  • 举例:在Unix或者Linux系统中,很多程序都支持标准输入。Unix的cat命令会把两个文件连接起来创建一个流,tr会转换流中的字符,sort会对流中的行进行排序,而tail-3则给出流的最后三行。Unix命令行允许这些程序通过管道(|)连接在一起,如下
cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3
  • 假设file1和file2每行都只有一个单词,会先把字母转成小写字母。然就打印了按照字段排序出现咋最后的三个单词
  • 请注意在Unix中,命令cat、tr、sort和tail 是同时执行的,这样sort就可以在cat或者tr完成前先处理头几行。
  • 基于这种思想,Stream API。Stream API的很多方法可以链接起来形成一个复杂的流水线,就像先前例子里面链接起来的Unix命令一样。
  • 现在你可以在一个更高的抽象层次上写Java 8程序了:思路变成了把这样的流变成那样的流(就像写数据库查询语句时的那种思路),而不是一次只处理一个项目。并且可以简单的方式处理并行。
  • 用行为参数化吧代码传递给方法
  • 并行与共享的可变数
  • Java需要演变,才能持续立足于编程世界
  • 几乎每个Java应用都会制造和处理集合。但集合用起来并不总是那么理想。比方说,你需要从一个列表中筛选金额较高的交易,然后按货币分组。你需要写一大堆套路化的代码来实现这个数据处理命令。就和上述的筛选苹果一样,当然,苹果的案例只是简单案例。
  • 针对目标数据的遍历和取值,非常麻烦;而且对于大数据内存,单个CPU无法高效的处理,但是其他的CPU又用不上,这就是很头疼的问题。
  • 所以Java8Stream流,支持并行流。这个在后面章节会讲解。
  • Java的Stream解决了2个问题:集合处理时的套路和晦涩;以及难以利用多核。
  • 多线程很难,比如两个线程同时向共享变量sum加上一个数时,可能出现的问题

Java函数式编程(一):为什么要关心Java8

  • 一些筛选操作可以并行化,如下图:在两个CPU上筛选列表,可以让一个CPU处理列表的前一半,第二个CPU处理后一半,这称为分支步骤(1)。CPU随后对各自的半个列表做筛选(2)。最后(3),一个CPU会把两个结果合并起来。

Java函数式编程(一):为什么要关心Java8

  • 到这里,我们只是说新的Stream API和Java现有的集合API的行为差不多:它们都能够访问数据项目的序列。不过,现在最好记得,Collection主要是为了存储和访问数据,而Stream则主要用于描述对数据的计算。这里的关键点在于,Stream允许并提倡并行处理一个Stream中的元素。
  • 稍微体验一下,如何利用Stream和Lambda表达式顺序或并行地从一个列表里筛选绿色的苹果。
// 顺序处理
public class Test4 {
    public static void main(String[] args) {
        AppleClient.getApples().stream().filter((Apple a) -> "green".equals(a.getColor())).collect(Collectors.toList());
    }
}
// 并行处理
public class Test4 {
    public static void main(String[] args) {
        AppleClient.getApples().parallelStream().filter((Apple a) -> "green".equals(a.getColor())).collect(Collectors.toList());
    }
}
  • Java中并行和无共享可变状态
    • 并行执行某段逻辑
    • 函数式编程中的函数的主要意思是“把函数作为一等值”,不过它也常常隐含着第二层意思,即“执行时在元素之间无互动”
  • 可以友好的处理NPE:空指针异常

为什么要新增默认方法和静态方法

  • 为了适配Stream方法,因为集合在Java中最常用,如果在顶级接口Collection添加了一个方法,需要子类去实现,那将是整个Java生态的灾难,所有的版本要不不升级,要么全部的实现都需要修改。
  • 你如何改变已发布的接口而不破坏已有的实现呢?所以索性直接改JDK,提供默认实现的方法,解决。使用 default 关键字修饰

小结

  • 请记住语言生态系统的思想,以及语言面临的“要么改变,要么淘汰”的压力。虽然Java可能现在非常有活力,但你可以回忆一下其他曾经也有活力但未能及时改进的语言的命运,如COBOL。
  • Java 8中新增的核心内容提供了令人激动的新概念和功能,方便我们编写既有效又简洁的程序。
  • 现有的Java编程实践并不能很好地利用多核处理器。
  • 函数是一等值;记得方法如何作为函数式值来传递,还有Lambda是怎样写的。
  • Java 8中Streams的概念使得Collections的许多方面得以推广,让代码更为易读,并允许并行处理流元素。
  • 你可以在接口中使用默认方法,在实现类没有实现方法时提供方法内容。
转载自:https://juejin.cn/post/7270421682140446739
评论
请登录