Какой вред в использовании класса Anonymous?

Вопрос возник при чтении ответа на этот вопрос - Как мне объединить два списка в Java. Этот ответ дал решение

List<String> newList = new ArrayList<String>() { { addAll(listOne); addAll(listTwo); } };

Читая комментарии, пользователи сказали, что это зло и безобразно и не должно использоваться в производстве.

Я хотел бы знать, какой вред в использовании этого? Почему это уродливо, зло или плохо использовать в производстве?


Примечание: задан вопрос как вопрос, потому что указанный пост слишком старый (2008 г.), а ответчик отсутствует уже несколько месяцев.

6 ответов

Решение

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

Приведенный пример куска исходного кода:

public interface Inner {
    void innerAction();
}

public class Outer {

    public void methodInOuter() {}

    private Inner inner = new Inner() {
        public void innerAction() {
            // calling a method outside of scope of this anonymous class
            methodInOuter();  
        }
    }
}

Во время компиляции происходит то, что компилятор создает файл класса для нового анонимного подкласса Inner который получает так называемое синтетическое поле со ссылкой на экземпляр Outer учебный класс. Сгенерированный байт-код будет примерно эквивалентен примерно так:

public class Outer$1 implements Inner {

    private final Outer outer; // synthetic reference to enclosing instance

    public Outer$1(Outer outer) {
        this.outer = outer;
    }

    public void innerAction() {
        // the method outside of scope is called through the reference to Outer
        outer.methodInOuter();
    }
}

Такой захват ссылки на включающий экземпляр происходит даже для анонимных классов, которые фактически никогда не обращаются ни к одному из методов или полей включающего класса, таких как список инициализированных двойной скобкой (DBI) в вашем вопросе.

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

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

С другой стороны, я бы по-прежнему утверждал, что в большинстве случаев использование DBI все еще не требуется. Что касается присоединения к списку, я бы создал простой метод многократного использования, который не только безопаснее, но и более лаконичен и понятен.

public static <T> List<T> join(List<? extends T> first, List<? extends T> second) {
    List<T> joined = new ArrayList<>();
    joined.addAll(first);
    joined.addAll(second);
    return joined;
}

И тогда код клиента становится просто:

List<String> newList = join(listOne, listTwo);

Дополнительное чтение: /questions/10205523/effektivnost-java-dvojnaya-initsializatsiya-skobki/10205528#10205528

Комментарии "некрасиво" и "не использовать в производстве" относятся к этому конкретному использованию анонимных классов, а не к анонимным классам в целом.

Это конкретное использование назначает newList анонимный подкласс ArrayList<String>совершенно новый класс, созданный с единственной целью - инициализация списка содержимым двух конкретных списков. Это не очень читабельно (даже опытный читатель потратит несколько секунд на то, чтобы понять это), но, что более важно, это может быть достигнуто без разделения на подклассы в том же количестве операций.

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

Это конкретное использование анонимных классов имеет несколько проблем:

  1. это малоизвестная идиома. Разработчики, которые этого не знают (или знают, и не часто его используют), будут замедлены при чтении и / или изменении кода, который его использует.
  2. на самом деле это неправильное использование языковой функции: вы не пытаетесь определить новый вид ArrayList, вы просто хотите получить список массивов с некоторыми существующими значениями в нем
  3. он создает новый класс, который потребляет ресурсы: место на диске для хранения определения класса, время для анализа / проверки /... it, permgen для хранения определения класса,...
  4. даже если "реальный код" немного длиннее, его можно легко перенести в метко названный служебный метод (joinLists(listOne, listTwo))

По моему мнению, № 1 является наиболее важной причиной, чтобы избежать этого, а за ним следует № 2. № 3, как правило, не так уж много проблем, но не следует забывать.

Потому что вам не нужен отдельный подкласс - вам просто нужно создать новый ArrayList нормального класса, и addAll() оба списка в него.

Вот так:

public static List<String> addLists (List<String> a, List<String> b) {
    List<String> results = new ArrayList<String>();
    results.addAll( a);
    results.addAll( b); 
    return results;
}

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

Это не плохой подход сам по себе, скажем, с точки зрения производительности или чего-то подобного, но этот подход немного неясен, и когда вы используете что-то подобное, вам всегда (скажем, 99%) приходится объяснять этот подход. Я думаю, что это одна из главных причин не использовать этот подход, а при наборе:

List<String> newList = new ArrayList<String>();
newList.addAll(listOne);
newList.addAll(listTwo);

немного больше печатать, немного легче читать, что очень помогает в понимании или отладке кода.

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

    Arrays.sort(args, new Comparator<String>() {
        public int compare(String o1, String o2) {
            return  ... 
        }});

Я бы назвал вышеизложенное примером наилучшей практики.

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