用高阶函数轻松实现Java对象的深度遍历

Java中的函数式编程(五)Java集合框架中的高阶函数 写在前面 随着Java8引入了函数式接口和lambda表达式,Java8中的集合框架(Java CollectionsFramework, JCF)也增加相应的接口以适应函数式编程。 本文的目标是带领大家熟悉Java8中集合框架新增的常用接口,让我们的代码更简洁、更高级。 本文的示例代码可从gitee上获取,完整文章获取,搜索关注公众号,员说: Java中的函数式编程示例代码https://gitee.com/cnmemset/javafp Java8中的集合框架 首先,和大家从整... 阅读详情

前言:高阶函数是函数式编程中较为初级的一项技术。笔者首先用高阶函数实现了对象浅遍历,然后在其基础上轻松实现了对象的深度遍历。然后将代码稍加改造,实现了带路线追踪的深度遍历和部分遍历。最后,笔者通过之前的例子对函数式编程进行进一步讨论。笔者希望通过本文向大家展示函数式编程相对于传统的编程模式在思路上的不同。

前言

高阶函数是函数式编程中较为初级的一项技术。笔者首先用高阶函数实现了对象浅遍历,然后在其基础上轻松实现了对象的深度遍历。然后将代码稍加改造,实现了带路线追踪的深度遍历和部分遍历。最后,笔者通过之前的例子对函数式编程进行进一步讨论。笔者希望通过本文向大家展示函数式编程相对于传统的编程模式在思路上的不同。

背景知识的简单介绍

本小节主要面对不太了解函数式编程的读者,如果你也是Functional Lover,可以跳过这一节。

幺元(identity)

幺元的概念非常简单。不严谨地说,幺元就是某个类型的基本单元。在数学上,对于类型A的某个元素e和任意元素a,在运算符⊕上,如果满足公式e ⊕ a = a ⊕ e = a,那么e就是类型A的幺元(双边)。

例如,相对于运算符+而言,Integer的幺元就是0,String的幺元就是"";对于*而言,Integer的幺元就是1;对于或运算而言,Boolean的幺元就是false;其他的,列表的幺元就是空列表,对象的幺元就是空对象,每次当我们new一个对象的时候,实际上就是创建了一个幺元。请注意,null不是对象的幺元。

Java的函数类型

Java 8添加了如下函数类型:一元函数Function、一元消费器Consumer、一元谓词Predicate、二元函数BiFunction、二元消费器BiConsumer、二元谓词BiPredicate等等。事实上,这些类型都只是接口,没有具体的对象实现,她们代表的是不同入参和回参的方法。本质上,方法就是函数。例如:

// 方法写法
// 接受一个整型入参,返回值类型为整型
Integer increase (Integer x) { return x = 1; }
increase(1); // 2

// 函数写法
// 一元函数:一个参数,一个返回值,类似于 y = f(x)
Function<Integer, Integer> increase = x -> x + 1;
increase.apply(1); // 2

// 其他函数
// 一元消费器:只有入参,没有返回值
Consumer printOne = System.out::println; // 等价于 print = o -> System.out.println(o);
printOne.accept(1);
// 一元谓词:返回布尔型的函数
Predicate<Integer> equalOne = x -> x == 1;
equalOne.test(1);
// 二元函数:两个参数,一个返回值,类似于 y = f(x1, x2)
BiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;
// 等等......

简单的说,我们只是将方法声明成了变量。从此,方法也成为Java名词王国中的一等公民。

函数的类型签名

既然函数变成了变量,那么变量总是有类型的,在函数变量名的左边就是函数类型。其中<>外的是自身的类型,<>内最后一个是返回值类型。类型签名是一个函数的灵魂。我们以上面的add为例,她的类型为BiFunction<Integer, Integer, Integer>。其中,最外面的BiFunction表示这是一个二元函数,前两个Integer表示两个参数必须是整型,最后一个Integer表示返回值是整型。

函数的幺元

既然函数有了类型,那就必然有幺元,那么函数的幺元是什么呢?其实很简单,函数的幺元就是:什么都不做。

Function identity = o -> o; // 一元函数幺元
Consumer pass = o -> {}; // 一元消费器幺元

高阶函数

函数有了类型,又可以接收参数,那么函数是否可以做为参数传入和返回呢?答案是肯定的。如果一个函数的返回类型还是函数,那么这个函数就是一个高阶函数。

我们写一个稍微复杂点的函数的类型签名,这个一元函数接收一个一元函数做为参数,并返回一个一元函数:

Function<Function<Integer, Integer>, Function<Integer,Integer>>
    increaseBeforeApplyF = f -> x -> f.apply(x + 1);
// 调用方式
Integer y = increaseBeforeApplyF.apply(someFunction).apply(x);
// 部分调用
Function<Integer,Integer> somePartialFunction = increaseBeforeApplyF.apply(someFunction);

首先,你可能会狐疑为什么这么多的.apply、.accept、.test,她们只是表示要调用一下。我们并不能像方法一样去调用函数,因为最初的Java认为什么都可以是对象,唯独方法不是对象。

另外,你可能想问,当发生部分调用的时候系统做了什么。事实上系统什么也没做,因为啥也做不了。只有完全调用后,系统才会开始计算。这种特性称为惰性求值。

Stream流式计算里的高阶函数

高阶函数并不罕见,Java 8中的流式计算都是高阶函数,主要用来处理集合类型,深受广大程序员的喜爱。在各种编程语言中几乎都支持以下高阶函数:

  • map 映射函数:接收一个一元函数,将集合中所有元素都映射到另一个集合上
  • reduce 归纳函数:接收一个幺元,和一个二元函数,将集合中所有元素归纳成一个元素
  • filter 过滤函数:接收一个一元谓词,返回集合中谓词断言为真的元素集合

注: 本文为了区分高阶函数和低阶函数,所有的高阶函数都使用F做为结尾。并且尽量不使用方法的写法,目的为了将函数的类型签名集中在一处,方便阅读。

对象的浅遍历

我们不用思考究竟要举什么样的例子来说明我们实现了对象的遍历,这种传统的思路是非常有害的。我们只需要知道将来有个很普通的函数f,她需要遍历处理对象中的所有元素。因此,我们写的遍历函数应当是一个修饰、或者是一个增强,她可以将一个普通的函数f升级成为一个可以遍历对象元素的函数。

我们所要遍历的目标,可能是集合或者是一个非集合对象,他们可能有若干个元素或属性,每个元素或属性又有可能是一个集合或者是一个非集合对象。直接思考如何深度遍历是见很困难的事情,我们先思考如何实现单层的浅遍历。

List列表的遍历

我们可以从最简单的List列表写起:
traversalListF - 接收一个普通的函数f做为参数,返回一个可以消费List的消费器,最终会遍历List的索引,将每个元素都用f处理一下

/** 列表单层遍历修饰符 */
private static final Function<Function, Consumer<List>>
    traversalListF = f -> l -> IntStream.range(0, l.size()).forEach(i -> l.set(i, f.apply(l.get(i))));

我们可以用x -> x + 1去测试一下List<Integer> list = Arrays.asList(1, 2, 3),然后将list打印出来,看看我们写的遍历器有没有将元素各个加1。事实上,根本没有这个必要,也非常不“函数范儿”。事实上,测试的时候,我们只需要知道我们的函数确实遍历到了每个元素就可以了,具体遍历做什么并不重要。

我们写一个辅助函数beforeF:这个函数依次接收一个函数f和一个消费器c做为参数,并在f处理入参之前,先用c消费一下入参。

/** 前置调用修饰符 */
public static final Function<Function, Function<Consumer, Function>>
    beforeF = f -> c -> o -> { c.accept(o); return f.apply(o); };

那么我们可以这样测试:遍历的对象原封不动,但是在此之前将他们打印出来。

/** 函数幺元 */
private static final Function identity = o -> o;

//测试
List<Integer> list = Arrays.asList(1, 2, 3);
traversalListF.apply(beforeF.apply(identity).apply(System.out::println)).accept(list);

// 输出
1
2
3

同样,我们可以写一个afterF

/** 后置调用修饰符 */
public static final Function<Function, Function<Consumer, Function>>
    afterF = f -> c -> o -> { Object oo = f.apply(o); c.accept(oo); return oo; };

// 测试
traversalListF.apply(afterF.apply(beforeF.apply(x -> x + 1).apply(System.out::println)).apply(System.out::println)).accept(list);
// 输出
1
2
2
3
3
4

有些人可能会笑了:这样岂不是可以一行写完所有代码?其实beforeF、afterF表达的是两个函数之间的顺序关系。更深一步来讲,不仅顺序结构,选择结构、循环结构都只是一种关系或者亦可称为一种数据结构,本质上没有区别。在数学家眼里:这个世界没有什么东西是对象,有的只是关系。

Map映射的浅遍历

对Map的遍历也很简单:
traversalMapF - 接收一个函数f,将Map里所有的值都处理一下

/** 映射单层遍历修饰符 */
private static final Function<Function, Consumer<Map>>
    traversalMapF = f -> m -> m.forEach((k,v) -> m.put(k, f.apply(v)));

// 测试
Map<String, Integer> map = new HashMap();
map.put("first", 1);
map.put("second", 2);
map.put("third", 3);
traversalMapF.apply(beforeF.apply(identity).apply(System.out::println)).accept(map);
// 输出
1
2
3

Set集合的浅遍历

对Set的遍历要特殊一些,因为处理完之后,元素的数量并不一定和原来相等。
traversalSetF - 使用traversalListF帮助Set遍历对象,将生成的元素重新加入到Set中

/** 集合单层遍历修饰符 */
private static final Function<Function, Consumer<Set>>
    traversalSetF = f -> s -> {
        List l = new ArrayList(s);
        traversalListF.apply(f).accept(l);
        s.clear();
        s.addAll(l);
    };

//测试
Set<Integer> set = new HashSet();
set.add(1);
set.add(2);
set.add(3);
traversalSetF.apply(x -> 1).accept(set);
set.forEach(System.out::println);
//输出
1

非集合类型的Object浅遍历

要想遍历一个未知对象的所有属性,只能使用反射。我们先写一个高阶函数modifyF。
modifyF - 她依次接收处理的函数f、处理的对象o和和她的属性名field,并用f去处理o的field

/** 反射修改对象属性 */
private static final Function<Function, Function<Object, Consumer<Field>>>
    modifyF = f -> o -> field -> {
        try {
            field.setAccessible(true);
            field.set(o, f.apply(field.get(o)));
        } catch (IllegalArgumentException | IllegalAccessException e){ /* pass.accept(o) */ }
    };

然后我们去写遍历对象属性的函数traversalObjectF,要注意对象的静态属性其实是全局变量,不能当作对象的成员去看待。
traversalObjectF - 遍历对象o的所有属性,如果是静态属性,则跳过;如果不是,就用f去处理o的属性

/** 谓词: 属性在对象中是否static修饰 */
private static final Predicate<Field> isStaticField = field -> Modifier.isStatic(field.getModifiers());

/** 对象单层遍历器 */
private static final Function<Function, Consumer>
    traversalObjectF = f -> o -> Stream.of(o.getClass().getDeclaredFields()).forEach(
        field -> {
            if (isStaticField.test(field))  ; /* pass.accept(field) */
            else                            modifyF.apply(f).apply(o).accept(field);
        });

我们测试一下:

class Person {
  private String name;
  private String englishName;
  private String age;
  private String sex;
  // ... oh, my god! a lot of get/set
}
//测试
Person myDaughter = new Person();
myDaughter.setName("Xinyu");
myDaughter.setEnglishName("Alice");
myDaughter.setAge("1");
myDaughter.setSex("Female");

traversalObjectF.apply(beforeF.apply(identity).apply(System.out::println)).accept(myDaughter);
//输出
Xinyu
Alice
1
Female

有人会较真,age怎么可能是String类型,分明应该是Integer,sex也有可能声明为Boolean。对于任意一个对象,不可能所有的属性类型都是相同的,我们的函数应该只处理她能处理的类型,不要越俎代庖。

我们再写一个高阶matchingF和typeF来解决这个问题。
matchingF - 依次接收谓词p、函数f、对象o,如果p断言为真,那么就使用f去处理对象,否则什么都不做
typeF - 接收一个类型,使用matchingF去约束函数的作用范围(仅当对象类型符合预期)

/** 谓词匹配修饰符 */
public static final Function<Predicate, Function<Function, Function>>
    matchingF = p -> f -> o -> p.test(o) ? f.apply(o) : o; /* identity.apply(o) */

/** 类型匹配修饰符 */
public static final Function<Class, Function<Function, Function>> 
    typeF = clazz -> matchingF.apply(o -> o.getClass().equals(clazz));

我们再测试一下:

class Person {
  private String name;
  private String englishName;
  private Long age; // Hope to live long
  private Boolean sex;
  // ... oh, my god! a lot of get/set
}
//测试
Person myDaughter = new Person();
myDaughter.setName("Xinyu");
myDaughter.setEnglishName("Alice");
myDaughter.setAge(1L);
myDaughter.setSex(true);
traversalObjectF.apply(afterF.apply(typeF.apply(String.class).apply(x -> "Hello, " + x)).apply(System.out::println)).accept(myDaughter);
//输出
Hello, Xinyu
Hello, Alice
1
true

以后需要遍历对象时,只需要typeF去约束一下函数就可以了,用matchingF则可以实现更灵活的约束。下文我们不会再使用typeF、matchingF,因为我们只是想实现遍历,具体做什么是没有意义的。

通用对象的浅遍历

到目前为止,我们都没有考虑对象为null的情况。因为我们不会一开始就使用上述遍历器去遍历对象。其实不止是null,如果不小心直接传入了一个Integer或者String,我们也会傻眼的。

我们首先写一个traversalF,让普通的函数f具有识别上述情况的能力。
traversalF - 依次接收f、o:
1. 如果o是null,那什么也不做,也就是跳过;
2. 如果o的类型(注意不是o本身)是final修饰的,那么我们认为已经遇到了f可以处理的对象;
3. 如果都不是,那么同样需要跳过。

/** 谓词: 对象是否为null */
private static final Predicate isNull = Objects::isNull; /* o -> o == null */

/** 谓词: 反射判断对象的类型是否final修饰 */
private static final Predicate isFinalClass = o -> Modifier.isFinal(o.getClass().getModifiers());

/** 通用对象的遍历修饰符 */
private static final Function<Function, Function>
    traversalF = f -> o ->
            isNull.test(o)       ?  o : /* identity.apply(o) */
            isFinalClass.test(o) ?  f.apply(o) :
                                    o; /* identity.apply(o) */

//注意:不可以写成 traversalF = f -> o -> isFinalClass.test(o) ?  f.apply(o) : o;

这里使用了三元运算符的? :的级联,可以清晰的表示向下依次进行匹配返回。因为在函数式编程中,只有表达式的概念,没有逻辑结构的概念。如果不是Java个别写法不支持,我们将干掉所有的if、else、break、continue、for、while、return。一切,为了流畅地阅读代码。

然后,我们写一个函数getTraversalF,让普通的函数f可以自动选择适合的高阶遍历函数。
getTraversalF - 她类似简单工厂模式,通过判断对象是否实现了集合的接口,来获取指定的遍历器。如果以后还有需要特殊处理的类型,那只要加入到遍历器字典就可以了。

/** 单层遍历器字典 */
private static final Map<Class, Function> traversalFMap = new HashMap<>();
static {
    traversalFMap.put(List.class, traversalListF);
    traversalFMap.put(Map.class, traversalMapF);
    traversalFMap.put(Set.class, traversalSetF);
}
private static final Function defaultTraversalF = traversalObjectF;

/** 获取实现已知接口的遍历修饰符 */
private static final Function<Object, Function<Function, Consumer>> 
    getTraversalF = o -> {
            Optional<Class> op = traversalFMap.keySet().stream().filter(clazz -> clazz.isAssignableFrom(o.getClass())).findAny();
            return op.isPresent() ? traversalFMap.get(op.get()) : defaultTraversalF;
        };

最后,我们写一个静态方法simpleTraversal,包装最后的通用对象浅遍历方法。并且我们传入一个String类型的对象,来看看simple是否识别。

/** 浅遍历 */
public static final Consumer simpleTraversal (Function f) {
    return o -> getTraversalF.apply(o).apply(traversalF.apply(f)).accept(o);
}

//测试
simpleTraversal(beforeF.apply(identity).apply(System.out::println)).accept("Hello, world!");
//输出
Hello, world!

深度遍历

终于开始深度遍历了。但是在深度遍历之前,我们需要先思考一下“深度遍历”究竟是什么。如果你的思维已经转变过来,那么你就会很自然地想到:既然各种类型的浅遍历是对函数的一种修饰和增强,那么深度遍历同样也是。

两步实现深度遍历

显然我们最终需要实现深度遍历fullTraversal方法应该具有如下的特点:
fullTraversal - 首先将f增强为深度遍历的函数,然后用getTraversalF增强获取指定遍历器,就可以深度遍历对象了

// depthTraversalF = ?

/** 全遍历 */
public static final Consumer fullTraversal (Function f) {
    return o -> getTraversalF.apply(o).apply(depthTraversalF.apply(f)).accept(o);
}

怎么写这个depthTraversalF呢?我们先简单点,使用写一个nextTraversal让f能够进行进行下一层遍历:
depthTraversalF - 类似traversalF,依次断言isNull、isFinalClass,如果都不是,则说明这个对象不能用f处理,需要进行跳过,但是在跳过对象之前,需要对对象进行遍历

// nextTraversal = ?

private static final Function<Function, Function>
    depthTraversalF = f -> o -> 
            isNull.test(o)       ?  o : /* identity.apply(o) */
            isFinalClass.test(o) ?  f.apply(o) :
                                    beforeF.apply(identity).apply(nextTraversal.apply(f)).apply(o);

即使你忘记做“跳过”动作(beforeF.apply(identity))也没有关系,编译器会提醒你的。

事实上,nextTraversal做的事情和fullTraversal一样,就像接力似的,继续将深度遍历的能力增强给full。最终的形式如下

/** 深度遍历修饰(双递归) */
private static final Function<Function, Function>
    depthTraversalF = f -> o -> 
            isNull.test(o)       ?  o : /* identity.apply(o) */
            isFinalClass.test(o) ?  f.apply(o) :
                                    beforeF.apply(identity).apply(fullTraversal(f)).apply(o);

/** 全遍历 */
public static final Consumer fullTraversal (Function f) {
    return o -> getTraversalF.apply(o).apply(depthTraversalF.apply(f)).accept(o);
}

测试一下

我们写个稍复杂的例子测试一下:

//测试:我的女儿家人有爸爸妈妈,和两个小狗朋友,爸爸的家人有爷爷和奶奶
class Person {
    private String name;
    private Long age; // I hope we all live long
    private Map<String, Person> family;
    private List<Person> friends;
    // ... oh, my god! a lot of get/set
}
Person grandfather = new Person();
grandfather.setName("Grandpa");
Person grandmother = new Person();
grandmother.setName("Grandma");
Map<String, Person> myParents = new HashMap<>();
myParents.put("grandfather", grandfather); myParents.put("grandmother", grandmother);
Person me = new Person();
me.setName("Larry"); me.setAge(30L);
me.setFamily(myParents);

Person lover = new Person();
lover.setName("Lotus"); lover.setAge(30L);
Map<String, Person> family = new HashMap<>();
family.put("father", me); family.put("mother", lover);

Person doggy = new Person();
doggy.setName("Doggy"); doggy.setAge(2L);
Person puppy = new Person();
puppy.setName("Puppy"); puppy.setAge(3L);

Person myDaughter = new Person();
myDaughter.setName("Alice"); myDaughter.setAge(1L);
myDaughter.setFamily(family);
List<Person> friends = new ArrayList<>();
friends.add(doggy); friends.add(puppy);
myDaughter.setFriends(friends);

fullTraversal(beforeF.apply(identity).apply(System.out::println)).accept(myDaughter);
//输出
Alice
1
Lotus
30
Larry
30
Grandma
Grandpa
Doggy
2
Puppy
3

我们数一数,一共设置了12次Integer和String的属性,也一共输出了12个值。深对遍历对象的功能就这么轻松实现了!

总结一下

到这里先总结一下,我们究竟做了什么。我们并没有具体实现深度遍历时要对对象做些什么,我们仅仅是描述了一种遍历的结构,这种结构描述了一个普通函数f到一个普通对象o的一种关系。

我们一共写了以下和深度遍历相关的函数identity、beforeF、isNull、isFinalClass、isStaticField、traversalListF、traversalMapF、traversalSetF、modifyF、traversalObjectF、getTraversalF、depthTraversalF、fullTraversal共13个函数表达式,每个函数都非常的简洁明了,就像平时说话一样。同时,我们并没有约束遍历时具体做什么,任何一个普通的函数都可以升级成深度遍历的函数。或者换句话说:我们通过“说出”这13句话,就让计算机明白了什么是深度遍历。

常规的编程模式是通过向计算机下达指令,step by step,一步一步地完成某个功能。这种编程模式叫做命令式编程。但是事实上,“一步一步下达指令”是计算机擅长的事情,并不是人类擅长的。而我们今天采用的方式,是通过向计算机描述,来让计算机理解某项功能。这种编程模式叫做声明式编程。其实声明式编程也不是很罕见的东西,常用的SQL、XML/HTML/CSS都是声明式的(想想我们平时写的SQL语句是不是也只有一行)。

函数式编程就是声明式编程的一种。简洁、不易出错、更加接近自然语言、高度抽象等等,这些都是函数式编程时的优势。在Java的泛型约束下,笔者书写深度遍历的代码时,几乎是“一遍灵”的,节约了大量的测试和debug时间。但同样值得注意的是,函数式编程提笔尤重,更多的时间是花费在思考和类型设计上。有人曾经开玩笑:命令式程序员把代码写完了,发现函数式程序员还在纸上写写画画;函数式程序员代码写完测试一下OK,发现命令式程序员还在debug。

带追踪器的深度遍历

上文的例子将深度遍历做成了黑盒,我们不能直观的看到函数f的遍历过程。下面我们将之前的代码,改写成一个带有路线追踪能力的深度遍历。可以假设,我们将提供一个函数trace专门负责追踪遍历的路线,而f的执行则可能需要参考trace追踪的结果。

也就说我们的路线追踪函数trace应该是一个二元函数,将对象的属性不断地放入到上次追踪的队列中,返回最新的队列;同时普通的函数f也应该是一个二元函数,她依靠已有的队列来决定如何处理传入的对象。同样的,f和trace究竟是什么并不重要。我们只需要知道f要处理对象,trace要处理队列就可以了。

//猜想
BiFunction trace = (property, lastQueue) -> ??; // push property into queue, and return queue
BiFunction f = (o, latestQueue) -> ??; // handle object according to queue, and return object

同时,我们还要明确什么样的属性才适合放入队列中,显然对象的成员名和Map的键适合做为属性,List和Set的索引则意义不大。

改写之前的函数

因此,对于List和Set,带追踪器的遍历函数只要改变入参数目和函数类型签名就可以了,队列不要变化;而Map和Object,我们则需要将键和属性放入到trace中,获取最新的队列,然后传给f。

/** 列表单层遍历器(带追踪器) */
private static final BiFunction<BiFunction, BiFunction, BiConsumer<List, Object>>
    traversalListBF = (f, trace) -> (l, queue) -> IntStream.range(0, l.size()).forEach(i -> l.set(i, f.apply(l.get(i), queue)));

/** 集合单层遍历器(带追踪器) */
private static final BiFunction<BiFunction, BiFunction, BiConsumer<Set, Object>>
    traversalSetBF = (f, trace) -> (s, queue) -> {
        List l = new ArrayList(s);
        traversalListBF.apply(f, trace).accept(l, queue);
        s.clear();
        s.addAll(l);
    };

/** 映射单层遍历器(带追踪器) */
private static final BiFunction<BiFunction, BiFunction, BiConsumer<Map, Object>>
    traversalMapBF = (f, trace) -> (m, queue) -> m.forEach((k,v) -> m.put(k, f.apply(v, trace.apply(k, queue))));

/** 反射修改对象属性(追踪属性) */
private static final BiFunction<BiFunction, BiFunction, BiFunction<Object, Object, Consumer<Field>>>
    modifyBF = (f, trace) -> (o, queue) -> field -> {
        try {
            field.setAccessible(true);
            field.set(o, f.apply(field.get(o), trace.apply(field, queue)));
        } catch (IllegalArgumentException | IllegalAccessException e){ /* biPass.accept(o, queue) */ } 
    };

/** 对象单层遍历器(带追踪器) */
public static final BiFunction<BiFunction, BiFunction, BiConsumer>
    traversalObjectBF = (f, trace) -> (o, queue) -> Stream.of(o.getClass().getDeclaredFields()).forEach(
    field -> {
        if (isStaticField.test(field))  ; /* pass.accept(field) */
        else                            modifyBF.apply(f, trace).apply(o, queue).accept(field);
    });

/** 单层带追踪器遍历器字典 */
private static final Map<Class, BiFunction> traversalBFMap = new HashMap<>();
private static final BiFunction defaultTraversalBF = traversalObjectBF;
static {
    traversalBFMap.put(List.class, traversalListBF);
    traversalBFMap.put(Map.class, traversalMapBF);
    traversalBFMap.put(Set.class, traversalSetBF);
    //traversalBFMap.put(Object.class, traversalObjectBF);
}
private static final Function<Object, BiFunction<BiFunction, BiFunction, BiConsumer>>
    getTraversalBF = o -> {
        Optional<Class> op = traversalBFMap.keySet().stream().filter(clazz -> clazz.isAssignableFrom(o.getClass())).findAny();
        return op.isPresent() ? traversalBFMap.get(op.get()) : defaultTraversalBF;
    };

private static final BiFunction biIdentity = (o1, o2) -> o1;

public static final Function<BiFunction, Function<BiConsumer, BiFunction>>
        beforeBF = f -> c -> (o, queue) -> { c.accept(o, queue); return f.apply(o, queue); };

private static final BiFunction<BiFunction, BiFunction, BiFunction>
    depthTraversalBF = (f, trace) -> (o, queue) ->
            isNull.test(o)       ?  o : /* biIdentity.apply(o, queue) */
            isFinalClass.test(o) ?  f.apply(o, queue) :
                                    beforeBF.apply(biIdentity).apply(traceTraversal(f, trace)).apply(o, queue);

/** 带追踪器的遍历 */
public static final BiConsumer traceTraversal (BiFunction f, BiFunction trace) {
    return (o, queue) -> getTraversalBF.apply(o).apply(depthTraversalBF.apply(f, trace), trace).accept(o, queue);
}

测试一下

现在可以测试了。事实上,在测试的时候,我们没有必要真的使用队列去完成路线追踪,只要具有和队列类似的追加功能就可以了。我们先简单点,就将对象的各个层级属性,设计成类似文件系统的/x/y/z的形式。因此我们可以这样测试:

//测试
BiFunction pathTrace = (unknown, lastPath) -> unknown.getClass().equals(Field.class) ? lastPath + "/" + ((Field)unknown).getName() : lastPath + "/" + unknown.toString();
BiConsumer printPath = (o, path) -> System.out.println(path + " = " + o);
traceTraversal(beforeBF.apply(biIdentity).apply(printPath), pathTrace).accept(myDaughter, ""); // 传入String的幺元做为初始路径
//输出
/name = Alice
/age = 1
/family/mother/name = Lotus
/family/mother/age = 30
/family/father/name = Larry
/family/father/age = 30
/family/father/family/grandmother/name = Grandma
/family/father/family/grandfather/name = Grandpa
/friends/name = Doggy
/friends/age = 2
/friends/name = Puppy
/friends/age = 3

部分遍历

假设现在我们要写一个部分遍历的方法partial。直接在f里判断当前遍历的对象是否需要处理是不能实现部分遍历的,因为f被增强后一定是个全遍历函数。即我们需要一个二元谓词p,当p去断言当前的o和queue为假时,就不再遍历。改动非常小,只要在深度遍历增强函数中添加一行! p.test(o, queue) ? o :就可以了。但是现在的首要问题是,我们的函数接收参数的形式变成了(f, p, trace) -> (o, queue) -> ...,这是三元高阶函数,但是Java并没有TriFunction这样的类型。这个没关系,我们写个接口告诉Java就行了。

@FunctionalInterface
public interface TriFunction<U, T, S, R> {
    R apply(U u, T t, S s);
}

@FunctionalInterface会告诉Java,这是一个函数接口,请向方法那样去调用她。在编辑器提示下,改造起来是非常快的,这里不再详细解释,有兴趣的朋友可以测试一下。

private static final TriFunction<BiFunction, BiPredicate, BiFunction, BiFunction>
    depthTraversalTF = (f, p, trace) -> (o, queue) ->
            isNull.test(o)       ?  o : /* biIdentity.apply(o, queue) */
            ! p.test(o, queue)   ?  o : /* biIdentity.apply(o, queue) */
            isFinalClass.test(o) ?  f.apply(o, queue) :
                                    beforeBF.apply(biIdentity).apply(partial(f, p, trace)).apply(o, queue);
public static final BiConsumer partial (BiFunction f, BiPredicate p, BiFunction trace) {
    return (o, queue) -> getTraversalBF.apply(o).apply(depthTraversalTF.apply(f, p, trace), trace).accept(o, queue);
}

有些朋友乍一看会有很大的疑惑:为什么浅层遍历没有改造,只传入了f, trace,没有p,那么继续遍历时p的断言还会有效吗?我们可以看一下partial:我们可以看到,浅层遍历的高阶函数传入的f其实是depthTraversalTF.apply(f, p, trace),里面已经包含了p。表面上看是我们将函数做为参数传递给了高阶函数,但事实上,对计算机而言,这是做不到的事情,实际情况是高阶函数将最后接收到的对象(o, queue)回传给了传入的(f, p, trace)。也就是说,高阶函数仅仅是组织了参数之间的关系,她没有办法单独起作用。就像一开始说的那样,如果高阶函数没有接收到完整的参数,那么计算机什么也不会做,因为什么也做不了。

无状态的讨论

再写部分遍历的时候,笔者刻意地将trace放在三个入参的最后,因为trace并不是个普通的函数。

那么我们现在讨论一个遗留的问题:我们写的trace究竟是什么?她该怎样去写?假设我们使用下面的代码去完成上述功能,能得到正确的结果吗?

//测试
BiFunction<Object, List<String>, List<String>> queueTrace = (unknown, lastQueue) -> {
        if(unknown.getClass().equals(Field.class))  lastQueue.add(((Field)unknown).getName());
        else                                        lastQueue.add(unknown.toString()); 
        return lastQueue;
};
BiConsumer<Object, List<String>> printQueue = (o, queue) -> System.out.println(queue.stream().reduce("", (x1, x2) -> x1 + "/" + x2) + " = " + o);
traceTraversal(beforeBF.apply(biIdentity).apply(printQueue), queueTrace).accept(myDaughter, new ArrayList<String>()); // 传入List的幺元做为初始队列

trace怎么写

执行上面的代码时,会发现我们打印的队列会越来越长,每次遍历都会将追踪到的路径加入到同一个队列中。这并不算太糟,至少我们的运行结果是确定的。但是如果traceTraversal内部是多线程执行的,那么f接收到的queue将不会再有意义。

之所以会产生这样的问题,是因为传入的初始幺元new ArrayList<String>()相对于traceTraversal是一个全局对象,而在命令式编程语言中确实存在全局变量被污染的情况。因此,为了保证高并发,我们总是要将锁控制加入到我们的代码中。

事实上,在函数式编程中,并不需要这么做。我们之前描述过:”我们的路线追踪函数trace应该是一个二元函数,将对象的属性不断地放入到上次追踪的队列中,返回最新的队列“。那我们每次都返回一个最新的队列就可以了。

// 正确的写法
BiFunction<Object, List<String>, List<String>> queueTrace = (unknown, lastQueue) -> {
        List<String> lastQueue_ = new ArrayList<>();
        lastQueue_.addAll(lastQueue);
        if(unknown.getClass().equals(Field.class))  lastQueue_.add(((Field)unknown).getName());
        else                                        lastQueue_.add(unknown.toString()); 
        return lastQueue_;
    };

在函数式编程语言(例如Lisp、Haskell、Clojure、Scala等等)中,并不存在全局变量的概念,只有局部变量(自由变量和约束变量),并且一个函数总是会返回一个新的对象。因此,在传统编程语言去书写函数式代码时,我们需要额外注意这一点。

有人会质疑创建新对象带来的性能损耗。事实上,这种损耗是值得的(并且Java new的效率也是非常高的),因为这种方式消灭了依赖状态而运行的代码,让我们真正实现了无锁高并发/并行。加入锁控制只会让我们的代码更加复杂、更加不可控。

没错,函数式编程语言不仅没有那么多的关键字(if、else、for…),没有全局变量,甚至连锁都可以没有。虽然这种说法过于绝对,但在函数式编程中确实可以让你体验”自由“的感觉。

trace是什么

如果抽象来看,trace实际上是traceTraversal运行时的状态迁移函数。trace总是接收当前的状态和之前的状态,返回下一个新的状态。

事实上,我们所有代码都可以抽象成如下的形式:

M a c h i n e ( ∑ i = 1 n ( f i ) , s t a t e ( c s , l s ) ) . l o a d ( d a t a , i s ) , ( i s = 初始状态 , c s = 当前状态 , l s = 上一个状态 ) Machine(\sum_{i=1}^{n}(f_i), state(cs, ls)).load(data, is), (is = 初始状态, cs = 当前状态, ls = 上一个状态) Machine(i=1∑n​(fi​),state(cs,ls)).load(data,is),(is=初始状态,cs=当前状态,ls=上一个状态)

正是这个状态函数state是一个无状态的函数,她只依赖传入参数,使得我们的程序是无状态的。

函数式编程的优势

我们这里总结一下函数式编程的优势:

  1. 自由、简洁、更加接近自然语言
  2. 高度抽象,天生反模式、反算法
  3. 惰性求值,减少不必要的计算
  4. 天生无状态,天生支持高并发/并行

骚年,扔掉GoF,拿起眼前这把剑吧,让我们为了自由去战斗!

(什么什么?你问我什么是单子?好吧,单子不过是自函子范畴上的一个幺半群罢了…T_T)

【Java学习】3大绝招,你真的掌握了IO对象序列化和深度搜索吗? 嘿,小伙伴们!IO对象序列化和深度搜索。是不是听起来就很高大上?别担心,我会用最简单易懂的语言,带你一步步掌握这些技能。准备好迎接挑战了吗?让我们开始吧! 阅读详情

相关推荐

高效解析与遍历:Java中如何循环树形结构

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

喵手的博客 1502

Java 高阶函数

一个高阶函数是指 一个函数使用函数作为参数或者执行完返回函数。在高阶函数教程中我将列举几个 Java 高阶函数的例子。

qq_36438618的博客 688

【Java学习】3步轻松掌握IO-InputStream、深度搜索与刨根问题,你get到了吗?

嘿,小伙伴们!IO-InputStream类型深度搜索以及刨根问题。这三个概念虽然听起来有点高大上,但其实它们都非常实用且容易上手。接下来,我将以最通俗易懂的方式,带大家一起学习这些技巧。准备好了吗?让我们开始吧!

java专栏 852

JAVA中的高阶函数

使用JDK 所提供的 Function<T,R> 接口,可以很容易地实现函数的复合:实现函数复合的关键在于andThen 和 compose 方法,它们接收一个 Function ,又返回一个Function ,这两个函数,都属于“高阶函数”。

xjsj62728的博客 868

【Java学习】3招玩转RandomAccessFile与深度搜索,你get到了吗?

定义类允许你以随机访问的方式读写文件。与传统的顺序读写不同,可以直接跳到文件的任意位置进行读写操作。特点随机访问:可以自由移动文件指针,读取或写入文件的任何部分。读写一体:既可以读取文件,也可以写入文件。字节流:处理的是字节数据。常见方法:将文件指针移动到指定的位置。:从文件的当前位置读取指定数量的字节。:从当前位置写入指定数量的字节。:获取当前文件指针的位置。length():获取文件的长度。示例// 写入数据// 读取数据// 随机访问。

java专栏 1158

【Java学习】3个关键点,你真的掌握了File类和深度搜索吗?

定义File类是 Java 中用于表示文件和目录路径名的抽象表示。它提供了多种方法来操作文件和目录。常见方法创建文件/目录和mkdirs()删除文件/目录delete()检查文件/目录是否存在exists()获取文件/目录的绝对路径列出目录下的文件和子目录示例// 创建一个 File 对象// 检查文件是否存在} else {try {// 创建文件} else {// 获取文件的绝对路径// 删除文件} else {// 创建目录} else {

java专栏 711

java 高阶函数_函数式编程里的高阶函数

高阶函数我记得自己在了解了上面列出的种种优点后曾想:“这都很棒,可是如果我不得不用天生残缺的语言对着全是 final 的变量编程,好特性也毫无意义。” 这其实是误解。在如 Java 这般的命令式语言环境里,所有变量都是 final 将带来一堆问题,但是在函数式语言里并非如此,函数式语言提供了不同的抽象工具使你忘记曾经习惯于修改变量。高阶函数就是这样一种工具。函数式语言中的函数不同于 Java 或 ...

weixin_35460054的博客 413

【Java学习】编写equals和hashCode:3个关键点,你真的掌握了对象的比较吗?

定义equals:用于判断两个对象是否相等。hashCode:用于生成对象的哈希码,通常用于哈希表(如HashMapHashSet)中的快速查找。特点equals:必须满足自反性、对称性、传递性和一致性。hashCode:如果两个对象相等,则它们的哈希码必须相同;如果两个对象不相等,它们的哈希码可以相同(哈希碰撞)。

java专栏 760

JS深层次多级对象Key的遍历方法,将多层级对象扁平化

写了个深层次多级对象Key的遍历方法,对于简单的对象遍历其中的key很简单,但是对于这种又是数组又是对象的多级对象,怎么办呢?难也不难,下面直接给出代码,熟练运用递归,判断一下引用类型,就行了!不要被下面这段代码唬住了,真的不难

专注前端、专注互联网、分享原创的博客 4691

【Java学习】3种Collection集合遍历方法大揭秘:你真的掌握了所有技巧吗?

使用传统的。

java专栏 808

【Java学习】类和接口:5个关键点,你真的掌握了面向对象和深度搜索吗?

类定义:类是对象的蓝图,定义了对象的属性和方法。常见形式属性:类的变量,用于存储数据。方法:类的函数,用于执行操作。构造器:用于创建对象的特殊方法。接口定义:接口定义了一组抽象方法,实现了接口的类必须实现这些方法。常见形式抽象方法:没有具体实现的方法。默认方法:提供了默认实现的方法。静态方法:属于接口本身的方法。

java专栏 980

Java对象差异对比终极指南:5分钟快速掌握对象比较技术

你是否曾经为比较两个Java对象的差异而头疼?在开发过程中,我们经常需要检测对象属性的变化,追踪数据变更,或者实现对象的增量更新。Java-Object-Diff库正是为解决这些问题而生,让你轻松实现Java对象差异检测和属性对比。 ## 为什么需要对象差异对比?🤔 在日常开发中,我们经常会遇到这样的场景: - **数据变更追踪**:记录对象从版本A到版本B的所有变化 - **增量更新**

gitblog_01113的博客 949

Java实现高阶函数,类似Python,支持串行并行计算,适用于Java和Android.zip

Java实现高阶函数,类似Python,支持串行并行计算,适用于Java和Android.zip,java implement Higher-order function ,support map,filter , reduce with parallel, android

java遍历string类型的数组

我整理的一些关于【CI】的项目学习资料(附讲解~~)和大家一起分享、学习一下:https://edu.51cto.com/surl=iqhDF2Java中遍历String类型数组的探索 在Java编程中,字符串数组是一个非常常用的数据结构。它可以用于存储一组字符串,并且在开发中经常需要对这些字符串进行遍历操作。本文将探讨...

weixin_32949451的博客 242

Windows下高效部署llama.cpp:从环境配置到模型量化的完整指南

本文提供了一份详尽的Windows系统下部署llama.cpp的完整指南。内容涵盖从Python环境配置、使用CMake与MinGW编译工具链的搭建,到编译llama.cpp项目、获取并转换模型为GGUF格式,以及执行模型量化和基础推理测试的全过程。指南旨在帮助开发者在普通Windows电脑上,无需高端显卡,仅凭CPU即可高效运行大语言模型,并重点解决了部署过程中常见的环境与编译错误。

weixin_28841265的博客 878

TypeScript的安装与使用

TypeScript的安装与使用

qq_51589308的博客 600

【Python-Day 9】掌握循环利器:for 循环遍历序列与可迭代对象详解

在编程的世界里,我们经常需要重复执行某些任务,比如逐一处理列表中的每个元素、读取文件的每一行,或者简单地执行某个操作固定的次数。手动重复编写代码不仅效率低下,而且容易出错。幸运的是,Python 提供了强大的循环结构来自动化这些重复性工作,for循环就是其中最常用、最灵活的一种。本篇我们将深入探讨for循环的奥秘,学习如何利用它高效地处理各种序列和可迭代对象,并掌握控制循环流程的关键语句break和continue。for嵌套循环意味着一个循环(外层循环)的循环体包含了另一个完整的循环结构(内层循环)。

吴师兄大模型的博客 1629

uses ARM-Compiler ‘Default Compiler Version 5‘ which is not available. MDK5.37安装ARM_Compiler_5

MDK5.37安装ARM_Compiler_5 最后一个AC5编译器,从MDK5.37开始,不再默认安装,需要独立安装 ARMCompiler_506_Windows_x86_b960.zip (80.94MB) 否则使用AC5编译代码报错如下: Build started: Project: Project *** Target 'STM3210C_EVAL' uses ARM-Compiler 'Default Compiler Version 5' which is not available. *

mowwwcom的专栏 2万+

Java对象差异比较库——java-object-diff的下载与安装指南

**Java对象差异比较库(java-object-diff)** 是一款由SQiShER维护的开源工具,它设计用于轻松地找到两个Java对象之间的差异并合并它们。此库具备强大的功能,能够处理任意深度的对象嵌套,并以易于遍历的树状结构返回差异信息。无需对现有类做重大修改,只需简单的配置即可适应各种场景,兼容性追溯至Java 1.5版本。 ![GitHub](https://img.shields...

gitblog_01221的博客 1065

华为OD机试真题 新系统 2026-07-22 Java&Go&C 实现【最小代价完成论文评审】

某校有 n 篇论文需要分配给教师评审。 每篇论文 i 可由 files[i] 对应的教师列表中任一教师评审。 每篇论文至少需要一名教师评审。 每位教师 t 不论被分配多少篇论文,其评审费用固定为 cost[t]。 请给出参与评审教师数量最少时的总费用。 如果存在多种方案满足教师数量最少,则选择总费用最小的方案。

MISAYAONE的博客 147

[Python从零到壹] 十四.机器学习之分类算法五万字总结全网首发(决策树、KNN、SVM、分类对比实验)

分类(Classification)属于有监督学习(Supervised Learning)中的一类,它是数据挖掘、机器学习和数据科学中一个重要的研究领域。分类模型类似于人类学习的方式,通过对历史数据或训练集的学习得到一个目标函数,再用该目标函数预测新数据集的未知属性。本章主要讲述分类算法基础概念,并结合决策树、KNN、SVM分类算法案例分析各类数据集,从而让读者学会使用Python分类算法分析自己的数据集,研究自己领域的知识,从而创造价值。 19.1 分类 19.1.1 分类模型 与前面讲述的聚类模型类似

杨秀璋的专栏 2万+

BabelDOC PDF 双语对照翻译工具快速指南

BabelDOC 是一款开源的 PDF 翻译工具:它在 PDF 内部原地翻译文字,排版、公式、表格都留在原来的位置。它面向最常见的"英文论文翻成中文"场景,也可以作为 Python 库嵌入你自己的程序。最省事的地方在于:跑完后你一次拿到两份文件,左边原文右边译文的对照 PDF,和一份纯译文 PDF,不用在两个窗口之间来回切换核对原文。 [![BabelDOC PDF 双语对照翻译效果示意](ht

gitblog_01043的博客 740

NR PUCCH(二)PUCCH format 2/3/4

继续看PUCCH2/3/4:PUCCH format 0和1所携带的信息bits少,UCI bits2bits,信息bit需要经过编码等过程下面分别来看。

ModemProtocol的博客 537

Java对象差异对比:让对象比较变得简单高效

> 💡 想象一下:你正在开发一个版本控制系统,需要精确追踪用户配置的每一次变更。传统方法需要编写大量重复的反射代码,而Java-Object-Diff库让这一切变得轻而易举。 ## 为什么你需要这个库? 在日常开发中,我们经常遇到这样的场景: - 🔍 **配置变更追踪**:用户修改了哪些系统配置项? - 📝 **文档版本对比**:不同版本的文档内容发生了什么变化? - 🔄 **数据同

gitblog_01190的博客 1021

如何使用 SAP ABAP Development Tool 连接 SAP BTP 上的免费 ABAP 编程环境

在云端环境编写 ABAP 代码会遇到一些限制,比如你无法在云端 ABAP 编程环境进行发生在传统 SAPGUI 里的 Dynpro 和 Screen 编程。部分在 SAPGUI 里使用的 List 处理相关的关键字比如 WRITE,还有 CALL TRANSACTION 等代码,也无法在云端使用。 当然如果大家觉得上面提到的这些限制不会影响自己的 ABAP 学习,就可以继续本文的阅读,使用 ABAP Development Tool 登录云端的 ABAP 编程环境。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 1513

【小白CV教程】YOLOv5+Deepsort实现车辆行人的检测、追踪和计数

【小白CV教程】YOLOv5+Deepsort实现车辆行人的检测、追踪和计数本文禁止转载!项目简介:YOLOv5检测器:DeepSort追踪器:运行demo:训练自己的模型:调用接口:创建检测器:调用检测接口: 本文禁止转载! 项目简介: 使用YOLOv5+Deepsort实现车辆行人追踪和计数,代码封装成一个Detector类,更容易嵌入到自己的项目中。 代码地址(欢迎star): https://github.com/Sharpiless/Yolov5-deepsort-inference 最终效果:

梁瑛平的博客 5万+
上一篇: Java 8 函数式编程入门之Lambda
下一篇: 一个轻量级服务编排框架的实现思路
coderlius
博客等级 码龄9年 141粉丝 10原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值