1

I wrote a method which "typifies" a String, and tries to infer the type of data held within it. (A slightly modified version of this gist). The method returns the inferred Class and the original String (possibly slightly modified -- surrounding whitespace trimmed, etc.) in a Map.Entry<Class, String>. For instance, typify("3f") returns <Float, "3.0">, typify(" c ") returns <Character, "c"> and so on.

My next step was to write a second method which "decodes" these returned Map.Entry objects so they can be directly assigned to an object of the inferred type. For instance:

Float f = decodeTypify(typify("3.14f"))
Boolean b = decodeTypify(typify("false"))

...and so on. This code is below:

  @SuppressWarnings("unchecked")
  public static <T> T decodeTypify (Entry<Class, String> entry) {

    // String
    if (entry.getKey() == String.class)
      return (T) entry.getValue();

    // Boolean
    else if (entry.getKey() == Boolean.class)
      return (T) (Boolean) Boolean.parseBoolean(entry.getValue());

    // Byte
    else if (entry.getKey() == Byte.class)
      return (T) (Byte) Byte.parseByte(entry.getValue());

    // Character
    else if (entry.getKey() == Character.class)
      return (T) (Character) entry.getValue().charAt(0);

    // Short
    else if (entry.getKey() == Short.class)
      return (T) (Short) Short.parseShort(entry.getValue());

    // Integer
    else if (entry.getKey() == Integer.class)
      return (T) (Integer) Integer.parseInt(entry.getValue());

    // Long
    else if (entry.getKey() == Long.class)
      return (T) (Long) Long.parseLong(entry.getValue());

    // Float
    else if (entry.getKey() == Float.class)
      return (T) (Float) Float.parseFloat(entry.getValue());

    // Double
    else if (entry.getKey() == Double.class)
      return (T) (Double) Double.parseDouble(entry.getValue());

    // LocalDateTime
    else if (entry.getKey() == LocalDateTime.class)
      return (T) (LocalDateTime) stringAsDate(entry.getValue());

    else return null;
  }

This seems to work great, especially when combined with Java's new local variable type inference:

var f = decodeTypify(typify("literally anything"))

Now I don't need to care about the returned type at all, because Java takes care of giving f the correct type. But notice that if the entry argument to decodeTypify() has a key which doesn't match any of the options in the big if-else tree, then decodeTypify() returns null. Here's this method running in the jshell with Java 11.0.1:

jshell> var x = decodeTypify(typify(null))
x ==> null

I assigned a null value to a local, type-inferred variable! This isn't supposed to be possible. A side effect of this (it seems) is that I can actually tell x to have any type at all, with no warnings:

jshell> Object x = decodeTypify(typify(null))
x ==> null

jshell> String x = decodeTypify(typify(null))
x ==> null

jshell> Byte x = decodeTypify(typify(null))
x ==> null

Note that this is not the case with non-null returns:

jshell> var x = decodeTypify(typify("3"))
x ==> 3.0

jshell> Boolean x = decodeTypify(typify("3"))
|  Exception java.lang.ClassCastException: class java.lang.Double cannot be cast to class java.lang.Boolean (java.lang.Double and java.lang.Boolean are in module java.base of loader 'bootstrap')
|        at (#21:1)

Did I break something? If not, can someone explain what's going on here?

awwsmm
  • 1,353
  • 1
  • 18
  • 28
  • 1
    It seems to me your `typify` would be more important than the `decodeTypify` you've given. – daniu Dec 12 '18 at 15:34
  • I don't see problems, you cannot infer from null the type but you can have null. String b = null; var c = b; This is valid and c is a String. – Sodala Dec 12 '18 at 15:49

2 Answers2

2

You haven't broken anything. You can't assign null directly, but it's perfectly fine to assign it indirectly via a method call.

The reason for this is that by just assigning null the compiler has no information to know what type you want. The only inference that can be made is for the most generic type available, Object, and if that's the correct inference then just declare it as that explicitly! It's 3 extra characters.

When the compiler has a method call to use, it can use the return type of the method to make the type inference.

public static String foo() {
    return null;
}

public static <T> T bar() {
    return null;
}

public static <T> T baz(Class<T> clazz) {
    return null;
}

public static void main(String[] args) {
   var a = null;  // compile error
   var b = foo(); // fine
   var c = bar(); // fine
   var d = baz(String.class); //fine
}
Michael
  • 41,989
  • 11
  • 82
  • 128
  • What about the bit where I did `Byte x = decodeTypify(typify(null))`? The method returns `null` and because the return signature of the method is `T`, and I'm assigning it to an object of type `Byte`, Java creates a `null Byte` object? Is that what's happening there? – awwsmm Dec 12 '18 at 16:03
  • "Java creates a `null Byte` object" Yeah. Why is that weird? – Michael Dec 12 '18 at 16:05
  • Not weird, just wondering exactly how the compiler determines what type to return from the method. (Or interpreter, I should say, in the case of the jshell.) – awwsmm Dec 12 '18 at 16:06
  • Always by using the signature of the method. – Michael Dec 12 '18 at 16:07
  • But you can't implicitly cast an `Object` to a `Byte`, so the method can't be returning a `null Object`, then casting it to a `null Byte` in the assignment of the variable. If I did `Boolean y = decodeTypify(typify(null))`, the only thing that's changed is the type of the variable to which I'm trying to assign the return value from the method. So it must take that into account when it decides what type of object to return. – awwsmm Dec 12 '18 at 16:10
  • I'm starting to confuse myself thinking about it, but I'm wondering at what point does the `null` get wrapped in a `Byte` or `Boolean` or whatever. The following code is fine: `public T example() { return null; } Byte b = example(); Float f = example();` but this throws an error: `Double d = b` because `Byte` cannot be converted to `Double`. The `null` return value could not have been wrapped in an `Object` before it was assigned to `b` or `f` because `Object` cannot be downcast to `Double` or `Byte`. So the compiler must infer the return type from the variable assignment. Is that right? – awwsmm Dec 12 '18 at 16:47
  • You said `always by using the signature of the method`, but it's not _just_ that, is what I'm saying, I guess. Does the compiler use additional context to determine the return type if the return type is generic? – awwsmm Dec 12 '18 at 16:48
  • 1
    @awwsmm you seem to be failing to comprehend Java generics here, not the 'var' stuff. https://docs.oracle.com/javase/tutorial/java/generics/erasure.html might help you. Tl;dr of that article is that Java will, under-the-hood, make sure that any usage of a generic is covered by the generated byte code. There's no implicit casting of Object to Byte, Double to Float, etc... any necessary casts are very explicit and generated by the compiler, no special source code necessary from you. – Jeutnarg Dec 12 '18 at 19:27
0

You can assign a null value to a local, type-inferred variable. You just can't instantiate such a variable with a null initializer.

I checked that gist you referenced, and it's clear that if you give the method 'null' as in input (the value, not a String), then it will set things to the Object type. You are initializing a null Object, something 'var' can handle. At the very least, the compiler knows you're working with the Object class. Also, a method will have a return type, so var can work with that, too.

As for the type assignment switching side effect stuff... casting null always works, so it's not strange that the generics are handling that just fine: No Exception while type casting with a null in java

Jeutnarg
  • 1,138
  • 1
  • 16
  • 28