Is It Possible in Java Make Something Like Comparator But for Implementing Custom Equals() and Hashcode()

Is it possible in java make something like Comparator but for implementing custom equals() and hashCode()

Yes it is possible to do such a thing. (And people have done it.) But it won't allow you to put your objects into a HashMap, HashSet, etc. That is because the standard collection classes expect the key objects themselves to provide the equals and hashCode methods. (That is the way they are designed to work ...)

Alternatives:

  1. Implement a wrapper class that holds an instance of the real class, and provides its own implementation of equals and hashCode.

  2. Implement your own hashtable-based classes which can use a "hashable" object to provide equals and hashcode functionality.

  3. Bite the bullet and implement equals and hashCode overrides on the relevant classes.

In fact, the 3rd option is probably the best, because your codebase most likely needs to to be using a consistent notion of what it means for these objects to be equal. There are other things that suggest that your code needs an overhaul. For instance, the fact that it is currently using an array of objects instead of a Set implementation to represent what is apparently supposed to be a set.

On the other hand, maybe there was/is some real (or imagined) performance reason for the current implementation; e.g. reduction of memory usage. In that case, you should probably write a bunch of helper methods for doing operations like concatenating 2 sets represented as arrays.

Aggregating similar objects using comparator without consistency with equals()

Considering that "similarity" is not a transitive operation: if a is similar to b, and b is similar to c, then a may not be similar to c, I would suggest not to use Comparable/Comparator here, because its contract implies transitivity.

A custom interface that suits your needs should be a good option:

interface SimilarityComparable<T, D> {
// D - object that represents similarity level
D getSimilarityLevel(T other);
}

However with this approach you won't be able to define multiple groups of similarity for the same type, because of Java generics:

class Location implements SimilarityComparable<Location, Distance>, SimilarityComparable<Location, NameDifference> {
// won't compile - can't use two generic interfaces of the same type simultaneously
}

In this case you can fall-back to comparators:

interface SimilarityComparator<T, D> {
D getSimilarityLevel(T a, T b);
}

class LocationDistanceSimilarityComparator implements SimilarityComparator<Location, Distance> {
...
}

class LocationNameSimilarityComparator implements SimilarityComparator<Location, NameDifference> {
...
}

Compare two sets of a class without equals

Give a try to AssertJ, it provides a way to compare element field by field recursively, example:

// the Dude class does not override equals
Dude jon = new Dude("Jon", 1.2);
Dude sam = new Dude("Sam", 1.3);
jon.friend = sam;
sam.friend = jon;

Dude jonClone = new Dude("Jon", 1.2);
Dude samClone = new Dude("Sam", 1.3);
jonClone.friend = samClone;
samClone.friend = jonClone;

assertThat(asList(jon, sam)).usingRecursiveFieldByFieldElementComparator()
.contains(jonClone, samClone);

Another possibility is to compare collections using a specific element comparator so SomethingComparator in your case, this requires a bit more work than the first option but you have full control of the comparison.

Hope it helps!

Do my equals and hashcode must be implemented based on compareTo method?

If you want to use this class as a key for maps you need to implement equals() and hashcode(), it is not an option.

compareTo() is not used by the map at all. It's only purpose is to order elements, a map is unordered.

To use this class as a key you need to make sure that two equals objects considered have the same hashCode. Make sure that you use the same fields in the construction of your equality and of your hash code.

One last note about your class : This count variable is scaring me a bit. To use a class as a key for a map you need to be sure none of the fields that are taken into consideration to compute the hashCode can change. If you key's fields change between the moment you put it in your map and the moment you want to fetch it back then the hashCode will have changed which is a problem since a Map rely on the hashCode to internally find your value.

Can adding equals() and hashCode() method spoil something

Can I be totally sure that it won't break anything?

No. You're changing the meaning of equality from reference identity to a sort of value equality. You'll break anything relying on the current behaviour. For example, here's some valid code:

Person person1 = new Person("Foo", 100);
Person person2 = new Person("Foo", 100);
// This is fine. Two distinct Person objects will never be equal...
if (person1.equals(person2)) {
launchNuclearMissiles();
}

Your proposed change would break that.

Do you actually have code like that? It's hard to tell.

More likely, if you want to change hashCode to include hashes from the List<Friend>, you could very easily break code unless the type is actually immutable. For example:

Map<Person, String> map = new HashMap<>();
Person person = new Person("Foo", 100);
map.put(person, "Some value");

// If this changes the result of hashCode()...
person.addFriend(new Friend("Bar"));
// ... then you may not be able to find even the same object in the map.
System.out.println(map.get(person));

Fundamentally, you need to be aware of what other code uses Person, so you know what it's relying on. If Person is immutable, that makes life much simpler because you don't need to worry about the second kind of problem. (Overriding equals() and hashCode() for mutable types is a fundamentally treacherous business.)

How to compare two Sets of objects Not overriding equals/hashCode with Comparator and Streams

A Fix for Comparator

In order to create a Comparator in this case you need to provide generic type information explicitly, like that: <Book, String>comparing(), where Book and String are the parameter type and return type of the Function that Comparator.comparing() expects as a parameter.

Without an explicit declaration, the compiler has not enough data to determine the type of the variable book, and inside both comparing() and thenComparing() its type will be inferred as Object.

Comparator<Book> comparingBooks = 
Comparator.<Book, String>comparing(book -> book.getName())
.thenComparing(book -> book.getType());

If the case if only one of the static methods was used, the type of the book variable will be correctly inferred by the compiler as Book based on the type of the locale variable Comparator<Book> comparingBooks.

Both method lambda expressions could be replaced with method references:

Comparator<Book> comparingBooks =
Comparator.<Book, String>comparing(Book::getName)
.thenComparing(Book::getType);

for information on the syntax of generic methods, take a look at this tutorial

for more information on how to build comparators with Java 8 methods take a look at this tutorial

Stream

should validate any difference of Name and Type by returning true, if both Sets have exactly the same objects, it should return false.

Stream s1.stream().anyMatch(a -> s2.stream().anyMatch(b -> ...)) is currently checking whether there's an element in the first set s1 that is different from ANY of the elements in s2. I.e. the nested anyMatch() will return true for the first element encountered in s1 that has not matching element in s2. After the comparison of Book("book 1", BookType.ONE) (s1) with Book("book 2", BookType.TWO) (s2) anyMatch() terminates by returning true. And enclosing anyMatch() operation propagates this result.

Precisely the same will happen with the second example. The same pair of elements differ, although you've changed a type, names are not equal. Result is true.

Note:

  • that by convention classes are usually named with singular nouns, i.e. Book (not Books), Event, Person, etc.
  • as a rule of thumb, if your objects are intended to be used with collection they must implement equals/hashCode contract, your requirement is very unusual.

HashSet using custom hashCode

Use Comparator with TreeSet

As commented by Johannes Kuhn, you can get your desired behavior by using a NavigableSet (or SortedSet). No need to invent your own class.

Implementations of NavigableSet such as TreeSet may offer a constructor taking a Comparator object. That Comparator is used for sorting the elements of the set.

To our point here in this Question, that Comparator is also used in deciding to admitting new distinct elements rather than using the elements’ own Object#equals method.

And since there is no hashing involved in a TreeSet, there is no concern about overriding hashCode.

We can easily define our Comparator implementation. For convenience, we can call Comparator.comparing to make a comparator implementation. We define the comparator by passing a method reference for the getter method of your desired name field: User :: name.

You can add more criteria to your comparator by calling thenComparing. I leave that as an exercise for the reader.

For brevity, let's define your User class as a record. We simply declare the type and name of member fields. The compiler implicitly creates the constructor, getters, equals & hashCode, and toString.

record User( String name , String email , int age ) { }

Make some sample data.

List < User > listOfUsers =
List.of(
new User( "Bob" , "bob@x.com" , 7 ) ,
new User( "Alice" , "alice@x.com" , 42 ) ,
new User( "Carol" , "carol@x.com" , 77 )
);

Define our set, a TreeSet.

NavigableSet < User > setOfUsers = new TreeSet <>( Comparator.comparing( User :: name )  );

Populate our set with 3 elements. Verify 3 elements by dumping to console.

setOfUsers.addAll( listOfUsers );
System.out.println( setOfUsers.size() + " elements in setOfUsers = " + setOfUsers );

Now we try to add another user with the same name but different values in the other fields.

setOfUsers.add( new User( "Alice" , "a@aol.com" , -666  ) );

By default, a record decides on equality by comparing each and every member field. So:

  • If we have failed in our goal of using only name for comparison, we would get 4 elements in this set.
  • If we have succeeded in using only name, then we should get 3 elements after having blocked admission of this interloper.

Dump to console.

System.out.println( setOfUsers.size() + " elements in setOfUsers = " + setOfUsers );

3 elements in setOfUsers = [User[name=Alice, email=alice@x.com, age=42], User[name=Bob, email=bob@x.com, age=7], User[name=Carol, email=carol@x.com, age=77]]

3 elements in setOfUsers = [User[name=Alice, email=alice@x.com, age=42], User[name=Bob, email=bob@x.com, age=7], User[name=Carol, email=carol@x.com, age=77]]

We see in those results (a) sorting of the elements by name, and (b) Blocking of the second Alice, with the original Alice remaining.

To see the alternate behavior, replace the setOfUsers definition with this:

Set < User > setOfUsers = new HashSet <>();

Running that edition of the code results in setOfUsers.size() being:

3 elements in setOfUsers = [User[name=Bob, email=bob@x.com, age=7], User[name=Carol, email=carol@x.com, age=77], User[name=Alice, email=alice@x.com, age=42]]

4 elements in setOfUsers = [User[name=Bob, email=bob@x.com, age=7], User[name=Carol, email=carol@x.com, age=77], User[name=Alice, email=alice@x.com, age=42], User[name=Alice, email=a@aol.com, age=-666]]

We see in those results (a) no particular sorting, and (b) the addition of a second "Alice", having increased the set from 3 elements to 4.

Caveat

One possible downside to my solution here is that we are violating the recommendation of the Javadoc of TreeSet to be “consistent with equals”, thereby violating the general contract of Set.

I am not sure if that issue is problematic or not — I do not have enough perspective to form a judgement.

Java HashSet with a custom equality criteria?

Nope, you've found exactly the solution you're supposed to use.

Even for TreeSet, it's frowned upon to use comparison criteria that aren't compatible with equals:

Note that the ordering maintained by a sorted set (whether or not an explicit comparator is provided) must be consistent with equals if the sorted set is to correctly implement the Set interface.

I don't know about Apache Commons, but Guava specifically rejected requests for this sort of thing, although you can achieve what you want using Guava Equivalence:

Equivalence<T> equivalence = new Equivalence<T>() {
@Override
protected boolean doEquivalent(T a, T b) {
return CustomComparator.equals(a, b);
}

@Override
protected int doHash(T item) {
return CustomHashCodeGenerator.hashCode(item);
}
};
List<T> items = getItems();
Set<Equivalence.Wrapper<T>> setWithWrappedObjects = items.stream()
.map(item -> equivalence.wrap(item))
.collect(Collectors.toSet());


Related Topics



Leave a reply



Submit