© Mikael Olsson 2018
Mikael OlssonJava Quick Syntax Referencehttps://doi.org/10.1007/978-1-4842-3441-9_22

22. Generics

Mikael Olsson1 
(1)
Hammarland, Länsi-Suomi, Finland
 

Generics refers to the use of type parameters, which provide a way to define methods, classes, and interfaces that can operate with different data types. The benefits of generics are that they provide compile-time type safety and they eliminate the need for most type conversions.

Generic classes

Generic classes allow class members to use type parameters. Such a class is defined by adding a type parameter section after the class name, which contains a type parameter enclosed between angle brackets. The naming convention for type parameters is that they should consist of a single uppercase letter. Typically, the letter T for type is used. The following example defines a generic container class that can hold a single element of the generic type:

// Generic container class
class MyBox<T> { public T box; }

When an object of this generic class is instantiated, the type parameter must be replaced with an actual data type, such as Integer:

MyBox<Integer> iBox = new MyBox<Integer>();

Alternatively, as of Java 7, a generic class can be instantiated with an empty set of type parameters. This type of instantiation is possible as long as the compiler can infer (determine) the type parameters from the context:

MyBox<Integer> iBox = new MyBox<>();

When an instance of MyBox is created, each type parameter in the class definition is replaced with the passed-in type argument. The object therefore behaves as a regular object, with a single field of the Integer type:

iBox.box = 5;
Integer i = iBox.box;

Notice that no casting is required when the stored value is set or retrieved from the box field. Furthermore, if the generic field is mistakenly assigned to or set to an incompatible type, the compiler will point that out:

iBox.box = "Hello World"; // compile-time error
String s = iBox.box;      // compile-time error

Generic methods

A method can be made generic by declaring it with a type parameter section before the method’s return type. The type parameter can be used like any other type inside of the method. You can also use it for the method’s return type, in the throws clause and for its parameter types. The next example shows a generic class method that accepts a generic array parameter, the content of which is printed out:

class MyClass
{
  public static <T> void printArray(T[] array)
  {
    for (T element : array)
      System.out.println(element);
  }
}

The preceding shown class isn’t generic. Methods can be declared as generic, independently of whether or not the enclosing class or interface is generic. The same is true for constructors.

Calling generic methods

A generic method is typically invoked just as a regular (non-generic) method, without specifying the type argument:

Integer[] iArray = { 1, 2, 3 };
MyClass.printArray(iArray);

In most cases, the Java compiler can infer the type argument of a generic method call, so it doesn’t have to be included. But if that’s not the case, then the type argument will need to be explicitly specified:

MyClass.<Integer>printArray(iArray);

Generic interfaces

Interfaces that are declared with type parameters become generic interfaces. Generic interfaces have the same two purposes as regular interfaces: they’re either created to expose members of a class that will be used by other classes, or to force a class to implement a specific functionality. When a generic interface is implemented, the type argument must be specified. The generic interface can be implemented by both generic and non-generic classes:

// Generic functionality interface
interface IGenericCollection<T>
{
  public void store(T t);
}
// Non-generic class implementing generic interface
class Box implements IGenericCollection<Integer>
{
  public Integer myBox;
  public void store(Integer i) { myBox = i; }
}
// Generic class implementing generic interface
class GenericBox<T> implements IGenericCollection<T>
{
  public T myBox;
  public void store(T t) { myBox = t; }
}

Generic type parameters

The passed-in type argument for a generic can either be a class type, interface type, or another type parameter, but it can’t be a primitive type. Generics can have more than one type parameter defined, by adding more of them between the angle brackets in a comma-separated list. Bear in mind that each parameter within the brackets must be unique:

class MyClass<T, U> {}

If a generic has multiple type parameters defined, the same number of type arguments need to be specified when the generic is used:

MyClass<Integer, Float> m = new MyClass<Integer, Float>();

Generic variable usages

Generics are only a compile-time construct in Java. After the compiler has checked that the types used with generic variables are correct, it will then erase all type-parameter and argument information from the generic code and insert the appropriate casts instead. That means generics don’t provide any performance benefits over non-generic code, because of removed runtime casts, as they do in, for example, C#. It also means generic types can’t be used for anything that requires runtime information—such as creating new instances of generic types or using the instanceof operator with type parameters. Operations that are allowed include declaring variables of the generic type, assigning null to generic variables, and calling Object methods:

class MyClass<T>
{
  public void myMethod(Object o)
  {
    T t1;                            // allowed
    t1 = null;                       // allowed
    System.out.print(t1.toString()); // allowed
    if (o instanceof T) {}           // invalid
    T t2 = new T();                  // invalid
  }
}

The process of removing type information from generic code is known as type erasure. For example, MyBox<Integer> would be reduced to MyBox, which is called the raw type. This step is performed in order to maintain backward-compatibility with code written before generics became part of the language in Java 5.

Bounded type parameters

It’s possible to apply compile-time enforced restrictions on the kinds of type parameters that a generic may be used with. These restrictions, called bounds, are specified within the type parameter section using the extends keyword. Type parameters can be bounded by either superclass or interface. For example, the following class B may only be instantiated with a type argument that’s either of the type A or has that class as a superclass:

// T must be or inherit from A
class B<T extends A> {}
class A {}

The next example specifies an interface as the bound. This will restrict the type parameter to only those types that implement the specified interface or are of the interface type itself:

// T must be or implement interface I
class C<T extends I> {}
interface I {}

Multiple bounds can be applied to a type parameter by specifying them in a list separated by ampersands:

class D<T extends A & I> {}

The ampersand acts as the separator instead of a comma because comma is already used for separating type parameters:

class E<T extends A & I, U extends A & I> {}

Aside from restricting the use of a generic to only certain parameter types, another reason for applying bounds is to increase the number of permitted method calls supported by the bounded type. An unbounded type may only call the Object methods. However, by applying a superclass or interface bound, the accessible members of that type will also become available:

class Fruit
{
  public String name;
}
class FruitBox<T extends Fruit>
{
  private T box;
  public void FruitBox(T t) { box = t; }
  public String getFruitName()
  {
    // Use of Fruit member allowed since T extends Fruit
    return box.name;
  }
}

Generics and Object

Before generics were introduced in Java 5, the Object type was used to create container classes that could store any type of objects. Now that generics are available, this use of the Object type as a universal container should be avoided. That’s because the compiler helps ensure that generics are type safe at compile-time, which can’t be done when using the Object type.

The collection classes in the Java library, among them ArrayList, have all been replaced with generic versions. Even so, any generic class can still be used as if it weren’t generic, simply by leaving out the type argument section. The default Object type will then be used as the type argument. That’s why the non-generic version of ArrayList is still allowed. Consider the following use of a non-generic ArrayList:

import java.util.ArrayList;
// ...
// Object ArrayList
ArrayList a = new ArrayList();
a.add("Hello World");
// ...
Integer b = (Integer)a.get(0); // run-time error

This String-to-Integer conversion will fail at runtime by throwing a ClassCastException. Had a generic ArrayList been used instead, the mistaken conversion would have been detected upon compilation, or immediately in an IDE such as Netbeans. This compile-time debugging feature is a major advantage with using generics over other coding approaches:

import java.util.ArrayList;
// ...
// Generic ArrayList (recommended)
ArrayList<String> a = new ArrayList<String>();
a.add("Hello World");
// ...
Integer b = (Integer)a.get(0); // compile-time error

With the generic alternative, only the specified type argument will be allowed into the ArrayList collection. Additionally, values obtained from the collection don’t have to be cast to the correct type because the compiler takes care of that.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.163.250