Специальное использование обобщений Java: «хак» или «приятное повышение производительности»? - PullRequest
8 голосов
/ 21 февраля 2012

мы упростили некоторые определения и использование обобщений в нашем коде. Теперь у нас есть интересный случай, возьмем этот пример:

public class MyWeirdClass {

    public void entryPoint() {
        doSomethingWeird();
    }

    @SuppressWarnings( "unchecked" )
    private <T extends A & B> T getMyClass() {
        if ( System.currentTimeMillis() % 2 == 0 ) {
           return (T) new MyClass_1();
        } else {
           return (T) new MyClass_2();
        }
    }

    private <T extends A & B> void doSomethingWeird() {
        T obj = getMyClass();

        obj.methodFromA();
        obj.methodFromB();
    }

    static interface A {
        void methodFromA();
    }

    static interface B {
        void methodFromB();
    }

    static class MyClass_1 implements A, B {
        public void methodFromA() {};
        public void methodFromB() {};
    }

    static class MyClass_2 implements A, B {
        public void methodFromA() {};
        public void methodFromB() {};
    }
}

Теперь посмотрим на метод doSeomthingWeird () в MyWeirdClass: Этот код будет правильно скомпилирован с использованием JDT-компилятора eclipse, однако произойдет сбой при использовании компилятора Oracle. Поскольку JDT может генерировать рабочий байт-код, это означает, что на уровне JVM это допустимый код, и он является «только» компилятором Oracle, не позволяющим компилировать такие грязные (!?) вещи. Мы понимаем, что компилятор Oracle не примет вызов 'T obj = getMyClass ();' так как T не является действительно существующим типом. Однако, поскольку мы знаем, что возвращаемый объект реализует A и B, почему бы не разрешить это? (Компилятор JDT и JVM делают). Также обратите внимание, что, поскольку универсальный код используется только внутренне в закрытых методах, мы не хотим раскрывать их на уровне класса, загрязняя внешний код определениями универсальных элементов, в которых мы не заинтересованы (вне класса).

Школьным решением будет создание интерфейса AB, расширяющего A, B, однако, поскольку мы имеем большее количество интерфейсов, которые используются в разных комбинациях и поступают из разных модулей, создание общих интерфейсов для всех комбинаций значительно увеличит количество «фиктивных» интерфейсов и, наконец, делает код менее читабельным. В теории это потребовало бы до N-перестановок различных интерфейсов-оболочек, чтобы охватить все случаи. Решением «бизнес-ориентированный инженер» (другие называют это «ленивый инженер») было бы оставить код таким образом и начать использовать только JDT для компиляции кода. Редактировать: Это ошибка в Oracle Javac 6 и работает без проблем также на Oracle Javac 7

Что ты имеешь в виду? Есть ли какие-то скрытые опасности при принятии этой «стратегии»?


Дополнение во избежание обсуждения (для меня) неактуальных моментов: Я не спрашиваю, почему приведенный выше код не компилируется на компиляторе Oracle Я знаю причину и не хочу модифицировать этот вид кода без очень веской причины, если он отлично работает при использовании другого компилятора. Пожалуйста, сконцентрируйтесь на определении и использовании (без указания конкретного типа) метода doSomethingWeird (). Есть ли веская причина, почему мы не должны использовать только JDT-компилятор, который позволяет писать и компилировать этот код и прекращать компиляцию с помощью компилятора Oracle, который не примет приведенный выше код? (Спасибо за ввод)

Редактировать : Приведенный выше код корректно компилируется в Oracle Javac 7, но не в Javac 6. Это ошибка Javac 6. Это означает, что в нашем коде нет ничего плохого, и мы можем придерживаться его. На вопрос дан ответ, и я отмечу его как таковой после двухдневного перерыва в моем собственном ответе. Спасибо всем за конструктивный отзыв.

Ответы [ 7 ]

10 голосов
/ 21 февраля 2012

В Java вы можете использовать универсальные методы, если универсальный тип используется в Параметре или Возвращаемый тип сигнатуры метода.В вашем примере универсальный метод doSomethingWeird, но никогда не использовал его в сигнатуре метода.см. следующий пример:

class MyWeirdClass
{
    public void entryPoint()
    {
        doSomethingWeird(new MyClass_1());
    }

    private <T extends A & B> T getMyClass()
    {
        if (System.currentTimeMillis() % 2 == 0)
        {
            return (T) new MyClass_1();
        }
        else
        {
            return (T) new MyClass_2();
        }
    }

    private <T extends A & B> void doSomethingWeird(T a)
    {
        T obj = getMyClass();

        obj.methodFromA();
        obj.methodFromB();
    }
}

Этот код работает нормально.

JLS (спецификация языка Java) говорит в Универсальный метод part:

Type parameters of generic methods need not be provided explicitly when a
generic method is invoked. Instead, they are almost always inferred as specified in
§15.12.2.7

По этой цитате, когда вы не используете T в doSomethingWeird сигнатуре метода. Какой необработанный тип вы указываете T во время вызова (в entryPoint методе)?

0 голосов
/ 25 февраля 2012

Ваш подход сомнителен, потому что непроверенные броски жертвуют безопасностью во время выполнения. Рассмотрим этот пример:

interface A {
    void methodFromA();
}

interface B {
    void methodFromB();
}

class C implements A { // but not B!
    @Override public void methodFromA() {
        // do something
    }
}

class D implements A, B {
    @Override
    public void methodFromA() {
        // TODO implement

    }
    @Override
    public void methodFromB() {
        // do something
    }
}

class Factory {
    @SuppressWarnings( "unchecked" )
    public static <T extends A & B> T getMyClass() {
        if ( System.currentTimeMillis() % 2 == 0 ) {
           return (T) new C();
        } else {
           return (T) new D();
        }
    }
}

public class Innocent {
    public static <T extends A & B> void main(String[] args) {
        T t = Factory.getMyClass();

        // Sometimes this line throws a ClassCastException
        // really weird, there isn't even a cast here!
        //                         The maintenance programmer
        t.methodFromB();
    }
}

(Возможно, вам придется запустить программу несколько раз, чтобы увидеть, что смутило программиста по обслуживанию.)

Да, в этой простой программе ошибка довольно очевидна, но что, если объект будет передан примерно половине вашей программы, пока его интерфейс не пропущен? Как бы вы узнали, откуда появился плохой объект?

Если это вас не убедило, что насчёт этого:

class NotQuiteInnocent {
    public static void main(String[] args) {
        // Sometimes this line throws a ClassCastException
        D d = Factory.getMyClass();
    }
}

Действительно ли удаление пары объявлений интерфейса действительно того стоит?

0 голосов
/ 21 февраля 2012

Я бы снова пошел за создание того, что вы называете "фиктивным" интерфейсом AB.

Прежде всего, я вообще не нахожу его фиктивным.Есть два класса с одинаковыми определениями общих методов, и в одном месте вам нужно использовать один из них независимо от того, какой это на самом деле.Это точное использование наследования.Так что интерфейс AB идеально подходит здесь.И дженерики здесь неправильное решение.Дженерики никогда не предназначались для реализации наследования.

Во-вторых, определение интерфейса удалит все обобщенные элементы в вашем коде и сделает его намного более читабельным.На самом деле добавление интерфейса (или класса) никогда не делает ваш код менее читабельным.В противном случае было бы лучше поместить весь код в один класс.

0 голосов
/ 21 февраля 2012

Вот что ребята из OpenJDK ответили на мой вопрос:

Эти сбои вызваны тем, что компилятор JDK 6 неправильно реализует вывод типов.Много усилий было вложено в компилятор JDK 7, чтобы избавиться от всех этих проблем (ваша программа прекрасно компилируется в JDK 7).Однако некоторые из этих улучшений логического вывода требуют изменений, несовместимых с исходным кодом, поэтому мы не можем сделать бэкпорт этих исправлений в выпуске JDK 6.

Так что это значит для нас: в нашем коде нет ничего плохого иофициально поддерживается и Oracle.Мы также можем придерживаться такого рода кода и использовать Javac 7 с target = 1.6 для наших сборок maven, в то время как разработка в eclipse гарантирует, что мы не используем API Java 7: D yaaahyyy ​​!!!

0 голосов
/ 21 февраля 2012

Исходя из ответа @ MJM, я предлагаю вам обновить код, как показано ниже. Тогда ваш код не будет полагаться на вывод типа JVM.

public void entryPoint() {
    doSomethingWeird(getMyClass());
}

private <T extends A & B> T getMyClass() {
    if (System.currentTimeMillis() % 2 == 0) {
        return (T)new MyClass_1();
    } else {
        return (T)new MyClass_2();
    }
}

private <T extends A & B> void doSomethingWeird(T t) {
    t.methodFromA();
    t.methodFromB();
}
0 голосов
/ 21 февраля 2012

Я думаю, что причина в другом.Это не правда, что тип T в строке "T obj = getMyClass ();"неизвестно - на самом деле, из-за определения "T расширяет A & B", его стирание равно A .Это называется множественные границы и применяется следующее: «Когда используется множественная граница, первый тип, упомянутый в границе, используется как стирание переменной типа.»

0 голосов
/ 21 февраля 2012

Я не проверял код (скомпилировать с обоими компиляторами). Существует много странных вещей в спецификации языка на еще более базовом уровне (ну, проверьте объявление массива ...). Тем не менее, я считаю, что приведенный выше дизайн немного «чрезмерно спроектирован», и если я правильно переведу необходимость, требуемая функциональность может быть достигнута с помощью шаблона Factory или если вы используете какую-то инфраструктуру IoC (Spring?) тогда инъекция метода поиска может сделать магию для вас. Я думаю, что код будет более интуитивно понятным, простым для чтения и поддержки.

...