1

How can I format a string with streams, without using lambda? I've been looking at Formatter but can't find any method that would only take a single string... so I could do:

Set<String> imported = new HashSet<>();
extendedModels.stream().filter((x)->imported.add(x))
    .map(new Formatter("import {%1$s} from './%1$s';\n")::format);

I'm just starting with Java 8 so not sure if the above is a right syntax (referencing a method of an object).

Specifically I look for a way to format the strings without the lambda expression. The reason is brevity - because, the pre-Java 8 form is just:

for (String m : extendedModels)
    if (imported.add(m))
        tsWriter.write(String.format("import {%1$s} from './%1$s';\n", m));

Details not related to the question:

I'm trying to go through a list of strings, reduce them to unique*) ones, and then use them in an formatted string, which will ultimately written to a Writer. Here's what I have now:

This would work but I'd have to handle an IOException in forEach:

extendedModels.stream().filter(imported::add)
.map((x)->{return String.format("import {%1$s} from './%1$s';\n", x);})
.forEach(tsWriter::write);

So for now I use this:

tsWriter.write(
    extendedModels.stream()
        .filter(imported::add)
        .map((x)->{return String.format("import {%1$s} from './%1$s';\n", x);})
        .collect(Collectors.joining())
);

*) The uniqueness is across multiple sets, not just the extendedModels so I don't want to use some sort of unique stream util.

Ondra Žižka
  • 43,948
  • 41
  • 217
  • 277

2 Answers2

3

As for avoiding using a lambda expression and using only method references, you need to extract the formatting part to a static or an instance method and reference it using a method reference expression:

static String formatImportStatement(String imp) {
    return String.format("import {%1$s} from './%1$s';\n", imp);
}

then .map(YourClass::formatImportStatement). Or you could also extract the labmda itself to a variable like this:

Function<String, String> importFormatter = 
    (s) ->  String.format("import {%1$s} from './%1$s';\n", s);

then use it directly: .map(importFormatter).


Regarding exceptions:

You can use a delegating Writer wrapper which softens (converts checked to unchecked) exceptions and undeclares the checked IOException-s from it's method signatures, then use that with .forEach(softeningWriter::write).

You can also use a lambda wrapper factory to wrap your lambda to soften the exceptions like the LambdaExceptionUtil class in this answer, with the .forEach(rethrowConsumer(tsWriter::write)) pattern.

And your third solution could also work if you don't mind collecting the import statements to a String using Collectors.joining() first.

Unfortunately, you need to work around the checked exceptions (at least in Java8 as of today). Maybe a future version of Java will do something about this legacy, but this is not guaranteed to happen as far as I know.

Community
  • 1
  • 1
Nándor Előd Fekete
  • 6,988
  • 1
  • 22
  • 47
  • Ok thanks, but the question is about the formatting - how to avoid the lambda less readable expression and use something like new Formatter("%s...")::format – Ondra Žižka Oct 09 '16 at 01:34
  • 1
    Yea, sure, you can use `Formatter` too, but then you need to manually query the formatter for `IOException`-s, because `Formatter` swallows IOExceptions for you. – Nándor Előd Fekete Oct 09 '16 at 01:41
  • `String.format` also “swallows the `IOException`” or did you have to add a `try catch` block? – Holger Oct 10 '16 at 17:21
  • @Holger [`String.format`](https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#format-java.lang.String-java.lang.Object...-) doesn't declare to throw `IOException` so there's no need to catch it. – Nándor Előd Fekete Oct 10 '16 at 17:24
  • So does `Formatter.format`. That’s what your statement “then you need to *manually* query the formatter for `IOException`” implies. For the records, `String.format(String,Object...)` is implemented as `return new Formatter().format(format, args).toString();`. So the question is why the OP should be required to “manually query the formatter” when using the `Formatter` the same way `String.format` does. – Holger Oct 10 '16 at 17:28
  • `Formatter` can output to `File`, `OutputStream` or `Appendable`, all of them could throw an `IOException` which `Formatter` will suppress, and you need to explicitly check for that case by querying `Formatter.ioException()` to handle it. `String.format()` will use `Formatter` with a default `StringBuilder` which is an `Appendable` too, but it's guaranteed never to throw an `IOException`. That's a subtle but important difference between the two usage patterns. – Nándor Előd Fekete Oct 10 '16 at 17:38
  • The OP’s suggested use case, `new Formatter("%s...")::format`, if it was valid, isn’t different to what `String.format` does. Actually, the form doesn’t matter, the OP only wants to pre-bind the format string to the construct, be it `String::format` or `new Formatter()::format`. Unfortunately, there is no difference in these two, regarding this intention. – Holger Oct 11 '16 at 11:09
0

Note that while .filter(imported::add) looks like a clever trick, it’s a discouraged technique, as it creates a stateful predicate. If all you want, is uniqueness, just use .distinct() instead. If you need imported later-on, create it with a straight-forward Collection operation, i.e. imported = new HashSet<>(extendedModels) and stream over the Set.

So if your Writer is going to write into a file or any path, there is a FileSystem implementation for, a simple solution is

Set<String> imported = new HashSet<>(extendedModels);
Files.write(path, () -> imported.stream()
    .<CharSequence>map(x->String.format("import {%1$s} from './%1$s';\n", x)).iterator());

or, if you don’t need the imported Set later-on:

Files.write(path, () -> extendedModels.stream().distinct()
    .<CharSequence>map(x->String.format("import {%1$s} from './%1$s';\n", x)).iterator());

If there is no Path, i.e. you can’t avoid using a predefined Writer, you can use a Formatter, but have to care about not missing an exception:

Formatter f=new Formatter(tsWriter);
extendedModels.stream().distinct().forEachOrdered(
    x -> f.format("import {%1$s} from './%1$s';\n", x));
f.flush();
if(f.ioException()!=null) throw f.ioException();

There is no way to provide a bound parameter to a method reference, but since the tokens String, format and the string literal are unavoidable, there is not much potential saving anyway.

Holger
  • 285,553
  • 42
  • 434
  • 765