benf.org :  other :  cfr :  Java 9 observations / support

What's changed (from a decompilation point of view)?

There are two areas of interest from the point of view of CFR - runtime changes, and compiler changes. You probably don't care about any of this, but I found the effort required to get CFR happy with the java 9 JDK/JRE to be significant enough to write about!

It's also interesting (for me, anyway) to disable resugaring features of CFR to see how certain behaviours have changed...

Runtime Changes

The module system has removed rt.jar and tools.jar

As documented here, rt and tools.jar are no longer shipped.

Instead, a jimage file (which is not a documented format) contains the runtime's classes.

This means that CFR's reflection free technique of loading standard classes from the classpath (eg to get generic information) no longer worked. However, it's possible to use NIO to extract the classfile, and then analyse it as before.

But..... that would stop CFR working in java6 (oh no, I hear no-one cry!). Yes. So I've broken my hard rule, and used reflection to access NIO classes. This means that CFR will still only require java 6, however will run against (and extract jimage classes) from java 9. \o/

Yay! Comatrices has a better way.

Compiler Changes

Non-static anonymous classes

JDK-8034044 describes how anonymous classes are no longer marked as static if generated from inside a static function. This is really annoying, as that was useful!

Mutating synthetic accessors

Inner <-> outer class access is implemented by using public synthetic methods, to fake up C++'s friend behaviour.

In Jdk9, the synthetics were improved to allow simple operations.

public class InnerClassTest11c {
    private int a;

    public class Inner1 {
        public Inner1(int x) {
            a /= x;
        }
    }
}
Decompile with --removeinnerclasssynthetics false
Javac 1.8Javac 1.9
public class InnerClassTest11c {
    private int a;

    static /* synthetic */ int access$000(InnerClassTest11c innerClassTest11c) {
        return innerClassTest11c.a;
    }

    static /* synthetic */ int access$002(InnerClassTest11c innerClassTest11c, int n) {
        innerClassTest11c.a = n;
        return innerClassTest11c.a;
    }

    public class Inner1 {
        final /* synthetic */ InnerClassTest11c this$0;

        public Inner1(InnerClassTest11c innerClassTest11c, int n) {
            this.this$0 = innerClassTest11c;
            InnerClassTest11c.access$002(innerClassTest11c, 
                  InnerClassTest11c.access$000(innerClassTest11c) / n);
        }
    }
}
public class InnerClassTest11c {
    private int a;

    static /* synthetic */ int access$036(InnerClassTest11c x0, int x1) {
        return x0.a /= x1;
    }

    public class Inner1 {
        final /* synthetic */ InnerClassTest11c this$0;

        public Inner1(InnerClassTest11c this$0, int x) {
            this.this$0 = this$0;
            InnerClassTest11c.access$036(this$0, x);
        }
    }
}

Lots of explicit Objects.requireNonNull

JDK-8074306 describes how null checks are emitted now as Objects.requireNonNull.

This is handled the same way spurious getClass calls (which were previously done for null checks) are handled - a pattern of dup, check(requireNonNull), pop is a good identifier.

Javac 1.8Javac 1.9
public class Saturn1 {
    public static void main(String[] arrstring) {
        Supplier supplier = Saturn1::new;
        Supplier supplier2 = supplier::get;
        System.out.println(supplier2.getClass());
    }
}
public class Saturn1 {
    public static void main(String[] args) {
        Supplier c;
        Supplier supplier = c = Saturn1::new;
        Objects.requireNonNull(supplier);
        Supplier d = supplier::get;
        System.out.println(d.getClass());
    }
}

Much cleaner 'try with resources'

Try-with-resources tends to generate a lot of code when it's decompiled without desugaring (--tryresources false).

As of jdk9, it seems if there is more than one resource statement, then the compiler emits a synthetic method covering the finally block.

Original
public class UsingTest6 {
    public void testEnhancedTryEmpty() throws IOException {
        try (final StringWriter writer = new StringWriter();
             final StringWriter writer2 = new StringWriter()) {
            writer.write("This is only a test 2.");
            writer2.write("Also");
        }
    }
}
Decompiled with --tryresources false
Javac 1.8Javac 1.9
public class UsingTest6 {
    public void testEnhancedTryEmpty() throws IOException {
        StringWriter stringWriter = new StringWriter();
        Throwable throwable = null;
        try {
            StringWriter stringWriter2 = new StringWriter();
            Throwable throwable2 = null;
            try {
                stringWriter.write("This is only a test 2.");
                stringWriter2.write("Also");
            }
            catch (Throwable throwable3) {
                throwable2 = throwable3;
                throw throwable3;
            }
            finally {
                if (stringWriter2 != null) {
                    if (throwable2 != null) {
                        try {
                            stringWriter2.close();
                        }
                        catch (Throwable throwable4) {
                            throwable2.addSuppressed(throwable4);
                        }
                    } else {
                        stringWriter2.close();
                    }
                }
            }
        }
        catch (Throwable throwable5) {
            throwable = throwable5;
            throw throwable5;
        }
        finally {
            if (stringWriter != null) {
                if (throwable != null) {
                    try {
                        stringWriter.close();
                    }
                    catch (Throwable throwable6) {
                        throwable.addSuppressed(throwable6);
                    }
                } else {
                    stringWriter.close();
                }
            }
        }
    }
}
public class UsingTest6 {
    public void testEnhancedTryEmpty() throws IOException {
        StringWriter writer = new StringWriter();
        Throwable throwable = null;
        try {
            StringWriter writer2 = new StringWriter();
            Throwable throwable2 = null;
            try {
                writer.write("This is only a test 2.");
                writer2.write("Also");
            }
            catch (Throwable throwable3) {
                throwable2 = throwable3;
                throw throwable3;
            }
            finally {
                UsingTest6.$closeResource(throwable2, writer2);
            }
        }
        catch (Throwable writer2) {
            throwable = writer2;
            throw writer2;
        }
        finally {
            UsingTest6.$closeResource(throwable, writer);
        }
    }

    private static /* synthetic */ void $closeResource(Throwable x0, AutoCloseable x1) {
        if (x0 != null) {
            try {
                x1.close();
            }
            catch (Throwable throwable) {
                x0.addSuppressed(throwable);
            }
        } else {
            x1.close();
        }
    }
}

With --tryresources true (which is the default behaviour), cfr does a reasonable job at rebuilding the resource. Both code samples produce the same output.


Last updated 04/2018