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 <Double> doubleQueue = new Queue <Double> ();
        Queue <Integer> intQueue = new Queue <Integer> ();
        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 <T>
{
    private ListMember <T> first;
    private ListMember <T> last;
    private int numMembers;
    public Queue ()
    {
        first = null;
        last = null;
        numMembers = 0;
    }
    public void push (T member)
    {
        ListMember <T> newMember = new ListMember <T> (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 <T>
{
    private T member;
    private ListMember <T> nextMember;
    public ListMember (T newMember)
    {
        member = newMember;
        nextMember = null;
    }
    public void setNext (ListMember <T> newNextMember)
    {
        nextMember = newNextMember;
    }
    public ListMember <T> 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: 3.9 (18 votes)

Comments

CH15 of the book "Thinking

CH15 of the book "Thinking in JAVA, 4th edition" tell you why

the generic of JAVA is suck.

Just Stop!

You know what you are doing? You are giving free advice. Advice people don't want to hear.

If Harvard stopped charging students tuition, what would be the value of a degree from Harvard? Would people want to get an MBA from Harvard? Of course, not!

I used to be a C++ fanatic, who was forced into Java almost since its inception. I have suffered the humiliating task of delivering solutions written in Java. Every once in a while I have had the chance to dabble in C# and have come away even more disillusioned with Java.

The promise of Java is dead. People need to wake up. The problem is most people tie their careers to TOOLs instead of learning Software Development at large. They are therefore insecure when someone criticises their tool of choice. Case in point is one ridiculous reply mentioning verbose error messages from C++ templates.

The simple fact of the matter is those fools who couldn't discipline themselves to call <i>delete</i> or didn't understand the power of constructors and destructors for resource cleanup, are the same idiots writing buggy leaky Java code. Some of them are now "enterprise architects".

STOP giving people advice for free. Let them F up using JAVA. Then CHARGE THEM 10 times your salary to fix their S***. THAT is what they deserve. I started doing that this year. Yes. I gave up. No more free advice.

JAVA is GREAT. LONG LIVE JAVA! BRING IN THE MOOLAH! 

C++ Templates cause horrendous error messages

It's odd to hear you criticise Java generics for erasing a tiny bit of type information at compile time, yet claim to be a huge fan of the C++ templating system, which erases masses of type information at compile time.  In C++, tiny errors when programming with simple container types like map<string,string> lead to impenetrable multi-line error messages:

   test.cpp: In function 'void print(const StringToStringMap&)':test.cpp:8: error: conversion from'std::_Rb_tree_const_iterator<std::pair<const std::basic_string<char,std::char_traits<char>, std::allocator<char> >, std::basic_string<char,std::char_traits<char>, std::allocator<char> > > >' to non-scalar type'std::_Rb_tree_iterator<std::pair<const std::basic_string<char,std::char_traits<char>, std::allocator<char> >, std::basic_string<char,std::char_traits<char>, std::allocator<char> > > >' requested

This is because C++ compiles away nice abstractions like "string" into low-level types like "basic_string<char, char_traits<char>, allocator<char>>", and when you make a container of strings this blows up recursively.

For more information, see http://yosefk.com/c++fqa/defective.html#defect-12 .

Huh?

I'd agree that C++ templates can generate very long and cryptic error messages - if you use them incorrectly, that is.  So what?  How is that relevant to this discussion?

I'd also agree that, because C++ doesn't yet do much in the way of reflection, that lots of type information is lost at compile time - whether you're compiling template or non-template code.  Again, so what?  C++ does not have to rely on run-time type information to implement templates or to create generic code.

In Java and C#, generic declarations are compiled and generic instances reference that code.  In C++, template declarations are not compiled and template instances have no compiled code to reference; instead, template instances are compiled by expanding the template declaration.  So, in C++, a std::list <Bicycle> is a completely different type of object from a std::list <Chores> - no run-time type information required.

Simulation engineers do it with models virtually every day!

thank you

Thanks for your post. Recently needed to do some small Java class not knowing any Java. On my 2nd day in the world of Java bumped "T[] a = new T[10]" issue (within a "generic" class) and could hardly believe what I read on "Generic array creation". Kept assuming I must be reading stuff that was relevant for some Java version long gone. Sadly not. Refreshing to find out I am not the only one thinking "what a monumental cockup on the part of the language designers". In terms of productivity lost just mind boggling.

Java generics are NOT C++

Java generics are NOT C++ templates. They look like this, they are intended to behave in a similar way, but they are not templates.

To turn generics into a C++ template it would require a slight change in javac compiler. It wil however, as You suggested, break the compatiblity with current concept.

For those, who needs details about generics at runtime reflection provides some mechinsms to do that.

Yours int/double example shows not the weakness of generics, but the weakness of OO programming - if you say that "everything is an object" and build a generic collection which has an engine to "hold anything" then sorry - neither double nor int can be stored as simple bytes. This is where auto boxing helps programmer to spare some typing.

Anyway, what is really cute in a whole Java development process is, that none of most recent changes (generics, autoboxing) did caused ANY CHANGES in virtual machine. Implementing VM in an embedded environment is a a very expensive process (and java is very often used in embedded environment) and this is a GREAT idea that SUN is extending java language without even touching VM specs.

 

Regards, Tomasz Sztejka

 

P.S. It seems I'm not a "human visitor" according to my OCR capabilities :(

Java Generics != Generic Programming

I don't believe I made the claim that Java Generics should be like C++ templates.  In many ways, Java's approach is better than that of C++ - in particular the ability to specify constraints on type parameters.

Unfortunately, Java's type erasure - a feature named, ironically, to sound like it's doing something active instead of being a passive omission of run-time type data - effectively kills Java as a generic programming language.  Java devotees seem to miss the point, so I'll say it again: type erasure kills Java as a generic programming language.  If Java Generics had been called "Java Improved Collection Type Safety" then I'd have no problems with it.  But they called it Java Generics, implying that you can do generic programming with it.  You can't.

John Wang, it's time to move on

> I have tried more than 5 times to try to udnerstand generics

Ten years of Java would damage anybody. Time to try some Groovy or Scala, perhaps?

why cant add a method for type-safe?

i dont understand why sun cannot simply add a new mthod to Colletion API, like for Map, setKeyType(java.lang.Class type)...

then this imformation can go to both compile time and run time.....

I have been using java for 10 yrs now, I have tried more than 5 times to try to udnerstand generics, still cannot get it... the generic interface and class and method..... the simplicity of java syntax is merely gone!

 

 

 

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?

and get corrected again....

Generics were part of the CLR from .net 1.1.

The ECMA spec supported them in MSIL from the begining.

What missed back then was the tools needed to generate these type i.e. compilers like C#, Vb , etc

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.

There is no substitute for

There is no substitute for unit testing, but in practice, when the gun is to your head and you need to get the code into production yesterday, your unit tests are not going to have 100% coverage. Plus, with unit tests, you need to spend time debugging to find the error. So, do I want a red underline a fraction of a second after writing incorrect code along with an explanation of why it is wrong, or do I want to hope that the 1) the erroneous code is actually executed y a unit test, 2) the code results in a visible error (exception, error message, assertion failure), and 3) I have extra time on my hand to debug why the error occurred and fix it. All that time could be saved by having a red underline. If you are pressed for time (who isn't?), there is no substitute for "edit time" error detection. Again, I'm all for unit testing, but to save time a first line of defense should be the IDE's editor.

 

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.