
Java Is Unsound: The Industry Perspective - bhalp1
https://dev.to/rosstate/java-is-unsound-the-industry-perspective
======
norswap
The industry perspective is that nobody cares (and rightly so).

I was at the conference where this was presented. The paper is terrifically
written, and it's good that someone is investigating this.

That being said, it's just not that big of a deal. In practice, this will
never be a problem for anyone writing Java.

The Java wildcard system is _insane_ and no-one is trying to build type
systems based on these ideas, not even Scala and the new batch of JVM
languages (Kotlin, Ceylon). They have their own problems, but this isn't it.

~~~
nerdwaller
Agreed no one in practice will likely ever see this. Ability to compile vs
warned about it are two different things. Those that know Java would quickly
be aware that a) type erasure is a thing, it allows stuff like this b) even if
they wrote some of this their IDE or compiler would generate a ton of
warnings. Generally when you have a compiler - you learn to look at the output
of it.

~~~
zaphar
Generally when your compiler spits out a lot of warnings you learn to ignore
them.

------
xg15
I tried to rewrite the bug into a simpler example to wrap my mind around it:

    
    
        public interface Helper<T extends Number> {
        }
    
        public static <T extends Number> Number nice(Helper<T> helper, T value) { // (1)
            return value; 
        }
    
        public static <SUB_T> Number naughty(Helper<? super SUB_T> helper, SUB_T value) { // (2)
            return nice(helper, value); // (3)
        }
    
        public static Number evil(String value) {
            return naughty(null, value); // (4)
        }
    

From what I understand, the following is happening:

1: Nothing unusual here. Just defining a type parameter with a constraint and
referencing another type which happens to have the same constraint.

2: I think this is the actual bug (?) - SUB_T is permitted as the subtype of a
constrained type, yet itself is not subject to the constraint. This compiles.

3: The compiler derives T from pattern matching (this is the sole reason why
the helper is needed)

T is inferred as the set of hypothetical types that are both subtypes of
Number and supertypes of SUB_T.

4: Because SUB_T is unconstrained, we can happily instantiate SUB_T such that
the set of hypothetical types is empty (e.g. set it to String).

This does mean there is no way we could create a helper object as no type
would satisfy the constraints - but we don't actually need a helper object, so
we can simply pass null.

In short, this bug lets the compiler infer a type hierarchy of Number -> T ->
SUB_T without ensuring that Number -> SUB_T holds. Usually, that wouldn't get
you far as there is no type that could substitute for T - except that you
don't need a type because the compiler will accept null even for "impossible"
types.

It's interesting to note that the class cast exception occurs in (3), not in
(1). From what I know, that is because Java's erasure takes constraints into
account - the arguments of nice() become (Helper, Number) after erasure, not
(Helper, Object).

~~~
lowbloodsugar
yes, (2) is the bug. funny that my IDE catches it, but not the compiler!

~~~
NoGravitas
The article mentions that some compilers catch it, but that by doing so they
don't comply with the language definition.

------
aembleton
Interesting that Ross Tate (the author) is an adivisor to the Ceylon and
Kotlin teams. When this was brought up before on HN [1], I found that Kotlin
handled these type issues. I can't be 100% confident of that though as I did
struggle to fully understand the issue.

1\.
[https://news.ycombinator.com/item?id=13050491](https://news.ycombinator.com/item?id=13050491)

~~~
djsumdog
I've been developing in Scala for years, but lately I've really grown sour at
the TypeSafe/Lightbend libraries and tools (poorly documented, half-
implemented libraries with major breakage on each release with little to no
migration documentation).

People like Paul Philips (yes yes, I know he's ranty and rubs people wrong and
is kinda intense, but he makes good points) show that you can build a sane
language on top of the JVM, but Scala makes a lot of the same concessions and
refuses to fix a lot of implementation bugs in favor of backwards
comparability and enterprise support.

I wrote Kotlin off originally as a late into the game pull by Jetbrains, but
now I really want to take another look at it and see what type of library and
JVM support is has.

~~~
bdavisx
Since Java interop was a #1 priority w/ Kotlin, it has the same library
support that Java does. Almost _all_ of the major libraries just work.

Lately bigger players have started to take notice:

[https://spring.io/blog/2017/01/04/introducing-kotlin-
support...](https://spring.io/blog/2017/01/04/introducing-kotlin-support-in-
spring-framework-5-0)

[https://blog.gradle.org/kotlin-meets-gradle](https://blog.gradle.org/kotlin-
meets-gradle)

I'm still not sure where it will end up, but things look pretty good right
now.

But wrt Type Erasure, Kotlin doesn't completely get rid of it; but it does
offer some ability to retain Types in some generic situations.

------
lacampbell
It's much easier to write robust code in Java - with it's Optional type, and
checked exceptions - than in C#.

 _You probably would never write this, and your colleagues would never write
this. That reasoning is good to apply in many situations. I in fact do this
all the time; it’s a huge part of my research agenda. But you have to be
careful where you apply it. Soundness is a security guarantee, not a usability
concern. What matters is if someone can write this, because then that someone
can get around the security measures that people have placed their confidence
in and do whatever they would like to do. In the case of soundness, it’s the
malicious programmer you should be worried, not just you and your friends._

I really hate this attitude. Programming languages should be designed to be
safe and strict by default, but with easy outs. I utterly reject the notion
that we should design languages first and foremost to protect us from people
can't program that well or aren't careful when they write code.

Do we always have to appeal to the lowest common denominator? Empower the
developers that care rather than trying to turn this into an unskilled
profession.

~~~
dsp1234
_with it 's Optional type,_

C# has Nullable<T> (aka T?)[0]. I'm not a Java expert, but they appear to be
the same (represent a value or a null value, allow you to inspect for the
presence of a value or null).

ex:

    
    
      int? x = null;
      int y = x ?? 0;
      int z = x.HasValue ? x.Value : 0;
      if(x.HasValue && x.Value == 42){}
    

C# definitely doesn't have checked exceptions.

edit:

For reference types, as compared to value types, c# has the null coalescing
operator

    
    
      List<string> y = someList() ?? new List<string>;
    

and the null conditional operator

    
    
      int? length = customers?.Length; // null if customers is null
    

which are defintely not Optional, but are what I use.

[0] - [https://msdn.microsoft.com/en-
us/library/1t3y8s4s.aspx](https://msdn.microsoft.com/en-
us/library/1t3y8s4s.aspx)

~~~
jpfed
I'm unfamiliar with Java's Optional type, but the .NET type Nullable can only
be parameterized over value types. It is there to add the concept of null to
value types, not allow you to more precisely characterize the nullability of
reference types.

~~~
platz
Optional in Java is just a library. If you want to to use Optional in C#,
there is no reason to believe that that is not possible due to some issue that
makes it only available to Java. [https://github.com/louthy/language-
ext/tree/master/LanguageE...](https://github.com/louthy/language-
ext/tree/master/LanguageExt.Core)

~~~
lacampbell
And in Java they're in the the standard library. Which is going to go down
better - simply using something official that every java library program will
already have, or trying to explain to your colleagues why all your methods are
returning something defined in this obscure library you added to all your
builds?

~~~
tigershark
Only since Java 8. The coalesce operator is in c# since the version 3.5 or 4 I
think and c# 6 introduced the null conditional operator that, as I said
before, is objectively nicer than the verbose Java optional. Before c# 6 I
used this simple extension method:

    
    
        public static TDest IfNotNull<TSource, TDest>(this TSource obj, Func<TSource, TDest> func)
        {
            return obj != null ? func(obj) : default(TDest);
        }
    

Or something similar (writing c# code on the iPad before sleeping is not
exactly simple), inspired by some blog post ages ago. Just adding these three
line of codes in a static class in the solution will enable you to write
something like this:

    
    
        var id = order.IfNotNull(o => o.Customer).IfNotNull(c => c.Id) ?? "INVALID"
    

Still not as good as the null check operator but comparable with the Java
optional (arguably better from my point of view)

~~~
lacampbell
How is it better? All you've done is re-implemented mapping over an option.
Your C# code is this in Java:

    
    
        String id = order.map(o -> o.Customer).map(c -> c.Id).orElse("INVALID");
    

The Java code is actually shorter and works straight out of the box, so I
don't know why you're calling Optionals "verbose".

~~~
vile
It's not shorter than out-of-the-box c# 6, which would be var id =
order?.Customer?.Id ?? "INVALID";

I think that was the point he was making, that it's always been possible, and
now is simple.

~~~
lacampbell
Except that nullable types aren't the same thing as option types, so it's not
doing the same thing. Nullables are much weaker than options. You can't do
this for instance:

    
    
        string? id = "Name";
    

While you can do stuff like this:

    
    
       int x = 0;
       int? y = x;
    

Again, F# has nullable types and options, and the fact that no one in F# uses
nullable types should tell you something.

------
aembleton
"So had history been a little different, either with Sun deciding to throw out
backwards compatibility after adding generics, or with Sun adding generics
into the JVM, or with Java first being released with generics, then this
exception would not be thrown"

It returns a ClassCastException because underneath it all, in the Java
bytecode the Java compiler stores generics as casts. Therefore it will
complain because it can't cast. If they had baked generics in at the beginning
of Java then it wouldn't have compiled.

It would be the equivalent of writing String one = 1;

~~~
chrisseaton
> It returns a ClassCastException because underneath it all, in the Java
> bytecode the Java compiler stores generics as casts.

Right - the article already says that doesn't it?

> If they had baked generics in at the beginning of Java then it wouldn't have
> compiled

Why wouldn't it have compiled if the JVM had generics? It's an error in the
design of the type system of the language isn't it? The type system is the
same if we have reified generics or erased generics isn't it? And that's where
the failure is - not at the JVM level.

~~~
_old_dude_
I agree with Chris, it would be a lot worse if generics were reified in Java
because instead of having some spurious ClassCastExceptions, you will get
memory corruptions instead.

------
mark242
I don't understand how lines 4 and 15 don't get caught by the compiler-- a
String cannot extend an Integer?

I'm not even sure how you would be able to write this in Scala without
violating rule number 1 of Scala, which is "never write the word null in your
code".

~~~
partkyle
In the article the author mentions that it's due to the null assignment in the
first line of the `coerce` method.

Look for the "Thanks, NULL!" section near the bottom.

~~~
djsumdog
I wish Java had done more backwards comparability breakage. I wonder if we'd
see the same issues if Java eliminated null in exchange for a Option/Maybe
style monad and if generic types were saved instead of erased at compile time.

~~~
_old_dude_
Java 9 is not fully compatible with Java 8. So we will soon see first hand if
doing more backward compat breakage, for good they told, will be accepted or
not. I predict there will be an internet outrage when Java 9 will be out.

------
amluto
As a C++ programmer but not a Java programmer, why is it reasonable for Java
to consider nonsensical types (where constraints are violated) to be valid but
just not instantiatable? Wouldn't it be better to reject the type entirely?
Then you couldn't have a null object of the type since the type itself is ill-
formed.

~~~
TJSomething
For C++, one kind of nonsensical types is abominable function types, which are
bare functions with cv-qualifiers [1] [2].

[1] [http://www.open-
std.org/jtc1/sc22/wg21/docs/papers/2015/p017...](http://www.open-
std.org/jtc1/sc22/wg21/docs/papers/2015/p0172r0.html)

[2]
[https://news.ycombinator.com/item?id=10935171](https://news.ycombinator.com/item?id=10935171)

~~~
amluto
That's a rather different issue. Abominable types are annoying and mostly
worthless, but there's nothing inherently wrong with code containing a type
variable that refers to the type. Java's screwy generic instantiations are
types that violate constraints by merely being named.

I'm not saying that C++'s type system is great. I'm just saying that Java's
problem mystifies me.

------
jankotek
I would not call this "industry perspective". Java type system is good enough
for "industrial use".

Most problems described in article are not for backward compatibility. But
because java at runtime (JVM) is dynamically typed (Java compiler is
statically typed). You can do much nastier tricks with reflection.

Scala is not much better, I wrote some examples:
[http://www.mapdb.org/blog/scala_has_weakly_typed_syntax/](http://www.mapdb.org/blog/scala_has_weakly_typed_syntax/)

------
scythe
So... you can assign null to a variable whose type is illegal to instantiate
(but not to define), and code which has that variable in scope which will
compile with the assumption that the type _can_ be instantiated.

>Is it safe for Foo<String> to be a valid type?

Wrong question. Is it useful for Foo<String> to be a valid type? The answer is
probably no. So why allow it? The below example does not come close to
explaining why this should be a language feature.

------
pierrebai
The article makes multiple misleading claims.

First, it calls unsound the fact that an exception gets thrown in the shown
code. There is no data security issue, no possible JVM escape, no type system
failure. You can get the same cast exception with straight-forward code. The
claim of unsoundness is merely based on the fact that the problem is
obfuscated. Using unsound here is misleading.

Second, the whole point relies on implicit type relations that are not checked
at compile-time when nullptr is involved. Certainly, it's a weakness in the
specification that can cause suprising run-time exceptions, but it's not a
hole in the type system.

Third, it makes claim that the JVM was saved by its early choice of backward
compatibility. This is a bad habit that I've seen elsewhere: making claim
about unspecified, unknown alternate histories. In this case, had generic not
erased type, then assignment from nullptr would not work. Surely, the nullptr
would have acquired typed alternative (nullptr<T>: nullptr<String>,
nullptr<Integer> ...) which would have letthe compiler type-check properly and
catch the problem at compile-time.

~~~
scott_s
Your first point runs counter to my understanding of what the author said.
Specifically:

"Here’s our “true” unsoundness example that has been making the rounds most
recently. It assigns an Integer instance to a String variable. According to
the Java specification, this program is well-typed, and according to the Java
specification it should execute without exception (unlike the aforementioned
backdoors) and actually assign the Integer instance to the String variable,
violating the intended behavior of well-typed programs. Thus it illustrates
that Java’s type system is unsound."

That is, the author's unsoundness claim is independent of the class cast
exception.

On your second point, you may be in violent agreement with the author. What is
the difference between "a weakness in the specification" and being "unsound"?
The author's definition is that this is an _unintentional_ hole in the type
system, unlike the intentional ones he outlined.

------
exabrial
The real implication here is that Java behaves funny because of backwards
compatibility. We're quite a distance away from < jdk1.5 days... What's the
weather like to update the Java memory model or type system to take care of
these obscure cases? We managed to add a new opcode to the JVM (invokeDynamic)
and the world survived!

~~~
_old_dude_
It's the opposite actually. Here the erasure is a good thing otherwise the bug
described in the blog will have break the VM. The type system is broken
because it supposes you have a type that exists and in the code you send null
instead. If generics were in the VM, this kind of bug would have break the VM
but 'thanks' to the way generics are handled in order to be backward
compatible, a cast is inserted so this bug in the type system do not break the
VM.

------
lowbloodsugar
This seems to be a set of examples that don't work, rather than a set of
examples that prove a point. For example:

    
    
        interface IFoo<T>  { Number foo(T t);  }
    
        class Foo<T extends Number> implements IFoo<T> {
            public Number foo(T t) { return t; }
        }
    
        Number bar(IFoo<String> foos) { return foos.foo("Nan"); }
    

This implies that something bad is going on. But the class in the middle is a
red herring. It is not used in this example, nor can it be used as an argument
to the `bar` method. Try instantiating a Foo<String> and it wont work. Try
instantiating a Foo<Integer> and then passing that to bar(), which requires an
IFoo<String>. Both are _compile_ time errors.

By all means claim, correctly, that support of unchecked code and raw types
means that the type system is not "sound", but the incorrect examples
undermine the claim that this is a problem. (I don't personally think that its
a problem, and the fact that someone claiming otherwise can only provide
examples that fail to prove that its a problem isn't terribly convincing. But
then I grew up with 6502 ASM, which had a fairly limited type system).

~~~
munificent
This example is not to show that Java is unsound. It's to teach you the
difference between types that are invalid and types that would be unsafe but
are allowed because you can never instantiate one.

Like you note:

> Try instantiating a Foo<Integer> and then passing that to bar(), which
> requires an IFoo<String>.

That's what Ross is trying to get you to see. He's saying that Java does _not_
have to make the declaration of IFoo<T> an error because it can rely on the
fact that your later instantiation of it with <String> will be an error
instead.

As long as the bad code gets flagged as an error _somehow_ before it gets
used, you're safe.

The actual unsoundness bug is the "class Unsound" example in Josh Bloch's
tweet. What it shows was that you _can 't_ safely rely on "we'll flag an error
when you create an instance of this type, so we don't need to flag an error in
its declaration." Because you can assign _null_ to be an instance of the type
and then you sidestep the later error.

~~~
lowbloodsugar
I disagree.

class Foo<T extends Number> implements IFoo<T> {}

Does not implement an unconstrained T. It implements "T extends Number". When
the T appears in an implements, its not declaring a new type. You can't do,
for example

    
    
      class Foo implements IFoo<T> {}
      java: cannot find symbol T
    

The mistake is believing that the T's, in the example above, are in any way
related. It would be more appropriate to write:

    
    
      interface Foo<TUnconstrained> {}
      class Foo<TExtendsNumber extends Number> implements IFoo<TExtendsNumber> {...}
    

And then it becomes clear that the foos in bar will never be a Foo, because
Foo _does not implement_ IFoo<String> and never will. Yes you can nuke it
unchecked code. No, I don't have a problem with that. But the example given is
not a useful example.

And this example is in response to the previous one where he asks "Is this
code safe? .. your gut instinct would say this is unsafe". No. My gut instinct
is that it _would not compile_. And indeed:

    
    
      java: type argument java.lang.String is not within bounds of type-variable T

~~~
munificent
> I disagree.

I don't know what you're disagreeing with.

> And then it becomes clear that the foos in bar will never be a Foo, because
> Foo does not implement IFoo<String> and never will.

The statement "Foo does not implement IFoo<String>" is not meaningful. "Foo"
is not a type, it's a type constructor. To refer to a type, you need to say
Foo _of what_. And it is the case that Foo<String> implements IFoo<String>.

Consider:

    
    
        interface IFoo<TUnconstrained> {
          Number foo(TUnconstrained t);
        }
    
        void useFoo(IFoo<String> foo) {
          Number n = foo.foo("not a number");
        }
    

This code compiles fine. Add this:

    
    
        class Foo<TExtendsNumber extends Number> implements IFoo<TExtendsNumber> {
          public Number foo(TExtendsNumber t) { return t; }
        }
    

It still compiles fine. This is exactly what the author is saying. The reason
it is fine for the compiler to permit the above code is because it is relying
on the fact that you will not be able to instantiate Foo<String>.

This is one half of the puzzle of the actual unsoundness example. The other
half is that you can then sidestep _that_ restriction using a wildcard to
define a type that hypothetically meets the required bound even though no such
real type exists.

That would still be safe, since you've never be able to initialize that
variable since no value of that type is possible. Except that Java lets you
initialize it with null.

------
B1FF_PSUVM
> Consequently, all the proofs were correct, but they failed to model Java
> correctly and missed this bug.

David Byrne repeating "Same as it ever was" comes to mind ... (
[https://www.youtube.com/watch?v=I1wg1DNHbNU](https://www.youtube.com/watch?v=I1wg1DNHbNU)
)

------
carsongross
How many ClassCastExceptions or ArrayStoreExceptions are people seeing?

Meanwhile, javascript is winning.

From a practical perspective, static type systems have proven useful for
tooling: to help us with code completion and refactoring. Beyond that, they
appear mainly to hurt developer productivity.

