JsInterop for Java primitive object wrapper classes

318 Views Asked by At

GWT makes collection classes available via JsInterop out of the box, but the same is not true for Integer, Long, Char and so on (primitive object wrapper classes).

Is there a reason for that?

In order to make those Java emulated classes available via JsInterop, I needed to copy them into my own source, putting them in the same package as the one in gwt-user, then manually modifying them to use JsType (and fixing all method name clashes, as well as making only one constructor available via JsInterop).

Is there any easier way for achieving that other than doing this?

1

There are 1 best solutions below

0
Colin Alworth On

In order for those "boxed primitives" to behave as correctly as possible while in the Java side of your code, they need to actually be objects, rather than whatever might make sense as a primitive.

Double and Boolean get special treatment (as of GWT 2.6 or so), such that they can pass seamlessly between Java and JS. This makes sense for those types, since they actually are the "same" on both sides in terms of the values that can possibly be assigned (a js boolean is always nullable, so java.lang.Boolean makes sense, and a js number is specified to be a nullable 64-bit IEEE 754 floating point number, so likewise it makes sense to be java.lang.Double), but this comes at a cost: any js number would always pass an instanceof Double check, even if it started its life as a java int.

In contrast, the other Java primitives have no JS counterpart, and so may even behave weirdly as primitives, much less Objects.

  • char, byte - outside of "string" with single character in it, JS doesn't have a notion of a single character or byte. You can technically use these primitives as long as you take on any precision issues, but giving up the ability to use their boxed variants doesn't really make sense, since they don't really fit.
  • int, short, float - these look like they make sense to pass from Java to JS as a "number", but if they come back to Java as a number there is the possibility that they would be too big or too precise - without an explicit cast you are just trusting that they make sense. Adding two floats may also give you an unexpected result, since GWT doesn't emulate 32 bit float point values, just lets JS treat them as 64 bit values. Similarly to char/byte, it doesn't make sense to treat these like Objects, since they really aren't the same at all.
  • long/java.lang.Long is an even more special case - it isn't possible in JS (until bigint arrived, which is still not the same thing) to represent precise integers larger than +/- 2^53, since all numbers in JS are 64 bit floats. To correctly handle java long values requires emulation, and expensive math, so even primitive longs that pass back and forth to JS risk either losing precision, or ending up as an Object in JS (with fields for "high", "medium", and "low" bits in the full value).

Consider some example code where you box some simple primitives, and interact with external JS:

@JsMethod
public void takeNumber(Number foo) {
  if (foo instanceof Integer) {
    //...
  } else if (foo instanceof Double) {
    //...
  }
}

How can that instanceof work, if Integer, Double, etc are all the equivelent of JS number? What if this method never exists in JS at all... could you guarantee that it would never be called from any JS method? What if the argument was a Object instead, so that arbitrary JS values could be passed in, and you could test to see if it was a String, Double, Integer, etc and respond accordingly?

What if both Integer and Double were the same when passed in and out of JS for a value like zero - would plain Java zeros be implemented differently in only-Java parts of your program? Would instanceof behave differently in some parts than others, depending on if it were at all possible that JS values could reach them?

--

In order to be the most consistent with how the "outside JS world" behaves, you almost always want to pass Double, Boolean when dealing with these sorts of values - this lets you test for null (JS has no checker to confirm an API isn't surprising you and passing null when it isn't legal), and do any kinds of bounds checking that might be required to see what you should do with the value. For some APIs you can get away with trusting that it will never be null, and likewise you can usually feel safe trusting that it is an int (JsArray.length, for example), but these are typically the exceptions. To retain the ability for your own Java to know the difference between these types, GWT has to let them actually behave like real Java classes, and have a notion of their own type.

--

Getting distracted here from the main answer, but how does String work? GWT is able to special case String, but it ends up having to also special case CharSequence, Comparable, Serializble, since it is possible that you could pass a String in from JS, assign it to a field of type CharSequence, then do an instanceof check against Comparable. For this reason, each of those types has a special case - if the instance actually implements the interface, the instanceof will pass, or if the instance is a plain JS string, it also will pass. Special casing is required in Object.equals, hashCode, getClass() as well to support String, so that two Object fields that happen to both be Strings know how to check their types. Going back now to the question at hand, what if ((Object) zeroFromJS) instanceof Integer and ((Object) zeroFromJS) instanceof Double were both true? What would ((Object) zeroFromJS).getClass() return?