Generics in Java

Generic in Java

Introduction

Hi Guys! Today I will show you another important feature of the Java programming language called "Generics". Well, generics means defining and using generalized types instead of specific ones. We can find different types in Java such as classes and interfaces. When we define our classes, interface, and methods, we can use the aforementioned types as parameters to use the same code with different inputs. In normal parameters, we pass values as inputs but here in typed parameters, we pass types as inputs, which is the difference between those parameters. We have a few benefits of using generic classes rather than non-generic classes. Some of them are,

  • Checking types at compile-time instead of at run-time. Normally it is much easier to check types at compile-time rather than run-time.
  • Elimination of casting because the compiler knows the type before starting the application.
  • Enabling developers to write their own algorithms with generic types, hence they can increase the re-usability and the readability of the code.
We can use the Object typed parameters without generics in order to pass any typed value into the class but there is no way to check the type at compile-time and it may cause errors if the mistakenly pass wrong typed object into the class. The below example describes that.

package generics.oracle;

/*We can pass any type of object to this class but there is no way to check what has been passed.
  For example, we may mistakenly pass Integer into the class where it is required a string.*/
public class MyObjectClass {

    private Object object;

    public Object getObject() {
        return object;
    }

    public void setObject(Object object) {
        this.object = object;
    }
}

But if we use generic types, we will be able to pass the exact type into the class, interfaces, or methods because it will be checked by the compiler at compile-time.

package generics.oracle;

/*We can check the type that we should to pass into the class.*/
public class MyGenericClass<T> {

    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}

By convention, we can use single letters in upper case format in order to denote type parameters. The following letters are the most commonly used type parameters.

  • E - Element (in Java Collections Framework)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S, U, V etc. - 2nd, 3rd, 4th types
We can instantiate the aforementioned MyGenericClass<T> and invoke its methods as below.

package generics.oracle;

public class GenericsDemo {

    public static void demoGenerics() {
        // Invoking and Instantiating a Generic Type
        MyGenericClass<String> stringMyGenericClass = new MyGenericClass<>();
        stringMyGenericClass.setT("Hello");
        System.out.println(stringMyGenericClass.getT());

        MyGenericClass<Integer> integerMyGenericClass = new MyGenericClass<>();
        integerMyGenericClass.setT(1000);
        System.out.println(integerMyGenericClass.getT());
    }
}

Generic classes can have multiple type parameters as well. Look at the below implementation. It is about the different data plans.

TelecomServicePlan.java

package generics.oracle;
public interface TelecomServicePlan<T, S> { T getPlanName(); S getDataLimit(); double getMonthlyCost(); }

TelecomServicePlanImpl.java

package generics.oracle;
public class TelecomServicePlanImpl<T, S> implements TelecomServicePlan<T, S> { private final T planName; private final S dataLimit; private final double monthlyCost; public TelecomServicePlanImpl(T planName, S dataLimit, double monthlyCost) { this.planName = planName; this.dataLimit = dataLimit; this.monthlyCost = monthlyCost; } @Override public T getPlanName() { return planName; } @Override public S getDataLimit() { return dataLimit; } @Override public double getMonthlyCost() { return monthlyCost; } @Override public String toString() { return "TelecomServicePlanImpl{" + "planName=" + planName + ", dataLimit=" + dataLimit + ", monthlyCost=" + monthlyCost + '}'; } }

GenericsDemo.java

package generics.oracle;
public class GenericsDemo { public static void demoGenerics() { // Mobile plan TelecomServicePlan<String, Integer> mobilePlan = new TelecomServicePlanImpl<>("Mobile_Plan_1", 5, 200.00); System.out.println(mobilePlan.getPlanName()); System.out.println(mobilePlan.getDataLimit()); System.out.println(mobilePlan.getMonthlyCost()); // Broadband plan TelecomServicePlan<String, Double> broadbandPlan = new TelecomServicePlanImpl<>("Broadband_Plan_1", 30.50, 2000.00); System.out.println(broadbandPlan.getPlanName()); System.out.println(broadbandPlan.getDataLimit()); System.out.println(broadbandPlan.getMonthlyCost()); } }

Type Inference

Type inference in generics in Java refers to the ability of the compiler to automatically determine the generic type argument based on the context in which the generic method or class is used. It allows you to use generics without explicitly specifying the type argument, making the code more concise and readable.

Type inference was introduced in Java 7, and it simplifies the syntax when working with generics. Prior to Java 7, you had to specify the type argument explicitly, even when it was obvious from the context. With type inference, the compiler can infer the type argument by examining the method's or class's invocation or assignment context.

Here's an example to illustrate type inference:

// Prior to Java 7
List<String> names1 = new ArrayList<String>();

// Java 7 and later (Type Inference)
List<String> names2 = new ArrayList<>();

In the first example (prior to Java 7), you had to explicitly specify the type argument String when creating the ArrayList. In the second example (Java 7 and later), type inference is used, and the compiler automatically infers the type argument String based on the variable declaration.

Type inference in generics not only simplifies the code but also reduces the chances of errors related to type mismatches. It is especially useful when working with complex generic types, where specifying the type argument explicitly can be cumbersome. However, it's important to note that type inference has limitations, and in some cases, you may still need to explicitly specify the type argument, especially when dealing with nested generics or ambiguous cases. Nonetheless, type inference is a valuable feature that improves the readability and maintainability of code when working with generics in Java.

Wildcards

In Java generics, wildcards are used to provide more flexibility when working with generic types. They allow you to define generic classes, methods, or interfaces that can work with different types, including unknown types, without having to specify the exact type parameter.

There are three types of wildcards in Java generics: Unbounded Wildcard (?), Upper Bounded Wildcard (? extends T), and Lower Bounded Wildcard (? super T)

Unbounded Wildcard (?): Represented by a question mark ?, it allows the generic type to match any type. It is useful when you want to work with a collection of unknown types or when the specific type parameter is not important for the operation.

Example of an unbounded wildcard:

List<?> myList; // myList can be a List of any type

Upper Bounded Wildcard (? extends T): The upper bounded wildcard restricts the generic type to be a specific type or any of its subclasses. It is used when you want to work with a collection of objects of a certain type and its subclasses.

Example of an upper bounded wildcard:

List<? extends Number> numbersList; // numbersList can be a List of Number or its subclasses

Lower Bounded Wildcard (? super T): The lower bounded wildcard restricts the generic type to be a specific type or any of its superclasses. It is used when you want to work with a collection of objects of a certain type and its superclasses.

Example of a lower bounded wildcard:

List<? super Integer> integerList; // integerList can be a List of Integer or its superclasses (e.g., Number, Object)

Usage of wildcards in Java generics allows you to create more flexible and reusable code when dealing with unknown or diverse types. Wildcards enable you to work with collections of objects with different types, perform operations that are independent of specific types, and avoid unnecessary casting.

Example of using wildcards in a generic method:

WildcardsExample.java

package generics.oracle;
import java.util.List; public class WildcardsExample { public static double sumOfNumbers(List<? extends Number> numbers) { double sum = 0.0; for (Number number : numbers) { sum += number.doubleValue(); } return sum; } }

GenericsDemo.java

package generics.oracle;
import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class GenericsDemo { public static void demoGenerics() { // Wildcards examples List<Integer> integers = Arrays.asList(1, 2, 3); List<Double> doubles = Arrays.asList(1.5, 2.5, 3.5); System.out.println(WildcardsExample.sumOfNumbers(integers)); System.out.println(WildcardsExample.sumOfNumbers(doubles)); } }

Type Erasure

Type erasure is a process in Java generics where the compiler replaces the generic type parameters with their bounding types or with the raw types. It happens during the compilation process, and the actual generic type information is removed ("erased") at runtime.

The purpose of type erasure is to maintain backward compatibility with older Java versions that do not support generics. Type erasure allows code written with generics to interact seamlessly with non-generic code, ensuring that generic code can be used in environments that do not have knowledge of generics.

Key points about type erasure in Java generics:

  • Type erasure applies only to generic types, not to non-generic types.
  • During compilation, the generic type parameters are replaced with their upper bounds (in the case of upper-bounded wildcards) or with the raw types.
  • For example, if you have a generic class MyClass<T>, all occurrences of T within the class are replaced with its upper bound or with Object if no upper bound is specified.
  • Type information is not available at runtime for generic types. For example, List<Integer> and List<String> are both treated as List at runtime.
  • The use of type erasure allows generic classes and methods to be compiled into bytecode that is compatible with older non-generic Java versions.
Here's an example to illustrate type erasure:

TypeErasureExample.java

package generics.oracle;

public class TypeErasureExample<T> {

    private final T value;

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

    public T getValue() {
        return value;
    }
}

GenericsDemo.java

package generics.oracle;

import java.util.Arrays;
import java.util.List;

public class GenericsDemo {

    public static void demoGenerics() {
        // Type erasure examples
        TypeErasureExample<Integer> integerObject = new TypeErasureExample<>(42);
        TypeErasureExample<String> stringObject = new TypeErasureExample<>("Hello");

        System.out.println(integerObject.getValue()); // Output: 42
        System.out.println(stringObject.getValue()); // Output: Hello

        System.out.println(integerObject.getValue() instanceof Integer); // Output: true
        System.out.println(stringObject.getValue() instanceof String); // Output: true

        // At runtime, both classes are treated as Object
        System.out.println(integerObject.getValue() instanceof Object); // Output: true
        System.out.println(stringObject.getValue() instanceof Object); // Output: true
    }
}

Restrictions

Generics in Java provide powerful type safety and flexibility, but they come with some restrictions that developers need to be aware of to use them effectively. Here are the main restrictions on generics in Java:

  1. Cannot Instantiate Generic Types with Primitive Types: Java generics do not support using primitive types as type arguments. For example, you cannot instantiate a generic class with int, double, or char as type parameters. Instead, you must use their corresponding wrapper classes, such as Integer, Double, or Character.
  2. Cannot Create Arrays of Generic Types: Java does not allow creating arrays of parameterized types, such as List<String>[] or T[]. You can use collections (e.g., List<List<String>>) instead of arrays to work with generic types.
  3. Cannot Use Type Parameters in Static Context: Generic type parameters cannot be used in static contexts, such as static fields or static methods, because static members are shared among all instances of the class, and the type parameter's value is not specific to any instance.
  4. Cannot Catch Generic Types with Catch Clause: Java does not allow catching generic types in catch clauses. For example, you cannot write catch (List<String> e); instead, you must use the raw type catch (Exception e) or catch specific exceptions.
  5. Cannot Create Instances of Type Parameters: You cannot create new instances of a type parameter directly, such as new T() inside a generic class or method. This limitation is due to type erasure, which erases the generic type information at runtime.
  6. Cannot Overload Based on Generic Types: Java does not allow overloading methods based solely on generic types. For example, you cannot define two methods void myMethod(List<String> list) and void myMethod(List<Integer> list) because they have the same erasure at runtime.
  7. Cannot Access Static Members with Parameterized Types: You cannot access static members of a generic class using parameterized types. For example, you cannot use T.myStaticField or T.myStaticMethod() because the type information for T is not available at runtime.
  8. Cannot Create Instances of Type Parameters in Arrays or Varargs: Java does not allow creating arrays of type parameters, such as T[], or using type parameters in varargs, such as T.... This limitation is due to type erasure.
Understanding these restrictions is crucial to writing effective and type-safe code with generics in Java. It's essential to be mindful of the limitations and design your code accordingly to avoid potential issues and ensure the proper functioning of generic types in your programs.

In conclusion, generics in Java are a powerful feature that enables type safety, code reusability, and flexibility when working with collections and classes. They allow us to create generic algorithms and data structures that can work with different types without sacrificing type safety. Generics help catch type-related errors at compile time, providing early feedback and reducing runtime exceptions. However, it's important to be aware of the restrictions on generics, such as the inability to use primitive types as type arguments or create arrays of parameterized types. By understanding these limitations and leveraging the full potential of generics, Java developers can write more concise, robust, and maintainable code that scales well across different data types and use cases. Embracing generics is a key step towards writing elegant and type-safe code that enhances code quality and promotes a better programming experience in the world of Java development.

0/Post a Comment/Comments