Tidbits #1 from JCrete 2022 - Puzzlers


Every year, Heinz Kabutz and his merry band of disorganizers run the fantastic JCrete conference. Well, except for the last two years, when COVID-19 put a stop to it. This year, JCrete was back with a limited number of attendees to avoid a superspreader event. This is the first of a three-part series of tidbits that I learned.

Don't Be Evil

Evil genius José Paumard presented twenty puzzlers. Here is a simplified version of one of them:

record Evil<Integer>() {
   private Integer get() { return Integer.valueOf(42); }
   <String> String invoke(Supplier<String> s) { return s.get(); }
   Integer invokeGet() { return invoke(this::get); }

Which of the methods doesn't compile?

  1. get
  2. invoke
  3. invokeGet
  4. All of them fail to compile

Despite abundant morning caffeine, I was sure that the invoke method couldn't possibly compile. You can't put a type name in brackets.

Except of course, String isn't a type name here. It's just an identifier. Generic types don't have to be T or U. Anything goes. Or almost anything. You can't use keywords such as int or restricted keywords such as record. After renaming, the second method is

<T> T invoke(Supplier<T> s) { return s.get(); }

That is perfectly fine. Now rename Integer into U, and you'll see the answer right away.

Don't Count On That

Here is another puzzle inspired by José. What are the values of count1 and count2?

public class Count {
   public static long count1 = 0;
   public static void main(String[] args) {
      long count2 = Stream.of(8, 9, 11, 17).map(i -> { count1++; return i * i; }).count();
      System.out.println(count1 + " " + count2);
  1. 0 and 4
  2. 4 and 4
  3. The code doesn't compile because you can't have a side effect in a lambda
  4. It depends on the version of Java

It is my job to dutifully study all changes in the Java API, but it had never occurred to me that the behavior of count might have changed over time. Yet it has. Read through the Java 8 and Java 9 javadoc.

As you can see from the enhancement ticket, all intermediate stream operations are skipped. It is as if the map call never happened, and count1 stays 0. Even peek, whose only point is to execute a function with a side effect, is skipped. Try adding .peek(System.out::print) and you'll see no output. (Of course, the map and peek calls happened, but the lambdas that they provided were never used.)

At first, this sounds pretty bizarre. But consider

var first = Stream.of(8, 9, 11, 17).filter(i -> { count1++; return i % 2 != 0; }).findFirst();

You wouldn't expect count1 to be incremented four times. Streams are lazy, so the pipeline stops as soon as the answer is known. With the count method, the answer is known surprisingly quickly when the stream is “sized” and the intermediate operations don't change the size.

Seeing Double

Surely the following cannot compile:

public static Double Double(Double Double) { return Double + Double; }

But it does. Section 6.5 of the Java Language Specification lays out the rules, which are amazingly complex and tedious. The first time I ever wondered about “name reuse” was with records. It is totally ok for the instance field and its accessor to have the same name. And, yes, the following is also legal:

public static double record(double record) { return (double) record;}

Variable and method names can be restricted keywords, but type names cannot be. You could not define a record record(Double Double) {}, but record Record(double Double) {} is ok.

I've never cared before, because nobody codes like that. But it is handy knowledge for eliminating distractors when you have to solve a puzzle in sixty seconds.

Inflationary Expressions

Here is a challenge, not a multiple-choice puzzler. In JShell, write an expression so that its type is more than 4 longer than the expression.

The type of "" is String, which has six letters. only three times longer than the two characters "". The type of 0. is double, also six letters and three times longer than the characters forming the expression. You need to do better than that.

Go ahead and try a few things, then come back here.

Here is my solution (inspired from one of José's puzzlers):


Try it out: Type it into JShell and enter /types. You see the type displayed as

List<Number&Comparable<? extends Number&Comparable<?>>>

The expression has 13 characters, and the type 55, with an impressive inflation factor of 4.2.

Why isn't it just List<Number>, the common supertype of Integer and Double?

Note that the Number class does not implement Comparable, but both Integer and Double do.

So, Number isn't actually the common supertype. Instead, that type—let's call it T—is something like Number & Comparable<some other type>

Integer implements Comparable<Integer>, and Double implements Comparable<Double>. The common supertype of those types is Comparable<? extends T (where T is the common supertype of Integer and Double). That could go on forever:

List<Number&Comparable<? extends Number&Comparable<? extends Number&Comparable<...>>>>

The recursion is capped by using a <?>.

Did you come up with an even more inflationary expression? If so, let me know.

Why Does the JDK Change When Japan Gets a New Emperor?

Thanks to Marc Hoffmann for this tidbit. If you haven't seen his https://javaalmanac.io site, check it out now. It is invaluable for investigating JDK changes.

The Japanese imperial calendar system has an era for each emperor. For example, 2022 is year 4 in the reign of emperor Naruhito. Each era has an auspicious name. The current era is named Reiwa. Try this out in JShell:

import java.time.chrono.*;
JapaneseDate.from(LocalDate.of(2022, 2, 22))

The initial year is tricky because the dates depend on whether they fell before or after the ascension of the new emperor:

JapaneseDate.from(LocalDate.of(2019, 2, 22)) // Heisei
JapaneseDate.from(LocalDate.of(2019, 12, 22)) // Reiwa

The JapaneseEra class gains an instance for each imperial era. https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/chrono/JapaneseEra.html#REIWA was added in JDK 13. Yet another reason for keeping your JDK up to date.

Comments powered by Talkyard.