coding……
但行好事 莫问前程

Java8函数式编程

java中存在一个概念,一切皆是对象。在 Java 中定义的函数或方法不可能完全独立,也不能将方法作为参数或返回一个方法给实例。如果要给一个方法传递函数功能,只能通过匿名类的方法。如下所示:

button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        System.out.println("button clicked");
    }
});

在上面的例子里,为了给Button监听器添加自定义代码,我们定义了一个匿名内部类ActionListener并创建了它的对象,通过这种方式,我们将一些函数功能传给 actionPerformed方法。(ActionListener是一个接口,这里new了一个类实现了ActionListener接口,然后重写了actionPerformed方法。actionPerformed方法接收ActionEvent类型参数),显然这种方法是很繁琐的,因为其实我关心的只是打按钮点击时,打印”button clicked”这句话,匿名内部类并不是我关注的信息。为此,Java 8 增加了一个语言级的新特性,名为 Lambda 表达式

lamda表达式简述

Lambda 表达式可以理解是一种匿名函数,简单地说,它是没有声明的方法,也即没有访问修饰符、返回值声明和名字。你可以将其想做一种速记,在你需要使用某个方法的地方写上它。当某个方法只使用一次,而且定义很简短,使用这种速记替代之尤其有效,这样,你就不必在类中费力写声明与方法了。Java中Lambda 表达式一共有五种基本形式,具体如下:

  • Lambda表达式不包含参数,使用空括号()表示没有参数。如下Lambda表达式实现了Runnable接口,该接口也只有一个run方法,没有参数,且返回类型为void。
Runnable noArguments = () -> System.out.println("Hello World");
  • Lambda表达式包含且只包含一个参数,可省略参数的括号。
ActionListener oneArgument = event -> System.out.println("button clicked");
  • Lambda表达式的主体是一个代码块,使用大括号 {}将代码块括起来
Runnable multiStatement = () -> {
    System.out.print("Hello");
    System.out.println(" World");
};
  • Lambda表达式表示包含多个参数的方法
BinaryOperator<Long> add = (x, y) -> x + y;

ps:(x, y) -> x +y这个lambda表达式创建了一个函数,用来计算 两个数字相加的结果,BinaryOperator<T> add可以理解为定义了一个对象add,当通过add调用接口中的apply方法时,lambda表达式定义的函数就是apply方法的实现。如add.apply(3L, 4L);即可获取结果,但是我并没有定义一个继承与BinaryOperator的add类,并实现apply方法。

  • 声明参数类型
BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;

函数接口

函数式接口是只包含一个抽象方法声明的接口。java.lang.Runnable就是一种函数式接口,在Runnable接口中只声明了一个方法void run(),相似地,ActionListener接口也是一种函数式接口,我们使用匿名内部类来实例化函数式接口的对象,有了 Lambda 表达式,这一方式可以得到简化。

每个 Lambda 表达式都能隐式地赋值给函数式接口,例如,我们可以通过Lambda表达式创建Runnable接口的引用。

Runnable r = () -> System.out.println("hello world");

当不指明函数式接口时,编译器会自动解释这种转化:

new Thread(
   () -> System.out.println("hello world")
).start();

因此,在上面的代码中,编译器会自动推断:根据线程类的构造函数签名 public Thread(Runnable r) { },将该 Lambda 表达式赋给 Runnable 接口。

JDK 8 中提供了一组常用的核心函数接口:

接口 参数 返回类型 描述
Predicate<T> T boolean 用于判别一个对象。比如求一个人是否为男性
Consumer<T> T void 用于接收一个对象进行处理但没有返回,比如接收一个人并打印他的名字
Function<T, R> T R 转换一个对象为不同类型的对象
Supplier<T> None T 提供一个对象
UnaryOperator<T> T T 接收对象并返回同类型的对象
BinaryOperator<T> (T, T) T 接收两个同类型的对象,并返回一个原类型对象

集合处理

Stream 简介

在程序编写过程中,集合的处理应该是很普遍的。Java 8 中,引入了流(Stream)的概念,所有继承自 Collection 的接口都可以转换为 Stream。假设我们有一个 List包含一系列的Person,Person有姓名name 和年龄 age 连个字段,现要求这个列表中年龄大于 20 的人数。

通常按照以前我们可能会这么写:

long count = 0;
for (Person p : persons) {
    if (p.getAge() > 20) {
        count ++;
    }
}

但如果使用stream的话(链式调用),则会简单很多:

long count = persons.stream()
                    .filter(person -> person.getAge() > 20)
                    .count();

Stream 常用操作

Stream 的方法分为两类。一类叫惰性求值,一类叫及早求值。

判断一个操作是惰性求值还是及早求值很简单:只需看它的返回值。如果返回值是 Stream,那么是惰性求值。其实可以这么理解,如果调用惰性求值方法,Stream 只是记录下了这个惰性求值方法的过程,并没有去计算,等到调用及早求值方法后,就连同前面的一系列惰性求值方法顺序进行计算,返回结果。

通用形式为:

Stream.惰性求值.惰性求值. ... .惰性求值.及早求值

collect(toList())

collect(Collectors.toList()) 方法由Stream里的值生成一个列表,是一个及早求值操作。可以理解为Stream向Collection 的转换。如下:

List<String> collected = Stream.of("a", "b", "c")
                               .collect(Collectors.toList());
assertEquals(Arrays.asList("a", "b", "c"), collected);

map

如果有一个函数可以将一种类型的值转换成另外一种类型,map操作就可以使用该函数,将一个流中的值转换成一个新的流。如下将一个List的每一个元素都转化进行平方操作。

List<String> collected = Stream.of("a", "b", "hello")
        .map(string -> string.toUpperCase())
        .collect(Collectors.toList());
assertEquals(Arrays.asList("A", "B", "HELLO"), collected);

filter

遍历数据并检查其中的元素时,可尝试使用Stream中提供的新方法filter方法,传入过滤条件,对元素进行过滤。如下可以将以数字开头的元素过滤出来,组成一个新的List。

List<String> beginningWithNumbers =
        Stream.of("a", "1abc", "abc1")
                .filter(value -> Character.isDigit(value.charAt(0)))
                .collect(Collectors.toList());
assertEquals(Arrays.asList("1abc"), beginningWithNumbers);

flatMap

flatMap 方法可以将多个Stream连接成一个Stream,常用作合并多个Collection。如下:

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

max和min

Stream的Max和Min方法可以接收一个Comparator函数式接口,返回集合最大值和最小值,如下:

List<Integer> arrList = Arrays.asList(2, 5, 3, 9, 1);
int maxInt = arrList.stream()
        .max((x, y) -> x.compareTo(y))
        .get();
System.out.println(maxInt);
int minInt = arrList.stream()
        .min((x, y) -> x.compareTo(y))
        .get();
System.out.println(minInt);

也可以使用Java8的新特性,“方法引用”,如下:

List<Integer> arrList = Arrays.asList(2, 5, 3, 9, 1);
int maxInt = arrList.stream()
        .max(Integer::compareTo)
        .get();
System.out.println(maxInt);
int minInt = arrList.stream()
        .min(Integer::compareTo)
        .get();
System.out.println(minInt);

reduce

reduce操作可以实现从一组值中生成一个值。在上述例子中用到的count、min 和max方法,因为常用而被纳入标准库中。事实上,这些方法都是reduce操作。如下reduce的第一个参数是一个初始值,计算0 + 1 + 2 + 3 +4 的和。

int result = Stream.of(1, 2, 3, 4)
        .reduce(0, (acc, element) -> acc + element);
System.out.println(result);

数据并行化操作

数据并行化是指将数据分成块,为每块数据分配单独的处理单元。这样可以充分利用多核 CPU 的优势。并行化操作流只需改变一个方法调用。如果已经有一个Stream对象,调用它的parallel()方法就能让其拥有并行操作的能力。如果想从一个集合类创建一个流,调用parallelStream()就能立即获得一个拥有并行能力的流。如下获取一个字符串列表中各个字符串长度总和。

int sumSize = Stream.of("Apple", "Banana", "Orange", "Pear")
        .parallel()
        .map(String::length)
        .reduce(Integer::sum)
        .get();
System.out.println(sumSize);

ps:如果你去计算这段代码所花的时间,很可能比不加上 parallel() 方法花的时间更长。这是因为数据并行化会先对数据进行分块,然后对每块数据开辟线程进行运算,这些地方会花费额外的时间。并行化操作只有在数据规模比较大或者数据的处理时间比较长的时候才能体现出有事,所以并不是每个地方都需要让数据并行化,应该具体问题具体分析。

测试代码 卓立-码云-java8函数式编程

参考

  1. 深入浅出 Java 8 Lambda 表达式
  2. 函数式编程初探
  3. JDK 8 函数式编程入门

赞(0) 打赏
Zhuoli's Blog » Java8函数式编程
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址