tags:

views:

22555

answers:

10

I'm currently using Java in a larger project and was curious which of the technical arguments in JWZ's famous "java sucks" article were still valid ten years later. The article starts like this:

I think Java is the best language going today, which is to say, it's the marginally acceptable one among the set of complete bagbiting loser languages that we have to work with out here in the real world. Java is far, far more pleasant to work with than C or C++ or Perl or Tcl/Tk or even Emacs-Lisp. When I first started using Java, it felt like an old friend: like finally I was back using a real object system, before the blights of C (the PDP-11 assembler that thinks it's a language) and C++ (the PDP-11 assembler that thinks it's an object system) took over the world.

It's less polemic than its title suggests, and continues to list some technical aspects that he doesn't like about the language. It seems to me that some of these are outdated, and the original article has received so much attention over time, that I thought it would be nice to know what they were.

For instance, given the popularity of scripting languages and recent technologies like .Net, it seems pretty much consensual that the performance overhead for interpreting code at runtime can be addressed well with JIT compilers (as in Java or .Net) or doesn't matter that much (as in PHP or Python). Also, not many people seem to hate garbage collection, so are there still any other reasons to dislike the JVM?

+11  A: 

Well, I'll give it a shot.

Most of these points seem to be obsolete (e.g. the absence of inline functions: modern Java environments are actually optimizing so this is solved for all intents and purposes).

Other show a misunderstanding (perhaps because Java was new) to the way things work in such a language. Examples here include the complaints about interfaces, which time has proven to be a quite effective compromise between multiple inheritance and single inheritance, and the complaints about byte[] not being hashable: simply encapsulating the raw array in a type solves this particular problem quite elegantly (even if some boilerplate code is still necessary).

Other criticisms have been recognized by other people as well and have been fixed in subsequent versions of Java, or other languages (prominently C# or Scala). I haven't read the whole article in quite a while but all in all I'd say that it's no longer relevant, except (perhaps) for deciding between Java and C#/.NET and then only if you're already intimately familiar with the properties of both languages/systems.

A few criticisms are IMHO fundamentally right and some of them got unfortunately even carried into other systems but most of these are argumentative (e.g. integer overflow vs. bignum).

Konrad Rudolph
A: 

Lets get straight to some issues: Using a word like 'sucks' in your question automatically makes it non-neutral. The fact that the original article used the word 'sucks' in it's title makes it non-neutral. Who is this guy that you are still paying attention to some rant he wrote ten years ago?

But to try to answer your question, I made a random sampling of his 'technical points'.

1) Some were not even true then.

2) Many were the author's personal opinion about what he didn't like with the language, not an actual technical defect: "The notion of methods belonging to classes is lame";

3) hardly any technical issues are still true. "Can't cast short to Short"; "can't change the type on a method return override"; "can't do assert"; "there are no weak pointers."

DJClayworth
"Who is this guy that you are still paying attention to some rant he wrote ten years ago?" - see my reply below.
duffymo
Check the links in the article.
Robert S.
Which are those 'technical points' that were not true even then?
Cristian Ciupitu
"Who is this guy"... I love that quote :D
Thorbjørn Ravn Andersen
+1  A: 

I think there are a few things that have changed since the original article.

  1. It's not that the performance hit of running interpreted code isn't considered harmful anymore, it's that the Java VM doesn't always run interpreted code anymore. The JIT compiler in the Java VM has gotten very good, and that gives the perception of improved performance in the interpreter, when in fact it's native code. The improvements in the JIT mechanims of the VM has made it more useful as a general-purpose platform.
  2. I don't think the basic facts underlying the arguments against Java and it's VM have changed; I think perceptions have changed more - lots of application developers have finally accepted the tradeoff of performance hits due to garbage collection and the error-prone nature of manual memory management (to name two things of many).
  3. The garbage collector has gotten better, which helped #2 happen.
  4. There hasn't been any other technology platform emerge that is as "big" (e.g., widely-used, has corporate support, performs well, etc, etc) and as portable. C# is as "big" (probably "bigger"), but will never be as portable. Other platforms are as portable, but not as "big".
  5. There has been a significant amount of time for the tradeoffs involved in VM technology to be tested and evaluated. In the decade+ since Java has been around, lots of companies have used it to great success, while others have wasted large budgets and ended up with either a crappy application or not much to show for it. Experience has allowed people to find the Java Sweet Spot (TM).

So I think the sum is that there have been improvements in the technology and the benefit of experience which have allowed perceptions to shift.

Ben Collins
Ben, were you thinking of any particular platform when you said "as portable, but not as big"? Just being curious - portability was a key point for choosing Java this time.
Hanno Fietz
Sure - there are several portable language stacks out there. Common Lisp is the first that comes to mind, but Python, Lua, Erlang, Ruby, and Tcl/Tk are all pretty portable, but none of them have the kind of community and inertia that Java/C# do.
Ben Collins
OK - for some reason I was assuming that you were referring to something more obscure, I don't even really know why. :) Especially Python had already been on my radar, as I've used it before and I liked it.
Hanno Fietz
+40  A: 

Haven't thought about that in years... (and yeah, sorry for it not being short :-)

Ignoring the validity of his overall argument :-)

It's hard to live with none of: lexically scoped local functions; a macro system; and inlined functions.

inlined funtions hava been there since JDK 1.0... you just have no control over what is inlines. And in C++ inline is just a hint, so you have no real control there either.

I really hate the lack of downward-funargs; anonymous classes are a lame substitute. (I can live without long-lived closures, but I find lack of function pointers a huge pain.)

Still no function pointers... though this was never a VM thing, just a language thing. Closures are in the works... but not arriving any time soon by the looks of things.

The fact that static methods aren't really class methods (they're actually global functions: you can't override them in a subclass) is pretty dumb.

Still cannot.

It's far from obvious how one hints that a method should be inlined, or otherwise go real fast. Does final' do it? Does private final' do it? Given that there is no preprocessor to let you do per-function shorthand, and no equivalent of Common Lisp's flet (or even macrolet), one ends up either duplicating code, or allowing the code to be inefficient. Those are both bad choices.

You hint at it by keeping your methods small - same as C++ :-) (in C++ if you mark a method as inline it may or may not actually be inlined). Also HotSpot can inline virtual methods in some cases. So you may get more methods inlined automagically then if you went around requesting them to be inlined.

Two identical byte[] arrays aren't equal and don't hash the same. Maybe this is just a bug, but:

There are cases when you wouldn't want that too... there is the Arrays class that can do that for you now (deepEquals).

can't seem to manage to iterate the characters in a String without implicitly involving half a dozen method calls per character.

and

The other alternative is to convert the String to a byte[] first, and iterate the bytes, at the cost of creating lots of random garbage.

nope... don't know why they don't make String (or CharSequence) Iteratable

Generally, I'm dissatisfied with the overhead added by Unicode support in those cases where I'm sure that there are no non-ASCII characters. There ought to be two subclasses of an abstract String class, one that holds Unicode, and one that holds 8-bit quantities. They should offer identical APIs and be indistinguishable, except for the fact that if a string has only 8-bit characters, it takes up half as much memory!

Hasn't changed.

Interfaces seem a huge, cheesy copout for avoiding multiple inheritance; they really seem like they were grafted on as an afterthought. Maybe there's a good reason for them being the way they are, but I don't see it; it looks like they were just looking for a way to multiply-inherit methods without allowing call-next-method and without allowing instance variables?

They were grafted on as an afterthought (early in the langauge development). They haven't changed since he wrote the article.

There's something kind of screwy going on with type promotion that I don't totally understand yet but that I'm pretty sure I don't like. This gets a compiler error about type conflicts:

    abstract class List {
      abstract List next();
    }

    class foo extends List {
      foo n;
      foo next() { return n; }
    }

I think that's wrong, because every foo is-a List. The compiler seems to be using type-of rather than typep.

Fixed with generics.

This integers aren't objects'' nonsense really pisses me off. Why did they do that? Is the answer as lame as, we wanted the `int' type to be 32 bits instead of 31''? (You only really need one bit of type on the pointer if you don't need small conses, after all.)

At the start (before it was public) Java did have ints as objects... it was too slow. Rather than fix it the Smalltalk way they went the C route... hasn't changed. Though autoboxing/unboxing has "helped".

And in related news, it's a total pain that one can't iterate over the contents of an array without knowing intimate details about its contents: you have to know whether it's byte[], or int[], or Object[]. I mean, it is not rocket science to have a language that can transparently access both boxed and unboxed storage. It's not as if Java isn't doing all the requisite runtime type checks already! It's as if they went out of their way to make this not work...

Generics, autoboxing/unboxing, and the new style for loop probably address this.

After all this time, people still think that integer overflow is better than degrading to bignums, or raising an exception?

That related to ints not being objects... still the same as when he wrote the article.

I miss typedef. If I have integers that represent something, I can't make type assertions about them except that they are ints. Unless I'm willing to swaddle them in blankets by wrapping Integer objects around them.

No typedef (for various reasons). However given that he was wrong about inlining... and given advances in the VM wrt object creation and GC making classes to represent this is actually better (you can validate things in the constructor). So no change since his original article from a typing point, but from a VM point the state is better.

Similarly, I think the available idioms for simulating enum and :keywords are fairly lame. (There's no way for the compiler to issue that life-saving warning, `enumeration value x' not handled in switch'', for example.)

Enum is now part of the language... but sadly nothing in the switch to warn you when you forget a case (ponders modifying javac... shudders when remembers the code that is in javac... wonders again how compiler writers actually manage to get stuff to compile... forgets about javac again :-)

As far as I can see, there's no efficient way to implement assert' or #ifdef DEBUG'. Java gets half a point for this by promising that if you have a static final boolean, then conditionals that use it will get optimized away if appropriate. This means you can do things like

Assert is part of the langauge now. However it is delt with at runtime instead of compile time like C/C++. However HotSpot to the rescue again... it is fast.

By having `new' be the only possible interface to allocation, and by having no back door through which you can escape from the type safety prison, there are a whole class of ancient, well-known optimizations that one just cannot perform. If something isn't done about this, the language is never going to be fast enough for some tasks, no matter how good the JITs get. And ``write once run everywhere'' will continue to be the marketing fantasy that it is today.

Has not changed, except of course that the JVMs keep getting better - go with the "marketing fantasy" :-)

I sure miss multi-dispatch. (The CLOS notion of doing method lookup based on the types of all of the arguments, rather than just on the type of the implicit zero'th argument, this).

No change, but if you search in google you will find "ways" of "doing it" (what you think of those ways is another thing :-)

The finalization system is lame. Worse than merely being lame, they brag about how lame it is! To paraphrase the docs: ``Your object will only be finalized once, even if it's resurrected in finalization! Isn't that grand?!'' Post-mortem finalization was figured out years ago and works well. Too bad Sun doesn't know that.

No changes since he wrote it (except that people don't use finalization normally).

Relatedly, there are no ``weak pointers.'' Without weak pointers and a working finalization system, you can't implement a decent caching mechanism for, e.g., a communication framework that maintains proxies to objects on other machines, and likewise keeps track of other machines' references to your objects.

I believe that References (WeakReference, etc...) deals with this.

You can't close over anything but final variables in an inner class! Their rationale is that it might be ``confusing.'' Of course you can get the effect you want by manually wrapping your variables inside of one-element arrays. The very first time I tried using inner classes, I got bitten by this -- that is, I naively attempted to modify a closed-over variable and the compiler complained at me, so I in fact did the one-element array thing. The only other time I've used inner classes, again, I needed the same functionality; I started writing it the obvious way and let out a huge sigh of frustration when, half way through, I realized what I had done and manually walked back through the code turning my

No change yet, but closures might "fix" that.

Hmmm... quoting seems to have died (too many I guess)... so now I'll have to use "s.

"The access model with respect to the mutability (or read-only-ness) of objects blows. Here's an example:"

No change that I am aware of.

"Something else related to this absurd lack of control over who can modify an object and who cannot is that there is no notion of constant space: constantry is all per-class, not per-object. If I've got a loop that does "

No change that I am aware of.

"The locking model is broken."

Major changes here in the libraries.

"There is no way to signal without throwing: that is, there is no way to signal an exceptional condition, and have some condition handler tell you ``go ahead and proceed anyway.'' By the time the condition handler is run, the excepting scope has already been exited."

Termination is still the only model supported.

"The distinction between slots and methods is stupid. Doing foo.x should be defined to be equivalent to foo.x(), with lexical magic for foo.x = ...'' assignment. Compilers should be trivially able to inline zero-argument accessor methods to be inline object+offset loads. That way programmers wouldn't break every single one of their callers when they happen to change the internal implementation of something from something which happened to be a slot'' to something with slightly more complicated behavior."

No change (but he still gets inlining wrong even from when he wrote the article - I guess he means javac and not HotSpot (or the Symantec or Borland JITs at the time).

"The notion of methods "belonging" to classes is lame. Anybody anytime should be allowed to defined new, non-conflicting methods on any class (without overriding existing methods.) This causes no abstraction-breakage, since code which cares couldn't, by definition, be calling the new, ``externally-defined'' methods."

Still don't have that (objective-c thing).

"It comes with hash tables, but not qsort? Thanks! "

Many changes with respect to collections.

"String has length+24 bytes of overhead over byte[]: " VM/library maker dependent, but likely true in all cases.

"The only reason for this overhead is so that String.substring() can return strings which share the same value array. Doing this at the cost of adding 8 bytes to each and every String object is not a net savings..."

True... but there are benefits to the immutability of String.

"If you have a huge string, pull out a substring() of it, hold on to the substring and allow the longer string to become garbage (in other words, the substring has a longer lifetime) the underlying bytes of the huge string never go away. "

As with the above points... implementation detail (I don't think there is a requirement that the String class MUST behave the way it does when dealing with substring).

"The file manipulation primitives are inadequate; for example, there's no way to ask questions like is the file system case-insensitive?'' or, what is the maximum file name length?'', or ``is it required that file extensions be exactly three characters long?'' Which could be worked around, but for: "

A bit better now than when he wrote it... but still the above issues exist.

"The architecture-interrogation primitives are inadequate; there is no robust way to ask am I running on Windows'' or am I running on Unix.''"

Still no good way.

"There is no way to access link() on Unix, which is the only reliable way to implement file locking. "

NIO now exists... but the locking model is odd (since it has to work in Windows and Unix which have very differnt ways of doing locking)

"There is no way to do ftruncate(), except by copying and renaming the whole file. "

Not sure if that still is there or iof something in NIO deals with it.

"Is "%10s %03d" really too much to ask? Yeah, I know there are packages out on the net trying to reproduce every arcane nuance of printf(), but controlling field width and padding seems pretty darned basic to me. "

Fixed.

"A RandomAccessFile cannot be used as a FileInputStream. More specifically, there is no class or interface which those two classes have in common. So, despite the fact that both implement read() and a slew of other like-functioning methods, there is no way to write a method which works on streams of either type. "

There have been a number of changes, NIO might deal with this (been a while since I used NIO and wasn't for trying to do that).

"markSupported is stupid."

Given no change I assume he would still think it is :-)

"What in the world is the difference between System and Runtime? The division seems completely random and arbitrary to me. "

Probably is random and arbitrary... was cleaned up a bit... but still there.

"What in the world is application-level crap like checkPrintJobAccess() doing in the base language class library? There's all kinds of special-case abstraction-breaking garbage like this. "

I am sure that he would think the library is worse now then.

DONE!

TofuBeer
Hey, that's not actually the *short* type of answer I was looking for, but the content is exactly the things I was curious about. Thanks!
Hanno Fietz
I figured enough people were going for the short version :-)
TofuBeer
Well, even more were going for the discussion, take a look at the comments and the revision history. :-)
Hanno Fietz
RandomAccessFile can be used as an InputStream, just code a wrapper for it :) Nice answer btw.
Esko
Wow, great answer. Must have took a while.
dean nolan
I think this is one of the best posts I've ever read on SO.
A. Rex
+4  A: 

Not really an answer to the question but maybe an interesting sidenote:

JWZ is the guy that Peter Norvig is referring to his "Teach Yourself Programming In Ten Years":

One of the best programmers I ever hired had only a High School degree; he's produced a lot of great software, has his own news group, and made enough in stock options to buy his own nightclub.

duffymo
People unaware of who is jwz are probably just as unaware of who is Peter Norvig :) But there's little you can do about it.
Constantin
How about Steve Yegge? http://steve-yegge.blogspot.com/2008/04/xemacs-is-dead-long-live-xemacs.html:
Michael Myers
"Jamie Zawinski is a hero. A living legend. A major powerhouse programmer who, among his other accomplishments, wrote the original Netscape Navigator and the original XEmacs. A guy who can use the term "downward funargs" and then glare at you just daring you to ask him to explain it, you cretin."
Michael Myers
It's a response to DJClayworth's question "Who is this guy...?"
duffymo
@duffymo: Yeah, I was giving you another possible quote. But as Constantin said, people who haven't heard of jwz or Peter Norvig are also unlikely to have heard of Steve Yegge. Perhaps a link to his Wikipedia page would be better?
Michael Myers
+12  A: 

Ok, let me try to go point-by-point. Apologies for the extremely long answer.

About the Java language itself:

  • It's hard to live with none of: lexically scoped local functions; a macro system; and inlined functions.
    • The first two I can't really respond to; I've never had them available, so I don't know how useful they are (ref. The Blub Paradox). The HotSpot compiler now handles inlined functions much more efficiently and intelligently than the static compiler could.
  • I really hate the lack of downward-funargs; anonymous classes are a lame substitute. (I can live without long-lived closures, but I find lack of function pointers a huge pain.)
    • I tend to agree. The little experience I've had with function pointers has soured me on anonymous classes.
  • The fact that static methods aren't really class methods (they're actually global functions: you can't override them in a subclass) is pretty dumb.
    • I agree here also.
  • It's far from obvious how one hints that a method should be inlined, or otherwise go real fast.
    • As I mentioned, this is no longer necessary due to HotSpot.
  • Two identical byte[] arrays aren't equal and don't hash the same.
    • This is somewhat solved by java.util.Arrays, although arrays still won't work in HashMaps. His possible solution ("What you can do is wrap an Object around an Array and let that implement hashCode and equals by digging around in its contained array, but that adds not-insignificant memory overhead (16 bytes per object, today.)") is less of a problem nowadays since memory is cheap, but still can be annoying in embedded development. I don't know of any better solution.
  • I can't seem to manage to iterate the characters in a String without implicitly involving half a dozen method calls per character.
  • The other alternative is to convert the String to a byte[] first, and iterate the bytes, at the cost of creating lots of random garbage.
    • Still can't access things directly, but with HotSpot, those "half a dozen method calls" should be inlined if they're a problem.
  • Generally, I'm dissatisfied with the overhead added by Unicode support in those cases where I'm sure that there are no non-ASCII characters.
    • Memory is a lot cheaper now. That's the only consolation.
  • Interfaces seem a huge, cheesy copout for avoiding multiple inheritance; they really seem like they were grafted on as an afterthought. Maybe there's a good reason for them being the way they are, but I don't see it; it looks like they were just looking for a way to multiply-inherit methods without allowing call-next-method and without allowing instance variables?
    • I don't know what kind of a response is possible.
  • There's something kind of screwy going on with type promotion that I don't totally understand yet but that I'm pretty sure I don't like. This gets a compiler error about type conflicts:

    abstract class List {
      abstract List next();
    }
    
    
    class foo extends List {
      foo n;
      foo next() { return n; }
    }
    

    I think that's wrong, because every foo is-a List. The compiler seems to be using type-of rather than typep.

    • As of Java 5 (I believe), this is valid code.
  • This "integers aren't objects" nonsense really pisses me off. Why did they do that? Is the answer as lame as, "we wanted the `int' type to be 32 bits instead of 31"? (You only really need one bit of type on the pointer if you don't need small conses, after all.)
    • We now have a half-baked solution to this: Autoboxing!
  • And in related news, it's a total pain that one can't iterate over the contents of an array without knowing intimate details about its contents: you have to know whether it's byte[], or int[], or Object[]. I mean, it is not rocket science to have a language that can transparently access both boxed and unboxed storage. It's not as if Java isn't doing all the requisite runtime type checks already! It's as if they went out of their way to make this not work...
    • This is somewhat less of a problem with autoboxing.
  • After all this time, people still think that integer overflow is better than degrading to bignums, or raising an exception?

    Of course, they have Bignums now (ha!) All you have to do (ha!) is rewrite your code to look like this:

    result = x.add(y.multiply(BigInteger.valueOf(7))).pow(3).abs().setBit(27);

    Note that some parameters must be BigIntegers, and some must be ints, and some must be longs, with largely no rhyme or reason. (This complaint is in the "language" section and not the "library" section because this shit should be part of the language, i.e., at the syntax level.)

    • Still a valid point. The Bignum syntax has been proposed for Java 7, but I believe it was rejected.
  • I miss typedef. If I have integers that represent something, I can't make type assertions about them except that they are ints. Unless I'm willing to swaddle them in blankets by wrapping Integer objects around them.
    • Unfortunate, but I've tried to read C++ code that had a lot of typedefs. I'm not really sorry that typedef isn't in Java.
  • Similarly, I think the available idioms for simulating enum and :keywords are fairly lame. (There's no way for the compiler to issue that life-saving warning, "enumeration value 'x' not handled in switch", for example.)
    • Enums were added in Java 5, but for some reason, they never bothered to add that particular warning to the compiler.
  • As far as I can see, there's no efficient way to implement assert or #ifdef DEBUG. Java gets half a point for this by promising that if you have a static final boolean, then conditionals that use it will get optimized away if appropriate.
    • assert was added in Java 1.4. #ifdef DEBUG is still hard to simulate.
  • By having new be the only possible interface to allocation, and by having no back door through which you can escape from the type safety prison, there are a whole class of ancient, well-known optimizations that one just cannot perform. If something isn't done about this, the language is never going to be fast enough for some tasks, no matter how good the JITs get. And "write once run everywhere" will continue to be the marketing fantasy that it is today.
    • I am far, far away from understanding what he's talking about.
  • I sure miss multi-dispatch. (The CLOS notion of doing method lookup based on the types of all of the arguments, rather than just on the type of the implicit zero'th argument, this).
    • The Blub Paradox strikes again.
  • The finalization system is lame.
    • Definitely. Absolutely. Inarguably.
  • Relatedly, there are no "weak pointers."
  • You can't close over anything but final variables in an inner class! Their rationale is that it might be "confusing." Of course you can get the effect you want by manually wrapping your variables inside of one-element arrays.
    • Yep. This has been a point of pain for some time.
  • The access model with respect to the mutability (or read-only-ness) of objects blows. Here's an example: System.in, out and err (the stdio streams) are all final variables. They didn't used to be, but some clever applet-writer realized that you could change them and start intercepting all output and do all sorts of nasty stuff. So, the whip-smart folks at Sun went and made them final. But hey! Sometimes it's okay to change them! So, they also added System.setIn, setOut, and setErr methods to change them!

    "Change a final variable?!" I hear you cry. Yep. They sneak in through native code and change finals now. You might think it'd give 'em pause to think and realize that other people might also want to have public read-only yet privately writable variables, but no.

    Oh, but it gets even better: it turns out they didn't really have to sneak in through native code anyway, at least as far as the JVM is concerned, since the JVM treats final variables as always writable to the class they're defined in! There's no special case for constructors: they're just always writable. The javac compiler, on the other hand, pretends that they're only assignable once, either in static init code for static finals or once per constructor for instance variables. It also will optimize access to finals, despite the fact that it's actually unsafe to do so.

    • It is still possible to change final fields (with reflection, it's actually pretty easy). I don't know anything about the JVM and javac problems.
  • Something else related to this absurd lack of control over who can modify an object and who cannot is that there is no notion of constant space: constantry is all per-class, not per-object. If I've got a loop that does

    String foo = "x";

    it does what you'd expect, because the loader happens to have special-case magic that interns strings, but if I do:

    String foo[] = { "x", "y" };

    then guess what, it conses up a new array each time through the loop! Um, thanks, but don't most people expect literal constants to be immutable? If I wanted to copy it, I would copy it. The language also should impose the contract that literal constants are immutable.

    Even without the language having immutable objects, a non-losing compiler could eliminate the consing in some limited situations through static analysis, but I'm not holding my breath.

    Using final on variables doesn't do anything useful in this case; as far as I can tell, the only reason that final works on variables at all is to force you to specify it on variables that are closed over in inner classes.

    • Still true, as far as I know.
  • The locking model is broken. (examples omitted)
    • I am definitely not qualified to answer this.
  • There is no way to signal without throwing: that is, there is no way to signal an exceptional condition, and have some condition handler tell you "go ahead and proceed anyway." By the time the condition handler is run, the excepting scope has already been exited.
    • There might be some hackish workarounds, but I don't know for sure.
  • The distinction between slots and methods is stupid. Doing foo.x should be defined to be equivalent to foo.x(), with lexical magic for "foo.x = ..." assignment. Compilers should be trivially able to inline zero-argument accessor methods to be inline object+offset loads. That way programmers wouldn't break every single one of their callers when they happen to change the internal implementation of something from something which happened to be a "slot" to something with slightly more complicated behavior.
    • I'm not quite sure what he's complaining about. If you want to change the accessors without breaking callers, it seems to me that you're inviting trouble anyway. But then again, he's JWZ and I'm not.
  • The notion of methods "belonging" to classes is lame. Anybody anytime should be allowed to defined new, non-conflicting methods on any class (without overriding existing methods.) This causes no abstraction-breakage, since code which cares couldn't, by definition, be calling the new, "externally-defined" methods.
    • AspectJ seems to be the only solution for this. I'm not entirely convinced of the utility of doing this, though.

Library:

  • It comes with hash tables, but not qsort? Thanks!
  • String has length+24 bytes of overhead over byte[]:

    class String implements java.io.Serializable { private char value[]; // 4 bytes + 12 bytes of array header private int offset; // 4 bytes private int count; // 4 bytes }

    The only reason for this overhead is so that String.substring() can return strings which share the same value array. Doing this at the cost of adding 8 bytes to each and every String object is not a net savings...

    • There have been several proposals for changing String, but I believe all of them have been rejected. I agree that there is room for improvement.
  • If you have a huge string, pull out a substring() of it, hold on to the substring and allow the longer string to become garbage (in other words, the substring has a longer lifetime) the underlying bytes of the huge string never go away.
    • This is still a common "gotcha" of Java.
  • The file manipulation primitives are inadequate; for example, there's no way to ask questions like "is the file system case-insensitive?" or, "what is the maximum file name length?", or "is it required that file extensions be exactly three characters long?" Which could be worked around, but for:
  • The architecture-interrogation primitives are inadequate; there is no robust way to ask "am I running on Windows" or "am I running on Unix."
  • There is no way to access link() on Unix, which is the only reliable way to implement file locking.
  • There is no way to do ftruncate(), except by copying and renaming the whole file.
    • Yes, Java does not do most architecture-dependent things very well.
  • Is "%10s %03d" really too much to ask? Yeah, I know there are packages out on the net trying to reproduce every arcane nuance of printf(), but controlling field width and padding seems pretty darned basic to me.
    • Since Java 5, System.out.format() handles this.
  • A RandomAccessFile cannot be used as a FileInputStream. More specifically, there is no class or interface which those two classes have in common. So, despite the fact that both implement read() and a slew of other like-functioning methods, there is no way to write a method which works on streams of either type.

    Identical lossage exists for the pairing of RandomAccessFile and FileOutputStream. WHAT WERE THEY THINKING?

    • This is still true.
  • markSupported is stupid.
    • I'm not arguing.
  • What in the world is the difference between System and Runtime? The division seems completely random and arbitrary to me.
    • That's probably because it is random and arbitrary.
  • What in the world is application-level crap like checkPrintJobAccess() doing in the base language class library? There's all kinds of special-case abstraction-breaking garbage like this.
    • Good question.

(I believe I've just set a personal record for longest SO answer!)

Michael Myers
Thanks for that much effort, I changed my mind and reckon it's good to have some detail on this question. But maybe it would be easier for later readers if this were just the diffs to TofuBeer. Would you mind if it were edited accordingly?
Hanno Fietz
Um, well, I actually wrote all of this without seeing his answer (which hadn't been posted when I started, but was halfway posted when I finished). So why should _I_ pay for his answer being ahead of mine? ;)
Michael Myers
Seriously, this answer took about an hour and a half to write. I _really_ don't want to have to go through it all again.
Michael Myers
@mmyers: Echoing my comment to TofuBeer, this is one of the best posts I've ever read on SO, too.
A. Rex
sorry about the halfway posting - I didn't think it would take that long whaen I started and I had to go to work... and I'd put in all that effort :-)
TofuBeer
+138  A: 

Inlining

It's hard to live with none of: lexically scoped local functions; a macro system; and inlined functions.

It's far from obvious how one hints that a method should be inlined, or otherwise go real fast. Does final do it? Does private final do it? Given that there is no preprocessor to let you do per-function shorthand, and no equivalent of Common Lisp's flet (or even macrolet), one ends up either duplicating code, or allowing the code to be inefficient. Those are both bad choices.

The distinction between slots and methods is stupid. Doing foo.x should be defined to be equivalent to foo.x(), with lexical magic for "foo.x = ..." assignment. Compilers should be trivially able to inline zero-argument accessor methods to be inline object+offset loads. That way programmers wouldn't break every single one of their callers when they happen to change the internal implementation of something from something which happened to be a "slot" to something with slightly more complicated behavior.

Java in 1.0 with some JITs did method inlining (the Symantec one for certain, the Borland one probably). So these points were wrong when he wrote the article. Hotspot and similar VMs do a decent job of inlining code. They can even inline code that regular C++ compilers cannot (virtual method inlining is possible). In C++ you can hint that things should be inlined. The compiler can ignore that hint. In Java you hint at inlining things by making small methods (and probably ones without loops).

new

By having new be the only possible interface to allocation, and by having no back door through which you can escape from the type safety prison, there are a whole class of ancient, well-known optimizations that one just cannot perform. If something isn't done about this, the language is never going to be fast enough for some tasks, no matter how good the JITs get. And "write once run everywhere" will continue to be the marketing fantasy that it is today.

Has not changed, except of course that the JVMs keep getting better - go with the "marketing fantasy" :-)

Function pointers

I really hate the lack of downward-funargs; anonymous classes are a lame substitute. (I can live without long-lived closures, but I find lack of function pointers a huge pain.)

Closures are in the works. A bit of a controversial topic in the Java community.

Properties

The distinction between slots and methods is stupid. Doing foo.x should be defined to be equivalent to foo.x(), with lexical magic for "foo.x = ..." assignment. Compilers should be trivially able to inline zero-argument accessor methods to be inline object+offset loads. That way programmers wouldn't break every single one of their callers when they happen to change the internal implementation of something from something which happened to be a "slot" to something with slightly more complicated behavior.

Part of this is his misunderstanding of how inlining worked at the time he wrote the article. The other part would be addresed by Properties if they make it into Java.

Multi-dispatch

I sure miss multi-dispatch. (The CLOS notion of doing method lookup based on the types of all of the arguments, rather than just on the type of the implicit zero'th argument, this).

No change, but if you search in google you will find "ways" of "doing it" (what you think of those ways is another thing :-)

Adding methods to other classes (I am sure there is a proper word for that)

The notion of methods "belonging" to classes is lame. Anybody anytime should be allowed to defined new, non-conflicting methods on any class (without overriding existing methods.) This causes no abstraction-breakage, since code which cares couldn't, by definition, be calling the new, "externally-defined" methods.

This is just another way of saying that the pseudo-Smalltalk object model loses and that generic functions (suitably constrained by the no-external-overrides rule) win.

This is an Objective-Cism. AspectJ seems to be the only solution for this. I'm not entirely convinced of the utility of doing this, though.

Static methods

The fact that static methods aren't really class methods (they're actually global functions: you can't override them in a subclass) is pretty dumb.

This has not changed.

Arrays and hashCode

Two identical byte[] arrays aren't equal and don't hash the same.

This is somewhat solved by java.util.Arrays.deepHashCode(), although arrays still won't work in HashMaps. There are also cases when you would not want them to hash to the same value (you can please some of the people some of the time).

His possible solution ("What you can do is wrap an Object around an Array and let that implement hashCode and equals by digging around in its contained array, but that adds not-insignificant memory overhead (16 bytes per object, today.)") is less of a problem nowadays since memory is cheap, but still can be annoying in embedded development. I don't know of any better solution.

String iteration

I can't seem to manage to iterate the characters in a String without implicitly involving half a dozen method calls per character.

The other alternative is to convert the String to a byte[] first, and iterate the bytes, at the cost of creating lots of random garbage.

Still can't access things directly, but with HotSpot, those "half a dozen method calls" should be inlined if they're a problem. Also CharSequence could be made Iterable.

The alternative is to convert to a char[] (not a byte[]).

Unicode and Strings in general

Generally, I'm dissatisfied with the overhead added by Unicode support in those cases where I'm sure that there are no non-ASCII characters. There ought to be two subclasses of an abstract String class, one that holds Unicode, and one that holds 8-bit quantities. They should offer identical APIs and be indistinguishable, except for the fact that if a string has only 8-bit characters, it takes up half as much memory!

Of course, String being final eliminates even the option of implementing that.

Hasn't changed. Memory prices have (small consolation if you are keen to reduce memory).

The benefit is that things are likely easier to internationalize.

If String were not final there would be a whole lot of defensive copying going on... and I'd be sitting here agreeing with the argument he would have had 10 years ago about why String should have been made final :-)

Interfaces

Interfaces seem a huge, cheesy copout for avoiding multiple inheritance; they really seem like they were grafted on as an afterthought. Maybe there's a good reason for them being the way they are, but I don't see it; it looks like they were just looking for a way to multiply-inherit methods without allowing call-next-method and without allowing instance variables?

They were grafted on as an afterthought (early in the langauge development). They haven't changed since he wrote the article.

Covariant Return Types

There's something kind of screwy going on with type promotion that I don't totally understand yet but that I'm pretty sure I don't like. This gets a compiler error about type conflicts:

   abstract class List {
     abstract List next();
   }

   class foo extends List {
     foo n;
     foo next() { return n; }
   }

I think that's wrong, because every foo is-a List. The compiler seems to be using type-of rather than typep.

JDK 1.5 addressed this.

Array iteration

And in related news, it's a total pain that one can't iterate over the contents of an array without knowing intimate details about its contents: you have to know whether it's byte[], or int[], or Object[]. I mean, it is not rocket science to have a language that can transparently access both boxed and unboxed storage. It's not as if Java isn't doing all the requisite runtime type checks already! It's as if they went out of their way to make this not work...

Is there some philosophical point I'm missing? Is the notion of separating your algorithms from your data structures suddenly no longer a part of the so-called "object oriented" pantheon?

Generics, autoboxing/unboxing, and the new style for loop probably address this.

ints are not Objects

This "integers aren't objects" nonsense really pisses me off. Why did they do that? Is the answer as lame as, "we wanted the 'int' type to be 32 bits instead of 31"? (You only really need one bit of type on the pointer if you don't need small conses, after all.)

The way this bit me is, I've got code that currently takes an array of objects, and operates on them in various opaque ways (all it cares about is equality, they're just cookies.) I was thinking of changing these objects to be shorts instead of objects, for compactness of their containing objects: they'd be indexes into a shared table, instead of pointers to shared objects.

To do this, I would have to rewrite that other code to know that they're shorts instead of objects. Because one can't assign a short to a variable or argument that expects an Object, and consequently, one can't invoke the equal method on a short.

Wrapping them up in Short objects would kind of defeat the purpose: then they'd be bigger than the pointer to the original object rather than smaller.

At the start (before it was public) Java did have ints as objects... it was too slow. Rather than fix it the Smalltalk way they went the C route... hasn't changed since the article was written. We now have a half-baked solution to this: Autoboxing!

ints are not Objects part II

After all this time, people still think that integer overflow is better than degrading to bignums, or raising an exception?

Of course, they have Bignums now (ha!) All you have to do (ha!) is rewrite your code to look like this:

result = x.add(y.multiply(BigInteger.valueOf(7))).pow(3).abs().setBit(27);

Note that some parameters must be BigIntegers, and some must be ints, and some must be longs, with largely no rhyme or reason. (This complaint is in the "language" section and not the "library" section because this shit should be part of the language, i.e., at the syntax level.)

That related to ints not being objects... still the same as when he wrote the article.

typedef

I miss typedef. If I have integers that represent something, I can't make type assertions about them except that they are ints. Unless I'm willing to swaddle them in blankets by wrapping Integer objects around them.

No typedef (for various reasons). However given that he was wrong about inlining... and given advances in the VM wrt object creation and GC making classes to represent this is actually better (you can validate things in the constructor). So no change since his original article from a typing point, but from a VM point the state is better.

enums

Similarly, I think the available idioms for simulating enum and :keywords are fairly lame. (There's no way for the compiler to issue that life-saving warning, "enumeration value 'x' not handled in switch", for example.)

They go to the trouble of building a single two-element enumerated type into the language (Boolean) but won't give us a way to define our own?

Enums were added in Java 5, but for some reason, they never bothered to add that particular warning to the compiler. (ponders modifying javac... shudders when remembers the code that is in javac... wonders again how compiler writers actually manage to get stuff to compile... forgets about javac again :-)

assert

As far as I can see, there's no efficient way to implement assert or #ifdef DEBUG. Java gets half a point for this by promising that if you have a static final boolean, then conditionals that use it will get optimized away if appropriate. This means you can do things like

if (randomGlobalObject.DEBUG) { assert(whatever, "whatever!"); }

but that's so gratuitously verbose that it makes my teeth hurt. (See also, lack of any kind of macro system.)

assert was added in Java 1.4. #ifdef DEBUG is still hard to simulate.

Finalization

The finalization system is lame. Worse than merely being lame, they brag about how lame it is! To paraphrase the docs: "Your object will only be finalized once, even if it's resurrected in finalization! Isn't that grand?!" Post-mortem finalization was figured out years ago and works well. Too bad Sun doesn't know that.

No changes since he wrote it (except that people don't use finalization normally).

References

Relatedly, there are no "weak pointers." Without weak pointers and a working finalization system, you can't implement a decent caching mechanism for, e.g., a communication framework that maintains proxies to objects on other machines, and likewise keeps track of other machines' references to your objects.

References added in 1.2 (WeakReference, etc...) deal with this.

Inner classes and final variables

You can't close over anything but final variables in an inner class! Their rationale is that it might be "confusing." Of course you can get the effect you want by manually
wrapping your variables inside of one-element arrays. The very first time I tried using inner classes, I got bitten by this -- that is, I naively attempted to modify a closed-over variable and the compiler complained at me, so I in fact did the one-element array thing. The only other time I've used inner classes, again, I needed the same functionality; I started writing it the obvious way and let out a huge sigh of frustration when, half way through, I realized what I had done and manually walked back through the code turning my

Object foo = <whatever>;

into

final Object[] foo = { <whatever> };

and all the occurence of foo into foo[0]. Arrrgh!

No change yet, but closures might "fix" that.

Access model and final

The access model with respect to the mutability (or read-only-ness) of objects blows. Here's an example:

System.in, out and err (the stdio streams) are all final variables. They didn't used to be, but some clever applet-writer realized that you could change them and start intercepting all output and do all sorts of nasty stuff. So, the whip-smart folks at Sun went and made them final. But hey! Sometimes it's okay to change them! So, they also added System.setIn, setOut, and setErr methods to change them!

"Change a final variable?!" I hear you cry. Yep. They sneak in through native code and change finals now. You might think it'd give 'em pause to think and realize that other people might also want to have public read-only yet privately writable variables, but no.

Oh, but it gets even better: it turns out they didn't really have to sneak in through native code anyway, at least as far as the JVM is concerned, since the JVM treats final variables as always writable to the class they're defined in! There's no special case for constructors: they're just always writable. The javac compiler, on the other hand, pretends that they're only assignable once, either in static init code for static finals or once per constructor for instance variables. It also will optimize access to finals, despite the fact that it's actually unsafe to do so.

It is possible to change final fields with reflection (it's actually pretty easy).

final variables

Something else related to this absurd lack of control over who can modify an object and who cannot is that there is no notion of constant space: constantry is all per-class, not per-object. If I've got a loop that does

String foo = "x";

it does what you'd expect, because the loader happens to have special-case magic that interns strings, but if I do:

String foo[] = { "x", "y" };

then guess what, it conses up a new array each time through the loop! Um, thanks, but don't most people expect literal constants to be immutable? If I wanted to copy it, I would copy it. The language also should impose the contract that literal constants are immutable.

Even without the language having immutable objects, a non-losing compiler could eliminate the consing in some limited situations through static analysis, but I'm not holding my breath.

Using final on variables doesn't do anything useful in this case; as far as I can tell, the only reason that final works on variables at all is to force you to specify it on variables that are closed over in inner classes.

No changes since he wrote this. Final is very useful (I have about 90%-95% of every variable I make as final... it avoids a lot of silly mistakes).

Locking/Synchronization

The locking model is broken.

First, they impose a full word of overhead on each and every object, just in case someone somewhere sometime wants to grab a lock on that object. What, you say that you know that nobody outside of your code will ever get a pointer to this object, and that you do your locking elsewhere, and you have a zillion of these objects so you'd like them to take up as little memory as possible? Sorry. You're screwed.

Any piece of code can assert a lock on an object and then never un-lock it, causing deadlocks. This is a gaping security hole for denial-of-service attacks.

In any half-way-rational design, the lock associated with an object would be treated just like any other slot, and only methods statically "belonging" to that class could frob it.

But then you get into the bug of Java not doing closures properly. See, you want to write a method:

 public synchronized void with_this_locked (thunk f)
 {
   f.funcall ();
 }

but then actually writing any code becomes a disaster because of the mind-blowing worthlessness of inner classes.

A number of changes have been made in the concurrency area.

Exception handling

There is no way to signal without throwing: that is, there is no way to signal an exceptional condition, and have some condition handler tell you "go ahead and proceed anyway." By the time the condition handler is run, the excepting scope has already been exited.

Termination is still the exception handling model used.

Collections

It comes with hash tables, but not qsort? Thanks!

Since 1.2, there is a modified merge sort. Still no quicksort, though.

String and memory usage

String has length+24 bytes of overhead over byte[]:

class String implements java.io.Serializable {
    private char value[];  // 4 bytes + 12 bytes of array header
    private int offset;    // 4 bytes
    private int count;     // 4 bytes
}

The only reason for this overhead is so that String.substring() can return strings which share the same value array. Doing this at the cost of adding 8 bytes to each and every String object is not a net savings...

If you have a huge string, pull out a substring() of it, hold on to the substring and allow the longer string to become garbage (in other words, the substring has a longer lifetime) the underlying bytes of the huge string never go away.

There have been several proposals for changing String, but I believe all of them have been rejected. I agree that there is room for improvement.

File System

The file manipulation primitives are inadequate; for example, there's no way to ask questions like "is the file system case-insensitive?" or, "what is the maximum file name length?", or "is it required that file extensions be exactly three characters long?" Which could be worked around, but for:

The architecture-interrogation primitives are inadequate; there is no robust way to ask "am I running on Windows" or "am I running on Unix."

There is no way to access link() on Unix, which is the only reliable way to implement file locking.

There is no way to do ftruncate(), except by copying and renaming the whole file.

NIO may help with some of this, but overall there are many issues with the file system still.

printf

Is "%10s %03d" really too much to ask? Yeah, I know there are packages out on the net trying to reproduce every arcane nuance of printf(), but controlling field width and padding seems pretty darned basic to me.

Since Java 5, System.out.format() and System.out.printf() handles this.

RandomAccessFile

A RandomAccessFile cannot be used as a FileInputStream. More specifically, there is no class or interface which those two classes have in common. So, despite the fact that both implement read() and a slew of other like-functioning methods, there is no way to write a method which works on streams of either type.

Identical lossage exists for the pairing of RandomAccessFile and FileOutputStream. WHAT WERE THEY THINKING?

NIO probably deals with this, but the RandomAccessFile is still essentially the same.

markSupported is stupid.

markSupported is stupid.

markSupported is still stupid.

System and Runtime

What in the world is the difference between System and Runtime? The division seems completely random and arbitrary to me.

Probably is random and arbitrary... was cleaned up a bit... but still there.

Library bloat

What in the world is application-level crap like checkPrintJobAccess() doing in the base language class library? There's all kinds of special-case abstraction-breaking garbage like this.

Good question. I am sure that he would think the library is worse now.


TofuBeer's and mmyers' posts were both great, both extremely long and have now been merged into this wiki post (by TofuBeer again), which is even better. Kudos & many thanks!

-- Hanno

TofuBeer
I like Java approach to "evolution" ( slowly ) . One thing I wouldn't like to see in the language is to start including special cases and awkward syntax just to throw in new features. So far Sun has done a good job at this point. New/different features should be addressed by new (JVM) languages.
OscarRyz
+1, good job combining the two. I like the formatting, also.
Michael Myers
"This is an Objective-Cism." No, it's a CLOS-ism; see his previous complaint. And I think it's pretty clear the CLOS dispatcher is much more flexible/powerful than Java's: see Peter Seibel's Google Talk for why you end up greenspunning this if you don't have it.
Ken
Some things suggest that he'd rather be using Common Lisp in some areas. Multiple dispatch, downward funargs, real multiple inheritance, having slots and methods being the same, and the ability to add methods to classes freely are all found there. I'm not sure there is any Objective-Cism.
David Thornley
I assumed he was taking about Objective-C categories: http://en.wikipedia.org/wiki/Objective-C#Categories (I never learned Lisp... looked at a book once...)
TofuBeer
Objective-C categories, by the way, are pretty useful. If it was going to copy anything from Obj-C, though, I'd rather have Foundation's magic data structures instead of TreeMap.
alex strange
RE: Switch on enums. The Eclipse compiler has that warning, which (like all warnings in Eclipse) can be made a compiler error.
Brandon DuRette
RE: Covariant Return Types. Isn't he wrong? Isn't that Liskovs Substitution Principle?
troelskn
He wasn't wrong before JDK 1.5... JDK 1.5 had to add that for generics to work.
TofuBeer
Wow, is there a badge for a super-long answer? Even though it's 2/3 cut-n-paste, it's very well done.
Mark Ransom
About "file system": there is going to be a new file system API in Java 7, but that still doesn't fix many of his (very platform-specific) issues.
Kees Kist
"Adding methods to other classes" = C# calls those methods "extension methods". Not sure if that is the correct name but I thought I'd point that out.
Wesley Wiser
+3  A: 

http://en.wikipedia.org/wiki/Jamie_Zawinski - is an American computer programmer responsible for significant contributions to the free software projects Mozilla and XEmacs, and early versions of the proprietary Netscape Navigator web browser. He still actively maintains the XScreenSaver project.

... and the short answer to the question is, yes, in some ways, Java still sucks. :)

A: 

" it seems pretty much consensual that the performance overhead for interpreting code at runtime is not considered harmful for most applications anymore."

Uh? .NET uses JIT (http://en.wikipedia.org/wiki/Just-in-time_compilation) to compile the .NET byte code into native code specifically targeting the machine it is running on.

Yep, and so does Java. I'm not sure what that sentence was referring to.
Michael Myers
Sorry, I was being quite sloppy. Fixed that, thx.
Hanno Fietz
A: 

How good a programming language is not (or should not) be tested by "how cool is its syntax". The latest ActionScript3 language (used in Flash player) is pretty advanced. So, will it make it an ultimate language when it can`t do more than create a nice GUI site?

A language should be measured by "how fast can I create diverse applications" with it. Another test I would add is "if your workplace could only use one language for the entire company needs, which language will you choose".

I would say that java is a "jack of all trades, master of none"(or almost none) But if you need to choose one language to learn, I would choose it.

Java is great because there are lots of free classes/libraries that make it extremely easy to create a diversity of different applications. It is very easy for enterprise goals ( web-sites/DB applications) . It can do calculation-intensive , grid-computing . It is portable (windows/unix-es/linux) and it can even do rich-GUI(*)

It has few drawbacks too: not-so-good rich-client(*), no level driver coding at all , bad-file-system-scripting language and not the fastest to develop with.

with C/C++ can do anything , but is so slow to program that is not worth doing. with Ruby can develop websites in a "jiffy" , but not a lot more than that. with C# you can do almost anything , but when it comes to deploying in on linux you get a small problem. Perl/Python are nice and fast to code with, but do not have as rich libraries as java , maybe because the sheer number of developers.

I think the real question should be: if you need to develop application to type X , what will you choose ?

C - drivers Ruby - websites Perl - file-system/regexp scripting C# - rich windows UI etc etc

"if your workplace could only use one language for the entire company needs, which language will you choose" -- Bad test we would be stuck with assembly everywhere. How about 95%?
Joshua