DEV Community

Code Craft Club
Code Craft Club

Posted on • Originally published at Medium

Generics in Java was never this EASY! — A Complete Beginner’s Guide!

Imagine you have a collection of jars, all identical in shape and size, but each one with a unique label. Now, these jars can be used to store a variety of items, from candies to spices, without any confusion. The label on each jar is the key — it tells you exactly what should go inside. Just like these labeled jars simplify your storage needs, Generics in Java are like labels for your code, making it easy to work with multiple data types while keeping things organized.

Jars

What Are Generics?

Generics in Java are like those labels on your jars, but for your code. They allow you to create classes, methods, and interfaces that can work with different data types while ensuring type safety. These labels (or type parameters) specify what kind of data each container can hold, just like the labels on your jars specify what goes inside.

The Need for Generics

Imagine if your jars had no labels. You’d have to open each one to figure out what’s inside. It would be a chaotic mess! Without Generics, your code can end up in a similar mess, with lots of redundancy. You might need to create separate classes for each data type, which is inefficient and prone to errors. Generics come to the rescue, allowing you to create a single, versatile container that can hold various types of data, just like those labeled jars help keep your kitchen organized.

Type Parameters and Representation

In Java, generics use type parameters, which are placeholders for specific data types. These type parameters can be represented in two common ways:

  1. Single Letter Convention: Typically, single uppercase letters are used to distinguish from regular class or interface names. The most commonly used parameters are
    E - Element (used extensively by the Java Collections Framework)
    K - Key
    N - Number
    T - Type
    V - Value
    They don't carry any special meaning; they're just placeholders.
    For example, the class java.util.HashMap<K, V> has two type parameters, K and V, representing the type of the keys and values, respectively, stored in the map. The interface java.util.List<E> has a single type parameter E representing the type of the elements stored in the list.

  2. Descriptive Names: Sometimes, more descriptive names like KeyType or ValueType are used for type parameters, especially when clarity is important, just as you might use labels with descriptive names for your jars.

Generic Classes

Now, let’s delve into generic classes with code and explanations.
Syntax of a Generic Class -

class ClassName<T1, T2, ..., Tn> {
    // block of code
}
Enter fullscreen mode Exit fullscreen mode

Here, T1, T2, …, Tn are the comma-separated type variables.
Let’s understand with an example:

class Box<T> {
    private T contents;

    public Box(T contents) {
        this.contents = contents;
    }

    public void printDataType() {
        System.out.println("Type: " + this.contents.getClass().getSimpleName());
    }
}

class Main{
   public static void main(String[] args) {
    Box<Integer> integerBox = new Box<>(42);
  integerBox.printDataType();

    Box<String> stringBox = new Box<>("Hello, Generics!");
  stringBox.printDataType();
}
}

// Output:
// Type: Integer
// Type: String
Enter fullscreen mode Exit fullscreen mode

In this code, we've defined a generic class Box<T> that can hold any type of object, where T is our type parameter. The printDataType method prints the type of the contents stored in the Box using getClass().getSimpleName().

In the main method, we create instances of Box for both Integer and String types and call the printDataType method on each of them. This should print the respective types of the contents stored in the Box.

Analogy -

Just like your labeled jars can hold different items, the Box class can hold different data types. The type parameter T acts like the label, ensuring that you only put compatible items inside.

Generic Methods

Now, let’s delve into generic methods within a generic class.
Syntax of a Generic Method:

<T1, T2, ..., Tn> returnType methodName(parameters) {
    // block of code
}

// Example Syntax -

  //Example 1
  public <T> void methodName(T obj) {
      // block of code
  }

  //Example 2
  public static <T, N> T methodName(T obj, N num) {
      // block of code
  }
Enter fullscreen mode Exit fullscreen mode

Let’s understand with an example now:

class Box<T> {
    private T contents;

    public Box(T contents) {
        this.contents = contents;
    }

    public T getContents(){ //method returns T type
      return this.contents;
    }
}

class Main{
   public static void main(String[] args) {
    Box<Integer> integerBox = new Box<>(42);
  System.out.println("Method Returned:" + integerBox.getContents());

    Box<String> stringBox = new Box<>("Hello, Generics!");
  System.out.println("Method Returned:" + stringBox.getContents());
}
}

// Output -
// Method Returned:42
// Method Returned:Hello, Generics!
Enter fullscreen mode Exit fullscreen mode

In this code, we have a generic class Box<T> with a constructor that accepts an object of type T and a getContents method that returns an object of type T.

The integerBox object is created with an Integer type parameter and the stringBox object is created with a String type parameter.

In the main() method, we are calling the getContents() method on both objects and displayed the returned values on the console.

Generic Interfaces

Similar to creating Generic Classes and Generic Methods, we can also create interfaces in Java that can be used with any type of data by using generics. This type of method is known as a Generic Interface.
Syntax of a Generic Interface:

interface InterfaceName<T1, T2, ..., Tn> {
    // block of code
}

class ClassName<T1, T2, ..., Tn> implements InterfaceName<T1, T2, ..., Tn> {
    // block of code
}



// Example Syntax -

interface Pair<K, V> {
    K getKey();
    V getValue();
}
Enter fullscreen mode Exit fullscreen mode

Pair is a generic interface defining two type parameters, K and V.
Implementing classes specify the actual types:

interface Pair<K, V> {
    K getKey();
    V getValue();
}


class OrderedPair<K, V> implements Pair<K, V> {
    private K key;
    private V value;

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

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

class Main{
  public static void main(String[] args){
    OrderedPair<String, Integer> obj = new OrderedPair<>("Mobile No", 999999999);
    System.out.println("Key: " + obj.getKey());
    System.out.println("Value: " + obj.getValue());
  }
}

// Output -
// Key: Mobile No
// Value: 999999999
Enter fullscreen mode Exit fullscreen mode

In this code, we have defined a generic interface Pair<K, V> with two type parameters, K and V, which define methods getKey and getValue. Then, we have implemented this interface with the OrderedPair<K, V> class, where K and V are specific types (in this case, String and Integer).

In the main method, we create an instance of OrderedPair<String, Integer> with values "Mobile No" and 999999999.

Bounded Types

Now, imagine if you wanted to limit the types that can be used with generics, just like specifying that a jar is for storing only chocolates. This is where bounded types come in.

Chocolate jar
As we saw above of how we can create generic classes, interfaces, and methods that can work with any data type, however, sometimes we may want to limit the data types that can be used with a generic to a specific set of types. In these cases, we can use bounded types by specifying the upper bound type parameter with the extends keyword.

Syntax of a Bounded Types:

<T extends A>
Enter fullscreen mode Exit fullscreen mode

Here, the type parameter T can only accept data types that are of type A or any classes or interfaces that extends A.
Let’s understand with an example:

class NumberContainer<T extends Number> {
    private T value;

    public NumberContainer(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

class Main{
  public static void main(String[] args) {
    NumberContainer<Integer> intContainer = new NumberContainer<>(42);
    Integer intValue = intContainer.getValue(); // Works fine
    System.out.println(intValue);

    NumberContainer<Double> doubleContainer = new NumberContainer<>(3.14);
    Double doubleValue = doubleContainer.getValue(); // Works fine
    System.out.println(doubleValue);
    }
}

// Output -
// 42
// 3.14
Enter fullscreen mode Exit fullscreen mode

In this example, NumberContainer is a generic class that can hold any type that extends the Number class, which includes numeric types such as Integer, Double, and Float.

We create NumberContainer instances for Integer and Double types and successfully store and retrieve numeric values from them.

Failing Scenario in Bounded Types

Now, let’s see a failing scenario for the above example:

public static void main(String[] args) {
    NumberContainer<String> stringContainer = new NumberContainer<>("Hello"); // Compilation error!
}
Enter fullscreen mode Exit fullscreen mode

In this scenario, we attempt to create a NumberContainer for the String type, which is not a numeric type. This results in a compilation error because it violates the constraint that the type must extend Number.

So, in this example, bounded types ensure that the container only works with numeric types and prevents non-numeric types from being stored in it.

Conclusion

In summary, generics in Java are like labeled jars, making it easy to understand, organize, and work with multiple data types in your code. Whether you're working with classes, methods, or interfaces, generics allow you to create more versatile and reliable code. So, just like those labeled jars in your kitchen, generics help you keep your Java code neatly organized and easy to grasp.

Closing Thoughts:

Thank you for reading our blog. We appreciate your time and interest in our content. If you have any questions, feedback, or topics you’d like us to cover in our future articles, please feel free to leave a comment below. Your input is valuable, and it helps us create content that matters to you.

Stay connected with us for more insightful articles on various topics related to technology, programming, and much more.

Happy coding!

Top comments (0)