Replace Object with var: what could go wrong?

Recently, I came across a situation that replacing Object with var in a Java 10 program throws an exception at runtime. I wondered how many different ways to achieve this effect, and I addressed this issue to the community:









It turned out that you can achieve the effect in different ways. Although all of them are slightly complicated, it is interesting to recall the various subtleties of the language using the example of such a task. Let's see what methods were found.







Members



Among the respondents there were many famous and not so people. This is Sergey bsideup Egorov, Pivotal employee, speaker, one of the creators of Testcontainers. This is Victor Polishchuk , famous for reports about the bloody enterprise. Also noted Nikita Artyushov from Google; Dmitry Mikhailov and Maccimo . But I was especially delighted with the arrival of Wouter Coekaerts . He is known for his article last year , where he walked through the Java type system and told how hopelessly it was broken. Jbaruch and I even used something from this article in the fourth release of Java Puzzlers .







Task and solutions



So, the essence of our task is this: there is a Java program in which there is a declaration of a variable of the form Object x = ...



(honest standard java.lang.Object



, no type substitutions). The program compiles, runs, and prints something like "Ok." We replace Object



with var



, requiring automatic type inference, after which the program continues to compile, but crashes on launch with an exception.







Solutions can be roughly divided into two groups. In the first, after replacing with var, the variable becomes primitive (that is, it was originally autoboxing). The second type remains object, but more specific than Object



. Here you can highlight an interesting subgroup that uses generics.







Boxing



How to distinguish an object from a primitive? There are many different ways. The easiest is to check for identity. This solution was proposed by Nikita:







 Object x = 1000; if (x == new Integer(1000)) throw new Error(); System.out.println("Ok");
      
      





When x



is an object, it certainly cannot be equal by reference to the new object new Integer(1000)



. And if it is a primitive, then according to the rules of the language new Integer(1000)



immediately unfolds into a primitive too, and numbers are compared as primitives.







Another way is overloaded methods. You can write your own, but Sergey came up with a more elegant option: use the standard library. The List.remove



method is List.remove



, which is overloaded and can remove either an element by index if a primitive is passed, or an element by value if an object is passed. This has repeatedly led to bugs in real programs if you use List<Integer>



. For our task, the solution may look like this:







 Object x = 1000; List<?> list = new ArrayList<>(); list.remove(x); System.out.println("Ok");
      
      





Now we are trying to remove the nonexistent element 1000 from the empty list, this is just a useless action. But if we replace Object



with var



, we will call another method that removes the element with index 1000. And this already leads to IndexOutOfBoundsException



.







The third method is a type conversion operator. We can successfully convert another primitive to a primitive type, but an object is converted only if there is a wrapper over the same type to which we will convert (then anboxing will happen). Actually, we need the opposite effect: an exception is in the case of a primitive, and not in the case of an object, but using try-catch this is easy to achieve, which Viktor used:







 Object x = 40; try { throw new Error("Oops :" + (char)x); } catch (ClassCastException e) { System.out.println("Ok"); }
      
      





Here ClassCastException



is the expected behavior, then the program exits normally. But after using var



this exception disappears, and we throw something else. I wonder if this is inspired by the real code from the bloody enterprise? ..







Another type conversion option was proposed by Wouter. Can we use the strange operator logic ?:



. True, its code just gives different results, so you have to modify it somehow so that there is an exception. So, it seems to me, quite elegantly:







 Object x = 1.0; System.out.println(String.valueOf(false ? x : 100000000000L).substring(12) + "Ok");
      
      





The difference between this method is that we do not use the value of x



directly, but the type x



affects the type of the expression false ? x : 100000000000L



false ? x : 100000000000L



. If x



is an Object



, then the type of the entire expression is Object



, and then we just have boxing, String.valueOf()



will String.valueOf()



string of 100000000000



, for which substring(12)



is an empty string. If you use var



, then the type x



becomes double



, and therefore the type false ? x : 100000000000L



false ? x : 100000000000L



also double



, that is, 100000000000L



will turn into 1.0E11



, where it is much less than 12 characters, so calling substring



leads to a StringIndexOutOfBoundsException



.







Finally, we take advantage of the fact that a variable can actually be changed after creation. And in the object variable, unlike the primitive, you can put null



. Putting null



in a variable is easy; there are many ways. But here, Wouter also took a creative approach using the ridiculous Integer.getInteger



method:







 Object x = 1; x = Integer.getInteger("moo"); System.out.println("Ok");
      
      





Not everyone knows that this method reads a system property called moo



and, if there is one, tries to convert it to a number, otherwise it returns null



. If there is no property, we calmly assign null



to the object, but fall from NullPointerException



when trying to assign it to a primitive (automatic anboxing occurs there). It could have been easier, of course. Rough version x = null;



it doesn’t crawl - it does not compile, but the compiler will swallow it now:







 Object x = 1; x = (Integer)null; System.out.println("Ok");
      
      





Object type



Suppose you can no longer play with primitives. What else can you think of?







Well, firstly, the simplest method overload method proposed by Dmitry:







 public static void main(String[] args) { Object x = "Ok"; sayWhat(x); } static void sayWhat(Object x) { System.out.println(x); } static void sayWhat(String x) { throw new Error(); }
      
      





Linking of overloaded methods in Java occurs statically, at the compilation stage. The sayWhat sayWhat(Object)



method is sayWhat(Object)



, but if we infer the type x



automatically, then the String



sayWhat(String)



, and therefore the more specific sayWhat(String)



method will be linked.







Another way to make an ambiguous call in Java is by using variable arguments (varargs). Wouter recalled this again:







 Object x = new Object[] {}; Arrays.asList(x).get(0); System.out.println("Ok");
      
      





When the variable type is Object



, the compiler thinks it is a variable argument and wraps the array in another array of one element, so get()



fulfills successfully. If you use var



, the type Object[]



displayed, and there will be no additional wrapping. This way we get an empty list, and the get()



call will fail.







Maccimo went for hardcore: he decided to call println



through the MethodHandle API:







 Object x = "Ok"; MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual( PrintStream.class, "println", MethodType.methodType(void.class, Object.class)); mh.invokeExact(System.out, x);
      
      





The invokeExact



method and several other methods from the java.lang.invoke



have the so-called "polymorphic signature". Although it is declared as a normal vararg invokeExact method invokeExact(Object... args)



, it does not occur in standard array packing. Instead, a signature is generated in the bytecode that matches the types of arguments actually passed. The invokeExact



method invokeExact



designed for super-fast call of method handles, so it does not do any standard argument transformations like casting or boxing. The handle method type is expected to exactly match the call signature. This is checked at runtime, and as in the case of var



match is broken, we get a WrongMethodTypeException



.







Generics



Of course, parameterization of types can add a twinkle to any task in Java. Dmitry brought a solution similar to the code that I initially came across. Dmitry's decision is verbose, so I will show my:







 public static void main(String[] args) { Object x = foo(new StringBuilder()); System.out.println(x); } static <T> T foo(T x) { return (T)"Ok"; }
      
      





Type T



is output as StringBuilder



, but in this code the compiler is not required to insert a type check at the dial-peer into the bytecode. It is enough for him that StringBuilder



can be assigned to Object



, which means that everything is fine. No one is against the fact that the method with the return value StringBuilder



actually returned the string if you assigned the result to a variable of type Object



anyway. The compiler honestly warns that you have an unchecked cast, which means that he washes his hands. However, when replacing x



with var



type x



is also output as StringBuilder



, and it is no longer possible without type checking, because assigning something else to the StringBuilder



type variable is worthless. As a result, after changing to var



program safely crashes with a ClassCastException



.







Wouter suggested a variant of this solution using standard methods:







 Object o = ((List<String>)(List)List.of(1)).get(0); System.out.println("Ok");
      
      





Finally, another option from Wouter:







 Object x = ""; TreeSet<?> set = Stream.of(x) .collect(toCollection(() -> new TreeSet<>((a, b) -> 0))); if (set.contains(1)) { System.out.println("Ok"); }
      
      





Here, depending on the use of var



or Object



the stream type is displayed as either Stream<Object>



or Stream<String>



. Accordingly, the TreeSet



type and the comparator-lambda type are displayed. In the case of var



, strings must come to the lambda, so when generating a lambda runtime representation, a type conversion is automatically inserted, which gives a ClassCastException



when trying to cast a unit to a string.







In general, the result was very boring. If you can come up with fundamentally different methods to break var



, then write in the comments.








All Articles