如何在Java中实现列表折叠

26

我有一个列表,并且想要将其缩减为单个值(函数式编程术语“折叠”,Ruby术语inject),例如

Arrays.asList("a", "b", "c") ... fold ... "a,b,c"

由于我受到函数式编程思想(Scala)的影响,我正在寻找一种比...更简单/更短的编码方式。

sb = new StringBuilder
for ... {
  append ...
}
sb.toString

为什么不编写一个辅助方法并调用它呢?你可以编写自己的函数。 - Peter Lawrey
1
当然,我可以使用给定的代码编写一个函数。但前提是我确定不存在这样的函数。 - Peter Kofler
1
Java中的函数式编程?要小心,穿上厚靴子。 - Juliet
我知道这个问题是在'09年添加的,但是有一个答案表明Java 8已经实现了这个功能,而且有很多答案依赖于库,而现在它已经成为一种语言特性。 - ford prefect
14个回答

15

回答你的原问题:

public static <A, B> A fold(F<A, F<B, A>> f, A z, Iterable<B> xs)
{ A p = z;
  for (B x : xs)
    p = f.f(p).f(x);
  return p; }

F 的长相如下:

public interface F<A, B> { public B f(A a); }

正如dfa所建议的那样,Functional Java已经实现了这个功能,并且还有更多其他功能。

示例1:

import fj.F;
import static fj.data.List.list;
import static fj.pre.Monoid.stringMonoid;
import static fj.Function.flip;
import static fj.Function.compose;

F<String, F<String, String>> sum = stringMonoid.sum();
String abc = list("a", "b", "c").foldLeft1(compose(sum, flip(sum).f(",")));

例子2:

import static fj.data.List.list;
import static fj.pre.Monoid.stringMonoid;
...
String abc = stringMonoid.join(list("a", "b", "c"), ",");

示例3:

import static fj.data.Stream.fromString;
import static fj.data.Stream.asString;
...
String abc = asString(fromString("abc").intersperse(','));

10

考虑到

public static <T,Y> Y fold(Collection<? extends T> list, Injector<T,Y> filter){
  for (T item : list){
    filter.accept(item);
  }
  return filter.getResult();
}

public interface Injector<T,Y>{
  public void accept(T item);
  public Y getResult();
}

然后使用方法看起来像这样:
fold(myArray, new Injector<String,String>(){
  private StringBuilder sb = new StringBuilder();
  public void Accept(String item){ sb.append(item); }
  public String getResult() { return sb.toString(); }
}
);

我仍然无法决定是更倾向于称呼 Injector#getResult 还是 Injector#yield。 - Tetsujin no Oni
在我看来,yield 更多地是一个函数式编程术语,而且由于 fold 也是一个函数式编程术语,所以使用 yield 应该更好。 - Esko
列表参数应该是 Collection<? extends T> list 而不是 super,因为列表是 T 的生产者。 - Juraj Blahunka

8
如果你想在不切换语言的情况下将一些功能方面应用于普通的Java,虽然你可以这样做LamdaJfork-join (166y)google-collections是帮助你添加语法糖的库。
借助google-collections的帮助,你可以使用Joiner class
Joiner.on(",").join("a", "b", "c")
Joiner.on(",")是一个不可变对象,因此您可以自由地共享它(例如作为常量)。
您还可以配置空值处理,例如Joiner.on(", ").useForNull("nil");Joiner.on(", ").skipNulls()
为了避免在生成大字符串时分配大字符串,您可以通过Appendable接口或StringBuilder类将其附加到现有的流、字符串构建器等中:
Joiner.on(",").appendTo(someOutputStream, "a", "b", "c");

写地图时,需要使用两种不同的分隔符来分别表示条目和键值对之间的分隔。
Joiner.on(", ").withKeyValueSeparator(":")
            .join(ImmutableMap.of(
            "today", "monday"
            , "tomorrow", "tuesday"))

7
您需要的是一个字符串 join() 方法,Java 从8.0版本开始支持。请尝试以下方法之一。
  1. Static method String#join(delimiter, elements):

    Collection<String> source = Arrays.asList("a", "b", "c");
    String result = String.join(",", source);
    
  2. Stream interface supports a fold operation very similar to Scala’s foldLeft function. Take a look at the following concatenating Collector:

    Collection<String> source = Arrays.asList("a", "b", "c");
    String result = source.stream().collect(Collectors.joining(","));
    

    You may want to statically import Collectors.joining to make your code clearer.

    By the way this collector can be applied to collections of any particular objects:

    Collection<Integer> numbers = Arrays.asList(1, 2, 3);
    String result = numbers.stream()
            .map(Object::toString)
            .collect(Collectors.joining(","));
    

6
你需要的是一个字符串“join”函数,不幸的是,Java没有这个函数。你需要自己编写join函数,这并不难。
编辑:org.apache.commons.lang.StringUtils似乎有许多有用的字符串函数(包括join)。

是的。我只是希望在Java中,如果问题是将一堆字符串连接成一个字符串,没有正常思维的人会选择https://dev59.com/o3NA5IYBdhLWcg3wdtpd#951004中的用法,而不是使用StringUtils.join()方法。 :) - Jonik
4
@Jonik: 我希望没有理智的人会为了一个方法引入新的依赖 :) - Esko
4
没错,但是一般的项目会受益于使用像Commons Lang或Guava这样的库中的很多东西。 :) - Jonik

3

Eclipse Collections具有类似于Ruby和Smalltalk中的“injectInto”、 “makeString”和“appendString”方法。以下内容可适用于您的示例:

String result1 = FastList.newListWith("a", "b", "c").makeString(",");
StringBuilder sb = new StringBuilder();
FastList.newListWith("a", "b", "c").appendString(sb, ",");
String result2 = sb.toString();
Assert.assertEquals("a,b,c", result1); 
Assert.assertEquals(result1, result2);

注意:我是 Eclipse Collections 的提交者。

2

Java 8的风格(函数式):

// Given
List<String> arr = Arrays.asList("a", "b", "c");
String first = arr.get(0);

arr = arr.subList(1, arr.size());
String folded = arr.stream()
            .reduce(first, (a, b) -> a + "," + b);

System.out.println(folded); //a,b,c

2

首先,您需要一个为Java提供通用functor和功能投影(例如fold)的功能库。我在这里设计并实现了一个功能强大(通过优势)但简单的库:http://www.codeproject.com/KB/java/FunctionalJava.aspx(我发现其他提到的库过于复杂)。

然后,您的解决方案将如下所示:

Seq.of("","a",null,"b","",null,"c","").foldl(
    new StringBuilder(), //seed accumulator
    new Func2<StringBuilder,String,StringBuilder>(){
        public StringBuilder call(StringBuilder acc,String elmt) {
            if(acc.length() == 0) return acc.append(elmt); //do not prepend "," to beginning
            else if(elmt == null || elmt.equals("")) return acc; //skip empty elements
            else return acc.append(",").append(elmt);
        }
    }
).toString(); //"a,b,c"

请注意,应用fold时,真正需要考虑的部分只有对Func2.call的实现,这是三行代码,定义了接受累加器和元素并返回累加器的运算符(我的实现考虑了空字符串和null的情况,如果您删除该情况,则只剩下两行代码)。
以下是Seq.foldl的实际实现,Seq实现了Iterable<E>:
public <R> R foldl(R seed, final Func2<? super R,? super E,? extends R> binop)
{
    if(binop == null)
        throw new NullPointerException("binop is null");

    if(this == EMPTY)
        return seed;

    for(E item : this)
        seed = binop.call(seed, item);

    return seed;
}

2

很不幸,在Java中你无法避免这个循环,但是有几个库可以使用。例如,你可以尝试以下几个库:


2
Google Collections有一个Function接口和一个Lists.map方法,但没有等价的fold方法。然而,它有一个Joiner类适用于这个特定的应用程序。 - Chris Conway

1
借助lambda表达式,我们可以使用以下代码实现:
static <T, R> R foldL(BiFunction<R, T, R> lambda, R zero, List<T> theList){

     if(theList.size() == 0){
      return zero;
     }

     R nextZero = lambda.apply(zero,theList.get(0));

     return foldL(lambda, nextZero, theList.subList(1, theList.size()));                  
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接