当前位置: 首页 > 知识库问答 >
问题:

流与集合作为返回类型

史烈
2023-03-14

我正在讨论设计我们的API(Stream vs Collection作为返回类型)的最佳方式。这篇文章中的讨论非常有价值。

@BrainGotez的答案提到了一个条件,即集合优于流。我不太明白这意味着什么,谁能帮我举个解释的例子?

当有很强的一致性要求时,您必须生成移动目标的一致快照

我的问题是,具体而言,“强一致性要求”和“移动目标的一致快照”在现实应用中意味着什么?

共有3个答案

李兴庆
2023-03-14

所以基本上当你返回一个< code >集合时,你是在那个特定的时刻返回玩家对象的快照。也就是在本例中调用“getPlayersAsCollection”方法时的players对象的副本。其他线程对玩家列表的任何更改都不会反映到之前返回的集合中。这解释了,< code >保持了一致性,并且在调用getPlayersAsCollection方法时,您实际上获得了球员列表中的内容,该列表通过添加新球员详细信息或从中删除球员详细信息而不断修改。这就解释了< code >移动目标的一致快照。

class Team {
    private List<Player> players = new ArrayList<>();

    // ...

    public List<Player> getPlayersAsCollection() {
        return Collections.unmodifiableList(players);
    }

    public Stream<Player> getPlayersAsStream() {
        return players.stream();
    }
}

然而,当在此处返回stream时,它将类似于返回指向列表玩家的指针。Stream之间对玩家对象的任何更改都由“getPlayersAsStream”方法返回,当您尝试访问或对stream对象执行流操作时,对玩家对象所做的更改也将在此处反映。因此,在这种情况下“没有强一致性”,因为数据是从调用getPlayersAsStream并获得响应以及您尝试访问该响应(Stream)时更改的。

但同样,返回Stream有其自身的优势,因为它在问题中共享的链接中进行了解释。这取决于特定用例是返回流还是集合。

我希望这有助于并澄清您对“当有强烈的一致性要求时,您必须生成移动目标的一致快照”的疑问。

后阳炎
2023-03-14

在这种情况下,“强一致性要求”的概念是相对于代码所在的系统或应用程序而言的。没有独立于系统或应用程序的“强一致性”的特定概念。下面是一个“一致性”示例,该示例由您对结果可以做出的断言决定。应该清楚的是,这些断言的语义完全是特定于应用程序的。

假设你有一些代码实现了一个人们可以进出的房间。您可能希望同步相关的方法,以便所有的进入和离开操作以某种顺序发生。例如:(使用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,它不会抛出 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视图构建的,它表示“视图的迭代器和分裂器是弱一致的”弱一致的定义依次是:

大多数并发集合实现(包括大多数队列)也不同于通常的java.util约定,因为它们的迭代器和Spliterator提供弱一致性而不是快速失败遍历:

    < li >它们可以与其他操作同时进行 < li >它们永远不会引发concurrent modification exception < li >它们保证遍历构造时存在的元素一次,并且可以(但不保证)反映构造后的任何修改。

这对我们的房间应用程序意味着什么?我会说这意味着如果一个人出现在人群中,这个人在某个时候就在房间里。这是一个相当弱的声明。特别注意,它不允许你说Dana和Ashley同时在房间里。从列表的内容来看,似乎是这样,但简单的检查就会发现,这是不正确的。

现在假设我们要更改Room类以返回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]

你可以对这份名单做出比前一份更有力的陈述。你可以说克里斯和黛娜同时在房间里,而在这个特定的时间点,艾希礼不在房间里。

列表版本的占用者()为您提供了特定时间房间占用者的快照。这允许您比流版本更强有力的陈述,流版本只告诉您某些人在某个时候在房间里。

为什么要使用语义较弱的API?同样,这取决于应用程序。如果你想向使用房间的人发送调查,你所关心的是他们是否曾经在房间里。你不关心其他事情,比如同一时间房间里还有谁。

语义更强的API可能更昂贵。它需要制作集合的副本,这意味着分配空间和花费时间进行复制。在此过程中,它需要持有一个锁,以防止并发修改,这会暂时阻止其他更新的进行。

总之,“强”或“弱”一致性的概念高度依赖于上下文。在本例中,我用一些相关的语义构建了一个示例,例如“在同一时间在房间里”或“在某个时间点在房间里”。应用程序所需的语义决定了结果一致性的强弱。这反过来决定了应该使用哪些Java机制,例如流与集合以及何时应用锁。

杜烨伟
2023-03-14

当有很强的一致性要求时,您必须生成移动目标的一致快照

作者@Brian Goetz所指的是流被消耗的时间点。

这是对java.util.stream-API的第一个误解。

当你返回一个流时,你得到一个对象的句柄,这个对象还没有开始它的拉。

只有当您调用终止方法时,集合才会被迭代。在此之前,集合及其项目可以更改。这是关于流的唯一懒惰的部分。否则,您可能想骑上< code>RxJava2的公牛..;- )

//编辑该赏金:

一个真实的例子是:到目前为止,这些特定股票的价格是多少?

然后你想传递不可变对象,检查后可以使用它来下单。

如果在此期间价格发生了变化——但是需要该对象来下订单——您不需要关心用户下订单需要多长时间。价格是事先定好的。

//编辑结束。

无论如何,在开始迭代之前,集合也会发生同样的情况。这两种情况都与并发访问有关。

此外,这不是项目本身的迭代。
每个对象都通过链传递。

因此,恕我直言,你必须以不同的方式处理整个问题。

  • 集合应该是可变的还是不可变的?
  • 你在传递不可变对象吗?(如果没有,你需要考虑以下问题:)
  • 您是否将引用传递给对象,以便它们可以被更改还是需要深度复制?

所以在回答了这些问题之后,让我们谈谈流的一个缺点:O(n)访问。用户想要访问索引处的对象。首先,他必须迭代所有对象以将其附加到新的数据结构中。或者他必须按顺序迭代,直到访问到该项目。后者仅在最坏的情况下,但是——新的数据结构只是将堆内存分配增加了一倍。这也会影响之后的垃圾回收机制。

但是为什么溪流这么可爱呢?

  1. 因为你可以编写更具可读性的代码。就是这样!当客户端所做的只是消耗项目时,那么使用流是一个很好的建议。这样,他的代码库更具可读性。
  2. 房间里有一头大大象 - 并发。如果使用得当,引入成熟的多线程是廉价的(就开发时间而言)。
  3. Streams实现了AutoClosable-Interface,这很好。

详细说明第三点:当你需要在消费后关闭一个资源时,总是需要你自己去做。因此,访问者模式是更适用的选项,如果用户想使用< code >流或< code >集合,他可以在其中自行选择。:- )

在我看来,您应该始终坚持api的集合。这样,您不需要熟悉流api。任何想使用流的人都可以自己这样做。

//编辑2:详细说明流的混淆-OPINIONATED

这种“强一致性要求”似乎与更多的设计要求有关。如果答案有权威参考的细节,我很乐意提供赏金。

它不是关于流与集合。它是关于一个人使用集合的时间点(无论如何,两者都是集合)。如果用户只想获取对象的当前状态,则返回一个集合。如果你的用户想要订阅新项目,他会在你的 api 上注册一个 Observable。

在我看来,这就是关于溪流的困惑的根源。https://react vex . io中的库提供了一个类似流的接口来订阅数据源。

  1. 所有同事都必须熟悉它们
  2. 调试将变得更加困难,因为调用堆栈更加冗长。
  3. 一个人很容易陷入回调地狱。
  4. 应用程序是高度专业化的,很少使用它们。如果您连续为每个用户发出相同的项目,则它们非常适合。如果您正在执行正常的 CRUD 操作,请不要引入可观察量。

但它们很有趣。:-)

 类似资料:
  • 假设我有一个方法将只读视图返回到成员列表中: 进一步假设客户机所做的只是立即对列表进行一次迭代。也许是为了把玩家放进一个JList或者别的什么。客户端没有存储对列表的引用以供以后检查! 对于这种常见的场景,我是否应该返回一个流呢? 还是返回流在Java中不是惯用的?流被设计成总是在创建它们的同一个表达式中“终止”吗?

  • 我有一个服务层,其中有许多方法返回(通常是很多)对象的集合。这些集合仅用于迭代(最终并行)。 使用的最佳/常用返回类型是什么? <代码>拆分器

  • 问题内容: 假设我有一个将只读视图返回到成员列表的方法: 进一步假设所有客户要做的就是立即遍历列表一次。也许将播放器放入JList之类。客户端就不能存储到列表的引用以便稍后进行检查! 在这种常见情况下,我应该返回流吗? 还是在Java中返回流非惯用语?流是否设计为始终在创建它们的相同表达式内被“终止”? 问题答案: 答案是一如既往的“取决于”。这取决于返回的集合的大小。这取决于结果是否随时间变化,

  • spop key 如果set是空或者key不存在返回nil

  • 我有一个任务,我们要创造一个石头,纸,剪刀的游戏。它指定我们必须创建一个抽象的“工具”类,其中有三个子类:“ToolRock”、“ToolPaper”、“ToolScissors”。抽象类应该有一个“+getFeagnet():tool”函数(用斜体写成)。 我的假设是做一个像这样的抽象函数: RockTool类被指定为具有函数“+get弱点():tool”(不是用斜体写的),我的想法是创建一个覆