How to format the content of a Java Properties object when using a Stream?

Viewed 999

The static method System.getProperties() returns a Properties object containing the System Properties as key-value pairs.

In the SO post How would I print the JVM's system properties using Java 8 and lambdas? Radiodef provided a solution which works fine, and it can be enhanced to also sort the output:

        System.getProperties()
                .entrySet()
                .stream()
                .map(e -> e.getKey() + ": " + e.getValue())
                .sorted()
                .forEach(System.out::println);

Here is a small portion of the sorted output produced by that statement:

Unformatted output

I tried unsucessfully to amend the statement above so that the property values in the output are left-aligned. Since java.vm.specification.version happens to be the longest System Properties key, the formatted presentation of the output shown above should look like this:

Formatted output

Obviously the tricky part is to somehow know the length of the longest key prior to formatting the output. Any suggestions on how to achieve this in a single statement using a Stream, or is it impossible?

Update:

I forgot to state in the original post that an additional constraint is that the solution should also work using a parallel stream. My apologies for the oversight.

3 Answers

If you would like to try a single statement, you may try:

    AtomicInteger len = new AtomicInteger();
    System.getProperties()
          .entrySet()
          .stream()
          .sorted((e1, e2) -> Integer.compare(e2.getKey().toString().length(), e1.getKey().toString().length()))
          .peek(e -> len.set(Math.max(len.get(), e.getKey().toString().length())))
          .map(e -> String.format("%-" + len.get() + "s: %s", e.getKey(), e.getValue()))
          .sorted()
          .forEach(System.out::println);

It involves 2 sorts, so the elements will go through the peek to find the maximum length first.

It is not impossible to do so, but I do not recommended indeed, as an unnecessary sort is introduced.

This works:

    AtomicInteger len = new AtomicInteger();
    ((Map<String, String>)(Map)System.getProperties())
    .entrySet()
    .parallelStream()
    .peek(e -> len.set(Integer.max(e.getKey().length(), len.get())))
    .sorted((e1, e2) -> (e1.getKey()).compareTo(e2.getKey()))
    .forEachOrdered(e -> System.out.printf("%-" + len + "s %s%n", e.getKey() + ":", e.getValue()));

Just separate the steps.

  1. Determine the maximum key length
  2. Format all entries using the length

E.g. 

int maxLen = System.getProperties().keySet()
    .stream().mapToInt(k -> ((String)k).length()).max().orElse(0);
String format = "%-"+maxLen+"s %s%n";
System.getProperties().forEach((k,v)->System.out.printf(format, k+":", v));

Note that this also prints the colon left-aligned, as in your example output. When you want to sort this map and/or ensure that there is no modification between these two operations, you may use

@SuppressWarnings("unchecked") Map<String,String> map
                                   = new HashMap<>((Map)System.getProperties());
int maxLen = map.keySet().stream().mapToInt(String::length).max().orElse(0);
String format = "%-"+maxLen+"s %s%n";
map.entrySet().stream()
   .sorted(Map.Entry.comparingByKey())
   .forEach(e -> System.out.printf(format, e.getKey()+":", e.getValue()));

It’s worth noting that all other solution are just using different tricks to hide the fact that there are at least two processing steps.

  • Stream.sort is a stateful intermediate operation. It will buffer the entire stream contents, then sort the buffer and only after that proceed with the downstream operation. This answer relies on the peek following the first sort operation to see the maximum before all other values, which isn’t even a guaranteed property. Whereas this answer does it the other way round, relies on peek having seen all values, before the subsequent sorted step passes any elements to the terminal operation.

  • Another, Collector based answer did two steps in one, calculating the maximum key length and accumulating all entries into a new map, but the printing of the formatted output still could only be done after the completion of the entire collect operation.

It is impossible to do this in one step, but if all you want, is to hide the fact that there are two steps, by letting the operation look like one statement, you can do this with the above solution as well:

((Map<String,String>)(Map)System.getProperties()).entrySet().stream()
   .collect(collectingAndThen(toMap(Map.Entry::getKey,Map.Entry::getValue), map -> {
        int maxLen = map.keySet().stream().mapToInt(String::length).max().orElse(0);
        String format = "%-"+maxLen+"s %s%n";
        map.entrySet().stream()
           .sorted(Map.Entry.comparingByKey())
           .forEach(e -> System.out.printf(format, e.getKey()+":", e.getValue()));
        return null;
   }));
Related