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.