CodeGym /Java Course /Module 1. Java Syntax /Parameterized types in Java: Generics

Parameterized types in Java: Generics

Module 1. Java Syntax
Level 16 , Lesson 6
Available

1. All classes inherit Object

All classes in Java implicitly inherit the Object class.

We'll analyze what inheritance is and how it works in Java in the Java Core quest. For now, we'll consider one simple fact that follows from this:

An object of any class can be assigned to an Object variable. Example:

Code Note
Object o = new Scanner(System.in);
The o variable stores a reference to a Scanner object
Object o = new String();
The o variable stores a reference to a String object
Object o = new Integer(15);
The o variable stores a reference to an Integer object
Object o = "Hello";
The o variable stores a reference to a String object

This is where the good news ends. The compiler doesn't keep track of the original type of object saved in an Object variable, so you won't be able to call methods on the saved object other than the methods of the Object class.

If you need to call the methods associated with the object's original type, then you need to first save a reference to it in a variable of the correct type, and then call the methods on that variable:

Code Note
Object o = new Scanner(System.in);
int x = o.nextInt();
The program will not compile. The Object class has no nextInt() method.
Object o = new Scanner(System.in);

Scanner console = (Scanner) o;

int x = console.nextInt();
This will work.

Here we save a reference to a Scanner object in a Scanner variable using a typecast operator.

You can't just go and assign an Object variable to a Scanner variable, even if the Object variable stores a reference a Scanner object. But you can do this if you use the typecast operator, which you already know about. This is its general appearance:

Type name1 = (Type) name2;

Where name1 is the name of a Type variable, and name2 is the name of an Object variable that stores a reference to a Type object.

Typecasting

If variable's type and the object's type do not match, then a ClassCastException will be thrown. Example:

Code Note
Object o = new Integer(5);
String s = (String) o;
An error will occur at runtime:
a ClassCastException will be thrown here

There's a way to avoid this error in Java: we do this by checking the type of the object stored in a variable:

name instanceof Type

The instanceof operator checks whether the name variable is a Type object.

As an example, let's find a string in an array of diverse objects:

Code Note
Object[] objects = {10, "Hello", 3.14};

for (int i = 0; i < objects.length; i++)
{
   if (objects[i] instanceof String)
   {
      String s = (String) objects[i];
      System.out.println(s);
   }
}
Autoboxing will convert these values into an Integer, String, and Double, respectively.

Loop over the array of objects

If the object is a String

Save it to a String variable
Display the variable on the screen.


2. Why generics appeared — collections

Let's return to collections.

As soon as Java developers created the ArrayList class, they wanted to make it universal, so it could store any type of object. So they used an array of Objects to store the elements.

The strength of this approach is that you can add an object of any type to the collection.

Of course, there are several weaknesses.

Disadvantage 1.

It was always necessary to write a type conversion operator when retrieving elements from a collection:

Code Note
ArrayList numbers = new ArrayList();


for (int i = 0; i < 10; i++)
   numbers.add(i * 10);


int sum = 0;
for (int i = 0; i < 10; i++)
{
   sum = sum + (Integer) numbers.get(i);
}
Create a collection to store references to Object objects

Fill the collection with numbers 10, 20, ... 100;



Sum the elements of the collection


Typecasting is necessary

Disadvantage 2.

There was no guarantee that a collection contains a specific type of element

Code Note
ArrayList numbers = new ArrayList();


for (int i = 0; i < 10; i++)
   numbers.add(i * 2.5);


int sum = 0;
for (int i = 0; i < 10; i++)
{
   sum = sum + (Integer) numbers.get(i);
}
Create a collection to store references to Object objects

We fill the collection with numbers represented as Double objects:
0.0, 2.5, 5.0, ...


Sum the elements of the collection


There will be an error: a Double cannot be cast to an Integer

Data can be put into the collection anywhere:

  • in another method
  • in another program
  • from a file
  • over the network

Disadvantage 3.

The data in the collection can be changed accidentally.

You can pass a collection filled with your data to some method. That method, written by a different programmer, adds its data to your collection.

The name of the collection doesn't clearly indicate which types of data can be stored in it. And even if you give your variable a clear name, a reference to it can be passed to a dozen methods, and those methods definitely won't know anything about the original name of the variable.


3. Generics

Generics in Java

In Java, all these problems are eliminated by this cool thing called generics.

In Java, generics means the ability to add type parameters to types. The result is a complex composite type. The general view of such a composite type is this:

ClassName<TypeParameter>

This is a generic class. And it can be used wherever you normally use classes.

Code Description
ArrayList<Integer> list;
Creating variables
list = new ArrayList<Integer> ();
Creating objects
ArrayList<Integer>[] array;
Creating arrays

Only Integer variables can be stored in such a collection:

Code Description
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(new Integer(1));
list.add(2);
list.add("Hello");
ArrayList collection with Integer elements
This is allowed
And this will also work
Autoboxing

But this is not allowed: compilation error

You will learn how to create your own classes with type parameters in the Java Collections quest. For now, we'll look at how to use them and how they work.


4. How generics work

Actually, generics are terribly primitive.

The compiler simply replaces generic types with ordinary types. But when methods of a generic type are used, the compiler adds a typecast operator to cast parameters to the type parameters:

Code What the compiler does
ArrayList<Integer> list = new ArrayList<Integer>();
ArrayList list = new ArrayList();
list.add(1);
list.add( (Integer) 1 );
int x = list.get(0);
int x = (Integer) list.get(0);
list.set(0, 10);
list.set(0, (Integer) 10);

Suppose we have a method that sums the numbers in a collection of integers:

Code What the compiler does
public int sum(ArrayList<Integer> numbers)
{
   int result = 0;

   for (int i = 0; i < numbers.size(); i++)
      result = result + numbers.get(i);

   return result;
}
public int sum(ArrayList numbers)
{
   int result = 0;

   for (int i = 0; i < numbers.size(); i++)
      result = result + (Integer) numbers.get(i);

   return result;
}

In other words, generics are a kind of syntactic sugar, just like autoboxing, but a little more. With autoboxing, the compiler adds methods for converting an int to an Integer and vice versa, and for generics it adds typecast operators.

After the compiler compiles your generic classes with type parameters, they are simply converted to ordinary classes and typecast operators. Information about the type arguments passed to variables of generic types is lost. This effect is also called type erasure.

Sometimes programmers writing generic classes (classes with type parameters) really need the information about the types passed as arguments. In the Java Collections quest, you'll learn how to deal with this and what it entails.



5. A few facts about generics

Here are some more interesting facts about generics.

Classes can have several type parameters. It looks something like this:

ClassName<TypeParameter1, TypeParameter2, TypeParameter3>

Actually, this isn't really surprising. Anywhere the compiler can add an operator to cast to one type, it can add multiple typecast operators.

Examples:

Code Note
HashMap<Integer, String> map = new HashMap<Integer, String>();
map.put(7, "Hello");
map.put(-15, "Hello");
The put method's first parameter is an Integer, and the second is a String

Generic types can also be used as parameters. It looks something like this:

ClassName<TypeParameter<TypeParameterParameter>>

Suppose we want to create a list that will store lists of strings. In this case, we will get something like this:

// List of greetings
ArrayList<String> listHello = new ArrayList<String>();
listHello.add ("Hello");
listHello.add ("Hi");

// List of goodbyes
ArrayList<String> listBye = new ArrayList<String>();
listBye.add("Bye");
listBye.add ("Goodbye");

// List of lists
ArrayList<ArrayList<String>> lists = new ArrayList<ArrayList<String>>();
lists.add(listHello);
lists.add(listBye);

Generic types (types with type parameters) can also be used as array types. It looks something like this:

ClassName<TypeParameter>[] array = new ClassName<TypeParameter>[size];

There's nothing magical happening here: the angle brackets just indicate the type name:

Code Non-generic counterpart
ArrayList<String>[] list = new ArrayList<String>[10];
StringArrayList[] list = new StringArrayList[10];
ArrayList<Integer>[] list = new ArrayList<Integer>[10];
IntegerArrayList[] list = new IntegerArrayList[10];
ArrayList<Scanner>[] list = new ArrayList<Scanner>[10];
ScannerArrayList[] list = new ScannerArrayList[10];
Comments (1)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
curi0usmind Level 9, Switzerland Expert
15 August 2023
I think one should try using enhanced for loops when looping through a collection it makes the code look more concise