5

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

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)
    }
}
4

7 回答 7

8

问题标题相当宽泛(转换 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,则可以通过移动 enum 声明和分类方法使Itemclass 变大(该方法将成为静态方法)来达到相同的效果。

于 2019-07-18T14:23:48.137 回答
4

另一种使用 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)))
    ))
);

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

于 2019-08-01T12:49:36.573 回答
1

您可以摆脱的另一种方法if-else是用Predicateand替换它们Consumer

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));

因此,您需要Item使用这两种方法predicateA()predicateB()使用您在您的isA()isB()

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

于 2019-07-18T15:15:23.030 回答
1

当然可以。函数式的方式是使用声明式的方式。

从数学上讲,您正在设置Equivalence 关系,然后,您可以编写

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]
于 2019-07-18T14:22:24.220 回答
1

既然您提到 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。在非关键代码路径和小型集合上,为了代码清晰而交换一些 CPU 周期可能是值得的。

当然,这也适用于所有其他 vavr 集合,因此您可以替换ArrayList, Vector,Stream等。

于 2019-07-18T14:50:42.563 回答
0

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

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

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

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

在这两种情况下,分组标准是相同的:即,一个项目的特定组合的任何“表示isAisB。对此有不同的可能解决方案。在下面的示例中,我使用 anEntry<Boolean, Boolean>作为键。

(如果您有其他条件,例如isCand isD,那么您实际上也可以使用 a 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 + ")";
        }
    }

}
于 2019-07-18T14:38:54.517 回答
0

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

/* 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);
  }
}
于 2019-07-18T14:33:50.247 回答