Generic
Generics are a mechanism that make it possible to use one function or class to handle many different data types. By using generics, we can design a single function/class that operates on data of many types, instead of having to create a separate function/class for each type. In this chapter we would first look at using generics with functions and then move on to using generics with classes.
Generic Functions
In Java, generics provide a way to write more flexible and reusable code. By using generics, you can create methods or classes that work with any data type, reducing code duplication and increasing maintainability. Here’s a breakdown of how generics help, particularly in the context of printing arrays and comparing or sorting elements of different types.
Problem with Overloaded Methods
Initially, to print arrays of different data types such as int, float, and char, we might write separate overloaded functions like this:
void printArr(int[] arr) {
for (int i : arr) System.out.println(i);
}
void printArr(float[] arr) {
for (float i : arr) System.out.println(i);
}
void printArr(char[] arr) {
for (char i : arr) System.out.println(i);
}
However, this approach has several disadvantages:
- Code Duplication: The same logic is repeated for each data type.
- Maintenance Issues: Any change in logic requires modification in all overloaded methods.
- Increased Code Size: Multiple versions of the function take up more space and require extra time to write and debug.
Solution: Generic Functions
Instead of writing separate functions for each data type, we can use generics to create a single function that works with any type.
Example: Generic Function for Printing Arrays
We can use a generic function to replace the multiple overloaded printArr functions:
public static <T> void printArray(T[] arr) {
for (T element : arr) {
System.out.printf("%s ", element);
}
System.out.println();
}
Here, T is a type parameter that acts as a placeholder for any type. The function can now be used with any data type:
public static void main(String[] args) {
Integer[] intArr = {10, -2, 37, 42, 15};
Float[] floatArr = {3.14f, 6.28f, -1.5f, -3.44f, 7.234f};
Character[] charArr = {'Q', 'U', 'E', 'S', 'T'};
printArray(intArr);
printArray(floatArr);
printArray(charArr);
}
Output:
10 -2 37 42 15
3.14 6.28 -1.5 -3.44 7.234
Q U E S T
This generic function works with any standard type, such as Integer, Float, Character, etc., significantly reducing code size and improving flexibility.
How Generics Work
The generic function is defined as:
public static <T> void printArray(T[] arr) {
for (T element : arr) {
System.out.printf("%s ", element);
}
System.out.println();
}
T: A type parameter that stands for any reference type. It is a placeholder for the actual type that will be passed when the function is called.- The function can now accept any array of objects (like
Integer[],Float[],Character[]), but not primitive arrays (likeint[],float[],char[]), because generics work only with reference types.
Generic Functions with Constraints
In some cases, you may want to restrict the types a generic function can work with. For example, you can use the Comparable interface to compare elements.
Example: Generic Function to Find the Minimum Value
public static <T extends Comparable<T>> T minimum(T a, T b) {
return (a.compareTo(b) < 0) ? a : b;
}
- The type parameter
Tis restricted to types that implement theComparableinterface. - The
compareTomethod is used to compare the values.
Usage Example:
public static void main(String[] args) {
Float a = 3.14f, b = -6.28f;
Character ch = 'A', dh = 'Z';
System.out.println(minimum(a, b)); // Output: -6.28
System.out.println(minimum(ch, dh)); // Output: A
}
Sorting Arrays with a Generic Function
You can also use generics to create a sorting function that works for any type that implements Comparable.
Example: Generic Sorting Function
public static <T extends Comparable<T>> void sort(T[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
for (int j = i + 1; j < arr.length; j++) {
if (arr[i].compareTo(arr[j]) > 0) {
// Swap arr[i] and arr[j]
T temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
Usage Example:
public static void main(String[] args) {
Float[] floatArr = {5.4f, 3.23f, 2.15f, 1.09f, 34.66f};
Integer[] intArr = {-12, 23, 14, 0, 245, 78, 66, -9};
sort(floatArr);
sort(intArr);
System.out.println(Arrays.toString(floatArr)); // Output: [1.09, 2.15, 3.23, 5.4, 34.66]
System.out.println(Arrays.toString(intArr)); // Output: [-12, -9, 0, 14, 23, 66, 78, 245]
}
Multiple Argument Types
In Java, generics allow us to write methods, classes, and interfaces that can work with any data type while still providing type safety. Generics are particularly useful when we want to write code that can handle different types of objects in a flexible and reusable manner.
When we talk about multiple argument types in generic methods, we refer to the ability of a generic method to accept parameters of different types, without requiring separate methods for each possible combination of argument types.
Understanding Multiple Argument Types in Generic Methods
In Java, generics allow us to write methods, classes, and interfaces that can work with any data type while still providing type safety. Generics are particularly useful when we want to write code that can handle different types of objects in a flexible and reusable manner.
When we talk about multiple argument types in generic methods, we refer to the ability of a generic method to accept parameters of different types, without requiring separate methods for each possible combination of argument types.
What Does It Mean to Have Multiple Argument Types?
In Java, a generic method can be defined to accept multiple arguments, each of a different type. These types are specified using generic type parameters. Unlike a non-generic method, which only accepts arguments of specific types, a generic method with multiple argument types uses type parameters to allow different types to be passed in.
For example, in a method that accepts three different types of arguments, we can use type parameters to represent these types.
Generic Method with Multiple Argument Types
Here’s a breakdown of how you can create and use a generic method with multiple argument types:
Generic Method Definition with Multiple Type Parameters
A generic method with multiple argument types is defined by specifying the type parameters in angle brackets (< >) after the method’s name. These type parameters can represent different data types, and the method can accept arguments of different types during the call.
Example:
public static <T, S, Z> void printTypes(T a, S b, Z c) {
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
}
In this method definition:
<T, S, Z>: These are the generic type parameters.T,S, andZcan represent any data types (e.g.,Integer,Float,Character, etc.).T a, S b, Z c: These are the method parameters that accept different types. Each of these parameters will be replaced by the types passed when the method is called.
Example of Method Invocation with Different Argument Types
public static void main(String[] args) {
Integer i = 10;
Float j = 3.14f;
Character ch = 'A';
// Call the generic method with three different types
printTypes(i, j, ch);
}
- Here,
iis anInteger,jis aFloat, andchis aCharacter. - The generic method
printTypescan accept these three different types because it is defined with multiple type parameters (T,S,Z).
The output of this code will be:
a = 10
b = 3.14
c = A
Example of Method Invocation with Same Argument Types
The same generic method can also be used when all the arguments are of the same type. For example:
Integer m = 20, n = 30;
printTypes(m, m, n); // All arguments are of type Integer
In this case, the method will still work perfectly because the generic method is flexible enough to handle the same type for all arguments.
The output will be:
a = 20
b = 20
c = 30
Why Use Multiple Argument Types?
Using multiple argument types in generic methods is beneficial because:
- Flexibility: A single generic method can handle different types of inputs, avoiding the need to create overloaded methods for each combination of argument types.
- Type Safety: Generics provide compile-time type checking, ensuring that the types passed to the method are compatible with the types expected by the method.
- Reusability: A method that can accept multiple types can be reused in many different situations without duplicating code.
Use Cases for Multiple Argument Types
- Printing different types: You could write a generic method to print values of different types, as demonstrated earlier with
printTypes. This could be useful in logging or debugging. - Data processing: If you are processing collections of various types, you could use generic methods to apply the same processing logic to various types of data.
- Handling mixed data: When you need to handle different types of objects in a flexible way (e.g., in containers like stacks, queues, etc.), generic methods with multiple argument types can simplify the implementation.
Example: Generic Method for Swapping Values
Let’s look at another example where a generic method with multiple argument types could be useful. We’ll create a method that swaps two values, regardless of their types:
public class GenericSwap {
public static <T, S> void swap(T a, S b) {
System.out.println("Before swap: a = " + a + ", b = " + b);
// Swapping values (for demonstration purposes)
// We can't swap values in the traditional sense, but we can print them
System.out.println("After swap: a = " + b + ", b = " + a);
}
public static void main(String[] args) {
Integer i = 5;
String s = "Hello";
swap(i, s); // swapping Integer and String values
Float f = 3.14f;
Character ch = 'X';
swap(f, ch); // swapping Float and Character values
}
}
Output:
Before swap: a = 5, b = Hello
After swap: a = Hello, b = 5
Before swap: a = 3.14, b = X
After swap: a = X, b = 3.14
This example shows how we can use multiple argument types (T and S) to swap values of different types, even though swapping isn’t logically meaningful for all data types. The point here is flexibility — the method can be used with Integer, String, Float, Character, or any other types.
Generic Classes
Detailed Explanation of Generic Classes in Java
What is a Generic Class?
In Java, a generic class is a class that can operate on objects of any type. By using generics, you can create a class where the type of the data is specified at runtime when an object is instantiated. This allows the class to handle different types of objects without writing multiple versions of the same class.
Generics in Java help with:
- Type safety: Prevents type errors during runtime by ensuring that objects are only used with the correct data type.
- Code reusability: Enables writing flexible code that can work with any data type.
Key Concepts of Generic Classes
- Type Parameterization:
- In a generic class, the type of the object the class will operate on is specified using a type parameter (e.g.,
T,E,K,V, etc.). This is written in angle brackets (<>) after the class name. - For example, in the class
Stack<T>, theTis a placeholder that represents any type the stack will store.
- Using Generic Types:
- You can declare a generic type and use it for fields, method parameters, and return types in the class.
- This allows a generic class to operate on different types of data without compromising type safety.
- Type Inference and Instantiation:
- When creating an object of a generic class, you specify the actual type to replace the type parameter. This is called instantiating the generic class.
- Type Bounds:
- You can restrict the types that can be used as a parameter to a generic class using bounded type parameters. For example,
class Stack<T extends Number>would restrict the generic typeTto classes that extendNumber.
Structure of a Generic Class
The basic structure of a generic class is:
class ClassName<T> {
// Fields, methods, and constructors using T
}
<T>: This defines a type parameterT. The nameTis arbitrary; it can be any valid identifier likeE,K,V, etc. It just represents a placeholder for a specific type that will be provided when an instance of the class is created.- Fields, Constructors, and Methods: You can use
Tin fields, constructor parameters, return types, and method parameters. This makes the class flexible enough to accept different types at runtime.
Example: Generic Stack Class
Let’s walk through a detailed explanation of the generic stack implementation, which is a basic data structure that follows the Last In, First Out (LIFO) principle. The Stack class can hold any type of object, such as Integer, Float, Character, or even custom objects.
Generic Stack Class Code
class Stack<T> {
private T[] arr; // Array to hold stack elements of type T
private int top; // Index of the top element in the stack
private int size; // Size of the stack
// Constructor to initialize the stack with a given size
Stack(int sz) {
size = sz;
top = -1; // Indicates that the stack is empty initially
arr = (T[]) new Object[sz]; // Typecasting to a generic array
}
// Method to check if the stack is full
boolean isFull() {
return top == size - 1;
}
// Method to push an element onto the stack
void push(T data) {
if (!isFull()) {
arr[++top] = data; // Add the element and update the top index
} else {
System.out.println("Stack is full");
}
}
// Method to check if the stack is empty
boolean isEmpty() {
return top == -1;
}
// Method to pop an element from the stack
T pop() {
if (!isEmpty()) {
return arr[top--]; // Return the top element and update the top index
}
return null; // Return null if stack is empty
}
}
Explanation of the Code:
- Generic Type Parameter (
T):
- The class
Stack<T>is a generic class whereTrepresents the type of elements the stack will hold. When you create a stack object, you specify what type the stack will hold (e.g.,Stack<Integer>for a stack of integers).
- Array of Type
T(arr):
- The stack uses an array
arrto store elements. The array is created with the generic typeTat runtime, allowing it to store any type of object. - The array is initialized as
(T[]) new Object[sz]. Since Java doesn’t allow the creation of a generic array directly (new T[]is not allowed), we create anObject[]array and cast it toT[].
- Constructor:
- The constructor initializes the stack with a given size
sz, creates the arrayarrof that size, and setstopto-1, indicating the stack is initially empty.
isFull()andisEmpty():
isFull()checks if the stack has reached its maximum capacity (i.e.,top == size - 1).isEmpty()checks if the stack is empty (i.e.,top == -1).
push(T data):
- The
push()method adds an element of typeTto the stack, as long as the stack isn’t full. The element is added to thetopof the stack (incrementing thetopindex).
pop():
- The
pop()method removes the element at thetopof the stack and returns it. Thetopindex is then decremented.
Example Usage of Generic Stack
In the following example, we create stacks of different types (Integer, Float, and Complex), demonstrate pushing and popping elements, and print the popped elements:
public class GenericStackDemo {
public static void main(String[] args) {
// Stack of Integers
Stack<Integer> s1 = new Stack<>(10); // Stack of Integer with size 10
s1.push(10);
s1.push(20);
s1.push(30);
// Popping and printing Integer elements
if (!s1.isEmpty()) {
System.out.println(s1.pop()); // Output: 30
}
if (!s1.isEmpty()) {
System.out.println(s1.pop()); // Output: 20
}
// Stack of Floats
Stack<Float> s2 = new Stack<>(10); // Stack of Float with size 10
s2.push(10.5f);
s2.push(20.5f);
s2.push(18.5f);
// Popping and printing Float elements
if (!s2.isEmpty()) {
System.out.println(s2.pop()); // Output: 18.5
}
if (!s2.isEmpty()) {
System.out.println(s2.pop()); // Output: 20.5
}
// Stack of Complex Numbers
Stack<Complex> s3 = new Stack<>(10); // Stack of Complex with size 10
Complex c1 = new Complex(1.1f, 2.2f);
Complex c2 = new Complex(3.3f, 4.4f);
Complex c3 = new Complex(5.5f, 6.6f);
s3.push(c1);
s3.push(c2);
s3.push(c3);
// Popping and printing Complex objects
if (!s3.isEmpty()) {
Complex c = s3.pop();
c.printData(); // Output: Real = 5.5 Imag = 6.6
}
if (!s3.isEmpty()) {
Complex c = s3.pop();
c.printData(); // Output: Real = 3.3 Imag = 4.4
}
}
}
- Output:
30
20
18.5
20.5
Real = 5.5 Imag = 6.6
Real = 3.3 Imag = 4.4
Advantages of Using Generic Classes
- Type Safety:
- Generics provide compile-time type checking, which prevents ClassCastExceptions at runtime. For example, if you try to push a
Stringonto aStack<Integer>, the compiler will give an error.
- Generics provide compile-time type checking, which prevents ClassCastExceptions at runtime. For example, if you try to push a
- Code Reusability:
- The same
Stackclass can be reused for different types without rewriting the class for each data type. This reduces code duplication and increases maintainability.
- The same
- Flexibility:
- Generics allow you to define more general and flexible classes that work with various data types. You can write code that is not dependent on specific data types but can still enforce strong type constraints.
- Improved Readability:
- Generics improve code readability by making it clear what types of objects the class operates on.
Bounded Generics
Bounded Generics in Java
Bounded Generics allow us to restrict the types that can be used with a generic class or method. In other words, it allows you to limit the generic type to certain classes or interfaces. This ensures that the generic class can only accept types that meet specific criteria, making the code more robust and type-safe.
In your example, the class Statistics<T> is bounded so that it can only accept types that are derived from the Number class. This prevents using non-numeric types (like String) with this generic class.
Bounded Generics Syntax
The bounded generic type is defined using the extends keyword:
class Statistics<T extends Number> {
// Class body
}
T extends Number: This restricts the generic typeTto only those types that are subtypes ofNumber, such asInteger,Float,Double, etc. It ensures that only numeric types can be passed to the class.
Example: Statistics Class with Bounded Generics
Here’s the example program that demonstrates the use of bounded generics in a Statistics class, which calculates the average of an array of Integer or Float values, but not other types like String.
package statsdemo;
public class StatsDemo {
public static void main(String[] args) {
Integer[] iarr = {1, 2, 3, 4, 5}; // Array of Integers
Statistics<Integer> iobj; // Instantiate Statistics class with Integer
double avg1; // Variable to store the average
iobj = new Statistics<Integer>(iarr); // Pass Integer array to Statistics class
avg1 = iobj.getAverage(); // Get average of Integer array
System.out.println("avg1 = " + avg1); // Output: avg1 = 3.0
Float[] farr = {1.1f, 2.1f, 1.0f}; // Array of Floats
Statistics<Float> fobj; // Instantiate Statistics class with Float
double avg2; // Variable to store the average
fobj = new Statistics<Float>(farr); // Pass Float array to Statistics class
avg2 = fobj.getAverage(); // Get average of Float array
System.out.println("avg2 = " + avg2); // Output: avg2 = 1.7333333...
}
}
class Statistics<T extends Number> {
private T[] arr; // Array to hold the numeric values
// Constructor: Initializes the Statistics object with an array of numbers
Statistics(T[] obj) {
arr = obj;
}
// Method to compute the average of the values in the array
public double getAverage() {
double sum = 0.0;
for (int i = 0; i < arr.length; i++) {
sum = sum + arr[i].doubleValue(); // Convert each number to double and add to sum
}
return sum / arr.length; // Return the average
}
}
How the Program Works
- Statistics Class:
- The
Statisticsclass is a generic class with a type parameterTthat is bounded byNumber. This means the typeTmust be a subclass ofNumber(i.e.,Integer,Float,Double, etc.). - The
Statisticsclass contains an array of typeTthat holds the numeric values. - The constructor accepts an array of
Tand initializes the internalarrfield with this array. - The method
getAverage()calculates the average by iterating through the array, summing the elements using thedoubleValue()method (which is available in all subclasses ofNumber), and then dividing by the length of the array.
- Type Bounding (
T extends Number):
- The line
class Statistics<T extends Number>ensures that only classes that inherit fromNumber(likeInteger,Float,Double) can be used as the generic typeT. This prevents non-numeric types likeStringorBooleanfrom being used, thus avoiding runtime errors.
- Using the Class in
StatsDemo:
- The program demonstrates how to create
Statisticsobjects with different numeric types (IntegerandFloat). - For each array (
iarrfor integers andfarrfor floats), the program creates a correspondingStatisticsobject (iobjfor integers andfobjfor floats), computes the average, and prints the result.
Output of the Program
avg1 = 3.0
avg2 = 1.7333333333333334
Key Points:
T extends Number: This restrictsTto be a subclass ofNumber, allowing only numeric types. This ensures that methods likedoubleValue()can be called onT, which is required to perform arithmetic operations like sum and average.- Type Safety: By restricting the generic type to
Number, we avoid passing in non-numeric types likeStringto theStatisticsclass, which ensures that the class works only with numeric values. - Reusability: The
Statisticsclass can now work with any numeric type (such asInteger,Float,Double, etc.), and the code does not need to be rewritten for each new numeric type.
Bounded Generics: Why Use Them?
- Type Safety: Bounded generics ensure that the code only works with appropriate types, preventing invalid types at compile time. For example, if we try to pass a
Stringto theStatisticsclass, the compiler will throw an error. - Flexibility with Restrictions: While bounded generics provide flexibility by working with different types, they allow you to place restrictions on what types can be used. This ensures that only types that make sense for a particular class (in this case, numeric types) are allowed.
- Preventing Runtime Errors: By bounding the generic type, we avoid runtime errors like
ClassCastExceptionthat could occur if inappropriate types were passed to the class.