Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Possible bug in sun.reflect.Reflection handling of abstract enums?

Tags:

java

I've identified what is at least undesirable behavior and at most a bug in the Sun JDK's handling of reflection on Java enums with an abstract method. I've searched for a bug report and StackOverflow answer for this particular behavior and come up dry. You're more or less always wrong when you think you've found an issue like this in such well-used and carefully-tested code, so please sanity check me and tell me where I've gotten this wrong.

The Code

Consider the following code:

a/Greeting.java

package a;

public enum Greeting {
    HELLO {
        @Override
        public void greet() {
            System.out.println("Hello!");
       }
    };
    public abstract void greet();
}

b/EnumTest.java

package b;

import java.lang.reflect.Method;

import a.Greeting;

public class EnumTest {
    public static void main(String[] args) throws Exception {
        Greeting g=Greeting.HELLO;
        Method greet=g.getClass().getMethod("greet");

        System.out.println("Greeting "+g.getClass()+" ...");
        greet.invoke(g);
        System.out.println("Greeted!");
    }
}

Also, please note that Greeting and EnumTest are in different packages. (This ends up mattering.)

The Error

When you run this code, you expect to get the following output:

Greeting class a.Greeting ...
Hello!
Greeted!

Instead, you get the following output:

Greeting class a.Greeting$1 ...
Exception in thread "main" java.lang.IllegalAccessException: Class b.EnumTest can not access a member of class a.Greeting$1 with modifiers "public"
    at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:95)
    at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:261)
    at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:253)
    at java.lang.reflect.Method.invoke(Method.java:594)
    at b.EnumTest.main(EnumTest.java:13)

Understanding the Behavior

First, please note that Greeting is public and Greeting$greet is public. (Even the error message indicates public access!) So what's going on?

What The Heck is Going On Here?

If you step through the code, you find that the ultimate "problem" is that sun.reflect.Reflection$verifyMemberAccess() returns false. (So, the Reflection API claims we do not have access to this method.) The particular code that fails is here:

public static boolean verifyMemberAccess(Class currentClass,
                                        // Declaring class of field
                                        // or method
                                        Class  memberClass,
                                        // May be NULL in case of statics
                                        Object target,
                                        int    modifiers)
    // ...

    if (!Modifier.isPublic(getClassAccessFlags(memberClass))) {
       isSameClassPackage = isSameClassPackage(currentClass, memberClass);
       gotIsSameClassPackage = true;
       if (!isSameClassPackage) {
           return false;
       }
    }

    // ...

Essentially, this method determines whether code in currentClass can see members of memberClass with modifiers of modifiers.

Clearly, we should have access. We're calling a public method in a public class! However, this code returns false, in the indicated return statement. Therefore, the class of the value we're trying to invoke the method on is not public. (We know this because the outer test -- !Modifier.isPublic(getClassAccessFlags(memberClass)) -- passes, since the code reaches the inner return.) But Greeting is public!

However, the type of Greeting.HELLO is not a.Greeting. It's a.Greeting$1! (As careful readers will have noticed above.)

enum classes with one or more abstract methods create child classes under the covers (one for each constant). So what's happening is that the "under the covers" child classes are not marked public, so we're not allowed to see public methods on those classes. Bummer.

Confirmation of the Theory

To test this theory, we can invoke the superclass enum's greet() method on the child instead:

public static void main(String[] args) throws Exception {
    Greeting g=Greeting.HELLO;
    Method greet=g.getClass().getSuperclass().getMethod("greet");

    System.out.println("Greeting "+g.getClass()+" ...");
    greet.invoke(g);
    System.out.println("Greeted!");
}

...and meet with success:

Greeting class a.Greeting$1 ...
Hello!
Greeted!

Also, if we move a.Greeting to b.Greeting (the same package as b.EnumTest), that works too, even without the getSuperclass() call.

So... Bug or No?

So... is this a bug? Or is this simply undesired behavior that is an artifact of the underlying implementation? I checked the relevant section of the Java Language Specification and this syntax is legal. Also, the specification doesn't specify how child classes will be arranged, so while this technically in violation of the standard (or at least the part of the standard I read), I'm inclined to call this a bug.

What does StackOverflow think: is this a bug, or simply undesired behavior? I realize this is a bit of an unconventional question, so please forgive the format.

Also, I'm on a Mac (in case that matters), and java -version prints the following, for anyone who wants to reproduce:

$ java -version
java version "1.7.0_21"
Java(TM) SE Runtime Environment (build 1.7.0_21-b12)
Java HotSpot(TM) 64-Bit Server VM (build 23.21-b01, mixed mode)

EDIT: Interesting to find a bug open for an similar (at least related) issue since 1997: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4071957

EDIT: Per the answer below, the JLS does say that enum classes with an abstract method shall behave like anonymous classes:

The optional class body of an enum constant implicitly defines an anonymous class declaration (§15.9.5) that extends the immediately enclosing enum type. The class body is governed by the usual rules of anonymous classes

Per the bug above, anonymous class handling has been a "bug" since 1997. So with respect to whether this is actually a bug or not is a bit semantic at this point. Bottom line: don't do this, since it doesn't work and it's not likely to in the future. :)

like image 979
sigpwned Avatar asked Nov 19 '13 02:11

sigpwned


1 Answers

Not a bug.

As a careful examination of the exception message shows, the problem class is a.Greeting$1. That's an anonymous inner class. The method happens to be public and irrelevantly the enclosing class and the static field it is assigned to are public, but the actual class is non-public.

a.Greeting.class and a.Greeting.HELLO.getClass().getSuperclass() should work.

Therefore, the class of the value we're trying to invoke the method on is not public.

Class.getMethod operates on a class (Class) not a value, so that's irrelevant (unless you were trying to get the Field a.Greeting.HELLO.

EDIT: From the Java Language Specification:

"The optional class body of an enum constant implicitly defines an anonymous class declaration (§15.9.5) that extends the immediately enclosing enum type. The class body is governed by the usual rules of anonymous classes; ..."

So the enum is treated as an anonymous class, and this is how anonymous classes work.

like image 72
Tom Hawtin - tackline Avatar answered Nov 10 '22 00:11

Tom Hawtin - tackline