Java Generic Classes, Interfaces and Methods
Introduction
Using generic library classes such as
(Note - This tutorial builds on Introduction to Java GenericsClick to read tutorial: Introduction to Java Generics: Why and How.) A generic class A generic class declares one or more type parameters in angle brackets after its name. Inside the class, the type parameter is used like any other type - for fields, parameters and return types. Here is a
OUTPUT of the above code
Explanation of the code
OUTPUT of the above code
Type arguments can themselves be generic types, as in
A generic interface
Interfaces can be generic in the same way. A class implementing a generic interface either fixes the type argument, or stays generic itself and passes its own type parameter on.
OUTPUT of the above code
Generic methods
A generic method declares its own type parameters, in angle brackets before the return type. It can be in any class - generic or not - and can be static. The type parameter is available only inside that method.
When the method is called, the compiler usually infers the type argument from the arguments, so you rarely write it. If needed, it can be given explicitly:
OUTPUT of the above code
Explanation of the code
OUTPUT of the above code
This is the same pattern as
List<E> is only half the story - you can write your own. A generic class, interface or method is written once, and works type-safely with any type its users choose. This tutorial explains how to write generic classes, generic interfaces and generic methods in Java. We will start with a simple generic class, and then one with two type parameters. We will then write a generic interface, and implement it in two different ways. Next we will write generic methods, including static ones, and see how the compiler infers their type arguments. Lastly, we will look at when to make a method generic, and the common mistakes.(Note - This tutorial builds on Introduction to Java GenericsClick to read tutorial: Introduction to Java Generics: Why and How.) A generic class A generic class declares one or more type parameters in angle brackets after its name. Inside the class, the type parameter is used like any other type - for fields, parameters and return types. Here is a
Box which can hold one value of any type.
A generic class with one type parameter
//Box.java
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T get() {
return value;
}
public void set(T value) {
this.value = value;
}
public boolean isEmpty() {
return value == null;
}
@Override
public String toString() {
return "Box[" + value + "]";
}
public static void main(String[] args) {
Box<String> nameBox = new Box<>("Tom");
Box<Integer> numberBox = new Box<>(42);
String name = nameBox.get(); // no cast needed
int number = numberBox.get() + 8; // Integer unboxed to int
System.out.println(nameBox + " -> " + name.toUpperCase());
System.out.println(numberBox + " -> " + number);
nameBox.set("Harry");
System.out.println("After set: " + nameBox + ", empty? " + nameBox.isEmpty());
}
}
Box[Tom] -> TOM Box[42] -> 50 After set: Box[Harry], empty? false
class Box<T>declares the type parameterT. InBox<String>, everyTmeansString, soget()returns aStringandset()accepts only aString.- One class, written once, gave us a type-safe box for strings and another for integers.
nameBox.set(99)would not compile. - Inside the class, nothing is known about
Texcept that it is some object type - sovaluecan be compared withnulland passed to string joining, which work for any object.
Map<K, V> does. A Pair holding two values of possibly different types is a classic example.
A generic class with two type parameters
//Pair.java
public class Pair<A, B> {
private final A first;
private final B second;
public Pair(A first, B second) {
this.first = first;
this.second = second;
}
public A getFirst() { return first; }
public B getSecond() { return second; }
public Pair<B, A> swap() {
return new Pair<>(second, first);
}
@Override
public String toString() {
return "(" + first + ", " + second + ")";
}
public static void main(String[] args) {
Pair<String, Integer> nameAge = new Pair<>("Nancy", 29);
Pair<Integer, String> ageName = nameAge.swap();
System.out.println(nameAge + " swapped is " + ageName);
Pair<String, Pair<Double, Double>> city = new Pair<>("London", new Pair<>(51.5072, -0.1276));
System.out.println(city.getFirst() + " is at latitude " + city.getSecond().getFirst());
}
}
(Nancy, 29) swapped is (29, Nancy) London is at latitude 51.5072
Pair<String, Pair<Double, Double>>. (For simple data carriers like this, Java 16+ records can be generic too - record Pair<A, B>(A first, B second) {}.)
A generic interface
//Repository.java
import java.util.List;
import java.util.Optional;
public interface Repository<T, ID> {
void save(ID id, T item);
Optional<T> findById(ID id);
List<T> findAll();
}
Implementing the generic interface, staying generic
//InMemoryRepository.java
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class InMemoryRepository<T, ID> implements Repository<T, ID> {
private final Map<ID, T> items = new LinkedHashMap<>();
@Override public void save(ID id, T item) { items.put(id, item); }
@Override public Optional<T> findById(ID id) { return Optional.ofNullable(items.get(id)); }
@Override public List<T> findAll() { return new ArrayList<>(items.values()); }
}
Using the generic repository with two different types
//RepositoryDemo.java
public class RepositoryDemo {
// A class which fixes the type arguments of the interface
static class ProductNames extends InMemoryRepository<String, Integer> { }
public static void main(String[] args) {
Repository<String, String> users = new InMemoryRepository<>();
users.save("u1", "Tom");
users.save("u2", "Harry");
System.out.println("All users : " + users.findAll());
System.out.println("User u2 : " + users.findById("u2").orElse("not found"));
System.out.println("User u9 : " + users.findById("u9").orElse("not found"));
ProductNames products = new ProductNames();
products.save(101, "Java book");
products.save(102, "USB cable");
System.out.println("Products : " + products.findAll());
}
}
All users : [Tom, Harry] User u2 : Harry User u9 : not found Products : [Java book, USB cable]
InMemoryRepository<T, ID> stays generic, so it can store users by String ID and products by Integer ID. ProductNames fixes the types, so it works only with those. Library interfaces such as Comparable<T>, Comparator<T> and the functional interfacesClick to read tutorial on Functional Interfaces of Java 8 are generic in exactly this way.
public static <T> T firstOrDefault(List<T> list, T defaultValue)Utils.<String>firstOrDefault(...).
Static generic methods
//GenericMethods.java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class GenericMethods {
static <T> T firstOrDefault(List<T> list, T defaultValue) {
return list.isEmpty() ? defaultValue : list.get(0);
}
static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
static <T> List<T> repeat(T value, int times) {
List<T> result = new ArrayList<>();
for (int i = 0; i < times; i++) {
result.add(value);
}
return result;
}
static <K, V> String describe(K key, V value) {
return key.getClass().getSimpleName() + " " + key + " -> " + value.getClass().getSimpleName() + " " + value;
}
public static void main(String[] args) {
System.out.println(firstOrDefault(List.of("Tom", "Harry"), "nobody"));
System.out.println(firstOrDefault(Collections.<Integer>emptyList(), -1));
String[] names = {"Tom", "Harry", "Ethan"};
swap(names, 0, 2);
System.out.println(Arrays.toString(names));
List<String> stars = repeat("*", 5);
System.out.println(stars + " has " + stars.size() + " elements");
System.out.println(describe("id", 42));
System.out.println(GenericMethods.<Double>repeat(1.5, 2));
}
}
Tom -1 [Ethan, Harry, Tom] [*, *, *, *, *] has 5 elements String id -> Integer 42 [1.5, 1.5]
- For
firstOrDefault(List.of("Tom", "Harry"), "nobody"), the compiler inferredTasString; forrepeat("*", 5), it inferredStringfrom the argument, so the result is aList<String>. swap()works on an array of any object type - but not on anint[], becauseTcannot be a primitive.Collections.<Integer>emptyList()andGenericMethods.<Double>repeat(...)give the type argument explicitly, which is occasionally needed when there is nothing to infer it from.- The JDK is full of generic methods -
List.of(),Collections.sort(),Arrays.asList(),Optional.of()and many more.
map() method of a Mapper<T> class converts its content to another type R, using a function supplied by the caller (written here as a method reference, such as String::length).
An instance method with its own type parameter
//Mapper.java
import java.util.function.Function;
public class Mapper<T> {
private final T value;
Mapper(T value) { this.value = value; }
<R> Mapper<R> map(Function<T, R> converter) { // R is declared by this method
return new Mapper<>(converter.apply(value));
}
T get() { return value; }
public static void main(String[] args) {
Mapper<String> text = new Mapper<>(" java fundamentals ");
Mapper<Integer> length = text.map(String::length);
Mapper<String> cleaned = text.map(String::strip).map(String::toUpperCase);
System.out.println("length = " + length.get());
System.out.println("cleaned = " + cleaned.get());
}
}
length = 21 cleaned = JAVA FUNDAMENTALS
Optional.map() and Stream.map() in the Streams APIClick to read tutorial on the Streams API: the class has a type parameter T, and the method introduces a new type R for its result.
Common mistakes
- Using a class's type parameter in a static method: static members belong to the class, not to a
Box<String>or aBox<Integer>, sostatic T defaultValuedoes not compile. Make the static method generic with its own<T>instead. - Putting <T> in the wrong place: a method's type parameters go before the return type -
static <T> T first(...), notstatic T <T> first(...). - Calling methods T does not have: inside
Box<T>,value.length()does not compile, becauseTmight not be aString. To call specific methods, restrictTwith a bounded type parameterClick to read tutorial: Bounded Type Parameters in Java Generics. - Creating T objects or arrays:
new T()andnew T[10]are not allowed, because of type erasureClick to read tutorial: Type Erasure in Java Generics and Its Limitations.
Box<T>, and a class with two type parameters, Pair<A, B>. We then wrote a generic interface, Repository<T, ID>, and implemented it both by staying generic and by fixing the types. Next we wrote static generic methods, whose type parameters come before the return type and are usually inferred by the compiler. We then gave an instance method its own type parameter, as map() does. Lastly, we went through common mistakes. In the next tutorial, we will restrict type parameters with bounded type parametersClick to read tutorial: Bounded Type Parameters in Java Generics.Core Java Tutorials
Introduction to Java Generics: Why and HowClick to read tutorial: Introduction to Java Generics: Why and How Bounded Type Parameters in Java GenericsClick to read tutorial: Bounded Type Parameters in Java Generics Java Generics Wildcards: extends, super and PECSClick to read tutorial: Java Generics Wildcards: extends, super and PECS Type Erasure in Java Generics and Its LimitationsClick to read tutorial: Type Erasure in Java Generics and Its Limitations Java Math Class: Essential Methods with ExamplesClick to read tutorial: Java Math Class: Essential Methods with Examples
Introduction to Java Generics: Why and HowClick to read tutorial: Introduction to Java Generics: Why and How Bounded Type Parameters in Java GenericsClick to read tutorial: Bounded Type Parameters in Java Generics Java Generics Wildcards: extends, super and PECSClick to read tutorial: Java Generics Wildcards: extends, super and PECS Type Erasure in Java Generics and Its LimitationsClick to read tutorial: Type Erasure in Java Generics and Its Limitations Java Math Class: Essential Methods with ExamplesClick to read tutorial: Java Math Class: Essential Methods with Examples