fhossfel
fhossfel

Reputation: 2191

Collection containing only one type

FI need a collection to contain only one subtype. It does not matter which one but it is important that all elements of the collection are of the same class. The subtype itself is not known at compile time.

What I need can be best described by the follwing unit test:

import java.util.ArrayList;
import java.util.List;
import org.junit.Test;

public class SingleTypeTest {

    public static abstract class AbstractFoo {

        public abstract void someMethod();

    }

    public static class FooA extends AbstractFoo {

        @Override
        public void someMethod() {};

    }

    public static class TestB extends AbstractFoo {

        @Override
        public void someMethod() {};

    }

    public List<? extends AbstractFoo> myList;

    @Test
    public void testFooAOnly() {

        myList = new ArrayList<FooA>();
        myList.add(new FooA()); // This should work!
        myList.add(new FooB()); // this should fail!


    }

    @Test
    public void testFooBOnly() {

        myList = new ArrayList<FooB>();
        myList.add(new FooB()); // This should work!
        myList.add(new FooA()); // this should fail!


    }


}

This piece of code is acutally not compilable due to type erasure but it best specifies what I want to do.

The question is: What other apporaches are available to ensure that all elements are of the same type?

Only thing I can thing of is to write a delegate that wraps around the list and checks the objects added to the class are all of the same type but that seems fairly clunky. Any other ideas?

Update: I have I have clarified the question and the unit test code.

Upvotes: 2

Views: 543

Answers (2)

fhossfel
fhossfel

Reputation: 2191

I have solved this by a delegate/wrappper like so:

import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Spliterator;
import java.util.function.UnaryOperator;

/**
 *
 * @author fhossfel
 */
public class SingleTypeList<T> implements List<T> {

    private final List<T> wrappedList;
    private final boolean allowSubTypes;
    private Class clazz;

    public SingleTypeList(List<T> list, Class clazz) {

        this(list, clazz, true);

    }

    public SingleTypeList(List<T> list, Class clazz, boolean allowSubTypes) {

        this.wrappedList = list;
        this.allowSubTypes = allowSubTypes;
        this.clazz = clazz;

    }


    @Override
    public int size() {
        return wrappedList.size();
    }

    @Override
    public boolean isEmpty() {
        return wrappedList.isEmpty();
    }

    @Override
    public boolean contains(Object o) {
        return wrappedList.contains(o);
    }

    @Override
    public Iterator<T> iterator() {
        return wrappedList.iterator();
    }

    @Override
    public Object[] toArray() {
        return wrappedList.toArray();
    }

    @Override
    public <T> T[] toArray(T[] a) {
        return wrappedList.toArray(a);
    }

    @Override
    public boolean add(T e) {

        if (isAcceptable(e)) {

            return wrappedList.add(e);

        } else {

            throw new IllegalArgumentException("Object " + e.toString() + "is of class " + e.getClass() 
                                                  + " but only elements of type " + clazz.getName()
                                                  + (allowSubTypes ? " or any subtype of it " : "") + " may be added to this collection");           
        }
    }

    @Override
    public boolean remove(Object o) {
        return wrappedList.remove(o);
    }

    @Override
    public boolean containsAll(Collection<?> c) {
        return wrappedList.containsAll(c);
    }

    @Override
    public boolean addAll(Collection<? extends T> c) {

        if (areAllAcceptable(c)) {

            return wrappedList.addAll(c);

        } else {

            throw new IllegalArgumentException("Not all elements are of type " + clazz.getName()
                                                  + (allowSubTypes ? " or any subtype of it " : "") + " and may not be added to this collection");           
        }

    }

    @Override
    public boolean addAll(int index, Collection<? extends T> c) {

        if (areAllAcceptable(c)) {

            return wrappedList.addAll(index, c);

        } else {

            throw new IllegalArgumentException("Not all elements are of type " + clazz.getName()
                                                  + (allowSubTypes ? " or any subtype of it " : "") + " and may not be added to this collection");           
        }
    }

    @Override
    public boolean removeAll(Collection<?> c) {
        return wrappedList.removeAll(c);
    }

    @Override
    public boolean retainAll(Collection<?> c) {
        return wrappedList.retainAll(c);
    }

    @Override
    public void replaceAll(UnaryOperator<T> operator) {
        wrappedList.replaceAll(operator);
    }

    @Override
    public void sort(Comparator<? super T> c) {
        wrappedList.sort(c);
    }

    @Override
    public void clear() {
        wrappedList.clear();
    }

    @Override
    public boolean equals(Object o) {
        return wrappedList.equals(o);
    }

    @Override
    public int hashCode() {
        return wrappedList.hashCode();
    }

    @Override
    public T get(int index) {
        return wrappedList.get(index);
    }

    @Override
    public T set(int index, T element) {
        return wrappedList.set(index, element);
    }

    @Override
    public void add(int index, T element) {
        wrappedList.add(index, element);
    }

    @Override
    public T remove(int index) {
        return wrappedList.remove(index);
    }

    @Override
    public int indexOf(Object o) {
        return wrappedList.indexOf(o);
    }

    @Override
    public int lastIndexOf(Object o) {
        return wrappedList.lastIndexOf(o);
    }

    @Override
    public ListIterator<T> listIterator() {
        return wrappedList.listIterator();
    }

    @Override
    public ListIterator<T> listIterator(int index) {
        return wrappedList.listIterator(index);
    }

    @Override
    public List<T> subList(int fromIndex, int toIndex) {
        return wrappedList.subList(fromIndex, toIndex);
    }

    @Override
    public Spliterator<T> spliterator() {
        return wrappedList.spliterator();
    }

    private boolean isAcceptable(T o) {

        return (o == null                                   // o is null -> then it can be added
                ||  (allowSubTypes  && clazz.isInstance(o)) // sub-types are allowed and o is a sub-type of clazz
                ||  (o.getClass().equals(clazz)));          // or o is actually of the type clazz

    }

    private boolean areAllAcceptable(Collection<? extends T> c) {

        if (c == null || c.isEmpty()) return true;

        for (T o : c) {

            if (! isAcceptable(o)) {

                return false;

            }                        
        }

        return true;

    }

}

A unit test to show usage looks like that:

import biz.direction.punkrockit.snapastyle.snapastyle.servlet.util.SingleTypeList;
import java.util.ArrayList;
import java.util.List;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class SingleTypeListTest {

    public static abstract class AbstractFoo {

        public abstract void someMethod();

    }

    public static class FooA extends AbstractFoo {

        @Override
        public void someMethod() {};

    }

    public static class BarA extends FooA {

        @Override
        public void someMethod() {};

    }

    public static class FooB extends AbstractFoo {

        @Override
        public void someMethod() {};

    }

    public List<AbstractFoo> myList;

    @Before
    public final void setUp() {

         myList = new SingleTypeList<AbstractFoo>(new ArrayList<AbstractFoo>(), FooA.class);

    }


    @Test
    public void testFooAOnly() {

        myList.add(new FooA()); // this should work 
        Assert.assertFalse("List must not be empty.", myList.isEmpty());


    }

    @Test
    public void testFooAandBarA() {

        myList.add(new FooA()); // this should work 
        myList.add(new BarA()); // this should work 
        Assert.assertTrue("List must contain two elements.", myList.size() == 2);


    }

   @Test(expected=IllegalArgumentException.class)
    public void testAddFooB() {

        myList.add(new FooB());

    }

    @Test(expected=IllegalArgumentException.class)
    public void testNoSubtype() {

        myList = new SingleTypeList<AbstractFoo>(new ArrayList<AbstractFoo>(), FooA.class, false);

        myList.add(new FooA()); // this should work 
        Assert.assertFalse("List must not be empty.", myList.isEmpty());

        myList.add(new BarA()); // This should fail!

    }

}

The impact on the code is fairly small but it seems a lot of code for what I want to do.

P.S.: The allowSubType flag in the construcotr wil determine whether sub types of the passed in class shall be accepted or whether the call requirement shall be considered "final".

Upvotes: 0

kajacx
kajacx

Reputation: 12959

OK, i think i understand what you want, so let's see why your code can't compile.

First, you say that you need

a collection to contain only one subtype

That is perfectly fine, List<? extends AbstractFoo> is exactly that, a collection of some subtype of AbstractFoo. Problem is, that since it can by any subtype, you cannot add anything into the collection (except null). Example:

List<? extends AbstractFoo> myList = getListSomewhere();
myList.add(new FooA()); //illegal -> compile-time error

Now compiler doesn't know what type that list is, it could be List<TestB>, thus adding to such list is never type safe.

Possible solution:

Don't lose hope just jet, you can still work with a collection that contains only one subtype, read and write to it:

public static <T extends AbstractFoo> void test(List<T> list, T t) {
    if(list.contains(t)) {
        t.someMethod();
    } else {
        list.add(t); //you can add to list
    }
    list.get(0).someMethod(); //you can read from list
}

Upwards is an attempt for simple yet demonstrating method that works with list of unknown subtype of AbstractFoo.

The trick is that now instead of wildcard, you use bounded type parameter, so while you still don't know what exactly the type is, you know that the parameter t is the same type as the list, so you can add it.

Usage:

List<FooA> listA = new ArrayList<>();
test(listA, new FooA()); //OK
test(listA, new TestB()); //Compile error

Upvotes: 2

Related Questions