JAVA学习笔记 15 - 函数式接口、Lambda表达式和方法引用

本文是Java基础课程的第十五课。主要介绍在JDK8中,Java引入的部分新特性,包括函数式接口、Lambda表达式和方法引用。这些新特性使得Java能够在按照面向对象思想进行开发的基础上,融合函数式编程的许多优点。学习掌握并熟练使用这些新特性,可以使Java代码更简洁、清晰、优雅

文章目录

  • 一、函数式接口
    • 1、什么是函数式接口
    • 2、声明与应用函数式接口
      • 2.1、声明函数式接口
      • 2.2、应用函数式接口
    • 3、JDK中的函数式接口
      • 3.1、消费接口
      • 3.2、生产接口
      • 3.3、功能接口
      • 3.4、操作接口
      • 3.5、断言接口
  • 二、Lambda表达式
    • 1、什么是Lambda表达式
    • 2、Lambda表达式之初体验
    • 3、进一步了解Lambda表达式
      • 3.1、Lambda表达式的语法
      • 3.2、综合使用Lambda表达式和JDK中的函数式接口
      • 3.3、Lambda表达式与其外部的局部变量
      • 3.4、Lambda表达式中的this关键字
    • 4、关于函数式编程思想的理解
  • 三、方法引用
    • 1、什么是方法引用
    • 2、方法引用的分类
    • 3、方法引用应用举例
      • 3.1、静态方法引用
      • 3.2、特定对象的实例方法引用
      • 3.3、特定类任意对象的实例方法引用
      • 3.4、构造方法引用

一、函数式接口

1、什么是函数式接口

JDK8中新增了许多有趣的特性,比如前文中已经介绍过的新的日期时间API、接口默认方法等。函数式接口也是JDK8中提出的新特性之一。

函数式接口Functional Interface),也被称为功能性接口,简单的理解就是指有且只有一个抽象方法的接口

JDK8为了规范函数式接口定义,提供了@FunctionalInterface注解,编译器会检查被@FunctionalInterface注解标注的接口是否是一个合法的函数式接口。

一个规范函数式接口应该遵循以下规则

  1. 有且仅有一个抽象方法可以有其他非抽象方法重新显式声明java.lang.Object相同非最终公开(由public修饰且没有被final修饰)的方法声明可以忽略
  2. @FunctionalInterface注解(但如果某接口只有一个抽象方法,有没有标注@FunctionalInterface注解并不影响它是一个函数式接口的事实,@FunctionalInterface注解只是触发编译器的检查,起到强制保证该接口是一个合法函数式接口的作用)。

函数式接口是新版本的Java之所以能够支持函数式编程基础之一函数式接口核心思维如下:

  1. 规范且仅规范一种行为
  2. 该行为通过函数式接口的实现类可以方便进行实现
  3. 近似得将该行为当作调用方法参数使用,事实上是将函数式接口实现类对象作为方法实际参数进行传参。

2、声明与应用函数式接口

2.1、声明函数式接口

声明一个函数式接口非常简单,按照声明接口正常语法,并让该接口符合函数式接口规范即可
下面是一个示例:
Action接口的源码:

package com.codeke.java.test;

/**
 * 行为接口
 */
@FunctionalInterface
public interface Action<T> {
    /**
     * 接收一个输入参数并进行处理的方法,该方法无返回值
     * @param t 需要接收的参数
     */
    void accept(T t);
}

说明:

  • 本例中声明了一个泛型的函数式接口,名称为Action,该接口只包含一个抽象方法accept(T t),并且该接口标注了@FunctionalInterface注解。
  • 该接口的作用即是规范一种行为,该行为需要接收一个参数、不返回任何结果,该行为由接口中唯一的抽象方法accept(T t)来声明,使用时由具体的实现类来实现。

2.2、应用函数式接口

在之前介绍集合的章节中,通过编程实现了一个简易的集合,包含MyList接口和以数组为基础的集合实现类MyArrayList。这里结合函数式接口,简化简易集合的遍历操作。

下面是一个示例:
MyList接口的源码:

package com.codeke.java.test;

/**
 * 简易的List接口
 */
public interface MyList<T> {

    // 向集合中添加元素
    boolean add(T o);

    // 获取指定下标处的元素
    T get(int index);

    // 移除并返回集合中指定下标处的元素
    T remove(int index);

    // 获取集合中元素的个数
    int size();

    /**
     * 遍历处理的方法
     * @param action 消费者
     */
    default void forEach(Action<T> action) {
        for(int i = 0; i < this.size(); i ++){
            T t = this.get(i);
            action.accept(t);
        }
    }
}

MyArrayList类的源码同之前章节中编程实现简易集合的示例。
测试类Test类的源码:

package com.codeke.java.test;

public class Test {
    public static void main(String[] args) {
        // 实例化一个自定义实现的集合对象,并赋值给一个MyList接口类型的变量
        MyList<Integer> list = new MyArrayList<>();
        // 添加元素
        list.add(11);
        list.add(32);
        list.add(152);
        list.add(87);
        list.add(76);
        list.add(90);
        list.add(6);
        // 使用MyList接口中声明的forEach(Action action)方法遍历
        list.forEach(new Action<Integer>() {
            @Override
            public void accept(Integer num) {
                System.out.print(num + " ");
            }
        });
    }
}

执行输出结果:

11 32 152 87 76 90 6 

说明:

  • 本例的MyList接口中,声明了一个默认方法forEach(Action action),该方法的参数是一个Action接口类型的变量,在调用该方法时需要传入一个Action接口的实现类对象,之后,在forEach(Action action)方法中遍历当前集合,并将集合中的每一个元素交由Action接口规范的行为,即accept(T t)方法进行处理。
  • 本例的Test测试类main方法中,使用了匿名内部类实例化了一个的Action接口实现类对象,并将该对象作为forEach(Action action)方法的实际参数进行传参,看起来,犹如在调用集合listforEach(Action action)方法时,将一种行为(本例中是System.out.print(num + " "))作为方法参数而使用。

3、JDK中的函数式接口

函数式接口虽然是JDK8中才提出的特性,但之前版本的JDK中其实已经存在一些事实上的函数式接口,JDK8对它们进行了规范,标注@FunctionalInterface注解。它们中常见的一些如下:

接口名称 抽象方法 返回值类型
java.util.Comparator compare(T o1, T o2) int
java.lang.Runnable run() void
java.util.concurrent.Callable call() V
java.io.FileFilter accept(File pathname) boolean
java.nio.file.PathMatcher matches(Path path) boolean

JDK8除了规范以前存在函数式接口之外,还提供了一系列函数式接口,它们都在java.util.function下,主要分为以下几种

  1. 消费接口
  2. 生产接口
  3. 功能接口
  4. 操作接口
  5. 断言接口

3.1、消费接口

消费型函数式接口的名称中都带有Consumer字样,该型接口中的抽象方法均为accept()(不同的消费型函数式接口中accept()方法的参数列表不同),这些accept()方法规范“消费给定数据或对象”的行为,这种消费行为通常不需要返回值,故这些accept()方法的返回值类型均为void

常见消费型函数式接口如下:

接口名称 抽象方法 返回值类型
Consumer accept(T t) void
BiConsumer accept(T t, U u) void
DoubleConsumer accept(double value) void
IntConsumer accept(int value) void
LongConsumer accept(long value) void
ObjDoubleConsumer accept(T t, double value) void
ObjIntConsumer accept(T t, int value) void
ObjLongConsumer accept(T t, long value) void

下面是一个示例:

package com.codeke.java.test;

import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;

public class Test {
    public static void main(String[] args) {
        // 声明Map类型的变量map,并赋值为一个HashMap实例
        Map<String, String> map = new HashMap<>();
        // 向map集合中添加键值映射数据
        map.put("宋江","呼保义");
        map.put("卢俊义","玉麒麟");
        map.put("吴用","智多星");
        map.put("公孙胜","入云龙");
        map.put("关胜","大刀");
        // 调用map集合的forEach(BiConsumer action)方法
        // 该方法需要一个消费型函数式接口BiConsumer的实现类对象
        map.forEach(new BiConsumer<String, String>() {
            // 实现accept(T t, U u)方法
            @Override
            public void accept(String k, String v) {
                System.out.println(k + "的绰号是" + v);
            }
        });
    }
}

执行输出结果:

公孙胜的绰号是入云龙
卢俊义的绰号是玉麒麟
关胜的绰号是大刀
吴用的绰号是智多星
宋江的绰号是呼保义

说明:

  • 本例演示了借助消费型函数式接口BiConsumerjava.util.Map集合的forEach(BiConsumer action)方法遍历java.util.Map集合。遍历的过程中,具体的循环由java.util.Map集合的forEach(BiConsumer action)方法执行,而循环中每次迭代获取到的键值映射数据要如何进行消费,则由BiConsumer接口的accept(T t, U u)方法负责。
  • 在本例的代码中,通过匿名内部类实现了消费型函数式接口BiConsumer,即实现了该接口中的accept(T t, U u)方法,具体实现的行为是对该方法接收到的参数,即遍历java.util.Map集合获取到的一对一对的键值映射数据进行打印。

3.2、生产接口

生产型函数式接口的名称中都带有Supplier字样,该型接口中的抽象方法均带有get字样,这些方法规范“生产一个数据或对象”的行为,这些方法根据其所在的接口的不同,都声明了对应的、不同的返回值类型。

常见生产型函数式接口如下:

接口名称 抽象方法 返回值类型
Supplier get() T
BooleanSupplier getAsBoolean() boolean
DoubleSupplier getAsDouble() double
IntSupplier getAsInt() int
LongSupplier getAsLong() long

下面是一个示例:

package com.codeke.java.test;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.IntSupplier;

public class Test {
    public static void main(String[] args) {
        // 获取一个集合list,并添加了若干个字符串形式的人名作为集合元素
        List<String> list = Arrays.asList("林冲","秦明","呼延灼","花荣","柴进");
        // 调用printName(List namelist, IntSupplier indexSupplier)打印一个人名
        printName(list, new IntSupplier() {
            @Override
            public int getAsInt() {
                return new Random().nextInt(list.size());
            }
        });
    }
    /**
     * 打印名字的方法
     * @param namelist 名字集合
     * @param indexSupplier 下标生产者
     */
    private static void printName(List<String> namelist, IntSupplier indexSupplier){
        int index = indexSupplier.getAsInt();
        System.out.println(namelist.get(index));
    }
}

执行输出结果不再赘述。
说明:

  • 本例中,Test类中除了main方法之外,提供了一个静态方法printName(List namelist, IntSupplier indexSupplier),该方法在调用是需要传入一个包含若干字符串的列表nameList,以及一个生产型函数式接口IntSupplier的实现类对象作为整型数字的生产者,该方法中,借助IntSupplier接口中的getAsInt()方法获得一个int类型的值,并将该值作为下标,访问集合list中的元素并打印。
  • 在本例的main方法中,通过java.util.Arrays类的asList(T... a)方法获得一个保存了若干个字符串形式的人名的集合list,并调用printName(List namelist, IntSupplier indexSupplier)方法,调用时使用匿名内部类实现了生产型函数式接口IntSupplier,即实现了该接口中的getAsInt()方法,实现逻辑为随机获取一个[0,list.size())区间内的整数并返回。

3.3、功能接口

功能型函数式接口的名称中都带有Function字样,该型接口中的抽象方法均带有apply字样,这些方法规范的行为可以理解为“使用某些数据或对象加工转换成另一类数据或对象”,这些方法根据其所在的接口的不同,都声明了对应的、不同的方法参数列表和返回值类型。

常见功能型函数式接口如下:

接口名称 抽象方法 返回值类型
Function apply(T t) R
BiFunction apply(T t, U u) R
DoubleFunction apply(double value) R
DoubleToIntFunction applyAsInt(double value) int
DoubleToLongFunction applyAsLong(double value) long
IntFunction apply(int value) R
IntToDoubleFunction applyAsDouble(int value) double
IntToLongFunction applyAsLong(int value) long
LongFunction apply(long value) R
LongToDoubleFunction applyAsDouble(long value) double
LongToIntFunction applyAsInt(long value) int
ToDoubleBiFunction applyAsDouble(T t, U u) double
ToDoubleFunction applyAsDouble(T value) double
ToIntBiFunction applyAsInt(T t, U u) int
ToIntFunction applyAsInt(T value) int
ToLongBiFunction applyAsLong(T t, U u) long
ToLongFunction applyAsLong(T value) long

下面是一个示例:

package com.codeke.java.test;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;

public class Test {
    public static void main(String[] args) {
        // 总成绩单
        Map<String, Integer> scoreMap = new HashMap<>();
        scoreMap.put("鲁智深", 67);
        scoreMap.put("吴用", 90);
        scoreMap.put("武松", 72);
        scoreMap.put("林冲", 83);
        // 补充成绩单
        Map<String, Integer> additionalScoreMap = new HashMap<>();
        additionalScoreMap.put("公孙胜", 88);
        additionalScoreMap.put("花荣", 79);
        additionalScoreMap.put("武松", 75);
        // 将补充成绩单汇总到总成绩单,涉及两个方面
        // 1.总成绩单中已有的学员根据补充成绩单调整成绩
        // 2.补充成绩单中新增的学员及成绩添加到总成绩单中
        Set<Map.Entry<String, Integer>> entries = additionalScoreMap.entrySet();
        for (Map.Entry<String, Integer> entry : entries) {
            String name = entry.getKey();
            Integer score = entry.getValue();
            scoreMap.computeIfPresent(name, new BiFunction<String, Integer, Integer>() {
                @Override
                public Integer apply(String s, Integer i) {
                    System.out.printf("之前%s的成绩为%d,现在调整为%d\n", s, i, score);
                    return score;
                }
            });
            scoreMap.computeIfAbsent(name, new Function<String, Integer>() {
                @Override
                public Integer apply(String s) {
                    System.out.printf("之前没有%s的成绩,现在补录\n", s);
                    return score;
                }
            });
        }
        // 打印汇总后的成绩单
        scoreMap.forEach(new BiConsumer<String, Integer>() {
            @Override
            public void accept(String s, Integer i) {
                System.out.printf("%s的成绩为%d\n", s, i);
            }
        });
    }
}

执行输出结果:

之前没有公孙胜的成绩,现在补录
之前没有花荣的成绩,现在补录
之前武松的成绩为72,现在调整为75
鲁智深的成绩为67
花荣的成绩为79
公孙胜的成绩为88
武松的成绩为75
林冲的成绩为83
吴用的成绩为90

说明:

  • 本例中,使用java.util.Map集合提供了两个成绩单,为总成绩单和补充成绩单,首先需要将总成绩单中已有的学员根据补充成绩单调整成绩,其次需要将补充成绩单中新增的学员及成绩添加到总成绩单中。
  • 本例使用了java.util.Map集合中的computeIfPresent(K key, BiFunction remappingFunction)方法,该方法根据传入的键(key),判断集合中是否存在对应的值(value),如果存在,则执行参数中传入的功能型函数式接口BiFunction的实现类所实现的处理逻辑,并使用返回值作为新的值(value),存入集合。
  • 本例还使用了java.util.Map集合中的computeIfAbsent(K key, Function mappingFunction)方法,该方法根据传入的键(key),判断集合中是否存在对应的值(value),如果不存在,则执行参数中传入的功能型函数式接口Function的实现类所实现的处理逻辑,并使用返回值作为值(value),和键(key)一起作为一组键值映射数据,存入集合。

3.4、操作接口

操作型函数式接口的名称中都带有Operator字样,该型函数式接口和功能型函数式接口相似,不同的是,在语意上,功能型函数式接口倾向完成某些功能,而操作型函数式接口倾向完成一些操作或计算。事实上,接口BinaryOperator既是继承自BiFunction接口的,很多时候也将操作型函数式接口归为功能型函数式接口。操作型函数式接口中的抽象方法也均带有apply字样,这些方法根据其所在的接口的不同,都声明了对应的、不同的方法参数列表和返回值类型。

常见操作型函数式接口如下:

接口名称 抽象方法 返回值类型
BinaryOperator apply(T t1, T t2) T
UnaryOperator apply(T t) T
DoubleBinaryOperator applyAsDouble(double left, double right) double
DoubleUnaryOperator applyAsDouble(double operand) double
IntBinaryOperator applyAsInt(int left, int right) int
IntUnaryOperator applyAsInt(int operand) int
LongBinaryOperator applyAsLong(long left, long right) long
LongUnaryOperator applyAsLong(long operand) long

下面是一个示例:

package com.codeke.java.test;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.function.BinaryOperator;

public class Test {
    public static void main(String[] args) {
    	// 本金1000元,复利率为10%,逐年计算10年内每年的本金与利息
    	// 声明并实例化长度为10的BigDecimal类型数组
        BigDecimal[] nums = new BigDecimal[10];
        // 数组的第一个元素赋值为本金
        nums[0] = new BigDecimal("1000");
        // 数组其他元素赋值为年利率
        Arrays.fill(nums, 1, 10, new BigDecimal("0.1"));
        // 借助Arrays类的parallelPrefix(T[] array, BinaryOperator op)方法完成逐年复利计算
        // 计算操作由操作型函数式接口BinaryOperator的实现类具体实现
        Arrays.parallelPrefix(nums, new BinaryOperator<BigDecimal>() {
            @Override
            public BigDecimal apply(BigDecimal left, BigDecimal right) {
                return left.multiply(right).add(left)
                        .setScale(2, RoundingMode.HALF_UP);
            }
        });
        // 逐年打印
        for (int i = 0; i < nums.length; i++) {
            System.out.printf("第%d年时利息加本金共:%s\n", (i + 1), nums[i]);
        }
    }
}

执行输出结果:

1年时利息加本金共:10002年时利息加本金共:1100.003年时利息加本金共:1210.004年时利息加本金共:1331.005年时利息加本金共:1464.106年时利息加本金共:1610.517年时利息加本金共:1771.568年时利息加本金共:1948.729年时利息加本金共:2143.5910年时利息加本金共:2357.95

说明:

  • 本例的代码计算了以1000元为本金,按10%的复利率在10年间逐年的利息与本金之和。
  • 在本例的main方法中,先声明并实例化了一个长度为10java.math.BigDecimal类型的数组nums,数组的第一个元素被赋值为本金1000,之后的元素被赋值为复利率10%。之后,调用java.util.Arrays类的parallelPrefix(T[] array, BinaryOperator op)方法,调用该方法时,首先需要传入被操作的数组,本例中即是数组nums,其次需要传入一个操作型函数式接口BinaryOperator的实现类对象,该方法会将数组nums中每个元素和其之前的一个元素一起传入BinaryOperator接口实现类对象的apply(T t1, T t2)方法中,然后使用apply(T t1, T t2)方法的返回值替换数组中相应的元素。借助parallelPrefix(T[] array, BinaryOperator op)方法的功能,只需在实现BinaryOperator接口的apply(T t1, T t2)方法时,使用复利的计算公式(本金 * 利率)+ 本金,对入参进行计算并返回计算结果,即可完成本例的需求。

3.5、断言接口

断言型函数式接口的名称中都带有Predicate字样,该型接口中的抽象方法均带有test字样,这些方法规范的行为可以理解为“判断传入的数据或对象断言它们是否符合某种标准”,这些方法根据其所在的接口的不同,都声明了对应的、不同的方法参数列表,但断言的结果均可以使用一个布尔值表示,故这些方法的返回值类型均为boolean

常见断言型函数式接口如下:

接口名称 抽象方法 返回值类型
Predicate test(T t) boolean
BiPredicate test(T t, U u) boolean
DoublePredicate test(double value) boolean
IntPredicate test(int value) boolean
LongPredicate test(long value) boolean

下面是一个示例:

package com.codeke.java.test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class Test {
    public static void main(String[] args) {
        // 声明并实例化一个ArrayList集合list
        List<String> list = new ArrayList<>();
        // 添加元素
        list.add("鲁智深");
        list.add("武松");
        list.add("林冲");
        list.add("吴用");
        list.add("公孙胜");
        // 删除元素"林冲"及"吴用"
        list.removeIf(new Predicate<String>() {
            @Override
            public boolean test(String s) {
                return "林冲".equals(s) || "吴用".equals(s);
            }
        });
        // 打印集合元素
        System.out.println(Arrays.toString(list.toArray()));
    }
}

执行输出结果:

[鲁智深, 武松, 公孙胜]

说明:

  • 在本例的main方法中,声明并实例化了一个java.util.ArrayList集合list,并添加了若干字符串形式的人名作为元素,之后调用了list集合的removeIf(Predicate filter)方法,该方法调用时需要传入一个断言型函数式接口Predicate的实现类对象,removeIf(Predicate filter)方法会对集合中的每一个元素使用Predicate实现类所实现的断言逻辑进行判断,如果断言为真,则从集合中删除该元素。

二、Lambda表达式

1、什么是Lambda表达式

JAVA学习笔记 15 - 函数式接口、Lambda表达式和方法引用_第1张图片
Lambda表达式也是JDK8中新增的特性之一,可以替换用以实现函数式接口匿名内部类

新版本的Java之所以能够支持函数式编程Lambda表达式最重要基石Lambda表达式核心思维是:

  1. 通过Lambda表达式简洁的语法,专注于函数型接口所规范的行为具体应该做什么,进一步简化函数型接口实现
  2. Lambda表达式作为方法参数使用,即将具体行为作为方法实际参数而使用。

2、Lambda表达式之初体验

上文中在介绍消费型函数式接口时使用过一个示例,这里再次观察该示例:

package com.codeke.java.test;

import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;

public class Test {
    public static void main(String[] args) {
        // 声明Map类型的变量map,并赋值为一个HashMap实例
        Map<String, String> map = new HashMap<>();
        // 向map集合中添加键值映射数据
        map.put("宋江","呼保义");
        map.put("卢俊义","玉麒麟");
        map.put("吴用","智多星");
        map.put("公孙胜","入云龙");
        map.put("关胜","大刀");
        // 调用map集合的forEach(BiConsumer action)方法
        // 该方法需要一个消费型函数式接口BiConsumer的实现类对象
        map.forEach(new BiConsumer<String, String>() {
            // 实现accept(T t, U u)方法
            @Override
            public void accept(String k, String v) {
                System.out.println(k + "的绰号是" + v);
            }
        });
    }
}

在这一示例中,通过匿名内部类实现了消费型函数式接口BiConsumer,具体实现的消费行为是将accept(String k, String v)方法接收的参数,即遍历map集合所获取的一对一对的键值映射数据进行打印;如果要使用另外一种消费方式进行消费,比如打印这些键值映射数据的字符长度,按照本例的编码风格,改造如下:

package com.codeke.java.test;

import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;

public class Test {
    public static void main(String[] args) {
        // 声明Map类型的变量map,并赋值为一个HashMap实例
        Map<String, String> map = new HashMap<>();
        // 向map集合中添加键值映射数据
        map.put("宋江","呼保义");
        map.put("卢俊义","玉麒麟");
        map.put("吴用","智多星");
        map.put("公孙胜","入云龙");
        map.put("关胜","大刀");
        // 调用map集合的forEach(BiConsumer action)方法
        // 该方法需要一个消费型函数式接口BiConsumer的实现类对象
        map.forEach(new BiConsumer<String, String>() {
            // 实现accept(T t, U u)方法
            @Override
            public void accept(String k, String v) {
                System.out.printf("%s,名字的长度是:%s,绰号长度是:%s\n",
                        k, k.length(), v.length());
            }
        });
    }
}

观察并比较上文两段源码最主要的部分:
JAVA学习笔记 15 - 函数式接口、Lambda表达式和方法引用_第2张图片
不难发现,不论使用什么样的行为来消费数据,只有被绿色下划线标示出的代码(形参名称方法具体实现代码)才有可能发生变化,这一部分代码也正是需要被关注得、具体消费行为;而被黄色下划线标示出的代码是没有变化的,这一部分代码或者是固定不变的,或者是编译器可以通过上下文进行推断的,这一部分代码也被称之为冗余的模板代码

既然冗余的模板代码不会随着消费行为的变化而变化,那么如果可以省略它们,势必代码会变得简洁,也更专注于具体行为Lambda表达式便让这一设想成为现实。

下面使用Lambda表达式重构上例中的两段源码,初步体验Lambda表达式
下面是一个示例:

package com.codeke.java.test;

import java.util.HashMap;
import java.util.Map;

public class Test {
    public static void main(String[] args) {
        // 声明Map类型的变量map,并赋值为一个HashMap实例
        Map<String, String> map = new HashMap<>();
        // 向map集合中添加键值映射数据
        map.put("宋江","呼保义");
        map.put("卢俊义","玉麒麟");
        map.put("吴用","智多星");
        map.put("公孙胜","入云龙");
        map.put("关胜","大刀");
        // 调用map集合的forEach(BiConsumer action)方法
        // 使用Lambda表达式作为方法的参数
        // 第一种消费行为
        map.forEach((k, v) -> System.out.println(k + "的绰号是" + v));
        // 第二种消费行为
        map.forEach((k, v) -> System.out.printf("%s,名字的长度是:%s,绰号长度是:%s\n",
                k, k.length(), v.length()));
    }
}

执行输出结果:

公孙胜的绰号是入云龙
卢俊义的绰号是玉麒麟
关胜的绰号是大刀
吴用的绰号是智多星
宋江的绰号是呼保义
公孙胜,名字的长度是:3,绰号长度是:3
卢俊义,名字的长度是:3,绰号长度是:3
关胜,名字的长度是:2,绰号长度是:2
吴用,名字的长度是:2,绰号长度是:3
宋江,名字的长度是:2,绰号长度是:3

说明:

  • 本例中的代码(k, v) -> System.out.println(k + "的绰号是" + v)和代码(k, v) -> System.out.printf("%s,名字的长度是:%s,绰号长度是:%s\n", k, k.length(), v.length())即是Lambda表达式。
  • 相对于前例中使用的匿名内部类,Lambda表达式使代码中冗余的模板代码消失了,代码变的简洁,清晰,重点突出,最重要的是,将具体的行为当作了方法的参数。
  • 需要注意的是,Lambda表达式并非匿名内部类简化写法,其编译过程实现机制等均与匿名内部类不同

3、进一步了解Lambda表达式

3.1、Lambda表达式的语法

Lambda表达式具体的语法格式如下:

(parameters) -> {expression 或 statements}

Lambda表达式的语法结构具有以下特征:

  • 可以具有零个一个多个参数
  • 参数类型可选可以显式声明参数类型也可以编译器自动从上下文推断参数类型,如:
    (x, y) -> x – y;				// 省略参数类型
    (int x, int y) -> x + y			// 显示声明参数类型
    
  • 参数圆括号()可选,如果只有一个参数不声明参数类型可以不使用圆括号(),但没有参数、有多个参数显式声明参数类型需要使用圆括号(),如:
    x -> 2 * x;						// 只有一个参数,没有声明参数类型,此时可以不使用参数圆括号()
    (int x) -> 3 * x;				// 显式声明参数类型,需要使用参数圆括号()
    () -> 5;						// 没有参数,需要使用参数圆括号()
    (x, y) -> x – y;				// 有一个以上参数,需要使用参数圆括号()   
    
  • 始终有符号->
  • 符号->后的部分为主体,主体可以包含零条一条多条语句,如:
    x - > {};						// 不包含语句
    x - > {int y = x * 3; System.out.println(y);}			// 包含一条以上语句
    
  • 大括号{}可选,如果主体只包含一条语句可以不使用大括号{},如:
    (x, y) -> x – y;				// 省略大括号{}
    
  • 返回关键字return可选,在Lambda表达式需要返回值的情况下,如果主体只有一个表达式,则编译器会自动返回表达式不需要使用return关键字如果使用大括号{},则需要使用return关键字指明返回值,如:
    (x, y) -> x – y;	// 如果该Lambda表达式对应的函数式接口所声明的方法有返回值,则该Lambda表达式返回 x-y 的值
    (x, y) -> {return x - y;};			// 该Lambda表达式返回 x - y 的值
    x - > {int y = x * 3; return y;};	// 该Lambda表达式返回 y 的值
    
  • 还需注意,如果主体使用大括号{},则大括号{}内的每条语句后都需使用分号;结尾

下面是一个示例:

package com.codeke.java.test;

public class Test {
    public static void main(String[] args) {
        // 加法操作,显式声明参数类型,使用了(),没有使用{}和return关键字
        MathOperation addition = (int a, int b) -> a + b;
        // 减法操作,不显式声明参数类型,使用了(),没有使用{}和return关键字
        MathOperation subtraction = (a, b) -> a - b;
        // 乘法操作,不显式声明参数类型,使用了(),使用{}和return关键字
        MathOperation multiplication = (a, b) -> {
            return a * b;
        };
        // 除法运算,不显式声明参数类型,使用了(),使用{}和return关键字
        MathOperation division = (a, b) -> {
            return a / b;
        };

        // 使用Lambda
        System.out.println("8 + 2 = " + operate(8, 2, addition));
        System.out.println("8 - 4 = " + operate(8, 4, subtraction));
        System.out.println("8 x 3 = " + operate(8, 3, multiplication));
        System.out.println("8 / 4 = " + operate(8, 4, division));

        // 用英文打招呼的操作,不显式声明参数类型,不使用了()
        GreetingService greetService1 = message ->
                System.out.println("Hello " + message);

        // 用中文打招呼的操作,不显式声明参数类型,使用了()
        GreetingService greetService2 = (message) ->
                System.out.println("你好 " + message);

        greetService1.sayMessage("tom");
        greetService2.sayMessage("鲁智深");

    }

    // 进行算数运算的方法,需要两个int类型的操作数和一个操作行为
    private static int operate(int a, int b, MathOperation mathOperation) {
        return mathOperation.operation(a, b);
    }
}

// 函数式接口,规范一种数学运算操作,
// 需要输入两个int类型的操作数,返回一个int类型的操作数
@FunctionalInterface
interface MathOperation {
    int operation(int a, int b);
}

// 函数式接口,规范一个打招呼的行为
// 需要输入一个字符串,无返回
@FunctionalInterface
interface GreetingService {
    void sayMessage(String message);
}

执行输出结果:

8 - 4 = 4
8 x 3 = 24
8 / 4 = 2
Hello tom
你好 鲁智深

说明:

  • 本例中声明了两个函数式接口,MathOperationGreetingService,并在此基础上演示了Lambda表达式灵活的语法,具体代码逻辑非常简单,这里不再赘述。

3.2、综合使用Lambda表达式和JDK中的函数式接口

下面结合上文中使用过的部分示例,综合使用Lambda表达式JDK中的函数式接口,进一步理解和巩固Lambda表达式的使用。

Lambda表达式和JDK中的消费型函数式接口的综合使用在初步体验Lambda表达式时已经举例,这里不再举例。

Lambda表达式和JDK中的生产型函数式接口综合使用,下面是一个示例:

package com.codeke.java.test;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.IntSupplier;

public class Test {
    public static void main(String[] args) {
        // 获取一个集合list,并添加了若干个字符串形式的人名作为集合元素
        List<String> list = Arrays.asList("林冲","秦明","呼延灼","花荣","柴进");
        // 调用printName(List namelist, IntSupplier indexSupplier)打印一个人名
        // 用Lambda表达式替换前文该例中使用的匿名内部类
        printName(list, () -> new Random().nextInt(list.size()));
    }
    /**
     * 打印名字的方法
     * @param namelist 名字集合
     * @param indexSupplier 下标生产者
     */
    private static void printName(List<String> namelist, IntSupplier indexSupplier){
        int index = indexSupplier.getAsInt();
        System.out.println(namelist.get(index));
    }
}

执行输出结果不再赘述。

Lambda表达式和JDK中的功能型函数式接口综合使用,下面是一个示例:

package com.codeke.java.test;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class Test {
    public static void main(String[] args) {
        // 总成绩单
        Map<String, Integer> scoreMap = new HashMap<>();
        scoreMap.put("鲁智深", 67);
        scoreMap.put("吴用", 90);
        scoreMap.put("武松", 72);
        scoreMap.put("林冲", 83);
        // 补充成绩单
        Map<String, Integer> additionalScoreMap = new HashMap<>();
        additionalScoreMap.put("公孙胜", 88);
        additionalScoreMap.put("花荣", 79);
        additionalScoreMap.put("武松", 75);
        // 将补充成绩单汇总到总成绩单,涉及两个方面
        // 1.总成绩单中已有的学员根据补充成绩单调整成绩
        // 2.补充成绩单中新增的学员及成绩添加到总成绩单中
        Set<Map.Entry<String, Integer>> entries = additionalScoreMap.entrySet();
        for (Map.Entry<String, Integer> entry : entries) {
            String name = entry.getKey();
            Integer score = entry.getValue();
            // 用Lambda表达式替换前文该例中使用的匿名内部类
            scoreMap.computeIfPresent(name, (s, i) -> {
                System.out.printf("之前%s的成绩为%d,现在调整为%d\n", s, i, score);
                return score;
            });
            // 用Lambda表达式替换前文该例中使用的匿名内部类
            scoreMap.computeIfAbsent(name, s -> {
                System.out.printf("之前没有%s的成绩,现在补录\n", s);
                return score;
            });
        }
        // 打印汇总后的成绩单
        // 用Lambda表达式替换前文该例中使用的匿名内部类
        scoreMap.forEach((s, i) -> System.out.printf("%s的成绩为%d\n", s, i));
    }
}

执行输出结果不再赘述。

Lambda表达式和JDK中的操作型函数式接口综合使用,下面是一个示例:

package com.codeke.java.test;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;

public class Test {
    public static void main(String[] args) {
        // 本金1000元,复利率为10%,逐年计算10年内每年的本金与利息
        // 声明并实例化长度为10的BigDecimal类型数组
        BigDecimal[] nums = new BigDecimal[10];
        // 数组的第一个元素赋值为本金
        nums[0] = new BigDecimal("1000");
        // 数组其他元素赋值为年利率
        Arrays.fill(nums, 1, 10, new BigDecimal("0.1"));
        // 借助Arrays类的parallelPrefix(T[] array, BinaryOperator op)方法完成逐年复利计算
        // 计算操作由操作型函数式接口BinaryOperator的实现类具体实现
        // 用Lambda表达式替换前文该例中使用的匿名内部类
        Arrays.parallelPrefix(nums, (left, right) -> left.multiply(right).add(left)
                .setScale(2, RoundingMode.HALF_UP));
        // 逐年打印
        for (int i = 0; i < nums.length; i++) {
            System.out.printf("第%d年时利息加本金共:%s\n", (i + 1), nums[i]);
        }
    }
}

执行输出结果不再赘述。

Lambda表达式和JDK中的断言型函数式接口综合使用,下面是一个示例:

package com.codeke.java.test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        // 声明并实例化一个ArrayList集合list
        List<String> list = new ArrayList<>();
        // 添加元素
        list.add("鲁智深");
        list.add("武松");
        list.add("林冲");
        list.add("吴用");
        list.add("公孙胜");
        // 删除元素"林冲"及"吴用"
        // 用Lambda表达式替换前文该例中使用的匿名内部类
        list.removeIf(s -> "林冲".equals(s) || "吴用".equals(s));
        // 打印集合元素
        System.out.println(Arrays.toString(list.toArray()));
    }
}

执行输出结果不再赘述。

结合这些例子,更容易看出,比起使用匿名内部类,用Lambda表达式配合函数式接口来使用,会使相同功能的代码得到大幅度简化,并且代码也更易读易懂易维护。对于开发人员来说,熟练使用Lambda表达式,会使开发效率得到提高,也更容易写出简介清晰优雅代码

3.3、Lambda表达式与其外部的局部变量

需要注意的是,Lambda表达式可以引用外层局部变量,但是只限于标记final外层局部变量,或事实上final外层局部变量没有被后面代码修改,即隐性具有final语义)。

下面是一个示例:

package com.codeke.java.test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        // 申明并初始化一个循环变量
        int index = 1;
        // 将若干名字封装成一个java.util.ArrayList集合
        List<String> names = new ArrayList<>(Arrays.asList("宋江", "卢俊义", "吴用", "公孙胜"));
        // 使用集合的forEach(Consumer action)方法
        // 使用Lambda表达式输出每个名字,希望连同循环变量一同输出
        names.forEach(name -> {
            System.out.println(index + " " + name);
            // index +=1;  // Variable used in lambda expression should be final or effectively final
        });
    }
}

说明:

  • 本例中的代码在循环输出集合names中的名字时,希望连同序号(将循环变量index作为序号)一同输出。故而希望在实现输出的Lambda表达式中对外部变量index进行自增操作。然而编译器会提示一个错误Variable used in lambda expression should be final or effectively final,既要么外部变量index应该被final修饰,要么变量index在Lambda表达式中不会被修改。

下面是另一个示例:

package com.codeke.java.test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        // 申明并初始化一个只有一个元素的数组,用来存放循环变量
        final int[] index = {1};
        // 将若干名字封装成一个java.util.ArrayList集合
        List<String> names = new ArrayList<>(Arrays.asList("宋江", "卢俊义", "吴用", "公孙胜"));
        // 使用集合的forEach(Consumer action)方法
        // 使用Lambda表达式输出每个名字,希望连同循环变量一同输出
        names.forEach(name -> {
            System.out.println(index[0] + " " + name);
            index[0] += 1;  // 数组index是引用类型,修改数组中某一元素的值时,数组本身的地址没有发生变化,
        });
    }
}

执行输出结果:

1 宋江
2 卢俊义
3 吴用
4 公孙胜

说明:

  • 本例的意图和上例一样。不同的是,本例使用了一个只有一个元素的int类型数组index来存放循环变量。数组是引用类型,改变数组中某一元素的值并不会影响数组本身的地址。故可以在Lambda表达式中对数组index的第一个元素进行自增,从而实现本例的意图。

3.4、Lambda表达式中的this关键字

前文中提到,Lambda表达式并非匿名内部类简化写法,其编译过程实现机制等均与匿名内部类不同。事实上,Lambda表达式不会被编译为对应功能的匿名内部类,在Lambda表达式中使用this关键字时,便能体会到这一点。

下面是一个示例:

package com.codeke.java.test;

import java.util.function.Supplier;

public class Test {

    // 主方法
    public static void main(String[] args) {
        Test test = new Test();
        test.testThis();
    }

    /**
     * 测试方法,用来测试匿名内部类和Lambda表达式中的this有何区别
     */
    private void testThis() {
        // 使用匿名内部类实现一个生产者supplier1
        Supplier<String> supplier1 = new Supplier<String>() {
            @Override
            public String get() {
                // 返回this引用的对象的类名称
                return this.getClass().getName();
            }
        };
        // 将生产者提供的字符串打印在控制台上
        System.out.println(supplier1.get());

        // 使用Lambda表达式声明一个生产者supplier2
        Supplier<String> supplier2 = () -> this.getClass().getName();
        // 将生产者提供的字符串打印在控制台上
        System.out.println(supplier2.get());
    }
}

执行输出结果:

com.codeke.java.test.Test$1
com.codeke.java.test.Test

说明:

  • 本例的输出结果中,类名com.codeke.java.test.Test$1中的Test$1是编译器为测试类Test中的匿名内部类自动起的名字,而类名com.codeke.java.test.Test即是测试类Test的完全限定名。
  • 通过比较输出结果,不难发现,Lambda表达式中this的是声明它的类当前对象,而匿名内部类this的是匿名内部类当前对象

4、关于函数式编程思想的理解

在学习了Java中的函数式接口、Lambda表达式的基础知识,并在观察、体会了若干使用这些特性的代码示例的基础上,可以初步理解函数式编程思想。大体上的理解,函数就是有输入量输出量一套计算方案,也就是“拿什么东西做什么事情” 。相对而言,面向对过分强调必须通过对象的形式来做事情”,而函数式思想尽量忽略面向对象的复杂语法——强调做什么而不是以什么形式做

粗略的总结为:

  • 面向对象的思想:做一件事情找一个能解决这个事情的对象调用对象的方法完成事情
  • 函数式编程思想:做一件事情谁去做的并不重要重要的是怎么做获得什么结果不重视形式

三、方法引用

1、什么是方法引用

方法引用也是JDK8中新增非常有趣的特性之一。方法引用可以认为是某些特定场景Lambda表达式另一种表现形式

在一些特定场景下,Lambda表达式仅仅调用一个已经存在的方法,而没有其他多余动作,在这种情况下,可以使用方法引用简写Lambda表达式,使代码更简洁易于理解

注意方法引用仍然一个Lambda表达式方法引用操作符双冒号::

2、方法引用的分类

方法引用具体可以分为四种形式:即静态方法引用实例方法引用对象方法引用构造方法引用。这四种形式的方法引用可以简化的对应Lambda表达式如下表:

类型 语法 对应Lambda表达式
静态方法引用 Class::static_method (args ...) -> Class.static_method(args ...)
特定对象的实例方法引用 instance::method (args ...) -> instance.method(args ...)
特定类任意对象的实例方法引用 Class::method (instance, args ...) -> instance.method(args ...)
构造方法引用 Class::new (args ...) -> new Class(args ...)

3、方法引用应用举例

3.1、静态方法引用

静态方法引用,下面是一个示例:

package com.codeke.java.test;

import java.util.Arrays;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        List<Integer> nums = Arrays.asList(13, 4, 31, 5, 19, 2, 25);
        // 对集合中的元素进行排序
        // 使用匿名内部类
        // nums.sort(new Comparator() {
        //     @Override
        //     public int compare(Integer num1, Integer num2) {
        //         return Integer.compare(num1, num2);
        //     }
        // });
        // 使用Lambda表达式
        // nums.sort((num1, num2) -> Integer.compare(num1, num2));
        // 静态方法引用,相当于(num1, num2) -> Integer.compare(num1, num2)
        nums.sort(Integer::compare);
        System.out.println(nums.toString());
    }
}

执行输出结果:

[2, 4, 5, 13, 19, 25, 31]

说明:

  • 本例中,nums是一个实现了java.util.List接口的集合,可以调用java.util.List集合的方法sort(Comparator c)对其中的元素进行排序。
  • java.util.List集合的方法sort(Comparator c)需要一个实现了java.util.Comparator接口的比较器作为参数,而java.util.Comparator接口是一个函数式接口;另外,集合nums中存放的是Integer类型的整数,故可以使用java.lang.Integer类的静态方法compare(int x, int y)来实现比较逻辑。综上,可以使用Lambda表达式完成比较,代码如下:
    nums.sort((num1, num2) -> Integer.compare(num1, num2));
    
  • 该Lambda表达式仅仅是调用了java.lang.Integer类的静态方法compare(int x, int y),需要注意的是,调用compare(int x, int y)方法时的实际参数即是Lambda表达式中传入的参数num1num2。该Lambda表达式可以使用静态方法引用简写,如下:
    nums.sort(Integer::compare);
    

3.2、特定对象的实例方法引用

特定对象的实例方法引用,下面是一个示例:

package com.codeke.java.test;

import java.io.PrintStream;
import java.util.Arrays;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("宋江", "卢俊义", "公孙胜", "吴用", "关胜", "林冲");
        // 声明并获得控制台输出对象out
        PrintStream out = System.out;
        // 遍历输出集合names中的元素
        // 使用Lambda表达式
        // names.forEach(name -> out.println(name));
        // 特定对象的实例方法引用
        names.forEach(out::println);
    }
}

执行输出结果:

宋江
卢俊义
公孙胜
吴用
关胜
林冲
  • 本例非常简单,意图借助java.util.List集合的方法forEach(Consumer action)遍历打印集合中的元素。为了清晰起见,代码中声明并获得控制台输出对象out,它是一个java.io.PrintStream类的实例,println(String x)方法即是java.io.PrintStream类中的一个实例方法。如果使用Lambda表达式完成本例,代码如下:
    names.forEach(name -> out.println(name));
    
  • 该Lambda表达式仅仅是调用了out对象的实例方法println(String x),需要注意的是,调用println(String x)方法时的实际参数即是Lambda表达式中传入的参数name。该Lambda表达式可以使用特定对象的实例方法引用简写,如下:
    names.forEach(out::println);
    

3.3、特定类任意对象的实例方法引用

特定类任意对象的实例方法引用,下面是一个示例:

package com.codeke.java.test;

import java.util.*;

public class Test {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>(Arrays.asList("宋江", "", "公孙胜", "", "关胜", "林冲"));
        // 删除集合中的空字符串
        // 使用Lambda表达式
        // names.removeIf(name -> name.isEmpty());
        // 特定类任意对象的实例方法引用
        names.removeIf(String::isEmpty);
        System.out.println(names);
    }
}

执行输出结果:

[宋江, 公孙胜, 关胜, 林冲]

说明:

  • 本例中,names是一个java.util.ArrayList集合的实例,用来存放若干个人名,但其中部分元素是空字符串,需要将这些空字符串从集合中删除掉,为了达到这一目的,可以调用java.util.Collection接口中的默认方法removeIf(Predicate filter)删除集合中满足特定条件的元素。
  • removeIf(Predicate filter)方法需要的参数是java.util.function.Predicate接口类型的,而java.util.function.Predicate接口是一个函数式接口;另外,判断字符串是否为空字符串可以使用字符串的实例方法isEmpty()。综上,可以使用Lambda表达式完成本例,代码如下:
    names.removeIf(name -> name.isEmpty());
    
  • 观察该Lambda表达式,仅仅调用了字符串的实例方法isEmpty(),而方法的调用者为Lambda表达式传入的第一个参数。该Lambda表达式可以用特定类任意对象的实例方法引用来简写,如下:
    names.removeIf(String::isEmpty);
    

下面是另一个示例:

package com.codeke.java.test;

import java.util.*;

public class Test {
    public static void main(String[] args) {
        List<Integer> nums = Arrays.asList(13, 4, 31, 5, 19, 2, 25);
        // 对集合中的元素进行排序
        // 使用匿名内部类
        // nums.sort(new Comparator() {
        //     @Override
        //     public int compare(Integer num1, Integer num2) {
        //         return num1.compareTo(num2);
        //     }
        // });
        // 使用Lambda表达式
        //  nums.sort((num1, num2) -> num1.compareTo(num2));
        // 特定类任意对象的实例方法引用
        nums.sort(Integer::compareTo);
        System.out.println(nums.toString());
    }
}

执行输出结果:

[2, 4, 5, 13, 19, 25, 31]

说明:

  • 本例和介绍静态方法引用时使用的示例非常相似,不同的是,本例实现比较逻辑时没有使用java.lang.Integer类的静态方法compare(int x, int y),而是使用java.lang.Integer类的实例方法compareTo(Integer anotherInteger)。Lambda表达式如下:
    nums.sort((num1, num2) -> num1.compareTo(num2));
    
  • 观察该Lambda表达式,仅仅调用了java.lang.Integer类的实例方法compareTo(Integer anotherInteger),调用时,调用者是Lambda表达式传入的第一个参数num1,方法的实际参数是Lambda表达式传入的第二个参数num2。此时,可以使用特定类任意对象的实例方法引用来简写该Lambda表达式,如下:
    nums.sort(Integer::compareTo);
    
  • 注意,特定对象的实例方法引用特定类任意对象的实例方法引用区别就在于,Lambda表达式传入的第一个参数是作为实例方法实际参数,还是作为实例方法调用者

3.4、构造方法引用

构造方法引用,下面是一个示例:
Person类的源码:

package com.codeke.java.test;

public class Person {
    // 名称
    private String name;
    // 构造方法
    public Person(String name) {
        this.name = name;
    }
    // toString()方法
    @Override
    public String toString() {
        return "Person{" + "name='" + name + '\'' + '}';
    }
}

Test类的源码:

package com.codeke.java.test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;

public class Test {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("宋江", "卢俊义", "公孙胜", "吴用", "关胜", "林冲");
        // 将存放字符串(人名)的集合转换成存放Person类对象的集合
        // Lambda表达式
        // List persons = convertList(names, name -> new Person(name));
        // 构造方法引用
        List<Person> persons = convertList(names, Person::new);
        persons.forEach(System.out::println);
    }
    // 将存放T类型数据的集合按function给定的方式转换为存放R类型数据的集合
    private static <T, R> List<R> convertList(List<T> list, Function<T, R> function) {
        List<R> result = new ArrayList<>();
        list.forEach(t -> result.add(function.apply(t)));
        return result;
    }
}

执行输出结果:

Person{name='宋江'}
Person{name='卢俊义'}
Person{name='公孙胜'}
Person{name='吴用'}
Person{name='关胜'}
Person{name='林冲'}

说明:

  • 本例的Test类中有一个泛型方法convertList(List list, Function function),该方法有两个参数,第一个参数是一个存放T类型数据的集合,第二个参数是一个功能型函数式接口类型的变量function,方法的返回类型为List。该方法可以按参数function给定的规则,根据存放T类型数据的集合生成一个存放R类型数据的集合并返回。
  • 再观察本例Test类中的main方法,集合names存放了若干字符串类型的人名,需要以这些字符串类型的人名作为Person类的name属性来实例化Person类对象,并获得存放这些Person类对象的集合persons。借助convertList(List list, Function function)方法,用Lambda表达式完成这一功能,代码如下:
    List<Person> persons = convertList(names, name -> new Person(name));
    
  • 观察该Lambda表达式,仅仅是调用了Person类的含参构造方法,调用时方法的实际参数即是Lambda表达式中传入的参数name。该Lambda表达式可以使用构造方法引用简写,如下:
    List<Person> persons = convertList(names, Person::new);
    

你可能感兴趣的:(JAVA学习笔记)