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.
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 machineString.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\nSame 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.
The parallelStream().collect(groupingBy(...)) version is shorter and was tempting. Reasons
it is not here:
Collector.of(...) with a supplier,
accumulator, combiner, and finisher — four lambdas — which is longer than the loop and much
harder to read.malformed with its line number) does not fit
a stream cleanly. Line numbers require the index, and Java 8 streams have no enumerate.Worth writing the stream version as a second file later and putting both in Q7. The comparison is the content.
--shard N (SPEC 6) with ExecutorService, and the parallelStream one-liner next to
it, timed. Expectation: parallel is slower on a 200-line file. Confirming that with a
number is the lesson.LogAggStreams.java, diffed against the same golden
file.java.util.ArrayList from src.zip (Q9) — it is sitting in the JDK directory,
no download needed.