【问题标题】:Java 8 streams: conditional CollectorJava 8 流:条件收集器
【发布时间】:2026-02-05 21:25:01
【问题描述】:

我想使用 Java 8 流将字符串值列表转换为单个字符串。像“A”、“B”这样的值列表应该返回像“Values: 'A', 'B' added”这样的字符串。这很好用,但是我想根据值的数量更改 Pre- 和 Postfix。例如,如果我有一个只有“A”的列表,我希望得到的字符串是“添加了值 'A'”。

import java.util.stream.Collectors;
import java.util.ArrayList;
import java.util.List;

public class HelloWorld
{
  public static void main(String[] args)
  {
    List<String> values = new ArrayList<>();
    values.add("A");
    values.add("B");
    values.add("C");
    List<String> value = new ArrayList<>();
    value.add("A");
    System.out.println(log(values));
    System.out.println(log(value));
  }
  public static String log(List<String> values){
    return values.stream()
                 //.filter(...)
                 .map(x -> "'" + x + "'")
                 .collect(Collectors.joining(",","values:"," added"));

  }
}

有没有办法根据结果列表的大小更改 Collctor?然后我可以做类似的事情

.collect(Collectors.joining(",", size = 1 ? "Value " : "Values: "," added"));

我更喜欢没有中间 List 结果的单个流操作。我事先也不知道结果,因为我过滤了流。

编辑:我最终使用了尤金的建议。我想做的是找到两个列表之间的差异,并以人类可读的形式返回差异。效果很好!

import java.util.stream.Collectors;
import java.util.ArrayList;
import java.util.List;


public class HelloWorld
{

  public static void main(String[] args)
  {
    List<String> oldValues = new ArrayList<>();
    oldValues.add("A");
    oldValues.add("B");
    oldValues.add("C");
    List<String> newValues = new ArrayList<>();
    newValues.add("A");
    newValues.add("C");
    newValues.add("D");
    newValues.add("E");
    System.out.println(HelloWorld.<String>log(oldValues, newValues, " deleted"));
    System.out.println(HelloWorld.<String>log(newValues, oldValues, " added"));
  }

    public static <T> String log(List<T> first, List<T> second, String postfix) {
        return  (String) first
                .stream()
                .filter(x -> !second.contains(x))
                .map(x -> "'" + x.toString() + "'").
                collect(Collectors.collectingAndThen(Collectors.toList(),
                    list -> {
                        if (list.size() == 1) {
                            return "Value " + list.get(0) + postfix;
                        }
                        if (list.size() > 1) {
                            List<String> strings = new ArrayList<>(list.size());
                            for (Object object : list) {
                                strings.add(object.toString());
                            }
                            return "Values: " + String.join(",", strings) +  postfix;
                        }
                        return "";
                    }));
    }
}

输出:

Value 'B' deleted
Values: 'D','E' added

【问题讨论】:

    标签: java java-stream collectors


    【解决方案1】:

    不幸的是,joining() 收集器使用的StringJoiner 只允许“无值”情况的替代表示,但不能用于单值情况。要添加该功能,我们必须手动跟踪计数,例如

    public static String log(List<String> values) {
        return values.stream()
                     //.filter(...)
                     .collect(
                             () -> new Object() {
                                 StringJoiner sj = new StringJoiner("', '", "'", "' added");
                                 int num;
                                 String result() {
                                     return num==0? "No values added":
                                                    (num==1? "Value ": "Values ")+sj;
                                 }
                             },
                             (o,s) -> { o.sj.add(s); o.num++; },
                             (o,p) -> { o.sj.merge(p.sj); o.num+=p.num; }
                     ).result();
    }
    

    这很复杂,但却是一个“干净”的解决方案;如果需要,它甚至可以与并行流一起使用。

    例子

    System.out.println(log(Arrays.asList("A", "B", "C")));
    System.out.println(log(Arrays.asList("A")));
    System.out.println(log(Collections.emptyList()));
    
    Values 'A', 'B', 'C' added
    Value 'A' added
    No values added
    

    【讨论】:

    • 哦该死,这很好,这个匿名对象似乎创造了“复杂”的部分,否则我觉得它很漂亮。一个小的缺点是它不会在 Eclipse 下编译,但这里是 Eclipse 问题...
    • 非常感谢,我会尽力了解发生了什么,然后再报告。与 Eugene 的帖子中的 Collectors.collectingAndThen(...) 相比有什么区别/优势?
    • 好吧,collectingAndThen,顾名思义,在后缀操作中执行实际字符串操作之前,会将所有内容收集到List 中。由于在此之前流中没有发生太多事情,因此后缀操作甚至可以在没有流操作return values.isEmpty()? "Nothing found": values.size()==1? "Value '"+values.get(0)+"'": "Values '"+String.join("', '", values)+"'"; 的情况下工作。如果您有过滤等实际流操作,那么collectingAndThen 的缺点是需要为收集的List 提供临时空间。
    • 非常感谢,你的例子成功了!但是,我选择了 Eugene 的方式,因为它更容易理解并且更适合我的用例(使用通用列表)。我稍后会发布我的最终代码,非常感谢您的代码示例和解释!
    【解决方案2】:

    你可以通过:

     return values.stream()
                .map(x -> "'" + x + "'")
                .collect(Collectors.collectingAndThen(Collectors.toList(),
                        list -> {
                            if (list.size() == 1) {
                                return "value" + list.get(0);
                            }
                            if (list.size() > 1) {
                                return String.join(",", list);
                            }
                            return "Nothing found";
                        }));
    

    【讨论】:

      【解决方案3】:

      流的一个核心思想是单独查看包含的元素,可能是并行查看。有考虑流的所有(剩余)元素的聚合操作(如count)。 collect 方法也是一个聚合,因为它消耗所有元素。但是,只有在完成后才能知道确切的项目数量。

      在您的情况下,我将收集字符串的中间部分(以逗号分隔的元素列表),然后添加前缀 "Value""Values"

      【讨论】:

      • 谢谢。获取元素数量的最简单方法是什么?地图方法中的计数,还是有更简单的方法?我也可以计算分隔符,但这有点不安全(值可能包含分隔符)。
      【解决方案4】:

      我建议你先找到你想加入的元素,然后加入它们(在你找到它们的数量之后):

      public static String log(List<String> values) {
          List<String>
              elements = values.stream()
                             //.filter(...)
                               .map(x -> "'" + x + "'")
                               .collect(Collectors.toList());
          String joined = String.join (",", elements);
          return (elements.size () == 1 ? "value " : "values:") + joined + " added";
      }
      

      通过中间Stream 方法之一的一些副作用来计算元素听起来不是一个好主意。

      【讨论】:

      • 谢谢,绝对是一个不错的选择(我不知道为什么我没有考虑过)。
      【解决方案5】:

      另一个有趣的选择可能是这样的:

      public static String log(List<String> values) {
          Spliterator<String> sp = values.stream()
                  .map(x -> "'" + x + "'")
                  .spliterator();
      
          StringBuilder sb = new StringBuilder("Value = ");
      
          sp.tryAdvance(x -> sb.append(x));
          if (sp.tryAdvance(x -> {
              sb.replace(5, 6, "s ");
              sb.append(",").append(x);
          })) {
              sp.forEachRemaining(x -> {
                  sb.append(",").append(x);
              });
          }
      
          return sb.toString();
      
      }
      

      优点是您不需要收集到List 来进一步分别附加它们中的每一个。

      【讨论】:

      • sp.tryAdvance(sb::append)。缺点是代码难以维护,即sb.replace(5, 6, "s ") 必须与上面使用的"Value = " 保持同步。而且它不支持并行处理,如果需要的话……
      • 我赞成,因为看到使用拆分器总是很高兴,但是哦,伙计...
      • @FedericoPeraltaSchaffner 同意oh man 部分,我只认为可以做到吗?
      • 现在我要对你刻薄了......如果你有 people(复数)和 person(单数)而不是 values(复数)和 value(单数)怎么办?这只是为了好玩......
      • @FedericoPeraltaSchaffner 在StringBuilder 中进行了不同的替换...但是您的观点已被理解并且请更频繁地这样做,这就是我首先在这里的原因。谢谢 :)
      最近更新 更多