移除地图条目会导致地图条目内部的对象引用可选项发生更改

huangapple go评论89阅读模式
英文:

Removing a map entry causes object reference within Map entry optional to change

问题

当我从地图中检索一个地图条目,将其存储在可选项中,然后使用remove(entry.getKey())从地图中删除相同的条目,那么Optional突然开始指向地图中可用的下一个地图条目。

让我进一步解释:

我有一堆评论对象,我想对它们进行排序。评论列表应始终以被接受为答案的评论开头,它应该是列表中的第一个元素。排序方法从一个地图开始,使用entrySet上的流来检索第一个评论,其中acceptedAnswer布尔值设置为true

Map<Long, CommentDTO> sortedAndLinkedCommentDTOMap = sortCommentsAndLinkCommentRepliesWithOwningComments(commentDTOSet);
Optional<Map.Entry<Long, CommentDTO>> acceptedAnswerCommentOptional = sortedAndLinkedCommentDTOMap.entrySet().stream()
        .filter(entry -> entry.getValue().isAcceptedAsAnswer()).findFirst();

假设Map包含3个具有id 3、6和11的评论。键始终是评论的id,评论始终是值。标记为答案的评论的id为6。在这种情况下,执行以下代码:

if(acceptedAnswerCommentOptional.isPresent()){
    Map.Entry<Long, CommentDTO> commentDTOEntry = acceptedAnswerCommentOptional.get();
    sortedAndLinkedCommentDTOMap.remove(commentDTOEntry.getKey());
}

commentDTOEntryacceptedAnswerCommentOptional的值初始化时,它引用了id为6的被接受答案。现在,当我从sortedAndLinkedCommentDTOMap中删除该条目时,对被接受答案评论的引用不仅从sortedAndLinkedCommentDTOMap中删除,还从acceptedAnswerCommentOptional中删除!但是,acceptedAnswerCommentOptional的值为什么不简单地变为null?为什么acceptedAnswerCommentOptional在我从地图中删除它时不能保持对被接受答案评论的引用?

当您在IntelliJ IDEA中使用调试模式运行代码时,您可以自己看到这种行为,一旦调用remove方法,位于acceptedAnswerCommentOptional旁边的解释性调试标签从6 -> ....切换到11 -> ....

编辑:我根据WJS的要求制作了一个可重现的示例。这是代码:

import java.util.*;
import java.math.BigInteger;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.function.Function;

class CommentDTO implements Comparable<CommentDTO> {
    // ... (略去了类的定义)

public class HelloWorld implements Comparable<HelloWorld> {
    // ... (略去了类的定义)

public class Main {
    public static void main(String[] args) {
        HelloWorld helloWorld = new HelloWorld();
        helloWorld.doTest();
    }
}

您可以在此处运行上述代码:https://www.tutorialspoint.com/compile_java_online.php

编辑2:示例代码现在能够重现我的问题。

编辑3:
这行代码

commentDTOMap.values().removeIf(commentDTO -> commentDTO.getOwningCommentId() != null);

导致了观察到的行为。被接受的答案(id为6的评论DTO)有两个回复。这两个评论(id为7和8)是由评论DTO 6“拥有”的,并且还被引用于评论DTO 6内的replies列表中。在sortCommentsAndLinkCommentRepliesWithOwningComments()的末尾,我删除了所有可以被视为其他评论的回复的CommentDTO,其中owningCommentId != null。我这样做是因为这些评论现在从所拥有的评论的replies列表中引用。如果我将它们保留在原始地图中,那么这些回复将出现两次。因此,我将它们删除,但这会导致意外的行为。我想知道为什么会出现这种情况。

英文:

When i retrieve a map entry from a map, store it in an optional and then remove that same entry from the map using remove(entry.getKey()) then the Optional suddenly starts pointing to the next map entry available inside the map.

Let me explain further:

I have a bunch of comment objects that i would like to sort. The comment list should always start with the comment that is accepted as the answer, it should be the first element in the list. The sort method starts with a map and uses a stream on entrySet to retrieve the first comment which has the acceptedAnswer boolean set to true.

Map&lt;Long, CommentDTO&gt; sortedAndLinkedCommentDTOMap = sortCommentsAndLinkCommentRepliesWithOwningComments(commentDTOSet);
Optional&lt;Map.Entry&lt;Long, CommentDTO&gt;&gt; acceptedAnswerCommentOptional = sortedAndLinkedCommentDTOMap.entrySet().stream()
        .filter(entry -&gt; entry.getValue().isAcceptedAsAnswer()).findFirst();

Lets assume that the Map contains 3 comments with ids 3, 6, and 11. The key is always the id of the comment and the comment is always the value. The comment marked as the answer has id 6. In this case the code below is executed:

if(acceptedAnswerCommentOptional.isPresent()){
    Map.Entry&lt;Long, CommentDTO&gt; commentDTOEntry = acceptedAnswerCommentOptional.get();
    sortedAndLinkedCommentDTOMap.remove(commentDTOEntry.getKey());
}

When commentDTOEntry gets initialized with the value of acceptedAnswerCommentOptional it has a reference to the accepted answer with id 6. Now, when i remove that entry from sortedAndLinkedCommentDTOMap the reference to the accepted answer comment is not only removed from sortedAndLinkedCommentDTOMap but also from acceptedAnswerCommentOptional! But instead of becoming null acceptedAnswerCommentOptional now starts pointing to the next entry of sortedAndLinkedCommentDTOMap namely the one with key 11.

I don't understand what is causing this strange behavior. Why doesn't the value of acceptedAnswerCommentOptional simply become null? And why isn't acceptedAnswerCommentOptional able to maintain the reference to the accepted answer comment when i remove it from the map?

You can see this behavior yourself when running the code in intellij IDEA using debug mode, as soon as the remove method is called the explanatory debug label for commentDTOEntry next to acceptedAnswerCommentOptional flips from 6 -&gt; .... to 11 -&gt; ....

EDIT: I've made a reproducible example according to WJSs wishes. This is the code:

import java.util.*;
import java.math.BigInteger;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.function.Function;
class CommentDTO implements Comparable&lt;CommentDTO&gt; {
private BigInteger id;
private BigInteger owningCommentId;
private BigInteger commenterId;
private Long owningEntityId; 
private String commenterName;
private String commenterRole;
private String country;
private String thumbnailImageUrl;
private String content;
private String commentDateVerbalized;
private boolean flagged;
private Integer flagCount;
private boolean deleted; 
private boolean liked;
private Integer likeCount;
private String lastEditedOnVerbalized;
private boolean acceptedAsAnswer;
private boolean rightToLeft;
private TreeSet&lt;CommentDTO&gt; replies = new TreeSet&lt;&gt;(); 
public CommentDTO() {
}
public CommentDTO(boolean acceptedAsAnswer, BigInteger id){
this.acceptedAsAnswer = acceptedAsAnswer;
this.id = id;
}
public CommentDTO(boolean acceptedAsAnswer, BigInteger id, BigInteger owningCommentId){
this.acceptedAsAnswer = acceptedAsAnswer;
this.id = id;
this.owningCommentId = owningCommentId;
}
public BigInteger getId() {
return id;
}
public void setId(BigInteger id) {
this.id = id;
}
public BigInteger getOwningCommentId() {
return owningCommentId;
}
public void setOwningCommentId(BigInteger owningCommentId) {
this.owningCommentId = owningCommentId;
}
public BigInteger getCommenterId() {
return commenterId;
}
public void setCommenterId(BigInteger commenterId) {
this.commenterId = commenterId;
}
public Long getOwningEntityId() {
return owningEntityId;
}
public void setOwningEntityId(Long owningEntityId) {
this.owningEntityId = owningEntityId;
}
public String getCommenterName() {
return commenterName;
}
public void setCommenterName(String commenterName) {
this.commenterName = commenterName;
}
public String getCommenterRole() {
return commenterRole;
}
public void setCommenterRole(String commenterRole) {
this.commenterRole = commenterRole;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getCommentDateVerbalized() {
return commentDateVerbalized;
}
public void setCommentDateVerbalized(String commentDateVerbalized) {
this.commentDateVerbalized = commentDateVerbalized;
}
public boolean isFlagged() {
return flagged;
}
public void setFlagged(boolean flagged) {
this.flagged = flagged;
}
public Integer getFlagCount() {
return flagCount;
}
public void setFlagCount(Integer flagCount) {
this.flagCount = flagCount;
}
public boolean isDeleted() {
return deleted;
}
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
public boolean isLiked() {
return liked;
}
public void setLiked(boolean liked) {
this.liked = liked;
}
public Integer getLikeCount() {
return likeCount;
}
public void setLikeCount(Integer likeCount) {
this.likeCount = likeCount;
}
public TreeSet&lt;CommentDTO&gt; getReplies() {
return replies;
}
public void setReplies(TreeSet&lt;CommentDTO&gt; replies) {
this.replies = replies;
}
public String getLastEditedOnVerbalized() {
return lastEditedOnVerbalized;
}
public void setLastEditedOnVerbalized(String lastEditedOnVerbalized) {
this.lastEditedOnVerbalized = lastEditedOnVerbalized;
}
public String getThumbnailImageUrl() {
return thumbnailImageUrl;
}
public void setThumbnailImageUrl(String thumbnailImageUrl) {
this.thumbnailImageUrl = thumbnailImageUrl;
}
public boolean isAcceptedAsAnswer() {
return acceptedAsAnswer;
}
public void setAcceptedAsAnswer(boolean acceptedAsAnswer) {
this.acceptedAsAnswer = acceptedAsAnswer;
}
public boolean isRightToLeft() {
return rightToLeft;
}
public void setRightToLeft(boolean rightToLeft) {
this.rightToLeft = rightToLeft;
}
@Override
public int compareTo(CommentDTO o) {
return this.id.compareTo(o.id);
}
@Override
public String toString() {
return &quot;CommentDTO{&quot; +
&quot;id=&quot; + id +
&quot;, owningCommentId=&quot; + owningCommentId +
&quot;, commenterId=&quot; + commenterId +
&quot;, owningEntityId=&quot; + owningEntityId +
&quot;, commenterName=&#39;&quot; + commenterName + &#39;\&#39;&#39; +
&quot;, commenterRole=&#39;&quot; + commenterRole + &#39;\&#39;&#39; +
&quot;, country=&#39;&quot; + country + &#39;\&#39;&#39; +
&quot;, thumbnailImageUrl=&#39;&quot; + thumbnailImageUrl + &#39;\&#39;&#39; +
&quot;, content=&#39;&quot; + content + &#39;\&#39;&#39; +
&quot;, commentDateVerbalized=&#39;&quot; + commentDateVerbalized + &#39;\&#39;&#39; +
&quot;, flagged=&quot; + flagged +
&quot;, flagCount=&quot; + flagCount +
&quot;, deleted=&quot; + deleted +
&quot;, liked=&quot; + liked +
&quot;, likeCount=&quot; + likeCount +
&quot;, lastEditedOnVerbalized=&#39;&quot; + lastEditedOnVerbalized + &#39;\&#39;&#39; +
&quot;, acceptedAsAnswer=&quot; + acceptedAsAnswer +
&quot;, rightToLeft=&quot; + rightToLeft +
&quot;, replies=&quot; + replies +
&#39;}&#39;;
}
}
public class HelloWorld implements Comparable&lt;HelloWorld&gt; {
private Long id;
private boolean acceptedAsAnswer;
public HelloWorld(){}
public HelloWorld(boolean acceptedAsAnswer, Long id){
this.acceptedAsAnswer = acceptedAsAnswer;
this.id = id;
}
@Override
public String toString() {
return &quot;id= &quot; + id + &quot; acceptedAsAnswer= &quot; + acceptedAsAnswer;   
}
public boolean isAcceptedAsAnswer(){
return acceptedAsAnswer;
}
public long getId(){
return id;
}
public static void main(String []args){
HelloWorld helloWorld = new HelloWorld();
helloWorld.doTest();
}
@Override
public int compareTo(HelloWorld o) {
return this.id.compareTo(o.id);
}
public void doTest(){
Set&lt;CommentDTO&gt; commentDTOSet = new HashSet&lt;&gt;();
commentDTOSet.add( new CommentDTO(false, BigInteger.valueOf(3)));
commentDTOSet.add( new CommentDTO(true, BigInteger.valueOf(6)));
commentDTOSet.add( new CommentDTO(false, BigInteger.valueOf(11)));
commentDTOSet.add( new CommentDTO(true, BigInteger.valueOf(7), BigInteger.valueOf(6)));
commentDTOSet.add( new CommentDTO(true, BigInteger.valueOf(8), BigInteger.valueOf(6)));
Map&lt;Long, CommentDTO&gt; sortedAndLinkedCommentDTOMap = sortCommentsAndLinkCommentRepliesWithOwningComments(commentDTOSet);
Optional&lt;Map.Entry&lt;Long, CommentDTO&gt;&gt; acceptedAnswerCommentOptional = sortedAndLinkedCommentDTOMap.entrySet().stream()
.filter(entry -&gt; entry.getValue().isAcceptedAsAnswer()).findFirst();
if(acceptedAnswerCommentOptional.isPresent()){
Map.Entry&lt;Long, CommentDTO&gt; commentDTOEntry = acceptedAnswerCommentOptional.get();
System.out.println(commentDTOEntry.toString());
sortedAndLinkedCommentDTOMap.remove(commentDTOEntry.getKey());
System.out.println(commentDTOEntry.toString());
}
}
private Map&lt;Long, CommentDTO&gt; sortCommentsAndLinkCommentRepliesWithOwningComments(Set&lt;CommentDTO&gt; commentDTOSet){
Map&lt;Long, CommentDTO&gt; commentDTOMap = commentDTOSet.stream()
.collect(Collectors.toMap(comment -&gt; comment.getId().longValueExact(), Function.identity(), (v1,v2) -&gt; v1, TreeMap::new));
commentDTOSet.forEach(commentDTO -&gt; {
BigInteger owningCommentId = commentDTO.getOwningCommentId();
if(owningCommentId != null){
CommentDTO owningCommentDTO = commentDTOMap.get(owningCommentId.longValueExact());
owningCommentDTO.getReplies().add(commentDTO);
}
});
commentDTOMap.values().removeIf(commentDTO -&gt; commentDTO.getOwningCommentId() != null); 
return commentDTOMap;
}
}

You can run the code above here: https://www.tutorialspoint.com/compile_java_online.php

EDIT 2: example code reproduces my problem now.

EDIT 3:
This line of code
commentDTOMap.values().removeIf(commentDTO -&gt; commentDTO.getOwningCommentId() != null);
is causing the observed behavior. The accepted answer (commentDTO with id 6) has 2 replies to it. Those 2 comments (with id 7 and 8) are 'owned' by CommentDTO 6 and are also referenced by the replies list inside CommentDTO 6. At the end of sortCommentsAndLinkCommentRepliesWithOwningComments() I remove all CommentDTOs that can be considered replies to another comment owningCommentId != null. I do this because these comments are now referenced from within the replies lists of the owning comments. If i were to leave them in the original map then those replies will appear twice. Therefore i remove them but this is causing unexpected behavior. I would like to know why this is so.

答案1

得分: 6

因为你使用的是TreeMap,所以会发生这种情况。

TreeMap实际上是一个红黑树,它是一种自平衡的二叉树。

地图的条目被用作树的节点。

如果你移除一个条目,那么树就必须重新平衡,可能会出现条目被用来指向取代它的节点的情况。

由于TreeMap.entrySet()是由地图支持的,所做的更改会反映在集合中。

更改还取决于你正在删除哪个节点,例如,如果它是一个叶节点,那么它可能会被从树中取消链接,条目则不受影响。

如果你使用另一个地图实现,比如HashMap,你就不会得到这种行为。

顺便说一下,这里有一个更简单的示例,甚至不涉及Optional或自定义类:

Map<Long, String> map = new TreeMap<>();
map.put(1L, "a");
map.put(2L, "b");
map.put(3L, "c");
map.put(4L, "d");
map.put(5L, "e");
map.put(6L, "f");

Map.Entry<Long, String> entry = map.entrySet().stream()
        .filter(e -> e.getKey().equals(4L))
        .findFirst()
        .get();

System.out.println(entry);   // 输出 4=d
map.remove(entry.getKey());
System.out.println(entry);   // 输出 5=e
英文:

That happens because the map you are using is a TreeMap.

A TreeMap is implemented as a red-black tree, which is a self-balancing binary tree.

The entries of the map are used as the nodes of the tree.

If you remove one entry then the tree has to re-balance itself and it might happen that the entry is then used to point to the node that takes its place.

Since TreeMap.entrySet() is backed by the map, the changes are reflected in the set.

The changes depends also on which node you are removing, for example if it's a leaf then it might just be unlinked from the tree and the entry is left unaffected.

If you use another map implementation like an HashMap then you won't get this behavior.

By the way, here's a simpler example, which doesn't even involve Optional or custom classes:

Map&lt;Long, String&gt; map = new TreeMap&lt;&gt;();
map.put(1L, &quot;a&quot;);
map.put(2L, &quot;b&quot;);
map.put(3L, &quot;c&quot;);
map.put(4L, &quot;d&quot;);
map.put(5L, &quot;e&quot;);
map.put(6L, &quot;f&quot;);
Map.Entry&lt;Long, String&gt; entry = map.entrySet().stream()
.filter(e -&gt; e.getKey().equals(4L))
.findFirst()
.get();
System.out.println(entry);   // prints 4=d
map.remove(entry.getKey());
System.out.println(entry);   // prints 5=e

huangapple
  • 本文由 发表于 2020年8月26日 02:52:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/63585372.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定