Generic Class in Java: From Basics to Advanced with Examples

Mayur Upadhyay
Mayur Upadhyay
generic class java

Have you ever needed to write the same code for different data types in Java? That’s where Generic classes in Java can help. They help you write one class that works with many types, making your code cleaner and easier to manage.

In this blog, you’ll learn what a generic class is, how to create one, and why it’s useful in real-world coding. We’ll also cover wildcards, type bounds, and more with simple examples.

Whether you’re learning Java or looking to hire Java developers, understanding generics can make a big difference in your projects. So, let’s get started!

What is a Generic Class in Java?

When working with data in Java, it’s common to create classes that hold or process different types of objects. Instead of writing separate classes for each data type, Java allows you to define a generic class – a flexible structure that can work with any object type while still enforcing type safety at compile time. Just like other Java data structures, generic classes help organize and manage data efficiently.

A generic class uses type parameters as placeholders. These parameters are specified when the class is instantiated, enabling the class to operate on various types without requiring separate logic for each one. Here’s a basic example of a generic class called Box:

public class Box<T> {
    private T item;
    public void set(T item) {
        this.item = item;
    }
    public T get() {
        return item;
    }
}

Explanation:

  • T is the type parameter (can be any valid identifier like E, K, V).
  • When you create an object of Box, you specify the actual type for T.
  • The class then behaves as if it were written specifically for that type.

Usage: Creating a Box for Integer

Box<Integer> intBox = new Box<>();
intBox.set(100);
System.out.println(intBox.get()); // Output: 100

What’s happening here:

  • Box<Integer> tells Java that T should be replaced with Integer.
  • This gives you type safety—only integers can be stored in intBox.

Generic classes are the foundation for many Java libraries and frameworks. Understanding them opens the door to writing reusable, clean, and reliable code. As we move ahead, we’ll look into using multiple type parameters, bounds, wildcards, and more.

How to Create and Use a Generic Class

Once you understand what a generic class is, the next step is learning how to define and use one in your Java programs. This section breaks it down into two key parts: the syntax for declaring a generic class and how to create and use its objects with specific types.

Syntax Breakdown

A generic class is defined using a type parameter placed inside angle brackets (<>) after the class name. This type parameter acts as a placeholder for the actual type you’ll use later. Here’s an example:

public class Box<T> {
    private T item;
    public void set(T item) {
        this.item = item;
    }
    public T get() {
        return item;
    }
}

Explanation:

  • T represents a generic type.
  • You can use T anywhere inside the class as a type, just like you would use String, Integer, etc.

Instantiating a Generic Class

Once the class is defined, you can create instances of it by specifying the actual type you want to use. Here’s an example:

Box<Integer> intBox = new Box<>();
intBox.set(100);
System.out.println(intBox.get());  // Output: 100

What this does:

  • Box<Integer> means the Box will store an Integer.
  • Using generics avoids type casting and provides compile-time safety.

By separating the logic from the data type, generic classes make your code more versatile and safer. Now that you know how to create and use them, let’s explore how to take it further using multiple type parameters.

Need Help with Maintaining a Clean Java Code? We Can Help!

Using Multiple Type Parameters

Sometimes, you might want a class to handle more than one type. For example, to store a pair of values or a key-value combination. Java allows you to define a generic class with multiple type parameters, giving you even more flexibility while keeping the code clean and type-safe.

Syntax: Defining a Class with Two Type Parameters

You can define multiple type parameters by separating them with commas inside angle brackets. Here’s an example:

public class Pair<K, V> {
    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;
    }
}

Explanation:

  • K and V are type parameters often used for key and value.
  • You can use them like regular types throughout the class.

Usage: Creating a Pair

Pair<String, Integer> scoreEntry = new Pair<>("Math", 95);
System.out.println("Subject: " + scoreEntry.getKey());
System.out.println("Score: " + scoreEntry.getValue());

Output:

Subject: Math  
Score: 95

This approach is especially useful for creating structured data like entries in a map or database rows.

By using multiple type parameters, you can build more complex and powerful generic structures. Up next, we’ll look into how you can restrict these parameters using bounds to control which types are allowed.

Bounded Type Parameters

Generic classes are highly flexible, but sometimes you want to restrict the types that can be used. That’s where bounded type parameters come in. They allow you to define limits on the types accepted, ensuring your generic class or method only works with a specific group of types, like numbers or interfaces.

In some advanced cases, such as Java concurrency, bounded type parameters can help ensure thread-safe operations using type restrictions.

Upper Bound (<T extends Class>)

An upper bound ensures the type must be a subclass of a specific class or implement a specific interface. Here’s an example:

public class NumberBox<T extends Number> {
    private T number;
    public void setNumber(T number) {
        this.number = number;
    }
    public double getDoubleValue() {
        return number.doubleValue();
    }
}

Explanation:

  • T extends Number means only subclasses of Number (like Integer, Double) are allowed.
  • You can now safely use Number methods, such as doubleValue().

Lower Bound (<? super T>)

A lower bound is typically used in method parameters to allow a type and its supertypes, which is useful when writing into a generic structure. Here’s an example:

public void addIntegers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
}

Explanation:

  • <? super Integer> means the list can accept Integer and its supertypes (like Number, Object).
  • This is useful when adding elements, but less flexible for reading them.

Using bounds helps you strike a balance between flexibility and control. As we continue, you’ll see how wildcards build further on these ideas to allow even more dynamic use of generics.

Working with Wildcards in Generics

While generics provide type safety, sometimes you don’t know the exact type you’ll be working with. That’s where wildcards come in. They offer flexibility when reading from or writing to generic types without needing to specify an exact type.

Java provides three kinds of wildcards: unbounded, upper-bounded, and lower-bounded.

<?> Unbounded Wildcard

The unbounded wildcard represents an unknown type. It’s useful when you’re only reading from a collection and don’t care about the specific type. Here’s an example:

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

Explanation:

  • <?> allows any type.
  • You can read values but can’t safely add new ones (except null).

<? extends T> Upper Bounded Wildcard

This wildcard allows you to read data of a specific type or its subclasses. Here’s an example:

public double sumNumbers(List<? extends Number> list) {
    double sum = 0;
    for (Number number : list) {
        sum += number.doubleValue();
    }
    return sum;
}

Explanation:

  • <? extends Number> accepts List<Integer>, List<Double>, etc.
  • Great when you’re only reading values from the list.

<? super T> Lower Bounded Wildcard

This wildcard allows you to write data of a specific type or its subclasses into a structure. Here’s an example:

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

Explanation:

  • <? super Integer> accepts List<Integer>, List<Number>, and List<Object>.
  • Suitable when you’re writing values but don’t need to read them back as Integer.

Wildcards can seem confusing at first, but they’re incredibly useful for making your code more adaptable. As you get comfortable with them, you’ll be able to design more flexible and reusable APIs.

Generic Methods (Generic and Non-Generic Classes)

You don’t need to create a generic class just to use generics. Java allows you to define generic methods—methods that declare their own type parameters.

These methods can be part of either generic or non-generic classes, and are especially useful when the method logic depends on a type that’s independent of the class itself.

Method-Level Type Parameters

A generic method declares the type parameter before the return type. Here’s an example:

public <T> void display(T item) {
    System.out.println("Item: " + item);
}

Explanation:

  • <T> before the return type declares T as a method-level type.
  • You can call this method with any object: display(“Hello”), display(123), etc.
  • It works even in a non-generic class.

Static Generic Methods

Generic static methods follow the same rule but can’t access class-level generic types (since static context is independent of instance-level data). Here’s an example:

public class Utils {
    public static <T> T returnFirst(T[] array) {
        return array[0];
    }
}

Explanation:

  • This static method works with any array type: String[], Integer[], etc.
  • Ensures type safety while maintaining the utility method’s flexibility.

Generic methods make your code cleaner and more reusable, especially when building utility functions or working with multiple types. They’re a powerful tool, whether you’re using generics at the class level or not.

Generic Interface and Its Implementation

In Java, just like classes and methods, interfaces can also be generic. This means you can define an interface that works with any data type, and the actual type can be decided later when the interface is implemented. This adds a lot of flexibility and reusability to your code.

Generic interfaces are especially useful when building libraries, APIs, utility classes, or working with design patterns like the Java facade pattern, helping abstract and simplify complex subsystems. They allow you to write common logic once and then apply it to many different data types, without rewriting the interface each time.

Interface Example: Declaring a Generic Interface

To start, let’s declare a generic interface. We’ll use a type parameter T to make the interface work with any type. This allows the interface to remain flexible and be reused for different implementations.

interface Processor<T> {
    T process(T input);
}

Explanation:

  • T is a type parameter. It acts as a placeholder for any type (like String, Integer, User, etc.).
  • The method process() uses T both as an input and return type.
  • When a class implements this interface, it must provide the actual type it wants to use in place of T.

This setup gives you a flexible blueprint for writing reusable code. Let’s look at how it’s implemented.

Implementation Example: With Specified Type

Now, let’s implement the interface by specifying a concrete type—in this case, String. This means the class will only work with strings and must define how the process() method should behave for string inputs.

Implementation Example: With Specified Type
class StringProcessor implements Processor<String> {
    @Override
    public String process(String input) {
        return input.toUpperCase();
    }
}

Explanation:

  • Here, Processor<String> tells the compiler that this version of Processor will work specifically with String values.
  • The process() method takes a string as input and returns a string as output.
  • In this example, the method simply converts the input string to uppercase, but you can define any logic here.

This approach is useful when you know the type your implementation will handle and want to write logic tailored to it.

Alternative: Retaining Generic Type in Implementation

If you want the implementing class to remain generic too, you can keep the type parameter in the class itself. This makes the implementation more reusable and adaptable to different data types.

class IdentityProcessor<T> implements Processor<T> {
    @Override
    public T process(T input) {
        return input;
    }
}

Explanation:

  • In this version, the implementation remains generic by retaining the <T> in the class declaration.
  • The method simply returns whatever input it receives, without modifying it.
  • Since the class doesn’t bind the interface to a specific type, it can be reused with any type—String, Integer, custom objects, and more.

This is useful for creating general-purpose utilities where you don’t want to limit your class to one specific type. For example, this pattern could be used in a logging tool, a caching system, or a pass-through validator.

Generics with Java Collections

One of the most common and practical uses of generics in Java is within the Collections Framework. Collections like List, Set, and Map store groups of objects, but without generics, you would have to manually cast each object and risk runtime errors if the types don’t match.

By using generic types with collections, you make them type-safe. This means the compiler checks that only the correct type of data is added to the collection. It also removes the need for casting when retrieving elements and makes your code cleaner, safer, and easier to read. Let’s explore how generics work with some commonly used collections.

Example: Using Generics with Lists

When you use a generic with a List, you specify what type of object the list should hold. This prevents you from accidentally adding the wrong type of data and simplifies iteration.

List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
for (String name : names) {
    System.out.println(name);
}

Explanation:

  • List<String> means this list can only store String objects.
  • There’s no need to cast elements when retrieving them, as the compiler already knows they are of type String.
  • This improves readability and reduces the chances of ClassCastException at runtime.

Before generics, the same code would require casting, making it more error-prone and harder to maintain.

Example: Using Generics with Maps

Maps in Java hold key-value pairs. With generics, you can specify the type for both the key and the value, which keeps the map consistent and prevents accidental type errors.

Map<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("Math", 90);
scoreMap.put("Science", 85);
for (Map.Entry<String, Integer> entry : scoreMap.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

Explanation:

  • Map<String, Integer> tells the compiler that keys must be Strings and values must be Integers.
  • This ensures that when you add or retrieve data from the map, you are always working with the right types.
  • The entrySet() method provides access to each key-value pair, and generics make it easy to work with them without casting.

This results in safer and more predictable code, especially when working with large datasets or APIs.

Building a Java App? Let’s Do It Right.

FAQs on Generic Class in Java

Is an ArrayList a generic class?

Yes, ArrayList is a generic class in Java. It can store elements of any type, which you specify when creating the list. For example, ArrayList<String> stores only strings, and ArrayList<Integer> stores only integers. This helps ensure type safety and avoids the need for casting.

What is the difference between generic and non generic classes in Java?

A generic class works with any data type using type parameters (like <T>), while a non-generic class works with a specific type or uses Object to hold any type. Generic classes provide type safety, better code reusability, and avoid runtime type errors, unlike non-generic ones.

What is the difference between ArrayList and generic list?

ArrayList is a type of generic list. When you say “generic list”, you’re usually referring to an interface like List<T> that can have various implementations (ArrayList, LinkedList, etc.). The key difference is that List<T> is more flexible, while ArrayList is a specific implementation.

What is the main advantage of using generic classes in Java?

The main advantage is type safety. Generics allow you to catch type-related errors at compile time, reduce the need for casting, and write reusable and cleaner code that works with different data types.

What is the difference between generic and wrapper classes in Java?

Generic classes are user-defined or Java-provided classes that use type parameters to work with any object type. Wrapper classes (such as Integer, Double, and Boolean) are built-in Java classes that wrap primitive types into objects. They serve different purposes—generics for reusability, wrappers for treating primitives as objects.

Conclusion

Understanding Generic Classes in Java is a key step toward writing flexible, reusable, and type-safe code. Whether you’re building simple tools or large-scale applications, generics help you avoid errors and keep your code clean.

From generic interfaces to wildcards and bounded types, generics are widely used across Java, especially with collections. Once you get comfortable with them, you’ll notice how much easier and more efficient your code becomes.

If you’re looking to build robust Java applications, our experienced team can help. As a trusted Java development agency, we offer custom solutions tailored to your business needs. Contact us today to get started!

author
Mayur Upadhyay is a tech professional with expertise in Shopify, WordPress, Drupal, Frameworks, jQuery, and more. With a proven track record in web development and eCommerce development.