preface
Java decompression may sound unintuitive, but decompression is not a particularly advanced operation. Java has strict requirements for the generation of Class bytecode files. If you are familiar with the Java Virtual Machine specification and understand the role of some bytes in Class bytecode files, Understanding how decompilation works is not a problem. Even Class files like the one below can be read.
Decompilation is often used in reverse research and code analysis. However, in daily development, it is sometimes important to simply look at the decompilation of the dependent classes used.
As it happens, Java decompilation is also needed in recent work, so this article introduces the use of several common Java decompilation tools at present. At the end of the article, they will be tested from three dimensions of compilation speed, syntax support and code readability, and the advantages and disadvantages of several tools will be analyzed.
Procyon
Github link: github.com/mstrobel/pr… Procyon is more than just a decompile tool; it is a complete set of Java metaprogramming tools focused on Java code generation and analysis. It mainly includes the following parts:
- Core Framework
- Reflection Framework
- Expressions Framework
- Compiler Toolset (Experimental)
- Java Decompiler (Experimental)
As you can see, decompilation is just one of the modules of Procyon, which was hosted at Bitbucket and migrated to GitHub, and hasn’t been updated in nearly two years, according to GitHub’s submission records. However, there are other open source decompiler tools that rely on Procyon, such as ** decompiler-procyon**, which are updated frequently. This tool will also be selected for decompression tests.
Using Procyon
<! -- https://mvnrepository.com/artifact/org.jboss.windup.decompiler/decompiler-procyon -->
<dependency>
<groupId>org.jboss.windup.decompiler</groupId>
<artifactId>decompiler-procyon</artifactId>
<version>5.1.4 ensuring. The Final</version>
</dependency>
Copy the code
Write a simple decompilation test.
package com.wdbyte.decompiler;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.List;
import org.jboss.windup.decompiler.api.DecompilationFailure;
import org.jboss.windup.decompiler.api.DecompilationListener;
import org.jboss.windup.decompiler.api.DecompilationResult;
import org.jboss.windup.decompiler.api.Decompiler;
import org.jboss.windup.decompiler.procyon.ProcyonDecompiler;
/** * Procyon decompiles the test **@author https://github.com/niumoo
* @date2021/05/15 * /
public class ProcyonTest {
public static void main(String[] args) throws IOException {
Long time = procyon("decompiler.jar"."procyon_output_jar");
System.out.println(String.format("decompiler time: %dms", time));
}
public static Long procyon(String source,String targetPath) throws IOException {
long start = System.currentTimeMillis();
Path outDir = Paths.get(targetPath);
Path archive = Paths.get(source);
Decompiler dec = new ProcyonDecompiler();
DecompilationResult res = dec.decompileArchive(archive, outDir, new DecompilationListener() {
public void decompilationProcessComplete(a) {
System.out.println("decompilationProcessComplete");
}
public void decompilationFailed(List<String> inputPath, String message) {
System.out.println("decompilationFailed");
}
public void fileDecompiled(List<String> inputPath, String outputPath) {}public boolean isCancelled(a) {
return false; }});if(! res.getFailures().isEmpty()) { StringBuilder sb =new StringBuilder();
sb.append("Failed decompilation of " + res.getFailures().size() + " classes: ");
Iterator failureIterator = res.getFailures().iterator();
while (failureIterator.hasNext()) {
DecompilationFailure dex = (DecompilationFailure)failureIterator.next();
sb.append(System.lineSeparator() + "").append(dex.getMessage());
}
System.out.println(sb.toString());
}
System.out.println("Compilation results: " + res.getDecompiledFiles().size() + " succeeded, " + res.getFailures().size() + " failed.");
dec.close();
Long end = System.currentTimeMillis();
returnend - start; }}Copy the code
Procyon will output the progress of the number of decompile files in real time during decompile, and finally count the number of successful and failed Class files for decompile.
. On May 15, 2021 afternoon 10:58:28 org. Jboss. Windup. Decompiler. Procyon. ProcyonDecompiler $3 call information: Decompiling 650/783 on May 15, 2021 10:58:30 afternoon org. Jboss. Windup. Decompiler. Procyon. ProcyonDecompiler $3 call information: Decompiling 700/783 on May 15, 2021 10:58:37 afternoon org. Jboss. Windup. Decompiler. Procyon. ProcyonDecompiler $3 call information: Decompiling 750 / 783 decompilationProcessComplete Compilation results: 783 succeeded, 0 failed. decompiler time: 40599msCopy the code
Procyon GUI
For Procyon decompilation, there is also an open source GUI interface based on this implementation available on GitHub for those interested to download and try. Github address: github.com/deathmarine…
CFR
GitHub address: github.com/leibnitz27/… CFR’s official website: www.benf.org/other/cfr/ (you may need to FQ) Maven repositories: https://mvnrepository.com/artifact/org.benf/cfr
CFR supports decompilation of Java 9, Java 12, Java 14, and other latest versions of Java code. And the CFR itself is written in Java 6, so you can use CFR in almost any version of Java programs. It’s worth noting that using CFR you can even decompile JVM class files written in other languages back into Java files.
The CFR command is used
With CFR decompilation, you can either download the published JAR packages and decompile them on the command line, or use them in your code in the way Maven introduces. Let’s start with how the command line works.
Download the latest version of JAR from GitHub Tags. You can run to view help directly.
#See the helpJava jar CFR - 0.151. The jar -- helpCopy the code
If you just decompile a class.
#Decompile the class file and print the result to the consoleJava - jar CFR 0.151. Jar WindupClasspathTypeLoader. Class#Decompile the class file and output the result to the OUT folderJava - jar CFR 0.151. Jar WindupClasspathTypeLoader. Class -- outputpath. / outCopy the code
Decompile a JAR.
#Decompile the JAR file and output the result to the output_JAR folder➜ Desktop java-jar cfr-0.151.jar decompiler.jar --outputdir./output_jar Processing decompiler.jar (use silent to silence) Processing com.strobel.assembler.metadata.ArrayTypeLoader Processing com.strobel.assembler.metadata.ParameterDefinition Processing com.strobel.assembler.metadata.MethodHandle Processing com.strobel.assembler.metadata.signatures.FloatSignature .....Copy the code
The decompilation result is written to the specified folder according to the class package path.
Used in CFR code
Adding dependencies is not mentioned here.
<! -- https://mvnrepository.com/artifact/org.benf/cfr -->
<dependency>
<groupId>org.benf</groupId>
<artifactId>cfr</artifactId>
<version>0.151</version>
</dependency>
Copy the code
I didn’t actually see a specific unit test example on either the official website or GitHub. It doesn’t matter, since you can run it on the command line, you can write your own unit tests by looking directly at the decomcompiled Main method entry in IDEA and seeing how the command line works.
package com.wdbyte.decompiler;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.benf.cfr.reader.api.CfrDriver;
import org.benf.cfr.reader.util.getopt.OptionsImpl;
/**
* CFR Test
*
* @author https://github.com/niumoo
* @date2021/05/15 * /
public class CFRTest {
public static void main(String[] args) throws IOException {
Long time = cfr("decompiler.jar"."./cfr_output_jar");
System.out.println(String.format("decompiler time: %dms", time));
// decompiler time: 11655ms
}
public static Long cfr(String source, String targetPath) throws IOException {
Long start = System.currentTimeMillis();
// source jar
List<String> files = new ArrayList<>();
files.add(source);
// target dir
HashMap<String, String> outputMap = new HashMap<>();
outputMap.put("outputdir", targetPath);
OptionsImpl options = new OptionsImpl(outputMap);
CfrDriver cfrDriver = new CfrDriver.Builder().withBuiltOptions(options).build();
cfrDriver.analyse(files);
Long end = System.currentTimeMillis();
return(end - start); }}Copy the code
JD-Core
GiHub address: github.com/java-decomp… Jd-core official website: Java-decompiler.github. IO/JD-core is an independent Java library for decompilingJava. It supports decompilingbytecode from Java 1 to Java 12, including Lambda expressions, method references, default methods, and so on. The well-known jD-GUI and Eclipse seamlessly integrated decompiler engine is JD-Core. Jd-core also provides a separate Class decompiler method. However, if you want to decompiler the entire JAR package directly in your own code, you still need to modify it. If the code has anonymous functions, such as Lambda, you can decompiler directly. But there are extra considerations.
Use the JD – core
<! -- https://mvnrepository.com/artifact/org.jd/jd-core -->
<dependency>
<groupId>org.jd</groupId>
<artifactId>jd-core</artifactId>
<version>1.1.3</version>
</dependency>
Copy the code
In order to decompile the entire JAR package, I made some simple changes to the code used for the last part of the comparison test, but this example does not consider internal classes, Lambda, etc. to compile multiple Class files, so it cannot be used directly in production.
package com.wdbyte.decompiler;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.jd.core.v1.ClassFileToJavaSourceDecompiler;
import org.jd.core.v1.api.loader.Loader;
import org.jd.core.v1.api.printer.Printer;
/ * * *@author https://github.com/niumoo
* @date2021/05/15 * /
public class JDCoreTest {
public static void main(String[] args) throws Exception {
JDCoreDecompiler jdCoreDecompiler = new JDCoreDecompiler();
Long time = jdCoreDecompiler.decompiler("decompiler.jar"."jd_output_jar");
System.out.println(String.format("decompiler time: %dms", time)); }}class JDCoreDecompiler{
private ClassFileToJavaSourceDecompiler decompiler = new ClassFileToJavaSourceDecompiler();
// Store bytecode
private HashMap<String,byte[]> classByteMap = new HashMap<>();
/** * Note: The case of a Java Class compiling multiple Class files is not considered. * *@param source
* @param target
* @return
* @throws Exception
*/
public Long decompiler(String source,String target) throws Exception {
long start = System.currentTimeMillis();
/ /
archive(source);
for (String className : classByteMap.keySet()) {
String path = StringUtils.substringBeforeLast(className, "/");
String name = StringUtils.substringAfterLast(className, "/");
if (StringUtils.contains(name, "$")) {
name = StringUtils.substringAfterLast(name, "$");
}
name = StringUtils.replace(name, ".class".".java");
decompiler.decompile(loader, printer, className);
String context = printer.toString();
Path targetPath = Paths.get(target + "/" + path + "/" + name);
if(! Files.exists(Paths.get(target +"/" + path))) {
Files.createDirectories(Paths.get(target + "/" + path));
}
Files.deleteIfExists(targetPath);
Files.createFile(targetPath);
Files.write(targetPath, context.getBytes());
}
return System.currentTimeMillis() - start;
}
private void archive(String path) throws IOException {
try (ZipFile archive = new JarFile(new File(path))) {
Enumeration<? extends ZipEntry> entries = archive.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if(! entry.isDirectory()) { String name = entry.getName();if (name.endsWith(".class")) {
byte[] bytes = null;
try (InputStream stream = archive.getInputStream(entry)) {
bytes = IOUtils.toByteArray(stream);
}
classByteMap.put(name, bytes);
}
}
}
}
}
private Loader loader = new Loader() {
@Override
public byte[] load(String internalName) {
return classByteMap.get(internalName);
}
@Override
public boolean canLoad(String internalName) {
returnclassByteMap.containsKey(internalName); }};private Printer printer = new Printer() {
protected static final String TAB = "";
protected static final String NEWLINE = "\n";
protected int indentationCount = 0;
protected StringBuilder sb = new StringBuilder();
@Override public String toString(a) {
String toString = sb.toString();
sb = new StringBuilder();
return toString;
}
@Override public void start(int maxLineNumber, int majorVersion, int minorVersion) {}
@Override public void end(a) {}
@Override public void printText(String text) { sb.append(text); }
@Override public void printNumericConstant(String constant) { sb.append(constant); }
@Override public void printStringConstant(String constant, String ownerInternalName) { sb.append(constant); }
@Override public void printKeyword(String keyword) { sb.append(keyword); }
@Override public void printDeclaration(int type, String internalTypeName, String name, String descriptor) { sb.append(name); }
@Override public void printReference(int type, String internalTypeName, String name, String descriptor, String ownerInternalName) { sb.append(name); }
@Override public void indent(a) { this.indentationCount++; }
@Override public void unindent(a) { this.indentationCount--; }
@Override public void startLine(int lineNumber) { for (int i=0; i<indentationCount; i++) sb.append(TAB); }
@Override public void endLine(a) { sb.append(NEWLINE); }
@Override public void extraLine(int count) { while (count-- > 0) sb.append(NEWLINE); }
@Override public void startMarker(int type) {}
@Override public void endMarker(int type) {}}; }Copy the code
JD-GUI
Making address:Github.com/java-decomp…
Jd-core also provides the official GUI interface, you can download directly to try.
Jadx
Jadx is a decompiler that can decompilate JAR, APK, DEX, AAR, AAB, ZIP files and also comes with JadX-GUI for interface operation. Jadx uses Grade for dependency management and can run its own repository clone package.
git clone https://github.com/skylot/jadx.git
cd jadx
./gradlew dist
#See the help
./build/jadx/bin/jadx --help
jadx - dex to java decompiler, version: dev
usage: jadx [options] <input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab)
options:
-d, --output-dir - output directory
-ds, --output-dir-src - output directory for sources
-dr, --output-dir-res - output directory for resources
-r, --no-res - do not decode resources
-s, --no-src - do not decompile source code
--single-class - decompile a single class
--output-format - can be 'java' or 'json', default: java
-e, --export-gradle - save as android gradle project
-j, --threads-count - processing threads count, default: 6
--show-bad-code - show inconsistent code (incorrectly decompiled)
--no-imports - disable use of imports, always write entire package name
--no-debug-info - disable debug info
--add-debug-lines - add comments with debug line numbers if available
--no-inline-anonymous - disable anonymous classes inline
--no-replace-consts - don't replace constant value with matching constant field
--escape-unicode - escape non latin characters in strings (with \u)
--respect-bytecode-access-modifiers - don't change original access modifiers
--deobf - activate deobfuscation
--deobf-min - min length of name, renamed if shorter, default: 3
--deobf-max - max length of name, renamed if longer, default: 64
--deobf-cfg-file - deobfuscation map file, default: same dir and name as input file with '.jobf' extension
--deobf-rewrite-cfg - force to save deobfuscation map
--deobf-use-sourcename - use source file name as class name alias
--deobf-parse-kotlin-metadata - parse kotlin metadata to class and package names
--rename-flags - what to rename, comma-separated, 'case' for system case sensitivity, 'valid' for java identifiers, 'printable' characters, 'none' or 'all' (default)
--fs-case-sensitive - treat filesystem as case sensitive, false by default
--cfg - save methods control flow graph to dot file
--raw-cfg - save methods control flow graph (use raw instructions)
-f, --fallback - make simple dump (using goto instead of 'if', 'for', etc)
-v, --verbose - verbose output (set --log-level to DEBUG)
-q, --quiet - turn off output (set --log-level to QUIET)
--log-level - set log level, values: QUIET, PROGRESS, ERROR, WARN, INFO, DEBUG, default: PROGRESS
--version - print jadx version
-h, --help - print this help
Example:
jadx -d out classes.dex
Copy the code
If you want to decompiler. Jar into the out folder, follow the HELP information.
./build/jadx/bin/jadx -d ./out ~/Desktop/decompiler.jar
INFO - loading ...
INFO - processing ...
INFO - doneress: 1143 of 1217 (93%)
Copy the code
Fernflower
GitHub address: github.com/fesh0r/fern… Fernflower, like Jadx, uses Grade for dependency management and can run its own repository clone package.
➜ fernflower-master./gradlew build build SUCCESSFUL in 32s 4 actionable tasks: 4 executed ➜ fernflower - master Java - jar build/libs/fernflower jar the Usage: java -jar fernflower.jar [-<option>=<value>]* [<source>]+ <destination> Example: Java -jar fernflower. jar-dgs =true C :\my\source\ C :\my.jar D :\ decomcompiled \ ➜ fernflower-master mkdir out ➜ fernflower-master java -jar build/libs/fernflower.jar ~/Desktop/decompiler.jar ./out INFO: Decompiling class com/strobel/assembler/metadata/ArrayTypeLoader INFO: ... done INFO: Decompiling class com/strobel/assembler/metadata/ParameterDefinition INFO: ... done INFO: Decompiling class com/strobel/assembler/metadata/MethodHandle ... ➜ fernflower-master ll out total 1288-RW-r --r-- 1 Darcy Staff 595K 5 16 17:47 decompiler.jar ➜ Fernflower-masterCopy the code
When Fernflower decompiles JAR packages, the default decompilation results in a JAR package. Jad
Decompilation speed
Now that you’ve covered five Java decompiler tools, which one should you use in your daily development? Or which one should we choose during code analysis? I think the difference between the two cases is that the focus is different. If it’s everyday use, read the code, I think it should be more readable, if it’s a lot of code analysis work, then it might be more speed decompilation and syntax support. In order to have a simple reference, I ran simple tests on each of the five decompilation tools using the JMH microbenchmark tool. Here are some of the results.
The test environment
The environment variable | describe |
---|---|
The processor | 2.6 GHz Six-core Intel Core I7 |
memory | 16 GB 2667 MHz DDR4 |
Java version | The JDK 14.0.2 |
The test way | JMH benchmark. |
JAR 1 to be decompiled | Procyon – compilertools – 0.5.33. Jar (1.5 MB) |
JAR 2 needs to be decompiled | Python2java4common – 1.0.0-20180706.084921-1. The jar (42 MB) |
JAR 1: Procyon-compilerTools-0.5.33.jar (1.5MB)
Benchmark | Mode | Cnt | Score | Units |
---|---|---|---|---|
cfr | avgt | 10 | 6548.642 + / – 363.502 | ms/op |
fernflower | avgt | 10 | 12699.147 + / – 1081.539 | ms/op |
jdcore | avgt | 10 | 5728.621 + / – 310.645 | ms/op |
procyon | avgt | 10 | 26776.125 + / – 2651.081 | ms/op |
jadx | avgt | 10 | 7059.354 + / – 323.351 | ms/op |
JAR 2: python2Java4common-1.0.0-20180706.084921-1.jar (42 MB)
JAR 2 is a relatively large package that combines a lot of code repositories together, as well as a lot of python-to-Java generated code, which is theoretically more complex.
Benchmark | Cnt | Score |
---|---|---|
Cfr | 1 | 413838.826 ms |
fernflower | 1 | 246819.168 ms |
jdcore | 1 | Error |
procyon | 1 | 487647.181 ms |
jadx | 1 | 505600.231 ms |
Syntax support and readability
If the decomcompiled code needs to be seen by yourself, then the more readable code will prevail. Here I have written some code, mainly Java 8 and below code syntax and some nested flow controls, to see how the decomcompiled code works.
package com.wdbyte.decompiler;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import org.benf.cfr.reader.util.functors.UnaryFunction;
/ * * *@author https://www.wdbyte.com
* @date2021/05/16 * /
public class HardCode <A.B> {
public HardCode(A a, B b) {}public static void test(int. args) {}public static void main(String... args) {
test(1.2.3.4.5.6);
}
int byteAnd0(a) {
int b = 1;
int x = 0;
do {
b = (byte)((b ^ x));
} while (b++ < 10);
return b;
}
private void a(Integer i) {
a(i);
b(i);
c(i);
}
private void b(int i) {
a(i);
b(i);
c(i);
}
private void c(double d) {
c(d);
d(d);
}
private void d(Double d) {
c(d);
d(d);
}
private void e(Short s) {
b(s);
c(s);
e(s);
f(s);
}
private void f(short s) {
b(s);
c(s);
e(s);
f(s);
}
void test1(String path) {
try {
int x = 3;
} catch (NullPointerException t) {
System.out.println("File Not found");
if (path == null) { return; }
throw t;
} finally {
System.out.println("Fred");
if (path == null) { throw newIllegalStateException(); }}}private final List<Integer> stuff = newArrayList<>(); { stuff.add(1);
stuff.add(2);
}
public static int plus(boolean t, int a, int b) {
int c = t ? a : b;
return c;
}
// Lambda
Integer lambdaInvoker(int arg, UnaryFunction<Integer, Integer> fn) {
return fn.invoke(arg);
}
// Lambda
public int testLambda(a) {
return lambdaInvoker(3, x -> x + 1);
// return 1;
}
// Lambda
public Integer testLambda(List<Integer> stuff, int y, boolean b) {
return stuff.stream().filter(b ? x -> x > y : x -> x < 3).findFirst().orElse(null);
}
// stream
public static <Y extends Integer> void testStream(List<Y> list) {
IntStream s = list.stream()
.filter(x -> {
System.out.println(x);
return x.intValue() / 2= =0;
})
.map(x -> (Integer)x+2)
.mapToInt(x -> x);
s.toArray();
}
// switch
public void testSwitch1(a){
int i = 0;
switch(((Long)(i + 1L)) + "") {
case "1":
System.out.println("one"); }}// switch
public void testSwitch2(String string){
switch (string) {
case "apples":
System.out.println("apples");
break;
case "pears":
System.out.println("pears");
break; }}// switch
public static void testSwitch3(int x) {
while (true) {
if (x < 5) {
switch ("test") {
case "okay":
continue;
default:
continue;
}
}
System.out.println("wow x2!"); }}}Copy the code
The decompilation results of all the tools were originally posted here, but due to the length of the article and the reading experience, I did not put them out. However, I have the complete code on my personal blog. The layout of my personal website is relatively free, and you can use the Tab Tab to display it. If you need to check it out, you can visit www.wdbyte.com.
Procyon
It’s a bit surprising to see the decompilation of Procyon, which, under normal decompilation conditions, leaves the decompilated code almost exactly as it was. The only change in decompression and source syntax is that the initialization of a collection is slightly different.
/ / the source code
public HardCode(A a, B b) {}private final List<Integer> stuff = newArrayList<>(); { stuff.add(1);
stuff.add(2);
}
// Procyon decompiles
private final List<Integer> stuff;
public HardCode(final A a, final B b) {(this.stuff = new ArrayList<Integer>()).add(1);
this.stuff.add(2);
}
Copy the code
Other parts of the code, such as boxing and unboxing, Switch syntax, Lambda expressions, streaming operations and flow control, are almost identical and easy to read.
The boxing and unboxing operations are exactly the same after decompilation, with no extra type conversion code.
/ / the source code
private void a(Integer i) {
a(i);
b(i);
c(i);
}
private void b(int i) {
a(i);
b(i);
c(i);
}
private void c(double d) {
c(d);
d(d);
}
private void d(Double d) {
c(d);
d(d);
}
private void e(Short s) {
b(s);
c(s);
e(s);
f(s);
}
private void f(short s) {
b(s);
c(s);
e(s);
f(s);
}
// Procyon decompiles
private void a(final Integer i) {
this.a(i);
this.b(i);
this.c(i);
}
private void b(final int i) {
this.a(i);
this.b(i);
this.c(i);
}
private void c(final double d) {
this.c(d);
this.d(d);
}
private void d(final Double d) {
this.c(d);
this.d(d);
}
private void e(final Short s) {
this.b(s);
this.c(s);
this.e(s);
this.f(s);
}
private void f(final short s) {
this.b(s);
this.c(s);
this.e(s);
this.f(s);
}
Copy the code
The Switch part is the same, and the flow control part is the same.
/ / source switch
public void testSwitch1(a){
int i = 0;
switch(((Long)(i + 1L)) + "") {
case "1":
System.out.println("one"); }}public void testSwitch2(String string){
switch (string) {
case "apples":
System.out.println("apples");
break;
case "pears":
System.out.println("pears");
break; }}public static void testSwitch3(int x) {
while (true) {
if (x < 5) {
switch ("test") {
case "okay":
continue;
default:
continue;
}
}
System.out.println("wow x2!"); }}// Procyon decompiles
public void testSwitch1(a) {
final int i = 0;
final String string = (Object)(i + 1L) + "";
switch (string) {
case "1": {
System.out.println("one");
break; }}}public void testSwitch2(final String string) {
switch (string) {
case "apples": {
System.out.println("apples");
break;
}
case "pears": {
System.out.println("pears");
break; }}}public static void testSwitch3(final int x) {
while (true) {
if (x < 5) {
final String s = "test";
switch (s) {
case "okay": {
continue;
}
default: {
continue; }}}else {
System.out.println("wow x2!"); }}}Copy the code
Lambda expressions are exactly the same as streaming operations.
/ / the source code
// Lambda
public Integer testLambda(List<Integer> stuff, int y, boolean b) {
return stuff.stream().filter(b ? x -> x > y : x -> x < 3).findFirst().orElse(null);
}
// stream
public static <Y extends Integer> void testStream(List<Y> list) {
IntStream s = list.stream()
.filter(x -> {
System.out.println(x);
return x.intValue() / 2= =0;
})
.map(x -> (Integer)x+2)
.mapToInt(x -> x);
s.toArray();
}
// Procyon decompiles
public Integer testLambda(final List<Integer> stuff, final int y, final boolean b) {
return stuff.stream().filter(b ? (x -> x > y) : (x -> x < 3)).findFirst().orElse(null);
}
public static <Y extends Integer> void testStream(final List<Y> list) {
final IntStream s = list.stream().filter(x -> {
System.out.println(x);
return x / 2= =0;
}).map(x -> x + 2).mapToInt(x -> x);
s.toArray();
}
Copy the code
Process control, decompilation found missing meaningless parts of the code, and easy to read.
/ / the source code
void test1(String path) {
try {
int x = 3;
} catch (NullPointerException t) {
System.out.println("File Not found");
if (path == null) { return; }
throw t;
} finally {
System.out.println("Fred");
if (path == null) { throw newIllegalStateException(); }}}// Procyon decompiles
void test1(final String path) {
try {}
catch (NullPointerException t) {
System.out.println("File Not found");
if (path == null) {
return;
}
throw t;
}
finally {
System.out.println("Fred");
if (path == null) {
throw newIllegalStateException(); }}}Copy the code
Due to the length of the code, a comparison of the decompilation results below will only list the differences, and the similarities will be skipped.
CFR
The CFR decompilation results in more type conversions, which are personally less original than Procyon, but still quite good. The only thing I don’t like about the test case is the handling of while continue.
// CFR decompilation result
// Box and unbox
private void e(Short s) {
this.b(s.shortValue()); // There is a type conversion part in the box unpacking.
this.c(s.shortValue()); // There is a type conversion part in the box unpacking.
this.e(s);
this.f(s);
}
// Process control
void test1(String path) {
try {
int n = 3;// Process control decompilation results are very satisfactory, original, even the nonsense code here is retained.
}
catch (NullPointerException t) {
System.out.println("File Not found");
if (path == null) {
return;
}
throw t;
}
finally {
System.out.println("Fred");
if (path == null) {
throw newIllegalStateException(); }}}// Lambda and Stream operate exactly the same.
// On switch, the function is the same after decompilation, but the flow control is changed.
public static void testSwitch3(int x) {
block6: while (true) { // Only while(true) is available in the source code
if (x < 5) {
switch ("test") {
case "okay": {
continue block6; / / the block6}}continue;
}
System.out.println("wow x2!"); }}Copy the code
JD-Core
Jd-core, like CFR, is no longer consistent with decompression for boxing and unboxing, has a conversion part, and automatically optimizes data types. Personally, if it is decompiled after reading, the data type conversion optimization effect is quite big.
// jd-core decompiler
private void d(Double d) {
c(d.doubleValue()); // New data type conversion
d(d);
}
private void e(Short s) {
b(s.shortValue()); // New data type conversion
c(s.shortValue()); // New data type conversion
e(s);
f(s.shortValue()); // New data type conversion
}
private void f(short s) {
b(s);
c(s);
e(Short.valueOf(s)); // New data type conversion
f(s);
}
// The Stream operation also optimizes the cast automatically, which makes reading more tedious.
public static <Y extends Integer> void testStream(List<Y> list) {
IntStream s = list.stream().filter(x -> {
System.out.println(x);
return (x.intValue() / 2= =0);
}).map(x -> Integer.valueOf(x.intValue() + 2)).mapToInt(x -> x.intValue());
s.toArray();
}
Copy the code
Jadx
First, Jadx reported an error when decomcompiling the test code. The result also indicated that Lambda and Stream operations could not be decomcompiled. The variable names were messy and the flow control was almost dead.
// Jadx decompilation
private void e(Short s) {
b(s.shortValue());// New data type conversion
c((double) s.shortValue());// New data type conversion
e(s);
f(s.shortValue());// New data type conversion
}
private void f(short s) {
b(s);
c((double) s);// New data type conversion
e(Short.valueOf(s));// New data type conversion
f(s);
}
public int testLambda(a) { // testLambda decompile failed
/* r2 = this; r0 = 3 r1 = move-result java.lang.Integer r0 = r2.lambdaInvoker(r0, r1) int r0 = r0.intValue() return r0 */
throw new UnsupportedOperationException("Method not decompiled: com.wdbyte.decompiler.HardCode.testLambda():int");
}
// Stream decompilation failed
public static <Y extends java.lang.Integer> void testStream(java.util.List<Y> r3) {
/* java.util.stream.Stream r1 = r3.stream() r2 = move-result java.util.stream.Stream r1 = r1.filter(r2) r2 = move-result java.util.stream.Stream r1 = r1.map(r2) r2 = move-result java.util.stream.IntStream r0 = r1.mapToInt(r2) r0.toArray() return */
throw new UnsupportedOperationException("Method not decompiled: com.wdbyte.decompiler.HardCode.testStream(java.util.List):void");
}
public void testSwitch2(String string) { // Switch operation can not read properly, and the source code is large discrepancy.
char c = 65535;
switch (string.hashCode()) {
case -1411061671:
if (string.equals("apples")) {
c = 0;
break;
}
break;
case 106540109:
if (string.equals("pears")) {
c = 1;
break;
}
break;
}
switch (c) {
case 0:
System.out.println("apples");
return;
case 1:
System.out.println("pears");
return;
default:
return; }}Copy the code
Fernflower
Fernflower’s decompilation is generally good, but it does have some shortcomings, such as the specification of variable names and the decompilation of Switch strings.
// Decompilated variable names are not easy to read, there are many var variables
int byteAnd0(a) {
int b = 1;
byte x = 0;
byte var10000;
do {
int b = (byte)(b ^ x);
var10000 = b;
b = b + 1;
} while(var10000 < 10);
return b;
}
// Switch decompiles using hashCode
public static void testSwitch3(int x) {
while(true) {
if (x < 5) {
String var1 = "test";
byte var2 = -1;
switch(var1.hashCode()) {
case 3412756:
if (var1.equals("okay")) {
var2 = 0;
}
default:
switch(var2) {
case 0:}}}else {
System.out.println("wow x2!"); }}}Copy the code
conclusion
Five decompilation tools were compared, and when combined with decompilation speed and code readability tests, it looked like CFR was the winner, with Procyon a close second. CFR is the best in speed and readability of decomcompiled code, mainly in variable naming, boxing and unboxing, type conversion, flow control, as well as syntax support for Lambda expressions, Stream operations and Switch. According to CFR officials, Java 14 syntax is already supported, and as of this writing, CFR’s latest code submission was 11 hours ago, so updates are fast.
Some of the code in this article has been uploaded to GitHub: github.com/niumoo/lab-…
The last word
Article helpful can point a “like” or “share”, are support, I like! This article is updated every week. You can follow the “unread Code” public account or my blog, or add me to wechat: WN8398.