Deep analysis of five "black magic" in Java

Keywords: Java Programming JDK jvm

Nowadays, programming languages are becoming more and more complex. Despite a large number of documents and books, these learning materials can only describe the tip of the iceberg of programming languages. Many of the functions in these programming languages may be hidden in the dark forever. This article will explain the secrets hidden in five of Java, which can be called Java's "black magic". For these magic, we will describe their implementation principle, and give the implementation code combined with some application scenarios.

One stone, two birds: Annotation

From JDK5, Java began to introduce annotation function. Since then, annotation has become an important part of many Java applications and frameworks. In most cases, annotations will be used to describe language structures, such as classes, fields, methods, etc., but in another case, annotations can be used as implementable interfaces.

In normal usage, annotation is annotation, and interface is interface. For example, the following code adds a comment for the interface MyInterface.

@Deprecated
interface MyInterface {
}

And the interface can only play the role of the interface, such as the following code, Person implements the IPerson interface, and implements the getName method.

interface IPerson {
    public String getName();
}
class Person implements IPerson {
    @Override
    public String getName() {
        return "Foo";
    }
}

However, through the annotation of black magic, the interface and annotation can be combined into one, which plays a role of "one stone, two birds". That is to say, if it is used by annotation, it is annotation; if it is used by interface, it is interface. For example, the following code defines a Test comment.

@Retention(RetentionPolicy.RUNTIME)
@interface Test {
    String name();
}

The Test annotation is decorated with the retention annotation. Retention annotation can be used to decorate other annotations, so it is called meta annotation. The later retention policy.runtime parameter indicates that the annotation is not only saved to the class file, but still exists after the jvm loads the class file. In this way, after the program runs, you can still get the information of the annotation dynamically.

Test itself is a comment. There is a method named name. Name is an abstract method. You need to specify a specific value when using the comment. In fact, name is equivalent to the attribute of test. The following Sporter class modifies the run method using the test annotation.

class Sporter {
    @Test(name = "Bill")
    public void run (){
    }
}

You can obtain the comment information that decorates the run method through reflection, for example, the value of the name attribute. The code is as follows:

Sporter sporter = new Sporter();
var annotation = sporter.getClass().getMethod("run").getAnnotations()[0];
var method = annotation.annotationType().getMethod("name");
System.out.println(method.invoke(annotation));   // Output Bill

If we only consider comments, it's over here, but now we need to use the "comment black magic". Because there is a name method in the Test, we can simply use the name method to directly implement it with a class without defining a similar interface. The code is as follows:

class Teacher implements Test {
    @Override
    public String name() {
        return "Mike";
    }
    @Override
    public Class<? extends Annotation> annotationType() {
        return Test.class;
    }
}

Note that if you want to implement an annotation, you must implement the annotationType method, which returns the type of the annotation and the Class object of Test. Although in most cases, you don't need to implement an annotation, in some cases, such as in annotation driven frameworks, it can be useful.

Various initialization methods: initialization block

In Java, like most object-oriented programming languages, you can instantiate objects by using construction methods. Of course, there are some exceptions. For example, the deserialization of Java objects does not need to instantiate objects by construction methods (we will not consider these exceptions first). There are also ways to instantiate objects that do not use constructors on the surface, but still use constructors in essence. For example, to instantiate an object through the static factory mode is to declare the constructor of the class itself as private, so that the object cannot be instantiated directly through the constructor of the class. Instead, the constructor declared as private must be called by the method of the class itself to instantiate the object, so there is the following code:

class Person {
    private final String name;
    private Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
   // Static factory method 
    public static Person withName(String name) {
        return new Person(name);
    }
}

public class InitDemo {
    public static void main(String[] args){
        // Instantiate objects with static factory methods 
        Person person = Person.withName("Bill");
        System.out.println(person.getName());
    }
}

Therefore, when we want to initialize an object, we put the initialization logic into the object's constructor. For example, we initialized the name member variable with the parameter name in the constructor of the Person class. Although it seems reasonable to assume that all initialization logic is found in one or more constructor methods of a class. This is not the case with Java. In Java, in addition to initializing objects in construction methods, you can also initialize objects through code blocks.

class Car {
    // Common code block 
    {
        System.out.println("This is output in the code block");
    }
    public Car() {
        System.out.println("This is output in the constructor");
    }
}
public class InitDemo {
    public static void main(String[] args){
        Car car = new Car();        
    }
}

The initialization logic is accomplished by defining a bunch of curly braces inside the class, which is the function of the code block, also known as the initializer. When instantiating an object, the initializer of the class is first invoked, then the construction method of the class is invoked. Note that you can specify multiple initializers in your class, in which case each initializer will be called in the defined order.

class Car {
    // Common code block 
    {
        System.out.println("This is output in block 1");
    }
    // Common code block 
    {
        System.out.println("This is output in block 2");
    }    
    public Car() {
        System.out.println("This is output in the constructor");
    }
}
public class InitDemo {
    public static void main(String[] args){
        Car car = new Car();        
    }
}

In addition to ordinary code blocks (initializers), we can also create static code blocks (also known as static initializers) that execute when classes are loaded into memory. To create a static initializer, we just need to add the static keyword before the normal initializer.

class Car {
    {
        System.out.println("This is output in a normal block of code");
    }
    static {
        System.out.println("This is output in a static block of code");
    }
    public Car() {
        System.out.println("This is output in the constructor");
    }
}
public class InitDemo {
    public static void main(String[] args){
        Car car = new Car();
        new Car();
    }
}

The static initializer executes only once, and is the first block of code to execute. For example, in the above code, two Car objects are created, but the static block will only be executed once, and it is the first to execute. The construction methods of common code block and Car class will be executed in turn each time the Car instance is created.

If it's just a code block or construction method, it's not complicated, but if the construction method, common code block and static code block appear in the class at the same time, it's a little more complicated. In this case, the static code block will be executed first, then the common code block, and finally the construction method. When a parent class is introduced, the situation becomes more complex. The execution rules of static code blocks, common code blocks and construction methods of parent and child classes are as follows:

  1. Execute all static code blocks in the parent class in the declared order
  2. Execute all static code blocks in subclass in declaration order
  3. Execute all common code blocks in the parent class in the declared order
  4. Construction method of executing parent class
  5. Execute all common code blocks in subclass in declaration order
  6. Construction method of execution subclass

The following code demonstrates the execution process:

class Car {
    {
        System.out.println("This is in Car Output in normal code block");
    }
    static {
        System.out.println("This is in Car Output in static code block");
    }
    public Car() {
        System.out.println("This is in Car Output in construction method");
    }
}

class MyCar extends  Car {
    {
        System.out.println("This is in MyCar Output in normal code block");
    }
    static {
        System.out.println("This is in MyCar Output in static code block");
    }
    public MyCar() {
        System.out.println("This is in MyCar Output in construction method");
    }
}
public class InitDemo {
    public static void main(String[] args){

        new MyCar();
    }
}

Execute this code and you will get the following results:

A good way to initialize: Double curly bracket initialization

Many programming languages contain a syntax mechanism that allows you to quickly create list (array) and map (Dictionary) objects with very little code. For example, C + + can be initialized with braces, which allows developers to quickly create a list of enumeration values, and even initialize the entire object if the object's constructor supports this feature. Unfortunately, before JDK 9, so before JDK 9, we still need to create and initialize the list with the following code painfully and helplessly:

List<Integer> myInts = new ArrayList<>();
myInts.add(1);
myInts.add(2);
myInts.add(3);

Although the above code can do our goal well: create an ArrayList object with three integer values. But the code is too verbose, which requires developers to use the name of the variable (myInts) every time. To simplify this paragraph of diamante, you can use double brackets to do the same.

List<Integer> myInts = new ArrayList<>() {{
    add(1);
    add(2);
    add(3);
}};

Curly bracket initialization is actually a combination of multiple syntax elements. First, we create an anonymous inner class that extends the ArrayList class. Since ArrayList has no abstract methods, we can create an empty entity for the anonymous class implementation.

List<Integer> myInts = new ArrayList<>() {};

With this line of code, the original ArrayList is actually created with the same ArrayList anonymous subclass. One of the main differences between them is that our inner class has an implicit reference to the containing class, and we are creating a non static inner class. This enables us to write some interesting logic (if not very complex), such as adding the captured variable to anonymous, and the internal class code initialized by double curly braces is as follows:

package black.magic;

import java.util.ArrayList;
import java.util.List;
class InitDemo {
    public List<InitDemo> getListWithMeIncluded() {
        return new ArrayList<InitDemo>() {{
            add(InitDemo.this);
        }};
    }
}
public class DoubleBraceInitialization {
    public static void main(String[] args)  {

        List<Integer> myInts2 = new ArrayList<>() {};

        InitDemo demo = new InitDemo();
        List<InitDemo> initList = demo.getListWithMeIncluded();
        System.out.println(demo.equals(initList.get(0)));
    }
}

If the inner class in the above code is statically defined, we will not be able to access InitDemo.this. For example, the following code statically creates an inner class named MyArrayList, but cannot access the InitDemo.this reference, so it cannot be compiled:

class InitDemo {

    public List<InitDemo> getListWithMeIncluded() {
        return new FooArrayList();
    }
    private static class FooArrayList extends ArrayList<InitDemo> {{
        add(InitDemo.this);   // There will be compilation errors
    }}
}

After recreating the construction of the ArrayList initialized by double curly braces, once we have created a non static inner class, we can use instance initialization (as mentioned above) to add three initial elements when instantiating the anonymous inner class. Because the anonymous inner class will be instanced immediately, and only one object exists in the anonymous inner class, we essentially create a non static inner singleton object, which will add three initial elements when it is created. If we separate two braces, this will become more obvious. One of the braces clearly constitutes the definition of an anonymous inner class, and the other represents the beginning of the instance initialization logic:

List<Integer> myInts = new ArrayList<>() {
    {
        add(1);
        add(2);
        add(3);
    }
};

Although this technique is useful, JDK 9 (JEP 269) has replaced the utility of this technique with a static factory method of a set of lists (and many other collection types). For example, we can use these static factory methods to create the above List. The code is as follows:

List<Integer> myInts = List.of(1, 2, 3);

There are two main reasons why this static factory technology is needed:
(1) No need to create anonymous inner class;
(2) Reduced template code (noise) required to create lists.
But the price of creating a list this way is that it is read-only. That is to say, once created, it cannot be modified. In order to create a read-write list, you can only use the double curly bracket initialization method or the traditional initialization method described earlier.

Note that traditional initialization, double curly bracket initialization, and JDK 9 static factory methods are not only available for lists. They can also be used for Set and Map objects, as shown in the following code snippet:

Map<String, Integer> myMap1= new HashMap<>();
myMap1.put("key1", 10);
myMap1.put("key2", 15);

Map<String, Integer> myMap2 = new HashMap<>() {{
    put("Key1", 10);
    put("Key2", 15);
}};

Map<String, Integer> myMap3 = Map.of("key1", 10, "key2", 15);

Before initializing with double curly braces, consider its nature. Although it does improve the readability of the code, it has some implicit side effects. For example, an implicit object is created.

Comments are not soy sauce: executable comments

Annotations are almost an essential part of every program. The main advantage of annotations is that they are not executed, and it is easy to make the program more readable. This becomes more apparent when we comment out a line of code in our program. We want to keep the code in our application, but we don't want it executed. For example, the following procedure causes 5 to be printed to standard output:

public static void main(String args[]) {
    int value = 5;
    // value = 8;
    System.out.println(value);
}

Although not executing annotations is a basic assumption, this is not entirely true. For example, what will the following code snippet print to standard output?

public static void main(String args[]) {
    int value = 5;
    // \u000dvalue = 8;
    System.out.println(value);
}

You must guess 5, but if you run the above code, we see that 8 is output in Console. The reason behind this seemingly error is the Unicode character \ u000d. This character is actually a Unicode carriage return, and the Java source code is used by the compiler as a text file in Unicode format. Adding this carriage return changes "value= 8;" to the next line of the comment (there is no comment in this line, which is equivalent to pressing the carriage return key before value) to ensure that the assignment is performed. This means that the above snippet is actually equal to the following snippet:

public static void main(String args[]) {
    int value = 5;
    // 
value = 8;
    System.out.println(value);
}

Although this seems to be an error in Java, it's actually a built-in feature in the language. The initial goal of Java is to create a platform independent language (so create a Java virtual machine or JVM), and the interoperability of the source code is the key to this goal. Allowing java source code to contain Unicode characters means that non Latin characters can be included in this way. This ensures that code written in one part of the world, which may contain non Latin characters, such as in comments, can be executed anywhere else. For more information, see the Java language specification or Section 3.3 of JLS.

Combination of enumeration and interface: enumeration implementation interface

One of the limitations of enumerations (enumerations) compared to classes in Java is that enumerations cannot inherit from another class or enumerations. For example, you cannot:

public class Speaker {
    public void speak() {
        System.out.println("Hi");
    }
}
public enum Person extends Speaker {
    JOE("Joseph"),
    JIM("James");
    private final String name;
    private Person(String name) {
        this.name = name;
    }
}
Person.JOE.speak();

However, I can let enumeration implement an interface and provide an implementation for its abstract methods as follows:

public interface Speaker {
    public void speak();
}
public enum Person implements Speaker {
    JOE("Joseph"),
    JIM("James");
    private final String name;
    private Person(String name) {
        this.name = name;
    }
    @Override
    public void speak() {
        System.out.println("Hi");
    }
}
Person.JOE.speak();

Now, we can also use an instance of Person anywhere we need a Speaker object. In addition, we can provide the implementation of interface abstract methods (called constant specific methods) based on each constant:

public interface Speaker {
    public void speak();
}
public enum Person implements Speaker {
    JOE("Joseph") {
        public void speak() { System.out.println("Hi, my name is Joseph"); }
    },
    JIM("James"){
        public void speak() { System.out.println("Hey, what's up?"); }
    };
    private final String name;
    private Person(String name) {
        this.name = name;
    }
    @Override
    public void speak() {
        System.out.println("Hi");
    }
}
Person.JOE.speak();

Unlike some of the other magic in this article, the use of this technique should be encouraged where appropriate. For example, if you can use an enumeration constant, such as JOE or JIM, instead of an interface type, such as Speaker, the enumeration that defines the constant should implement the interface type.

summary

In this article, we have studied five hidden secrets in Java:
(1) Extensible notes;
(2) Instance initialization can be used to configure objects during instantiation;
(3) Double curly braces for initialization;
(4) Executable notes;
(5) Enumeration can realize the interface;
Although some of these functions have their proper uses, you should avoid using some of them (that is, creating executable comments). When deciding to use these secrets, make sure it's really necessary.

Posted by Warz on Mon, 13 Apr 2020 00:19:32 -0700