Java 8中的流(Streams)用于字符串操作

7
我希望在单个字符串上执行多个任务。 我需要获取一个字符串并使用分隔符("/")提取不同的子字符串,然后反转子字符串列表,最后使用另一个分隔符(".")将它们连接起来,使得/tmp/test/hello/world/变为:world.hello.test.tmp 在Java 7中,代码如下:
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());

我想知道如何使用Java 8流和其字符串连接函数来实现相同的操作

4个回答

10

最直接的方法是将项收集到列表中,反转列表并使用新分隔符连接:

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);
你可以不使用中间列表即可完成任务,但这会使代码不易阅读,对于实际应用来说并不值得麻烦。
另一个需要考虑的问题是你的字符串似乎是路径。通常最好使用 Path 类而不是手动按“/”拆分。以下是如何做到这一点(此方法还演示了如何使用索引上的 IntStream 反向流式处理列表):
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("."));

这将具有跨操作系统的优势。


1
不知道你在写“不太可读”时是否考虑到了这种简化方式。对我来说,reduce((s1, s2) -> String.join(".", s2, s1)) 看起来和 collectreversejoin 一样易读 :-) - Roland
2
使用Path是与操作系统无关的,如果任务确实是要替换默认FileSystem中的分隔符字符而不是特定的'/',就像问题中所要求的那样。如果我们讨论的是URI部分或ZIP文件条目等内容,这可能不是正确的解决方案(实际上会添加操作系统依赖项)。 - Holger
@Holger,你说得对,这就是为什么我使用“通常”更好的Path。我不知道问题的上下文,所以我觉得展示两种方法会很好。 - Misha

5

如果您不想要一个中间列表,而只是想反向连接String

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

或者直接使用.reduce((s1, s2) -> s2 + '.' + s1);,因为它可能与String.join(".", s2, s1); 一样易读(感谢Holger提出的建议)。

从那时起,您可以选择以下任何一种方案:

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

另一种使用 StreamSupportSpliterator 的方法(受Misha建议使用 Path 的启发):

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

当然,您可以简化它,省略中间的Optional对象并直接调用所需的方法:
stream(get(str).spliterator(), false)
    .map(Path::getFileName)
    .map(Path::toString)
    .reduce((s, s2) -> s2 + '.' + s)
    .ifPresent(out::println); // orElse... orElseThrow

在最后一个示例中,您需要添加以下静态导入:
import static java.lang.System.out;
import static java.nio.file.Paths.get;
import static java.util.stream.StreamSupport.stream;

"(s, s2) -> String.join(delimiter, s2, s)" 真的比 "(s1, s2) -> s2+delimiter+s1" 更优秀吗?" - Holger
好主意!s2 + '.' + s1 也肯定可以做到... 没有质疑那种用法;-) - Roland

2

你的Java 7代码并不是我所谓的直接解决方案。
这是我在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);

重新思考一下,这也是我在Java 8中实现它的方式,因为使用Stream API并不能真正改善此操作。


1
这可能是个人口味问题...我认为这个解决方案需要读者更多的关注...我需要阅读整个代码才能理解正在发生的事情...在流解决方案中,我可以逐步检查mapreduce和/或collect参数。 - Roland
1
@Roland:到目前为止,我还没有看到一个令人信服的基于Stream的解决方案,可以让读取变得如此简单。我认为Misha的解决方案使用Collections.reverse是可读的,但这也不是一个Stream操作。当然,在实际应用中,您会将我的代码放入一个命名方法中,并创建一些单元测试用例... - Holger
1
也许我已经成为了一个“Stream”迷,这就是为什么我认为map(Path::getFileName).map(Path::toString).reduce((s1, s2) -> s2 + '.' + s1);和将其放入中间列表一样易读...将难以理解的代码移入命名方法可能总是一个好主意;-)(无论应用哪种解决方案)...所以...这仍然是一个品味问题...(和性能)。 :-) - Roland

1
你可以这样写:

你的文字内容

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);

该过滤器会消除第一个反斜杠,reduce方法需要控制是否存在其他最后的空字符串。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接