Beauty and strangeness of generics
Recently, I was preparing for my Oracle Certified Professional, Java SE 7 Programmer exam and I happened to encounter some rather strange-looking constructions in the realm of generics in Java. Nevertheless, I have also seen some clever and elegant pieces of code. I found these examples worth sharing not only because they can make your design choices easier and resulting code more robust and reusable, but also because some of them are quite tricky when you are not used to generics. I decided to break this post into four chapters that pretty much map my experience with generics during my studies and work experience.
Do you understand generics?
When we take a look around we can observe that generics are quite heavily used in many different framework around Java universe. They span from web application frameworks to collections in Java itself. Since this topic has been explained by many before me, I will just list resources that I found valuable and move on to stuff that sometimes does not get any mention at all or is not explained quite well (usually in the notes or articles posted online). So, if you lack the understanding of core generics concepts, you can check out some of the following materials:
- SCJP Sun Certified Programmer for Java 6 Exam by Katherine Sierra and Bert Bates
- For me, primary aim of this book was to prepare myself for OCP exams provided by Oracle. But I came to realize that notes in this book regarding generics can also be beneficial for anyone studying generics and how to use them. Definitely worth reading, however, the book was written for Java 6 so the explanation is not complete and you will have to look up missing stuff like diamond operator by yourself.
- Lesson: Generics (Updated) by Oracle
- Resource provided by Oracle itself. You can go through many simple examples in this Java tutorial. It will provide you with the general orientation in generics and sets the stage for more complex topics such as those in following book.
- Java Generics and Collections by Maurice Naftalin and Philip Wadler
- Another great Java book from O’Reilly Media’s production. This book is well-organized and the material is well-presented with all details included. This book is unfortunately also rather dated, so same restrictions as with first resource apply.
What is not allowed to do with generics?
Assuming you are aware of generics and want to find out more, lets move to what can not be done. Surprisingly, there is quite a lot of stuff that cannot be used with generics. I selected following six examples of pitfalls to avoid, when working with generics.
Static field of type <T>
One common mistake many inexperienced programmers do is to try to declare static members. As you can see in following example, any attempt to do so ends up with compiler error like this one: Cannot make a static reference to the non-static type T
.
public class StaticMember<T> { // causes compiler error static T member; }
Instance of type <T>
Another mistake is to try instantiate any type by calling new on generic type. By doing so, compiler causes error saying: Cannot instantiate the type T
.
public class GenericInstance<T> { public GenericInstance() { // causes compiler error new T(); } }
Incompatibility with primitive types
One of the biggest limitation while working with generics is seemingly their incompatibility with primitive types. It is true that you can’t use primitives directly in your declarations, however, you can substitute them with appropriate wrapper types and you are fine to go. Whole situation is presented in the example below:
public class Primitives<T> { public final List<T> list = new ArrayList<>(); public static void main(String[] args) { final int i = 1; // causes compiler error // final Primitives<int> prim = new Primitives<>(); final Primitives<Integer> prim = new Primitives<>(); prim.list.add(i); } }
First instantiation of Primitives
class would fail during compilation with an error similar to this one: Syntax error on token "int", Dimensions expected after this token
. This limitation is bypassed using wrapper type and little bit of auto-boxing magic.
Array of type <T>
Another obvious limitation of using generics is the inability to instantiate generically typed arrays. The reason is pretty obvious given the basic characteristics of an array objects – they preserve their type information during runtime. Should their runtime type integrity be violated, the runtime exception ArrayStoreException comes to rescue the day.
public class GenericArray<T> { // this one is fine public T[] notYetInstantiatedArray; // causes compiler error public T[] array = new T[5]; }
However, if you try to directly instantiate a generic array, you will end up with compiler error like this one: Cannot create a generic array of T
.
Generic exception class
Sometimes, programmer might be in need of passing an instance of generic type along with exception being thrown. This is not possible to do in Java. Following example depicts such an effort.
// causes compiler error public class GenericException<T> extends Exception {}
When you try to create such an exception, you will end up with message like this: The generic class GenericException<T> may not subclass java.lang.Throwable
.
Alternate meaning of keywords super
and extends
Last limitation worth mentioning, especially for the newcomers, is the alternate meaning of keywords super
and extends
, when it comes to generics. This is really useful to know in order to produce well-designed code that makes use of generics.
<? extends T>
- Meaning: Wildcard refers to any type extending type T and the type T itself.
<? super T>
- Meaning: Wildcard refers to any super type of T and the type T itself.
Bits of beauty
One of my favorite things about Java is its strong typing. As we all know, generics were introduced in Java 5 and they were used to make it easier for us to work with collections (they were used in more areas than just collections, but this was one of the core arguments for generics in design phase). Even though generics provide only compile time protection and do not enter the bytecode, they provide rather efficient way to ensure type safety. Following examples show some of the nice features or use cases for generics.
Generics work with classes as well as interfaces
This might not come as a surprise at all, but yes – interfaces and generics are compatible constructs. Even though the use of generics in conjunction with interfaces is quite common occurrence, I find this fact to be actually pretty cool feature. This allows programmers to create even more efficient code with type safety and code reuse in mind. For example, consider following example from interface Comparable
from package java.lang
:
public interface Comparable<T> { public int compareTo(T o); }
Simple introduction of generics made it possible to omit instance of check from compareTo
method making the code more cohesive and increased its readability. In general, generics helped make the code easier to read and understand as well as they helped with introduction of type order.
Generics allow for elegant use of bounds
When it comes to bounding the wildcard, there is a pretty good example of what can be achieved in the library class Collections
. This class declares method copy
, which is defined in the following example and uses bounded wildcards to ensure type safety for copy operations of lists.
public static <T> void copy(List<? super T> dest, List<? extends T> src) { ... }
Lets take a closer look. Method copy
is declared as a static generic method returning void. It accepts two arguments – destination and source (and both are bounded). Destination is bounded to store only types that are super types of T
or T
type itself. Source, on the other hand, is bounded to be made of only extending types of T
type or T
type itself. These two constraints guarantee that both collections as well as the operation of copying stay type safe. Which we don’t have to care for with arrays since they prevent any type safety violations by throwing aforementioned ArrayStoreException
exception.
Generics support multibounds
It is not hard to imagine why would one want to use more that just one simple bounding condition. Actually, it is pretty easy to do so. Consider following example: I need to create a method that accepts argument that is both Comparable
and List
of numbers. Developer would be forced to create unnecessary interface ComparableList in order to fulfill described contract in pre-generic times.
public class BoundsTest { interface ComparableList extends List, Comparable {} class MyList implements ComparableList { ... } public static void doStuff(final ComparableList comparableList) {} public static void main(final String[] args) { BoundsTest.doStuff(new BoundsTest().new MyList()); } }
With following take on this task we get to disregard the limitations. Using generics allows us to create concrete class that fulfills required contract, yet leaves doStuff
method to be as open as possible. The only downside I found was this rather verbose syntax. But since it still remains nicely readable and easily understandable, I can overlook this flaw.
public class BoundsTest { class MyList<T> implements List<T>, Comparable<T> { ... } public static <T, U extends List<T> & Comparable<T>> void doStuff(final U comparableList) {} public static void main(final String[] args) { BoundsTest.doStuff(new BoundsTest().new MyList<String>()); } }
Bits of strangeness
I decided to dedicate the last chapter of this post two the strangest constructs or behaviors I have encountered so far. It is highly possible that you will never encounter code like this, but I find it interesting enough to mention it. So without any further ado, lets meet the weird stuff.
Awkward code
As with any other language construct, you might end up facing some really weird looking code. I was wondering what would the most bizarre code look like and whether it would even pass the compilation. Best I could come up with is following piece of code. Can you guess whether this code compiles or not?
public class AwkwardCode<T> { public static <T> T T(T T) { return T; } }
Even though this is an example of really bad coding, it will compile successfully and the application will run without any problems. First line declares generic class AwkwardCode
and second line declares generic method T
. Method T
is generic method returning instances of T
. It takes parameter of type T
unfortunately called T
. This parameter is also returned in method body.
Generic method invocation
This last example shows how type inference works when combined with generics. I stumbled upon this problem when I saw a piece of code that did not contain generic signature for a method call yet claimed to pass the compilation. When someone has only a little experience with generics, code like this might startle them at first sight. Can you explain the behavior of following code?
public class GenericMethodInvocation { public static void main(final String[] args) { // 1. returns true System.out.println(Compare.<String> genericCompare("1", "1")); // 2. compilation error System.out.println(Compare.<String> genericCompare("1", new Long(1))); // 3. returns false System.out.println(Compare.genericCompare("1", new Long(1))); } } class Compare { public static <T> boolean genericCompare(final T object1, final T object2) { System.out.println("Inside generic"); return object1.equals(object2); } }
Ok, let’s break this down. First call to genericCompare
is pretty straight forward. I denote what type methods arguments will be of and supply two objects of that type – no mysteries here. Second call to genericCompare
fails to compile since Long
is not String
. And finally, third call to genericCompare
returns false
. This is rather strange since this method is declared to accept two parameters of the same type, yet it is all good to pass it String
literal and a Long
object. This is caused by type erasure process executed during compilation. Since the method call is not using <String>
syntax of generics, compiler has no way to tell you, that you are passing two different types. Always remember that the closest shared inherited type is used to find matching method declaration. Meaning, when genericCompare
accepts object1
and object2
, they are casted to Object
, yet compared as String
and Long
instances due to runtime polymorphism – hence the method returns false
. Now let’s modify this code a little bit.
public class GenericMethodInvocation { public static void main(final String[] args) { // 1. returns true System.out.println(Compare.<String> genericCompare("1", "1")); // 2. compilation error System.out.println(Compare.<String> genericCompare("1", new Long(1))); // 3. returns false System.out.println(Compare.genericCompare("1", new Long(1))); // compilation error Compare.<? extends Number> randomMethod(); // runs fine Compare.<Number> randomMethod(); } } class Compare { public static <T> boolean genericCompare(final T object1, final T object2) { System.out.println("Inside generic"); return object1.equals(object2); } public static boolean genericCompare(final String object1, final Long object2) { System.out.println("Inside non-generic"); return object1.equals(object2); } public static void randomMethod() {} }
This new code sample modifies Compare
class by adding a non-generic version of genericCompare
method and defining a new randomMethod
that does nothing and gets called twice from main
method in GenericMethodInvocation
class. This code makes the second call to genericCompare
possible since I provided new method that matches given call. But this raises a question about yet another strange behavior – Is the second call generic or not? As it turns out – no, it is not. Yet, it is still possible to use <String>
syntax of generics. To demonstrate this ability more clearly I created new call to randomMethod
with this generic syntax. This is possible thanks to the type erasure process again – erasing this generic syntax.
However, this changes when a bounded wildcard comes on the stage. Compiler sends us clear message in form of compiler error saying: Wildcard is not allowed at this location
, which makes it impossible to compile the code. To make the code compile and run you have to comment out line number 12. When the code is modified this way it produces following output:
Inside generic true Inside non-generic false Inside non-generic false
Reference: | Beauty and strangeness of generics from our JCG partner Jakub Stas at the Jakub Stas blog. |