Generics in Java

Udayaraj SubediUdayaraj Subedi
7 min read

Generics can be super confusing when you see all of those T, <K, V>, and <? extends String> etc.
Let’s take a look at what generics are and why we need them.

Before jumping into generics, let’s first understand the problems we face without using generics.Suppose we need a class that takes a name and prints it:


class JavaGenericPractice {
    public static void main(String[] args) {
        NamePrinter name = new NamePrinter();
        name.setName("Udaya");
        System.out.println("Name : " + name.getName());
    }
}

class NamePrinter {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String providedName) {
        name = providedName;
    }
}

Here, we simply set a name in the NamePrinter class and printed it. Cool, right?

Now what if we want to set age and print that too?

Let’s write a separate class for age:

class JavaGenericPractice {
    public static void main(String[] args) {
        AgePrinter age = new AgePrinter();
        age.setAge(23);
        System.out.println(age.getAge());
    }
}

class AgePrinter {
    private Integer age;

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer providedAge) {
        age = providedAge;
    }
}

So, this works fine. But it feels off, right?

For both AgePrinter and NamePrinter, the logic is exactly the same — we are just changing the data type. In real-world applications, we are not going to keep creating new classes just to handle name, age, salary, or other types of values.

We need something flexible, reusable, and type-safe.

That’s where generics come into play.

Let’s say we want a class that can store any type of data — whether it’s a String, Integer, or something else. Instead of writing a new class each time, we can create a generic class that works for any type.

Let’s fix our problem using generics:

class GenericPrinter<T> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

}

class JavaGenericPractice {
    public static void main(String[] args) {
        GenericPrinter<String> namePrinter = new GenericPrinter<>();
        namePrinter.setValue("Udaya");
        System.out.println("Name : " + namePrinter.getAge());

        GenericPrinter<Integer> agePrinter = new GenericPrinter<>();
        agePrinter.setValue(23);
        System.out.println("Age : " + agePrinter.getAge());

    }
}

Here, we are using a type T in our class.

The T stands for Type — it’s just a placeholder that will be replaced with the actual data type when we use the class. We could name it anything, but by Java convention, we use:

  • E - Element (used extensively by the Java Collections Framework)

  • K - Key

  • N - Number

  • T - Type

  • V - Value

  • S,U,V etc. - 2nd, 3rd, 4th types

So when we write GenericPrinter<String>, it tells Java that for this instance, T should be treated as String.

When we write GenericPrinter<Integer>, it becomes an Integer.

This makes our class reusable for any type and avoids writing the same code again and again. This is the basic idea behind generics. Generics in Java allow us to write code that can work with any data type. Instead of writing the same logic again and again for different types like String, Integer, or Double, we write the logic once using a placeholder (like T) and Java will replace it with the actual type when needed.

You can think of generics like a template — once created, it can be reused for different types without rewriting the code.

Generics make your code:

Reusable – no need to write the same class or method for each data type.

Type-safe – helps catch type-related errors at compile time.

Clean and maintainable – because there's less duplication.

So, In simpler terms, generics allow you to write code that can work with any object type while ensuring type safety at compile time.

Generic Constructor

Just like classes can be generic, constructors can be generic too. This means they can accept values of any type.

Let’s look at an example:

class GenericConstructor<T> {
    private T value;

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

    public void printValue() {
        System.out.println(value);
    }
}

GenericConstructor<Integer> genericConstructorForInteger = new GenericConstructor<>(10);
genericConstructorForInteger.printValue();

GenericConstructor<String> genericConstructorForString = new GenericConstructor<>("Hello");
genericConstructorForString.printValue();

Here, the same constructor works for both Integer and String. We are using generics to create flexible and reusable constructors.

Generic Interfaces

Just like classes, interfaces can also have type parameters. This allows multiple implementations using different types.

Here’s a generic interface:

interface GenericInterface<T> {
    void doSomething(T value);
}

Now, we can implement this interface for different data types:

class GenericImplementationForString implements GenericInterface<String> {
    @Override
    public void doSomething(String value) {
        System.out.println("Doing something with " + value);
    }
}

class GenericImplementationForInteger implements GenericInterface<Integer> {
    @Override
    public void doSomething(Integer value) {
        System.out.println("Doing something with " + value);
    }
}

GenericInterface<String> genericImplementation = new GenericImplementationForString();
genericImplementation.doSomething("Hello world");

GenericInterface<Integer> genericImplementation2 = new GenericImplementationForInteger();
genericImplementation2.doSomething(10);

This shows how a single interface can support different types using generics.

Generic Methods

A generic method is a method that works with any data type. The type is defined at the method level, not the class level.

class GenericMethod {

    public <T> void printValue(T value) {
        System.out.println(value);
    }

    public <K, V> void printValue(K value, V value2) {
        System.out.println("Value 1 : " + value + " Value 2 : " + value2);
    }
}

Here’s what’s happening:

  • <T> and <K, V> before the return type (void) mean that we are declaring type parameters.

  • These parameters can be any type — String, Integer, Double, or even your custom class like User.

  • You can use these type parameters inside the method like regular data types.

And when we use it:

GenericMethod genericMethod = new GenericMethod();
genericMethod.printValue("hello");
genericMethod.printValue("hello", "world");

Even though we didn’t specify the type like <String> or <Integer>, Java figures it out automatically based on what we pass to the method. This is called type inference.

Bounded Types in Generics

Sometimes, you may want to restrict the types that can be used with generics. For example, only allowing types that extend Number. In such cases, we use bounded types.

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

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

    public void printDoubleValue() {
        System.out.println(value.doubleValue());
    }
}

BoundedGeneric<Integer> obj1 = new BoundedGeneric<>(5);
obj1.printDoubleValue(); // prints 5.0

BoundedGeneric<Double> obj2 = new BoundedGeneric<>(5.5);
obj2.printDoubleValue(); // prints 5.5

// BoundedGeneric<String> obj3 = new BoundedGeneric<>("hello"); // This will give compile-time error

Here, T extends Number ensures that only numeric types like Integer, Double, and Float can be used.

This is useful when you want to perform operations like doubleValue() that only make sense for numeric data types.

Wildcards in Generics (?)

Wildcards are used when the exact type is not known or when you want your method to work with a range of types.

There are three types of wildcards:

1. <?> — Unbounded Wildcard

Allows any type.

public void printList(List<?> list) {
    for (Object obj : list) {
        System.out.println(obj);
    }
}

You can pass List<Integer>, List<String>, List<Double>, etc.

2. <? extends Type> — Upper Bounded Wildcard

Allows any type that extends a particular type.

public void printNumbers(List<? extends Number> list) {
    for (Number num : list) {
        System.out.println(num);
    }
}

This allows List<Integer>, List<Double>, and List<Float>, but not List<String>.

3. <? super Type> — Lower Bounded Wildcard

Allows any type that is a superclass of the given type.

public void addNumbers(List<? super Integer> list) {
    list.add(10);  // Allowed
}

This allows List<Integer>, List<Number>, or List<Object>.

Why Use Wildcards?

Wildcards allow your methods to work with multiple related types while still maintaining type safety. They are especially useful in collections, where you might want to work with a group of elements without knowing their exact type in advance.

Conclusion

Generics are one of the most powerful features in Java. They help you write code that is:

Reusable – You don’t have to repeat the same logic for every type.

Type-safe – You catch errors during compile time instead of runtime.

Clean and maintainable – Your code is easier to manage, update, and understand.

By understanding generics, you can write flexible and efficient code that works with different data types while still being safe and readable. Once you get comfortable with type parameters, bounded types, wildcards, and generic methods, you'll find generics to be a natural part of writing modern Java applications.

0
Subscribe to my newsletter

Read articles from Udayaraj Subedi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Udayaraj Subedi
Udayaraj Subedi

Software Engineer