oracle/graal · error · IncompatibleClassChangeError

cannot invokeinterface %s

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.

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)

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.