oracle/graal · error · IncompatibleClassChangeError

cannot invokeinterface

Error message

cannot invokeinterface %s

What it means

When this custom resolver emulates invokeinterface linkage, it applies the JVM rule that the resolved method must be public and declared in an interface (or java.lang.Object); otherwise it throws IncompatibleClassChangeError('cannot invokeinterface ...'), reproducing the check HotSpot performs at link time.

Solutions

  1. Recompile the caller against the interface version actually present at runtime.
  2. If you generate bytecode, emit the correct opcode: INVOKESTATIC for static, INVOKESPECIAL for private interface methods, INVOKEVIRTUAL/INVOKESPECIAL for default-method calls.
  3. Align dependency versions so the compiled hierarchy matches the runtime hierarchy.

Example fix

// ASM bytecode generation
// before
mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "Iface", "staticHelper", "()V", true); // static method -> error

// after
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "Iface", "staticHelper", "()V", false);
Defensive patterns

Strategy: try-catch

Validate before calling

// Before linking, check the invokeinterface contract with reflection
static void checkInvokeInterface(Class<?> iface, String name, Class<?>... params) throws NoSuchMethodException {
    java.lang.reflect.Method m = iface.getMethod(name, params); // getMethod => public only
    if (!iface.isInterface() && !m.getDeclaringClass().equals(Object.class)) {
        throw new IncompatibleClassChangeError("cannot invokeinterface " + m);
    }
}

Try / catch

try {
    JavaMethod m = pool.lookupMethod(index, opcode);
} catch (IncompatibleClassChangeError e) {
    // Recompile the caller against the current interface version, or fix the opcode; not retryable
    throw new IllegalStateException("Interface/class hierarchy mismatch at link time", e);
}

Prevention

When it happens

Trigger: invokeinterface resolves to a non-public method (e.g. a private interface method, which since JDK 9 must be invoked with invokespecial) or to a method whose declaring class is a class, not an interface (hierarchy changed between compilation and runtime).

Common situations: Stale jars on the classpath where a method moved between class and interface; bytecode generators (ASM) emitting INVOKEINTERFACE for static/private interface methods; compiling against one interface version and running against another.

Related errors


AI-assisted analysis of oracle/graal@a66e9ccd1d (2026-08-14). Data as JSON: /api/errors/4f56bd711013e659. Report an issue: GitHub.

Appendix: source

Thrown at compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/replacements/classfile/ClassfileConstant.java:148

        ExecutableRef(byte tag, DataInputStream stream) throws IOException {
            super(tag, stream);
        }

        ResolvedJavaMethod resolve(ClassfileConstantPool cp, int opcode) {
            if (method == null) {
                ResolvedJavaType cls = cp.get(ClassRef.class, classIndex).resolve(cp);
                NameAndType nameAndType = cp.get(NameAndType.class, nameAndTypeIndex);
                String name = nameAndType.getName(cp);
                String type = nameAndType.getType(cp);

                if (opcode == Bytecodes.INVOKEINTERFACE) {
                    method = resolveMethod(cp.context, cls, name, type, false);
                    if (method == null) {
                        throw new NoSuchMethodError(cls.toJavaName() + "." + name + type);
                    }
                    if (!method.isPublic() || !(method.getDeclaringClass().isInterface() || method.getDeclaringClass().isJavaLangObject())) {
                        throw new IncompatibleClassChangeError("cannot invokeinterface " + method.format("%H.%n(%P)%R"));
                    }
                } else if (opcode == Bytecodes.INVOKEVIRTUAL || opcode == Bytecodes.INVOKESPECIAL) {
                    method = resolveMethod(cp.context, cls, name, type, false);
                    if (method == null) {
                        throw new NoSuchMethodError(cls.toJavaName() + "." + name + type);
                    }
                } else {
                    assert opcode == Bytecodes.INVOKESTATIC : Assertions.errorMessage(opcode, cp);
                    method = resolveMethod(cp.context, cls, name, type, true);
                    if (method == null) {
                        throw new NoSuchMethodError(cls.toJavaName() + "." + name + type);
                    }
                }
            }
            return method;
        }
    }

View on GitHub (pinned to a66e9ccd1d)