Ondra Žižka
Ondra Žižka

Reputation: 46876

Java 8 streams & Formatter or String::format

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.

Upvotes: 1

Views: 9991

Answers (2)

Holger
Holger

Reputation: 298439

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.

Upvotes: 0

N&#225;ndor Előd Fekete
N&#225;ndor Előd Fekete

Reputation: 7108

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.

Upvotes: 3

Related Questions