For compilation and decompilation, see here: juejin.cn/post/684490…
What is grammar sugar?
Syntactic sugar, also known as sugar-coated syntax, is a syntax added to a computer language that has no effect on the language’s functionality but is more convenient for programmers to use. It’s called “sugar” because it makes code easier to write and easier to look at. It’s like adding sugar to code.
The opposite is syntactic salt, which, while using syntactic features that make it less likely to write bad code, forces programmers to do extra things that don’t describe the program’s behavior at all, but prove that they know what they’re doing, which in short is unpleasantly salty.
However, while syntax sugars can make development easier, they are not actually supported by the Java VIRTUAL machine. They are reduced to Java’s basic syntactic structures at compile time, a process also known as decoding sugar.
Let’s take a look at the syntactic sugar in Java and see what these sugar cubes look like. (o゚ω゚o)
Syntax sugar in Java
Switch Support for String and enum
Prior to Java7, the switch supported only five types of arguments: int, short, char, byte, and enumeration. As far as the compiler is concerned, only integer arguments can be used in Switch, and its support for char is achieved by comparing its ASCII characters.
As of JDK1.7, String support has been added to switch.
package demo;
public class Demo {
public static void main(String[] args) {
String s = "hello";
switch (s) {
case "hello" :
System.out.println("hello");
break;
case "world" :
System.out.println("world");
break;
default:}}}Copy the code
After decompiling the above code, we get the following result:
As you can see, switch support for strings is actually implemented through hashCode() and equals(). It’s worth noting that the necessary second check is done with equals() to prevent hash collisions, where the hash code is the same but the objects are different.
Write a paragraph with enum:
package demo;
public class Demo {
public static void main(String[] args) {
DemoEnum e = DemoEnum.UP;
switch (e) {
case UP :
System.out.println("hello");
break;
case DOWN :
System.out.println("world");
break;
default:}}}enum DemoEnum {
UP, DOWN, LEFT, RIGHT
}
Copy the code
This is done in conjunction with decompilation of enumerated classes, which will be covered later. Enumeration classes are implemented by assigning them a code of type int, which the switch actually uses to compare enums.
The generic
For the Java virtual machine, “generics” is something that doesn’t exist. Syntaxes like List
already perform a syntax called type erasure at compile time.
Type erasure is divided into two main steps:
- Replace all generic arguments with their top-level parent (typically Object)
- Remove all type parameters
So when we decompile a piece of code that uses generics, we get something like this:
package demo;
import java.util.ArrayList;
import java.util.List;
public class Demo {
public static void main(String[] args) {
List<Student> list = new ArrayList<>();
list.add(new Student("a".1)); }}class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age; }}Copy the code
List
is completely erased to List, but not for nothing. Using generics can limit the code specification at compile time, preventing the addition of a Teacher to a List declaring Student by mistake.
Automatic packing and unpacking
Automatic packing means that the original Java type is automatically converted to the corresponding packaging type, while automatic unpacking means the reverse. There should be eight specific types: Byte-byte, short-short, char-character, int-INTEGER, long-long, float-float, double-double, Boolean – Boolean.
package demo;
public class Demo {
public static void main(String[] args) {
/ / packing
Integer a = 11;
System.out.println(a);
/ / split open a case
intb = a; System.out.println(b); }}Copy the code
As you can see from a simple boxing unboxing, boxing is done by calling the valueOf() method of the wrapper, and unboxing is done by calling xxxValue().
Method variable length parameter
Mutable parameters are a feature introduced in Java5 that allows a method to take an arbitrary number of values as parameters.
package demo;
public class Demo {
public static void main(String[] args) {}private static void demo(String... args) {
for(String s : args) { System.out.println(s); }}}Copy the code
Above, when a variable parameter is used, it first creates an array of the actual number of parameters passed and puts the parameter values into the array. The argument list declared by the called method is actually compiled as an array.
The enumeration
It is said that everything in Java is an object, and an object must have a class. Where is the enumerated class? Let’s start with a simple enumerated class:
package demo;
public enum DemoEnum {
UP, DOWN, LEFT, RIGHT
}
Copy the code
Then decompile it:
As you can see, enumerations are maintained by a compiler that automatically creates a class called Public Final Class XXEnum extends Enum, and the concrete enumerations we define are declared as static constants in the class. This class inherits Enum and is decorated with the final keyword, which is the root reason why enumerated types cannot be inherited.
The inner class
An inner class, also known as a nested class, is an ordinary member of an outer class.
In reality, however, inner classes are just a compile-time concept. Although it exists as a “member” of the external class, during actual compilation it will exist as a separate class and generate a.class file named $inner class name.class, independent of the external class.
package demo;
public class Demo {
class InnerClass {
private String name;
private intage; }}Copy the code
However, when we decompile the external class, we still package the decompiled OwO with the.class file of the inner class
Conditional compilation
Typically, every line of code in a program needs to be compiled. However, sometimes for the sake of code optimization, it is hoped that only part of the content is compiled. At this time, conditions need to be added in the program, so that the compiler can only compile the code that meets the conditions, and discard the code that does not meet the conditions. This is conditional compilation.
Take this code for example. Given that flag is true, the code logic will definitely not enter the branch that prints false, so at compile time, the branch that is confirmed not to enter is discarded directly, and the entire conditional clause is reduced to the branch that executes true directly.
package demo;
public class Demo {
public static void main(String[] args) {
final boolean flag = true;
if(flag) {
System.out.println("true");
} else {
System.out.println("false"); }}}Copy the code
assertions
In Java, the Assert keyword was introduced beginning with Java SE 1.4. To avoid some conflicts with the boss, Java does not enable assertion checking by default when executing, that is, all assertion statements are ignored by default. If you need to enableassertions, you can do so by setting -enableassertions or -ea.
package demo;
public class Demo {
public static void main(String[] args) {
int a = 1, b = 1;
assert a == b;
System.out.println("a == b");
asserta ! = b :"false";
System.out.println("a ! = b"); }}Copy the code
As you can see from the decompiled code, the underlying implementation of an assertion is an if statement: if the assertion is true, nothing is done and the program continues. If the result of the assertion is not true, an AssertError is thrown to interrupt execution.
Numeric literal
A numeric literal that inserts any number of underscores between numbers (either integer or floating-point) to make it easier for developers to read but not to compile. The way it works is, when you compile, you delete the underline QvQ
package demo;
public class Demo {
public static void main(String[] args) {
int a = 100 _00; System.out.println(a); }}Copy the code
for-each
Enhancements to the for loop can make the for loop more concise by using the normal for loop and the iterator QWQ
package demo;
import java.util.ArrayList;
import java.util.List;
public class Demo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for(Integer i : list) { System.out.println(i); }}}Copy the code
try-with-resource
Try-catches with resources can help us handle shutting down resources used in operations, especially file operations and database connections. Instead of releasing resources ina finally with a normal try-catch, using try-with-resource avoids this tedious and repetitive close() work, making code cleaner and easier to read.
package demo;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class Demo {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("path"))) {
String line;
while((line = br.readLine()) ! =null) { System.out.println(line); }}catch (IOException e) {
}
}
}
Copy the code
After decomcompiling the code, you can see that the underlying implementation principle of try-with-resource is still the traditional way of shutting down resources that we didn’t do, and the compiler does it for us.
Lambda expressions
Finally, our lambda expression! It is implemented by calling lambda-related apis provided underneath the JVM. Here, for example, is to call the Java. Lang. Invoke the LambdaMetafactory# metafactory method, and then using a lambda $$0 main methods for output:
package demo;
import java.util.Arrays;
import java.util.List;
public class Demo {
public static void main(String[] args) {
String[] str = {"aa"."bb"."cc"}; List<String> list = Arrays.asList(str); list.forEach((s) -> System.out.println(s)); }}Copy the code