Reputation: 3908
Say I have 2 nested collections, kind of a collection of multiple OneToMany relations.
For example a Post
has many Comments
, a Comment
can have many Tags
, sets. So let's just limit to the Post and Comments relation defined as follows as POJOs:
@Getter
@Setter
private static class Post {
private String title;
private Set < Comment > comments = new HashSet < > ();
public Post(String title) {
this.title = title;
}
public Post addComment(Comment comment) {
getComments().add(comment);
return this;
}
}
@Getter
@Setter
private static class Comment {
private String text;
public Comment(String text) {
this.text = text;
}
}
Let's create 2 Posts with comments:
Post post1 = new Post("post-1").
addComment(new Comment("com-1")).
addComment(new Comment("com-2"));
Post post2 = new Post("post-2").
addComment(new Comment("com-21")).
addComment(new Comment("com-22")).
addComment(new Comment("com-1")).
addComment(new Comment("com-2"));
The question is how to find a collection of comments for the above posts having the same text
value?
I tried to use retainAll
but it fails to solve that:
Set <Comment> post1Comments = new HashSet(post1.getComments());
System.out.println("post1 comments before: " + post1Comments);
post1Comments.retainAll(post2.getComments());
System.out.println("post1 comments after: " + post1Comments);
Any ideas?
Upvotes: 0
Views: 280
Reputation: 3004
retainAll()
is the standard method to compute the intersection between two collections.
However, any collection method that needs to compare elements for equivalence or for sorting, relies on equals()
and hashCode()
.
If you don't provide your own implementations of such methods, the defaults in Object
will consider two objects equivalent only if they're exactly the same instance, and every time you do new Comment()
you get a physically new instance, no matter if the comment's text is the same of some other Comment
object.
In order to do what you want, you should override the Comment
equivalence methods, using this.text
as delegates.
In general, that's tricky, cause it forces you to adopt a unique equivalence criterion, which isn't always possible, eg, if you just want to list unique comments that have been made on a post, the above is fine, if you want to know the number of times the same thing has been repeated, you need some other equivalence criteria (eg, adding the author or the timestamp).
There are alternatives to the above.
One is to make computations manually, using streams as suggested by Hulya is a good way to do it (once you've learned about streams), cause it's quick and you can easily take advantage of parallelism (though in this case, you would require Collections.synchronizedSet()
and maybe the amounts of needed synchronization wouldn't make it so fast).
Another approach is to implement delegates, ie, new CommentDelegate( originalComment ))
, where each delegate would provide its own flavour of equals()
and hashCode()
. Variants of this are possible, eg, you could have a CustomCommentKey
for each comment and populate a HashMap
to create a custom-criterion index of comments, the map always contains the intersection of any sets of comments you add to it, since it factorises by key unicity.
Another very clean option is to use collection implementations that are alternatives to those available in the standard Java, where the new implementations accept an HashingStrategy
interface, having methods like equals ( o1, o2 )
and hashCode ( o )
. This, like the existing Comparator
, allow for comparing objects of a given type against a custom criterion, which is untied from the original type (ie, from Comment
) and allows for composition. While I've never tried it, Eclipse has a library for doing that.
Upvotes: 1
Reputation: 3433
You can collect text
values of the comments of post2
to a set. Then you can use the contains()
method in a filter to find the intersection:
Set<Comment> post1Comments = post1.getComments().stream() // check every comment of post1
.filter( // filter if post2 has the comment with same text value
comment -> post2.getComments().stream() // get comments of post2
.map(Comment::getText) // extract text values
.collect(Collectors.toSet()) // collect text values of post2's comments to a set
.contains(comment.getText())) // check if the set contains the text value of the comment in the post1
.collect(Collectors.toSet()); // collect the filtered comments to a set
System.out.println(post1Comments);
Output:
[com-1, com-2]
I added a toString()
method to Comment
class to get the output above:
public class Comment {
//...
@Override
public String toString() {
return this.text;
}
}
Upvotes: 0
Reputation: 127
What you could do if you only need to check the content and don't need any ideas corresponding to a post, then you could Stream over the first list, then put a filter to the stream which transforms the second list to a string stream and filters all values that don't exist in both streams, after that map your text back to a post and you're done
List<Post> duplicates = posts1.stream()
.map(Post::getText)
.filter(posts2.stream().map(Post::getText).collect(Collectors.toSet())::contains)
.map(Post::new)
.collect(Collectors.toList());
Maybe it would be more efficient to store the stream of the second list and reference this right there, and also it could be more nicely to read
Upvotes: 0
Reputation: 1339
Just add equals and hashCode methods to Comment class. Something like this
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Comment comment = (Comment) o;
return Objects.equals(text, comment.text);
}
@Override
public int hashCode() {
return Objects.hash(text);
}
Upvotes: 1
Reputation: 21124
Set
uses equals
and hashcode
methods to compare objects. If you don't override them in your Comment
class, then it will have those methods inherited from the java.lang.Object
class. That implementation uses object identity and two objects with the same content in it will be recognized as different entities. You have to correctly override equals
and hashcode
methods in your Comment
class to solve this problem.
Upvotes: 4