Hacker News new | ask | show | jobs
by JanecekPetr 2373 days ago
The I/O example is moot since Java 11 where Java got https://docs.oracle.com/en/java/javase/13/docs/api/java.base..., so we can do `Files.readString(Paths.get(doc.txt))`.

Java 14 is getting an experimental preview of Records (https://openjdk.java.net/jeps/359) which takes care of much (but not all) of ceremony around "data classes".

And Java 13 got a preview of text blocks, https://openjdk.java.net/jeps/355.

Things are coming. That said, I hardly believe those minor syntax improvements are what is so "good" about Kotlin. It has some better defaults (non-null by default, final classes by default), and is a more modern language. Java will stay with us for many years to come, though.

2 comments

I wonder if Java's speeding up of new language features over the past several years, after many years of a stagnant feature set, is a direct result of pressure from the likes of kotlin.
It is mostly C# that is keeping Java on their toes. Java competes with C# than any other language in this world. In the enterprise space, it is either Java or C#. If you check most of their improvements, they were done ion C# first, then they follow.
kotlin copied a lot from C# first.
Now that is a good strategy. Copy directly from a language that is moving at a very fast pace and competing directly with the language you want to replace.
Kotlin will never replace Java, unless you are speaking about Android Java.
Of course it won't ever replace it but it has the potential to overtake it.
I am indeed talking about Android Java.
Who has it now is a good question, but so too is 'who had it first'.

And if you also consider the upgrade treadmill, and where the average Java developer is relative to say ten years ago, it may turn out that at any given point in time, the Kotlin version you'd be able to deploy may have a number of features the most recent Java version you could deploy does not have.

(to say nothing of companies where 'the version' is decided by committee or fiat and thus having a more obscure language sometimes gets you less scrutiny).

Pressure is only from C# and languages like Go.

There is nothing to fear from Kotlin, it is a guest language on the JVM.

It is only relevant on Android, as Google is making ART into the KVM.

Outside Android, Kotlin market share adoption is very low, as any trends chart will easily prove.

It's not so much pressure as data. Java is conservative by design. Even back in 1997, James Gosling wrote that Java only adopts features after they've been tried in other, more adventurous languages, and then Java picks those who've shown the best cost/benefit tradeoff, and only after enough people seem comfortable with them. Java, then, depends on languages like Kotlin or Scala for crucial data about features. The interesting thing, though, is not which features Java adopts, but which ones it doesn't.
Great example of that are checked exceptions which have never been tried before or after in other languages. (at least in the form Java implemented)
Ask "how does Oracle benefit?"
You are right, but you still have to catch the checked IOException.
> You are right, but you still have to catch the checked

> IOException.

Rather than which alternative? There are tonnes of IO errors that can occur and are captured in Java that you may not ever even consider. IO is exceptionally tough and programmers should be aware that there could be 1 of a possible million reasons for failure, even if they don't care exactly why.

It's not a massive ask either:

    try{
      /* Perform some IO that 99.999999% of the time */
    }catch(Exception e){
      /* Something went wrong and we don't know the state of the disk */
    }
Personally I believe applications should have their own wrapper around IO and handle it accordingly. I.e. not caring, re-trying, show-stopper, etc, etc.

A lot of code I've written will have the following if I really don't care if it works or not:

    try{
      /* IO code */
    }catch(Exception e){
      /* Do nothing */  // <-- Let others know that this was on purpose
    }
99% of the time I want to do the same thing for IOException as I would for NoSuchElementException or a DOMException.

That said, even if typed checked exception handling is important...it quickly becomes untenable.

   Thing thing = create();
   try {
     ...
   } finally {
     cleanup(thing);
   }
You might try to abstract this

   withThing(thing -> ...)
But what if "..." potentially throws IOException? What if it potentially throws IOException or AWTException?

You have to give up all your exception type safety to make an abstraction like withThing. It's just not composable.

    interface ThrowingConsumer<T, E extends Exception> {
        void accept(T value) throws E;
    }

    // ...

    <E extends Exception> void withThing(ThrowingConsumer<Thing,E> callback) throws E { ... }
The problems are that (a) you need to add an extra type parameter for exceptions all over the place, (b) exception unification (sum type) doesn't work generically - you can say E1 | E2 in a catch block but it's not a real type, and (c) it composes poorly with existing libraries that expect Consumer<T> throughout.

For one level deep callbacks (for resource handling and the like) it works reasonably well though.

(b) is what I had in mind as the problem.
You can always opt out of the typed checked exceptions.

    try {
        ...
    } except (IOException|NoSuchElementException e) {
        throw new RuntimeException(e);
    }
If you decide you don't want checked exceptions in your code, wrapping the exceptions at the boundaries is not a huge deal.
Or switch to a language that does that for you, like Scala or Kotlin.
For a lot of scripts you want the program to crash with a stack trace if you get an IOException, which is the default behavior if it's uncaught. The user is the developer, and if there's an error the developer fixes it by munging something on their filesystem.

Java was designed for "production" software, which in the late 90s and early 00s meant software that ran for long periods of time as a server, servicing thousands of mission-critical customers. Crashing was not acceptable behavior for that. But now a lot of production software often requires a lot of ancillary one-off tasks that are just done by the developers - testing, data-munging, exploratory code, migrations, demos, etc. That's a very different environment from where you code to spec, the spec never changes, and once the software is done it's supposed to run for years without crashing.

Early Java was designed for set-top boxes and then web browsers. Large-scale servers came later; in fact a lot of libraries still don't support the async futures that were officially added five years ago.
I actually like it. I also create my own exceptions for rare (exceptional) cases, just not to forget to handle them.

But some Java SDK exceptions drive me crazy. For example `URL.parse("http://example.com")` over hard-coded strings that I know 100% won't throw exceptions, but I still to catch 3 of them.

That's why URI has a factory method that you're only supposed to use for known strings (although of course this can't be checked):

https://docs.oracle.com/javase/7/docs/api/java/net/URI.html#...

After URI appeared in 1.4, the only reason to use URL was to create a URLConnection from a URI. Since openConnection() throws IOException, it's not a big deal that toURL() throws a MalformedURLException - just catch it along with all the other IOExceptions.

Since 11, there's no reason to use URL at all, because you can use HttpClient to actually do HTTP.

Ok I'm a veteran Java developer that understands exactly what you're saying but try explaining that to people unfamiliar with Java or junior engineers and watch their brains glaze over.

The JDK is filled with a ton of dumb "once upon a time we thought this was okay..." things that don't properly encode the correct modern idiom

For a long time I've wanted a sort of "local deprecation" tool where we could have a list of things in the JDK that shouldn't be used, and any direct reference to them would cause our build to fail.
https://errorprone.info/ can be configured to look at the AST and warn or reject a lot of specific problems.
Yes, it absolutely is! But then so are all other languages of its vintage. Part of learning a programming language is learning that.

Newer languages definitely have an advantage here, in that they haven't been around long enough for people to have figured out which bits of them are dumb.

It's worse. In 1.4 they added URI because URL is very flawed. You should never use URL. The equals method actually does a DNS resolve and compares the ip addresses. This means that comparing two URL's on the same server will always return true when compared. A lot of URL's you compare will give you true as there are a lot of sites run on the same machine with the same ip address.
Had this happen in production in a third party library we used many moons ago. Sysop came and asked us why we did six figure DNS lookups over a short period of time (24h? Less? Don't remember).

Would probably have gone unoticed at most AWS/GCP/Azure shops today.

Worse was when I had somebody adding the ip address as an Inet4Address on every message passed between machines in a production environment that explicitly didn't have DNS (banks have occasionally very odd ideas about securing subnets). Every single message was doing a reverse DNS lookup and then timing out. And there were a _lot_ of messages.
Wait, seriously? Did whoever designed that have any idea what URLs actually were? Even without DNS, comparing the hosts is no way to compare URLs. I can't think of a single example of two 'equal' URLs whose strings don't match (up until the parameters, at least)...
Yes - bear in mind the URL class has been around since 1995, when the landscape was somewhat different!

At that point, URLs resolving to the same IP were considered equal, even if the host names were different. Even now, there’s no real difference between say “http://example.com” and “http://example.com:80”; it would have been reasonable to consider these equal.

Wow. And of course .equals is a synchronous method, so the whole thread blocks on a network operation whenever you do that.

Helluva way to thread starve your application. :(

https://docs.oracle.com/javase/7/docs/api/java/net/URL.html#...

`URI.create("https://example.com")`
Absolutely wild how people are all in favour of Either/Result, but still look down on checked exceptions.
Most languages with Either/Result have some form of monadic composition and type inference. This lets you basically ignore the error condition until you get to a level where it's appropriate to handle it, and the compiler will check the error types for consistency without you having to specify anything explicitly.

Either/Result has often been found to be untenable without these mechanisms, as well. Rust first added the try! macro and then the ? operator because it was so tedious to deal with raw Results otherwise.

This works for checked exceptions as well, just bubble it up to a place where you want to handle them by stating that your function might throw those exceptions.
I love both, but Java's implementation of checked exceptions cause harder and harder problems, especially when combined with tools such as lambdas, since there's no way to generically compose or handle checked exceptions inside them.
Proper Either/Result would fix some of the exception problems with lambdas in Java, though.

It also allows the exceptional behavior to be defined and to be controlled by the developer, one thing that you don't really have today when throwing RuntimeExceptions with say Stream APIs.

Java's checked exceptions are "proper Either/Result." The problem is that they can be of a more complex types (union types) combined with subtyping. In other words, what you see with Either is not a result of it being written as a return type, but a result of it being a much simpler type than Java's exceptions.
Java's implementation of checked exceptions looks pretty minimal and sound to me.

How would you implement them?

The problem boils down to the fact that you can have a disjunction of any number of checked exception types (including zero). No other party of the type system allows disjunctions, so it causes a lot of problems. The checked exception is conceptually part of the return type, but is split out.

I wish Kotlin had, instead of ignoring the existence of checked exceptions, instead translated them into part of the return type. I use Kotlin a lot these days, and one annoyance for me is dealing with code that throws exceptions. They fixed the annoying "(almost) anything can be null" problem of java and replaced it with an equivalent problem. Why can't nullability and failure results both be part of the static type?

(The workaround it to manually use an Either type yourself, but it doesn't help you with calling anyone else's code, since virtually everything throws exceptions on failure.)

Checked exceptions are just sum types and you unpack it with a try catch block.
You might like Rust :)
It's certainly fair to say I have no alternative recommendation. I enjoyed using them prior to Java 8, and they gave the guarantees I was looking for. They have just had a much harder time integrating with newer features than could be hoped for. I'm not sure how much of that is intrinsic to checked exceptions, and how much is intrinsic to Java's implementation.

One example of this: there is no way to encode the type of a checked exception in a generic. I would like to be able to express something like the following:

  interface ExceptionHandler<E extends Exception, S, T> {
    T wrap(
      Function<S, T, throwing E> fn,
      Function<E, T> exceptionMapper
    );
  }
But there is no way to express that 'throwing E' part. The type of a checked exception is firmly embedded in the interface. So I can't supply, say, an IO method as a Function<S, T> parameter, since the IOException causes a mismatch, and I'd have to write a handler specifically for methods that throw IOException, another for those that throw TimeoutException, a third for those that throw IOException AND TimeoutException, etc; a fairly fruitless goal without automatic code generation.
You can actually use a generic parameter in a throws clause, so you can write something like:

    interface FunctionThrows1<S, T, E1 extends Throwable> {
      T apply(S in) throws E1
    }
    interface FunctionThrows2<S, T, E1 extends Throwable, E2 extends Throwable> {
      T apply(S in) throws E1, E2
    }
    ...
But you still have to write one version of your method for each arity of throwing that you want to be able to wrap.
The big difference between the two is that Either/Result are trivial to compose and otherwise deal with in higher-order functions. Java has a massive problem whereby any API that invokes a callback has to either require that said callback not throw anything outside of a given short list of exceptions; or else declare both itself and the callback as "throws Exception", and force the API caller to deal with that - even though the specific callback that caller is passing in might not be throwing anything, or might be throwing a very specific exception only.
Result / Either put the error condition into the return type, and thus compose better.

Checked exceptions were an excellent hack, but an ergonomic failure, despite the almost-pattern-matching of the `catch` clauses.

After Scala and Kotlin, it's so frustrating that Java almost had pattern matching but only for exceptions and nothing else.
iirc scala literally began as just java with pattern matching from odersky's frustration from building javac in raw java. pattern matching is well on its way in modern java, with switch expressions and instanceof binding in jdk14 preview and more general destructuring (further enhanced by records) and guards up next.

in reality though aside from the rock/hardplace of language conservatism/fanatical back-compat i have to imagine a real reason modern java has yet to fully embrace Optional/Either over null/exceptions is lack of value types - yeah escape analysis lets you ~mostly~ not have to worry about the IdentityObjects you're returning everywhere, until you hit something it can't/won't inline or try to stuff those Optionals on heap and wind up making our already painful pointer chasing situation ten times worse. value types are also well on their way via project valhalla but have yet to actually land.

maybe ironically one of the things i recently got bitten by was the fact that MethodHandle .invoke* methods throw Throwable when used directly in plain java - the very machinery that enables the efficient composition of these kinds of functional programming styles is gated behind having to catch literally anything in the language itself. i guess the recurring theme here is java putting the horse before the cart, and that's frankly my favorite thing about the ecosystem.

I've been reading articles like this for twenty years now, and i've never found one anything other than idiotic.
I loved checked exceptions too, but the ergonomics of Either<Error, Result> works much better if you're writing code in a functional style. I even wrote my own Either which was a fun exercise.
Checked Exceptions are the main reason I switched to C#.NET. I hated checked exception. I know the designers wanted to save us from ourselves but they ended up making the language verbose.