> there is nearly no magic
I agree with the rest, but there's definitely a lot of magic in Java. This is from both what features the languages makes available (many) and how the community uses them (often). I've had so many hard-to-debug issues in Java over the years due to reflection, annotations, and bytecode manipulation shenanigans.
And another positive point for Java: checked exceptions. It's verbose, but knowing exactly in which ways a function can fail is extremely helpful for building robust applications.
I haven't really been in the java space for a while now, but I recall there being a fair bit of criticism[1][2] of checked exceptions over the years.
[1] https://www.javacodegeeks.com/2026/01/javas-checked-exceptio...
[2] https://reflectoring.io/do-not-use-checked-exceptions/
WRT magic, I've generally thought that was a result of frameworks - Spring, for example. In the past, my feeling was that these impose a sort of meta/configuration language that itself is not checkable at compile time, so you'd get weird runtime errors that are somewhat inexplicable. This was like... 2018 though, so perhaps things have improved.
I agree, to name a few:
- Annotation processing: if you know Lombok, MapStruct.
- Class loader.
- Reflection.
- Garbage collection.
> And another positive point for Java: checked exceptions. It's verbose, but knowing exactly in which ways a function can fail is extremely helpful for building robust applications.
Sorry, but no, Java has the worst of both worlds here. It has checked exceptions AND unchecked exceptions, AND errors which are like unchecked exceptions but won't get caught by a normal catch-all (you're not supposed to catch Throwable, but it's the only way to prevent some dynamically loaded plugin code ten layers deep in the stack from breaking your invariants or stopping your periodic scheduled task due to an errant NoSuchMethodError or NoClassDefFoundError).
And you can't easily use checked exceptions with Java8-style functional code, since interfaces like Function aren't generic on the exception type. Which leads to aberrations like UncheckedIOException, which exists only to make IOException usable in the functional world.
> but knowing exactly in which ways a function can fail is extremely helpful for building robust applications
I've worked on Java apps that have failed in mysterious ways that no exception could explain. Meanwhile, the overhead of having to call out certain exceptions but not others in language syntax is a bit excessive.
For example, decoding a byte array (or URL encoded form field) into a UTF-8 string means handling a theoretical UnsupportedEncodingException. What the fuck? How the hell can one have a JVM that doesn't support UTF-8? Why does my code need boilerplate that will never run because there might be some broken-ass JVM out there that that doesn't support UTF-8? How did it launch a web server, safely load all the libraries, and accept a web request, and route it to my code without blowing up? "But the encoding scheme might change..." No, it won't change. It's always going to be UTF-8. It will always be UTF-8. If it's not, let it blow up.
A lot of that is coding style. I’ve also seen a lot of hard-to-debug issues in Python caused by reflection, weird decorators that muck around with name-mangled symbols, and bytecode manipulation. You can even manipulate the traceback object so it’s more difficult to make sense of why the exception comes from.
It took me quite a long time to accept that the recommended unit testing library manipulates bytecode so that the exception message for `assert a == b` prints the values for both.