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". To 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: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:
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:
However, in this case, because
List
takes objects, it can store not only String
s, but also Integer
s. 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:
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:
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:
After 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: Parameterized types, on the hand, include a "clarification": As 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:
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:
In fact, the reason why you even need to add a diamond here seems incomprehensible. But here's an example:
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.
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:
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:
We get an error:
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: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:
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:
We get an error:
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 aNumber
as the type argument.
This feature is described in the Bounded Type Parameters section of Oracle's tutorial.
Let's look at an example:
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:
Generics also support wildcards They are divided into three types:
- Upper bounded wildcards — <? extends Number>
- Unbounded wildcards — <?>
- Lower bounded wildcards — <? super Integer>
- 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.
Collections.copy
method:
And here's a little example of what WON'T work:
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:Because inheritance works differently with generics:
Again, everything is simple here.
List<String>
is not a descendant of List<Object>
, even though String
is a descendant of Object
.
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.