Type Checking in Java

Ozan Ağma
14 min readAug 8, 2020

--

Type checking is the process of verifying and enforcing the constraints of types, and it can occur either at compile time (i.e. statically) or at runtime i.e. dynamically). Type checking is all about ensuring that the program is type-safe, meaning that the possibility of type errors is kept to a minimum. A type error is an erroneous program behavior in which an operation occurs (or tries to occur) on a particular data type that it’s not meant to occur on.

  • Incompatible operand:
int[] x = new int[5];
int y = 6;
int z = x + y; // The operator + is undefined for the argument type(s) int, int[]
  • Variable used but not declared or defined multiple times:
int x;
int x = 5; // Duplicate local variable x
  • Type conversion(Widening, Narrowing, Autoboxing, Unboxing):
double x = 3.5;
int y = x; // Type mismatch: cannot convert from double to int
  • In java at runtime we can determine type of the object in three different ways:
x instanceof A
x.getClass().getName();
x.getClass() == A.class;
  • Some checks cannot be done statically:
String arr[]= new String[5];
.
.
.
arr[i] = “Merhaba Ozan”; // i cannot be known at compile time
  • Java is statically-typed, so it expects its variables to be declared before they can be assigned values. Although java is statically-typed, dynamic typing still exist for polymorphism.
class A {…}
class B extends A {…}
class C extends A {…}
A a = null; // Static type of a is A;
a = new B(); // Dynamic type of a is B;
a = new C(); // Dynamic type of a is C
  • Some compile time errors can be false positive:
class A{
A me(){
return this;
}
}
class B extends A{
public void doB(){...}
}
new B().me().doB(); // Error! at compile time we se me() returns A and has no doB() method. But at runtime me() returns B so calling doB() is OK.((B) new B().me()).doB() // Explicit Type Casting. Do Compiler Happy!!
  • Some type errors may not be get caught by the compiler:
class C{
public void doC() {...}
}
((C)new B().me()).doC(); // Error: Class B cannot be cast to Class C

Let’s examine above example. At compile time we see new B().me() returns A. Since C extends A, type casting is allowed. At runtime new B().me() returns B and cannot cast to C. Since C does not inherit from B.

  • Why do we need types?
  1. Provide a way to check if operations are applied meaningfully.
  2. Helps localization. (Related code is in one place.)
  3. Helps to define API.
  4. Document programmers intentions better than comments.
  5. Can be used to use optimize hardware for certain operations or use hardware more efficiently.
  • Autoboxing and Unboxing

Autoboxing is the automatic conversion that the Java compiler makes between the primitive types and their corresponding object wrapper classes. For example, converting an int to an Integer, a double to a Double, and so on. If the conversion goes the other way, this is called unboxing.

Character c = 'a'; // Compiler modifies this as below. AUTOBOXING
Character c = Character.valueOf('c');
Integer i = 5;
i++; // Compiler changes i to i.intValue() to unbox Integer to int. Because ++ do not apply to ınteger object. UNBOXING
  • Raw Types

Raw type is the name of a generic class or interface without any type arguments. The reason they exist is raw types behaves just like they were before generics were introduced.

Box b = new Box(); // Generic Type Box<T>, has no diamonds.

Raw types show up in legacy code because lots of API classes (such as the Collections classes) were not generic prior to JDK 5.0. When using raw types, you essentially get pre-generics behavior — a Box gives you Objects. For backward compatibility, assigning a parameterized type to its raw type is allowed:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox; // OK

Generic Methods

Generic methods are methods that introduce their own type parameters. This is similar to declaring a generic type, but the type parameter’s scope is limited to the method where it is declared. Static and non-static generic methods are allowed, as well as generic class constructors.

The syntax for a generic method includes a list of type parameters, inside angle brackets, which appears before the method’s return type. For static generic methods, the type parameter section must appear before the method’s return type.

The Util class includes a generic method, compare, which compares two Pair objects:

public class Util {
public static <K, V> boolean compare(K k1, K k2) {
return k2;
}
}
boolean same = Util.<Integer, String>compare(p1, p2);
// The type has been explicitly provided, as shown in bold. //Generally, this can be left out and the compiler will infer the //type that is needed:
same = Util.compare(p1, p2);

This feature, known as type inference, allows you to invoke a generic method as an ordinary method, without specifying a type between angle brackets. This topic is further discussed in the following section, Type Inference.

Bounded Type Parameters

There may be times when you want to restrict the types that can be used as type arguments in a parameterized type. For example, a method that operates on numbers might only want to accept instances of Number or its subclasses. This is what bounded type parameters are for.

To declare a bounded type parameter, list the type parameter’s name, followed by the extends keyword, followed by its upper bound, which in this example is Number. Note that, in this context, extends is used in a general sense to mean either "extends" (as in classes) or "implements" (as in interfaces).

class NaturalNumber<T extends Integer>{
T n;
public boolean isEven(){
return n.intValue() % 2 == 0;
}
}

n is a subtype of Integer. Therefore intValue() is available to call.

Generic algorithms need bounds.

public static <T> int CountGreaterThan(T[] arr, T elem){
int count = 0;
for(T e: arr){
if(e > elem) // Error: '>' operator is only defined for some primitive types.
++count;
return count;
}

Solution to above problem:

public static <T exteneds Comparable<T>> int CountGreaterThan(T[] arr, T elem){
int count = 0;
for(T e: arr){
if(e.compareTo(elem) == 1) // Error: '>' operator is only defined for some primitive types.
++count;
return count;
}

Now compareTo() is available because T extends Comparable class.

  • Multiple bounds

The preceding example illustrates the use of a type parameter with a single bound, but a type parameter can have multiple bounds:

<T extends B1 & B2 & B3>

A type variable with multiple bounds is a subtype of all the types listed in the bound. If one of the bounds is a class, it must be specified first. For example:

Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }

If bound A is not specified first, you get a compile-time error:

class D <T extends B & A & C> { /* ... */ }  // compile-time error

Generics, Inheritance, and Subtypes

In Java it is possible to assign an object of one type to an object of another type provided that the types are compatible. For example, you can assign an Integer to an Object, since Object is one of Integer’s supertypes:

Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger; // OK

In object-oriented terminology, this is called an “is a” relationship. Since an Integer is a kind of Object, the assignment is allowed. But Integer is also a kind of Number, so the following code is valid as well:

public void someMethod(Number n) { /* ... */ }someMethod(new Integer(10));   // OK
someMethod(new Double(10.1)); // OK

The same is also true with generics. You can perform a generic type invocation, passing Number as its type argument, and any subsequent invocation of add will be allowed if the argument is compatible with Number:

Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK

Now consider the following method:

public void boxTest(Box<Number> n) { /* ... */ }

What type of argument does it accept? By looking at its signature, you can see that it accepts a single argument whose type is Box<Number>. But what does that mean? Are you allowed to pass in Box<Integer> or Box<Double>, as you might expect? The answer is “no”, because Box<Integer> and Box<Double> are not subtypes of Box<Number>.

Box<Integer> is not a subtype of Box<Number> even though Integer is a subtype of Number.

You can subtype a generic class or interface by extending or implementing it. The relationship between the type parameters of one class or interface and the type parameters of another are determined by the extends and implements clauses.

Using the Collections classes as an example, ArrayList<E> implements List<E>, and List<E> extends Collection<E>. So ArrayList<String> is a subtype of List<String>, which is a subtype of Collection<String>. So long as you do not vary the type argument, the subtyping relationship is preserved between the types.

A sample Collections hierarchy

Type Inference

Java compilers ability to look at each methods invocation and corresponding declaration to determine the type of arguments that made the invocation applicable.

Box<Integer> integerBox = new Box<>() // Compiler fill the RHS diamond with Integer automatically.

Inference algorithms determines the type of arguments and the type that the result is being assigned or returned. Inference algorithms tries to find most specific type that works with all of the arguments.

static<T> T pick(T a, T b) { return b;}
Serializable s = pick(“d”, new ArrayList<String>());

Options for above example are String, ArrayList, Serializable . T is infereed to Serializable because it is the most specific type that works with other arguments.

  • Target Types

The Java compiler takes advantage of target typing to infer the type parameters of a generic method invocation. The target type of an expression is the data type that the Java compiler expects depending on where the expression appears. Consider the method Collections.emptyList, which is declared as follows:

static <T> List<T> emptyList();
List<String> listOne = Collections.emptyList();

This statement is expecting an instance of List<String>; this data type is the target type. Because the method emptyList returns a value of type List<T>, the compiler infers that the type argument T must be the value String. This works in after Java SE 7. Alternatively, you could use a type witness and specify the value of T as follows:

List<String> listOne = Collections.<String>emptyList();

Wildcards

In generic code, the question mark (?), called the wildcard, represents an unknown type. The wildcard can be used in a variety of situations: as the type of a parameter, field, or local variable; sometimes as a return type (though it is better programming practice to be more specific). The wildcard is never used as a type argument for a generic method invocation, a generic class instance creation, or a super type.

The following sections discuss wildcards in more detail, including upper bounded wildcards, lower bounded wildcards, and wildcard capture.

Upper Bounded Wildcards

You can use an upper bounded wildcard to relax the restrictions on a variable. For example, say you want to write a method that works on List<Integer>, List<Double>, and List<Number>; you can achieve this by using an upper bounded wildcard.

To declare an upper-bounded wildcard, use the wildcard character (‘?’), followed by the extends keyword, followed by its upper bound. Note that, in this context, extends is used in a general sense to mean either “extends” (as in classes) or “implements” (as in interfaces).

To write the method that works on lists of Number and the subtypes of Number, such as Integer, Double, and Float, you would specify List<? extends Number>. The term List<Number> is more restrictive than List<? extends Number> because the former matches a list of type Number only, whereas the latter matches a list of type Number or any of its subclasses.

Consider the following process method:

public static void process(List<? extends Foo> list) { /* ... */ }

The upper bounded wildcard, <? extends Foo>, where Foo is any type, matches Foo and any subtype of Foo. The process method can access the list elements as type Foo.

Unbounded Wildcards

The unbounded wildcard type is specified using the wildcard character (?), for example, List<?>. This is called a list of unknown type. There are two scenarios where an unbounded wildcard is a useful approach:

  • If you are writing a method that can be implemented using functionality provided in the Object class.
  • When the code is using methods in the generic class that don’t depend on the type parameter. For example, List.size or List.clear. In fact, Class<?> is so often used because most of the methods in Class<T> do not depend on T.

Consider the following method, printList:

public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}

The goal of printList is to print a list of any type, but it fails to achieve that goal — it prints only a list of Object instances; it cannot print List<Integer>, List<String>, List<Double>, and so on, because they are not subtypes of List<Object>. To write a generic printList method, use List<?>:

public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}

Because for any concrete type A, List<A> is a subtype of List<?>, you can use printList to print a list of any type:

List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);

Lower Bounded Wildcards

The Upper Bounded Wildcards section shows that an upper bounded wildcard restricts the unknown type to be a specific type or a subtype of that type and is represented using the extends keyword. In a similar way, a lower bounded wildcard restricts the unknown type to be a specific type or a super type of that type.

A lower bounded wildcard is expressed using the wildcard character (‘?’), following by the super keyword, followed by its lower bound: <? super A>.

Note: You can specify an upper bound for a wildcard, or you can specify a lower bound, but you cannot specify both.

Say you want to write a method that puts Integer objects into a list. To maximize flexibility, you would like the method to work on List<Integer>, List<Number>, and List<Object> — anything that can hold Integer values.

To write the method that works on lists of Integer and the supertypes of Integer, such as Integer, Number, and Object, you would specify List<? super Integer>. The term List<Integer> is more restrictive than List<? super Integer> because the former matches a list of type Integer only, whereas the latter matches a list of any type that is a supertype of Integer.

The following code adds the numbers 1 through 10 to the end of a list:

public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}

Wildcards and Subtyping

As described in Generics, Inheritance, and Subtypes, generic classes or interfaces are not related merely because there is a relationship between their types. However, you can use wildcards to create a relationship between generic classes or interfaces.

Given the following two regular (non-generic) classes:

class A { /* ... */ }
class B extends A { /* ... */ }

It would be reasonable to write the following code:

B b = new B();
A a = b;

This example shows that inheritance of regular classes follows this rule of subtyping: class B is a subtype of class A if B extends A. This rule does not apply to generic types:

List<B> lb = new ArrayList<>();
List<A> la = lb; // compile-time error

Given that Integer is a subtype of Number, what is the relationship between List<Integer> and List<Number>?

The common parent is List<?>.

Although Integer is a subtype of Number, List<Integer> is not a subtype of List<Number> and, in fact, these two types are not related. The common parent of List<Number> and List<Integer> is List<?>.

In order to create a relationship between these classes so that the code can access Number’s methods through List<Integer>’s elements, use an upper bounded wildcard:

List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // OK. List<? extends Integer> is a subtype of List<? extends Number>

Because Integer is a subtype of Number, and numList is a list of Number objects, a relationship now exists between intList (a list of Integer objects) and numList. The following diagram shows the relationships between several List classes declared with both upper and lower bounded wildcards.

A hierarchy of several generic List class declarations.

Wildcard Capture and Helper Methods

In some cases, the compiler infers the type of a wildcard. For example, a list may be defined as List<?> but, when evaluating an expression, the compiler infers a particular type from the code. This scenario is known as wildcard capture.

For the most part, you don’t need to worry about wildcard capture, except when you see an error message that contains the phrase “capture of”.

The WildcardError example produces a capture error when compiled:

import java.util.List;public class WildcardError {    void foo(List<?> i) {
i.set(0, i.get(0));
}
}

In this example, the compiler processes the i input parameter as being of type Object. When the foo method invokes List.set(int, E), the compiler is not able to confirm the type of object that is being inserted into the list, and an error is produced. When this type of error occurs it typically means that the compiler believes that you are assigning the wrong type to a variable. Generics were added to the Java language for this reason — to enforce type safety at compile time.

In this example, the code is attempting to perform a safe operation, so how can you work around the compiler error? You can fix it by writing a private helper method which captures the wildcard. In this case, you can work around the problem by creating the private helper method, fooHelper, as shown in WildcardFixed:

public class WildcardFixed {    void foo(List<?> i) {
fooHelper(i);
}
// Helper method created so that the wildcard can be captured
// through type inference.
private <T> void fooHelper(List<T> l) {
l.set(0, l.get(0));
}
}

Guidelines for Wildcard Use

One of the more confusing aspects when learning to program with generics is determining when to use an upper bounded wildcard and when to use a lower bounded wildcard. This page provides some guidelines to follow when designing your code.

For purposes of this discussion, it is helpful to think of variables as providing one of two functions:

An “in” variable serves up data to the code. Imagine a copy method with two arguments: copy(src, dest). The src argument provides the data to be copied, so it is the “in” parameter.

An “out” variable holds data for use elsewhere. In the copy example, copy(src, dest), the dest argument accepts data, so it is the “out” parameter.

Of course, some variables are used both for “in” and “out” purposes — this scenario is also addressed in the guidelines.

You can use the “in” and “out” principle when deciding whether to use a wildcard and what type of wildcard is appropriate. The following list provides the guidelines to follow:

Wildcard Guidelines:

  • An “in” variable is defined with an upper bounded wildcard, using the extends keyword.
  • An “out” variable is defined with a lower bounded wildcard, using the super keyword.
  • In the case where the “in” variable can be accessed using methods defined in the Object class, use an unbounded wildcard.
  • In the case where the code needs to access the variable as both an “in” and an “out” variable, do not use a wildcard.

Type Erasure

Generics were introduced to the Java language to provide tighter type checks at compile time and to support generic programming. To implement generics, the Java compiler applies type erasure to:

  • Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.
  • Insert type casts if necessary to preserve type safety.
  • Generate bridge methods to preserve polymorphism in extended generic types.

Type erasure ensures that no new classes are created for parameterized types; consequently, generics incur no runtime overhead.

--

--

Ozan Ağma
Ozan Ağma

No responses yet