Как передать универсальный интерфейс методу, в котором он реифицируется?

У меня общий интерфейс

public interface MyInterface<T> {
    T method(T input);
}

и пара его реализаций через обычные классы, такие как

public class MyClass<T> implements MyInterface<T> {
    @Override
    public T method(T input) {
        T output = input; // whatever
        return output;
    }
}

и анонимные классы (см. ниже). Теперь я хочу протестировать эти реализации:

class TestClass1 {
    // ...
}

class TestClass2 {
    final int n;
    final String s;

    TestClass2(int n, String s) {
        this.n = n;
        this.s = s;
    }
    // ...
}

public class TestGenericImplementation {
    private static <T> void makeTest(T testObject, MyInterface<T> impl) {
        T output = impl.method(testObject);
        if (output == null)
            throw new NullPointerException();
        // verify output further
    }

    // Question 1. How to specify the parameter here properly?
    public static void testImplementation(MyInterface impl) {
        // Question 2. How to avoid compiler warning about unchecked cast below?

        // Doesn't work if impl is of type MyInterface<?> above
        makeTest(new TestClass1(), impl);
        makeTest(new TestClass2(1, "ABC"), impl); 

        // Ugly typecasts. Compiler complains.
        makeTest(new TestClass1(), (MyInterface<TestClass1>) impl);  
        makeTest(new TestClass2(1, "ABC"), (MyInterface<TestClass2>) impl); 
    }

    public static void main(String[] args) {
        // Question 3. How to pass the interface argument here?

        // Works but issues compiler warning about raw type
        testImplementation(new MyClass());
        testImplementation(new MyInterface() {
            @Override
            public Object method(Object input) {
                return null; // whatever
            }
        });

        // Looks ugly
        testImplementation(new MyClass<Object>()); 
        testImplementation(new MyInterface<Object>() {
            @Override
            public Object method(Object input) {
                return null;
            }
        });

        /* Diamond operator appeared only in Java 7,
         * while generics were introduced in Java 5.
         * What was the recommended way to solve this problem between 2004 and 2011?
         * Besides that, this doesn't work for anonymous classes.
         */
        testImplementation(new MyClass<>()); 
        testImplementation(new MyInterface<>() { // Doesn't work
            @Override
            public Object method(Object input) {
                return null;
            }
        });

        testImplementation(x -> x); // Lambda exprssions are for simple cases only
    }
}

Проблема в том, что компилятор выдает серию ошибок и предупреждений из-за перехода от универсального интерфейса к его реифицируемым версиям (которые мне нужно использовать с конкретными классами TestClass1 и TestClass2 вместо переменной универсального типа T). Можно ли полностью избежать этих предупреждений? Если нет (т.е.если их можно только подавить), есть ли из этого какие-то подводные камни?

Дженерики в Java никогда не реифицируются. Что ты имеешь в виду?

Michael 18.12.2018 16:10

@Michael Я имел в виду переход с T на TestClass1 и TestClass2 в testImplementation(). Если овеществлять здесь неправильный термин, поправьте меня.

John McClane 18.12.2018 16:13
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
2
98
2

Ответы 2

<T> void makeTest(T testObject, Test.MyInterface<T> impl)

ожидает, что T будет одинаковым для обоих аргументов.

public static void testImplementation(Test.MyInterface impl) {
    makeTest(new Test.TestClass1(), impl);
    makeTest(new Test.TestClass2(1, "ABC"), impl);
    //...

Получили противоречие. В первом случае T будет TestClass1, во втором - TestClass2. Если бы вы использовали универсальную версию Test.MyInterface, не было бы типа, который мог бы ей удовлетворить. Вам это сойдет с рук только потому, что вы используете необработанные типы. Это не может быть Test.MyInterface<TestClass1> и Test.MyInterface<TestClass2> одновременно.

Вам нужно избавиться от метода testImplementation и перестать использовать необработанные типы. Первая часть вашего основного метода может выглядеть так:

public static void main(String[] args) {

    makeTest(new Test.TestClass1(), new Test.MyClass<>());
    makeTest(new Test.TestClass2(1, "ABC"), new Test.MyClass<>());

Метод testImplementation() неизбежен, потому что он объединяет набор тестов для конкретной реализации MyInterface и выводит окончательный результат в конце. Что, если мне нужно провести 10 тестов для 10 различных реализаций? Вставить 100 вызовов makeTest() в main()?

John McClane 18.12.2018 16:32

Я упомянул об использовании подстановочного знака <?> в вопросе. Может быть, testImplementation(MyInterface<?> impl) был бы лучшей подписью метода?

John McClane 18.12.2018 16:36

Это не решает того факта, что это логическое противоречие.

Michael 18.12.2018 18:38

Вот как я это решил.

/* This utility method bears the brunt.
 * It shows that the cast is safe for this particular interface.
 * It is recommended to explain why. Example is given below. */
@SuppressWarnings("unchecked")
public static <T> MyInterface<T> reify(MyInterface<?> impl) {
    /* Safe because instances of MyInterface<T> doesn't suppose to hold
     * objects of type T (directly or indirectly) between invocations
     * of its methods. */
    return (MyInterface<T>) impl;
}

// Use a wildcard type in the signature
public static void testImplementation(MyInterface<?> impl) {
    // No warnings now
    makeTest(new TestClass1(), reify(impl));
    makeTest(new TestClass2(1, "ABC"), reify(impl));
}

public static void main(String[] args) {
    // Use the diamond operator for ordinary classes
    testImplementation(new MyClass<>());
    // Use lambda expressions when possible
    testImplementation(x -> x);
    /* Anonymous classes still require explicit type
    (Object in this case, Bound when the type variable is bounded
    at the interface definition: MyInterface<T extends Bound> */
    testImplementation(new MyInterface<Object>() {
        @Override
        public Object method(Object input) {
            return input;
        }
    });
}

По-прежнему требуется один @SuppressWarnings, но все небезопасные операции сосредоточены в одном месте, где необходимо объяснить, почему подавление безопасно.

Если у кого-то есть лучшее решение, дайте мне знать.

Другие вопросы по теме