Pages

Generics in Java: how to use angled brackets in practice

Introduction

Starting with JSE 5.0, generics were added to the Java language's arsenal.

What are generics in java?

Generics are Java's special mechanism for implementing generic programming — a way to describe data and algorithms that lets you work with different datatypes without changing the description of the algorithms. The Oracle website has a separate tutorial dedicated to generics: "Lesson: Generics". Generics in Java: how to use angled brackets in practice - 1To understand generics, you first need to figure out why they're needed and what they give. The "Why Use Generics?" section of the tutorial says that a couple purposes are stronger type checking at compile time and elimination of the need for explicit casts. Let's prepare for some tests in our beloved Tutorialspoint online java compiler. Suppose you have the following code:
import java.util.*;
public class HelloWorld {
 public static void main(String []args) {
  List list = new ArrayList();
  list.add("Hello");
  String text = list.get(0) + ", world!";
  System.out.print(text);
 }
}
This code will run perfectly well. But what if the boss comes to us and says that "Hello, world!" is an overused phrase and that you must return only "Hello"? We'll remove the code that concatenates ", world!" This seems harmless enough, right? But we actually get an error AT COMPILE TIME:
error: incompatible types: Object cannot be converted to String
The problem is that in our List stores Objects. String is a descendant of Object (since all Java classes implicitly inherit Object), which means we need an explicit cast, but we didn't add one. During the concatenation operation, the static String.valueOf(obj) method will be called using the object. Eventually, it will call the Object class's toString method. In other words, our List contains an Object. This means that wherever we need a specific type (not Object), we will have to do the type conversion ourselves:
import java.util.*;
public class HelloWorld {
 public static void main(String []args) {
  List list = new ArrayList();
  list.add("Hello!");
  list.add(123);
  for (Object str : list) {
      System.out.println("-" + (String)str);
  }
 }
}
However, in this case, because List takes objects, it can store not only Strings, but also Integers. But the worst thing is that the compiler doesn't see anything wrong here. And now we'll get an error AT RUN TIME (known as a "runtime error"). The error will be:
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
You must agree that this isn't very good. And all this because the compiler isn't an artificial intelligence capable of always correctly guessing the programmer's intent. Java SE 5 introduced generics to let us tell the compiler about our intentions — about which types we are going to use. We fix our code by telling the compiler what we want:
import java.util.*;
public class HelloWorld {
 public static void main(String []args) {
  List<String> list = new ArrayList<>();
  list.add("Hello!");
  list.add(123);
  for (Object str : list) {
      System.out.println("-" + str);
  }
 }
}
As you can see, we no longer need a cast to a String. In addition, we have angle brackets surrounding the type argument. Now the compiler won't let us to compile the class until we remove the line that adds 123 to the list, since this is an Integer. And it will tell us so. Many people call generics "syntactic sugar". And they're right, since after generics are compiled, they really do become the same type conversions. Let's look at the bytecode of the compiled classes: one that uses an explicit cast and one that uses generics: Generics in Java: how to use angled brackets in practice - 2After compilation, all generics are erased. This is called "type erasure". Type erasure and generics are designed to be backward compatible with older versions of the JDK while simultaeously allowing the compiler to help with type definitions in new versions of Java.

Raw types

Speaking of generics, we always have two categories: parameterized types and raw types. Raw types are types that omit the "type clarification" in angle brackets: Generics in Java: how to use angled brackets in practice - 3Parameterized types, on the hand, include a "clarification": Generics in Java: how to use angled brackets in practice - 4As you can see, we used an unusual construct, marked by an arrow in the screenshot. This is special syntax that was added to Java SE 7. It is called the "diamond". Why? The angle brackets form a diamond: <>. You should also know that the diamond syntax is associated with the concept of "type inference". After all, the compiler, seeing <> on the right, looks at the left side of the assignment operator, where it finds the type of the variable whose value is being assigned. Based on what it finds in this part, it understands the type of the value on the right. In fact, if a generic type is given on the left, but not on the right, the compiler can infer the type:
import java.util.*;
public class HelloWorld {
 public static void main(String []args) {
  List<String> list = new ArrayList();
  list.add("Hello, World");
  String data = list.get(0);
  System.out.println(data);
 }
}
But this mixes the new style with generics and the old style without them. And this is highly undesirable. When compiling the code above, we get the following message:
Note: HelloWorld.java uses unchecked or unsafe operations
In fact, the reason why you even need to add a diamond here seems incomprehensible. But here's an example:
import java.util.*;
public class HelloWorld {
 public static void main(String []args) {
  List<String> list = Arrays.asList("Hello", "World");
  List<Integer> data = new ArrayList(list);
  Integer intNumber = data.get(0);
  System.out.println(data);
 }
}
You will recall that ArrayList has a second constructor that takes a collection as an argument. And this is where something sinister lies hidden. Without the diamond syntax, the compiler doesn't understand that it is being deceived. With the diamond syntax, it does. So, Rule #1 is: always use the diamond syntax with parameterized types. Otherwise, we risk missing where we're using raw types. To eliminate "uses unchecked or unsafe operations" warnings, we can use the @SuppressWarnings("unchecked") annotation on a method or class. But think about why you've decided to use it. Remember rule number one. Maybe you need to add a type argument.

Generic methods

Generics let you create methods whose parameter types and return type are parameterized. A separate section is devoted to this capability in the Oracle tutorial: "Generic Methods". It's important to remember the syntax taught in this tutorial:
  • it includes a list of type parameters inside angle brackets;
  • the list of type parameters goes before the method's return type.
Let's look at an example:
import java.util.*;
public class HelloWorld {

    public static class Util {
        public static <T> T getValue(Object obj, Class<T> clazz) {
            return (T) obj;
        }
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
  List list = Arrays.asList("Author", "Book");
  for (Object element : list) {
      String data = Util.getValue(element, String.class);
      System.out.println(data);
      System.out.println(Util.<String>getValue(element));
  }
    }
}
If you look at the Util class, you'll see that it has two generic methods. Thanks to the possibility of type inference, we can either indicate the type directly to the compiler, or we can specify it ourselves. Both options are presented in the example. By the way, the syntax makes a lot of sense if you think about it. When declaring a generic method, we specify the type parameter BEFORE the method, because if we declare the type parameter after the method, the JVM wouldn't be able to figure out which type to use. Accordingly, we first declare that we will use the T type parameter, and then we say that we are going to return this type. Naturally, Util.<Integer>getValue(element, String.class) will fail with an error: incompatible types: Class<String> cannot be converted to Class<Integer>. When using generic methods, you should always remember type erasure. Let's look at an example:
import java.util.*;
public class HelloWorld {

    public static class Util {
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
  List list = Arrays.asList(2, 3);
  for (Object element : list) {
      System.out.println(Util.<Integer>getValue(element) + 1);
  }
    }
}
This will run just fine. But only as long as the compiler understands that the return type of the method being called is Integer. Replace the console output statement with the following line:
System.out.println(Util.getValue(element) + 1);
We get an error:
bad operand types for binary operator '+', first type: Object, second type: int.
In other words, type erasure has occurred. The compiler sees that no one has specified the type, so the type is indicated as Object and the method fails with an error.

Generic classes

Not only methods can be parameterized. Classes can as well. The "Generic Types" section of Oracle's tutorial is devoted to this. Let's consider an example:
public static class SomeType<T> {
 public <E> void test(Collection<E> collection) {
  for (E element : collection) {
   System.out.println(element);
  }
 }
 public void test(List<Integer> collection) {
  for (Integer element : collection) {
   System.out.println(element);
  }
 }
}
Everything is simple here. If we use the generic class, the type parameter is indicated after the class name. Now let's create an instance of this class in the main method:
public static void main(String []args) {
 SomeType<String> st = new SomeType<>();
 List<String> list = Arrays.asList("test");
 st.test(list);
}
This code will run well. The compiler sees that there is a List of numbers and a Collection of Strings. But what if we eliminate the type parameter and do this:
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
We get an error:
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Again, this is type erasure. Since the class no longer uses a type parameter, the compiler decides that, since we passed a List, the method with List<Integer> is most appropriate. And we fail with an error. Therefore, we have Rule #2: If you have a generic class, always specify the type parameters.

Restrictions

We can restrict the types specified in generic methods andn classes. For example, suppose we want a container to accept only a Number as the type argument. This feature is described in the Bounded Type Parameters section of Oracle's tutorial. Let's look at an example:
import java.util.*;
public class HelloWorld {

    public static class NumberContainer<T extends Number> {
        private T number;

        public NumberContainer(T number) { this.number = number; }

        public void print() {
            System.out.println(number);
        }
    }

    public static void main(String []args) {
  NumberContainer number1 = new NumberContainer(2L);
  NumberContainer number2 = new NumberContainer(1);
  NumberContainer number3 = new NumberContainer("f");
    }
}
As you can see, we've restricted the type parameter to the Number class/interface or its descendants. Note that you can specify not only a class, but also interfaces. For example:
public static class NumberContainer<T extends Number & Comparable> {
Generics also support wildcards They are divided into three types:
Your use of wildcards should adhere to the Get-Put principle. It can be expressed as follows:
  • Use an extend wildcard when you only get values out of a structure.
  • Use a super wildcard when you only put values into a structure.
  • And don't use a wildcard when you both want to get and put from/to a structure.
This principle is also called the Producer Extends Consumer Super (PECS) principle. Here's a small example from the source code for Java's Collections.copy method: Generics in Java: how to use angled brackets in practice - 5And here's a little example of what WON'T work:
public static class TestClass {
 public static void print(List<? extends String> list) {
  list.add("Hello, World!");
  System.out.println(list.get(0));
 }
}

public static void main(String []args) {
 List<String> list = new ArrayList<>();
 TestClass.print(list);
}
But if you replace extends with super, then everything is fine. Because we populate the list with a value before displaying its contents, it is a consumer. Accordingly, we use super.

Inheritance

Generics have another interesting feature: inheritance. The way inheritance works for generics is described under "Generics, Inheritance, and Subtypes" in Oracle's tutorial. The important thing is to remember and recognize the following. We cannot do this:
List<CharSequence> list1 = new ArrayList<String>();
Because inheritance works differently with generics: Generics in Java: how to use angled brackets in practice - 6
https://docs.oracle.com/javase/tutorial/java/generics/inheritance.html
And here's another good example that will fail with an error:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Again, everything is simple here. List<String> is not a descendant of List<Object>, even though String is a descendant of Object.

Conclusion

So we've refreshed our memory concerning generics. If you rarely take full advantage of their capabilities, some of the details grow fuzzy. I hope this short review has helped jog your memory.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.