Stream 与 Collection as return 类型
Stream vs Collection as return type
我正在讨论哪种设计我们的 API 的最佳方式(Stream 与 Collection as return 类型)。 this post中的讨论很有价值。
@BrainGotez 的回答提到了集合优于流的这种情况。我不太明白这是什么意思,谁能帮忙举个例子解释一下?
"当有很强的一致性要求,并且你必须对移动目标生成一致的快照时。"
我的问题是,具体来说,“强一致性要求”和“移动目标的一致快照”在现实世界的应用程序中意味着什么?
将强一致性视为不断变化的源的时间点快照。假设你是一个电商巨头,想看一个月的销售额,你可以从数据库中return获取12月1日到12月31日所有销售记录的快照,这是一个有限集合(例如List ),尽管对于某些公司来说它可能相当大。这是一个一致的快照集合,因为现有销售可能会因取消或 return 随时间发生变化,但是 API 只是提供创建列表时销售情况的时间点快照。
在同一家公司的另一个用例中,假设数据科学团队有一个应用程序可以在销售交易发生时(移动目标)持续监控以检测欺诈行为,但它是一个没有有限边界的连续数据流此流中的每笔交易都会被提取和分析。
所以基本上,当您 return collection
时,您是 return 在那个特定时刻拍摄玩家对象的快照。也就是说,在这种情况下调用“getPlayersAsCollection”方法时玩家对象的副本。
其他线程对玩家列表的任何更改都不会反映到之前 returned 的集合中。
这解释了,the consistency is maintained
并且在调用 getPlayersAsCollection 方法时,您实际上得到了玩家列表中的内容,该列表不断通过添加新玩家详细信息或从中删除玩家详细信息来修改。这解释了 consistent snapshot of a moving target
.
class Team {
private List<Player> players = new ArrayList<>();
// ...
public List<Player> getPlayersAsCollection() {
return Collections.unmodifiableList(players);
}
public Stream<Player> getPlayersAsStream() {
return players.stream();
}
}
然而,当 stream
在这里被 returned 时,就像指向玩家列表的指针被 returned 一样。 Stream 之间对播放器对象的任何更改都由“getPlayersAsStream”方法 return 编辑,当您尝试访问流对象或对流对象执行流操作时,对播放器对象所做的更改也将在此处反映出来。
因此在这种情况下“没有强一致性”,因为从调用 getPlayersAsStream 并获得响应时到您尝试访问该响应(Stream)时数据发生了变化。
但是,returning Stream 有其自身的优势,正如问题中分享的 link 中所解释的那样。 return Stream 还是 Collection 取决于特定的用例。
我希望这有助于澄清您对
“当有很强的一致性要求时,您必须为移动目标生成一致的快照。”
“当有很强的一致性要求时,您必须生成移动目标的一致快照。”
作者@Brian Goetz指的是流被消费的时间点。
java.util.stream
-API.
的第一个误区就在这里
当您 return 一个流时,您会得到一个对象的句柄,该对象尚未开始拉动。
只有当您调用 termination method 时,集合才会迭代。在此之前,集合及其项目可以更改。
这是关于流的唯一惰性部分。否则你可能想骑 RxJava2
的公牛.. ;- )
// 为赏金编辑:
一个真实世界的例子是:到现在为止,这些特定股票的价格是多少?
然后你想传递不可变的对象,可以用来在检查后下订单。
如果同时价格发生变化 - 但该对象需要下订单 - 您不关心用户下单需要多长时间。
价格只是事先确定的。
// 编辑结束。
无论如何,在开始迭代之前,集合可能会发生同样的情况。这两种情况都与并发访问有关。
Also, this isn't an iteration of the items per-se.
Each object is passed through the chain.
因此,恕我直言,您必须以不同的方式处理整个问题。
- 集合应该是可变的还是不可变的?
- 你传递的是不可变对象吗? (如果不是,你需要考虑以下问题:)
- 您是否将引用传递给对象,以便它们可以被更改或是否需要深层复制?
那么回答完这些问题,我们再来说说流的一个劣势:O(n)访问。
用户想要访问索引处的对象。
首先,他必须迭代所有对象以将其附加到新的数据结构中。或者他必须按顺序迭代,直到访问到这个项目。
后者仅在最坏的情况下出现,但是 - 一种新的数据结构使堆内存分配增加了一倍。而且这也会影响之后的垃圾回收。
但为什么流如此可爱?
- 因为您可以编写更具可读性的代码。 就是这样!
当客户端所做的只是消费这些项目时,那么对他来说使用流是一个很好的建议。
这样 他的 代码库更具可读性。
- 房间里有一头大象——并发。如果使用得当,引入成熟的多线程是廉价的(就开发时间而言)。
- Streams 实现了 AutoClosable 接口,这很好。
阐述第三点:
当您需要在使用后关闭资源时,您总是需要自己执行此操作。
因此 Visitor-Pattern 是更适用的选项 - 如果他想使用 stream
或 collection
,用户可以自行选择。 :-)
Imo,你应该始终坚持收集 api。
这样您就不需要熟悉流-api。
任何想使用流的人都可以自己使用。
// 编辑 2:详细说明流的混淆 - 意见
This "strong consistency requirements" seems related to more of design requirement. I would be happy to provide the bounty if the answer has details with authoritative references.
这与流与集合无关。
它是关于消费集合的时间点(无论如何都是集合)。
如果您的用户只想获取对象的当前状态,您 return 一个集合。
如果您的用户想要订阅新项目,他会在您的 api.
注册一个 Observable
这是,imo,关于流的混淆是有根源的。 https://reactiveX.io 中的库提供了类似流的接口来订阅数据源。
这张照片显示了他们 类 之一的时间线。
发生的事情很简单:
一旦您开始发出项目,调用者就会注册调用的转换方法和回调。
这是 Observer-Callback 的确切旧原则。
出于各种原因,我强烈建议不要使用 Observables。
- 各位同仁一定要熟悉
- 调试将变得更加困难,因为调用堆栈更加冗长。
- 一个人很容易陷入回调地狱。
- 应用程序高度专业化,很少使用它们。如果您连续为每个用户发送相同的项目,它们就非常适合。如果您正在进行正常的 CRUD 操作,请不要引入 Observables。
不过他们很有趣。 :-)
我会尽可能简短地解释你的句子(据我所知)。
我要解释的第一个术语是“强一致性要求”。例如银行应用程序、实时网络流量分析等都是一些关键任务应用程序,一致性(在任何意义上)是这里的第一要求,因为任何错位或丢失的部分数据都可能出现数据完整性问题。所以,会造成数据不一致。对于这些应用程序来说,这是一个非常大的问题。
第二项是“您必须生成移动目标的一致快照”。这里我们可以说“移动目标”是我们的流数据。 如果您对(实时)流数据进行机器学习,则必须对数据进行采样,以便在机器学习(或深度学习)算法中进行处理。为此,您应该在特定时间间隔(时间范围)内从数据中选取样本,然后处理大量数据,然后再处理下一个数据。此过程称为批处理(或批量)处理。在这种情况下,我们可以说我们的“快照”术语是这里的样本。然后我们应该从流中选择每个数据样本当然是“特定”时间间隔,并以某种方式确保样本(批次)中数据的完整性。
在此上下文中,“强一致性要求”的概念与代码所在的系统或应用程序相关。没有独立于系统或应用程序的“强一致性”的特定概念。这是一个“一致性”的例子,它由您可以对结果做出的断言决定。应该清楚的是,这些断言的语义完全是特定于应用程序的。
假设您有一些代码可以实现一个人们可以进出的房间。您可能希望同步相关方法,以便所有进入和离开操作都按某种顺序发生。例如:(使用 Java 16)
record Person(String name) { }
public class Room {
final Set<Person> occupants = Collections.newSetFromMap(new ConcurrentHashMap<>());
public synchronized void enter(Person p) { occupants.add(p); }
public synchronized void leave(Person p) { occupants.remove(p); }
public Stream<Person> occupants() { return occupants.stream(); }
}
(注意,我在这里使用了 ConcurrentHashMap,因为如果它在迭代期间被修改,它不会抛出 ConcurrentModificationException。)
接下来,考虑一些线程按顺序执行这些方法:
room.enter(new Person("Brett"));
room.enter(new Person("Chris"));
room.enter(new Person("Dana"));
room.leave(new Person("Dana"));
room.enter(new Person("Ashley"));
现在,大约在同一时间,假设呼叫者通过执行以下操作获得了房间内人员的列表:
List<Person> occupants1 = room.occupants().toList();
结果可能是:
[Dana, Brett, Chris, Ashley]
这怎么可能?流被延迟评估,并且元素被拉入列表,同时其他线程正在修改流的源。特别是,流有可能“看到”Dana,然后删除 Dana 并添加 Ashley,然后流前进并遇到 Ashley。
那么流代表什么?为了找到答案,我们必须深入研究 ConcurrentHashMap 在并发修改的情况下对其流的说法。该集是从CHM的keySet view, which says "The view's iterators and spliterators are weakly consistent." The definition构建的弱一致依次为:
Most concurrent Collection implementations (including most Queues) also differ from the usual java.util conventions in that their Iterators and Spliterators provide weakly consistent rather than fast-fail traversal:
- they may proceed concurrently with other operations
- they will never throw ConcurrentModificationException
- they are guaranteed to traverse elements as they existed upon construction exactly once, and may (but are not guaranteed to) reflect any modifications subsequent to construction.
这对我们的 Room 应用程序意味着什么?我会说这意味着如果一个人出现在居住者流中,那么那个人在某个时候在房间里。这是一个相当软弱的声明。请特别注意,它 而不是 允许您说 Dana 和 Ashley 同时在房间里。从列表的内容来看可能看起来是这样,但正如简单的检查所揭示的那样,这是不正确的。
现在假设我们要将 Room class 更改为 return List 而不是 Stream,而调用方将使用它:
// in class Room
public synchronized List<Person> occupants() { return List.copyOf(occupants); }
// in the caller
List<Person> occupants2 = room.occupants();
结果可能是:
[Dana, Brett, Chris]
与上一个列表相比,您可以对这个列表做出更有力的陈述。你可以说 Chris 和 Dana 同时在房间里,而在这个特定的时间点,Ashley 不在房间里。
occupants() 的 List 版本为您提供特定时间房间占用者的快照。与流版本相比,这允许您提供更强大的陈述,流版本只告诉您某些人在某个时候在房间里。
为什么你会想要一个语义较弱的 API?同样,这取决于应用程序。如果您想向使用过房间的人发送调查,您只关心他们是否曾经在房间里。你不关心其他事情,比如同一时间还有谁在房间里。
具有更强语义的API可能更昂贵。它需要制作一个集合的副本,这意味着分配 space 并花费时间进行复制。它在执行此操作时需要持有锁,以防止并发修改,这会暂时阻止其他更新的进行。
总而言之,“强”或“弱”一致性的概念在很大程度上取决于上下文。在这种情况下,我用一些相关的语义构成了一个例子,例如“同时在房间里”或“在某个时间点在房间里”。应用程序所需的语义决定了结果一致性的强弱。这反过来又推动了应该使用什么 Java 机制,例如流与集合以及何时应用锁。
我正在讨论哪种设计我们的 API 的最佳方式(Stream 与 Collection as return 类型)。 this post中的讨论很有价值。
@BrainGotez 的回答提到了集合优于流的这种情况。我不太明白这是什么意思,谁能帮忙举个例子解释一下?
"当有很强的一致性要求,并且你必须对移动目标生成一致的快照时。"
我的问题是,具体来说,“强一致性要求”和“移动目标的一致快照”在现实世界的应用程序中意味着什么?
将强一致性视为不断变化的源的时间点快照。假设你是一个电商巨头,想看一个月的销售额,你可以从数据库中return获取12月1日到12月31日所有销售记录的快照,这是一个有限集合(例如List ),尽管对于某些公司来说它可能相当大。这是一个一致的快照集合,因为现有销售可能会因取消或 return 随时间发生变化,但是 API 只是提供创建列表时销售情况的时间点快照。 在同一家公司的另一个用例中,假设数据科学团队有一个应用程序可以在销售交易发生时(移动目标)持续监控以检测欺诈行为,但它是一个没有有限边界的连续数据流此流中的每笔交易都会被提取和分析。
所以基本上,当您 return collection
时,您是 return 在那个特定时刻拍摄玩家对象的快照。也就是说,在这种情况下调用“getPlayersAsCollection”方法时玩家对象的副本。
其他线程对玩家列表的任何更改都不会反映到之前 returned 的集合中。
这解释了,the consistency is maintained
并且在调用 getPlayersAsCollection 方法时,您实际上得到了玩家列表中的内容,该列表不断通过添加新玩家详细信息或从中删除玩家详细信息来修改。这解释了 consistent snapshot of a moving target
.
class Team {
private List<Player> players = new ArrayList<>();
// ...
public List<Player> getPlayersAsCollection() {
return Collections.unmodifiableList(players);
}
public Stream<Player> getPlayersAsStream() {
return players.stream();
}
}
然而,当 stream
在这里被 returned 时,就像指向玩家列表的指针被 returned 一样。 Stream 之间对播放器对象的任何更改都由“getPlayersAsStream”方法 return 编辑,当您尝试访问流对象或对流对象执行流操作时,对播放器对象所做的更改也将在此处反映出来。
因此在这种情况下“没有强一致性”,因为从调用 getPlayersAsStream 并获得响应时到您尝试访问该响应(Stream)时数据发生了变化。
但是,returning Stream 有其自身的优势,正如问题中分享的 link 中所解释的那样。 return Stream 还是 Collection 取决于特定的用例。
我希望这有助于澄清您对 “当有很强的一致性要求时,您必须为移动目标生成一致的快照。”
“当有很强的一致性要求时,您必须生成移动目标的一致快照。”
作者@Brian Goetz指的是流被消费的时间点。
java.util.stream
-API.
当您 return 一个流时,您会得到一个对象的句柄,该对象尚未开始拉动。
只有当您调用 termination method 时,集合才会迭代。在此之前,集合及其项目可以更改。
这是关于流的唯一惰性部分。否则你可能想骑 RxJava2
的公牛.. ;- )
// 为赏金编辑:
一个真实世界的例子是:到现在为止,这些特定股票的价格是多少?
然后你想传递不可变的对象,可以用来在检查后下订单。
如果同时价格发生变化 - 但该对象需要下订单 - 您不关心用户下单需要多长时间。 价格只是事先确定的。
// 编辑结束。
无论如何,在开始迭代之前,集合可能会发生同样的情况。这两种情况都与并发访问有关。
Also, this isn't an iteration of the items per-se.
Each object is passed through the chain.
因此,恕我直言,您必须以不同的方式处理整个问题。
- 集合应该是可变的还是不可变的?
- 你传递的是不可变对象吗? (如果不是,你需要考虑以下问题:)
- 您是否将引用传递给对象,以便它们可以被更改或是否需要深层复制?
那么回答完这些问题,我们再来说说流的一个劣势:O(n)访问。 用户想要访问索引处的对象。 首先,他必须迭代所有对象以将其附加到新的数据结构中。或者他必须按顺序迭代,直到访问到这个项目。 后者仅在最坏的情况下出现,但是 - 一种新的数据结构使堆内存分配增加了一倍。而且这也会影响之后的垃圾回收。
但为什么流如此可爱?
- 因为您可以编写更具可读性的代码。 就是这样! 当客户端所做的只是消费这些项目时,那么对他来说使用流是一个很好的建议。 这样 他的 代码库更具可读性。
- 房间里有一头大象——并发。如果使用得当,引入成熟的多线程是廉价的(就开发时间而言)。
- Streams 实现了 AutoClosable 接口,这很好。
阐述第三点:
当您需要在使用后关闭资源时,您总是需要自己执行此操作。
因此 Visitor-Pattern 是更适用的选项 - 如果他想使用 stream
或 collection
,用户可以自行选择。 :-)
Imo,你应该始终坚持收集 api。 这样您就不需要熟悉流-api。 任何想使用流的人都可以自己使用。
// 编辑 2:详细说明流的混淆 - 意见
This "strong consistency requirements" seems related to more of design requirement. I would be happy to provide the bounty if the answer has details with authoritative references.
这与流与集合无关。 它是关于消费集合的时间点(无论如何都是集合)。 如果您的用户只想获取对象的当前状态,您 return 一个集合。 如果您的用户想要订阅新项目,他会在您的 api.
注册一个 Observable这是,imo,关于流的混淆是有根源的。 https://reactiveX.io 中的库提供了类似流的接口来订阅数据源。
这张照片显示了他们 类 之一的时间线。
- 各位同仁一定要熟悉
- 调试将变得更加困难,因为调用堆栈更加冗长。
- 一个人很容易陷入回调地狱。
- 应用程序高度专业化,很少使用它们。如果您连续为每个用户发送相同的项目,它们就非常适合。如果您正在进行正常的 CRUD 操作,请不要引入 Observables。
不过他们很有趣。 :-)
我会尽可能简短地解释你的句子(据我所知)。 我要解释的第一个术语是“强一致性要求”。例如银行应用程序、实时网络流量分析等都是一些关键任务应用程序,一致性(在任何意义上)是这里的第一要求,因为任何错位或丢失的部分数据都可能出现数据完整性问题。所以,会造成数据不一致。对于这些应用程序来说,这是一个非常大的问题。
第二项是“您必须生成移动目标的一致快照”。这里我们可以说“移动目标”是我们的流数据。 如果您对(实时)流数据进行机器学习,则必须对数据进行采样,以便在机器学习(或深度学习)算法中进行处理。为此,您应该在特定时间间隔(时间范围)内从数据中选取样本,然后处理大量数据,然后再处理下一个数据。此过程称为批处理(或批量)处理。在这种情况下,我们可以说我们的“快照”术语是这里的样本。然后我们应该从流中选择每个数据样本当然是“特定”时间间隔,并以某种方式确保样本(批次)中数据的完整性。
在此上下文中,“强一致性要求”的概念与代码所在的系统或应用程序相关。没有独立于系统或应用程序的“强一致性”的特定概念。这是一个“一致性”的例子,它由您可以对结果做出的断言决定。应该清楚的是,这些断言的语义完全是特定于应用程序的。
假设您有一些代码可以实现一个人们可以进出的房间。您可能希望同步相关方法,以便所有进入和离开操作都按某种顺序发生。例如:(使用 Java 16)
record Person(String name) { }
public class Room {
final Set<Person> occupants = Collections.newSetFromMap(new ConcurrentHashMap<>());
public synchronized void enter(Person p) { occupants.add(p); }
public synchronized void leave(Person p) { occupants.remove(p); }
public Stream<Person> occupants() { return occupants.stream(); }
}
(注意,我在这里使用了 ConcurrentHashMap,因为如果它在迭代期间被修改,它不会抛出 ConcurrentModificationException。)
接下来,考虑一些线程按顺序执行这些方法:
room.enter(new Person("Brett"));
room.enter(new Person("Chris"));
room.enter(new Person("Dana"));
room.leave(new Person("Dana"));
room.enter(new Person("Ashley"));
现在,大约在同一时间,假设呼叫者通过执行以下操作获得了房间内人员的列表:
List<Person> occupants1 = room.occupants().toList();
结果可能是:
[Dana, Brett, Chris, Ashley]
这怎么可能?流被延迟评估,并且元素被拉入列表,同时其他线程正在修改流的源。特别是,流有可能“看到”Dana,然后删除 Dana 并添加 Ashley,然后流前进并遇到 Ashley。
那么流代表什么?为了找到答案,我们必须深入研究 ConcurrentHashMap 在并发修改的情况下对其流的说法。该集是从CHM的keySet view, which says "The view's iterators and spliterators are weakly consistent." The definition构建的弱一致依次为:
Most concurrent Collection implementations (including most Queues) also differ from the usual java.util conventions in that their Iterators and Spliterators provide weakly consistent rather than fast-fail traversal:
- they may proceed concurrently with other operations
- they will never throw ConcurrentModificationException
- they are guaranteed to traverse elements as they existed upon construction exactly once, and may (but are not guaranteed to) reflect any modifications subsequent to construction.
这对我们的 Room 应用程序意味着什么?我会说这意味着如果一个人出现在居住者流中,那么那个人在某个时候在房间里。这是一个相当软弱的声明。请特别注意,它 而不是 允许您说 Dana 和 Ashley 同时在房间里。从列表的内容来看可能看起来是这样,但正如简单的检查所揭示的那样,这是不正确的。
现在假设我们要将 Room class 更改为 return List 而不是 Stream,而调用方将使用它:
// in class Room
public synchronized List<Person> occupants() { return List.copyOf(occupants); }
// in the caller
List<Person> occupants2 = room.occupants();
结果可能是:
[Dana, Brett, Chris]
与上一个列表相比,您可以对这个列表做出更有力的陈述。你可以说 Chris 和 Dana 同时在房间里,而在这个特定的时间点,Ashley 不在房间里。
occupants() 的 List 版本为您提供特定时间房间占用者的快照。与流版本相比,这允许您提供更强大的陈述,流版本只告诉您某些人在某个时候在房间里。
为什么你会想要一个语义较弱的 API?同样,这取决于应用程序。如果您想向使用过房间的人发送调查,您只关心他们是否曾经在房间里。你不关心其他事情,比如同一时间还有谁在房间里。
具有更强语义的API可能更昂贵。它需要制作一个集合的副本,这意味着分配 space 并花费时间进行复制。它在执行此操作时需要持有锁,以防止并发修改,这会暂时阻止其他更新的进行。
总而言之,“强”或“弱”一致性的概念在很大程度上取决于上下文。在这种情况下,我用一些相关的语义构成了一个例子,例如“同时在房间里”或“在某个时间点在房间里”。应用程序所需的语义决定了结果一致性的强弱。这反过来又推动了应该使用什么 Java 机制,例如流与集合以及何时应用锁。