写在前面

前面说过,判断一门语言是否支持函数式编程,一个重要的判断标准就是:它是否将函数看做是“第一等公民(first-class citizens)”。
函数是“第一等公民”,意味着函数和其它数据类型具备同等的地位——可以赋值给某个变量,可以作为另一个函数的参数,也可以作为另一个函数的返回值。

Java 8是通过函数式接口,赋予了函数“第一等公民”的特性。

本文将详细介绍Java 8中的函数式接口。

本文的示例代码可从gitee上获取:https://gitee.com/cnmemset/javafp

函数式接口

什么是函数式接口(function interface)?只有一个抽象方法的接口都属于函数式接口。

按照规范,我们强烈建议在定义函数式接口时,加上注解 @FunctionalInterface,这样在编译阶段就可以判断该接口是否符合函数式接口的规范。当然,也可以不加注解 @FunctionalInterface,这并不影响函数式接口的定义和使用。

以下是一个典型的函数式接口 Consumer:

// 强烈建议加上注解 @FunctionalInterface
@FunctionalInterface
public interface Consumer<T> {
    // 唯一的抽象方法
    void accept(T t);     // 可以有多个非抽象方法(默认方法)
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

函数式接口本质是一个接口(interface),所以我们可以通过一个具体的类(包括匿名类)来实现一个函数式接口。但与普通接口不同,函数式接口的实现还可以是一个lambda表达式,甚至可以是一个方法引用(method reference)。

下面,我们逐一介绍JDK中内置的一些典型的函数式接口。

Java 8中内置的函数式接口

Java 8新增的内置函数式接口都在包 java.util.function 中定义,主要包括:

1. Functions

在代码世界,最为常见的一种函数式接口是接收一个参数值,然后返回一个响应值。JDK提供了一个标准的泛型函数式接口 Function:

@FunctionalInterface
public interface Function<T, R> {
    /**
     * 给定一个类型为 T 的参数 t ,返回一个类型为 R 的响应值。
     *
     * @param t 函数参数
     * @return 执行结果
     */
    R apply(T t);
    ...
}

Function的一个经典应用场景是Map的computeIfAbsent函数。

public V computeIfAbsent(K key,
                         Function<? super K, ? extends V> mappingFunction);

computeIfAbsent函数会先判断对应key在map中是否存在,如果key不存在,则通过参数 mappingFunction 来计算得出一个value,并将这个键值对<key, value="">写入到map中,并返回计算出来的value。如果key已存在,则返回map中key对应的value。</key,>

假设一个应用场景,我们要构建一个HashMap,key是某个单词,value是单词的字母长度。实例代码如下:

public static void testFunctionWithLambda() {
    // 构建一个HashMap,key是某个单词,value是单词的字母长度
    Map<String, Integer> wordMap = new HashMap<>();
    Integer wordLen = wordMap.computeIfAbsent("hello", s -> s.length());
    System.out.println(wordLen);
    System.out.println(wordMap);
}

上面的实例会输出:
5
{hello=5}

注意到代码片段“s -> s.length()”,这是一个典型的lambda表达式,含义等同于函数:

public static int getStringLength(String s) {
    return s.length();
}

更详尽具体的lambda表达式的介绍可以参考随后的系列文章。

之前提到过,函数式接口也可以通过一个方法引用(method reference)来实现。实例代码如下:

public static void testFunctionWithMethodReference() {
    Map<String, Integer> wordMap = new HashMap<>();
    Integer wordLen = wordMap.computeIfAbsent("hello", String::length);
    System.out.println(wordLen);
    System.out.println(wordMap);
}

注意到方法引用“String::length”,Java 8允许我们将一个实例方法转化成一个函数式接口的实现。 它的含义和 lambda 表达式 “s -> s.length()” 是相同的。

更详尽具体的方法引用的介绍可以参考随后的系列文章。

—BiFunction
Function 限制了只能有一个参数,但两个参数的情形也非常常见,所以就有了BiFunction,它接收两个参数值,然后返回一个响应值。

@FunctionalInterface
public interface BiFunction<T, U, R> {
    /**
     * 给定类型分别为 T 的参数 t 和 类型为 U 的参数 u,返回一个类型为 R 的响应值。
     *
     * @param t 第一个参数
     * @param u 第二个参数
     * @return 执行结果
     */
    R apply(T t, U u);     ...
}

Function的一个经典应用场景是Map的replaceAll函数。

public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)

Map的replaceAll函数,会遍历Map中的所有Entry,通过BiFunction类型的参数 function 计算出一个新值,然后用新值替换旧值。

假设一个应用场景,我们使用一个HashMap,记录了一些单词和它们的长度,接着产品经理提了一个新需求,要求对某些指定的单词,长度统一记录为0。实例代码如下:

public static void testBiFunctionWithLambda() {
    Map<String, Integer> wordMap = new HashMap<>();
    wordMap.put("hello", 5);
    wordMap.put("world", 5);
    wordMap.put("on", 2);
    wordMap.put("at", 2);     // lambda表达式中的k和v,分别是Map中Entry的key和原值value。
    // lambda表达式的返回值是一个新值value。
    wordMap.replaceAll((k, v) -> {
        if ("on".equals(k) || "at".equals(k)) {
            // 对应单词 on 和 at,单词长度统一记录为 0
            return 0;
        } else {
            // 其它单词,单词长度保持原值
            return v;
        }
    });     System.out.println(wordMap);
}

上述代码的输出为:
{world=5, at=0, hello=5, on=0}

2. Supplier
除了Function和BiFunction,还有一种常见的函数式接口是不需要任何参数,直接返回一个响应值。这就是Supplier:

@FunctionalInterface
public interface Supplier<T> {
    /**
     * 获取一个类型为 T 的对象实例。
     *
     * @return 对象实例
     */
    T get();
}

Supplier的一个典型应用场景是快速实现了工厂类的生产方法,包括延时的或者异步的生产方法。实例代码如下:

public class SupplierExample {
    public static void main(String[] args) {
        testSupplierWithLambda();
    }     public static void testSupplierWithLambda() {
        final Random random = new Random();
        // 生成一个随机整数
        lazyPrint(() -> {
            return random.nextInt(100);
        });         // 延时3秒,生成一个随机整数
        lazyPrint(() -> {
            try {
                System.out.println("waiting for 3s...");
                Thread.sleep(3*1000);
            } catch (InterruptedException e) {
                // do nothing
            }             return random.nextInt(100);
        });
    }     public static void lazyPrint(Supplier<Integer> lazyValue) {
        System.out.println(lazyValue.get());
    }
}

上述代码输出类似:
26
waiting for 3s…
27

3. Consumers
如果说Supplier属于生产者,那与之相对的是消费者Consumer。

Consumer
与Supplier相反,Consumer 接收一个参数,而不返回任何值。

@FunctionalInterface
public interface Consumer<T> {
    /**
     * 对给定的单一参数执行相关操作。
     *
     * @param t 输入参数
     */
    void accept(T t);     ...
}

示例代码:

public static void testConsumer() {
    List<String> list = Arrays.asList("Guangdong", "Zhejiang", "Jiangsu");     // 消费 list 中的每一个元素
    list.forEach(s -> System.out.println(s));
}

上述代码的输出为:
Guangdong
Zhejiang
Jiangsu

—BiConsumer
还有BiConsumer,语义和Consumer一致,不同的是BiConsumer接收2个参数。

@FunctionalInterface
public interface BiConsumer<T, U> {
    /**
     * 对给定的2个参数执行相关操作。
     *
     * @param t 第一个参数
     * @param u 第二个参数
     */
    void accept(T t, U u);     ...
}

示例代码:

public static void testBiConsumer() {
    Map<String, String> cityMap = new HashMap<>();
    cityMap.put("Guangdong", "Guangzhou");
    cityMap.put("Zhejiang", "Hangzhou");
    cityMap.put("Jiangsu", "Nanjing");     // 消费 map中的每一个(key, value)键值对
    cityMap.forEach((key, value) -> {
        System.out.println(String.format("%s 的省会是 %s", key, value));
    });
}

上述代码的输出是:
Guangdong 的省会是 Guangzhou
Zhejiang 的省会是 Hangzhou
Jiangsu 的省会是 Nanjing

4. Predicate
Predicate 的含义是接收一个参数值,然后依据给定的断言条件,返回一个boolean值。它实质上一个特殊的 Function,一个指定了返回值类型为boolean的 Function。

@FunctionalInterface
public interface Predicate<T> {
    /**
     * 根据给定参数,计算得到一个boolean结果。
     *
     * @param t 输入参数
     * @return 如果参数符合断言条件,返回 true,否则返回 false
     */
    boolean test(T t);     ...
}

Predicate 的使用场景通常是用来作为某种过滤条件。实例代码:

public static void testPredicate() {
    List<String> provinces = new ArrayList<>(Arrays.asList("Guangdong", "Jiangsu", "Guangxi", "Jiangxi", "Shandong"));     boolean removed = provinces.removeIf(s -> {
        return s.startsWith("G");
    });     System.out.println(removed);
    System.out.println(provinces);
}

上述代码是过滤掉以字母 G 开头的省份,输出为:
true
[Jiangsu, Jiangxi, Shandong]

5. Operators
Operator 函数式接口是一种特殊的 Function,要求返回值类型和参数类型是相同的。
和 Function/BiFunction 一样,Operators 也支持1个或2个参数。

—UnaryOperator
UnaryOperator 支持1个参数,UnaryOperator 等同于 Function<t, t="">:</t,>

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> { ... }

UnaryOperator的示例代码——将省份拼音转换大写与小写字母:

public static void testUnaryOperator() {
    List<String> provinces = Arrays.asList("Guangdong", "Jiangsu", "Guangxi", "Jiangxi", "Shandong");     // 将省份的字母转换成大写字母
    // 使用lambda表达式来实现 UnaryOperator
    provinces.replaceAll(s -> s.toUpperCase());
    System.out.println(provinces);     // 将省份的字母转换成小写字母。
    // 使用方法引用(method reference)来实现 UnaryOperator
    provinces.replaceAll(String::toLowerCase);
    System.out.println(provinces);
}

上述代码输出为:
[GUANGDONG, JIANGSU, GUANGXI, JIANGXI, SHANDONG]
[guangdong, jiangsu, guangxi, jiangxi, shandong]

—BinaryOperator
BinaryOperator 支持2个参数,BinaryOperator 等同于 BiFunction<t, t,="" t=""></t,>

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T, T, T> { ... }

BinaryOperator的示例代码——计算List中的所有整数的和:

public static void testBinaryOperator() {
    List<Integer> values = Arrays.asList(1, 3, 5, 7, 11);     // 使用 reduce 方法进行求和:0+1+3+5+7+11 = 27
    int sum = values.stream()
            .reduce(0, (a, b) -> a + b);     System.out.println(sum);
}

上述代码的输出为:
27

6. Java 7及之前版本遗留的函数式接口
前面提到过函数式接口的定义:只有一个抽象方法的接口都属于函数式接口。

按照这个定义,在Java 7或之前版本中定义的一些“老”接口也属于函数式接口,包括:
Runnable、Callable、Comparator等等。

当然,这些遗留的函数式接口,在Java 8中也加上了注解 @FunctionalInterface 。

组合函数式接口

我们在第一篇提到过:函数式编程是一种编程范式(programming paradigm),追求的目标是整个程序都由函数调用以及函数组合构成的。

函数组合(function composing),指的是将一系列简单函数组合起来形成一个复合函数。

Java 8中的函数式接口也提供了函数组合的功能。大家注意观察,可以发现基本每个内置的函数式接口都有一个非抽象的方法 andThen。andThen方法的功能是将多个函数式接口组合在一起,以串行的顺序逐一执行,从而形成一个新的函数式接口。

以Consumer.andThen方法为例,它返回一个新的Consumer实例。新的Consumer实例会先执行当前的accpet方法,然后再执行 after 的accpet方法。源码片段如下:

@FunctionalInterface
public interface Consumer<T> {
    ...      default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);         // 先执行当前Consumer的accept方法,再执行 after 的accept方法
        // 特别要注意的是,accept(t) 不能写在 return 语句之前,否则accept(t)将会被提前执行
        return (T t) -> { accept(t); after.accept(t); };
    }     ...
}

示例代码如下:

public static void testConsumerAndThen() {
    Consumer<String> printUpperCase = s -> System.out.println(s.toUpperCase());
    Consumer<String> printLowerCase = s -> System.out.println(s.toLowerCase());     // 组合得到一个新的 Consumer :先打印大写样式,再打印小写样式
    Consumer<String> prints = printUpperCase.andThen(printLowerCase);     List<String> list = Arrays.asList("Guangdong", "Zhejiang", "Jiangsu");
    list.forEach(prints);
}

上述代码的输出是:
GUANGDONG
guangdong
ZHEJIANG
zhejiang
JIANGSU
jiangsu

Function.andThen 方法则更复杂一些,它返回一个新的Function实例,在新的Function中,会先用类型为 T 的参数 t 执行当前的apply方法,得到一个类型为 R 的返回值 r,然后将 r 作为输入参数,继续执行 after 的apply方法,最终得到一个类型为 V 的返回值:

@FunctionalInterface
public interface Function<T, R> {
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);         // 先用类型为 T 的参数 t 执行当前的apply方法,得到一个类型为 R 的返回值 r ;
        // 然后将 r 作为输入参数,继续执行 after 的apply方法,最终得到一个类型为 V 的返回值;
        // 特别要注意的是,apply(t) 不能写在 return 语句之前,否则apply(t)将会被提前执行。
        return (T t) -> after.apply(apply(t));
    }

代码示例:

public static void testFunctionAndThen() {
    // wordLen 计算单词的长度
    Function<String, Integer> wordLen = s -> s.length(); // 等同于 s -> { return s.length(); }     // effectiveWord 单词长度大于等于4,才认为是有效单词
    Function<Integer, Boolean> effectiveWordLen = len -> len >= 4;     // Function<String, Integer> 和 Function<Integer, Boolean> 组合得到一个新的 Function<String, Boolean> ,
    // 像是消消乐: <String, Integer> 遇到了 <Integer, Boolean> ,消去了 Integer 类型后,得到了 <String, Boolean> 。
    Function<String, Boolean> effectiveWord = wordLen.andThen(effectiveWordLen);     Map<String, Boolean> wordMap = new HashMap<>();
    wordMap.computeIfAbsent("hello", effectiveWord);
    wordMap.computeIfAbsent("world", effectiveWord);
    wordMap.computeIfAbsent("on", effectiveWord);
    wordMap.computeIfAbsent("at", effectiveWord);     System.out.println(wordMap);
}

上述代码输出为:
{at=false, world=true, hello=true, on=false}

结语

Java 8是通过函数式接口,赋予了函数“第一等公民”的特性。

通过函数式接口,使得函数和其它数据类型一样,可以赋值给某个变量、可以作为另一个函数的参数、也可以作为另一个函数的返回值。

函数式接口的实现,可以是一个类(包括匿名类),但更多的是一个lambda表达式或者一个方法引用(method reference)。

Java中的函数式编程(二)函数式接口Functional Interface的更多相关文章

  1. Java 8函数式接口functional interface的秘密

    Java 8函数式接口functional interface的秘密 2014年10月29日 17:52:55 西瓜可乐520 阅读数:3729   目录 [−] JDK 8之前已有的函数式接口 新定 ...

  2. Java中的集合(十二) 实现Map接口的WeakHashMap

    Java中的集合(十二) 实现Map接口的WeakHashMap 一.WeakHashMap简介 WeakHashMap和HashMap一样,WeakHashMap也是一个哈希表,存储的也是键值对(k ...

  3. Java中的集合(二)单列集合顶层接口------Collection接口

    Java中的集合(二)单列集合顶层接口------Collection接口 Collection是一个高度封装的集合接口,继承自Iterable接口,它提供了所有集合要实现的默认方法.由于Iterab ...

  4. 第62节:探索Java中的网络编程技术

    前言 感谢! 承蒙关照~ 探索Java中的网络编程技术 网络编程就是io技术和网络技术的结合,网络模型的定义,只要共用网络模型就可以两者连接.网络模型参考. 一座塔有七层,我们需要闯关. 第一层物理层 ...

  5. Java8 函数式接口-Functional Interface

    目录 函数式接口: JDK 8之前已有的函数式接口: 新定义的函数式接口: 函数式接口中可以额外定义多个Object的public方法一样抽象方法: 声明异常: 静态方法: 默认方法 泛型及继承关系 ...

  6. Java中的 多线程编程

    Java 中的多线程编程 一.多线程的优缺点 多线程的优点: 1)资源利用率更好2)程序设计在某些情况下更简单3)程序响应更快 多线程的代价: 1)设计更复杂虽然有一些多线程应用程序比单线程的应用程序 ...

  7. java8函数式接口(Functional Interface)

    介绍 函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口. 函数式接口可以被隐式转换为 lambda 表达式(箭头函数). 函数式接口代 ...

  8. 读懂Java中的Socket编程

    Socket,又称为套接字,Socket是计算机网络通信的基本的技术之一.如今大多数基于网络的软件,如浏览器,即时通讯工具甚至是P2P下载都是基于Socket实现的.本文会介绍一下基于TCP/IP的S ...

  9. 读懂Java中的Socket编程(转)

    Socket,又称为套接字,Socket是计算机网络通信的基本的技术之一.如今大多数基于网络的软件,如浏览器,即时通讯工具甚至是P2P下载都是基于Socket实现的.本文会介绍一下基于TCP/IP的S ...

  10. Java Socket聊天室编程(二)之利用socket实现单聊聊天室

    这篇文章主要介绍了Java Socket聊天室编程(二)之利用socket实现单聊聊天室的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下 在上篇文章Java Socket聊天室编程(一)之 ...

随机推荐

  1. vue+Element-ui 的 el-cascader 做高德地图的省市区三级联动并且是异步加载,点击省市区跳转到对应的区(地图可以通过后端返回的点进行标点)

    // 完整版高德地图,可以复制代码直接使用 index.html <script type="text/javascript" src="https://webap ...

  2. Install Docker Engine on CentOS 在CentOS 7 上安装Docker

    Install Docker Engine on CentOS OS Requirements 系统要求 To install Docker Engine,you need a maintained ...

  3. Git 系列教程(6)- 查看 commit 提交历史

    查看提交历史 在提交了若干更新,又或者克隆了某个项目之后,如何查看提交历史 git log 官方栗子 运行下面的命令获取该项目: git clone https://github.com/scha 运 ...

  4. Docker入门之zabbix-agent篇

    在client端启动zabbix-agent服务 启动zabbix-agent有如下2种方式: agent start root@lykj-45:/srv# ls leyao zabbix zabbi ...

  5. bean的作用域和生命周期

    一.Bean作用域 二.生命周期 其中,这个类实现各种接口重写各种方法,会按bean的声明周期按序执行: 其中,自定义的初始化和自定义销毁的方法不是实现接口重写,而是成员方法,并且在装配bean即在x ...

  6. 硕盟SM-T54| TYPE C转HDMI+VGA+USB3.0+PD3.0四合一多功能扩展坞

    硕盟SM-T54是一款 TYPE C转HDMI+VGA+USB3.0+PD3.0四合一多功能扩展坞,支持四口同时使用,您可以将含有USB 3.1协议的电脑主机,通过此产品连接到具有HDMI或VGA的显 ...

  7. Haproxy搭建web集群

    目录: 一.常见的web集群调度器 二.Haproxy应用分析 三.Haproxy调度算法原理 四.Haproxy特性 五.Haproxy搭建 Web 群集 一.常见的web集群调度器 目前常见的we ...

  8. golang isPowerOfTwo判断是否是2的幂

    iota.go   strconv包 func isPowerOfTwo(x int) bool { return x & (x -1) } 了解n&(n-1)的作用如下: n& ...

  9. Windows Phone 页面之间参数传递方法

    目前对WP7开发正在研究,对页面之间参数传递进行了一个小总结,有不正确的地方,欢迎大家指正.. WP7编程采用的技术是Silverlight,页面之间参数传递的方式主要有 通过NavigationCo ...

  10. 个人作业--体温上报APP

    第一阶段目标: 1.要求增加用户注册功能,用户注册信息包括用户ID(学号).用户名(姓名),手机号码,用户单位(班级),用户班级四项基本信息,用户第一次注册后,用户姓名不用每次输入 . 2.体温上报界 ...