Rants about Java and other internet technologies by Sam Pullara

Usability and Java 1.5 generics vs. Autocasting

was published on May 13th, 2004 and is listed in Technology
One of the most often touted effects of 1.5 generics is that it will reduce the number of casts and make collections more usable and maintainable. I think that it has succeeded for the most part, but the cure may be worse than the disease.
At The ServerSIde Symposium I went to Graham Hamilton’s and Bill Shannon’s talk on usability in Java 1.5 “Tiger”. Basically, the argument goes like this, in Java <=1.4 you have to cast objects that are coming out of collections, this problem is twofold, you have lots of additional casts in your code reducing readability and you have runtime exceptions for casting the object to the wrong thing as it comes out of the collection. Here is some sample 1.4 code:

 Map map = new HashMap(); map.put("key", "value");
 String value = (String) map.get("key");

The equivalent in 1.5, if you indeed wanted the Map to be limited to string keys and values would be:

 Map<String, String> map = new HashMap<String, String>();
 map.put("key", "value"); String value = map.get("key");

As you can see, in a simple example we are actually specifying more types than in the 1.4 example. Some might say that as you increase accesses to the map, you eventually pay down this debt, but for most cases there will only be 1 possibly 2 places where you access the map. From a usability standpoint we don’t seem to have saved a lot. As you can see, the solution to the original problem is focusing primarily on the second issue, the problem of pulling things out of the map and casting it to the wrong type. This is an admirable goal for maintainability but I must confess that this basically doesn’t happen in most Java code. So in exchange for a one line comment on the map variable we’ve added a huge new syntax to Java that makes the code more verbose and, in my opinion, quite ugly. In addition, the 1.5 generics feature doesn’t solve this problem:

 Context ctx = new InitialContext();
 MyEJBHome1 home = (MyEJBHome1) ctx.lookup("ejb/myhome1");
 MyEJBHome2 home = (MyEJBHome2) ctx.lookup("ejb/myhome2");

Since the lookup is truly not typed, we must do a dynamic runtime cast in order to ensure type safety. There is no solution within the generics framework for removing this cast. I have a counter proposal that gets us most of the way to readability Nirvana. I call it Autocasting. While experimenting with our Javelin compiler I found that I could correct errors in the framework instead of failing. For instance, let’s say I get code from the user that looks like this:

 Map map = new HashMap(); map.put("key", "value");
 String value = map.get("key");

Notice there is no cast on the map.get(). I could, in the compiler, automatically insert a cast on the right hand side to the type on the left hand side if the cast is possible. This would of course fail at compile-time if the LHS was incompatible with the RHS just as if you tried to cast an Integer to a String or something similar. At runtime it would fail the same way as if I inserted the cast myself, i.e. if the thing coming out of the map is not a String, you get a ClassCastException. This also has the property that you can write the above J2EE style code like this:

 Context ctx = new InitialContext();
 MyEJBHome1 home = ctx.lookup("ejb/myhome1");
 MyEJBHome2 home = ctx.lookup("ejb/myhome2");

That code would be just as safe as before. Some people might argue that the cast forces people to think about it more clearly. I would argue that its just one more compile-edit step where the developer immediately goes back to the code and types the cast. Obviously in todays IDEs this isn’t quite as big a deal because it will tell you right away that you must cast it, but you still have the readability problem — no one likes looking at code full of extraneous information when it is clear you believe that the thing on the right can be assigned to the type on left.

I have talked with a bunch of people about this and the biggest argument against it that I have gotten is this code:

 int length = map.get("key").length();

This would work with the generics, but would not work with autocasting. Autocasting would require you to write one of these:

 String value = map.get("key");
 int length = value.length();

or

 int length = ((String)map.get("key")).length();

Where the second one is no better than what you have to do today. In the end, what I am really worried about, is adding all of this “generics” syntax to the language? I mean, have you looked at the javadocs for HashMap lately? That is enough to intimidate virtually anyone but a C++ programmer. If they aren’t careful Java will be crushed by much easier to use languages like Groovy just because the programs are about to get a lot more complicated.

Example Javadocs:

public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable

public HashMap(Map<? extends K,? extends V> m)

"Usability and Java 1.5 generics vs. Autocasting" was published on May 13th, 2004 and is listed in Technology.

Follow comments via the RSS Feed | Leave a comment | Trackback URL

  • Chris
    I think the point you are missing is that what you call a "mixed map" can often be folded into a generic through the use of an interface; wrap the elements of your map in an interface that abstracts the functionality that binds them together into a single map in the first place. Then you declare the generic map using the interface as the element type.

    This has the side effect that you must make explicit in the API the design decisions behind why the elements are in the same map, thus self-documenting the features of the underlying connectivity between the pieces. This was part of the goal with generics. An object-oriented purist would say that anytime you are forced to cast you are missing an opportunity for polymorphism. Unfortunately the Java 1.4 Collections API forced you to cast just to use the most basic features.

    In Java 1.5 we are closer to the ideal, which is that you should see every cast as an opportunity to add polymorphism.
  • You are totally missing the point on the example. There is no opportunity for doing what you suggest with JNDI because the common 'interface' is Object and actually may not have any commonality at all.
  • Krish
    Hmmm... If I follow your logic correctly, in the simple case, you know the type of the values by inspecting what is being inserted into the map. In a local map this is pretty simple to do... But what if it is a static map and values are getting inserted (retrieved) into (from) this map from all over the application code. How would you know what type you are supposed to autocast to at compile time. Generics elegantly solves that problem by letting us define the type of the collection.

    Also, in the J2EE case, how do you know the type of the EJB to autocast it to? All you have is the jndi name.
  • The simple case where you have:

    String value = map.get(key)

    I'm not looking at what was inserted into the map, merely assuming that you know what you put in the map and automatically adding a cast under the covers so the bytecode actually looks like:

    String value = (String) map.get(key)

    Similarly in the case of doing a JNDI lookup, the developer has already declared the type that is coming out of the JNDI context on the left-hand side of the assignment and the cast is just repeating yourself.
  • Krish
    Hmmm... That putting the type checking responsibility on the developer. That's taking the main power of java (static type checking) out of the equation. In the EJB case, it's fine, because it will immediately be caught when we start testing the app.

    But in a generic Map, in big apps, if we don't rely on the compiler to do the type checking if can become a big issue in a production environment. Having said that, java till 1.4 had that hole and that's what generics avoids by strictly typing collections.

    Still there can still be uses for a mixed type maps. In those cases, your solution can reduce some clutter in the code.
  • Amusingly, generics in Java don't prevent you from using the wrong key type, here is some legal code that fails at runtime:

    Map<String, String> map = new HashMap<String, String>();
    map.put("test", "test");
    StringBuilder sb = new StringBuilder("test");
    String a = map.get(sb); // works fine, returns null, no error at compile time.

    So even they do not prevent some kinds of errors that could be statically prevented. IntelliJ will warn you about that one though.
blog comments powered by Disqus

YUI-Mainstream Theme by Buzzdroid.com

 Premium Wordrpess Theme