Почему я не могу добавить содержимое ArrayList в другой ArrayList с помощью метода addAll, когда оба ArrayLists имеют общий тип? - PullRequest
0 голосов
/ 02 мая 2018

Я пытаюсь прочитать файл на ArrayList с неизвестным типом, объявленным с помощью символа подстановки '?'. После использования ObjectInputStream.readObject() для получения десериализованного Object я затем разыгрываю это Object как ArrayList<?>. Затем я пытаюсь добавить элементы моего приведенного ArrayList<?> в другой ArrayList<?> с помощью метода ArrayList.addAll(Collection). Однако моя попытка вызвать list.addAll(buffer) не удалась с этим сообщением об исключении:

Не найдено подходящего метода для addAll(ArrayList<CAP#1>)

Почему я не могу добавить элементы ArrayList<?> в другой экземпляр ArrayList<?> с помощью метода addAll()?


Вот метод, который создает это исключение:

public void readFile(ArrayList<?> list, String fileName) throws Exception
{
    FileInputStream fis = new FileInputStream(fileName);
    try (ObjectInputStream ois = new ObjectInputStream(fis))
    {
        ArrayList<?> buffer = (ArrayList<?>) ois.readObject();
        list.addAll(buffer);
        System.out.println("Added  to the customer list.");
    }
}

Ответы [ 3 ]

0 голосов
/ 02 мая 2018

Если вы не знаете тип, но хотите безопасно добавить в список тип, это должно быть ArrayList<Object>.

Это единственный тип, к которому можно добавить что-либо. (Полагаю, технически это может быть ArrayList<? super Object>; но ничто не ограничено Object, кроме себя самого).

В противном случае вы могли бы позвонить readFile с ArrayList<Integer>, а затем попытаться добавить, скажем, String s, которые вы прочитали из ObjectInputStream. Это может произойти позже, когда вы попытаетесь получить Integer из списка, но на самом деле это элемент String.

Вы не можете безопасно использовать ArrayList<T>, по той же причине: метод с такой подписью:

public <T> void readFile(ArrayList<T> list, String fileName) throws Exception

можно назвать как:

ArrayList<String> strings = new ArrayList<>();
readFile(strings, "filename");

ArrayList<Integer> integers = new ArrayList<>();
readFile(integers, "filename");  // <-- Note the same filename

Если filename не содержит пустой список, по крайней мере один из этих списков неверен по типу; таким образом, метод не является безопасным типом. Но это коварно небезопасный тип: этот код будет выполняться нормально, но код (возможно, далеко) потерпит неудачу с ClassCastException, когда попытается извлечь объект из списка и обнаружит, что он имеет неправильный тип.

0 голосов
/ 02 мая 2018

Дженерики или: Как я научился беспокоиться и стирать ненависть

Ваша JVM пытается определить, что вы имели в виду, когда вы дали ей определенную серию байт-кодов / литералов через скомпилированный исходный код. «Обычно» все красиво объявляется с использованием конкретных типов классов, таких как String или конкретных примитивных типов, таких как int. Например, значение 6 поставляется с некоторыми метаданными, которые сообщают JVM, что это литерал int, а не какой-то другой тип. Ряд байтов сам по себе не имеет смысла; вам нужен способ сообщить JVM, что именно кодируют двоичные числа. Одно и то же двоичное значение может представлять бесконечное число идей или состояний, поэтому важно, чтобы метаданные типа были включены, чтобы помочь разобраться во всем этом. Без типов JVM не сможет определить char со значением 'a' и байт со значением 0x61 друг от друга. Когда вы задаете универсальный тип, такой как типы, используемые реализациями интерфейса List (например, используемый вами ArrayList), вы говорите компилятору: «Это не конкретный конкретный тип, это может быть что угодно» (хотя используя extends вы можете немного ограничить вещи). Обычно это может не быть проблемой, но из-за проблем обратной совместимости, с которыми я не буду сталкиваться, динамические типы стираются в процессе, называемом стиранием типов (очень подробный взгляд на этот ответ ). объяснение рассуждений). Короткий и приятный ответ, который решает вашу проблему, состоит в том, чтобы просто учесть дополнительные сложности родовых типов и активно проверять и предотвращать приведение, которые не допускаются. Таким образом, безопасность типов может быть достигнута, главным образом, путем проверки присваиваемости общих типов вручную, вместо того чтобы полагаться на автоматическую безопасность типов, как при использовании конкретных типов.

Пример метода

Это относительно безопасная для вашего типа версия вашего кода (вы получите сообщение об ошибке во время выполнения в худшем случае неправильного использования) с нулевой проверкой и общим ограничением типов (ObjectInputStream и ObjectOutputStream используют методы из Serializable интерфейс для реализации их чтения и записи Object экземпляров в потоки).

public static <OUTPUT extends Serializable, INPUT extends Serializable>
    OUTPUT getObjectFromFile(Class<INPUT> type, CharSequence fileName)
    throws IOException, ClassNotFoundException {

    final String filenameString = Objects.requireNonNull(fileName).toString();
    final Class<INPUT> inputType = Objects.requireNonNull(type);

    try (ObjectInput ois = new ObjectInputStream(new FileInputStream(filenameString))) {
        final Object rawObject = ois.readObject();

        @SuppressWarnings("unchecked")
        final OUTPUT output = (OUTPUT) (rawObject == null ? null : inputType.cast(rawObject));

        return output;
    }

}

public static <DESIRED extends Serializable, STORED extends Serializable>
    boolean addArrayListFromFile(Collection<DESIRED> out, Class<STORED> type, CharSequence fileName)
    throws IOException, ClassNotFoundException {

    @SuppressWarnings("unchecked")
    final ArrayList<DESIRED> inList = getObjectFromFile(type, fileName);

    return inList != null && out.addAll(inList);
}

Подробнее

Эти методы читают сериализованные объекты из файла со спецификацией безопасности типа и местоположения файла, заданной через параметры метода. Возвращаемое значение метода верхнего уровня на самом деле является boolean, указывающим на успех или неудачу (true - это успех), но фактический вывод данных записывается в экземпляр Collection, предоставленный вызываемым объектом. В отличие от неуниверсального метода, который может явно указывать приведение типов безопасно с использованием знакомого синтаксиса a = (a) b;, универсальные методы с универсальными типами (такими как DESIRED, STORED, INPUT и OUTPUT) не могут быть точно уверены какой тип они будут обрабатывать. Они теряют эту информацию о типах через процесс, называемый стиранием типов.

Через разработчиков стирания типов и среду выполнения JVM теряются метаданные о конкретных типах для типов, которые генерируются. В результате среда выполнения и разработчик зависят от типа ввода, назначаемого для требуемого типа вывода. Только компилятор может даже иметь доказуемость для приведений, и он не может проверять универсальные типы в общем смысле, как это может делать конкретные типы. Часто бывает так, что до времени выполнения проверка безопасности типов невозможна. List<?> во время выполнения мог бы фактически быть скомпилирован и запрограммирован как List<String>, но мы не можем определить разницу между двумя списками до выполнения, потому что компилятор не может поддерживать или определять информацию, которую нам нужно сделать поэтому.

В дополнение к стиранию типа, вызывающему потерю метаданных типа, компилятор может также не полностью знать график типов программы во время выполнения. Потенциал для таких вещей, как связывание и введение новых типов, которые наш текущий компилятор не распознает, означает, что мы никогда не сможем избежать небезопасных приведений к универсальным типам с полностью алгоритмическими подходами. Фактически, нет никакого способа удовлетворить эту проблему безопасности типов для обобщенных типов в обобщенном виде, поскольку это составляет проблему булевой выполнимости. Однако, есть где-то еще, мы можем получить конкретную информацию о том, какие типы вызывал вызывающий метод и предоставлял ... разработчику! Разработчики должны указать явное намерение для универсальных типов, чтобы обеспечить безопасность типов. Сделав так, чтобы метод, вызывающий метод (в конечном счете, разработчик), отвечал за то, чтобы сообщить среде выполнения, что ожидать при передаче экземпляра Class<T> в код / ​​метод, мы можем попытаться проверить универсальные типы таким образом, чтобы избежать ClassCastException .

Если у нас есть List<T>, он просто становится List, поэтому мы больше не можем идентифицировать его как отличающийся от любого другого List. Однако это не так для Class<T>, который мы можем использовать для передачи метаданных типа вокруг компилятора и во время выполнения (поскольку объекты Class хранят информацию о типе в графе объектов вместо того, чтобы использовать исключительно систему типов Java ). Однако в конечном итоге программист несет ответственность за предоставление правильного Class<T> для каждого вызова метода. Выполняя проверку типов во время выполнения на достоверность, способность компилятора обнаруживать ошибки безопасности типов во время компиляции в основном приносится в жертву для выражений, использующих универсальные типы. Вы больше не будете получать предупреждения, такие как «int нельзя преобразовать как String» при компиляции, вместо этого вы просто получите исключение приложения или ошибку проверки безопасности приведения, которая должна быть надлежащим образом обработана во время выполнения.

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

0 голосов
/ 02 мая 2018
  1. Если type взаимосвязаны: Чтобы иметь возможность добавить List к другому, первые элементы должны быть того же типа, что и подтип основного List, вам необходимо:

    • добавить <T> в подписи, чтобы сказать, что используется универсальный
    • чтобы использовать его в ArrayList<T> для основного
    • до ArrayList<? extends T> для подсписка

    public <T> void readFile(ArrayList<T> list, String fileName) throws Exception {
        FileInputStream fis = new FileInputStream(fileName);
        try (ObjectInputStream ois = new ObjectInputStream(fis)) {
            ArrayList<? extends T> buffer = (ArrayList<? extends T>) ois.readObject();
            list.addAll(buffer);
            System.out.println("Added  to the customer list.");
        }
    }
    

  1. Если тип случайный, измените на

    ArrayList<Object> list
    
...