Java 函数式编程:如何将 for 循环中的 if-else 阶梯转换为函数式风格?

Java Functional Programming: How to convert a if-else ladder inside for loop to functional style?

预期是从输入列表 items 中导出 3 个列表 itemIsBothaItemsbItems。 如何将下面的代码转换为功能样式? (我知道这段代码在命令式风格中已经足够清晰了,但我想知道声明式风格是否真的无法处理这样一个简单的例子)。谢谢。

for (Item item: items) {
    if (item.isA() && item.isB()) {
        itemIsBoth.add(item);
    } else if (item.isA()) {
        aItems.add(item);
    } else if (item.isB()){
        bItems.add(item)
    }
}

当然可以。功能性方式是使用声明方式。

数学上你设置了一个Equivalence relation,然后,你可以写

Map<String, List<Item>> ys = xs
    .stream()
    .collect(groupingBy(x -> here your equivalence relation))

一个简单的例子说明了这一点

public class Main {

    static class Item {
        private final boolean a;
        private final boolean b;

        Item(boolean a, boolean b) {
            this.a = a;
            this.b = b;
        }

        public boolean isB() {
            return b;
        }

        public boolean isA() {
            return a;
        }
    }

    public static void main(String[] args) {
        List<Item> xs = asList(new Item(true, true), new Item(true, true), new Item(false, true));
        Map<String, List<Item>> ys = xs.stream().collect(groupingBy(x -> x.isA() + "," + x.isB()));
        ys.entrySet().forEach(System.out::println);
    }
}

有输出

true,true=[com.foo.Main$Item@64616ca2, com.foo.Main$Item@13fee20c]
false,true=[com.foo.Main$Item@4e04a765]

题名比较宽泛(转换if-else阶梯),但由于实际问题问的是具体场景,所以我提供一个示例,至少可以说明可以做什么。

因为 if-else 结构基于应用于项目的谓词创建了三个不同的列表,我们可以将此行为更明确地表达为分组操作。要开箱即用,唯一需要做的额外工作是使用标记对象折叠多个布尔谓词。例如:

class Item {
    enum Category {A, B, AB}

    public Category getCategory() {
        return /* ... */;
    }
}

那么这个逻辑可以简单的表示为:

Map<Item.Category, List<Item>> categorized = 
    items.stream().collect(Collectors.groupingBy(Item::getCategory));

其中每个列表都可以根据其类别从地图中检索。

如果无法更改 class Item,可以通过将枚举声明和分类方法移至 Item class (该方法将成为静态方法)。

不是(在某种意义上是功能性的)使用 lambda 左右,但在仅使用函数(根据数学)并且在任何地方都没有本地 state/variabels 的意义上非常实用:

/* returns 0, 1, 2 or 3 according to isA/isB */
int getCategory(Item item) {
  return item.isA() ? 1 : 0 + 2 * (item.isB() ? 1 : 0)
}

LinkedList<Item>[] lists = new LinkedList<Item> { initializer for 4-element array here };

{
  for (Item item: items) {
    lists[getCategory(item)].addLast(item);
  }
}

这个问题似乎有点争议(在撰写本文时为 +5/-3)。

正如您所提到的,这里的命令式解决方案很可能是最简单、合适和可读的解决方案。

函数式或声明式风格并不真正"fail"。它更像是提出关于 确切 目标、条件和上下文的问题,甚至可能是关于语言细节的哲学问题(比如为什么核心中没有标准 Pair class Java)。

可以在此处应用功能解决方案。一个简单的技术性问题就是您是否真的要 填充 现有列表,或者是否可以创建新列表。在这两种情况下,您都可以使用 Collectors#groupingBy 方法。

两种情况的分组标准是一样的:即任意一项isAisB的特定组合"representation"。对此有不同的可能解决方案。在下面的示例中,我使用 Entry<Boolean, Boolean> 作为键。

(如果你有更多的条件,比如 isCisD,那么你实际上也可以使用 List<Boolean>)。

该示例显示了如何将项目添加到现有列表(如您的问题)或创建新列表(这更简单、更清晰)。

import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;

public class FunctionalIfElse
{
    public static void main(String[] args)
    {
        List<Item> items = new ArrayList<Item>();
        items.add(new Item(false, false));
        items.add(new Item(false, true));
        items.add(new Item(true, false));
        items.add(new Item(true, true));

        fillExistingLists(items);
        createNewLists(items);
    }

    private static void fillExistingLists(List<Item> items)
    {
        System.out.println("Filling existing lists:");

        List<Item> itemIsBoth = new ArrayList<Item>();
        List<Item> aItems = new ArrayList<Item>();
        List<Item> bItems = new ArrayList<Item>();

        Map<Entry<Boolean, Boolean>, List<Item>> map = 
            new LinkedHashMap<Entry<Boolean, Boolean>, List<Item>>();
        map.put(entryWith(true, true), itemIsBoth);
        map.put(entryWith(true, false), aItems);
        map.put(entryWith(false, true), bItems);

        items.stream().collect(Collectors.groupingBy(
            item -> entryWith(item.isA(), item.isB()), 
            () -> map, Collectors.toList()));

        System.out.println("Both");
        itemIsBoth.forEach(System.out::println);

        System.out.println("A");
        aItems.forEach(System.out::println);

        System.out.println("B");
        bItems.forEach(System.out::println);
    }

    private static void createNewLists(List<Item> items)
    {
        System.out.println("Creating new lists:");

        Map<Entry<Boolean, Boolean>, List<Item>> map = 
            items.stream().collect(Collectors.groupingBy(
                item -> entryWith(item.isA(), item.isB()), 
                LinkedHashMap::new, Collectors.toList()));

        List<Item> itemIsBoth = map.get(entryWith(true, true));
        List<Item> aItems = map.get(entryWith(true, false));
        List<Item> bItems = map.get(entryWith(false, true));

        System.out.println("Both");
        itemIsBoth.forEach(System.out::println);

        System.out.println("A");
        aItems.forEach(System.out::println);

        System.out.println("B");
        bItems.forEach(System.out::println);
    }

    private static <K, V> Entry<K, V> entryWith(K k, V v) 
    {
        return new SimpleEntry<K, V>(k, v);
    }

    static class Item
    {
        private boolean a;
        private boolean b;

        public Item(boolean a, boolean b)
        {
            this.a = a;
            this.b = b;
        }

        public boolean isA()
        {
            return a;
        }

        public boolean isB()
        {
            return b;
        }
        @Override
        public String toString()
        {
            return "(" + a + ", " + b + ")";
        }
    }

}

既然你提到了 vavr 作为标签,我将提供一个使用 vavr 集合的解决方案。

import static io.vavr.Predicates.allOf;
import static io.vavr.Predicates.not;

...

final Array<Item> itemIsBoth = items.filter(allOf(Item::isA,     Item::isB));
final Array<Item> aItems     = items.filter(allOf(Item::isA, not(Item::isB)));
final Array<Item> bItems     = items.filter(allOf(Item::isB, not(Item::isA)));

此解决方案的优点是简单易懂,一目了然,而且它的功能与 Java 一样。缺点是它将遍历原始集合三次而不是一次。这仍然是一个 O(n),但乘数为 3。在 non-critical 代码路径和小集合上,可能值得交换一些 CPU 周期代码清晰。

当然,这也适用于所有其他 vavr 集合,因此您可以将 Array 替换为 ListVectorStream

另一种摆脱 if-else 的方法是将它们替换为 PredicateConsumer:

Map<Predicate<Item>, Consumer<Item>> actions = 
  Map.of(item.predicateA(), aItems::add, item.predicateB(), bItems::add);
actions.forEach((key, value) -> items.stream().filter(key).forEach(value));

因此,您需要使用您在 isA()isB() 中实现的逻辑,通过 predicateA()predicateB() 这两种方法来增强您的 Item

顺便说一句,我仍然建议使用您的 if-else 逻辑。

另一种解决方案使用 Vavr 并且只对项目列表进行一次迭代可以使用 foldLeft:

来实现
list.foldLeft(
    Tuple.of(List.empty(), List.empty(), List.empty()), //we declare 3 lists for results
    (lists, item) -> Match(item).of(
        //both predicates pass, add to first list
        Case($(allOf(Item::isA, Item::isB)), lists.map1(l -> l.append(item))),
        //is a, add to second list
        Case($(Item::isA), lists.map2(l -> l.append(item))),
        //is b, add to third list
        Case($(Item::isB), lists.map3(l -> l.append(item)))
    ))
);

它将return一个包含三个结果列表的元组。