Java8函数式编程

最近使用lambda表达式,感受使用起来很是舒服,箭头函数极大加强了代码的表达能力。因而决心花点时间深刻地去研究一下java8的函数式。html

1、lambda表达式

先po一个最经典的例子——线程java

public static void main(String[] args) {
  // Java7
  new Thread(new Runnable() {
    @Override
    public void run() {
      for (int i = 0; i < 100; i++) {
        System.out.println(i);
      }
    }
  }).start();

  // Java8
  new Thread(() -> {
    for (int i = 0; i < 100; i++) {
      System.out.println(i);
    }
  }).start();
}
复制代码

第一次接触lambda表达式是在建立线程时,比较直观的感觉就是lambda表达式至关于匿名类的语法糖,emm~,真甜。不过事实上,lambda表达式并非匿名类的语法糖,并且通过一段时间的使用,感受偏偏相反,在使用上匿名类更像是Java中lambda表达式的载体。api

使用场景

下面的一些使用场景均为我的的一些体会,可能存在不当或遗漏之处。数组

1. 简化匿名类的编码

上面的建立线程就是一个很好简化编码的例子,此处就再也不重复。多线程

2. 减小没必要要的方法建立

在Java中,咱们常常会遇到这样一种场景,某个方法只会在某处使用且内部逻辑也很简单,在Java8以前咱们一般都会建立一个方法,可是事实上咱们常常会发现这样写着写着,一个类中的方法可能会变得很是庞杂,严重影响阅读体验,进而影响编码效率。可是若是使用lambda表达式,那么这个问题就能够很容易就解决掉了。oracle

一个简单的例子,若是咱们须要在一个函数中屡次打印时间。(这个例子可能有些牵强,可是实际上仍是挺常碰见的)app

public class FunctionMain {
    
    public static void main(String[] args) {
        TimeDemo timeDemo = new TimeDemo();
        timeDemo.createTime = System.currentTimeMillis();
        timeDemo.updateTime = System.currentTimeMillis() + 10000;
        outputTimeDemo(timeDemo);
    }

    private static void outputTimeDemo(TimeDemo timeDemo) {
        Function timestampToDate = timestamp -> {
            DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            return df.format(new Date(timestamp));
        };

        System.out.println(timestampToDate.apply(timeDemo.createTime));
        System.out.println(timestampToDate.apply(timeDemo.updateTime));
    }


    interface Function {
        String apply(long timestamp);
    }
}

class TimeDemo {
    long createTime;
    long updateTime;
}
复制代码

在这段代码的outputTimeDemo中咱们能够看到,对于时间戳转换的内容,咱们并无额外建立一个方法,而是相似于建立了一个变量来表达。不过,这个时候出现了另外一个问题,虽然咱们少建立了一个方法,可是咱们却多建立了一个接口Function,总有种因小失大的感受, 不过这个问题,咱们在后面的java.util.function包部分能够找到答案。jvm

3. 事件处理

一个比较常见的例子就是回调。ide

public static void main(String[] args) {
    execute("hello world", () -> System.out.println("callback"));
}

private static void execute(String s, Callback callback) {
    System.out.println(s);
    callback.callback();
}

@FunctionalInterface
interface Callback {
    void callback();
}
复制代码

在这里,能够发现一点小不一样,就是Callback多了一个注解@FunctionalInterface,这个注解主要用于编译期检查,若是咱们的接口不符合函数式接口的要求,那编译的时候就会报错。不加也是能够正常执行的。函数

4. stream中使用

这个在后面的stream中详解。

java.util.function包

在以前的例子中,咱们发现使用lambda表达式的时候,常常须要定义一些接口用来辅助咱们的编码,这样就会使得本应轻量级的lambda表达式又变得重量级。那是否存在解决方案呢?其实Java8自己已经为咱们提供了一些常见的函数式接口,就在java.util.function包下面。

接口 描述
Function<T,R> 接受一个输入参数,返回一个结果
Supplier<T> 无参数,返回一个结果
Consumer<T> 接受一个输入参数,而且不返回任何结果
BiFunction<T,U,R> 接受两个输入参数的方法,而且返回一个结果
BiConsumer<T,U> 接受两个输入参数的操做,而且不返回任何结果

此处列出最基本的几个,其余的都是在这些的基础上作了一些简单的封装,例如IntFunction<R>就是对Function<T,R>的封装。上面的这些函数式接口已经能够帮助咱们处理绝大多数场景了,若是有更复杂的状况,那就得咱们本身定义接口了。不过遗憾的是在java.util.function下没找到无参数无返回结果的接口,目前我找到的方案就是本身定义一个接口或者直接使用Runnable接口。

使用示例

public static void main(String[] args) {
    Function<Integer, Integer> f = x -> x + 1;
    System.out.println(f.apply(1));

    BiFunction<Integer, Integer, Integer> g = (x, y) -> x + y;
    System.out.println(g.apply(1, 2));
}
复制代码

lambda表达式和匿名类的区别

lambda表达式虽然使用时和匿名类很类似,可是仍是存在那么一些区别。

1. this指向不一样

lambda表达式中使用this指向的是外部的类,而匿名类中使用this则指向的是匿名类自己。

public class FunctionMain {

    private String test = "test-main";

    public static void main(String[] args) {
        new FunctionMain().output();
    }

    private void output() {
        Function f = () -> {
            System.out.println("1:-----------------");
            System.out.println(this);
            System.out.println(this.test);
        };
        f.outputThis();

        new Function() {
            @Override
            public void outputThis() {
                System.out.println("2:-----------------");
                System.out.println(this);
                System.out.println(this.test);
            }
        }.outputThis();
    }

    interface Function {
        String test = "test-function";

        void outputThis();
    }
}
复制代码

如上面这段代码,输出结果以下

image-20190417113153242

因此若是想使用lambda表达式的同时去访问原类中的变量、方法的是作不到的。

2. 底层实现不一样

编译

从编译结果来看,二者的编译结果彻底不一样。

首先是匿名类的方式,代码以下:

import java.util.function.Function;

public class ClassMain {

    public static void main(String[] args) {
        Function<Integer, Integer> f = new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer integer) {
                return integer + 1;
            }
        };
        System.out.println(f.apply(1));
    }
}
复制代码

编译后的结果以下:

image-20190418185503781

能够看到ClassMain在编译后生成了两个class,其中ClassMain$1.class就是匿名类生成的class。


那么接下来,咱们再来编译一下lambda版本的。代码和编译结果以下:

import java.util.function.Function;

public class FunctionMain {

    public static void main(String[] args) {
        Function<Integer, Integer> f = x -> x + 1;
        System.out.println(f.apply(1));
    }
}
复制代码

image-20190418185243575

在这里咱们能够看到FunctionMain并无生成第二个class文件。

字节码

更进一步,咱们打开他们的字节码来寻找更多的细节。首先依然是匿名类的方式

image-20190418190414990

在Code-0这一行,咱们能够看到匿名类的方式是经过new一个类来实现的。


接下来是lambda表达式生成的字节码,

image-20190418190726628

在lambda表达式的字节码中,咱们能够看到咱们的lambda表达式被编译成了一个叫作lambda$main$0的静态方法,接着经过invokedynamic的方式进行了调用。

3. lambda表达式只能替代部分匿名类

lambda表达式想要替代匿名类是有条件的,即这个匿名类实现的接口必须是函数式接口,即只能有一个抽象方法的接口。

性能

因为没有实际测试过lambda表达式的性能,且我使用lambda更可能是基于编码简洁度的考虑,所以本文就不探讨性能相关问题。

关于lambda表达式和匿名类的性能对比能够参考官方ppt www.oracle.com/technetwork…

2、Stream API

Stream API是Java8对集合类的补充与加强。它主要用来对集合进行各类便利的聚合操做或者批量数据操做。

1. 建立流

在进行流操做的第一步是建立一个流,下面介绍几种常见的流的建立方式

从集合类建立流

若是已经咱们已经有一个集合对象,那么咱们能够直接经过调用其stream()方法获得对应的流。以下

List<String> list = Arrays.asList("hello", "world", "la");
list.stream();
复制代码

利用数组建立流

String[] strArray = new String[]{"hello", "world", "la"};
Stream.of(strArray);
复制代码

利用可变参数建立流

Stream.of("hello", "world", "la");
复制代码

根据范围建立数值流

IntStream.range(0, 100);         // 不包含最后一个数
IntStream.rangeClosed(0, 99);    // 包含最后一个数
复制代码

BufferReader.lines()

对于BufferReader而言,它的lines方法也一样能够建立一个流

File file = new File("/Users/cayun/.m2/settings.xml");
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
br.lines().forEach(System.out::println);
br.close();
复制代码

2. 流操做

在Stream API中,流的操做有两种:Intermediate和Terminal

Intermediate:一个流能够后面跟随零个或多个 intermediate 操做。其目的主要是打开流,作出某种程度的数据映射/过滤,而后返回一个新的流,交给下一个操做使用。这类操做都是惰性化的(lazy),就是说,仅仅调用到这类方法,并无真正开始流的遍历。 Terminal:一个流只能有一个 terminal 操做,当这个操做执行后,流就被使用“光”了,没法再被操做。因此这一定是流的最后一个操做。Terminal 操做的执行,才会真正开始流的遍历,而且会生成一个结果,或者一个 side effect。

除此之外,还有一种叫作short-circuiting的操做

对于一个 intermediate 操做,若是它接受的是一个无限大(infinite/unbounded)的 Stream,但返回一个有限的新 Stream。 对于一个 terminal 操做,若是它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。

常见的流操做能够以下归类:

Intermediate map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered

Terminal forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

Short-circuiting anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

常见的流操做详解

1. forEach

forEach能够说是最多见的操做了,甚至对于List等实现了Collection接口的类能够不建立stream而直接使用forEach。简单地说,forEach就是遍历并执行某个操做。

Stream.of("hello", "world", "a", "b").forEach(System.out::println);
复制代码

2. map

map也一样是一个很是高频的流操做,用来将一个集合映射为另外一个集合。下面代码展现了将[1,2,3,4]映射为[1,4,9,16]

IntStream.rangeClosed(1, 4).map(x -> x * x).forEach(System.out::println);
复制代码

除此以外,还有一个叫作flatMap的操做,这个操做在映射的基础上又作了一层扁平化处理。这个概念可能比较难理解,那举个例子,咱们须要将["hello", "world"]转换成[h,e,l,l,o,w,o,r,l,d],能够尝试一下使用map,那你会惊讶地发现,可能结果不是你想象中的那样。若是不信能够执行下面这段代码,就会发现map与flatMap之间的区别了,

Stream.of("hello", "world").map(s -> s.split("")).forEach(System.out::println);
System.out.println("--------------");
Stream.of("hello", "world").flatMap(s -> Stream.of(s.split(""))).forEach(System.out::println);
复制代码

3. filter

filter则实现了过滤的功能,若是只须要[1,2,3,4,5]中的奇数,能够以下,

IntStream.rangeClosed(1, 5).filter(x -> x % 2 == 1).forEach(System.out::println);
复制代码

4. sorted和distinct

其中sorted表示排序,distinct表示去重,简单的示例以下:

Integer[] arr = new Integer[]{5, 1, 2, 1, 3, 1, 2, 4};    // 千万不要用int
Stream.of(arr).sorted().forEach(System.out::println);
Stream.of(arr).distinct().forEach(System.out::println);
Stream.of(arr).distinct().sorted().forEach(System.out::println);
复制代码

5. collect

在流操做中,咱们每每需求是从一个List获得另外一个List,而不是直接经过forEach来打印。那么这个时候就须要使用到collect了。依然是以前的例子,将[1,2,3,4]转换成[1,4,9,16]。

List<Integer> list1= Stream.of(1, 2, 3, 4).map(x -> x * x).collect(Collectors.toList());
        
// 对于IntStream生成的流须要使用mapToObj而不是map
List<Integer> list2 = IntStream.rangeClosed(1, 4).mapToObj(x -> x * x).collect(Collectors.toList());
复制代码

3. 补充

并行流

除了普通的stream以外还有parallelStream,区别比较直观,就是stream是单线程执行,parallelStream为多线程执行。parallelStream的建立及使用基本与stream相似,

List<Integer> list = Arrays.asList(1, 2, 3, 4);
// 直接建立一个并行流
list.parallelStream().map(x -> x * x).forEach(System.out::println);
// 或者将一个普通流转换成并行流
list.stream().parallel().map(x -> x * x).forEach(System.out::println);
复制代码

不过因为是并行执行,parallelStream并不保证结果顺序,一样因为这个特性,若是能使用findAny就尽可能不要使用findFirst。

使用parallelStream时须要注意的一点是,多个parallelStream之间默认使用的是同一个线程池,因此IO操做尽可能不要放进parallelStream中,不然会阻塞其余parallelStream。

3、Optional

Optional的引入是为了解决空指针异常的问题,事实上在Java8以前,Optional在不少地方已经较为普遍使用了,例如scala、谷歌的Guava库等。

在实际生产中咱们常常会遇到以下这种状况,

public class FunctionMain {

    public static void main(String[] args) {
        Person person = new Person();
        String result = null;
        if (person != null) {
            Address address = person.address;
            if (address != null) {
                Country country = address.country;
                if (country != null) {
                    result = country.name;
                }
            }
        }
        System.out.println(result);
    }
}

class Person {
    Address address;
}

class Address {
    Country country;
}

class Country {
    String name;
}
复制代码

往往写到这样的代码,做为编码者必定都会头皮发麻,满心地不想写,可是却不得不写。这个问题若是使用Optional,或许你就能找到你想要的答案了。

Optional的基本操做

1. 建立Optional

Optional.empty();          // 建立一个空Optional
Optional.of(T value);      // 不接受null,会报NullPointerException异常
Optional.ofNullable(T value);     // 能够接受null
复制代码

2. 获取结果

get();                                   // 返回里面的值,若是值为null,则抛异常
orElse(T other);                         // 有值则返回值,null则返回other
orElseGet(Supplier other);               // 有值则返回值,null则由提供的lambda表达式生成值
orElseThrow(Supplier exceptionSupplier); // 有值则返回值,null则抛出异常
复制代码

3. 判断是否为空

isPresent();       // 判断是否为空
复制代码

到这里,咱们可能会开始考虑怎么用Optional解决引言中的问题了,因而思考半天,写出了这样一段代码,

public static void main(String[] args) {
    Person person = new Person();
    String result = null;
    Optional<Person> per = Optional.ofNullable(person);
    if (per.isPresent()) {
        Optional<Address> address = Optional.ofNullable(per.get().address);
        if (address.isPresent()) {
            Optional<Country> country = Optional.ofNullable(address.get().country);
            if (country.isPresent()) {
                result = Optional.ofNullable(country.get().name).orElse(null);
            }
        }
     }
     System.out.println(result);
}
复制代码

啊嘞嘞,感受不只没有使得代码变得简单,反而变得更加复杂了。那么很显然这并非Optional的正确使用方法。接下来的部分才是Optional的正确使用方式。

4. 链式方法

在Optional中也有相似于Stream API中的链式方法map、flatMap、filter、ifPresent。这些方法才是Optional的精髓。此处以最典型的map做为例子,能够看看map的源码

public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else {
        return Optional.ofNullable(mapper.apply(value));
    }
}
复制代码

源码很简单,能够看到对于null状况仍然返回null,不然返回处理结果。那么此再来思考一下引言的问题,那就能够很简单地改写成以下的写法,

public static void main(String[] args) {
    Person person = new Person();
    String result = Optional.ofNullable(person)
            .map(per -> per.address)
            .map(address -> address.country)
            .map(country -> country.name).orElse(null);
    System.out.println(result);
}
复制代码

哇哇哇,相比原先的null写法真真是舒服太多了。

map与flatMap的区别

这二者的区别,一样使用一个简单的例子来解释一下吧,

public class FunctionMain {

    public static void main(String[] args) {
        Person person = new Person();
        String name = Optional.ofNullable(person).flatMap(p -> p.name).orElse(null);
        System.out.println(name);
    }
}

class Person {
    Optional<String> name;
}
复制代码

在这里使用的不是map而是flatMap,稍微观察一下,能够发现Person中的name再也不是String类型,而是Optional<String>类型了,若是使用map的话,那map的结果就是Optional<Optional<String>>了,很显然不是咱们想要的,flatMap就是用来将最终的结果扁平化(简单地描述,就是消除嵌套)的。

至于filter和ifPresent用法相似,就再也不叙述了。

4、其余一些函数式概念在Java中的实现

因为我的目前为止也只是初探函数式阶段,不少地方了解也很少,此处只列举两个。(注意:下面的部分应用函数与柯里化对应的是scala中的概念,其余语言中可能略有误差)

部分应用函数(偏应用函数)

部分应用函数指的是对于一个有n个参数的函数f,可是咱们只提供m个参数给它(m < n),那么咱们就能够获得一个部分应用函数,简单地描述一下,以下

f(x,y,z) = x + y + z
g(x,y) = f(x,y,3)

在这里g就是f的一个部分应用函数。

BiFunction<Integer, Integer, Integer> f = (x, y) -> x + y;
Function<Integer, Integer> g = x -> f.apply(1, x);
System.out.println(g.apply(2));
复制代码

柯里化

柯里化就是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,而且返回接受余下的参数并且返回结果的新函数的技术。换个描述,以下

f(x, y, z)  \rightarrow  f(x)(y)(z)

Java中对柯里化的实现以下,

Function<Integer, Function<Integer, Integer>> f = x -> y -> x + y;
System.out.println(f.apply(1).apply(2));
复制代码

由于Java限制,咱们不得不写成f.apply(1).apply(2)的形式,不过视觉上的体验与直接写成f(1)(2)相差就很大了。

柯里化与部分应用函数感受很相像,不过由于我的几乎未使用过这二者,所以此处就不发表更多看法。

参考

[1] java.util.stream 库简介
[2] Java 8 中的 Streams API 详解
[3] 了解、接受和利用Java中的Optional(类)
[4] 维基百科-柯里化
[5] 维基百科-λ演算

相关文章
相关标签/搜索