The taste of syntactic sugar in the JVM series

essay

Recently, I have been researching the content related to "microservices and cloud native", and I have gained a lot. As a result, the article was published a week late. There is a high probability that future bloggers' articles will be "microservices, cloud native related content. If you are interested You can pay attention to the dynamics of the blogger~~

insert image description here

introduction

Reference book: "In-depth understanding of Java Virtual Machine"

Personal java knowledge sharing project - gitee address

Personal java knowledge sharing project - github address

java syntactic sugar

The article mainly learns about the java syntax sugar we commonly use in daily development from the perspective of virtual machines, such as: generics, automatic unpacking, loop traversal, conditional compilation, variable length parameters, internal classes, enumeration classes, assertion statements , switch support for enumerations and strings (supported in JDK 1.7), define and close resources in try statements.

generic

Generics are a new feature of JDK 1.5. Its essence is the application of parameterized types, that is to say, the type of data to be manipulated is specified as a parameter. This parameter type can be used in the creation of classes, interfaces, and methods, known as generic classes, generic interfaces, and generic methods, respectively.

The role of generics

The idea of ​​generics began to take root in the template (Template) of the C++ language. When the Java language was in a version that had not yet appeared generics, it could only be achieved through the combination of Object being the parent class of all types and type coercion. to achieve type generalization. For example, if the type of elements stored in the List collection is not specified through generics, when the List collection is accessed, the return value is an Object object. Since all types in the Java language inherit from java.lang.Object, Object Casting to any object is possible. But also because there are infinite possibilities, only the programmer and the virtual machine at runtime know what type of object this Object is. During compilation, the compiler cannot check whether the forced transformation of this Object is successful. If you only rely on the programmer to ensure the correctness of this operation, the risk of many ClassCastExceptions will be transferred to the program runtime.

public static void main(String[] args) {
    
    
        demo(Lists.newArrayList(new User("disaster", 1), new User("disaster1", 2)));
        demo1(Lists.newArrayList(new User("disaster", 1), new User("disaster1", 2), "disaster"));
    }

    public static void demo(List<User> users) {
    
    
        for (User user : users) {
    
    
            System.out.println(user.getName());
        }
    }

    public static void demo1(List users) {
    
    
        for (Object user : users) {
    
    
            User user1 = (User) user;
            System.out.println(user1.getName());
        }
    }

The above case is a good illustration of this problem. When running the demo1 method, the program throws a ClassCastException.

Principles of Generics

The use of generic technology in C# and Java seems to be the same, but there are fundamental differences in implementation. In C#, generics are used in program source code or compiled IL (Intermediate Language, intermediate language, at this time) Generic is a placeholder), or in the runtime CLR, they all exist. List and List are two different types. They are generated during system runtime and have their own virtual method table and type data. This implementation is called type inflation, and the generics implemented based on this method are called real generics.

The generic type in the Java language is different. It only exists in the program source code. In the compiled bytecode file, it has been replaced by the original native type (Raw Type, also known as the naked type), and in The mandatory transformation code is inserted in the corresponding place. Therefore, for the Java language at runtime, ArrayList<int> and ArrayList<String> are the same class, so the generic technology is actually a syntactic sugar of the Java language. The generic implementation method in the language is called type erasure, and the generics implemented based on this method are called pseudo-generics.

case:

public static void method(List<String> list) {
    
    
        System.out.println("invoke method(List<String>list)");
    }
public static void method(List<Integer> list) {
    
    
        System.out.println("invoke method(List<Integer>list)");
    }

These two methods cannot be compiled, because the principle of generics is already known

But there is another special situation that needs to be mentioned a little bit more:

public static String method(List<String>list){
    
    
        System.out.println("invoke method(List<String>list)"); return"";
    }
public static int method(List<Integer>list){
    
     System.out.println("invoke method(List<Integer>list)"); return 1;
    }

You can use the version of jdk1.6 to compile this case, we can amazingly find that these two methods can exist in the same class, we know that the return value of the method overload does not participate in the overload selection, but the above two The parameters of the method are all List objects, and only the return value is different, so it is reasonable to say that it cannot be compiled. Let me emphasize again that overloading is not determined based on the return value. As for why two identical method methods can be compiled and executed here, it is because in the Class file format of the jdk1.6 version, as long as the descriptors are not completely consistent, the two methods can be coexist. In other words, if two methods have the same name and signature but different return values, they can also legally coexist in a Class file.

However, this peculiar phenomenon is contrary to the basic cognition that "the return value does not participate in the overload selection", so the JCP organization has made corresponding changes to the virtual machine specification, introducing new attributes such as Signature, LocalVariableTypeTable, etc. The identification of parameter types from generics. Signature is one of the most important attributes. Its function is to store the characteristic signature of a method at the bytecode level. The parameter type stored in this attribute is not a native type, but is information that includes parameterized types.

Automatic unpacking, loop traversal, variable length parameters

case:

public static void demo3(Integer... args) {
    
    
        List<Integer> list = Arrays.asList(args);
        Integer sum = 0;
        for (Integer i : list) {
    
    
            sum += i;
            System.out.println(i + sum);
        }
        System.out.println(sum);
    }

Compiled bytecode:

 0 aload_0
 1 invokestatic #27 <java/util/Arrays.asList : ([Ljava/lang/Object;)Ljava/util/List;>
 4 astore_1
 5 iconst_0
 6 invokestatic #4 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
 9 astore_2
10 aload_1
11 invokeinterface #12 <java/util/List.iterator : ()Ljava/util/Iterator;> count 1
16 astore_3
17 aload_3
18 invokeinterface #13 <java/util/Iterator.hasNext : ()Z> count 1
23 ifeq 70 (+47)
26 aload_3
27 invokeinterface #14 <java/util/Iterator.next : ()Ljava/lang/Object;> count 1
32 checkcast #28 <java/lang/Integer>
35 astore 4
37 aload_2
38 invokevirtual #29 <java/lang/Integer.intValue : ()I>
41 aload 4
43 invokevirtual #29 <java/lang/Integer.intValue : ()I>
46 iadd
47 invokestatic #4 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
50 astore_2
51 getstatic #15 <java/lang/System.out : Ljava/io/PrintStream;>
54 aload 4
56 invokevirtual #29 <java/lang/Integer.intValue : ()I>
59 aload_2
60 invokevirtual #29 <java/lang/Integer.intValue : ()I>
63 iadd
64 invokevirtual #30 <java/io/PrintStream.println : (I)V>
67 goto 17 (-50)
70 getstatic #15 <java/lang/System.out : Ljava/io/PrintStream;>
73 aload_2
74 invokevirtual #31 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
77 return

The above case includes several syntactic sugars such as autoboxing, unboxing, traversal loops, variable-length parameters, and generics. Let's use the compiled bytecode to see the actual face of these syntactic sugars. Generics don't have to Having said that, autoboxing and unboxing are transformed into corresponding packaging and restoration methods after compilation, such as the Integer.valueOf() and Integer.intValue() methods in this example, and the traversal loop restores the code to The implementation of iterators, which is why traversing the loop requires the class to be traversed to implement the Iterable interface. Finally, look at the variable-length parameters. When you see the first line of bytecode, you call the "<java/util/Arrays.asList : ([Ljava/lang/Object;)Ljava/util/List;>" method, then Let's go to the inside of this method to see:

public static <T> List<T> asList(T... a) {
    
    
        return new ArrayList<>(a);
    }

private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
{
    
    
		private static final long serialVersionUID = -2764017481108945198L;
        private final E[] a;

        ArrayList(E[] array) {
    
    
            a = Objects.requireNonNull(array);
        }
}

When it is called, it becomes an array-type parameter. Before variable-length parameters appeared, programmers used arrays to accomplish similar functions.

Although grammatical sugar has brought convenience to my developers, there are some pitfalls that we need to step on. Take a case that I encountered in an interview as an example:

public static void main(String[]args){
    
     Integer a=1;
	Integer b=2;
	Integer c=3;
	Integer d=3;
	Integer e=321;
	Integer f=321;
	Long g=3L; 
	System.out.println(c==d); 	
	System.out.println(e==f); 
	System.out.println(c==(a+b)); 
	System.out.println(c.equals(a+b)); 
	System.out.println(g==(a+b));
	System.out.println(g.equals(a+b));
 }

In this case, you can go to your own machine to run it to see the result (you can also feel the problem similar to String).

conditional compilation

case:

public static void demo5() {
    
    
        if (true) {
    
    
            System.out.println("block 1");
        } else {
    
    
            System.out.println("block 2");
        }
    }

Bytecode:

0 getstatic #15 <java/lang/System.out : Ljava/io/PrintStream;>
3 ldc #32 <block 1>
5 invokevirtual #17 <java/io/PrintStream.println : (Ljava/lang/String;)V>
8 return

Looking at the compiled bytecode, we found that after the demo5 method is compiled, there is only the code to print the "block 1" string, which is actually after the conditional compilation. In the same way, if the if inside is false, there will be only the code of "block 2" string after compilation.

There are still a few grammatical sugars that this article did not discuss, but you can check its principles according to the ideas of the grammatical sugar cases I mentioned above~~~~

Guess you like

Origin blog.csdn.net/a_ittle_pan/article/details/126567959