My background is worth noting here. I was an early adopter of Java when it was first introduced, and I did some very in-depth work back in the 3.0 generation of the browsers -- the Java applet that I wrote for the InterMOO/Windhaven project was one of the most sophisticated Java programs of its day. (A self-installing, self-updating multimedia client platform that ran in both IE and Netscape -- not a small problem in those days.) I haven't worked in it since then, though, and have never done server-side work with it before. I've spent the past five years working with its kissing cousin C#, and that heavily colors my views here. (It should also be noted that I'm not yet expert in Java, although I'm getting decently well-read; corrections are welcome to any errors I might make here.)
On the plus side, I broadly like the platform -- in many ways, it's still a good deal mature than .NET. This is largely because of the open-source support, which has produced libraries that are things of joy. For instance, I spent yesterday getting log4j up and running in CommYou: that was fairly easy, and the library is just plain better than anything Microsoft has. Similarly, while Hibernate is almost mind-bogglingly deep, it is proving to be exactly what I was looking for: a solid, and (once you grok it) delightfully sensible and powerful object-relational mapping system. Both of these map very closely to systems that we had to build ourselves at Convoq, because Microsoft just didn't have them. I may occasionally rant about weak documentation, especially in the integration of these libraries, but the systems themselves are generally very pretty.
(It's worth noting that, now that I'm over on this side of the fence, I'm discovering that many of these systems *have* been ported to .NET. The problem is, Microsoft doesn't support or encourage any of this the way the Java community does -- you'd never know that these open-source projects were out there, because Microsoft tries to hide anything that isn't closely connected to them. The difference in *attitude* is very significant here, and is a strength of the Java community.)
That said, I'm really pretty disappointed in the Java *language* itself. I knew that there was a lot that C# had that wasn't in Java, but it's actually worse than I expected. I mean, just on the syntactic-sugar and convenience side, there are a ton of minor features in C# that are apparently missing in Java, ranging from the yield operator (which makes iterators far easier to write) to properties (*really* just syntactic sugar, but given that the whole Beans environment is built around the same assumptions, I find it silly not to have them). And it's still missing some really useful tools like partial classes, that make programming in the large considerably easier.
But my real annoyance is in Java's apparently fanatical devotion to objects, at the cost of functions. In particular, Java is missing three features that I think greatly weaken it: proper closures, events and *especially* delegates. Let's talk about those.
First and foremost is delegates. In C#, these are function signatures, done properly. C#, like C++, allows you to pass functions around; however, in C# they are strongly-typed, easy to use and fairly hard to abuse the way you can C++ function pointers. More importantly, you can declare inline function blocks in C#, just as in functional languages, and pass them around in a strongly-typed way using delegates. Java's nearest cognate is anonymous inner classes, but those are a *very* poor substitute. In most cases, delegates are exactly what you want when doing higher-order programming: I want this thing to call this method, and nothing else. In the Java idiom, I have to create an extra class whose sole purpose is usually to hold a single function signature, and I have to use a fairly strange syntax to override that single function in order to pass my desired method through. Basically, it takes about eight times as much code in Java as in C# to do the same thing -- and given how often I like to do higher-order programming, that's a pain in the ass.
(An example: Hibernate requires a fairly particular boilerplate pattern in order to use it safely, to wrap the transaction properly. That's fine: I simply write a higher-order function to do the boilerplate, and pass in a function that does the specific work in this case. To do this in Java, I've wound up needing to declare two extra classes, and each call still needs two extra lines of pointless boilerplate that I wouldn't need in C#. And I need this in almost every API call: a real pain, resulting in significantly more opaque code.)
This is coupled with Java's broken attitude towards closures. In C# it's nice and straightforward: it provides lexical closures that pretty much always do exactly what you expect them to, just like most newer languages do. Again, this makes higher-order programming *much* easier: if the passed method needs access to external information, it's trivial to do. Java, though, doesn't really do closures: instead, it allows only reference copying, and requires you to declare your variables final in order to do it. This is goofy, frankly. It doesn't actually prevent information leakage (since the underlying objects can pass information around), but makes it significantly more work to extract information from your passed methods. And the required "final" declaration frequently results in needing to declare an extra variable that is a duplicate of an existing one, solely to pass information down into the closure.
And then there are events. C# recognizes, quite explicitly, that the Listener Pattern is at the heart of good modern programming -- if you want well-decoupled systems, you need to use the Listener Pattern liberally. Hence, C# writes them in as a first-class part of the language: you simply say, "This class publishes the following event, that takes the following delegate". Publishing an event takes three lines of code: one to declare the signature of the delegate to call, one to declare the event itself, and one to call it at the appropriate time; that does all the work, including multi-call if multiple listeners are registered. (Well, four lines actually: one null-pointer check as well. I regard this as a bug in C#.) This is one of C#'s oldest features -- in the language from the beginning, over five years ago -- and I'm very disappointed that Java hasn't picked up on it. Writing events manually is a pain in the ass, and the result is that it just doesn't happen as much in Java libraries: they simply aren't as well-decoupled and well-hooked as the .NET libraries are, and I'm fairly sure that that's because the language doesn't make it as easy.
It's not all bad, mind: Java has a few nice features that C# doesn't, such as a more sophisticated concept of inner classes, and a fascinatingly deep generics model. (I haven't quite internalized wildcards yet, but I'm working on that.) It has its own nice syntactic sugar, in features such as static imports. And again, the libraries (both the standard ones and the open-source extensions) are generally stronger than .NET's so far.
But overall, I'm afraid that the tradeoff is annoying me more than I'd expected. There is nothing you *can't* do in Java that you can in C#. But the weaknesses in the language -- and especially the fanatical love of object-orientedness, at the cost of decent functional-programming idioms -- make it just plain more *work* to write good programs, and the outcome is just plain more lines of code. That's a real price: while code size is only one measure of programming complexity, it's an important one. Concision isn't the be-all and end-all of good programming, but they correlate pretty closely. There are good reasons why languages tend to evolve into more-concise forms over the years...