curayious Language Lab

Java 8 — lab notes


The build, on this machine

JDK 17 at C:\Users\Ray Aun Fan\jdk-17, compiling to 8:

javac --release 8 -Xlint:-options -d build LogAgg.java
java -cp build LogAgg ../../bench/data/access.log

-Xlint:-options silences warning: [options] source value 8 is obsolete and will be removed in a future release. The warning is correct and not actionable while 8 is the target. Worth noting that it is a real deprecation clock: --release 8 will eventually stop working, and JDK 21 already warns harder. The -Xlint keys are documented in the javac man page — which is the JDK 17 man page, because --release itself did not exist on the JDK 8 javac.

The one compile error

ordered.sort(Comparator
        .comparingLong((Stats s) -> s.count).reversed()
        .thenComparing(s -> s.service));

Does not compile. thenComparing is overloaded for both Comparator<? super T> and Function<? super T, ? extends U>, and after .reversed() the inference has nothing to pin the lambda's parameter type to, so both overloads are applicable and the call is ambiguous.

Fix is an explicit parameter type:

.thenComparing((Stats s) -> s.service)

This is Java's generics design presenting its bill. The C version wrote a qsort comparator with a cast and no ambiguity; the Python version wrote key=lambda kv: (-kv[1].count, kv[0]) and had nothing to infer. Java is the only one of the three where the type system was the obstacle — and it is also the only one where the comparator is checked at compile time.

(The machinery doing this is JLS §15.12 overload resolution plus the new poly-expression inference rules for lambdas — the JLS 8 preface credits Dan Smith with "an entirely new chapter on type inference" written for JSR-335, and this call site is where it runs out of information.)

Locale.ROOT — the bug that passes on your machine

String.format(Locale.ROOT, "%d.%03d", micros / 1000, micros % 1000);

Without Locale.ROOT the default locale is read from the OS. Under de-DE or tr-TR the Formatter renders a decimal comma and may apply digit grouping, so 39.115 becomes 39,115. The diff fails on a colleague's laptop and passes on yours, which is the worst possible failure mode.

Its more famous cousin: "I".toLowerCase() under a Turkish locale is "ı" (dotless), not "i". Any code that lowercases an ASCII identifier for comparison — HTTP headers, config keys, file extensions — is a Locale.ROOT away from breaking for Turkish users. This has shipped in real products more than once. The trap is documented in the platform itself: the String.toLowerCase javadoc uses the Turkish example explicitly.

Rule: if a machine reads what you wrote, Locale.ROOT. If a human reads it, the user's locale. There is no third case and the default is always wrong for the first.

System.out.println emits \r\n

Same class of problem as the C track's _setmode. println uses System.lineSeparator(), which is \r\n on Windows, and there is no way to tell System.out otherwise. The fix is to build the whole report as a string and write the bytes:

System.out.write(out.toString().getBytes(StandardCharsets.US_ASCII));
System.out.flush();

The flush() matters: System.out is a PrintStream with autoflush on println, but write(byte[]) does not trigger it, so without the flush the output can be truncated when System.exit runs.

Adjacent bug, not hit here: the default charset. new String(bytes) reads it from the OS (on Windows a codepage like windows-1252), so files moved between machines can silently corrupt. It took until Java 18 — JEP 400 — to make UTF-8 the default everywhere.

System.exit inside a catch, and the return after it

} catch (NoSuchFileException e) {
    System.err.println(...);
    System.exit(12);
    return;            // <- unreachable at runtime, required by the compiler
}

System.exit never returns, but javac does not know that — it is an ordinary method call as far as flow analysis is concerned. Without the return the compiler complains about variables possibly not being initialised further down (definite assignment is JLS §16, and System.exit is not special-cased anywhere in it). A small, honest example of static analysis being conservative in a way that costs a line.

Why not streams for the aggregation

The parallelStream().collect(groupingBy(...)) version is shorter and was tempting. Reasons it is not here:

  1. Mutable reduction into a custom accumulator needs Collector.of(...) with a supplier, accumulator, combiner, and finisher — four lambdas — which is longer than the loop and much harder to read.
  2. The malformed-line side effect (appending to malformed with its line number) does not fit a stream cleanly. Line numbers require the index, and Java 8 streams have no enumerate.
  3. It would have obscured the point of the track, which is to sit next to the C and Python versions and be compared.

Worth writing the stream version as a second file later and putting both in Q7. The comparison is the content.

Things not done yet