Коллекция, содержащая только один тип

#java #generics #collections

#java #дженерики #Коллекции

Вопрос:

Мне нужна коллекция, содержащая только один подтип. Не имеет значения, какой именно, но важно, чтобы все элементы коллекции принадлежали к одному и тому же классу. Сам подтип неизвестен во время компиляции.

То, что мне нужно, лучше всего можно описать следующим модульным тестом:

 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!


    }


}
 

Этот фрагмент кода фактически не компилируется из-за удаления типа, но он лучше всего определяет, что я хочу сделать.

Вопрос в том, какие другие apporaches доступны, чтобы гарантировать, что все элементы имеют один и тот же тип?

Единственное, что я могу сделать, это написать делегат, который обтекает список и проверяет, что все объекты, добавленные в класс, имеют один и тот же тип, но это кажется довольно неуклюжим. Есть еще идеи?

Обновление: я прояснил вопрос и код модульного тестирования.

Комментарии:

1. Почему вы используете подстановочный знак? Если вам нужен список TestB , просто создайте List<TestB> . Если вам нужен список FooA , просто создайте List<FooA> .

2. Почему бы вам просто не объявить свой список как List<FooA> ?

3. Ваш тест выполняет то, что вы просите, нет ?..

4. Потому что он не знает во время компиляции, какой из них он хочет. Ему нужна динамическая безопасность типов во время выполнения, которая может быть достигнута в этом случае только путем создания пользовательской List оболочки, которая сохраняет тип времени выполнения первого добавления и проверяет все последующие добавления на равенство типов.

5. ИМХО, код модульного тестирования, который вы размещаете, более запутанный, чем демонстрация того, каковы ваши фактические требования. Пожалуйста, опубликуйте некоторый соответствующий код, который, по вашему мнению, поможет нам получить ясную картину. например, где вы создаете коллекцию, где добавляете в нее элементы и где вам нужно убедиться, что все элементы являются экземплярами одного конкретного подкласса.

Ответ №1:

Хорошо, я думаю, я понимаю, чего вы хотите, поэтому давайте посмотрим, почему ваш код не может скомпилироваться.

Во-первых, вы говорите, что вам нужно

коллекция, содержащая только один подтип

Это совершенно нормально, List<? extends AbstractFoo> именно так, коллекция некоторого подтипа AbstractFoo . Проблема в том, что, поскольку она может быть любого подтипа, вы не можете ничего добавить в коллекцию (кроме null ). Пример:

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

Теперь компилятор не знает, к какому типу относится этот список, это может быть List<TestB> , поэтому добавление в такой список никогда не является безопасным для типов.

Возможное решение:

Не теряйте надежды, просто jet, вы все равно можете работать с коллекцией, содержащей только один подтип, читать и записывать в нее:

 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
}
 

Upward — это попытка простого, но демонстрирующего метода, который работает со списком неизвестного подтипа AbstractFoo .

Хитрость в том, что теперь вместо подстановочного знака вы используете параметр ограниченного типа, поэтому, хотя вы все еще не знаете, какой именно тип, вы знаете, что параметр t имеет тот же тип, что и список, поэтому вы можете его добавить.

Использование:

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

Комментарии:

1. не вижу связи между решением и использованием, пожалуйста, уточните?

2. При использовании я просто вызываю метод из решения , чтобы показать, как должен вызываться метод. Возможно, мне следовало использовать example вместо usage .

3. Спасибо за ответ, но действительно ли это проще, чем делегат? Он массово изменяет подпись для доступа к коллекции.

4. Я не знаю, что именно делегировать, но я согласен с этим массовым изменением. Я бы подумал о том, как перепроектировать свой проект, чтобы избежать необходимости такого доступа к коллекциям. Вы также можете создать оболочку для arrayslist, которая запоминала бы его общий класс, и, таким образом, вы могли бы вызывать canAdd(E e) или что-то в этом роде.

Ответ №2:

Я решил это с помощью делегата / обертки следующим образом:

 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  amp;amp; 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;

    }

}
 

Модульный тест, показывающий использование, выглядит так:

 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!

    }

}
 

Влияние на код довольно мало, но кажется, что для того, что я хочу сделать, кода много.

PS: Флаг allowSubType в construcotr будет определять, должны ли быть приняты подтипы переданного в классе или требование вызова должно считаться «окончательным».