130

I've occasionally heard that with generics, Java didn't get it right. (nearest reference, here)

Pardon my inexperience, but what would have made them better?

Community
  • 1
  • 1
sdellysse
  • 39,108
  • 9
  • 25
  • 24

13 Answers13

164

Bad:

  • Type information is lost at compile time, so at execution time you can't tell what type it's "meant" to be
  • Can't be used for value types (this is a biggie - in .NET a List<byte> really is backed by a byte[] for example, and no boxing is required)
  • Syntax for calling generic methods sucks (IMO)
  • Syntax for constraints can get confusing
  • Wildcarding is generally confusing
  • Various restrictions due to the above - casting etc

Good:

  • Wildcarding allows covariance/contravariance to be specified at calling side, which is very neat in many situations
  • It's better than nothing!
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 2
    I second the above comment! And would like to add, that runtime checking is a performance hit and it (runtime checking) is also available via the Collections api. – Elijah Feb 07 '09 at 10:42
  • 53
    @Paul: I think I would have actually preferred a temporary breakage but a decent API afterwards. Or take the .NET route and introduce new collection types. (.NET generics in 2.0 didn't break 1.1 apps.) – Jon Skeet Feb 07 '09 at 11:13
  • 6
    With a big existing code base, I like the fact that I can start change the signature of one method to use generics without changing everybody who calls it. – Paul Tomblin Feb 07 '09 at 15:09
  • 40
    But the downsides of that are huge, and permanent. There's a big short term win, and a long term loss IMO. – Jon Skeet Feb 07 '09 at 16:32
  • 11
    Jon - "It's better than nothing" is subjective :) – MetroidFan2002 Mar 09 '09 at 17:05
  • 1
    Your first "Bad" item isn't correct: much of the generic type information IS preserved in the class file, and is available at runtime through the Java Reflection API. – Rogério Dec 27 '09 at 23:42
  • 3
    @Rogerio: However many times you downvote me for bringing up type erasure, you still can't ask an *object* what its type arguments are. That information *is* lost at compile-time. You can ask a *field* what its type arguments are, but that isn't the same thing. – Jon Skeet Dec 28 '09 at 09:36
  • Sure, I never said that none of the type info wasn't lost. But your answers related to type erasure are not completely correct. The big problem here is that other developers may get a worse impression from type erasure than it actually deserves. – Rogério Dec 29 '09 at 02:21
  • 7
    @Rogerio: You claimed the my first "Bad" item isn't correct. My first "Bad" item says that information is lost at compile time. For that to be incorrect, you must be saying that information *isn't* lost at compile time... As for confusing other developers, you seem to be the only one confused by what I'm saying. – Jon Skeet Dec 29 '09 at 07:26
  • 4
    Come on: your statement says without qualification that "type information is lost at compile time". This is not true at all for new type declarations, parameter declarations, return type declarations, and field declarations (as long as they actually specify the types). For example, when I declare a method parameter as being a List, that information is clearly not "lost at compile time". – Rogério Dec 29 '09 at 12:55
  • "Wildcarding allows covariance/contravariance to be specified at calling side, which is very neat in many situations". You include this as "good"? It would be much simpler, instead of having the burden of wildcards at every call site, to define them _once_ at declaration site (which is supported in Scala), so one could express things like "A List is a subtype of List if A is a subtype of B" _once_, instead of repeating this via wildcards all over the place. – Dimitris Andreou Apr 07 '10 at 16:20
  • 2
  • @Jon, the point is that to express things like this, call-site declarations (via wildcards) is an excessive burden that Java forces. Now whether an example is right or not is a separate issue. While covariance would be clearly wrong for java.util.List in particular, for some immutable List it is perfectly fine (like scala.List). For some write-only List type, contravariance would make sense. Again, the point is that Java makes it hard to express such types. (You can replace "List" with "X", it's just a name). – Dimitris Andreou Apr 07 '10 at 17:50
  • @Dimitris: But List demonstrated the flexibility of the Java solution so very neatly - it showed that even when covariance is wrong for List *in general* it's fine *in some situations*. If you only want to use a type in a way which works fine covariantly, even though the type itself is invariant, can you do that in Scala? You can't in C#, and just occasionally that's a pain. I totally agree that it's a pain in various places in Java, but it's an ability which is sometimes useful, and I see nothing wrong with acknowledging that usefulness. – Jon Skeet Apr 07 '10 at 18:35
  • @Jon, (not exactly sure if this is what you're asking) scala also supports wildcards ("existential types"), mainly for Java interoperability. As a side note, wildcards are in most cases just syntax over bounded type parameters, e.g. 'java.util.List extends Number>' can be also expressed as ' ... List' (which is a bit more useful than the first version since you can write T's to that List then) – Dimitris Andreou Apr 07 '10 at 19:03
  • 1
    @Dimitris: Yes, that's how you'd normally approach it in C# too - but it means (in C# at least) making another method generic which didn't need to be before. The genericness spreads up through the program like an infection if you're not careful :) Again, this is my experience with C# rather than Scala of course - maybe Scala gets round it in a cunning way. – Jon Skeet Apr 07 '10 at 19:16
  • 3
    @Jon Skeet I'd add this to your list: Java generics are not as type-safe as C# generics (cf Paul Tomblin's answer), the typing can be circumvented at runtime (perhaps using ugly hacks, but it can). Also the typing of some functions is strangely incomplete and bug-hiding-prone: who the heck designed `List.remove` to accept any Object as argument ? (I saw a real bug hidden this way.) (I guess the culprit is backwards-compatibility...) As a result, I'm never 100% sure my Java generic code avoid cast errors... while I feel safe with C# generics. – user192472 Aug 11 '10 at 13:49
  • 3
    Yep. It's 14 years after Java introduced generics. I want to deserialize json into generic objects but it's a total kludge (you have to declare your generic object AND you have to pass the generic type parameter to the constructor because you can't figure out the generic type inside the constructor at run time.) Great... java generics provided a short term win of not having to break existing code but now for the rest of eternity ALL future code has to be burdened with this garbage architecture. – WiegleyJ Dec 12 '17 at 21:06
27

The biggest problem is that Java generics are a compile-time only thing, and you can subvert it at run-time. C# is praised because it does more run-time checking. There is some really good discussion in this post, and it links to other discussions.

Community
  • 1
  • 1
Paul Tomblin
  • 179,021
  • 58
  • 319
  • 408
  • That's not really a problem, it was never made to be for runtime. It is as if you say that boats are a problem because they cannot climb mountains. As a language, Java does what many people need: express types in a meaningful way. For runtime type inforcements, people still can pass `Class` objects around. – Vincent Cantin Jan 29 '16 at 08:17
  • 3
    He's right. your anology is wrong because the people designing the boat SHOULD have designed it to climb mountains. Their design goals were not very well thought out for what was needed. Now we're all stuck with just a boat and we can't climb mountains. – WiegleyJ Dec 12 '17 at 21:11
  • 3
    @VincentCantin -- it is most definitely a problem. Hence, we're all complaining about it. Java generics are half-baked. – Josh M. Apr 24 '19 at 19:58
20

The main problem is that Java doesn't actually have generics at runtime. It's a compile time feature.

When you create a generic class in Java they use a method called "Type Erasure" to actually remove all of the generic types from the class and essentially replace them with Object. The mile high version of generics is that the compiler simply inserts casts to the specified generic type whenever it appears in the method body.

This has a lot of downsides. One of the biggest, IMHO, is that you can't use reflection to inspect a generic type. Types are not actually generic in the byte code and hence can't be inspected as generics.

Great overview of the differences here: http://www.jprl.com/Blog/archive/development/2007/Aug-31.html

JaredPar
  • 733,204
  • 149
  • 1,241
  • 1,454
  • 2
    I am not sure why you were down-voted. From what I understand you are right, and that link is very informative. Up-voted. – Jason Jackson Feb 06 '09 at 15:06
  • @Jason, thanks. There was a typo in my original version, I guess I was downvoted for that. – JaredPar Feb 06 '09 at 15:10
  • 4
    Err, because you *can* use reflection to look at a generic type, a generic method signature. You just can't use reflection to inspect the generic information associated with a parametrized instance. For example, if I have a class with a method foo(List), I can reflectively find "String" – oxbow_lakes Feb 06 '09 at 16:35
  • There are lots of articles that say type erasure makes finding the actual type parameters impossible. Lets say: Object x = new X[Y](); can you show how, given x, to find the type Y? – user48956 Dec 06 '13 at 21:37
  • @oxbow_lakes -- having to use reflection to find a generic type is almost as bad as not being able to do that. E.g. it's a bad solution. – Josh M. Apr 24 '19 at 19:59
15
  1. Runtime implementation (ie not type erasure);
  2. The ability to use primitive types (this is related to (1));
  3. While the wildcarding is useful the syntax and knowing when to use it is something that stumps a lot of people. and
  4. No performance improvement (because of (1); Java generics are syntactic sugar for castingi Objects).

(1) leads to some very strange behaviour. The best example I can think of is. Assume:

public class MyClass<T> {
  T getStuff() { ... }
  List<String> getOtherStuff() { ... }
}

then declare two variables:

MyClass<T> m1 = ...
MyClass m2 = ...

Now call getOtherStuff():

List<String> list1 = m1.getOtherStuff(); 
List<String> list2 = m2.getOtherStuff(); 

The second has its generic type argument stripped off by the compiler because it is a raw type (meaning the parameterized type isn't supplied) even though it has nothing to do with the parameterized type.

I'll also mention my favourite declaration from the JDK:

public class Enum<T extends Enum<T>>

Apart from wildcarding (which is a mixed bag) I just think the .Net generics are better.

cletus
  • 616,129
  • 168
  • 910
  • 942
  • But the compiler will tell you, particularly with the rawtypes lint option. – Tom Hawtin - tackline Feb 06 '09 at 14:56
  • Sorry, why is the last line a compile error? I'm messing around with it in Eclipse & can't get it to fail there - if I add enough stuff for the rest of it to compile. – oconnor0 Feb 04 '10 at 21:44
  • 1
    `public class Redundancy>` ;) – java.is.for.desktop Aug 08 '10 at 01:00
  • @oconnor0 It's not a compilation failure, but a compiler warning, for no apparent reason : `The expression of type List needs unchecked conversion to conform to List` – artbristol Nov 05 '12 at 11:16
  • `Enum>` may seem weird/redundant at first but is actually pretty interesting, at least within the constraints of Java/it's generics. Enums have a static `values()` method giving an array of their elements typed as the enum, not `Enum`, and that type is determined by the generic parameter, meaning you want `Enum`. Of course, that typing only makes sense in the context of an enumerated type, and all enums are subclasses of `Enum`, thus you want `Enum`. However, Java doesn't like mixing raw types with generics, thus `Enum>` for consistency. – JAB May 23 '14 at 16:12
11

I'm going to throw out a really controversial opinion. Generics complicate the language and complicate the code. For example, let's say that I have a map that maps a string to a list of strings. In the old days, I could declare this simply as

Map someMap;

Now, I have to declare it as

Map<String, List<String>> someMap;

And every time I pass it into some method, I have to repeat that big long declaration all over again. In my opinion, all that extra typing distracts the developer and takes him out of "the zone". Also, when code is filled with lots of cruft, sometimes it's hard to come back to it later and quickly sift through all the cruft to find the important logic.

Java already has a bad reputation for being one of the most verbose languages in common use, and generics just add to that problem.

And what do you really buy for all that extra verbosity? How many times have you really had problems where someone put an Integer into a collection that's supposed to hold Strings, or where someone tried to pull a String out of a collection of Integers? In my 10 years of experience working at building commercial Java applications, this has just never been a big source of errors. So, I'm not really sure what you're getting for the extra verbosity. It really just strikes me as extra bureaucratic baggage.

Now I'm going to get really controversial. What I see as the biggest problem with collections in Java 1.4 is the necessity to typecast everywhere. I view those typecasts as extra, verbose cruft that have many of the same problems as generics. So, for example, I can't just do

List someList = someMap.get("some key");

I have to do

List someList = (List) someMap.get("some key");

The reason, of course, is that get() returns an Object which is a supertype of List. So the assignment can't be made without a typecast. Again, think about how much that rule really buys you. From my experience, not much.

I think Java would have been way better off if 1) it had not added generics but 2) instead had allowed implicit casting from a supertype to a subtype. Let incorrect casts be caught at runtime. Then I could have had the simplicity of defining

Map someMap;

and later doing

List someList = someMap.get("some key");

all the cruft would be gone, and I really don't think I'd be introducing a big new source of bugs into my code.

John Topley
  • 113,588
  • 46
  • 195
  • 237
Clint Miller
  • 15,173
  • 4
  • 37
  • 39
  • 18
    Sorry, I disagree with just about everything you said. But I'm not going to vote it down because you argue it well. – Paul Tomblin Feb 07 '09 at 01:44
  • 2
    Of course, there are plenty of examples of large, successful systems built in languages like Python and Ruby that do exactly what I suggested in my answer. – Clint Miller Feb 07 '09 at 15:14
  • I think my whole objection to this type of idea is not that it's a bad idea per se, but rather that as modern languages such as Python and Ruby make life easier and easier for developers, developers in turn get intellectually complacent and eventually have a lower comprehension of their own code. – Dan Tao Jan 29 '10 at 18:10
  • 6
    "And what do you really buy for all that extra verbosity? ..." It tells the poor maintenance programmer what's supposed to be in those collections. I have found this to be an issue working with commercial Java applications. – richj Feb 13 '10 at 20:54
  • 6
    "How many times have you really had problems where someone put an [x] into a collection that's supposed to hold [y]s?" - Oh boy I have lost count! Also even if there is no bug it is a readability killer. And scanning many files to find out what the object is going to be (or debugging) really does pull me out of the zone. You might like Haskell - even strong typing but less cruft (because types are inferred). – Martin Capodici Aug 02 '15 at 19:33
  • 1
    I am mostly working with Python, and your argument can only come from someone who has only been working with Java, where the language actively prevents this problem. In Python, where you often have maps containing multiple types of data, this can get messy very quickly. Often the source of an object of the wrong type can be in a very different part of the project than where the error occurred, because this wrong value has been passed around for a while and there is no static type checking to find the problem. You always have the choice between verbosity or safety. Can't have both. – Dakkaron Sep 28 '18 at 12:47
  • @Dakkaron "You always have the choice between verbosity or safety." Perhaps... To some extent. But Java's verbosity is truly pointless. Most of it is not adding any information the compiler couldn't infer and validate, it's just vestigial cruft. There's no justification for a simple class taking hundreds of lines of getters/setters and all the other boilerplate. C# achieves the same level of type safety in a fraction of the code (better, in fact, as it has reified generics too). – Basic Oct 10 '18 at 16:14
  • 1
    Are you kidding me? I agree with you, generics add more complexity than they're worth, but implicit downcasting is a terrible idea. Although coercions like `Integer n = "7";` would still be illegal, why should an `Animal` be squashed into a doghouse, for not all `Animal`s are `Dog`s? Moreover, using `Object`s would completely eliminate type safety. – Sapphire_Brick Jun 23 '21 at 17:58
9

Another side effect of them being compile-time and not run time is that you can't call the constructor of the generic type. So you can't use them to implement a generic factory...


   public class MyClass {
     public T getStuff() {
       return new T();
     }
    }

--jeffk++

jdkoftinoff
  • 2,391
  • 1
  • 17
  • 17
8

Ignoring the whole type erasure mess, generics as specified just don't work.

This compiles:

List<Integer> x = Collections.emptyList();

But this is a syntax error:

foo(Collections.emptyList());

Where foo is defined as:

void foo(List<Integer> x) { /* method body not important */ }

So whether an expression type checks depends on whether it is being assigned to a local variable or an actual parameter of a method call. How crazy is that?

Nat
  • 9,820
  • 3
  • 31
  • 33
  • 3
    The inconsistent inference by javac is crap. – mP. Nov 17 '09 at 00:27
  • 3
    I would assume that the reason that the latter form is rejected is because there might be more than one version of "foo" due to method overloading. – Jason C Oct 28 '10 at 22:25
  • 3
    this critique no longer applies as Java 8 introduced improved target-type inference – obataku Jun 29 '16 at 20:39
6

Java generics are checked for correctness at compile time and then all type information is removed (the process is called type erasure. Thus, generic List<Integer> will be reduced to its raw type, non-generic List, which can contain objects of arbitrary class.

This results in being able to insert arbitrary objects to the list at runtime, as well as it's now impossible to tell what types were used as generic parameters. The latter in turn results in

ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if(li.getClass() == lf.getClass()) // evaluates to true
  System.out.println("Equal");
Anton Gogolev
  • 113,561
  • 39
  • 200
  • 288
5

The introduction of generics into Java was a difficult task because the architects were trying to balance functionality, ease of use, and backward compatibility with legacy code. Quite expectedly, compromises had to be made.

There are some who also feel that Java's implementation of generics increased the complexity of the language to an unacceptable level (see Ken Arnold's "Generics Considered Harmful"). Angelika Langer's Generics FAQs gives a pretty good idea as to how complicated things can become.

Zach Scrivena
  • 29,073
  • 11
  • 63
  • 73
5

Java generics are compile-time only and are compiled into non-generic code. In C#, the actual compiled MSIL is generic. This has huge implications for performance because Java still casts during runtime. See here for more.

Szymon Rozga
  • 17,971
  • 7
  • 53
  • 66
5

I wish this was a wiki so I could add to other people... but...

Problems:

  • Type Erasure (no runtime availability)
  • No support for primative types
  • Incompatability with Annotations (they were both added in 1.5 I'm still not sure why annotations don't allow generics aside from rushing the features)
  • Incompatability with Arrays. (Sometimes I really want to do somthing like Class<? extends MyObject>[], but I'm not allowed)
  • Wierd wildcard syntax and behavior
  • The fact that generic support is inconsistant across Java classes. They added it to most of the collections methods, but every once in a while, you run into an instance where its not there.
Laplie Anderson
  • 6,345
  • 4
  • 33
  • 37
  • 1
    What “Incompatability with Annotations” are you talking about? – Holger May 20 '22 at 10:42
  • @Holger , Before Java 8, you couldn't write annotations on generic types. For example `List<@NonNull String>`. You also can't use generics in the annotation's declaration. For example `@interface PairClass { Class value(); }` – Laplie Anderson Jul 11 '22 at 15:36
4

Java doesn't enforce Generics at run time, only at compile time.

This means that you can do interesting things like adding the wrong types to generic Collections.

Powerlord
  • 87,612
  • 17
  • 125
  • 175
2

If you listen to Java Posse #279 - Interview with Joe Darcy and Alex Buckley, they talk about this issue. That also links to a Neal Gafter blog post titled Reified Generics for Java that says:

Many people are unsatisfied with the restrictions caused by the way generics are implemented in Java. Specifically, they are unhappy that generic type parameters are not reified: they are not available at runtime. Generics are implemented using erasure, in which generic type parameters are simply removed at runtime.

That blog post, references an older entry, Puzzling Through Erasure: answer section, that stressed the point about migration compatibility in the requirements.

The goal was to provide backwards compatibility of both source and object code, and also migration compatibility.

Kevin Hakanson
  • 41,386
  • 23
  • 126
  • 155