4

I have a generic class with a generic list in it. I want to ensure that the generic list only contains unique classes.

What I have done so far is to compare the class names with reflection (getClass()). But I think that's not a clean solution. Are there any better practices to check?

public class MyGenericClass<T extends MyGenericClass.MyInterface> {
  private List<T> members = new ArrayList<>(0);

  public void add(T t) {
    final boolean[] classInMembers = {false};

    members.forEach(member -> {
      if (member.getClass().getName().equals(t.getClass().getName())) {
        classInMembers[0] = true;
      }
    });

    if (!classInMembers[0]) {
      members.add(t);
    }
  }

  public interface MyInterface {
    void doSomething(String text);
  }
}
public class Main {
  public static void main(String[] args) {
    MyGenericClass<MyGenericClass.MyInterface> myGenericClass = new MyGenericClass<>();

    myGenericClass.add(new Performer1());
    myGenericClass.add(new Performer2());
    myGenericClass.add(new Performer3());
    myGenericClass.add(new Performer3()); // should not be inserted!
  }

  private static class Performer1 implements MyGenericClass.MyInterface {
    @Override
    public void doSomething(String text) {
      text = "Hi, I am performer 1!";
    }
  }

  private static class Performer2 implements MyGenericClass.MyInterface {
    @Override
    public void doSomething(String text) {
      text = "Hi, I am performer 2!";
    }
  }

  private static class Performer3 implements MyGenericClass.MyInterface {
    @Override
    public void doSomething(String text) {
      text = "Hi, I am performer 3!";
    }
  }
}
PatrickMA
  • 887
  • 9
  • 23
  • 1
    As a side note, that use of an array just to get around scoping rules of a lambda is .... strange. You should just use a regular `for(T member : members)` loop instead. `forEach` isn't an improvement if you need to do something like that. – Radiodef Jul 30 '15 at 11:20
  • @Radiodef thanks, I'll change that in my code! – PatrickMA Jul 30 '15 at 11:38

6 Answers6

2

Don't use a List, use a java.util.Set instead.

A collection that contains no duplicate elements. More formally, sets contain no pair of elements e1 and e2 such that e1.equals(e2), and at most one null element.

If the iteration order is important or if you want to use a custom Comparator, the TreeSet implementation can be used:

A NavigableSet implementation based on a TreeMap. The elements are ordered using their natural ordering, or by a Comparator provided at set creation time, depending on which constructor is used.

Example of a Set using a Comparator:

class MyComparator implements Comparator<Object> {
    @Override
    public int compare(Object e1, Object e2) {
        if (e1.getClass() == e2.getClass())
            return 0;
        //if you wish to have some extra sort order                
        return e1.getClass().getName().compareTo(e2.getClass().getName());
    }
}

. . .

Set mySet = new TreeSet<Object>(new MyComparator());
mySet.add(new Object());
mySet.add(new Object());//same class already in set
mySet.add("wtf");

//mySet.size() is now 2 - the second "new Object()" was not inserted due to the comparator check
fantaghirocco
  • 4,761
  • 6
  • 38
  • 48
  • 2
    This would require that all Performer3 objects were equal. – Florian Schaetz Jul 30 '15 at 08:21
  • @Florian Schaetz Absolutely no: we're talking about **generics** – fantaghirocco Jul 30 '15 at 08:26
  • @fantaghirocco please elaborate, I don't understand what you mean. Florian's point sounds correct. – Andy Turner Jul 30 '15 at 08:28
  • @fantaghirocco but when I add an object to a set the reference of the object itself will be compared, not the class. – PatrickMA Jul 30 '15 at 08:28
  • @PatriX1996 override your classes' `hashCode` and `equals` methods to compare the class rather than the instance. – Andy Turner Jul 30 '15 at 08:29
  • @PatriX1996 it depens on your comparator – fantaghirocco Jul 30 '15 at 08:29
  • @Andy Turner I mean that a `Set` or `Set` can contain any kind of objects – fantaghirocco Jul 30 '15 at 08:30
  • @fantaghirocco but you did not mention a custom comparator in your answer – PatrickMA Jul 30 '15 at 08:32
  • 1
    @fantaghirocco - What do generics have to do with that? The Set contains Objects. If you put two Performer3 objects in there without overriding their equals() and hashcode() methods, the two will not be equals(), have different hashcode()s and thus can be both in the Set. – Florian Schaetz Jul 30 '15 at 08:35
  • @fantaghirocco but the comparator will not be used if I use `.contains()`. See http://docs.oracle.com/javase/7/docs/api/java/util/TreeSet.html#contains(java.lang.Object) – PatrickMA Jul 30 '15 at 08:59
  • @PatriX1996 you're right but for what reason should a comparator be used with `contains()`? – fantaghirocco Jul 30 '15 at 09:19
  • @fantaghirocco So that I can check if the list contains an object with the same class – PatrickMA Jul 30 '15 at 10:29
  • @PatriX1996 a comparator is not intended to be used like that (ok, it tests for *equality* also but it's used to basically maintain the *sorting order*): that's the point of the Richard Walton's answer. In that case overriding `contains()` is the right way to go – fantaghirocco Jul 30 '15 at 10:41
  • @PatriX1996 Sorry, I'm wrong: Sets do actually use a comparator to extablish if an element can be inserted or not. I edit my answer accordingly. It may be the solution you are looking for – fantaghirocco Jul 30 '15 at 11:01
  • @fantaghirocco I have implemented Richard Waltons solution, but If your solution is cleaner then I can change my code. – PatrickMA Jul 30 '15 at 11:04
  • Yeah, now I understand your answer. But for now I've taken the solution by @Richard Walton, because I think it's more readable for other developers, but I have taken your code as a comment in the java file ;) – PatrickMA Jul 31 '15 at 07:07
  • 1
    @PatriX1996 different approaches, both reliable solutions in my opinion – fantaghirocco Jul 31 '15 at 07:32
2

You could subclass a java.util.Set interface implementation. It will likely be easiest to subclass java.util.AbstractSet.

By default 'Set' will compare objects by their .equals() method - In your case, this is not sufficient. You will need to override the contains method to ensure that only instances of a unique class are added.

In your overrideen contains, it's probably the same / easier to compare class instances rather than their stringified package name

I.e. use a.getClass() == b.getClass(), rather than a.getClass().getName()

Richard Walton
  • 4,789
  • 3
  • 38
  • 49
  • Thank you, I think this solution is the cleanest. – PatrickMA Jul 30 '15 at 08:39
  • You can't subclass `Set` since it is an interface. Apart from that, you have to be careful when overriding methods of concrete classes since you have to make sure the overridden `contains` method remains consistent with the rest of the class. – user140547 Jul 30 '15 at 08:45
  • @user140547 thats true, I've already suggested an edit to this answer, so it will be corrected soon! – PatrickMA Jul 30 '15 at 08:47
  • @Richard Walton could you approve my edit suggestion, because It's an important point! – PatrickMA Jul 30 '15 at 08:51
  • Apologies. I don't see to have the ability to see any proposed edits unfortunately. I assumed the edit is to subclass AbstractSet? – Richard Walton Jul 30 '15 at 15:22
1

Why so complicated?

public class Main {

    public static void main(String[] args) {
        final Class<?> helloClass = "Hello".getClass();
        final Class<?> worldClass = "World".getClass();
        final Class<?> intClass = Integer.class;

        System.out.println(helloClass.equals(worldClass)); // -> true
        System.out.println(helloClass.equals(intClass)); // -> false
    }
}
Smutje
  • 17,733
  • 4
  • 24
  • 41
0

You could maintain a roster of members in a Set.

public static class MyGenericClass<T extends MyGenericClass.MyInterface> {

    private List<T> members = new ArrayList<>(0);
    // Add this.
    private Set<Class<?>> roster = new HashSet<>();

    public void add(T t) {
        if (!roster.contains(t.getClass())) {
            members.add(t);
            roster.add(t.getClass());
        }
    }

    private void soundOff() {
        for (T t : members) {
            t.doSomething();
        }
    }

    public interface MyInterface {

        void doSomething();
    }
}

private static class Performer implements MyGenericClass.MyInterface {

    final int n;

    public Performer(int n) {
        this.n = n;
    }

    @Override
    public void doSomething() {
        System.out.println("Hi, I am a " + this.getClass().getSimpleName() + "(" + n + ")");
    }
}

private static class Performer1 extends Performer {

    public Performer1(int n) {
        super(n);
    }

}

private static class Performer2 extends Performer {

    public Performer2(int n) {
        super(n);
    }
}

private static class Performer3 extends Performer {

    public Performer3(int n) {
        super(n);
    }
}

public void test() {
    MyGenericClass<MyGenericClass.MyInterface> myGenericClass = new MyGenericClass<>();

    myGenericClass.add(new Performer1(1));
    myGenericClass.add(new Performer2(2));
    myGenericClass.add(new Performer3(3));
    myGenericClass.add(new Performer3(4)); // should not be inserted!

    myGenericClass.soundOff();
}
OldCurmudgeon
  • 64,482
  • 16
  • 119
  • 213
0

You could implement a Wrapper which provides the necessary comparison and add the wrapped instance to the set. This way you don't have to override equals and hashcode in your concrete Performer classes and you don't have to subclass a concrete Set implementation (which you are coupled to. When you subclass a HashSet, you have to use that concrete class. But what if you want to use a LinkedHashSet at some point? You have to override LinkedHashSet as well) , which may be fragile since you have to make sure that the overridden method is consistent with the rest of the class.

class MyGenericClass<T extends MyInterface> {
    private Set<ClassCompareWrapper<T>> members = new HashSet<>();

    public void add(T t) {
        members.add(new ClassCompareWrapper<T>(t));
    }
}

class ClassCompareWrapper<T> {
    T t;

    public ClassCompareWrapper(T t) {
        this.t = t;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (!(o instanceof ClassCompareWrapper))
            return false;
        ClassCompareWrapper<?> that = (ClassCompareWrapper<?>) o;
        return Objects.equals(t.getClass(), that.t.getClass());
    }

    @Override
    public int hashCode() {
        return Objects.hash(t.getClass());
    }

    @Override
    public String toString() {
        return "Wrapper{" +
                "t=" + t +
                '}';
    }
}
user140547
  • 7,750
  • 3
  • 28
  • 80
0

Here are a few other ideas.

Using streams:

public void add(T t) {
    if (!members.stream().anyMatch(m -> m.getClass() == t.getClass())) {
        members.add(t);
    }
}

Using AbstractSet and HashMap:

class ClassSet<E> extends AbstractSet<E> {
    private final Map<Class<?>, E> map = new HashMap<>();
    @Override
    public boolean add(E e) {
        // this can be
        // return map.putIfAbsent(e.getClass(), e) != null;
        // in Java 8
        Class<?> clazz = e.getClass();
        if (map.containsKey(clazz)) {
            return false;
        } else {
            map.put(clazz, e);
            return true;
        }
    }
    @Override
    public boolean remove(Object o) {
        return map.remove(o.getClass()) != null;
    }
    @Override
    public boolean contains(Object o) {
        return map.containsKey(o.getClass());
    }
    @Override
    public int size() {
        return map.size();
    }
    @Override
    public Iterator<E> iterator() {
        return map.values().iterator();
    }
}

A HashMap could also be used without wrapping it in a Set. The Set interface is defined around equals and hashCode, so any implementation which deviates from this is technically non-contractual. Additionally, you might want to use LinkedHashMap if the values are iterated often.

Radiodef
  • 37,180
  • 14
  • 90
  • 125