简体   繁体   中英

Java 8 streams: conditional Collector

I want to use Java 8 streams to convert a List of String values to a single String. A List of values like "A", "B" should return a String like "Values: 'A', 'B' added". This works fine, however I want to change the Pre- and Postfix depending on the amount of values. For example, if I have a List of only "A" I want the resulting String to be "Value 'A' added".

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

  }
}

Is there a way to change the Collctor, depending on the size of the resulting List? Then I could do something like

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

I would prefer a single stream operation without an intermediate List result. I also do not know the result beforehand, because I filter the stream.

Edit: I ended up using Eugene's suggestion. What I wanted to do is find the differences between two Lists and return the differences in human readable form. Works nicely!

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

Outputs:

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

Unfortunately, the StringJoiner used by the joining() collector only allows an alternative representation for the “no values” case, but not for the single value case. To add that feature, we have to track the count manually, eg

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

This is quiet complicated, but a “clean” solution; it would even work with parallel streams, if ever needed.

Example

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

One core idea of streams is to have a look at the contained elements individually, possibly in parallel. There are aggregate operations (like count ) that consider all (remaining) elements of a stream. The collect method also is an aggregate, in the sense that it consumes all elements. However, only after it is finished the exact number of items is known.

In your case I would collect the middle part of the string (comma separated list of elements) and add the prefix "Value" or "Values" afterwards.

I suggest you first find the elements you wish to join, and then join them (after you find their count):

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";
}

It doesn't sound like a good idea to count the elements via some side effect of one of the intermediate Stream methods.

You could do it via:

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

And another interesting option probably would be this:

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

}

The advantage is that you don't need to collect to a List to further append each of them separately.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM