public String toString(boolean showRedirect) {
   if (isArray()) {
     return componentType.toString(showRedirect) + "[]";
   }
   String ret = getName();
   if (placeholder) ret = getUnresolvedName();
   if (!placeholder && genericsTypes != null) {
     ret += " <";
     for (int i = 0; i < genericsTypes.length; i++) {
       if (i != 0) ret += ", ";
       GenericsType genericsType = genericsTypes[i];
       ret += genericTypeAsString(genericsType, showRedirect);
     }
     ret += ">";
   }
   if (redirect != null && showRedirect) {
     ret += " -> " + redirect().toString();
   }
   return ret;
 }
 /**
  * This exists to avoid a recursive definition of toString. The default toString in GenericsType
  * calls ClassNode.toString(), which calls GenericsType.toString(), etc.
  *
  * @param genericsType
  * @param showRedirect
  * @return the string representing the generic type
  */
 private String genericTypeAsString(GenericsType genericsType, boolean showRedirect) {
   String ret = genericsType.getName();
   if (genericsType.getUpperBounds() != null) {
     ret += " extends ";
     for (int i = 0; i < genericsType.getUpperBounds().length; i++) {
       ClassNode classNode = genericsType.getUpperBounds()[i];
       if (classNode.equals(this)) {
         ret += classNode.getName();
       } else {
         ret += classNode.toString(showRedirect);
       }
       if (i + 1 < genericsType.getUpperBounds().length) ret += " & ";
     }
   } else if (genericsType.getLowerBound() != null) {
     ClassNode classNode = genericsType.getLowerBound();
     if (classNode.equals(this)) {
       ret += " super " + classNode.getName();
     } else {
       ret += " super " + classNode;
     }
   }
   return ret;
 }