Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Back in the day when I wrote Java for living, it was too common pattern that development is done on OpenJDK until we hit some mysterious problem. The problem then disappeared when we switched to Oracle JDK. Other problems are that Java's "everything is an object" and "generics parameters are lost at run time" ideas poison every other language too, and garbage collection never works as intended.


I used to have similar complaints about Java until I sat down and actually read the JVM and language specs and read some JEPs. The truth is, the Java maintainers have made a lot of good design decisions over the years and they’ve been incredibly careful when it comes to trade offs. Generic erasure for instance, was necessary to prevent major backwards compatibility and break the world problems with the massive existing ecosystem of running java programs. Also it’s not an idea that “poisons” other languages. It’d be pretty uneconomical to preserve generics in the runtime representations of any language—c++ doesn’t do this either and it heterogenous approach (opposed to java’s homogenous approach) has its own set of drawbacks (bloated code size, for instance)

In actuality, Java is pretty great and a lot of the pain I experienced using it was not because there was something inherently worse about Java’s design decisions but rather because I had made the (very incorrect) assumption that one could simply port over a lot of the unstated assumptions we make about the semantics of other languages when coding in Java, which isn’t true.


> Generic erasure for instance, was necessary to prevent major backwards compatibility and break the world problems with the massive existing ecosystem of running java programs.

Isn't that the crux of the general criticism of Java's generics though? That it's ugly and deeply limited precisely because it was designed around a hard requirement of backward-compatibility?


The biggest complaint is erasure. I’ve only ran into erasure issues a handful of times in my decade of programming Java. Most of the time you just change your design and it’s fine.


> Also it’s not an idea that “poisons” other languages. It’d be pretty uneconomical to preserve generics in the runtime representations of any language—c++ doesn’t do this either and it heterogenous approach (opposed to java’s homogenous approach) has its own set of drawbacks (bloated code size, for instance)

You know anything about .NET?


I'm not sure what to answer, except that we live in different realities and in my reality Java is pretty terrible even if a lot of effort has been put into it.


Java != JVM.

I feel that the two are being confused here (even though this post is literally about another language running on the JVM).


Java language design decisions affect JVM design, and JVM design affects other languages that run on it. Additionally, it seems that JVM languages justify their existence by FFI to Java, so the Java worldview leaks to other languages that way.


Hey, I mean fair enough. I can definitely respect that answer. Mind you I’m not trying to argue that Java should win any awards from an ahistorical perspective, however recognizing that the team had to make decisions in the context of maintaining a solid experience for an already existing large userbase made me appreciate their decisions more. I still wouldn’t choose Java as my favorite or goto language, but it’s not so bad as to warrant some of the disparagement it often receives.


The JVM is a marvel of software engineering, and Java is an okay language.

People just like to bond by hating the same unpopular things. It makes them feel 'in'.


Ironically Java is a PhD level language from point of view from Go folks.

I guess in a way they are right, given the various JVM implementations we can chose from, with capabilities the Go runtime will never be capable of.


In Ruby, everything is an object too (actually even more so than in Java) and in dynamically typed languages, generic type parameters of course don't exist, so I don't understand how those things matter to Clojure or (J)Ruby. Garbage collection pauses may be slow on the JVM, but Java is still miles faster than Ruby (idk about clojure).


To be honest, I don't really understand Ruby either. It's like combination of performance of Python, compatibility of C, transparency of any mystery dependency + build system and yet another syntax on top of it.


Ruby is awesome, but it's insanely powerful.

For example: I mean, you can monkeypatch anything (including _Object_) at runtime. You can use this to make 1 + 1 = 3, if you desired, or to add a method to all instances of any class currently instantiated. Want your database to respond to .fuckoff(), no problem..

This kind of stuff is also how rspec (used to?) actually run tests.

It's cool, but also pretty insane.

    irb(main):001:0> class Object
    irb(main):002:1> def fuckoff
    irb(main):003:2>  "lol"
    irb(main):004:2> end
    irb(main):005:1> end
    => :fuckoff
    irb(main):006:0> 5.fuckoff
    => "lol"
    irb(main):007:0> "Hello World".fuckoff
    => "lol"
    irb(main):008:0>


Yes, the nice thing is that you can use metaprogramming to create really powerful and readable DSLs.

The bad thing is that this is often abused horribly, I blame the Rails ecosystem with its "just add this gem to the Gemfile and it will magically change your application" attitude.


Today's java killer is tomorrow's bad practice


"I have altered the API, pray that I don't alter it any further"?


Malcolm Tucker style


This isn't that crazy IMO, both Kotlin and Scala 3 allow this with extension methods

https://kotlinlang.org/docs/extensions.html

https://docs.scala-lang.org/scala3/book/ca-extension-methods...


At least Kotlin (probably Scala too) requires you to explicitly import the corresponding method though.

In Ruby, however, there is no concept of an import. You can require other files but this is transitive. So if something anywhere in your chain of dependencies is defining a new method on String, or Object, it's available everywhere. Plus, in Rails usually you don't even have to require anything, as the framework magic does that for you.

The fact that there is no proper import/module system is easily one of my most disliked features about Ruby, it makes it incredibly hard to understand where things come from.

Also, Ruby doesn't only allow you to add new behaviour to existing classes. It also allows overriding existing methods. And that suffers from the exact same problems, i.e. it could be in some transitive dependency, and suddenly your strings will be doing things you didn't expect them to do...


Next Generation Shell: "hold my beer"

    F fuckoff(x) "lol"
    echo(5.fuckoff())
    echo("Hello World".fuckoff())


Welcome to Smalltalk and Lisp worlds.


Groovy can do this on the jvm.

But you get optional typing too for speed


I'm a static typing covert, but as far as dynamically typed languages go, I still prefer Ruby to Python.


clojure on the jvm has much better performance than ruby, but for my small side projects, that doesn’t matter.

I guess I was trying to describe the frustration of switching languages and ecosystems while also trying to ship, thought I could do it, but I couldn’t.

I suppose I would have had a much better time and stayed in C-land (and gotten much better performance) if I had tried golang instead of clojure.


If you have deadlines and can't afford being "bad" at something for a while then, yes, learning new technologies or ecosystems might not be a wise choice. I think that's independent of what those technologies are.

I switched to the JVM because I was annoyed enough at the things I had to deal with in Ruby that learning how to do things in a new way was worth it for me in the long run.

> I suppose I would have had a much better time and stayed in C-land (and gotten much better performance) if I had tried golang instead of clojure.

But Go is a completely different language than Clojure? By what criterion are you deciding which language to focus on? Mostly I'd think that people who choose Clojure do so because they want to use a homoiconic, dynamically typed, functional programming language and not something that looks a lot like a cleaned-up C.


These days I care less about how the language looks and more about the development/deployment experience, and how much memory it’s going to take on a VPS, since that’s the primary thing driving VPS costs.

Ruby (and rails) for web applications is very hard to beat ergonomically.

Go also feels this way to me, the development experience is very simple, deployment is even easier than ruby/rack (assuming no docker), and you get the added bonus of using a lot less memory on the server.


Saying garbage collection never works as intended somehow elides all the very large scale systems built on garbage collected languages including Java. GC is like most things, it has trade offs and you have to understand them to get the best out of it. It’s not impossible to achieve, though.


I have to admit that I have never built a large scale system, but with small to medium scale systems I have always ended up tuning GC parameters and looking for stray references. And of course there is always some piece of code that depends on finalizers to close some resource, so not doing enough GC a problem just as much as doing too much of it. In my current project (C#) there's the additional problem that there are situations where GC just isn't allowed but there are some language features that just create objects behind the scenes without any warning. So yes, it's not impossible to achieve but it's a lot more difficult than dealing with reference counting or C++ smart pointers.


> I have always ended up tuning GC parameters and looking for stray references.

I've been doing java for years and the key I've learned about the JVM is to stop tuning GC parameters.

The JVM has incredibly good heuristics provided you let them work. The most you should generally do is pick the algorithm and the heap size (maybe the pause time) doing more should only be done if you've got a large amount of evidence (GClogs) to support such changes. Way too many people shoot themselves in the foot trying to hand craft GC settings.

Beyond that, I've found that application changes are almost always more effective than GC changes. Use the JVMs fantastic telemetry tools (Java flight recorder) and find out what's really going on with your application. Again, grab the data first, optimize, and then remeasure to see what's next.

I've managed plenty of apps with 30+gb heaps and 0 tuning.


I wish I could say the same. Most times it ended up one of two ways: Either the app is stuck doing GC, or it crashes with OutOfMemoryError: too many this or that handles, open pipes or something else.


I don't know what kinds of things you're doing on the JVM, but the only situations where I've seen these kinds of errors were either a) batch jobs on massive amounts of data (which yeah... it figures that they could OOM, not sure how you'd prevent that), or b) a serious error in application logic.

In general, I don't really encounter these problems and neither do most other people who write for the JVM.


Same here, usually you get that from code that uses new everywhere without any kind of thought regarding proper use of algorithms and data structures.


> too many this or that handles, open pipes or something else.

I'm sorry, but that sort of message is a pretty strong indicator of programmer error.

Are you using try-with-resources to properly close out things?

Are you creating threads in an unbounded fashion? (new Thread)

Are you keeping references to objects for longer than needs be?

Have you used Java flight recorder to identify the root cause of you memory pressure or used eclipse MAT to find large long lubed allocations? (Or both?).

Are you using finalizes? Those have been STRONGLY discouraged (and for good reason) for decades now.


No. I am the poor programmer whose manager forced me to use a library their nephew's dog or someone wrote that does all of those. Except using finalize for closing pipes for subprocesses, that was by Java standard library itself. And keeping references longer than needed, that is every library that implements clever caching, ever. Regarding try-with-resources, RAII was invented for a reason. Java did not copy it, because they seemed to seriously believe that GC is a good substitute.

One of the many features of Java is that it is advertised as being easy enough that any idiot can write it, and managers then hire idiots.


> No. I am the poor programmer whose manager forced me to use a library their nephew's dog or someone wrote that does all of those

My condolences, that sucks. I'd still suggest using the profiling tools I've suggested to find these issues and create change requests to said libs. People will think you're a wizard if you simply learn how to use Java's telemetry. It's an invaluable skill.

> Except using finalize for closing pipes for subprocesses, that was by Java standard library itself

Java uses finalizers as a last resort, not a first line. Finalizers are due to be removed from the language so don't count on them being around for long.

> And keeping references longer than needed, that is every library that implements clever caching, ever.

Sure, caching is legitimate when the cost of creating exceeds the cost of long lived objects. Have you measured to see if the caches you have are improperly sized? Have you made pull requests/change requests to said libraries to fix it if it is?

> Regarding try-with-resources, RAII was invented for a reason. Java did not copy it, because they seemed to seriously believe that GC is a good substitute.

RAII was, few languages have it. The languages that don't have it generally don't have it because tracking the lifetime of objects in a GCed language can be extremely difficult. Consider, for example, how java would handle a destructor for an object allocated in one thread shared with multiple other threads.

Do you know how C++/Rust/D solve that problem? Through a system of reference counted smart pointers which have to be handled very delicately. Java allows such objects to be freely moved.

There are pros and cons to GCs (like most language features) so instead of just defaulting to "man, GCed languages suck because the finalizers aren't called when I want them to be!" perhaps it's a better route to learn the idiomatic way of handling resources and use that?


> People will think you're a wizard if you simply learn how to use Java's telemetry. It's an invaluable skill.

People who want to hire me to fix their Java app think already think I am a wizard. The problem is, they stop believing me the moment I tell that Java is not a good language completely stop listening when I say that I will not work with it as long as I have other options.

> Java uses finalizers as a last resort, not a first line.

At the time it was the only option. Or, reading the JDK source revealed the undocumented feature that destroy() closes the pipes too.

> Have you measured to see if the caches you have are improperly sized? Have you made pull requests/change requests to said libraries to fix it if it is?

Wasn't the point of using JVM and pre-made libraries that they would work out of the box? Anyway, I have found out that terrible libraries are terrible because their authors either genuinely want them to be that way or because they do not recognize good code when they see it. If you have managed an open source project you should know that pull requests out of nowhere are mostly pure nonsense and tend to be ignored.

> Do you know how C++/Rust/D solve that problem?

I do, thank you for asking. As far as I understand, Java does not allow moving objects any more than copying a shared pointer or moving unique one does, but it pretends that objects have no ownership.

> perhaps it's a better route to learn the idiomatic way of handling resources and use that?

It does not solve the problem. The actual solution would be more like "magically make the CTO's nephew and all authors of the zillion libraries the app depends on learn the idiomatic way of handling resources and then rewrite everything using those".


> People who want to hire me to fix their Java app think already think I am a wizard. The problem is, they stop believing me the moment I tell that Java is not a good language completely stop listening when I say that I will not work with it as long as I have other options.

If people hire you to "fix their Java app" and you then subsequently bury your head in the sand and insist that this cannot be fixed (or you will not fix it) without rewriting it in another language - either you have been lying to them, or they were not listening to you. Regardless, you were probably not a good hire.

It seems that you're trying to write Java as if it were C++. That will not work, so stop doing that.

> "magically make the CTO's nephew and all authors of the zillion libraries the app depends on learn the idiomatic way of handling resources and then rewrite everything using those"

Crap code is crap code and exists in every language. I'm not sure what your point is. Most battle-tested libraries in Java properly use try-with-resources. If you work for a crap startup that uses crappy libraries, that's not a problem of the language itself. All this criticism of "the CTO's nephew" is a deflection of any conversation about benefits of drawbacks of individual languages since you just try to measure a language by its worst users.


> If people hire you to "fix their Java app" and you then subsequently bury your head in the sand and insist that this cannot be fixed (or you will not fix it)

To be honest, most of these cases have been what could be called bait and switch. I think it tells something about the language that they can't find employees if they are honest about using it. Other times it had been because recruiters just decided that I'm a Java person and can't work with other technologies.

> It seems that you're trying to write Java as if it were C++. That will not work, so stop doing that.

If you can read my thoughts, James Randi has million dollars for you. I have only contempt, and no you didn't get it right.


How do you expect RAII to work in any GC language? It would be non-deterministic. The point of try-with-resources is that it is deterministic and you know when something is closed out. Every GC language has implemented this.


I would say that RAII is enforced try-with-resources without the extra syntax and releasing memory when resources are released instead of having a garbage collector is a nice performance optimization. C# with "using var" is on the right track, though. Just make RAII default and explicit close/dispose the special case and add syntax for type-deduced immutable values, and I'd be happy.


You have to work very poorly to get these types of errors.


> And of course there is always some piece of code that depends on finalizers to close some resource [...]

I don't think anyone should use finalizers in Java. It's been deprecated since Java 9 and even before that, it was widely considered a bad practice.


I don't think anyone should use finalizers in Java either, but it hasn't stopped anyone from doing that.


There are footguns in every language. There are certainly more footguns in C, which you're trying to advocate for, if I understand correctly (although I'm really not sure what your point is).

FWIW, I haven't ever seen anyone use finalize() in Java. I don't doubt that it happens occasionally, but you can't blame a language for some of its users being incompetent.


I'm not advocating C for anything except low-level embedded but even there I think we could do better. I'm not sure what would be the optimal language for writing web backends but I'm convinced that we could do better than Java. I could even go as far as claiming that C# is a lot like Java with some of the worst parts fixed.


In the next LTS release it won't matter, as they won't be around any longer.

https://openjdk.java.net/jeps/421


One of Java's weaknesses is that they _didn't_ move to everything is an object. Instead we still have all the primitive types (except unsigned, but that's another topic!) and they autobox to and from their object wrappers. I'm assuming moving to objects was considered and discarded for performance reasons. Now I guess we're getting inline classes soon? Which will solve the performance problem, but only decades later.


I think by object he means "reference type" - the only good thing about Java's primitives is that at least it has some value types somewhere!

Almost every decision in Java feels like it was chosen to waste memory. Even using less than int size ints is hard, because it makes you write explicit casts everywhere, which doesn't prevent bugs, but then makes overflow wrap silently, which encourages them.


Yes, "everything is a reference type" describes better what I was thinking. And also, "functions are classes, not objects".




Consider applying for YC's Fall 2026 batch! Applications are open till July 27.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: