【问题标题】:Removing a map entry causes object reference within Map entry optional to change删除映射条目会导致映射条目中的对象引用可选更改
【发布时间】:2020-08-25 18:52:44
【问题描述】:

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

让我进一步解释:

我有一堆想要排序的评论对象。评论列表应始终以被接受为答案的评论开头,它应该是列表中的第一个元素。 sort 方法从一个映射开始,并使用 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, and 11 的cmets。关键始终是评论的 id,评论始终是价值。标记为答案的评论的 ID 为 6。在这种情况下,将执行以下代码:

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

commentDTOEntry 使用acceptedAnswerCommentOptional 的值初始化时,它引用了ID 为6 的已接受答案。现在,当我从sortedAndLinkedCommentDTOMap 中删除该条目时,不仅删除了对已接受答案注释的引用来自sortedAndLinkedCommentDTOMap,也来自acceptedAnswerCommentOptional!但是 acceptedAnswerCommentOptional 现在开始指向 sortedAndLinkedCommentDTOMap 的下一个条目,即带有 key 11 的条目,而不是变为 null。

我不明白是什么导致了这种奇怪的行为。为什么acceptedAnswerCommentOptional 的值不直接变成null?为什么acceptedAnswerCommentOptional 在我将其从地图中删除时不能保持对已接受答案评论的引用?

您可以在使用调试模式在 intellij IDEA 中运行代码时自己看到此行为,只要调用 remove 方法,acceptedAnswerCommentOptional 旁边的 commentDTOEntry 的解释性调试标签就会从 6 -&gt; .... 翻转到 11 -&gt; ....

编辑:我根据 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> {
    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<CommentDTO> replies = new TreeSet<>(); 

    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<CommentDTO> getReplies() {
        return replies;
    }

    public void setReplies(TreeSet<CommentDTO> 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 "CommentDTO{" +
                "id=" + id +
                ", owningCommentId=" + owningCommentId +
                ", commenterId=" + commenterId +
                ", owningEntityId=" + owningEntityId +
                ", commenterName='" + commenterName + '\'' +
                ", commenterRole='" + commenterRole + '\'' +
                ", country='" + country + '\'' +
                ", thumbnailImageUrl='" + thumbnailImageUrl + '\'' +
                ", content='" + content + '\'' +
                ", commentDateVerbalized='" + commentDateVerbalized + '\'' +
                ", flagged=" + flagged +
                ", flagCount=" + flagCount +
                ", deleted=" + deleted +
                ", liked=" + liked +
                ", likeCount=" + likeCount +
                ", lastEditedOnVerbalized='" + lastEditedOnVerbalized + '\'' +
                ", acceptedAsAnswer=" + acceptedAsAnswer +
                ", rightToLeft=" + rightToLeft +
                ", replies=" + replies +
                '}';
    }
}

public class HelloWorld implements Comparable<HelloWorld> {
    

        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 "id= " + id + " acceptedAsAnswer= " + 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<CommentDTO> commentDTOSet = new HashSet<>();
            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<Long, CommentDTO> sortedAndLinkedCommentDTOMap = sortCommentsAndLinkCommentRepliesWithOwningComments(commentDTOSet);

        
        Optional<Map.Entry<Long, CommentDTO>> acceptedAnswerCommentOptional = sortedAndLinkedCommentDTOMap.entrySet().stream()
        .filter(entry -> entry.getValue().isAcceptedAsAnswer()).findFirst();
        
        if(acceptedAnswerCommentOptional.isPresent()){
            Map.Entry<Long, CommentDTO> commentDTOEntry = acceptedAnswerCommentOptional.get();
            System.out.println(commentDTOEntry.toString());
            sortedAndLinkedCommentDTOMap.remove(commentDTOEntry.getKey());
            System.out.println(commentDTOEntry.toString());

        }
     }
     
    private Map<Long, CommentDTO> sortCommentsAndLinkCommentRepliesWithOwningComments(Set<CommentDTO> commentDTOSet){
        Map<Long, CommentDTO> commentDTOMap = commentDTOSet.stream()
                .collect(Collectors.toMap(comment -> comment.getId().longValueExact(), Function.identity(), (v1,v2) -> v1, TreeMap::new));
        commentDTOSet.forEach(commentDTO -> {
            BigInteger owningCommentId = commentDTO.getOwningCommentId();
            if(owningCommentId != null){
                CommentDTO owningCommentDTO = commentDTOMap.get(owningCommentId.longValueExact());
                owningCommentDTO.getReplies().add(commentDTO);
            }
        });
        commentDTOMap.values().removeIf(commentDTO -> commentDTO.getOwningCommentId() != null); 
        return commentDTOMap;
    }

}

你可以在这里运行上面的代码:https://www.tutorialspoint.com/compile_java_online.php

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

编辑 3: 这行代码 commentDTOMap.values().removeIf(commentDTO -&gt; commentDTO.getOwningCommentId() != null); 导致观察到的行为。接受的答案(id 为 6 的commentDTO)有 2 个回复。这 2 个 cmets(id 为 7 和 8)由 CommentDTO 6“拥有”,并且也被 CommentDTO 6 中的replies 列表引用。在sortCommentsAndLinkCommentRepliesWithOwningComments() 的末尾,我删除了所有可以被视为回复的CommentDTOs另一条评论owningCommentId != null。我这样做是因为这些 cmets 现在是从拥有 cmets 的 replies 列表中引用的。如果我将它们留在原始地图中,那么这些回复将出现两次。因此我删除了它们,但这会导致意外行为。我想知道为什么会这样。

【问题讨论】:

  • 一个简单的minimal reproducible example 来证明问题会很有帮助。
  • @OleV.V.我刚刚完成了一个......我也更新了我的答案。当我尝试使用在线编译器重现问题时,我无法这样做。
  • @OleV.V.我添加了一个实际上导致问题的缺失方法。上面的代码示例现在确实重现了我的问题。你能看一下吗?
  • @WJS 我为你做了一个可重现的例子。
  • 313 行?那是你的 minimal 例子吗?你有多确定没有少于 300 行的程序可以证明同样的问题?不到100? (我是个乐观主义者。)

标签: java hashmap java-stream optional


【解决方案1】:

这是因为您使用的地图是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);   // prints 4=d
map.remove(entry.getKey());
System.out.println(entry);   // prints 5=e

【讨论】:

  • 非常有趣。
猜你喜欢
  • 2017-01-16
  • 1970-01-01
  • 2010-11-06
  • 1970-01-01
  • 2020-11-25
  • 1970-01-01
  • 2020-05-03
  • 2018-09-06
  • 1970-01-01
相关资源
最近更新 更多