Generics Types — A Refresher

Chandan Kumar
4 min readJun 2, 2024

Generics allow you to define classes, interfaces, and functions with placeholders for types that will be specified when they are instantiated or invoked. This means you can write code that works with any data type without sacrificing type safety.

Photo by AltumCode on Unsplash

Why Use Generics?

  1. Type Safety: Generics ensure that you only store and retrieve data of the expected type, catching type-related errors at compile time.
  2. Code Reusability: You can write a generic algorithm or data structure once and use it for any type.
  3. Flexibility: They allow for more flexible and readable code.

Lets begin by taking a non-generic Box class that operates on objects of any type, it needs only to provide two methods:

  1. set - which adds an object to the box,
  2. get - which retrieves it
public class Box {
private Object object;

public void set(Object object) { this.object = object; }
public Object get() { return object; }
}

As we see it accepts and returns an Object, consumer is allowed to pass any value that they want, provided that it is not a primitive type. In practical there’s no way to verify at compile time how the class is used.

At some places one might use Integers and at other places by mistake some other type which will result in runtime errors.

How does Generics solves this problem?

A generic class is defined with the following format:

class name<T1, T2, ..., Tn> { /* ... */ }

The type parameter section, delimited by angle brackets (<>), follows the class name. It specifies the type parameters (also called type variables) T1, T2, …, and Tn.

To update the Box class to use generics, you create a generic type declaration by changing the code "public class Box" to "public class Box<T>". This introduces the type variable, T, that can be used anywhere inside the class.

With this change, the Box class becomes:

/**
* Generic version of the Box class.
* @param <T> the type of the value being boxed
*/

public class Box<T> {
// T stands for "Type"
private T t;

public void set(T t) { this.t = t; }
public T get() { return t; }
}

As you can see, all occurrences of Object are replaced by T. A type variable can be any non-primitive type you specify: any class type, any interface type, any array type, or even another type variable.

There are so many example that we can find which uses generic classes for example

val numbers: List<Int> = listOf(1, 2, 3)
val names: List<String> = listOf("Alice", "Bob")

You may observe that List<Takes a type> which means it’s a generic type, consider the examples for Set, LinkedList, Queue etc,

Even a Pair or Triple class for example —

class Pair<K, V>(val first: K, val second: V)
class Triple<A, B, C>(val first: A, val second: B, val third: C)

Design Stack Data Structure using generics -

class Stack<T> {
private val elements = mutableListOf<T>()

fun push(item: T) {
elements.add(item)
}

fun pop(): T? {
if(elements.isNotEmpty()) {
return elements.removeAt(elements.size - 1)
}
return null
}

fun top(): T? {
if(elements.isNotEmpty()) {
return elements[elements.size-1]
}
return null
}

fun isEmpty(): Boolean {
return elements.isEmpty()
}
}

fun main() {
val intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
println(intStack.pop()) // Prints: 2

val stringStack = Stack<String>()
stringStack.push("Hello")
stringStack.push("World")
println(stringStack.pop()) // Prints: World
}

You see the implementation is pretty much same the only difference is instead of passing a strict type we’re now passing generic type.

Lets take another example of creating an in-memory cache using generics :

  1. Define cache interface
public interface Cache<K, V> {
void put(K key, V value);
V get(K key);
void remove(K key);
boolean containsKey(K key);
int size();
void clear();
}

2. Implement cache entry

A cache entry represents a single item in the cache, with the key and value pair. This can be a simple generic class.

public class CacheEntry<K, V> {
private K key;
private V value;
private long timestamp;

public CacheEntry(K key, V value) {
this.key = key;
this.value = value;
this.timestamp = System.currentTimeMillis();
}

public K getKey() {
return key;
}

public V getValue() {
return value;
}

public long getTimestamp() {
return timestamp;
}
}

3. Implementing in-memory cache class

import java.util.concurrent.ConcurrentHashMap;

public class InMemoryCache<K, V> implements Cache<K, V> {
private ConcurrentHashMap<K, CacheEntry<K, V>> cacheMap;

public InMemoryCache() {
cacheMap = new ConcurrentHashMap<>();
}

@Override
public void put(K key, V value) {
cacheMap.put(key, new CacheEntry<>(key, value));
}

@Override
public V get(K key) {
CacheEntry<K, V> entry = cacheMap.get(key);
return entry == null ? null : entry.getValue();
}

@Override
public void remove(K key) {
cacheMap.remove(key);
}

@Override
public boolean containsKey(K key) {
return cacheMap.containsKey(key);
}

@Override
public int size() {
return cacheMap.size();
}

@Override
public void clear() {
cacheMap.clear();
}
}

The cache itself does not need to differentiate between data types because Java’s type system will enforce type safety at compile time.

Uses —

public class Main {
public static void main(String[] args) {
Cache<String, String> stringCache = new InMemoryCache<>();
stringCache.put("key1", "value1");
System.out.println(stringCache.get("key1")); // Outputs: value1

Cache<String, Integer> integerCache = new InMemoryCache<>();
integerCache.put("key2", 100);
System.out.println(integerCache.get("key2")); // Outputs: 100

Cache<Integer, Double> doubleCache = new InMemoryCache<>();
doubleCache.put(1, 99.99);
System.out.println(doubleCache.get(1)); // Outputs: 99.99
}
}

Conclusion

Though we might not be using Genetics so frequently in development cycles, generics are an essential tool in a developer’s toolbox, providing the ability to write flexible, reusable, and type-safe code. Whether you’re working with collections, data structures, algorithms, or utility functions, understanding and leveraging generics can greatly enhance the quality and maintainability of your code.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Chandan Kumar
Chandan Kumar

Written by Chandan Kumar

A Devil’s Advocate and a Software Developer

No responses yet

Write a response