The Source for Java Technology Collaboration
User: Password:
Register | Login help    

Search

Online Books:
java.net on MarkMail:


Notes on unspecified behaviour, with a contentious aside about astronomy

Posted by emcmanus on November 10, 2006 at 8:09 AM PST

An inadvertent change in JDK 6 means that MBean attributes and operations no longer appear in the order they were in a Standard MBean interface. I wanted to fix this, but now I'm not so sure.

Here's the background. Suppose you have a Standard MBean with this interface:

public interface PlanetMBean {
    public int getMercury();

    public String getVenus();
    public void setVenus(String x);

    public BigInteger getEarth();
    public void setEarth(BigInteger x);

    public boolean isMars();

    public double getJupiter();

    public byte getSaturn();

    public short getUranus();
    public void setUranus(short x);

    public long getNeptune();

    public void neptune();
    public void uranus(int x);
    public int saturn(int x, int y);
    public short jupiter(int x, long y, double z);
    public void mars(boolean x);
    public BigInteger earth();
    public String venus();
    public int mercury();
}

As you can see, it defines a set of attributes and operations with random types, but names in a very definite and familiar order.

If we create a Standard MBean like this, using JDK 5.0, then it will look like this in JConsole.

Attributes and operations appear in the same order as in the interface

However, if we run the same program on JDK 6, then it looks like this:

Attributes and operations appear in alphabetical order

Observe that the planets are no longer in astronomical order, but in alphabetical order.

Aside: what about Pluto? You might have noticed that I omitted Pluto from my list. That's because Pluto is not a Planet. A rare victory in the ongoing and hopeless war against Stupidism was won earlier this year when the International Astronomical Union voted to recognize logic and common sense and ignore inertia, nostalgia, and welearneditinschoolsoitmustbetrueforeveria. More details than anyone could possibly want to know are in Wikipedia. Beyond just astronomical reality, computer scientists will surely be happier that there are eight planets, divided into four big ones and four small ones, and with each group of four further divisible into two big and two small. Or maybe that's just me.

It turns out that this reordering is an unintended side-effect of my rewrite of the MBean Server internals. I had intended to preserve the order from the MBean interface, but in a moment of distraction I used a TreeMap instead of a LinkedHashMap.

Nothing in the JMX specification requires that attributes and operations in a Standard MBean appear in the same order in the MBeanInfo as they do in the MBean interface, so none of our automated tests check that. We only discovered it recently, as a side-effect of some unrelated tests being coded by one of our Quality Engineers, Sandra Lions. Sandra logged bug 6486655 to track this.

This looks like something we would obviously want to fix, right? Perhaps you thought carefully about the order you want to present your attributes in, and followed that order in your MBean interface. Perhaps you want to group related operations together, for example addThingy() followed immediately by removeThingy() (example due to Daniel Fuchs).

So, since I broke it, I started the process of fixing it. The fix itself is completely trivial - just replace two TreeMaps with LinkedHashMaps. But then things got interesting.

The JDK has a clearly-defined engineering process. One rule in that process is that every time we fix a bug, there must be a regression test that fails without the fix and passes with the fix.

So I duly coded a regression test. It was pretty much what you would expect. In outline:

public class MethodOrderTest {
    public static interface PlanetMBean {
    	...same as above...
    }

    private static final String[] planets = {
    	"mercury", "venus", "earth", "mars", "jupiter", "saturn",
    	"uranus", "neptune"
    };

    public static void main(String[] args) throws Exception {
    	...create a Standard MBean using the PlanetMBean interface...
    	MBeanInfo mbi = ...the MBeanInfo of this MBean...;
    	MBeanAttributeInfo[] mbais = mbi.getAttributes();
    	...check that mbais[] is in the same order as planets[]...
    	MBeanOperationInfo[] mbois = mbi.getOperations();
    	...check that mbois[] is in the opposite order as planets[]...
    }
}

After fixing the usual stupid mistakes in the test, the attribute check passed. But the operation check failed! What was different about operations? I went back and looked at the introspection code but could see nothing.

What's more, the order the operations were appearing in was surprising. I would have understood if they were in alphabetical order. That would have meant that something else besides the TreeMaps I had removed was causing them to be sorted.

But the operations were in astronomical order, from Mercury to Neptune, even though the interface declared them in reverse astronomical order, from Neptune to Mercury. What could be reversing them?

I won't get into the details of the painstaking investigation that followed. Suffice it to say that I ended up boiling the test failure down to this program:

public class MethodOrderTest {
    public static interface PlanetMBean {
    	...same as above...
    }

    private static final String[] planets = {
    	"mercury", "venus", "earth", "mars", "jupiter", "saturn",
    	"uranus", "neptune"
    };

    public static void main(String[] args) throws Exception {
    	System.out.println(Arrays.asList(PlanetMBean.class.getMethods()));
    }
}

Notice that there's no JMX stuff at all here. It's just a call to the Reflection API. And it shows the methods in the following order:

[public abstract int MethodOrderTest$PlanetMBean.mercury(),
public abstract java.lang.String MethodOrderTest$PlanetMBean.venus(),
public abstract java.math.BigInteger MethodOrderTest$PlanetMBean.earth(),
public abstract void MethodOrderTest$PlanetMBean.mars(boolean),
public abstract short MethodOrderTest$PlanetMBean.jupiter(int,long,double),
public abstract int MethodOrderTest$PlanetMBean.saturn(int,int),
public abstract void MethodOrderTest$PlanetMBean.uranus(int),
public abstract void MethodOrderTest$PlanetMBean.neptune(),
public abstract int MethodOrderTest$PlanetMBean.getMercury(),
public abstract java.lang.String MethodOrderTest$PlanetMBean.getVenus(),
public abstract void MethodOrderTest$PlanetMBean.setVenus(java.lang.String),
public abstract java.math.BigInteger MethodOrderTest$PlanetMBean.getEarth(),
public abstract void MethodOrderTest$PlanetMBean.setEarth(java.math.BigInteger),
public abstract boolean MethodOrderTest$PlanetMBean.isMars(),
public abstract double MethodOrderTest$PlanetMBean.getJupiter(),
public abstract byte MethodOrderTest$PlanetMBean.getSaturn(),
public abstract short MethodOrderTest$PlanetMBean.getUranus(),
public abstract void MethodOrderTest$PlanetMBean.setUranus(short),
public abstract long MethodOrderTest$PlanetMBean.getNeptune()]

OK. So the getters and setters appear in the same order as in the interface, but the other methods have been reordered and appear earlier. Does reflection treat getters and setters differently from other methods? What on earth...what the devil is going on?

The next, surprising step is to comment out the definition of the planets[] array. Lo and behold, the methods are now printed out in the same order as they appear in the interface!

Further investigation shows that if I include just the strings "mercury", "venus", "earth" in the planets[] array, then the three methods mercury, venus, earth appear first in the list, and the remaining methods appear in the same order as in the interface.

If we go back and look at the documentation for Class.getMethods(), we learn that "the elements in the array returned are not sorted and are not in any particular order". I had read that text long ago, but I had also observed that in practice the elements were in the order that the methods were declared in the class. So I wasn't too uneasy about assuming they would be.

But the investigation above shows that this assumption is false. In fact, the order of methods is the order in which the JVM first encountered the method name strings. If your interface has methods called mercury and venus, then that will often be the first time the JVM is seeing those strings, especially in small test programs trying to determine how Class.getMethods() behaves. So the Method[] array will have the methods in the same order as in the interface. But if you have previously loaded another class that also has those method names, or even that happens to contain those names as string constants, then the order will be determined by that other class and could be completely different!

The implications are wide-ranging. In particular, JUnit runs the various testFoo() methods in a class in the order that they show up in Class.getMethods(). It's good practice to write tests so that they work no matter what order they are run in - in fact it's vital because you don't know what that order will be.

It turns out that this surprising order is recorded if you look very hard. For example bug 4789197 includes the text "In the HotSpot VM, the order is actually dependent on the order in which the VM's UTF-8 constants are created, i.e., order will be different if a program loads other classes which uses the same name-type id's in random order."

Anyway, now I'm faced with a dilemma. I'd quite like to restore the previous behaviour of Standard MBeans, where attributes and operations show up in the same order as the methods in the MBean interface. Code that depends on this is surely broken, but code like JConsole that presents the attributes and operations in a user interface would surely be better served by this arrangement. Except that now I know that that order can be perturbed by unrelated classes that happen to be loaded earlier. Is it still worth trying to preserve this fragile order? I'd be interested in what people think.

Related Topics >> Open JDK      
Comments
Comments are listed in date ascending order (oldest first)