Home > Enterprise >  HashSet using custom hashCode
HashSet using custom hashCode

Time:06-03

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.

  • Related