删除地图条目会导致地图条目中的对象引用可选更改
Removing a map entry causes object reference within Map entry optional to change
当我从地图中检索地图条目时,将其存储在一个可选项中,然后使用 remove(entry.getKey()) 从地图中删除相同的条目然后 Optional
突然开始指向到地图内可用的下一个地图条目。
让我进一步解释:
我有一堆要排序的评论对象。评论列表应始终以被接受为答案的评论开头,它应该是列表中的第一个元素。 sort 方法以 map 开始,并使用 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
的评论。键始终是评论的 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
方法被调用 [=20 旁边的 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 'owned' 并且也被 CommentDTO 6 中的 replies
列表引用。在 sortCommentsAndLinkCommentRepliesWithOwningComments()
的末尾,我删除了所有 CommentDTOs
可以被视为对另一条评论的回复 owningCommentId != null
。我这样做是因为这些评论现在是从拥有评论的 replies
列表中引用的。如果我将它们留在原始地图中,那么这些回复将出现两次。因此我删除了它们,但这会导致意外行为。我想知道为什么会这样。
发生这种情况是因为您使用的地图是 TreeMap
。
一个TreeMap
实现为一棵red-black树,也就是一棵self-balancing二叉树。
地图的条目用作树的节点。
如果您删除一个条目,那么树必须 re-balance 本身,然后可能会发生该条目用于指向取代它的节点的情况。
由于 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
当我从地图中检索地图条目时,将其存储在一个可选项中,然后使用 remove(entry.getKey()) 从地图中删除相同的条目然后 Optional
突然开始指向到地图内可用的下一个地图条目。
让我进一步解释:
我有一堆要排序的评论对象。评论列表应始终以被接受为答案的评论开头,它应该是列表中的第一个元素。 sort 方法以 map 开始,并使用 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
的评论。键始终是评论的 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
.
我不明白是什么导致了这种奇怪的行为。为什么 acceptedAnswerCommentOptional
的值不直接变成 null
?为什么当我从地图中删除已接受的答案评论时,acceptedAnswerCommentOptional
无法保留对它的引用?
当 运行 使用调试模式在 intellij IDEA 中调用代码时,您可以自己看到这种行为,一旦 remove
方法被调用 [=20 旁边的 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 'owned' 并且也被 CommentDTO 6 中的 replies
列表引用。在 sortCommentsAndLinkCommentRepliesWithOwningComments()
的末尾,我删除了所有 CommentDTOs
可以被视为对另一条评论的回复 owningCommentId != null
。我这样做是因为这些评论现在是从拥有评论的 replies
列表中引用的。如果我将它们留在原始地图中,那么这些回复将出现两次。因此我删除了它们,但这会导致意外行为。我想知道为什么会这样。
发生这种情况是因为您使用的地图是 TreeMap
。
一个TreeMap
实现为一棵red-black树,也就是一棵self-balancing二叉树。
地图的条目用作树的节点。
如果您删除一个条目,那么树必须 re-balance 本身,然后可能会发生该条目用于指向取代它的节点的情况。
由于 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