This is standard behaviour I would expect in most languages that have implicit type conversions. The article seems to be willfully disregarding that. I can think of no language that would not produce either that string, a fault, or (on the outside) NaN. Java is doing a completely sensible thing here.

That majority of operators are left associative, and that associativity is not dependent on the types involved.

Very high level let's say the grammar is:

Expr :- Expr + Primitive
| Primitive

Primitive :- Integer
| String

And then determining the type is always:

type(Integer) = int
type(String) = string
type(Expr) = type(Primitive) or minimum_type(left, right)

With the simple rule (in this case)

minimum_type(left, right)
if (left == right) return left;
return string

In java the type conversions are done as pass during compile time, doing something like this

for (node in AST)
if (type(node) != required type)
replace node with type_conversion_node(node, required_type)

But none of this is java specific -- all statically typed languages (with implicit conversion) would do this. Dynamically typed languages in general cannot do this (in practice you can do a degree of it with local inference, etc), but that just changes when the type check is done.

[Edit, because the same confusion about static/dynamic/strong/weak typing is being made in multiple places.

Strong typing: The language does not allow you to perform an operation on a type if that operation were invalid when applied to that type. In java (to make it explicit) I could do (int[])someUnrelatedObject - that cannot be statically verified but it will fault at run time, because it is strongly typed. The equivalent in C will happily go off into the weeds and destroy everything.

Weak typing: A language does not guarantee that invalid operations will be prevented. For example C and C++ do have types, and they will disallow invalid uses, but there is no guard to prevent you from casting to an unrelated type. A more extreme example is some older languages that don't even disallow argument mismatches, etc.

Static typing: the types in the code are static, specifically they do not change run-to-run. The types of fields, variables, etc in the source could be inferred, or explicit by the programmer, but they are all known before the code is ever run. So C, C++, Java, etc are statically typed. Mostly. Because of Java's boneheaded array sub typing behaviour Object array[]; array[0] = someObejct; can fail at runtime due to a dynamic type check.

Dynamic typing: some or all expression types are not known until the code is run, everyone knows python, js, etc

(getting tired running out of steam)

So anyway, you can have:

Strong/Dynamic: Python, JavaScript, ...
Strong/Static: Haskell, etc
Weak/Dynamic: Can't think of a good example here because tired :D
Weak/Static: C

And obviously there's a tonne of in-between

C++: all of C, plus statically type safe casts, and dynamically type safe casts
Objective-C: all of C, but the ObjC object system which lets you send arbitrary messages to arbitrary targets. But you don't have to have the correct parameter types...
...

And this all ignores what caused this article in the first place: implicit type conversions. These do not effect the Strong/Weak description of the language if they are well defined, eg. people often complain about string promotion, but neglect to comment on someInt + someFloat promoting the int to a float, even though such a promotion can lose data. The distinction is purely a matter of "does the language allow you to perform an operation on a value of a type that is not compatible with that operation". One way to achieve that is the say it's completely illegal: throw an exception at runtime, fail to compile, etc another alternative is to implicit convert the type to something that is valid. Note that /converting/ a value changes the value being used. Compare that to the not type safe version where you literally ignore the type of the value being used. In the case of int + float it's the difference between converting the int to a float value, and just treating the bits of the integer as if they were the bits in a float.

Many apologies about the awful writing, but it's late :D