Java 函数式编程:如何将 for 循环中的 if-else 阶梯转换为函数式风格?
Java Functional Programming: How to convert a if-else ladder inside for loop to functional style?
预期是从输入列表 items
中导出 3 个列表 itemIsBoth
、aItems
、bItems
。
如何将下面的代码转换为功能样式? (我知道这段代码在命令式风格中已经足够清晰了,但我想知道声明式风格是否真的无法处理这样一个简单的例子)。谢谢。
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
方法。
两种情况的分组标准是一样的:即任意一项isA
和isB
的特定组合"representation"。对此有不同的可能解决方案。在下面的示例中,我使用 Entry<Boolean, Boolean>
作为键。
(如果你有更多的条件,比如 isC
和 isD
,那么你实际上也可以使用 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
替换为 List
、Vector
、Stream
等
另一种摆脱 if-else
的方法是将它们替换为 Predicate
和 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));
因此,您需要使用您在 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一个包含三个结果列表的元组。
预期是从输入列表 items
中导出 3 个列表 itemIsBoth
、aItems
、bItems
。
如何将下面的代码转换为功能样式? (我知道这段代码在命令式风格中已经足够清晰了,但我想知道声明式风格是否真的无法处理这样一个简单的例子)。谢谢。
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
方法。
两种情况的分组标准是一样的:即任意一项isA
和isB
的特定组合"representation"。对此有不同的可能解决方案。在下面的示例中,我使用 Entry<Boolean, Boolean>
作为键。
(如果你有更多的条件,比如 isC
和 isD
,那么你实际上也可以使用 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
替换为 List
、Vector
、Stream
等
另一种摆脱 if-else
的方法是将它们替换为 Predicate
和 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));
因此,您需要使用您在 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一个包含三个结果列表的元组。