Jon Rumsey

An online markdown blog and knowledge repository.


Project maintained by nojronatron Hosted on GitHub Pages — Theme by mattgraham

Generics

Generics are used in Java, C#, and other strongly typed programming languages. Following are some notes and lessons learned about them.

Java Tutorials Notes

Baeldung states that Generics were added to Java in JDK 5.0. This explains why I wasn't already aware of them from college where I was introduced to Java at v.1.5, but the curriculum wasn't updated yet.

Basics

'Generics add stability to your code by making more of your bugs detectable at compile time.' [The Java Tutorials]

Generics enable Types to be Parameters.

Apply Generics to Classes, Interfaces, and Methods.

Formal Parameters: Strongly-typed parameter that is a value i.e. Integer weight.

Type Parameters: Generic parameters are parameters that are types i.e. T object.

Keyword 'T': Used as a placeholder for the Type that will be used in the Generic Class, Interface, or Function.

Raw Types: No type arguments are included with the instantiation of the Generic Type.

The catch is Un/Boxing penalties apply, and unsafe code can execute at run time. Code is unsafe because Type Safety cannot be validated. Older APIs (Java 7 and earlier) will require use of Raw Types for backward compatibility reasons. Use @SuppressWarnings syntax for ignore Raw Types warning messages.

Methods use Type Inference to process generic parameters.

Non-generic Classes support generic Methods.

Due to Type Erasure, generics incur no runtime overhead, ensure type safety, and ensure only ordinary classes, interfaces, and methods will be produced in bytecode.

Java Primitives

Primitives allocate less memory than Types.

Primitives cannot be used as a Type Parameter in the generic declaration at this time.

Primitives can utilize comparison operators like < > != etc.

List of Java SE 8 Primitives:

Types can compare for value or equality by:

Java Generic Type Invocation

Replace 'T' with a concrete non-Primitive or Reference Type Class.

public class DSNode<T> {
  private T data;
  public DSNode(T payload) {
    this.data = payload;
  }
}

Note: Primitives can then be used as the Type within the Generic Class, Interface, or Method.

int myValue = 11;
DSNode<Integer> myNode = new DSNode<>(myValue); // <-- DSNode will store an Integer, and a primitive int value is added

Create multiple versions of DSNode with different values:

string myData = "This is a message.";
DSNode<String> messageNode = new DSNode<>(myData);

MyCustomType payload = new MyCustomType();
DSNode<MyCustomType> anotherMessageNode = new DSNode<>(payload);

Define static and non-static method's generic parameters using similar syntax:

public class DSNode<T> {
  private T data;
  public DSNode(T payload) {
    this.data = payload;
  }
  public void setData(T newData) {
    this.data = newData; // <--
  }
}

A method signature of a Generic Method must include a generic type <T>:

public class Processor {
  // static method is prefixed with a generic template <T>
  public static <T> void exchangeElements(T[] list, int left, int right) {
      T firstItem = list[left];
      list[left] = list[right];
      list[right] = firstItem;
  }
}

When a generic method will use multiple types, they can be listed in the method signature [Baeldung.com/java-generics]:

public static <T, G> List<G> fromArrayToList(T[] arr, Function<T, G> mapperFunc) {
  return Arrays.stream(arr)
    .map(mapperFunc)
    .collect(Collectors.toList());
}

Bounded Types in Java

Allows restricting types that can be used in a parameterized type.

Sending a parameterized type of String when only a Number-type is expected will no longer work.

Example of a Bounded-type Parameter usage in a method:

public <U extends Number> void process(U u) {
  ...
}

This enables compile-time warnings and errors when types are passed-in to the method that don't meet the bounded type declaration in the parameters list.

Multiple Bounded Types can be included in the same parameter:

public <U extends Number & X1 & X2> void process(U u) {
  ...
}

Multiple Bounded Types in the Type Parameter list are sensitive to the inheritance chain. A Bounded Type following an Ampersand must be more-specific than the one preceeding it.

Use the OOP rule 'IS A' to determine if a primitive or Type will be allowed in a Bounded Types listing, and what order it should be within the Bounded TYpes listing.

Note: Integer and Double are subtypes of Number, but when used with Generics, the generic class T, whether Integer or Double, are not. Think of it this way: What is the common parent of the Genericized Type? If there is no common parent, than the Types will not be compatible with the Bounded Types list. Oracle Docs say to look at 'Wildcards and Subtyping' for details about this behavior and how to work with it.

Primitive Operators in Java

Certain Operators only work on primitive types and therefore cannot be used to compare custom Types or Generic arguments that don't have their own Operator definitions or overrides.

Implement an interface i.e. Comparable<T> so e.compareTo(elem) > 0 can be utilized.

Subtyping in Java

Extend or implement a generic class or interface to subtype it.

Determination of a subtype are made within 'extends' and 'implements' clauses.

Extending a class to make it a 'subclass':

public class Juice extends Liquid {
  ...
}
public class Juice implements Pourable {
  // extend Interface Pourable here
  ...
}

Leveraging abstract classes and methods allows definition of Bounded Wildcards in generics. Reminder of Inheritance:

abstract class Animal {
  abstract void talk();
}
class Dog extends Animal {
  void talk() {
    System.out.println("Bark!");
  }
}
class Cat extends Animal {
  void talk() {
    System.out.println("Meow!");
  }
}

Here, Dog IS-A Animal, and Cat IS-A Animal too, so filtering a type by tye super class Animal, allows including Dog and Cat types.

Using Interfaces, a Type can be made to fit requirement by implementing expected members.

interface Language {
  void speak();
}
class Terrier extends Animal implements Language {
  // dogs bark
  public talk() {
    System.out.println("Bark!");
  }
  // breeds bark in a dialect
  public speak() {
    System.out.println("Bow-wow!");
  }
}
class GermanShepherdDog extends Animal implements Language {
  void talk() {
    System.out.println("Bark!");
  }
  // GSDs have a deeper dialect
  public speak() {
    System.out.println("Woof!");
  }
}

More about this later.

Java Type Inference

The Java Compiler uses a Type Inference Algorithm that:

Use the diamond <> symbol when instantiating a new generic class to avoid incidentally referencing the Raw Type.

Constructors can use Generics as well, and Type Inference can figure out the Type the same as described above.

During compilation the 'T' is replaced with 'Object' during Type Erasure (below).

Type Witness Notation

When declaring a new Generic instance, the common syntax is:

// taken directly from Oracle's Java docs on generic type inference
static <T> List<T> emptyList();
List<String> myList = Collections.emptyList();

Type Witnesses are placed in front of the instance declaration for readability:

static <T> List<T> emptyList();
List<String> myList = Collections.<String>emptyList(); // <--

When Type Witnesses are not used, the compiler relies on Type Inference algorithm to figure it out.

Another example where Type Inference will chose 'Object' incorrectly unless a Type Witness is included:

// starter method code
static <T> List<T> emptyList();

// will not compile in JSE 8 and earlier because the Type cannot be inferred in the invocation or return
processStringList(Collections.emptyList());

// will compile because the Witness Notation was included
processStringList(Collections.<String>emptyList());

Wildcards in Java Generics

Wildcard Syntax: ?

Wildcard is a placeholder for an 'unknown type'.

Can be used as a Type in a parameter, field, or local variable.

Avoid using wildcards:

Bounded Wildcards are compiled-down to the bounded Type Parameter during Type Erasure (below).

Upper Bounded Wildcards

// bounded to List<String>
public static void process(List<Number> list) {
  ...
}
// less restrictive, allowing subtypes including Integer
public static void process(List<? extends Number> list) {
  // this can accept a list of any sub-class of Number
}

public static void addToFamily(T<? extends Animal> pet) {
  // accepts dogs or cats based on abstract class defined earlier
}

Unbounded Wildcards

Using an unbounded wildcard for an input limits members to that of the base class Object:

public static void printList(List<?> list) {
  // list.size() interrogates a List property size (length)
  // Types stored within the Collection are ignored
  System.out.println("list size is %s%n", list.size());
  // also note that any Type that has a toString method can be accessed
  for (Object item: list) {
    System.out.println("%s ", item);
  }
  System.out.println();
}

If Object were used instead of ? the iterator would provide Object instances represented as a String, instead of returing the String representation of the Type actually stored at each element.

There are Guidelines and if/when Wildcard should be used.

Avoid using a wildcard on a method output as it will make the return type unknown and difficult to debug and work with.

Lower Bounded Wildcards

public void printList(List<Integer> items) {
  //  limited to an Integer Type
}

public void printList(List<? super Integer>) {
  //  supports Integer, Number, and Object,
  //  anything that holds Integer values
}

Applying a lower-bounded wildcard to Animal public void show(T<? super Animal>){...} would result in only Animal and base class Object as acceptable types. Meaning neither Car nor Dog would be accepted as a wildcard matched type.

Wildcards and Subtyping

When one Class A 'IS A' Class B (such as through the 'extends' keyword), the extended Class is the super, and the extending Class is the inheritor.

The documentation example states:

class A {}
class B extends A {}
B b = new B();
A a = b; // possible because B extends A

BUT when used as a Type Parameter:

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

The problem is List<?> is the common parent, not A.

Therefore, use an upper-bounded wildcard:

List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList(); // OK

This is because List<? extends Integer> is a subtype of List<? extends Number>.

Wildcard Capture and Helper Methods

The compiler infers a particular type from the code when using Wildcards.

It is probably a good idea to assume the Compiler will guess the type is 'Object' if it isn't made apparent.

A private helper method should be implemented so Type inferrence will work properly (from the Oracle Docs):

public class WildcardFixed {
  void foo(List<?> i) {
    fooHelper(i);
  }

  // by convention name Helper Methods after the member they are for
  private <T> void wildcardFixedHelper(List<T> list) {
    list.set(0, list.get(0));
  }
}

Guidelines for Wildcard use (Java)

Variables provide one of 2 functions:

The In-or-Out-Principle:

Additionally:

Type Erasure in Java

See the docs for details, here are the highlights:

// type parameters are replaced by their bounds
public <T extends Animal> void foo(T animal) {...};
// becomes...
public void foo(Animal animal) {...};

The following are just a few things to watch out for:

Non-reifiable Types example:

public void addList(List<String> list) {...}
public void addList(List<Integer> list) {...}
// JVM cannot tell difference because these are Non-reifiable Types

Avoid using non-reifiable Types:

Heap Pollution:

Possibly good advice: If you find yourself using ___ consider rewriting your code to avoid non-reifiable Types or incorrectly handling casts:

Java Generics Restrictions

Things you cannot do with Java Generics:

Note: A method can throw a placeholder type e.g. public void parse(File file) throws T {...}.

Note: Method overriding must use type parameters that won't cause the methods to have the same signature after Type Erasure.

// before type erasure
public void print(Set<String> set) {...}
public void print(Set<Integer> set) {...}
// after type erasure
public void print(Set set) {...}
public void print(Set set) {...}

Common Naming Conventions (Java SE 8+)

Directly from [Oracle JavaSE Tutorial: Java Generics]

Use single, upper-case letters:

Multiple type placeholders? Use 'S', 'U', 'V', ...etc in the template diamond.

References

Oracle's Java Tutorials

Baeldung: Java Generics

Baeldung: Comparing Objects in Java

West Chester University's Dr. Robert Kline, CS Department (retired) on generics

Return to ContEd Index

Return to Root README