dotvav
dotvav

Reputation: 2848

What is a performant way to make Java8 streams produce a formatted string

Context: given a directory, I'd like to list all the files in there that contain a pattern in their name, ordered by lastModified timestamp, and format this list in a Json string where I'd get the name and timestamp of each file:

[{"name": "somefile.txt", "timestamp": 123456},
{"name": "otherfile.txt", "timestamp": 456789}]

I've got the following code:

private StringBuilder jsonFileTimestamp(File file) {
    return new StringBuilder("{\"name\":\"")
            .append(file.getName())
            .append("\", \"timestamp\":")
            .append(file.lastModified())
            .append("}");
}

public String getJsonString(String path, String pattern, int skip, int limit) throws IOException {

    return Files.list(Paths.get(path))
            .map(Path::toFile)
            .filter(file -> {
                return file.getName().contains(pattern);
            })
            .sorted((f1, f2) -> {
                return Long.compare(f2.lastModified(), f1.lastModified());
            })
            .skip(skip)
            .limit(limit)
            .map(f -> jsonFileTimestamp(f))
            .collect(Collectors.joining(",", "[", "]"));
}

This is working well. I am just concerned on the performance of the StringBuilder instantiation (or String concatenation). It is OK as long as the number of files keeps small (which is my case, so I'm fine) but I am curious : what would you suggest as an optimization ? I feel like I should use reduce with the correct accumulator and combiner but I can't get my brain around it.

Thanks.


UPDATE

I finally went with the following "optimization":

private StringBuilder jsonFileTimestampRefactored(StringBuilder res, File file) {
    return res.append(res.length() == 0 ? "" : ",")
            .append("{\"name\":\"")
            .append(file.getName())
            .append("\", \"timestamp\":")
            .append(file.lastModified())
            .append("}");
}

public String getJsonStringRefactored(String path, String pattern, int skip, int limit) throws IOException {
    StringBuilder sb = Files.list(Paths.get(path))
            .map(Path::toFile)
            .filter(file -> file.getName().contains(pattern))
            .sorted((f1, f2) -> Long.compare(f2.lastModified(), f1.lastModified()))
            .skip(skip)
            .limit(limit)
            .reduce(new StringBuilder(),
                    (StringBuilder res, File file) -> jsonFileTimestampRefactored(res, file),
                    (StringBuilder a, StringBuilder b) -> a.append(a.length() == 0 || b.length() == 0 ? "" : ",").append(b))
            ;
    return new StringBuilder("[").append(sb). append("]").toString();
}

This version is creating only 2 instances of StringBuilder when the older one is instantiating as many of them as there are files in the directory.

On my workstation, the first implementation is taking 1289 ms to complete over 3379 files, when the second one takes 1306 ms. The second implementation is costing me 1% more time when I was expecting (very small) savings.

I don't feel like the new version is easier to read or maintain so I'll keep the old one.

Thanks all.

Upvotes: 1

Views: 159

Answers (1)

String formatting is such a trivial portion of your application's performance that it's almost never worth optimizing; only think about it if profiling shows an actual hot spot. In fact, most applications use reflective JSON mappers, and their bottlenecks are elsewhere (usually I/O). The StringBuilder approach you're using is the most efficient way you can do this in Java without manually twiddling character arrays, and it's even going farther than I would myself (I'd use String#format()).

Write your code for clarity instead. The current version is fine.

Upvotes: 8

Related Questions