Hi I could someone point me right direction when I would like to have a custom HashSet without changing hashCode() method. Usage would be to have Set of object which has to have different one attribute (or more).
so for example for this Class:
@FieldDefaults(level = AccessLevel.PRIVATE)
@Getter @Setter
public class User{
String name;
String email;
String age;
}
I would like to have UserNameSet which would allow to contain only users which have different name. I do not want to override the hashCode and equals method in User, because I still want to differentiate between users with same name but different email for example.
I would like to somehow override the hashCode() method just for this one HashMap.
EDITED
I have come up with this solution on first glance it works, could someone check it?
package com.znamenacek.debtor.util;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@FieldDefaults(level = AccessLevel.PRIVATE)
public class CustomizableHashSet<T> implements Set<T> {
Function<T, Integer> customHashCode = Object::hashCode;
HashSet<ClassWrapper> storage = new HashSet<>();
public CustomizableHashSet(Function<T, Integer> customHashCode) {
this.customHashCode = customHashCode;
}
public CustomizableHashSet() {
}
public CustomizableHashSet(Collection<? extends T> c, Function<T, Integer> customHashCode) {
storage = new HashSet<>(c.stream().map(ClassWrapper::new).toList());
this.customHashCode = customHashCode;
}
public CustomizableHashSet(Collection<? extends T> c) {
storage = new HashSet<>(c.stream().map(ClassWrapper::new).toList());
}
public CustomizableHashSet(int initialCapacity, float loadFactor, Function<T, Integer> customHashCode) {
storage = new HashSet<>(initialCapacity, loadFactor);
this.customHashCode = customHashCode;
}
public CustomizableHashSet(int initialCapacity, float loadFactor) {
storage = new HashSet<>(initialCapacity, loadFactor);
}
public CustomizableHashSet(int initialCapacity, Function<T, Integer> customHashCode) {
storage = new HashSet<>(initialCapacity);
this.customHashCode = customHashCode;
}
public CustomizableHashSet(int initialCapacity) {
storage = new HashSet<>(initialCapacity);
}
@Override
public Iterator<T> iterator() {
return storage.stream().map(ClassWrapper::get).iterator();
}
@Override
public int size() {
return storage.size();
}
@Override
public boolean isEmpty() {
return storage.isEmpty();
}
@Override
public boolean contains(Object o) {
return storage.stream().map(ClassWrapper::get).collect(Collectors.toSet()).contains(o);
}
@Override
public boolean add(T t) {
return storage.add(new ClassWrapper(t));
}
@Override
public boolean remove(Object o) {
boolean returnValue;
var storageContent = storage.stream().map(ClassWrapper::get).collect(Collectors.toSet());
returnValue = storageContent.remove(o);
storage = storageContent.stream().map(ClassWrapper::new).collect(Collectors.toCollection(HashSet::new));
return returnValue;
}
@Override
public void clear() {
storage.clear();
}
@Override
public Object clone() {
throw new UnsupportedOperationException();
}
@Override
public Spliterator<T> spliterator() {
return storage.stream().map(ClassWrapper::get).spliterator();
}
@Override
public Object[] toArray() {
return storage.stream().map(ClassWrapper::get).toArray();
}
@Override
public <T1> T1[] toArray(T1[] a) {
return storage.stream().map(ClassWrapper::get).collect(Collectors.toSet()).toArray(a);
}
@Override
public boolean removeAll(Collection<?> c) {
boolean returnValue;
var storageContent = storage.stream().map(ClassWrapper::get).collect(Collectors.toSet());
returnValue = storageContent.removeAll(c);
storage = storageContent.stream().map(ClassWrapper::new).collect(Collectors.toCollection(HashSet::new));
return returnValue;
}
@Override
public boolean containsAll(Collection<?> c) {
boolean returnValue;
var storageContent = storage.stream().map(ClassWrapper::get).collect(Collectors.toSet());
returnValue = storageContent.containsAll(c);
storage = storageContent.stream().map(ClassWrapper::new).collect(Collectors.toCollection(HashSet::new));
return returnValue;
}
@Override
public boolean addAll(Collection<? extends T> c) {
return storage.addAll(c.stream().map(ClassWrapper::new).collect(Collectors.toSet()));
}
@Override
public boolean retainAll(Collection<?> c) {
boolean returnValue;
var storageContent = storage.stream().map(ClassWrapper::get).collect(Collectors.toSet());
returnValue = storageContent.retainAll(c);
storage = storageContent.stream().map(ClassWrapper::new).collect(Collectors.toCollection(HashSet::new));
return returnValue;
}
@Override
public String toString() {
return storage.stream().map(ClassWrapper::get).collect(Collectors.toSet()).toString();
}
@Override
public <T1> T1[] toArray(IntFunction<T1[]> generator) {
return storage.stream().map(ClassWrapper::get).collect(Collectors.toSet()).toArray(generator);
}
@Override
public boolean removeIf(Predicate<? super T> filter) {
boolean returnValue;
var storageContent = storage.stream().map(ClassWrapper::get).collect(Collectors.toSet());
returnValue = storageContent.removeIf(filter);
storage = storageContent.stream().map(ClassWrapper::new).collect(Collectors.toCollection(HashSet::new));
return returnValue;
}
@Override
public Stream<T> stream() {
return storage.stream().map(ClassWrapper::get);
}
@Override
public Stream<T> parallelStream() {
return storage.parallelStream().map(ClassWrapper::get);
}
@Override
public void forEach(Consumer<? super T> action) {
storage.stream().map(ClassWrapper::get).forEach(action);
}
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
@AllArgsConstructor
public class ClassWrapper{
T object;
@Override
public int hashCode() {
return customHashCode.apply(object);
}
@Override
public boolean equals(Object obj) {
if(this == obj) return true;
if(obj == null) return false;
return hashCode() == obj.hashCode();
}
public T get(){
return object;
}
@Override
public String toString() {
return "" hashCode() " - " object.toString();
}
}
}
CodePudding user response:
I would like to have UserNameSet which would allow to contain only users which have different name
You can apply composition and create a class that would maintain a Map and delegate all calls to it.
This approach is more flexible and less tedious than extending a collection, and also doesn't create tight coupling.
class UserNameSet {
private Map<String, User> userByName = new HashMap<>();
public User add(User user) {
return userByName.put(user.getName(), user);
}
public User remove(User user) {
return userByName.remove(user.getName());
}
public User remove(String name) {
return userByName.remove(name);
}
public boolean contain(String name) {
return userByName.containsKey(name);
}
public boolean contain(User user) {
return userByName.containsValue(user.getName());
}
// all other methods that are required
}
CodePudding user response:
commons-collections already provides an Equator interface to do what you suggest:
public interface Equator<T> {
boolean equate(T o1, T o2);
int hash(T o);
}
However direct support for creating collections based on an equator is limited. There are a few operations involving an equator available in CollectionUtils.
However you could leverage Transformer to wrap your desired objects into ones that use an equator and then use all the support commons-collections provides for transformers. For example using SetUtils.transformedSet:
class EquatorWrapper<T> {
private final Class<T> clazz;
private final T wrapped;
private final Equator<T> equator;
public EquatorWrapper(Class<T> clazz, T wrapped, Equator<T> equator) {
this.clazz = clazz;
this.wrapped = wrapped;
this.equator = equator;
}
@Override
public boolean equals(Object obj) {
if (clazz.isInstance(obj)) {
return equator.equate(wrapped, clazz.cast(obj));
}
return false;
}
@Override
public int hashCode() {
return equator.hash(wrapped);
}
}
class EquatorTransformer<T> implements Transformer<T, Object> {
private final Class<T> clazz;
private final Equator<T> equator;
public EquatorTransformer(Class<T> clazz, Equator<T> equator) {
this.clazz = clazz;
this.equator = equator;
}
@Override
public Object transform(T input) {
return new EquatorWrapper<>(clazz, input, equator);
}
}
SetUtils.transformedSet(someSet, EquatorTransformer.of(someEquator, SomeClazz.clazz));
CodePudding user response:
Use Comparator
with TreeSet
As commented, 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 by using a method reference for the getter method of your desired name
field: User :: name
.
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" , "[email protected]" , 7 ) ,
new User( "Alice" , "[email protected]" , 42 ) ,
new User( "Carol" , "[email protected]" , 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" , "[email protected]" , -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 protected], age=42], User[name=Bob, [email protected], age=7], User[name=Carol, [email protected], age=77]]
3 elements in setOfUsers = [User[name=Alice, [email protected], age=42], User[name=Bob, [email protected], age=7], User[name=Carol, [email protected], 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 protected], age=7], User[name=Carol, [email protected], age=77], User[name=Alice, [email protected], age=42]]
4 elements in setOfUsers = [User[name=Bob, [email protected], age=7], User[name=Carol, [email protected], age=77], User[name=Alice, [email protected], age=42], User[name=Alice, [email protected], 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.