The debate between static and dynamic typing is a long one, and not one I have a definite opinion on. I tend to jump from one side to the other depending on the phase of the moon. I am going, however, to make a statement now that is entirely true:
Java's implementation of static typing is stupid.
Before anyone jumps down my throat, and because "stupid" is also a general-purpose perjorative, I'm going to give a more formal definition of what it is to be a stupid type system:
A stupid type system is one where the programmer is forced to do unnecessary work for the compiler.
A good way to get an idea of how stupid Java's type system is, is to use a modern Java IDE for a while. Start using IDEA's intention actions, and see just how often IDEA knows what the type of an object is going to be before you bother to assign it a type, or make a cast. Every time you ask the IDE to assign a cast, create a field or iterate over a collection and you select the default value, you're telling the compiler something that IDEA was smart enough to know already.
And with 1.5 and the introduction of generics, the type system is getting even stupider, giving you more and more places where you have to tell the compiler what types to expect because it's too stupid to work it out for itself. Even though the IDEs are smart enough to do this, it's still annoying and disruptive for the programmer to have to make the context switch and alt-enter a cast into place. I'd much prefer Java itself was better able to this sort of stuff itself.
I'd love to see Java have some form of type inference. Because of the way Java treats separate classes as independent compilation units, you'd have to keep interactions between classes manifestly typed. But within a class, there's no excuse for the compiler not to be able to infer the type of variables and method signatures from the objects that are assigned to them, and the methods that are called on them.
You wouldn't lose any type-safety. We are not removing static typing, we're just leaving it to the compiler to fill in the obvious bits. The compiler would still be perfectly able to spot the assignment of incompatible classes to a variable, or the calling of non-existent methods on an object. It could even detect when you were moving incompatible objects in and out of a collection without any tedious mucking around with expressly declared generics.1
The only ambiguity would come from methods that are overridden with arguments of overlapping types, which is really bad practice anyway.
All you'd lose is, well, a lot of wasted typing.
(This post originally came with a code example which I wrote while also trying to wrestle with CruiseControl. As a result, I quickly removed the example because it was woefully incorrect in almost every respect, and I've already wasted enough time today writing this post. On the other hand, I fixed the CruiseControl build by changing the max size of the permanent generation. Hooray.)
1You'd still need generics so that you'd know you expect the same objects out of a collection as you put in, but they'd be a lot less obtrusive.