Changes of JDK8 in generic type derivation

Posted by Random on Tue, 09 Jun 2020 09:26:58 +0200

This article comes from: PerfMa technology community

Perfma official website

summary

JDK8 upgrade, most of the problems may be encountered in the compile time, but sometimes it is more painful, there is no problem in the compile time, but there is a problem in the run time, such as today's topic, so when you upgrade, you still need to test more and then go online, of course, JDK8 brings us a lot of dividends, and it is worth taking a little time to upgrade.

Problem description

It's still the old rule. First, go to demo to let you know what we are going to say intuitively.

public class Test {
      static <T extends Number> T getObject() {
              return (T)Long.valueOf(1L);
      }

      public static void main(String... args) throws Exception {
              StringBuilder sb = new StringBuilder();
              sb.append(getObject());
      }
}

demo is very simple. There is a generic function getObject whose return type is Number subclass. Then we pass the function return value to the polymorphic method append of StringBuilder. We know that there are many append methods and many parameter types, but there is no append method whose parameter is Number. If there is one, you should guess that this method will be preferred Now, since there is no one, which one will we choose? We use JDK6 (similar to JDK7) and jdk8 to compile the above classes respectively, and then use javap to see the output results (only see the main method):

Bytecode compiled by jdk6:

public static void main(java.lang.String...) throws java.lang.Exception;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    Code:
      stack=2, locals=2, args_size=1
         0: new           #3                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
         7: astore_1
         8: aload_1
         9: invokestatic  #5                  // Method getObject:()Ljava/lang/Number;
        12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
        15: pop
        16: return
      LineNumberTable:
        line 8: 0
        line 9: 8
        line 10: 16
    Exceptions:
      throws java.lang.Exception
jdk8 Compiled bytecode:

public static void main(java.lang.String...) throws java.lang.Exception;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    Code:
      stack=2, locals=2, args_size=1
         0: new           #3                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
         7: astore_1
         8: aload_1
         9: invokestatic  #5                  // Method getObject:()Ljava/lang/Number;
        12: checkcast     #6                  // class java/lang/CharSequence
        15: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/CharSequence;)Ljava/lang/StringBuilder;
        18: pop
        19: return
      LineNumberTable:
        line 8: 0
        line 9: 8
        line 10: 19
    Exceptions:
      throws java.lang.Exception

Compared with the difference above, we can see that bci has changed since 12. The following line is added in jdk8 to indicate that a type check should be performed on the data at the top of the stack to see if it is CharSequence type:

 12: checkcast     #6                  // class java/lang/CharSequence

In addition, the append method of StringBuilder called is also different. In jdk7, the append method of Object type is called, while in jdk8, the append method of CharSequence type is called.

The main thing is to run the above code under jdk6 and jdk8. It runs normally under jdk6, but directly throws an exception under jdk8:

Exception in thread "main" java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.CharSequence
    at Test.main(Test.java:9)

At this point, the whole problem should be described clearly.

problem analysis

First of all, let's talk about how to implement this function in the java compiler. Others are very clear. The focus is on how to determine which method to use in append as follows:

sb.append(getObject()); We know that getObject() returns a generic Object, which is a subclass of Number. Therefore, we will first traverse all visible methods of StringBuilder, including those inherited from the parent class, to find out whether there is a method called append, and the parameter type is Number. If there is one, use this method directly. If not, we have to think The key to finding the most suitable method is how to define it. For example, we see that there is an append method whose parameter is of Object type and Number is a subclass of Object, so we can choose this method. If there is another append method whose parameter is of Serializable type (of course, there is no such parameter method), numbe R implements this interface, so we can choose this method. Is it more suitable for the Object parameter or the Serializable parameter? What's more, we know that StringBuilder has a method, whose parameter is CharSequence, and the parameter we pass in is actually a subclass of Number, and we also implement the interface of CharSequence. So what are we going to do Do you want to choose it? We need to think about all these problems, and each has its own reasons. It seems reasonable to say so.

Type derivation of generics in JDK6

The Java C code of jdk6 is analyzed here, but the logic for this problem in jdk7 is almost the same. So take this as an example. The generic type derivation in jdk6 is actually relatively simple. From the above output, we can also guess that it is the append method with the parameter of Object type, which is the most appropriate:

private Symbol findMethod(Env<AttrContext> env,
                              Type site,
                              Name name,
                              List<Type> argtypes,
                              List<Type> typeargtypes,
                              Type intype,
                              boolean abstractok,
                              Symbol bestSoFar,
                              boolean allowBoxing,
                              boolean useVarargs,
                              boolean operator) {
        for (Type ct = intype; ct.tag == CLASS; ct = types.supertype(ct)) {
            ClassSymbol c = (ClassSymbol)ct.tsym;
            if ((c.flags() & (ABSTRACT | INTERFACE | ENUM)) == 0)
                abstractok = false;
            for (Scope.Entry e = c.members().lookup(name);
                 e.scope != null;
                 e = e.next()) {
                //- System.out.println(" e " + e.sym);
                if (e.sym.kind == MTH &&
                    (e.sym.flags_field & SYNTHETIC) == 0) {
                    bestSoFar = selectBest(env, site, argtypes, typeargtypes,
                                           e.sym, bestSoFar,
                                           allowBoxing,
                                           useVarargs,
                                           operator);
                }
            }
            //- System.out.println(" - " + bestSoFar);
            if (abstractok) {
                Symbol concrete = methodNotFound;
                if ((bestSoFar.flags() & ABSTRACT) == 0)
                    concrete = bestSoFar;
                for (List<Type> l = types.interfaces(c.type);
                     l.nonEmpty();
                     l = l.tail) {
                    bestSoFar = findMethod(env, site, name, argtypes,
                                           typeargtypes,
                                           l.head, abstractok, bestSoFar,
                                           allowBoxing, useVarargs, operator);
                }
             if (concrete != bestSoFar &&
                    concrete.kind < ERR  && bestSoFar.kind < ERR &&
                    types.isSubSignature(concrete.type, bestSoFar.type))
                    bestSoFar = concrete;
            }
        }
        return bestSoFar;
    }

The above logic is probably to traverse the current class (such as StringBuilder in this example) and its parent class, and then find the most appropriate method to return from their methods, focusing on the method of selectBest:

Symbol selectBest(Env<AttrContext> env,
                      Type site,
                      List<Type> argtypes,
                      List<Type> typeargtypes,
                      Symbol sym,
                      Symbol bestSoFar,
                      boolean allowBoxing,
                      boolean useVarargs,
                      boolean operator) {
        if (sym.kind == ERR) return bestSoFar;
        if (!sym.isInheritedIn(site.tsym, types)) return bestSoFar;
        assert sym.kind < AMBIGUOUS;
        try {
            if (rawInstantiate(env, site, sym, argtypes, typeargtypes,
                               allowBoxing, useVarargs, Warner.noWarnings) == null) {
                // inapplicable
                switch (bestSoFar.kind) {
                case ABSENT_MTH: return wrongMethod.setWrongSym(sym);
                case WRONG_MTH: return wrongMethods;
                default: return bestSoFar;
                }
            }
        } catch (Infer.NoInstanceException ex) {
            switch (bestSoFar.kind) {
            case ABSENT_MTH:
                return wrongMethod.setWrongSym(sym, ex.getDiagnostic());
            case WRONG_MTH:
                return wrongMethods;
            default:
                return bestSoFar;
            }
        }
        if (!isAccessible(env, site, sym)) {
            return (bestSoFar.kind == ABSENT_MTH)
                ? new AccessError(env, site, sym)
                : bestSoFar;
        }
        return (bestSoFar.kind > AMBIGUOUS)
            ? sym
            : mostSpecific(sym, bestSoFar, env, site,
                           allowBoxing && operator, useVarargs);
    }

The main logic of this method lies in the rawinstance method (the specific code will not be pasted, and I will look at the code with interest. I will paste the most critical call method, argumentsAcceptable, which is mainly used for parameter matching). If the current method is also suitable, then make a comparison with the best method selected before to see who is most suitable. This selection process is in the last mo In the stspecific method, it's actually similar to bubble sorting, but it's just to find the closest type (layer by layer searching for the method corresponding to the parent class, which is similar to the minimum common multiple).

    boolean argumentsAcceptable(List<Type> argtypes,
                                List<Type> formals,
                                boolean allowBoxing,
                                boolean useVarargs,
                                Warner warn) {
        Type varargsFormal = useVarargs ? formals.last() : null;
        while (argtypes.nonEmpty() && formals.head != varargsFormal) {
            boolean works = allowBoxing
                ? types.isConvertible(argtypes.head, formals.head, warn)
                : types.isSubtypeUnchecked(argtypes.head, formals.head, warn);
            if (!works) return false;
            argtypes = argtypes.tail;
            formals = formals.tail;
        }
        if (formals.head != varargsFormal) return false; // not enough args
        if (!useVarargs)
            return argtypes.isEmpty();
        Type elt = types.elemtype(varargsFormal);
        while (argtypes.nonEmpty()) {
            if (!types.isConvertible(argtypes.head, elt, warn))
                return false;
            argtypes = argtypes.tail;
        }
        return true;
    }

For specific examples, it is actually to see which method's parameter in StringBuilder is the parent class of Number. If it is not, it means it is not found. If all the parameters meet the expectations, it means it is found, and then it returns.

So the logic in jdk6 is relatively simple.

Type derivation of generics in JDK8

The derivation in jdk8 is relatively complex, but most of the logic is similar to the above, but the argumentsacceptable changes a lot, adding some data structures, making the rules more complex, and considering more scenarios. Because the code nesting level is very deep, I will not paste the specific code, and I will follow the next code if I am interested (the specific changes can be From AbstractMethodCheck.argumentsAcceptable This method starts).

For this demo, if the Object returned by getObject implements both CharSequence and Number subclass, it thinks that in this case, the append method with parameter of CharSequence type is more suitable than the method with parameter of Object type. It seems to be more strict, and its scope of application is narrowed, not to match the large and standard interface method. Therefore, it There is an extra layer of checkcast, but I think it's a little too radical.

Let's study together:

JVM parameters of PerfMa KO series [Memory chapter]

Troubleshooting and analysis of a large number of zombie processes in Docker container

Topics: Programming Java jvm Docker