Many common tasks take an absurd amount of code. For example, try producing a date in RFC2822 format, in the local time zone. Isn’t Java supposed to be a good language for Internet programming?
What’s with the special types that aren’t objects, like int? I just want to have integers, and leave the compiler to determine the appropriate implementation. In the worst case, I don’t mind specifying how wide I want my integers in advance, but please, can we have them act like proper objects (i.e. everything else)?
I mean, it’s not like anyone is going to be using Java for systems programming, and need access to raw machine words, especially since there are no unsigned integer types available. (Bytes are signed?!)
In Java 1.5 they try to make it less ugly by having the compiler automatically wrap primitive types in object wrappers when necessary. Which is still ugly.
Why must arrays be unlike every other type? For instance, array objects use variable.size() to find out their upper bounds, but arrays use variable.length. They’re not like the other special types either, because they can’t be converted.
A related gripe is that a little more consistency in method naming would be nice. As it is, Strings have a .length(), while arrays have a .length, Arrays have a .getLength(), and ArrayLists have a .size().
The class library is riddled with these kinds of little inconsistencies that make it difficult to remember how to use everyday objects. To pick another example, “HashMap” is camel-cased, but “Hashtable” isn’t–and of course, fussy Java will complain if you capitalize differently.
Apparently too little thought has been put into doing things right the first time. That makes for a horribly bloated and messy API. So we have arrays, Array objects, ArrayList objects, and Vector objects, all solving the same problem in subtly different and incompatible ways. Ditto Hashtable and HashMap.
No integer overflow detection. For a language designed to be safe that forces me to pick a size of integer in advance, that’s pretty stupid.
No abstract local functions. I happen to find map operations very useful at times.
Some arguments are passed by value, some are passed by reference; it depends on their type.
Similarly, the types that would be passed by value are copied during assignments, the types passed by reference are not. Hence, assuming you remember something has been passed in by reference and want to copy it, you specifically have to clone() it to make a copy–and if it’s in an array, you have to clone each dimension of the array manually.
Comparing pass-by-reference variables checks if they’re references to the same thing, rather than checking if they’re the same value. To check if they’re the same value, you have to remember that they’re ‘special’ and that you have to use .equals() instead of ==. Except it doesn’t work on arrays.
Constructors have their own special unique syntax. The compiler spots immediately if I don’t use the special syntax, therefore it clearly knows that the method is the constructor anyway, therefore it’s just arbitrarily making me jump through hoops for it.
You have to include constructors in subclasses, even if the code is identical to that in the parent class.
Importing packages doesn’t import dependencies. For instance, org.xml.sax.XMLReader is no use without org.xml.sax.helpers.XMLReaderFactory, so why make me import them separately?
There’s no way to break string literals across lines, so you have to concatenate them at run time.
Some of the documentation is really awful. I mean, does anyone understand RuleBasedCollator? Quote: “& Indicates that the next rule follows the position to where the reset text-argument would be sorted”. Is that even English?
There’s also an unfortunate tendency for people to assume that JavaDoc is documentation. Often, it’s more like an adventure game, where you click around in a maze of twisty turny JavaDoc pages for hundreds of classes, hoping that at some point you’ll find some documentation that actually tells you how to perform some task you need to perform, rather than just telling you what lots of random methods do.
Startup times are terrible. They have been improved in Java 5 and improved further in Java 6, but they’re still bad compared with pretty much any other commonly used language, even interpreted ones like Ruby.
Exceptions are a pain. The ability to throw and catch exceptions is a good thing in any language; the problem with Java is the insistence that every exception be explicitly dealt with or thrown up the chain.
In J2EE code, for example, we have the SQLException. It either indicates a syntactic error in the SQL code, or that the connection to the database is broken. There’s really nothing the application can usefully do with SQLException except report an error and die; yet because of the explicit exception rules in Java, code ends up bloated with repeated catching and rethrowing of SQLExceptions.
It’s not clear to me that forced exceptions do anything to make programmers deal with possible problems anyway; try running Azureus and watch how many stack traces it craps to the console. Good programmers don’t need to be force-fed exceptions, and bad programmers just catch and ignore the exceptions anyway.
The other side of the problem: if you’re implementing an interface that doesn’t declare any exceptions, you have to eat any exceptions that occur, no matter how deadly.
I’m not sure what the solution is, but with Java I get the feeling that the cure being tried is of marginal effectiveness, and possibly worse than the disease.
Generics were added too late. Now we’ve got a massive class library full of unnecessarily ugly APIs.
Convenient iteration syntax was added too late as well (in Java 6). There are all kinds of classes which sound like they ought to be iterable, but aren’t. Like Arrays, for example.
String and StringBuffer make straightforward string handling unnecessarily painful. You can split a string, but you can’t modify it. You can find a string within a StringBuffer, but only in case-sensitive fashion. You can’t search a StringBuffer for a regular expression; you have to convert it to a String first–but to modify what you find, you have to convert back to a StringBuffer again, or create a new String (and pay the cleanup costs).
So in typical input parsing, you end up repeatedly converting between String and StringBuffer objects. Even assuming that’s what the compiler would have to do anyway, why should I be forced to do it all explicitly? And if “replace string” is only available for StringBuffer, how come “replace regexp” is available for String? The whole division between the two has long ceased to make any sense.
In Java, null is a primitive value. But, as mentioned above, primitives like ints aren’t objects, and so neither is null. Which, in turn, means you can’t use null for null objects (for example when making objects optional)–you need to clutter the API with more methods, create empty objects, or implement a null object class.
IO is ugly. To do the normal “buffered write to a file”, you have to create a FileWriter object, wrap it in a BufferedWriter object to add buffering, then wrap that in a PrintWriter object to get a stream you can print to. That is:
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(new File(filename))));
And don’t forget to explicitly flush and close the PrintWriter, or you’ll lose data. So much for safety! Input is just as bad. Is it too much to ask that the 90% of cases (stream-based reading and writing of text) be optimized with some kind of 1-step BufferedPrintToFileFlushOnDispose class?
I find the use of “static” to indicate “class scope” unnecessarily confusing. To put it another way, I find it hard to remember that “final” means “static” (unchanging) and “static” means “class”…
Of course, the reason why “static” is used for class methods is that Java doesn’t really have class methods; static methods are static in the sense that they’re really global functions that can’t be overridden and are shared between all objects of that class. Perhaps the word “shared” would have been less confusing?
The insistance on a separate file for each class fragments the source code and makes it harder to keep track of where things are–or alternately, encourages the “one huge class” antipattern. The insistence on making the directory structure match the package naming adds the annoyance of having to navigate in and out of directories multiple layers deep even for a trivial project.
Date and time handling is broken (it uses POSIX epoch values internally), and the methods and classes are confusingly named. For example, Calendar.getTime() returns a Date, because Date objects are actually date and time objects.
Things I really like about Java
- Unicode everywhere, no special effort required.
- It’s pretty much portable between Linux and OS X, without having to worry about contortions like autoconf.
I don’t include automatic memory management in the list, because that ought to be a given for any modern programming language.
I should also point out that for all its faults, Java is still better than C++.