Java Generics Suck

The implementation of generics in Java is pathetic! C# was the first of the two languages (let's face it, C# is so similar to Java that one might make the mistake of thinking that C# was merely Microsoft's embraced and extended version) to release a compiler supporting generics - a feature that allows classes and functions to take type parameters, somewhat akin to the templates feature of C++. As a huge fan of the latter, I found the C# implementation intelligent and well thought out, even if it was a little restricted by comparison. Java has been around a lot longer than C#, and has had far more time to formulate a superior generics implementation. It has failed miserably to do so.

Before getting into the details, here's the main problems with Java's generics:

  1. At run-time, all generic type information is lost. For example, a Stack <Box> cannot be distinguished (at run-time) from a Stack <Receipt> - they both become just a Stack.
  2. At run-time, all type parameter instances are converted to/from Object references, meaning that there is a high upcast/downcast overhead (not to mention a high boxing-unboxing overhead, if using primitive types) when using generics.
  3. All instances of generic classes have the exact same static members.
  4. It is not possible to throw generic exceptions (generic classes derived from Throwable).

Problem 1 is the biggest and most significant, and is the underlying cause of the other three. The root cause of the problem is that a single generic definition is stored in the .class output file from the Java compiler. When a new generic instance of the class is created, it refers to this same block of code, with only compile-time checking to ensure that it is used consistently. The primary advantage of this method, and the primary excuse for this monumental screw-up, is that no changes needed to be made to the Java Virtual Machine specification in order to support generics. In my mind, the only beneficiaries of this decision are embedded devices that cannot upgrade or patch their virtual machine software.

C#, as a Java clone, faced a similar dilemma. It solved the problem by retaining exact generic type information at run-time (so that a Stack <Box> instance is a distinctly different type of object from a Stack <Receipt> instance) and by only creating a template block of code in the compiler's output file. Whenever a different generic type is required, the .NET virtual machine copies this template definition, at run-time, and modifies it to become the code for this specific generic type. Now, this solution does require more memory, has a slight run-time performance hit and required a new version of the .NET virtual machine specification to support this feature, but it is still a far superior solution to that adopted by Sun. And regular readers will attest that it's not very often that I admire something Microsoft have done! C++, which is free from many of the restrictions inherent in the design of both Java and C# merely creates generic-type specific code at compile time (or, for some implementations, at link time).

Problem 2 is also significant as it degrades run-time performance significantly. Because the same block of code must work for all supported type parameter instances, Java simply stores all type parameter data as Object class references. This means that all data going into storage must be upcast; a process that has almost no run-time overhead. However, when this data is returned, it must be downcast and converted from an Object reference back to a reference to the correct type. This has a much more significant run-time overhead.

Say I want to have a simple, generic queue of objects. I want to add objects to the end of the queue (a push operation) and remove objects from the head of the queue (a pop operation). I'd also like to know how many objects are in the queue. With generics, I can accomplish this as follows:

Main.java:

public class Main
{
    public static void main (String [] args)
    {
        Queue  doubleQueue = new Queue  ();
        Queue  intQueue = new Queue  ();
        doubleQueue.push (new Double (2.0));
        doubleQueue.push (new Double (5.0));
        doubleQueue.push (new Double (8.0));
        intQueue.push (new Integer (2));
        intQueue.push (new Integer (5));
        intQueue.push (new Integer (8));
        try
        {
            System.out.println ("Type of doubleQueue is " +
            doubleQueue.getClass ().getName ());
            while (doubleQueue.size () > 0)
            {
                System.out.println ("Next double in queue is " +
                doubleQueue.pop ().toString ());
            }
            System.out.println ("Type of intQueue is " + intQueue.getClass
            ().getName ());
            while (intQueue.size () > 0)
            {
                System.out.println ("Next integer in queue is " + intQueue.pop
                ().toString ());
            }
        }
        catch (Exception e)
        {
            System.out.println (e.toString ());
        }
    }
}

Queue.java:

public class Queue 
{
    private ListMember  first;
    private ListMember  last;
    private int numMembers;
    public Queue ()
    {
        first = null;
        last = null;
        numMembers = 0;
    }
    public void push (T member)
    {
        ListMember  newMember = new ListMember  (member);
        if (first == null)
        {
            first = newMember;
        }
        else
        {
            last.setNext (newMember);
        }
        last = newMember;
        ++numMembers;
    }
    public T pop ()
    throws Exception
    {
        if (first == null)
        {
            throw new Exception ("Queue is empty");
        }
        T poppedMember = first.getMember ();
        first = first.getNext ();
        if (first == null)
        {
            last = null;
        }
        --numMembers;
        return poppedMember;
    }
    public int size ()
    {
        return numMembers;
    }
}

ListMember.java:

public class ListMember 
{
    private T member;
    private ListMember  nextMember;
    public ListMember (T newMember)
    {
        member = newMember;
        nextMember = null;
    }
    public void setNext (ListMember  newNextMember)
    {
        nextMember = newNextMember;
    }
    public ListMember  getNext ()
    {
        return nextMember;
    }
    public T getMember ()
    {
        return member;
    }
}

When executed, this code produces the following output:

Type of doubleQueue is Queue
Next double in queue is 2.0
Next double in queue is 5.0
Next double in queue is 8.0
Type of intQueue is Queue
Next integer in queue is 2
Next integer in queue is 5
Next integer in queue is 8

However, here is some equivalent code that represents what the Java compiler actually emits when fed the above source. Note the complete lack of any generic syntax:

Main.java:

public class Main
{
    public static void main (String [] args)
    {
        Queue doubleQueue = new Queue ();
        Queue intQueue = new Queue ();
        doubleQueue.push (new Double (2.0));
        doubleQueue.push (new Double (5.0));
        doubleQueue.push (new Double (8.0));
        intQueue.push (new Integer (2));
        intQueue.push (new Integer (5));
        intQueue.push (new Integer (8));
        try
        {
            System.out.println ("Type of doubleQueue is " +
            doubleQueue.getClass ().getName ());
            while (doubleQueue.size () > 0)
            {
                System.out.println ("Next double in queue is " +
                ((Double) doubleQueue.pop ()).toString ());
            }
            System.out.println ("Type of intQueue is " + intQueue.getClass
            ().getName ());
            while (intQueue.size () > 0)
            {
                System.out.println ("Next integer in queue is " + ((Integer)
                intQueue.pop ()).toString ());
            }
        }
        catch (Exception e)
        {
            System.out.println (e.toString ());
        }
    }
}

Queue.java:

public class Queue
{
    private ListMember first;
    private ListMember last;
    private int numMembers;
    public Queue ()
    {
        first = null;
        last = null;
        numMembers = 0;
    }
    public void push (Object member)
    {
        ListMember newMember = new ListMember (member);
        if (first == null)
        {
            first = newMember;
        }
        else
        {
            last.setNext (newMember);
        }
        last = newMember;
        ++numMembers;
    }
    public Object pop ()
    throws Exception
    {
        if (first == null)
        {
            throw new Exception ("Queue is empty");
        }
        Object poppedMember = first.getMember ();
        first = first.getNext ();
        if (first == null)
        {
            last = null;
        }
        --numMembers;
        return poppedMember;
    }
    public int size ()
    {
        return numMembers;
    }
}

ListMember.java:

public class ListMember
{
    private Object member;
    private ListMember nextMember;
    public ListMember (Object newMember)
    {
        member = newMember;
        nextMember = null;
    }
    public void setNext (ListMember newNextMember)
    {
        nextMember = newNextMember;
    }
    public ListMember getNext ()
    {
        return nextMember;
    }
    public Object getMember ()
    {
        return member;
    }
}

Even the output is the same:

Type of doubleQueue is Queue
Next double in queue is 2.0
Next double in queue is 5.0
Next double in queue is 8.0
Type of intQueue is Queue
Next integer in queue is 2
Next integer in queue is 5
Next integer in queue is 8

In fact, when using Sun's javac compiler on Ubuntu Linux, the .class output files for both versions of the Main class are identical!

With this example, one might wonder about the benefits of programming with generics at all!

The third problem, unlike the other three, is likely to cement the current generics behavior. Once developers have come to rely on this behavior, their applications are going to break if Java's generics are ever fixed. Generally, language developers do not like to break existing code with new releases, so everyone will likely just have to live with this inferior implementation indefinitely.

The problem is this: if I have a generic class that has static data, then I expect each generic type to have its own copy of that static data. However, Java keeps a single copy of the static data for all generic types. Consider the following:

public class X <T>
{
    private static int count;
}

If you've done generic programming with either C++ or C#, then you'll know that X <Car>.count is a different object to X <Truck>.count. However, in Java they are identical. If you want to have common static data to your generic types, then you can easily derive your generic class from a base class that will contain the common static data. But if you want to make your static data unique to each generic type, then - in Java - you're stuffed.

As for the fourth problem, for me this is fairly minor. I'm sure that generic exceptions have their uses - such as presenting faulty type parameter data from generic classes - but the lack of this feature is relatively minor compared to the other three.

I'm sure that there are long-time Java gurus gnashing their teeth in anger at this blog, but the truth must be told. Although it will break the applications of some early adopters, Java's generics must be fixed.

Now that Sun has decided to release Java as free software, my hope is that the free software community will put matters to rights as soon as possible. If not (and Sun are going to stay in charge of the official branch of the software, after all), then I may have to consider going back to C# and .NET or picking some completely different language...

Your rating: None Average: 5 (1 vote)

Comments

More examples of how bad Java generics are...

Here's a thread that I started in the Sun Developer Network Java Generics forum: Overriding Object.equals in Generic class. It illustrates just how ridiculous Java's generics implementation really is...

Since writing this blog entry, I've discovered that I share these views with a whole army of other developers. Sadly, Sun's Java team do not appear to be amongst them.

It appears that the primary goal of Java "generics" (I now think that this feature is a misnomer) is to be able to run somewhat more type-safe code on old JVM implementations. The object of generic programming (which is the ability to write common code for similar - but different - classes) seems to have been lost on the Java language designers.

I think Generics are bad also ...

If the problem you are trying to solve for Collections is to avoid the cast HashSet myStringSet = new HashSet(); mySet.add("Hello"); mySet.add("World"); Iterator myStrings = myStringSet.iterator(); while (myStrings.hasNext() == true) { String myString = (String) myStrings.next(); //note the cast to String System.out.println(myString); } Why not trust what the developer is EXPLICITLY saying in this line: String myString = (String) myStrings.next(); and allow the developer to do this instead? String myString = myStrings.next(); then taking it further simply allow this sequence as they do in Generics to eliminate all the Iterator struff for (String myString: myStringSet) { System.out.println(myString); } Man that would be so much simpler! and easy to understand. I think the Sun team decided to create something that solves a bunch of problems, instead of just focusing on the real annoyance. Most developers don't need this kind of hand holding, and they certainly don't need man-handling, which is what Generics feels like.

Generics do a lot more than avoid casts...

I hope you don't mind me saying this, but I think you may be missing the point about Generics and casting. The point of Generics is not to "avoid the cast" as you claim - it's to avoid the possibility that the wrong type of data can be added to a collection. C#'s Generics implementation would also avoid the casting overhead that is necessary whether your programming language does automatic casting or not (Java's does not).

In your example, HashSet being a non-generic container (that is, a hashed set of Objects), what is going to stop me from adding an int, double, Elephant or Banana to your myString collection? (Just because you've used the word String in the name of the variable doesn't protect it from misuse.) Well, nothing - until I try (at run-time) to cast that data back to a String, whether explicitly or automatically, when I then get a bad cast exception.

C#'s Generics implementation would catch such a problem at compile-time (assuming that I used a HashSet <String> instead of a plain old HashSet) should I attempt to add a Fish to a String collection.

What you seem to be talking about is automatic casting, where the compiler figures out that an object needs to be cast to a new type, that the cast is possible and then puts that cast in without you having to type it in explicitly. But this has nothing to do with Generics at all - and Generics does not do any man-handling IMHO. Personally, I prefer the compiler to tell me that the two types in an assignment are different unless I explicitly cast one type to the other; I'm a developer, but I'm only human, and only too capable of making mistakes. I need all the help I can get...

Good point.  Generics do a

Good point.  Generics do a lot more than just avoid the cast, but it is part of the sell, and was the focus of what I wrote.

In connection to your comment regarding the compiler, as a matter of personal preference, I tend to rely on unit testing rather than the compiler to determine program correctness.  But that in itself is a whole other argument.

 Good article by the way.

You would do a great service

You would do a great service if you informed yourself BETTER ... Java 1.3 already had generics (surprise!) !!! Not the 'full-fledged' generics Java 5 "enhanced" (quotes add some sarcasm) but simpler - avoid the corner cases - generics. Ask Martin Oderski ... 

Wikipedia says Java 1.3 was released on the 8th of May of 2000

I pertfectly remember when .NET 1.0 was released as I was sacked for downloading it (using company resources for personal stuff! It was a student job anyway ...) at the start of 2002 ... Even then, generics were added as part of .NET Framework 2.0 in November 2005.

 Even more, Tiger aka Java 5 was released in September 30, 2004

So no Microsoft-head can claim prior art there either! Sorry, try again.

Anyway I find this type of flame unproductive, as every American knows that every sw thing was inverted in USA, and every European knows  viceversa (Zuse ...).

I stand corrected...

OK.  You're right, Java 1.5 (aka Java 5) did come out before C#/.NET 2.0.  I apologize and stand corrected.

However, I would like to point out that Java still doesn't have Generics.  It  may have a feature called "Generics" but you can't use it, "fully-fledged" as it may be, to do generic programming, so it doesn't qualify...  Sorry!

What's the relevance of the European vs. USA thing?

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <abbr> <acronym> <address> <br> <cite> <code> <col> <colgroup> <dd> <dfn> <dl> <dt> <em> <hr> <kbd> <li> <ol> <p> <pre> <samp> <strong> <sub> <sup> <table> <tbody> <td> <tfoot> <th> <thead> <tr> <tt> <ul> <var>

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Copy the characters (respecting upper/lower case) from the image.