java - 删除 map 条目会导致 map 条目中的对象引用可选更改

标签 java hashmap java-stream option-type

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

让我进一步解释:

我有一堆要排序的评论对象。评论列表应始终以被接受为答案的评论开头,它应该是列表中的第一个元素。 sort 方法以映射开始,并使用 entrySet 上的流来检索第一个将 acceptedAnswer boolean 值设置为 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());
}

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

我不明白是什么导致了这种奇怪的行为。为什么 acceptedAnswerCommentOptional 的值不直接变为 null?为什么 acceptedAnswerCommentOptional 无法在我从 map 中删除时保留对已接受答案评论的引用?

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

}

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

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

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

最佳答案

发生这种情况是因为您使用的 map 是 TreeMap

一个TreeMap被实现为一个红黑树,它是一个自平衡的二叉树。

map 的条目用作树的节点。

如果您删除一个条目,那么树必须重新平衡自身,然后可能会发生该条目用于指向取代它的节点的情况。

TreeMap.entrySet()由 map 支持,变化反射(reflect)在集合中。

更改还取决于您要删除的节点,例如,如果它是一个叶子,那么它可能只是从树上取消链接并且条目不受影响。

如果您使用另一个映射实现,例如 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

关于java - 删除 map 条目会导致 map 条目中的对象引用可选更改,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63585372/

相关文章:

java - 为什么 Iterable<T> 不提供 stream() 和 parallelStream() 方法?

java - android 中的 Jsoup java.io.EOFException

java - 获取特定状态下 hashmap 中新添加、删除、修改的值

java - 创建arraylist HashMap 的最佳方法

java - 如何拆分字符串流并生成字符串数组列表?

java - Stream.reduce() 和 Stream.collect() 之间令人惊讶的性能差异

java - Docx4j 字符串中的换行符

java - 子类 OOP 的限制

java - 如何将一个java类分成多个类?

java - 在Hashmap中搜索字符串数组的有效方法