2

I'm trying to read a file to an ArrayList which has a unknown type declared via the wildcard character '?'. After using ObjectInputStream.readObject() to acquire a deserialized Object I then cast that Object as an ArrayList<?>. Then I attempt to add the elements of my casted ArrayList<?> into another ArrayList<?> via the ArrayList.addAll(Collection) method. However, my attempt to call list.addAll(buffer) fails with this exception message:

No suitable method found for addAll(ArrayList<CAP#1>)

Why am I unable to add the elements of an ArrayList<?> into another instance of an ArrayList<?> via the addAll() method?


Here is the method that produces this exception:

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.");
    }
}
Emily Mabrey
  • 1,468
  • 1
  • 12
  • 27
  • 2
    If you don't know the type, but want to add to the list, it has to be `ArrayList`. – Andy Turner May 01 '18 at 22:02
  • Possible duplicate of [Java Generics (Wildcards)](https://stackoverflow.com/questions/252055/java-generics-wildcards) – VeeArr May 01 '18 at 22:04
  • You may want to read https://stackoverflow.com/questions/2723397/what-is-pecs-producer-extends-consumer-super – luk2302 May 01 '18 at 22:04
  • 1
    See [What is PECS (Producer Extends Consumer Super)?](https://stackoverflow.com/q/2723397/5221149) – Andreas May 01 '18 at 22:05

3 Answers3

1
  1. If the type are correlated : To be able to add a List to another, the first elements have to be same type of sub-type of the main List, you need :

    • to add <T> in the signature to tell that a generic is used
    • to use it in ArrayList<T> for the main one
    • to ArrayList<? extends T> for the sublist

    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. If type are random, change to

    ArrayList<Object> list
    
azro
  • 35,213
  • 7
  • 25
  • 55
  • This isn't safe. You can call `readFile` with an `ArrayList`, or an `ArrayList`, and the same filename: at least one of those is going to fail (unless `ois.readObject()` returns an empty list, of course). – Andy Turner May 01 '18 at 22:06
  • This answer should be edited to include type safety by use of `ObjectInputFilter` alongside `ObjectInputStream.setObjectInputFilter​(ObjectInputFilter)`. Then simply require callers to provide you either a single `Class` or `Class[]` instance, and use that to populate the filter's class name `String`s. Using these methods will cause the `ObjectInputStream` to throw an `InvalidClassException` if the filter returns `Status.REJECTED` upon reading a serialized object from the file. – Emily Mabrey May 01 '18 at 22:26
  • Actually I will provide an alternative answer myself with a better method than `ObjectInputFilter` for this specific use case (we are only trying to deserialize a file containing one `Object` who can only validly be castable to one type, which we can simply check without `ObjectInputFilter`. – Emily Mabrey May 01 '18 at 22:41
-1

Generics or: How I Learned to Start Worrying and Hate Type Erasure

Your JVM is trying to determine what you meant when you gave it a certain series of bytecodes/literals via the compiled source code. "Normally" everything is nicely declared using concrete class types like String or concrete primitive types such as int. For example, a value of 6 comes with some metadata that tells the JVM it is an int literal and not some other type. A series of bytes is meaningless on it's own; you need a way to tell the JVM what the binary numbers actually encode. The same binary value could represent an infinite number of ideas or states, so it is important that the type metadata is included to help make sense of it all. Without types the JVM wouldn't be able to tell a char with a value of 'a' and a byte with a value of 0x61 apart. When you give a generic type, like the kinds used by implementations of the List interface (like the ArrayList you used) you are telling the compiler "this isn't a specific concrete type, it could be anything" (though using extends you can limit things a bit). Now, normally that might not be a problem, but due to backwards-compatibility issues I won't get into, dynamic types get erased away in a process called type erasure (look at this answer for a very detailed explanation of the reasoning). The short and sweet answer which solves your problem is to simply take the extra complications of generic types into account and actively check and prevent casts which are not allowed. Thus type safety can mostly be achieved by checking generic type assignability manually instead of relying on automatic type safety like one might when using concrete types.

Example Method

Here is a relatively type-safe version of your code(you will get a runtime error message in the worst case misuse scenario) with null checking and generic type bounding ( ObjectInputStream and ObjectOutputStream use methods from the Serializable interface to implement their reading and writing of Object instances to the streams).

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);
}

More Details

These methods read serialized objects from a file with the specifics of type safety and file location specified via method parameters. The return value of the top-level method is actually a boolean indicating success or failure (true is success), but the actual data output is written into the Collection instance provided by the callee. Unlike a non-generic method, which can explicitly specify casts safely using the familiar a = (a) b; syntax, generic methods with generic types (like DESIRED,STORED,INPUT and OUTPUT) can't be sure exactly what type they will be handling. They lose this information about types through a process called type erasure.

Through type erasure developers and the JVM run-time lose the metadata about concrete types for types which are generified. As a result, the run-time and the developer are at the mercy of an input type being assignable to the required output type. Only the compiler could even possibly have provability for casts and it is not able to verify generic types in a general sense like it can concrete types. It is often the case that there is no type safety check possible until run-time. A List<?> at run-time might actually have been compiled and programmed as List<String> , but we can't determine the difference between two lists before run-time because the compiler is unable to maintain or determine the information we need to do so.

In addition to type erasure causing loss of type metadata, the compiler may also have incomplete knowledge of the program's run-time type graph. The potential for things like linking introducing new types that our current compiler doesn't recognize means we can never avoid unsafe casts on generic types with completely algorithmic approaches. In fact, there is no way to satisfy this type safety problem for generic types in a generalizable way as it amounts to a boolean satisfiability problem. However, there is somewhere else we can get the specific information on what types the caller of the method wanted and provided... the developer! Developers must specify explicit intent for generic types in order to provide type safety. By making the method caller (ultimately the developer) responsible for telling the run-time what to expect via passing a Class<T> instance to the code/method we can attempt to check generic types in such a way that we avoid a ClassCastException.

If we have a List<T> it simply becomes a List so we can no longer identify as differently typed from any other List. However, this isn't true for Class<T>, which we can exploit to pass the type metadata around the compiler and into the run-time (because Class objects store type information within the object graph instead of exclusively using Java's type system). It is, however, ultimately making the programmer responsible for providing the correct Class<T> for each method call. By making the run-time check types for validity the compiler's ability to detect type safety errors during compilation is mostly sacrificed for expressions utilizing generic types. You will no longer get warnings like "int cannot be cast as String" when you compile, instead you will just get an application exception or a cast safety verification failure that must be handled appropriately at run-time.

If you would like to play around with things and see what happens when you use this method I have created a gist on GitHub containing a short little single file Class with a main method you can run as a little test program.

Emily Mabrey
  • 1,468
  • 1
  • 12
  • 27
-2

If you don't know the type, but want to add to the list type-safely, it has to be ArrayList<Object>.

This is the only type which it is safe to add anything to. (I suppose it could technically be ArrayList<? super Object>; but there's nothing lower bounded by Object except itself).

Otherwise, you could call readFile with an ArrayList<Integer>, and then try to add, say, Strings that you read from the ObjectInputStream. That would fail later, when you try to get an Integer out of the list, but the element is actually a String.

You can't use ArrayList<T> safely, for exactly the same reason: a method with this signature:

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

can be called like:

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

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

Unless filename contains an empty list, at least one of these lists isn't type correct; thus, the method is not type safe. But it is insidiously type unsafe: this code would execute OK, but code (perhaps far away) would fail with a ClassCastException when it tries to retrieve an object from the list, and finds it is of the wrong type.

Andy Turner
  • 122,430
  • 10
  • 138
  • 216