Java – “method ___() in ___ is defined in inaccessible class or interface” compilation error

compiler-errorsjava

I found a strange compilation restriction which I cannot explain, and I don't understand this restriction's reason.

Example-1:

Consider these classes:

In package e1;:

public class C1 {
    enum E1 { A, B, C }
    public E1 x;
}

In package e2;:

import e1.C1;
public class C2 {
    public String test(C1 c1) {
        return c1.x.toString();    // here compilation error
    } 
}

This causes the following compilation error:

Error:(5,20) java: toString() in java.lang.Enum is defined in an inaccessible class or interface

Example-2:

Consider these classes:

In package i1;:

public interface I1 {
    int someMethod();
}

public class C1 {
    static class I2 implements I1 {
        public int someMethod() {
            return 1;
        }
    }
    public I2 x = new I2();
}

In package i2;:

import i1.C1;
import i1.I1;
public class C2 {
    public static void main(String[] args) {
        C1 c1 = new C1();
        System.out.println(c1.x.someMethod());  // compilation error
    }
}

This also causes the same compilation error, but if we change the offending line to:

System.out.println(((I1)c1.x).someMethod());

Then this can be compiled and works fine.


So, the question is:

Why is this restriction of accessibility needed?

Yes, I understand that classes C1.E in example-1) and C1.I2 in example-2) are package private. But at the same time it's clear that nobody could assign weaker access privileges to methods of a base interface (I1 of Object), so it will be always safe to make direct casting of object to its base interface and get access to restricted method.

Could somebody explain the purposes and the reason of this restriction?

UPDATE: assylias pointed out JLS §6.6.1:

A member (class, interface, field, or method) of a reference (class, interface, or array) type or a constructor of a class type is accessible only if the type is accessible…

Looks like this is the restriction, but it doesn't explain why this restriction (in the cases of the above mentioned examples) is needed…

Best Answer

For invocation of instance methods invokevirtual instruction is used. To invoke this method class must have a resolved reference to this method

From invokevirtual specification:

Linking Exceptions

During resolution of the symbolic reference to the method, any of the exceptions pertaining to method resolution (§5.4.3.3) can be thrown.

5.4.3.3. Method Resolution:

To resolve an unresolved symbolic reference from D to a method in a class C, the symbolic reference to C given by the method reference is first resolved (§5.4.3.1).

5.4.3.1. Class and Interface Resolution:

If C is not accessible (§5.4.4) to D, class or interface resolution throws an IllegalAccessError.

5.4.4. Access Control:

A class or interface C is accessible to a class or interface D if and only if either of the following conditions is true:

  • C is public.

  • C and D are members of the same run-time package (§5.3).

C and D are not from the same package. So even if java compiles this code for you, it will throw an IllegalAccessError during invocation. Compiler is clever enough to prevent such obvious errors. These restrictions are coming from requirements of java's class resolution process.

To invoke instance method JVM requires two things: reference to the object and description of the object(class or interface). Description is accessed through resolution process. If it fails, invocation fails.

If an error occurs during resolution of a symbolic reference, then an instance of IncompatibleClassChangeError (or a subclass) must be thrown at a point in the program that (directly or indirectly) uses the symbolic reference.

In your case C2 has access to I1. So interface invocation works well. But C2 does not have access to class I2. And that's why IllegalAccessError could be thrown at runtime if this code compiles.

How to reproduce IllegalAccessError:

  1. Make inner class public and compile all in IDE for example
  2. Make inner class package private and compile just it using javac from command line.
  3. Replace IDE generated classes with cmd generated
  4. And you'll see something like:

Exception in thread "main" java.lang.IllegalAccessError: tried to access class qq.Test1$I2 from class Test at Test.main(Test.java:30)