How to use generics in your Java programs

Introduced in Java 5, generics enhance the type safety of your code and make it easier to read. This helps you avoid runtime errors like the ClassCastException, which happens when you try to cast objects to incompatible types.

In this tutorial, you’ll learn about generics and see three examples of using them with the Java Collections Framework. I’ll also introduce raw types and discuss the instances when you might choose to use raw types rather than generics, along with the risks of doing so.

Generics in Java programming

  • Why use generics?
  • How to use generics for type safety
  • Generics in the Java Collections Framework
  • Examples of generic types in Java
  • Raw types vs. generics

Why use generics?

Generics are commonly used in the Java Collections Framework with java.util.List, java.util.Set, and java.util.Map. They also appear in other parts of Java, like java.lang.Class, java.lang.Comparable, and java.lang.ThreadLocal.

Before generics, Java code often lacked type safety. Here’s an example of Java code before generics:


List integerList = new ArrayList();
  
integerList.add(1);
integerList.add(2);
integerList.add(3);

for (Object element : integerList) {
      Integer num = (Integer) element; // Cast is necessary
      System.out.println(num);
 }

In this code, you intend to store Integer objects in the list. However, nothing stops you from adding a different type, like a String:


integerList.add("Hello");

This code would cause a ClassCastException at runtime, when you tried casting the String to an Integer.

Using generics for type safety

To solve the problem above and avoid ClassCastExceptions, we can use generics to specify the type of objects a list may contain. We don’t need to make a class cast in that case, which makes the code safer and easier to understand:


List integerList = new ArrayList<>();

integerList.add(1);
integerList.add(2);
integerList.add(3);

for (Integer num : integerList) {
     System.out.println(num);
}

List means “a list of Integer objects.” Based on this instruction, the compiler ensures that only Integer objects can be added to the list, eliminating the need for casting and preventing type errors.

Generics in the Java Collections Framework

Generics are integrated into Java Collections to provide compile-time type checking and to eliminate the need for explicit type casting. When you use a collection with generics, you specify the type of elements that the collection can hold. The Java compiler uses this specification to ensure that you do not accidentally insert an incompatible object into the collection, thus reducing bugs and improving code readability.

To illustrate how generics are used in the Java Collections Framework, let’s look at some examples.

List and ArrayList with generics

In the above example, we already briefly explored a simpler way to use the ArrayList. Now, let’s explore this concept a bit further by seeing how the List interface is declared:


public interface List extends SequencedCollection { … }

In this code, we are declaring our generic variable as “E,” and this variable can be replaced by any object type we want. Note that the variable E stands for element.

Now let’s see how to replace the variable E with the type we want for our List. In the following code, we replace the variable with :


List list = new ArrayList<>();
list.add("Java");
list.add("Challengers");
// list.add(1); // This line would cause a compile-time error.

Here, List specifies that the list can only hold String objects. As you see in the last line of the code, attempting to add an Integer results in a compilation error.

Set and HashSet with generics

The Set interface is similar to the List:


public interface Set extends Collection { … }

We will also replace with , so we can only insert a Double value into the doubles Set:


Set doubles = new HashSet<>();
doubles.add(1.5);
doubles.add(2.5);
// doubles.add("three"); // Compile-time error

double sum = 0.0;
for (double d : doubles) {
    sum += d;
}

The Set ensures that only Double values can be added to the set, preventing runtime errors that might occur from incorrect casting.

Map and HashMap with generics

We can declare as many generic types as we want. In the example of a Map, which is a key value data structure, we have K for key and V for value:


public interface Map { … }

Now, we replace K with String as the key type. We’ll also replace V with Integer as the value type:


Map map = new HashMap<>();
map.put("Duke", 30);
map.put("Juggy", 25);
// map.put(1, 100); // This line would cause a compile-time error

This example shows a HashMap that maps String keys to Integer values. Adding a key of type Integer is not allowed and would cause a compile-time error.

Examples of using generic types in Java

Now let’s look at some examples that will demonstrate further how to declare and use generic types in Java.

Using generics with objects of any type

We can declare a generic type in any class we create. It doesn’t need to be a collection type. In the following code example, we declare the generic type E to manipulate any element within the Box class. Notice in the code below that we declare the generic type after the class name. Only then we can use the generic type E as an attribute, constructor, method parameter, and method return type:


// Define a generic class Box with a generic type parameter E
public class Box {
    // Variable to hold an object of type E
    private E content;

    public Box(E content)  {  this.content = content; }
    public E getContent() { return content;  }
    public void setContent(E content) { this.content = content;  }

    public static void main(String[] args) {
        // Create a Box to hold an Integer
        Box integerBox = new Box<>(123);
        System.out.println("Integer Box contains: " + integerBox.getContent());

        // Create a Box to hold a String
        Box stringBox = new Box<>("Hello World");
        stringBox.setContent("Java Challengers");
        System.out.println("String Box contains: " + stringBox.getContent());
    }
}

The output of the Box example is:


Integer Box contains: 123
String Box contains: Java Challengers

Notice the following about the code:

  • The class Box uses the type parameter E as a placeholder for the object the box will hold. This allows Box to be used with any object type.
  • The constructor initializes a new instance of the Box class with the provided content. Type E ensures that the constructor can accept any object type defined when the instance is created, maintaining type safety.
  • getContent returns the box’s current content. Returning type E ensures that it conforms to the generic type specified when the instance was created, providing the correct type without the need for casting.
  • setContent updates the content of the box with the new content. Using type E as the parameter ensures that only an object of the correct type can be set as the new content, ensuring type safety throughout the use of the instance.
  • In the main method, two Box objects are created:
    integerBox holds an Integer, and stringBox holds a String.
  • Each Box instance operates on its specific data type, demonstrating the power of generics for type safety.

This example showcases the basic implementation and usage of generics in Java, highlighting how to create and manipulate objects of any type in a type-safe manner.

Using generics with different data types

We can declare as many types as we want as a generic type. In the following Pair class, we can add the generic values . If we wanted to add even more generic types, we could add , and so on. The code would compile without issues.

Let’s see the Pair class with the pair values :


class Pair {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }

    public void setKey(K key) {
        this.key = key;
    }
    public void setValue(V value) {
        this.value = value;
    }
}

public class GenericsDemo {
    public static void main(String[] args) {
        Pair person = new Pair<>("Duke", 30);

        System.out.println("Name: " + person.getKey());
        System.out.println("Age: " + person.getValue());

        person.setValue(31);
        System.out.println("Updated Age: " + person.getValue());
    }
}

The output of this code is:


Name: Duke
Age: 30
Updated Age: 31

Notice the following about the code:

  • The generic class Pair has two type parameters: K (for key) and V (for value), making it versatile for any data type.
  • Constructors and methods in the Pair class use these type parameters, which allows for strong type-checking.
  • A Pair object is created to hold a String (a person’s name) and an Integer (their age).
  • Accessors (getKey and getValue) and mutators (setKey and setValue) manipulate and retrieve the data from the Pair.
  • The Pair class can store and manage related information without being tied to specific data types. This demonstrates the power and flexibility of generics.

This example shows how generics can create reusable and type-safe components with different data types, enhancing code reusability and maintainability.

Let’s look at one more example.

Declaring a generic type within a method

It’s possible to declare a generic type directly within a method. It isn’t required to declare the generic type at a class level. So, if we needed a generic type only for a method, we could do that by declaring the generic type before the return type of the method signature:


public class GenericMethodDemo {

    // Declare generic type  and print elements with the chosen type
    public static  void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        // Using the generic method with an Integer array
        Integer[] intArray = {1, 2, 3, 4};
        printArray(intArray);

        // Using the generic method with a String array
        String[] stringArray = {"Java", "Challengers"};
        printArray(stringArray);
    }
}

The output of this code is:


1 2 3 4 
Java Challengers 

Raw types vs. generics

A raw type is essentially the name of a generic class or interface but without any type arguments. Raw types were common before generics were introduced in Java 5. Today, developers typically use raw types for compatibility with legacy code or interoperability with non-generic APIs. Even with generics, it’s good to know how to recognize and use raw types in your code.

A common example of using a raw type is declaring a List without a type parameter:


List rawList = new ArrayList();

In this example, List rawList declares a List without a generic type parameter. rawList can hold any type of object, including Integer, String, Double, and so on. Since no type is specified, there is no compile-time check on what types of objects are being added to the list.

Compiler warning when using raw types

The Java compiler sends warnings about using raw types in Java. These warnings are generated to alert developers about potential risks related to type safety when using raw types instead of generics.

When you use generics, the compiler checks the types of objects stored in collections (like List and Set), method return types, and parameters to ensure they match the declared generic types. This prevents common bugs like the runtime ClassCastException.

When you use a raw type, the compiler cannot perform these checks because raw types don’t specify the type of objects they’re intended to contain. As a result, the compiler issues warnings to indicate that you are bypassing the type safety mechanisms provided by generics.

Example of a compiler warning

Here’s a simple example to illustrate how the compiler issues a warning when using raw types:


List list = new ArrayList();  // Warning: Raw use of parameterized class 'List'
list.add("hello");
list.add(1);

When you compile this code, the Java compiler typically outputs a warning message like:


Note: SomeFile.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

If you compile with the -Xlint:unchecked flag, you will get more detailed information about where and why the warning was generated:


warning: [unchecked] unchecked call to add(E) as a member of the raw type List
    list.add("hello");
         ^
  
where E is a type-variable:
    E extends Object declared in interface List

If you are sure that using raw types in your code does not introduce risks or deal with legacy code that cannot be easily refactored to use generics, you can use the @SuppressWarnings("unchecked") annotation to suppress these warnings. Be cautious about suppressing compiler warnings, as it could mask real problems.

Consequences of using raw types

While raw types are helpful for backward compatibility, they have at least two significant drawbacks: loss of type safety and increased maintenance costs.

  • Loss of type safety: One of the biggest advantages of generics is type safety. By using raw types, you lose this benefit. The compiler does not check type correctness, which could lead to a ClassCastException at runtime.
  • Increased cost of maintenance: Code that uses raw types is harder to maintain because it lacks the clear type information that generics provide. This can lead to errors that are hard to detect until runtime.

As an example of a type safety issue, if you use List (a raw type) instead of the generic List, the compiler allows you to add any object type to the list, not just strings. This can lead to runtime errors when you retrieve an item from the list and attempt to cast it to a string, but the item is actually of another type.

What you’ve learned about generics

Generics provides type safety with great flexibility. Let’s recap the key points you’ve learned.

What are generics and why should I use them?

  • Generics were introduced in Java 5 to improve type safety and flexibility in code.
  • The key advantage of generics is that they help you avoid runtime errors such as ClassCastException.
  • Generics are commonly used in the Java Collections Framework, but they can also be used with code elements such as Class, Comparable, and ThreadLocal.
  • Generics enhance type safety by preventing the insertion of incompatible types.

Generics in Java Collections

  • List and ArrayList: List allows for specifying any type E, ensuring the list is type-specific.
  • Set and HashSet: Set limits elements to type E, promoting consistency and type safety.
  • Map and HashMap: Map defines types for both keys and values, increasing type safety and clarity.

General benefits of using generics

  • Reduce bugs by preventing the insertion of incompatible types.
  • Improve readability and maintainability by clarifying the types involved.
  • Facilitate creating and managing collections and other data structures in a type-safe manner.

Learn more about Java Collections

Go to Source

Author: