Generic programming in Java

from Wikipedia, the free encyclopedia

Generic programming in Java hasbeen made possibleby so-called generics since Java 1.5. The term is synonymous with “parameterized types”. The idea behind this is to introduce additional variables for types. These type variables represent unknown types at the time of implementation. These type variables are only replaced by specific types when the classes, interfaces and methods are used. In this way, type-safe programming can usually be guaranteed. But not always.

The concept

From version 5.0 ("Tiger", published in 2004), the Java programming language also provides generics, a syntactic means for generic programming . This allows classes and methods (methods also independent of their classes) to be parameterized with types . This opens up some similar possibilities for the language that are comparable with the templates in C ++ .

In principle, however, there are significant differences. While the type parameter is parameterized in Java via the interface , in C ++ the type parameter itself is parameterized directly . The source code of a C ++ template must be available to the user (i.e. when the type parameter is inserted), while a generic Java type can also be published as a translated bytecode. The compiler produces duplicated target code for various specifically used type parameters.

For example, the function std :: sort in C ++ offers the possibility of sorting all containers that offer certain methods (here specifically begin () and end (), which each return an iterator) and whose type parameters implement the 'operator <' (or another comparison function was explicitly specified). A disadvantage of this system is that it is more difficult (for the programmer!) To translate. The compiler has no other option than to replace the type parameter in each case with the required specific type and to recompile the entire code.

In the case of inappropriate type parameters and other problems, complicated and incomprehensible compiler messages can very easily arise, which is simply related to the fact that the specific requirements for the type parameters are unknown. Working with C ++ templates therefore requires complete documentation of the requirements for a type parameter. With template metaprogramming , most requirements (base class, availability of methods, copiability, assignability, etc.) can also be queried in special constructs, which results in more readable error messages. Although they conform to the standards, these constructs are not supported by all compilers .

In contrast, the generic classes and methods in Java are aware of the constraints on their own type parameters. To sort a collection (without a comparator), the elements it contains must be of the Comparable type, i.e. have implemented this interface. The compiler only has to check whether the type parameter is a subtype of Comparable and can thus ensure that the code is correct (i.e. the required method compareTo is available). Furthermore, one and the same code is used for all specific types and is not duplicated every time.

Useful examples

A program uses one to store ArrayLista list of JButtons.

So far the ArrayList was fixed on the type Object :

List list = new ArrayList();
list.add(new JButton("Button 1"));
list.add(new JButton("Button 2"));
list.add(new JButton("Button 3"));
list.add(new JButton("Button 4"));
list.add(new JButton("Button 5"));

for (int i = 0; i < list.size(); i++) {
    JButton button = (JButton) list.get(i);
    button.setBackground(Color.white);
}

Note the necessary explicit type conversion (also called "cast") and the type uncertainty associated with it. You could inadvertently ArrayListstore an object in the that is not an instance of the class JButton. The information about the exact type is when inserted into the list lost, so the compiler can not prevent at runtime when explicit type conversion of JButtonone ClassCastExceptionoccurs.

With generic types, you can do the following in Java:

List<JButton> list = new ArrayList<JButton>();
list.add(new JButton("Button 1"));
list.add(new JButton("Button 2"));
list.add(new JButton("Button 3"));
list.add(new JButton("Button 4"));
list.add(new JButton("Button 5"));

for (int i = 0; i < list.size(); i++)
    list.get(i).setBackground(Color.white);

An explicit type conversion is no longer necessary when reading out, when saving it is only possible to store JButtons in the ArrayList list .

As of Java7, the instantiation of generic types has been simplified. The first line in the above example can be written as follows since Java 7:

List<JButton> list = new ArrayList<>();

The above example can be summarized by combining generic types with the extended for loops:

List<JButton> list = new ArrayList<>();
list.add(new JButton("Button 1"));
list.add(new JButton("Button 2"));
list.add(new JButton("Button 3"));
list.add(new JButton("Button 4"));
list.add(new JButton("Button 5"));

for (JButton b: list)
    b.setBackground(Color.white);

The following sample code provides an example of a generic class that contains two objects of any type but of the same type:

public class DoubleObject<T> {
    private T object1;
    private T object2;

    public DoubleObject(T object1, T object2) {
        this.object1 = object1;
        this.object2 = object2;
    }

    public String toString() {
        return this.object1 + ", " + this.object2;
    }

    public static void main(String[] args) {
        DoubleObject<String> s = new DoubleObject<>("abc", "def");
        DoubleObject<Integer> i = new DoubleObject<>(123, 456);
        System.out.println("DoubleObject<String> s=" + s.toString());
        System.out.println("DoubleObject<Integer> i=" + i.toString());
    }
}

Cases of variance

The following cases of variance can be distinguished in Java. They each offer completely independent flexibility when dealing with generic types and are each absolutely statically type-safe.

Invariance

In the case of invariance, the type parameter is unique. In this way, invariance offers the greatest possible freedom when using the type parameter. For example, all actions are allowed for the elements of an ArrayList <Integer> that are also allowed when using a single integer directly (including autoboxing ). Example:

List<Integer> list = new ArrayList<Integer>();
// ...
Integer x = list.get(index);
list.get(index).methodeVonInteger();
list.set(index, 98347); // Autoboxing, entspricht Integer.valueOf(98347)
int y = list.get(index); // Auto-Unboxing

These possibilities are bought with little flexibility in the assignment of objects of the generic class itself. For example, the following is not allowed:

List<Number> list = new ArrayList<Integer>();

even though Integer is derived from Number. The reason is that the compiler can no longer ensure that no type errors occur. We have had bad experiences with arrays that allow such an assignment:

// OK, Integer[] ist abgeleitet von Number[]
Number[] array = new Integer[10];

// ArrayStoreException zur Laufzeit: Double -> Integer sind nicht
// zuweisungskompatibel
array[0] = new Double(5.0);

Covariance

Arrays are called covariant , which means:

From T extends V follows: T [] extends V []

or more generally:

From T extends V follows: GenericType <T> extends GenericType <V>

The array type behaves in the same way as the type parameter with regard to the inheritance hierarchy. Covariance is also possible with generic types, but only with restrictions so that type errors can be excluded at compile time.

References need with the syntax ? extends T must be explicitly marked as covariant. T is called upper typebound , which is the most general type parameter that is allowed.

List<? extends Number> list;
list = new ArrayList<Double>();
list = new ArrayList<Long>();
list = new ArrayList<Integer>();

// Typfehler vom Compiler
list.set(index, myInteger);

// OK aber Warnung vom Compiler: unchecked cast
((List<Integer>) list).set(index, myInteger);

It is not possible to store elements in these lists because, as described above, this is not type-safe (exception: zero can be stored). An error occurs at compile time. More generally, is the assignment

? ? extends T

not allowed.

However, it is possible to read out elements:

Number n = list.get(index); // OK
Integer i = list.get(index); // Typfehler: Es muss sich bei '? extends Number'
                             // nicht um ein Integer handeln.
Integer j = (Integer) list.get(index); // OK

The assignment

? extends TT (or base class)

is therefore allowed, but not the assignment

? extends Tderived from T

Generics, like arrays, offer covariant behavior, but prohibit all operations that are type-unsafe.

Contravariance

Contravariance describes the behavior of the inheritance hierarchy of the generic type contrary to the hierarchy of its type parameter. Applied to the above example this would mean: A list <Number> would be assignment-compatible to a list <Double>. This is done as follows:

List<? super Double> list;
list = new ArrayList<Number>();
list = new ArrayList<Double>();
list = new ArrayList<Object>();

An object that behaves in a contravariant manner must not make any assumptions about the extent to which an element of type V is derived from T, where T is the lower typebound (in the example of ? super Doubleis T Double). Therefore, the above lists cannot be read:

// Fehler: 'list' könnte vom Typ List<Object> sein
Number x = list.get(index);

// Fehler: 'list' könnte List<Object> oder List<Number> sein
Double x = list.get(index);

// Die einzige Ausnahme: Objects sind auf jeden Fall in der Liste
Object x = list.get(index);

Not allowed, because not type-safe, so is the assignment ? super T → (derived from Object)

Not difficult to guess: In return, an element can be placed in such a list:

List<? super Number> list;
list.add(new Double(3.0)); // OK: 'list' hat immer den Typ List<Number>
                           // oder List<Basisklasse von Number>. Damit
                           // ist die Zuweisung Double -> T immer erlaubt.

Unconstrained parametric polymorphism

Last but not least, generics offer completely polymorphic behavior. No statement can be made about the type parameters here, because no limit is specified in either direction. The wildcard was defined for this. It is represented by a question mark.

List<?> list;
list = new ArrayList<Integer>();
list = new ArrayList<Object>();
list = new ArrayList<String>();
// ...

The type parameter itself cannot be used here, as no statement is possible. Only the assignment T → Object is allowed, since T is definitely an object. In return, it is guaranteed that the code can work with all Ts.

Something like this can be useful if you only work with the generic type:

// Keine Informationen über den Typparameter nötig, kann ''beliebige'' Listen
// aufnehmen.
int readSize(List<?> list) {
    return list.size();
}

To make it clear that wildcards are unnecessary here, and that it is actually not about any variance, the following implementation of the above function is given:

<T> int readSize(List<T> list) {
    return list.size();
}

Web links

Individual evidence

  1. Java and Scala's Type Systems are Unsound .