Hossein
Hossein

Reputation: 1292

Java 8 Streams for String manipulation

I want to perform multiple tasks on a single string. I need to get a string and extract different sub-strings using a delimiter ("/"), then reverse the list of sub-strings and finally join them using another delimiter (".") such that /tmp/test/hello/world/ would turn into: world.hello.test.tmp

Using Java 7 the code is as follows:

String str ="/tmp/test/";
List<String> elephantList = new ArrayList<String>(Arrays.asList(str.split("/")));

StringBuilder sb = new StringBuilder();
for (int i=elephantList.size()-1; i>-1; i--) {
    String a = elephantList.get(i);
    if (a.equals(""))
    {
        elephantList.remove(i);
    }
    else
    {
        sb.append(a);
        sb.append('.');
    }
}
sb.setLength(sb.length() - 1);
System.out.println("result" + elephantList + "   " + sb.toString());

I was wondering how I could do the same thing using Java 8 streams and the join function it has for Strings

Upvotes: 7

Views: 7280

Answers (4)

mbtkasseria
mbtkasseria

Reputation: 21

You can write like this :

String newPath = Arrays.asList(path.split("/")).stream()
            .filter(x -> !x.isEmpty())
            .reduce("",(cc,ss)->{
                if(!cc.isEmpty())
                    return ss+"."+cc;
                else return ss;
            },(s1,s2)->s2+s1);

The filter eliminates first backslash and reduce method has to control if there are any other last empty strings.

Upvotes: 1

Roland
Roland

Reputation: 23262

If you do not want an intermediate list and just want to join the String reversely:

String delimiter = ".";
Optional<String> result = Pattern.compile("/")
                                 .splitAsStream(str)
                                 .filter(s -> ! s.isEmpty())
                                 .reduce((s, s2) -> String.join(delimiter, s2, s));

Or just use .reduce((s1, s2) -> s2 + '.' + s1); as it is probably as readable as String.join(".", s2, s1); (thanks Holger for the suggestion).

From then on you could do one of the following:

result.ifPresent(System.out::println); // print the result
String resultAsString = result.orElse(""); // get the value or default to empty string
resultAsString = result.orElseThrow(() -> new RuntimeException("not a valid path?")); // get the value or throw an exception

Another way using StreamSupport and Spliterator (inspired by Mishas suggestion to use a Path):

Optional<String> result = StreamSupport.stream(Paths.get(str).spliterator(), false)
                                       .map(Path::getFileName)
                                       .map(Path::toString)
                                       .reduce((s, s2) -> s2 + '.' + s);

Of course you can simplify it by omitting the intermediate Optional-object and just call your desired method immediately:

stream(get(str).spliterator(), false)
    .map(Path::getFileName)
    .map(Path::toString)
    .reduce((s, s2) -> s2 + '.' + s)
    .ifPresent(out::println); // orElse... orElseThrow

in the last example you would add the following static imports:

import static java.lang.System.out;
import static java.nio.file.Paths.get;
import static java.util.stream.StreamSupport.stream;

Upvotes: 5

Holger
Holger

Reputation: 298153

Your Java 7 code isn’t what I’d call a straight-forward solution.
This is, how I would implement it in Java 7:

String str = "/tmp/test/";

StringBuilder sb = new StringBuilder(str.length()+1);
for(int s=str.lastIndexOf('/'), e=str.length(); e>=0; e=s, s=str.lastIndexOf('/', e-1)) {
    if(s+1<e) sb.append(str, s+1, e).append('.');
}
if(sb.length()>0) sb.setLength(sb.length() - 1);
System.out.println("result " + sb);

and thinking about it again, this is also how I’d implement it in Java 8, as using the Stream API doesn’t really improve this operation.

Upvotes: 2

Misha
Misha

Reputation: 28133

The most straightforward way is to collect the terms into a list, reverse the list and join on the new delimiter:

import static java.util.stream.Collectors.toCollection;

List<String> terms = Pattern.compile("/")
        .splitAsStream(str)
        .filter(s -> !s.isEmpty())
        .collect(toCollection(ArrayList::new));

Collections.reverse(terms);

String result = String.join(".", terms);

You can do it without collecting into an intermediate list but it will be less readable and not worth the trouble for practical purposes.

Another issue to consider is that your strings appear to be paths. It is usually better to use Path class rather than splitting by "/" manually. Here's how you would do this (this approach also demonstrates how to use IntStream over indexes to stream over a list backwards):

Path p = Paths.get(str);

result = IntStream.rangeClosed(1, p.getNameCount())
        .map(i -> p.getNameCount() - i)  // becomes a stream of count-1 to 0
        .mapToObj(p::getName)
        .map(Path::toString)
        .collect(joining("."));

This will have the advantage of being OS-independent.

Upvotes: 10

Related Questions