Unit 25: Unchecked Warnings
Learning Objectives
Students should
- be aware of how to use generics with an array.
- be aware of unchecked warnings that compilers can give when we are using generics.
- be able to make arguments why a piece of code is type-safe for simple cases.
- know how to suppress warnings from compilers.
- be aware of the ethics when using the @SuppressWarnings("unchecked") annotation.
- know what is a raw type.
- be aware that raw types should never never be used in modern Java.
Creating Arrays with Type Parameters
We have seen how arrays and generics do not mix well. One way to get around this is to use Java Collections, a library of data structures provided by Java, instead of arrays, to store our items. The ArrayList
class
provides similar functionality to an array, with some performance overhead.
1 2 3 4 5 6 |
|
ArrayList
itself is a generic class, and when parameterized, it ensures type-safety by checking for appropriate types during compile time. We can't add a Pair<Double,Boolean>
object to a list of Pair<String,Integer>
. Furthermore, unlike Java array, which is covariant, generics are invariant. There is no subtyping relationship between ArrayList<Object>
and ArrayList<Pair<String,Integer>>
so we can't alias one with another, preventing the possibility of heap pollution.
Using ArrayList
instead of arrays only gets around the problem of mixing arrays and generics, as a user. ArrayList
is implemented with an array internally after all. As computing students, especially computer science students, it is important to know how to implement your own data structures instead of using ones provided by Java or other libraries.
Let's try to build one:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
This generic class is a wrapper around an array of type T
. Recall that we cannot use new T[]
directly. On Line 6, to get around this restriction, we new
an Object
array instead, and cast it to an array of T[]
instead.
The code now compiles. Why would we expect this to compile at all? Think about what the type erased code would look like. So there is at least some reason why we would expect the code to work.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Unfortunately, although the code compiles, we receive the following message:
1 2 3 |
|
Let's do what the compiler tells us, and compile with the -Xlint:unchecked
flags.
1 2 3 4 5 6 7 8 9 |
|
We get a warning that our Line 6 is doing an unchecked cast.
Unchecked Warnings
An unchecked warning is basically a message from the compiler that it has done what it can, and because of type erasures, there could be a run-time error that it cannot prevent. Recall that type erasure generates the following code:
1 |
|
Since array
is an array of Object
instances and Java array is covariant, the compiler can't guarantee that the code it generated is safe anymore.
Consider the following:
1 2 3 4 |
|
The last line would generate a ClassCastException
, exactly a scenario that the compiler has warned us.
It is now up to us humans to change our code so that the code is safe. Suppose we remove the getArray
method from the Array
class,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Can we prove that our code is type-safe? In this case, yes. Since array
is declared as private
, the only way someone can put something into the array
is through the Array::set
method1. Array::set
only put items of type T
into array
. So the only type of objects we can get out of array
must be of type T
. So we, as humans, can see that casting Object[]
to T[]
is type-safe.
If we are sure (and only if we are sure) that the line
1 |
|
is safe, we can thank the compiler for its warning and assure the compiler that everything is going to be fine. We can do so with the @SuppressWarning("unchecked")
annotation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
@SuppressWarning
is a powerful annotation that suppresses warning messages from compilers. Like everything that is powerful, we have the responsibility to use it properly:
@SuppressWarning
can apply to declaration at a different scope: a local variable, a method, a type, etc. We must always use@SuppressWarning
to the most limited scope to avoid unintentionally suppressing warnings that are valid concerns from the compiler.- We must suppress a warning only if we are sure that it will not cause a type error later.
- We must always add a note (as a comment) to fellow programmers explaining why a warning can be safely suppressed.
Note that since @SuppressWarnings
cannot apply to an assignment but only to declaration, we declare a local variable a
in the example above before assigning this.array
to a
.
Raw Types
Another common scenario where we can get an unchecked warning is the use of raw types. A raw type is a generic type used without type arguments. Suppose we do:
1 |
|
The code would compile perfectly. We are using the generic Array<T>
as a raw type Array
. Java allows this code to compile for backward compatibility. This is anyway what the code looks like after type erasure and how we would write the code in Java before version 5. Without a type argument, the compiler can't do any type checking at all. We are back to the uncertainty that our code could bomb with ClassCastException
after it ships.
Mixing raw types with paramterized types can also lead to errors. Consider:
1 2 3 |
|
where the method populateArray
uses raw types:
1 2 3 |
|
Since we use raw types in this method, the compiler can't help us. It will warn us:
1 2 3 4 5 6 |
|
If we ignore this warning or worse, suppress this warning, we will get a run-time error when we execute a.get(0)
.
Raw types must not be used in your code, ever. The only exception to this rule is using it as an operand of the instanceof
operator. Since instanceof
checks for run-time type and type arguments have been erased, we can only use the instanceof
operator on raw types.
-
Another win for information hiding! ↩