Java Templates I: What You Need to Know


Table of Contents
Introduction to Java Generics
Basic Generic Template
<T>
Double and Triple Templates
<T, R>
Making a Method Generic While the Class is Not
Bounded Type Parameters
Multiple Bounds in Generics
Conclusion
References
In Java, Generics allow you to create classes, methods, and interfaces that can operate on any data type, thereby reducing code duplication and increasing type safety. Generics enable you to write flexible and reusable code, and they allow you to work with unknown types, denoted by type parameters such as <T>
. Java templates (also known as generics) are especially useful for collections and algorithms that should work with multiple types.
Let's dive into each concept with code examples.
Basic Generic Template: <T>
with a Class
A simple use of generics involves defining a class with a type parameter <T>
. Here's an example of a generic class called Box
, which can hold any type of object:
Code Example: Generic Class with <T>
// A generic class that works with any type T
class Box<T> {
private T value;
// Constructor to set the value
public Box(T value) {
this.value = value;
}
// Method to get the value of the box
public T getValue() {
return value;
}
// Method to set the value
public void setValue(T value) {
this.value = value;
}
}
public class Main {
public static void main(String[] args) {
// Creating a Box for Integer
Box<Integer> intBox = new Box<>(123);
System.out.println("Integer value: " + intBox.getValue());
// Creating a Box for String
Box<String> strBox = new Box<>("Hello");
System.out.println("String value: " + strBox.getValue());
}
}
Explanation:
The class
Box<T>
has a type parameterT
that is replaced by a specific type (e.g.,Integer
orString
) when an object is created.The method
getValue()
andsetValue()
both use the generic typeT
.
Example of Non-Compiling Code:
Box<Integer> intBox = new Box<>(123);
intBox.setValue("This won't compile!"); // Error: incompatible types: String cannot be converted to Integer
This code does not compile because we're trying to set a String
in a Box<Integer>
. Generics ensure type safety at compile time.
Double and Triple Templates: <T, R>
Generics can have more than one type parameter. For example, let's create a Pair
class that can hold two objects of different types.
Code Example: Double Generic Class <T, R>
// A generic class that works with two types T and R
class Pair<T, R> {
private T first;
private R second;
public Pair(T first, R second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public R getSecond() {
return second;
}
}
public class Main {
public static void main(String[] args) {
// Creating a Pair with Integer and String
Pair<Integer, String> pair = new Pair<>(1, "One");
System.out.println("First: " + pair.getFirst());
System.out.println("Second: " + pair.getSecond());
}
}
Explanation:
Pair<T, R>
uses two type parameters,T
andR
. It can store two values of different types.In the
main
method, aPair<Integer, String>
is created, meaning the first element is of typeInteger
, and the second is of typeString
.
Example of Non-Compiling Code:
Pair<Integer, String> pair = new Pair<>(1, "One");
pair.setFirst("This won't compile!"); // Error: String cannot be converted to Integer
This code does not compile because we're trying to set a String
in place of an Integer
.
You can also add more type parameters, like in a Triple<T, U, V>
class, but it's less common.
Making a Method Generic While the Class is Not
In Java, it's possible to define a generic method in a non-generic class. This is useful when only certain methods need to operate on different types, while the rest of the class is not type-specific.
Generic Method with Two Type Parameters
You can define a method with two type parameters, which allows the method to handle different types for its arguments. Let’s extend the Utility
class with a method that compares two different types.
Code Example: Generic Method in Non-Generic Class (Two Types)
// A non-generic class
class Utility {
// A generic method with two different type parameters
public <T, U> void printTypes(T obj1, U obj2) {
System.out.println("Type of obj1: " + obj1.getClass().getName());
System.out.println("Type of obj2: " + obj2.getClass().getName());
}
}
public class Main {
public static void main(String[] args) {
Utility util = new Utility();
// Calling the method with a String and an Integer
util.<String, Integer>printTypes("Hello", 123);
// Type inference: no need to specify <String, Integer>
util.printTypes("World", 456.78); // Inferred types: String and Double
}
}
Explanation:
The method
printTypes
has two type parameters<T, U>
, meaningobj1
andobj2
can be of different types.In the
main
method, we explicitly specify the types<String, Integer>
when calling the method, but this is optional as Java can infer the types from the method arguments.The method prints the class names of the passed objects.
Example of Non-Compiling Code:
javaCopy code// This will cause a compilation error due to type mismatch
Utility util = new Utility();
util.<String, Integer>printTypes(123, "Hello"); // Error: inferred type mismatch
This code will not compile because we explicitly stated that the first parameter should be a String
and the second should be an Integer
, but provided 123
(an Integer
) as the first parameter and "Hello"
(a String
) as the second.
Bounded Type Parameters
Sometimes, you want to restrict the types that can be used with a generic class or method. Bounded type parameters allow you to specify that a type must be a subclass (or implementer) of a specific class or interface.
Code Example: Bounded Type Parameter
// A generic class that only works with Number or its subclasses
class Calculator<T extends Number> {
private T number;
public Calculator(T number) {
this.number = number;
}
public double square() {
return number.doubleValue() * number.doubleValue();
}
}
public class Main {
public static void main(String[] args) {
Calculator<Integer> intCalc = new Calculator<>(4);
System.out.println("Square of 4: " + intCalc.square());
Calculator<Double> doubleCalc = new Calculator<>(5.5);
System.out.println("Square of 5.5: " + doubleCalc.square());
}
}
Explanation:
The class
Calculator<T>
has a bounded type parameterT extends Number
. This means thatT
must be a subclass ofNumber
(e.g.,Integer
,Double
,Float
, etc.).This restriction allows us to call methods like
doubleValue()
that are available in theNumber
class.
Example of Non-Compiling Code:
Calculator<String> strCalc = new Calculator<>("This won't compile!"); // Error: String is not a subtype of Number
This code does not compile because String
is not a subclass of Number
.
Do you think that T super Number
is supported in Java? No, and I'll explain why. However, this concept can be addressed using wildcards, which we will discuss in the next article.
Why T super ClassName
is Not Supported in Java
When you declare a generic type parameter, like <T>
, you are saying that T
represents a specific type or class. The type parameter is invariant, meaning that the type is fixed once it's determined at compile-time.
For example, if you declare a generic class as
<T extends ClassName>
, you are constrainingT
to be eitherClassName
or any subclass ofClassName
. This allows you to ensure thatT
has the methods and properties ofClassName
or its subclasses.However,
T super ClassName
would imply thatT
could be any superclass ofClassName
, which creates ambiguity when you use the type parameter in the class or method. This is because generics are meant to provide type safety, and allowingsuper
would weaken this guarantee sinceT
could now be a variety of types, possibly without the necessary properties or methods.
Multiple Bounds
You can specify that a type parameter must extend multiple classes or implement multiple interfaces. This is called multiple bounds.
Code Example: Multiple Bounds
// An interface
interface Printable {
void print();
}
// A class that implements the Printable interface
class Report implements Printable {
@Override
public void print() {
System.out.println("Printing report...");
}
}
// A generic class with multiple bounds (must be Number and Printable)
class Document<T extends Number & Printable> {
private T document;
public Document(T document) {
this.document = document;
}
public void process() {
document.print();
System.out.println("Document number: " + document.doubleValue());
}
}
public class Main {
public static void main(String[] args) {
// Compilation error: Integer does not implement Printable
// Document<Integer> doc = new Document<>(123);
// Compilation error: Report does not extend Number
// Document<Report> doc2 = new Document<>(new Report());
// Correct usage would be a class that extends Number and implements Printable (if it existed)
}
}
Explanation:
T extends Number & Printable
specifies thatT
must extendNumber
and implementPrintable
.The commented-out code examples will not compile:
Integer
extendsNumber
but does not implementPrintable
.Report
implementsPrintable
but does not extendNumber
.
In practice, it's rare to find or need a class that fulfills multiple bounds like this.
Conclusion
Generics in Java are a powerful feature that enhances code flexibility and reusability while ensuring type safety. By allowing classes, methods, and interfaces to operate on any data type, generics minimize code duplication and reduce runtime errors. In this article, we have explored:
Basic Generics: Using a single type parameter
<T>
in classes and methods to handle various types safely.Multiple Type Parameters: Implementing double and triple templates to manage objects of different types within a single class.
Generic Methods in Non-Generic Classes: Defining methods with type parameters to perform operations on diverse types without modifying the entire class.
Bounded Type Parameters: Restricting type parameters to subclasses or implementers of specific classes or interfaces to enforce constraints.
Unsupported Type Parameters: Explaining why
<T super ClassName>
is not supported and how generics ensure type safety.Multiple Bounds: Combining multiple bounds to specify that a type must meet several requirements.
Generics are crucial in writing robust, reusable, and type-safe code in Java. Understanding and correctly applying generics concepts, such as type parameters, bounds, and wildcards, can greatly enhance the efficiency and maintainability of your code.
References
Subscribe to my newsletter
Read articles from Omar Mohamed directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
