loading...

Java generics and the problems it solved.

mrphayo profile image Chukwuebuka Anazodo ・5 min read

Prior to java 1.5, there was no way to specify types for objects when we need to use them in collections. Java is supposed to be a typed language and not being able to specify the types of its objects where necesary completely defeats the purpose. Developers find themselves working with a lot of collections and not guaranteeing the type objects contained in their collections exposed their code to potential bugs. To demonstrate this let us take a look at a very basic example.

package tutorial;

import java.util.ArrayList;
import java.util.List;

public class GenericsExample {
    public static void main(String[] args) {
        List sales = new ArrayList<>();
        sales.add(2.30);
        sales.add(3.30);
        sales.add(4.30);
        sales.add(5.30);
        sales.add(6.30);

        System.out.println("\nExpected usage average is " + findAverage(sales));

        sales = new ArrayList();
        sales.add(2.30);
        sales.add(3.30);
        sales.add(4.30);
        sales.add(5.30);
        sales.add(6.30);
        sales.add("7.30");

        System.out.println("Buggy use case is " + findAverage(sales));

    }

    private static double findAverage(List sales){
        int sum = 0;
        for(int i = 0; i < sales.size(); i++){
            sum += (double) sales.get(i);
        }

        return (double) sum / sales.size();
    }
}

The above code is will throw a ClassCastException at runtime. The exception is unchecked and therefore cannot be caught at compile time.
The developer of this program is clearly expecting a double type which makes sense given that s/he is dealing with money. But there is no way to actually guarantee that this value is what is actually supplied. To solve this problem we need to have a way of letting the compiler check for these potential errors and notify us of at compile time of the mistake.
Java 1.5 shipped with generics and with java 7 the diamond was introduced to specify the type of object we are expecting in a collection. You may have been using generics for too long without knowing what it is. Generics allow us to use typed parameters by specifying the type of Object (including autoboxed objects) we are expecting in a collection or even a class or method.
The above can be solved by including the Double autoboxed class in an angle bracket when declaring the list thus.

List<Double> sales = new ArrayList<>();

This will provide for compile-time checking of any object that is not of the type specified being added to our collection.

Generic Classes
To demonstrate how classes make use of generics let us create a player class that simulates actual sport players

abstract class Player{
    private String name;
    private int jerseyNumber;

    public Player(String name, int jerseyNumber) {
        this.name = name;
        this.jerseyNumber = jerseyNumber;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getJerseyNumber() {
        return jerseyNumber;
    }

    public void setJerseyNumber(int jerseyNumber) {
        this.jerseyNumber = jerseyNumber;
    }
}

lets us now create a basic basketball player class and a football player class that inherits from the player class.

class FootBallPlayer extends  Player{
    public FootBallPlayer(String name, int jerseyNumber) {
        super(name, jerseyNumber);
    }
}

class BasketBallPlayer extends Player{
    public BasketBallPlayer(String name, int jerseyNumber) {
        super(name, jerseyNumber);
    }
}

ALright lets say we have a teams class that can be composed of one or more players like so.

class Team{
    private String name;
    private int numberOfMatchesPlayed;
    private List<Player> players = new ArrayList<>();

    public Team(String name, int numberOfMatchesPlayed) {
        this.name = name;
        this.numberOfMatchesPlayed = numberOfMatchesPlayed;
        this.players = new ArrayList<>();
    }

    public String getName() {
        return name;
    }

    public int getNumberOfMatchesPlayed() {
        return numberOfMatchesPlayed;
    }

    public boolean addPlayer(Player player){
        if(!players.contains(player)){
            this.players.add(player);
            return true;
        }

        return false;
    }
}

To add players to a new football team Arsenal we could have a code like this

class GenericClassExample {
    public static void main(String[] args) {
        FootBallPlayer messi = new FootBallPlayer("Lionel Messi", 10);
        FootBallPlayer ronaldo = new FootBallPlayer("Cristiano Ronaldo", 7);

        BasketBallPlayer kawhi = new BasketBallPlayer("Kawhi Leonard", 2);
        BasketBallPlayer james = new BasketBallPlayer("King Lebron James", 23);

        Team arsenal = new Team("Arsenal", 0);
        arsenal.addPlayer(ronaldo);
        arsenal.addPlayer(messi);
        arsenal.addPlayer(kawhi);
    }

}

The code is valid but something is obviously wrong, Kawhi is a basketball player and should not be added to Arsenal. this could be an error in the programmers part and unfortunately, the code will compile fine. The caveat is that if this was a game. If one chooses arsenal he would see Kawhi playing with a basketball jersey much awkwardly.
Therefore if we could have a means that will ensure that our code does not compile if we try adding a player to a team he should not be on, we have fewer bugs to worry about.
One way of preventing the error above is to have football and basketball teams extend from the team class but if there was a better way of doing that, why duplicate code?
Introducing typed parameters T.
Using generics we would modify our Team class to this

class Team<T extends Player>{
    private String name;
    private int numberOfMatchesPlayed;
    private List<T> players;

    public Team(String name, int numberOfMatchesPlayed) {
        this.name = name;
        this.numberOfMatchesPlayed = numberOfMatchesPlayed;
        this.players = new ArrayList<>();
    }

    public String getName() {
        return name;
    }

    public int getNumberOfMatchesPlayed() {
        return numberOfMatchesPlayed;
    }

    public boolean addPlayer(T player){
        if(!players.contains(player)){
            this.players.add(player);
            System.out.println("Added " + player.getName() + " to Team " + this.getName());
            return true;
        }

        return false;
    }
}

And then we would change the initialization of Team in main to

Team<FootBallPlayer> arsenal = new Team<FootBallPlayer>("Arsenal", 0);

Things to note

  • The diamond operator after the class name.
  • The extends Player inside the diamond operator.
  • The T in both
private List<T> players;
public boolean addPlayer(T player)

What this generally implies is that

  • we are informing the class that we would be initializing it with a Type,
  • The type must have it's upper bound as a The Player class. ie must be a subclass of Player (This is called bounded parameters) and it cannot accept anything otherwise.
  • The type specified in the class during initialization should also be used where the Ts are. This would allow us to initialize the same Team class with more than once with more than one Type and all the compiler will be checking the types of an object we assign or use with these objects of Team type.

meaning that Kawhi and James cannot be added to Team Arsenal and if we initialize Team lakers like so

Team<BasketBallPlayer> arsenal = new Team<BasketBallPlayer>("LA Lakers", 0);

and be able to add James and Kawhi to the team.
This approach makes our code DRY and lets us avoid potential bugs.

Side Notes

  • You can also specify an interface as the type in java generics.
  • Generics can also be used on interfaces
Interface MyInterface<T extends FootBallPlayer>{
    //some code
}
  • Multiple bounds can be added to the typed parameter as long as it follows the inheritance rule ie. It can only extend one class and implement multiple interfaces. The syntax would look something like
class Team<T extends Player & Coach & Ranking>{
    // some code
}

where Player is a class and Coach and Ranking are interfaces.
(classes should be first in the order otherwise the code will fail to compile)

  • When using multiple types then you must write some code to check the actual type of the object (maybe using instanceof) before performing an operation on it otherwise there will be a runtime exception thrown when you perform an operation on the wrong type.

Alright, I know it has been quite a read, I do hope you enjoyed reading it and that you learnt a thing or more.

I look forward to your feedbacks.
Thank you

Posted on by:

mrphayo profile

Chukwuebuka Anazodo

@mrphayo

I'm passionate about Tech advancement, Java lover and enthusiast

Discussion

pic
Editor guide