Java helper. Part 3. Generics.

Ilia
4 min readMay 6, 2023

--

Generic types or methods differ from regular ones in that they have typed parameters. It allows us to get away from the rigid definition of the types used. One restriction of generics in Java is that the type parameter cannot be a primitive type. In this article, we will look at their use with classes and methods.

Java Collection Framework is a good example to start our journey. LinkedList<E>– is a typical generic type. It contains the E parameter that represents the type of elements that will be stored in the collection:

List<Integer> listOfIntegers = new LinkedList<>();

List<String> listOfStrings = new LinkedList<>();

Analyse one more example, let’s write our own method for working with collections:

public <T> List<T> collect(T[] array) {
return Arrays.stream(array).collect(Collectors.toList());
}

Also, we can extend our T type with extends keyword like with a typical class:

public <T extends Number> List<T> collectNumbers(T[] array) {
return Arrays.stream(array).collect(Collectors.toList());
}

Or using own class:

public void findAllHosts(List<? extends Connector> connectors) {
connectors.stream().map(Connector::host).forEach(System.out::println);
}

And with multiple classes:

public <T extends Number & Comparable> List<T> collectNumbersAndComparable(T[] array) {
return Arrays.stream(array).collect(Collectors.toList());
}

The Collection Framework is a great example where we can see the benefits of using generics.

Let’s try to implement this using classes. We should use <T> after a class name and set a data type. For example:

public class GenericWithClasses<T> {

// variable of T type
private T data;

public GenericWithClasses(T data) {
this.data = data;
}

// method that return T type variable
public T getData() {
return this.data;
}

}

“T data” might be any class here (String, Integer, OwnClass, etc). Let’s write a couple lines of code:

// genericWithMethods
var genericWithStringClasses = new GenericWithClasses<>("test");
System.out.println(genericWithStringClasses.getData());

var genericWithIntegerClasses = new GenericWithClasses<>(123);
System.out.println(genericWithIntegerClasses.getData());

Also, we have an opportunity to do the same with bounded classes:

public class GenericWithNumberClasses<T extends Number> {

// variable of T type
private T data;

public GenericWithNumberClasses(T data) {
this.data = data;
}

// method that return T type variable
public T getData() {
return this.data;
}

public void display() {
System.out.println("This is a bounded type generics class.");
}
}

And create class again:

var numbers = new GenericWithNumberClasses(123);
numbers.display();

But if we try to create new one, but with classes which don’t extend Number, we will have an exception:

// but with String we will have exceptions
var strings = new GenericWithNumberClasses("123"); // cast exception

But if we need to create a mapper class that receives T-class and returns V-class as result, what should we do?

Firstly, create the interface Mapper<T, V>:


public interface Mapper<T, V> {

V map(T data);

List<V> map(List<T> listOfData);

}

And now create a new class that implements the Mapper<T, V> interface:

public class ConnectorMapper implements Mapper<Connector, String> {

@Override
public String map(Connector data) {
return data.toString();
}

@Override
public List<String> map(List<Connector> listOfData) {
return listOfData
.stream()
.map(Connector::toString)
.collect(Collectors.toList());
}
}

Here’s a dummy implementation, but it’s a good example of how we can use the Connector-class as T (input class) and String-class as V (output class).

Type Erasure

Type erasure is the process of applying type constraints only at compile time and discarding element type information at run time. For example:

public <T> List<T> collector(List<T> list) {
return list.stream().collect(Collectors.toList());
}

And after compiltaion:

public List collector(List list) {
return list.stream().collect(Collectors.toList());
}

One more example with the Connector class:

public <T extends Connector> void genericMethod(T t) {
...
}

After:

public void genericMethod(Connector t) {
...
}

But why? The reason is that the compiler ensures type safety of our code.

Link with answers

One more an example with the Connector class:

public class Connector<E> {

private E[] content;

public Connector(int capacity) {
this.content = (E[]) new Object[capacity];
}

public void send(E data) {
// ..
}

public E read() {
// ..
}
}

At compilation, the compiler replaces the unbound type parameter E with the class Object:

public class Connector {

private Object[] content;

public Connector(int capacity) {
this.content = (Object[]) new Object[capacity];
}

public void send(Object data) {
// ..
}

public Object read() {
// ..
}
}

That’s all for today! Good luck 🍀 and thank you!

--

--

Ilia
Ilia

Written by Ilia

Lead software engineer | Kotlin/Java as main language and learning Golang and Rust | Try to become a rockstar

No responses yet