In the javac source code, why does closure(Type) return a non-empty list for non-class/interface types?

Viewed 34

In the javac source code, there is a method that returns a type closure given a com.sun.tools.javac.code.Type. It is documented to work only for "class or interface type[s]". However it works for many other types as well, by returning a one-element list consisting of the type itself and nothing else, seemingly violating its own documentation:

/**
 * Returns the closure of a class or interface type.
 */
public List<Type> closure(Type t) {
    List<Type> cl = closureCache.get(t);
    if (cl == null) {
        Type st = supertype(t);
        if (!t.isCompound()) { // <line 5>
            if (st.hasTag(CLASS)) {
                cl = insert(closure(st), t);
            } else if (st.hasTag(TYPEVAR)) {
                cl = closure(st).prepend(t);
            } else {
                cl = List.of(t);
            }
        } else {
            cl = closure(supertype(t));
        }
        for (List<Type> l = interfaces(t); l.nonEmpty(); l = l.tail)
            cl = union(cl, closure(l.head));
        closureCache.put(t, cl);
    }
    return cl;
}

(For any not versed in the wonderful world of the javac source code, a List is not a java.util.List, but a linked list structure, and a Type is not a java.lang.reflect.Type. A "compound" Type is basically an IntersectionType (in the language model terminology).)

So at line 5 in the excerpt above, if it is not compound (so let's pretend it's an array or a primitive type or something crazy like an executable type), and it is neither a class nor a type variable, then it is blindly added to the List that will be returned. You'll therefore get back a single element List consisting of the Type itself, if the type is not a class, interface or intersection type, and nothing else.

A concrete example:

Consider the type denoted by java.lang.Integer[]. Clearly it has a supertype (the type denoted by java.lang.Number[]), unlike, say, certain primitive types.

We know that the method itself is defined to work only on class and interface types, but it doesn't check this. Also, immediately above the method in a related section of code, we find:

A closure is a list of all the supertypes and interfaces of a class or interface type, ordered by ClassSymbol.precedes….

According to that documentation, we'd expect an empty list to be returned if we pass an array type in. But instead we get a single-element list whose sole element is the type denoted by java.lang.Integer[] (the very type we pass in).

Why is this?

(I am well aware I am looking at decades-old code and the answer might very well be: no one knows.)

1 Answers

I think returning a singleton list of just t is just convenient for other code that uses this method. It's similar to how using a 0-based index generally makes you do less of that - 1 and + 1 stuff, and so is more favourable.

In many places, code that uses this method aren't just dealing with class or interface types, but all kinds of types. They still want to get its supertypes (which includes the type t itself) if it is a class/interface type, but if it is not, returning an empty list means that the caller doesn't even get to operate on t itself. The caller would need to write extra code to check if the list is empty and other ugly-looking things.

The relevant usages of closure I found are:

  • when calculating the erased super types, as part of the calculation of the least upper bound (lub):

      List<Type> erasedSupertypes(Type t) {
          ListBuffer<Type> buf = new ListBuffer<>();
          for (Type sup : closure(t)) {
              if (sup.hasTag(TYPEVAR)) {
                  buf.append(sup);
              } else {
                  buf.append(erasure(sup));
              }
          }
          return buf.toList();
      }
    

    Returning the singleton list of Integer[] would add Integer[] to the list of erased super types. If it returned an empty list instead, one would have to add an extra isEmpty check and make the code look less clean, otherwise erasedSuperTypes would return an empty list and the lub calculation would go very wrong very quickly.

  • when calculating the greatest lower bound

    public Type glb(Type t, Type s) {
        if (s == null)
            return t;
        else if (t.isPrimitive() || s.isPrimitive())
            return syms.errType;
        else if (isSubtypeNoCapture(t, s))
            return t;
        else if (isSubtypeNoCapture(s, t))
            return s;
    
        List<Type> closure = union(closure(t), closure(s));
        return glbFlattened(closure, t);
    }
    

    If closure returned an empty list, one would need to add 2 isEmpty checks, passing List.of(t) and List.of(s) into union if the corresponding list is empty. Otherwise, glb would be incorrect.

Related