Java 8 Stream:使用map方法改变List中的最后一个元素。

4

我有以下代码:

List<String> list= Arrays.asList("a","b","c");
list.stream()
    .map(x->{
        //if(isLast()) return "last";
        return x;
    })
    .forEach(System.out::println);

我想把列表中的最后一项改为“last”,这样程序就会返回类似于以下内容:

a
b
last

注意: 我知道不使用流也可以很容易地完成这个任务,但我想知道是否可以使用流来实现。

注意2: 我提供了一个简单的例子,以便更容易理解流的概念。


2
只需使用循环,它会更简单。Java流不自然地支持诸如“zip”或“zipWithIndex”之类的操作。 - Dici
4
流并不一定按照底层列表中的元素顺序提供元素。另外,你实际上是在寻找什么?你想改变列表还是只想打印列表并对最后一个元素做出反应? - Seelenvirtuose
2
@Seelenvirtuose 你说流不一定按正确顺序提供元素,但是 Collection.stream 文档说它返回一个顺序流。这个流可能无序的唯一方式是使用的 List 实现重写了 streamspliterator 方法。 - Sam
3
@Sam 不是的,你还可以调用Stream.unordered来告诉流,如果需要,顺序可以被打破。但是再次强调,只有在你是流的消费者时,这可能会成为一个问题。如果你是生产者,你很清楚它的顺序。 - Dici
3个回答

12
这个怎么样:
<T> Stream<T> replaceLast(List<T> items, T last) {
    Stream<T> allExceptLast = items.stream().limit(items.size() - 1);
    return Stream.concat(allExceptLast, Stream.of(last));
}

该方法将输入列表除最后一项外的所有项分为一个流,将替换的最终项分为另一个流。然后它返回这两个流的连接。
示例调用:
>>> List<String> items = Arrays.asList("one", "two", "three");
>>> replaceLast(items, "last").forEach(System.out::println);
one
two
last

思考一下,如果使用Stream.limit让你不太舒服,也可以使用Stream<T> allExceptLast = items.subList(0, items.size() - 1).stream();


8
虽然这段代码能运行,且不像其他答案一样是个可怕的hack,但我认为Stack Overflow上的人不应该仅仅试图回答问题。有时我们的角色是说:“伙计,你试图做的事情没有意义,用这种更好的方式来完成它。”对我来说,这绝对不是一个流很擅长的事情,只需看一下使用循环会让它变得更短。 - Dici
1
@Dici 但是有些情况下,拥有访问Stream的能力非常有用,甚至是必须的。这完全取决于使用情况。原帖已经说明他们正在寻找基于流的解决方案,并且这只是真实场景的简化版本。 - Sam
1
对我来说,使用这种方法的优点是它使过滤步骤可重复使用,而不是使用循环。现在,您随时可以访问一个迭代器,以替换列表的最后一项。有什么不喜欢的呢? - Sam
2
很好。在更广泛的背景下,这可能是有意义的。+1 - Dici
1
从概念上讲,这两种方式是相同的,但考虑到当前的实现,“items.subList(0, items.size() - 1).stream()”比“items.stream().limit(items.size() - 1)”更有效。 - Holger

7
我不期望这个答案会被点赞,因为我不会直接回答你的问题,但我认为必须有人清楚地说出这一点。
Stack Overflow并不是一个我们应该尝试回答任何问题的地方。有时候最好的做法是解释问题的方法错误并向其展示更好的方法。
就这个特定的情况而言,我认为流并不是你要找的。你应该把流看作序列,处理它的方式通常是逐个元素处理,大多数时候没有向前或向后查找。流可能是无限的或非常巨大的,并且你不知道它何时结束。
相反,列表是具有给定大小的有限数据结构。最后一个元素的概念是定义明确的。其他人能够给你答案的唯一原因是他们知道流来自列表。对于可以来自任何地方的一般流,你将无法这样做。
这种问题的流方法是使用迭代器,它完全不关心流的来源。
Stream<String> stream = someStream;
Iterator<String> it = stream.iterator();
while (it.hasNext()) {
     String next = it.next();
     System.out.println(it.hasNext() ? "last" : next);
}

这并不美观,但这是正确的流式方法。现在,您有了一个列表,因此也可以使用传统的for循环。然而,这个迭代器循环实际上是处理未知列表的最佳方式。for循环只适用于随机访问列表,并且在其他情况下速度较慢。


2
使用hasNext()检查来识别最后一个项目是一种巧妙的方法,可以创建一个非常优雅的循环。当您有一个流(或任何可迭代对象)作为输入并且想要一个执行此操作的迭代器时,这是一个很好的解决方案。 - Sam
谢谢你的帖子。我已经点赞了你的回答。我理解流的概念,但我想要一个针对这特定情况的答案,即当我使用有限数据结构时的情况。我使用流的原因是因为更容易应用过滤器和其他有趣的功能。 - Doua Beri
3
@DouaBeri Sam的解决方案简洁明了,而且他提出了一个很好的论点,说明为什么这样做有用。如果你将流程移交给其他人,他的答案会更好;如果流程来自其他人,我的答案会更好。实际上,这很有趣,因为它取决于观点(生产者或消费者)。感谢您的赞同^^ - Dici

3
我想将列表中的最后一个项目更改为“last”。 你不需要(也不想)使用流。只需直接使用 set() 更改最后一个元素即可:
list.set(list.size() - 1, "last");

如果出于某种原因(未说明),您必须使用流,则会遇到问题,因为流不知道其当前位置与其余流的位置有什么关系。

您可以通过规避引用变量的“有效最终”要求来将计数器引入lambda中,方法如下:

int[] index = {0}; // array is effectively final (contents may change!)
list.stream()
    .map(x -> ++index[0] == list.size() ? "last" : x)
    .forEach(System.out::println);

除非你确定lambda表达式不会在其他地方使用,否则不要这样做。因为尽管列表按顺序流式传输它们的元素,但其他流可能是并行流,此时你的特殊操作将发生在流中一个不确定的位置。


7
第二段代码片段是一种可怕的黑客行为。在真实的代码中绝对不应该这样做。 - Dici
2
@Bohemian,这违背了函数式编程范式,它有些不优雅且与循环相比没有任何优势。正如您所提到的,它也无法在并行流中运行,但是流和函数式编程的整个要点就是一切都应该是不可变的,以便函数是纯的,并且无论哪个线程运行它们的代码都不会有影响。 - Dici
3
我知道。但是,由于显然OP是初学者,你的“解决方案”应该附带一些警告。它不能(或者最好不要)应用到其他代码区域...至少不是所有其他区域。此外,由于你的“解决方案”存在副作用,因此需要注意。 - Seelenvirtuose
1
@Seelenvirtuose 公正的评论。我已经添加了一个“请勿在家中尝试”的免责声明。 - Bohemian
1
这违背了流的概念。抽象地说,流不知道何时会被终止(或在您的情况下,列表何时结束)。对于这个答案点赞。 - Reut Sharabani
显示剩余11条评论

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