使用函数式接口,就这么简单

Posted 梦世界

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用函数式接口,就这么简单相关的知识,希望对你有一定的参考价值。

上一篇文章中,我们已经介绍了Lambda表达式的基本用法。函数式接口的抽象方法的签名,称为函数描述符,为了应用不同的lambda表达式,就需要一套能够描述常见函数描述符的函数式接口。幸运的是,Java8的库设计师早就帮我们定义好了很多常用的函数式接口,它们都放在java.util.function包中,接下来我就会挑选几个主要的函数式接口进行说明。

Java8中定义的函数式接口

Predicate

1@FunctionalInterface
2public interface Predicate<T{
3    boolean test(T t);
4}

java.util.function.Predicate<T>接口定义了一个test的抽象方法,它接受一个泛型T,并返回一个boolean。当你需要通过一个类型去做布尔判断时,就可以使用这个接口。例如,我们可以写一个filter方法,通过传入的Predicate,过滤出符合条件的对象。

 1public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
2    List<T> result = Lists.newArrayList();
3    for (T t : list) {
4        if (predicate.test(t)) {
5            result.add(t);
6        }
7    }
8
9    return result;
10}
11
12List<String> strList = Lists.newArrayList("hello""""lanheixingkong"null"java 8"" ""null");
13Predicate<String> notBlankStringPredicate = str -> StringUtils.isNotBlank(str);// 构造非空判断
14List<String> notBlankStrList = filter(strList, notBlankStringPredicate); // [hello, lanheixingkong, java 8, null]
15List<String> notBlankStrList = filter(strList, str -> StringUtils.isNotBlank(str));// 直接传入Lambda表达式

Consumer

1@FunctionalInterface
2public interface Consumer<T{
3    void accept(T t);
4}

java.util.function.Consumer<T>接口定义了一个accept的抽象方法,它接受一个泛型T,没有返回值(void)。如果你需要访问某个对象,并对它进行某些操作时,就可以使用这个接口。

 1public static <T> void forEach(List<T> list, Consumer<T> consumer) {
2    for (T t : list) {
3        consumer.accept(t);
4    }
5}
6
7List<String> strList = Lists.newArrayList("Hello""Java 8");
8Consumer<String> printStringConsumer = str -> System.out.println(str);
9forEach(strList, printStringConsumer);
10forEach(strList, str -> System.out.println(str));// 直接传入Lambda表达式

Function

1@FunctionalInterface
2public interface Function<TR{
3    apply(T t);
4}

java.util.function.Function<T>接口定义了一个apply的抽象方法,它接受一个泛型T,并返回一个泛型R。如果你需要将某个输入对象,通过操作映射为另外一个输出对象,就可以使用这个接口。例如,我们可以写一个map方法,将传入的T类型的集合对象,通过Function转换为R类型的集合。

 1public static <T, R> List<R> map(List<T> list, Function<T, R> function) {
2    List<R> result = Lists.newArrayListWithCapacity(list.size());
3    for (T t : list) {
4        result.add(function.apply(t));
5    }
6
7    return result;
8}
9
10List<String> strList = Lists.newArrayList("Hello""Java 8""lanheixingkong");
11Function<String, Integer> stringToLengthFunction = str -> str.length();// String转换为Integer
12List<Integer> lenList = map(strList, stringToLengthFunction); // [5, 6, 14]
13List<Integer> lenList = map(strList, str -> str.length());// 直接传入Lambda表达式

Supplier

1@FunctionalInterface
2public interface Supplier<T{
3    get();
4}

java.util.function.Supplier<T>接口定义了一个get的抽象方法,它不接受参数,返回一个泛型T。如果你需要创建一个对象或者重复获取某个对象时,就可以使用这个接口。

 1public enum TeamType {
2    CLUB, COUNTRY;
3}
4
5public class SoccerTeam {
6    private String name;
7    private Enum<TeamType> teamType;
8    private Integer score;
9
10    public SoccerTeam(Enum<TeamType> teamType) {
11        this.teamType = teamType;
12    }
13}
14
15public class ClubFactory {
16    public static SoccerTeam createClub() {
17        return new SoccerTeam(TeamType.CLUB);
18    }
19}
20
21Supplier<SoccerTeam> createClubTeam = ClubFactory::createClub;
22SoccerTeam club = createClubTeam.get();

原始类型特化

在Java中的类型分为两类,原始类型(如int,double,byte)和引用类型(Integer,Double,Byte,Object),而上面这几个函数式接口中定义的泛型都属于引用类型。在Java中,会通过自动装箱机制来自动帮你将这两种类型进行转换,但是,这在性能方面会付出一定的代价。例如:

1List<Integer> list = Lists.newArrayList();
2for (int i = 0; i < 100; i++) {
3    list.add(i);
4}

装箱后的值本质上就是通过引用类型将原始类型包裹起来,并保持在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。

为了避免自动装箱的操作,Java 8就针对前面的这几个接口,都设计了专门的版本,来直接对原始类型进行操作。例如:

 1@FunctionalInterface
2public interface IntPredicate {
3    boolean test(int value);
4}
5
6IntPredicate evenNumbers = (int i) -> i % 2 == 0;
7evenNumbers.test(1000); // 无装箱
8
9Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1;
10oddNumbers.test(1000); // 装箱

一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function接口还有针对输出参数类型的变种:ToIntFunction 、IntToDoubleFunction等。

Java8中常用的函数式接口

复合Lambda表达式的有用方法

Java 8的好几个函数式接口都有为方便而设计的方法,比如用于传递Lambda表达式的Comparator、Function和Predicate都提供了允许你进行复合的方法。这意味着你可以把多个简单的Lambda复合成一个复杂的表达式。比如,你可以让两个Predicate之间做一个or操作,组合成一个更大的Predicate,你还可以让一个Function的结果成为另一个Function的输入。

Comparator复合

在一个足球联赛中,会根据每个球队的积分进行排名,我们可以用Comparator.comparing,来根据球队的积分进行排序:

1import static java.util.Comparator.*;
2
3List<SoccerTeam> teams = Lists.newArrayList();
4teams.sort(comparing(SoccerTeam::getScore));// 按积分递增排序

而一般的球队积分榜都是根据积分从高到低逆序排列,这时我们就可以使用Comparator提供的逆序方法reversed来实现。

1teams.sort(comparing(SoccerTeam::getScore).reversed());// 按积分递减排序

有时候,如果两对积分相同的情况下,我们还需要根据积分相同的球队名称进行二次排序,这时,thenComparing方法就可以实现这个功能。

1teams.sort(comparing(SoccerTeam::getScore).reversed()// 按积分递减排序
2        .thenComparing(comparing(SoccerTeam::getName)));// 按名称二次排序

Predicate复合

Predicate接口包括三个方法:negateandor,它们就相当于逻辑判断中的非、与、或,通过他们进行组合,来创建更复杂的Predicate。例如,我们可以利用这三个方法,根据多个组合条件来筛选出想要的球队:

1Predicate<SoccerTeam> clubTeam = team -> team.getTeamType() == TeamType.CLUB;// 筛选出俱乐部
2Predicate<SoccerTeam> notClubTeam = clubTeam.negate();// 筛选出非俱乐部
3Predicate<SoccerTeam> highScoreClubTeam = clubTeam.and(team -> team.getScore() > 50);// 筛选出积分大于50的俱乐部
4Predicate<SoccerTeam> highScoreClubOrCountryTeam = clubTeam.and(team -> team.getScore() > 50).or(team -> team.getTeamType() == TeamType.COUNTRY);// 筛选出积分大于50的俱乐部或者国家队

注意:and和or方法是按照在表达式链中的位置,从左向右确定优先级的。因此,a.or(b).and(c)可以看作(a || b) && c。

Function复合

Function接口提供了两个默认方法,andThencompose,它们都会返回Function的一个实例。通过这两个方法,你就可以将多个Lambda表达式复合使用,就像是创建一条流水线,每个Lambda表达式就是流水线中的一个环节,组合不同的Lambda表达式就可以创建出不同的流水线。

例如,我们有一个工具类,可以对用String表示的一封信进行文本处理:

 1public class Letter {
2
3    // 增加抬头
4    public static String addHeader(String content) {
5        return "From Lionel Messi:" + content;
6    }
7
8    // 增加落款
9    public static String addFooter(String content) {
10        return content + " Kind regards";
11    }
12
13    // 拼写检查
14    public static String checkSpelling(String content) {
15        return content.replaceAll("labda""lambda");
16    }
17}

现在,我们就可以复合这些工具方法来创建不同的处理流水线。例如,我们可以创建一条流水线,先加上抬头,然后进行拼写检查,最后再加上落款:

1Function<String, String> addHeader = Letter::addHeader;// 增加抬头
2Function<String, String> transformationPipeline = addHeader.andThen(Letter::checkSpelling)// 拼写检查
3        .andThen(Letter::addFooter);// 增加落款

第二条流水线,我们只需要增加抬头和落款,不需要进行评选检查:

1Function<String, String> addHeader = Letter::addHeader;// 增加抬头
2Function<String, String> transformationPipeline = addHeader.andThen(Letter::addFooter);// 增加落款

结束语

在本篇文章中,介绍了如何使用Java 8的API中自带的几个最常用的函数式接口,以及他们针对原始类型如何进行优化,最后又介绍了如何将多个Lambda表达式进行复合使用。现在,你就可以开始在自己的代码中,引入Lambda表达式,去掉那些丑陋不堪的匿名内部类,将那些仅仅需要描述一种行为的参数替换为Lambda表达式。而从下一篇文章开始,我将介绍因为有了Lambda表达式,从而产生的一种全新的对集合的处理方式,

本文案例源码:

https://github.com/lanheixingkong/HelloJava8

参考资料

  • 《Java 8实战》


以上是关于使用函数式接口,就这么简单的主要内容,如果未能解决你的问题,请参考以下文章

函数式接口&Lambda表达式——简单应用笔记

函数式接口&Lambda表达式——简单应用笔记

函数式接口&Lambda表达式——简单应用笔记

Java基础36:函数式编程

常用函数式接口

Java Lambda