删除地图条目会导致地图条目中的对象引用可选更改

Mau*_*ice 1 java hashmap optional java-stream

当我从地图中检索地图条目时,将其存储在一个可选中,然后使用 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();
Run Code Online (Sandbox Code Playgroud)

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

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

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

我不明白是什么导致了这种奇怪的行为。为什么acceptedAnswerCommentOptional不简单地成为价值nullacceptedAnswerCommentOptional当我从地图中删除它时,为什么无法保留对已接受答案评论的引用?

使用调试模式在 intellij IDEA 中运行代码时,您可以自己看到这种行为,只要remove调用该方法,注释 DTOEntry 旁边的解释性调试标签将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> {
    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;
    }

}
Run Code Online (Sandbox Code Playgroud)

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

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

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

Lor*_*uro 6

发生这种情况是因为您使用的地图是TreeMap.

ATreeMap被实现为红黑树,它是一种自平衡二叉树。

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

如果删除一个条目,则树必须重新平衡自身,并且可能会发生该条目随后用于指向取代其位置的节点的情况。

由于TreeMap.entrySet()由地图支持,因此更改会反映在集合中。

更改还取决于您要删除的节点,例如,如果它是叶子,那么它可能只是与树断开链接,而条目不受影响。

如果您使用其他地图实现,例如 anHashMap那么您将不会得到这种行为。

顺便说一下,这是一个更简单的例子,它甚至不涉及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
Run Code Online (Sandbox Code Playgroud)

  • 很有意思。 (3认同)