Notes on generification of the JMX API eamonn.mcmanus@sun.com October 2004 Updated: March 2006 The JMX API could not be generified in J2SE 5 due to compatability constraints with J2EE 1.4. These constraints no longer apply in J2SE 6, and the API should be generified for usability and better typesafeness. This document describes the design choices that were made and their rationale. * COLLECTIONS Most of the generification consists of application of the generified collections API (java.util.List etc). So for example where MBeanServer.queryNames previously returned just Set, it now returns Set. In most cases this generification is obvious. - AttributeList, RoleList, RoleUnresolvedList The classes javax.management.AttributeList, javax.management.relation.RoleList, and javax.management.RoleUnresolvedList present a problem. Logically, AttributeList, for example, would now extend ArrayList rather than just ArrayList as at present. The problem is that this class already has the following two methods: void add(Attribute object) void set(int index, Attribute object) If the parent class becomes ArrayList, then the return types of these two methods clash with the methods inherited from ArrayList: boolean add(Attribute object) Attribute set(int index, Attribute object) So this change is not possible. The same remarks apply to RoleList and RoleUnresolvedList. AttributeList instances can be both supplied to and returned from the JMX API (see javax.management.MBeanServer.getAttributes and .setAttributes). Type-safeness would be improved if an AttributeList were a List, but that is not possible as just explained. So the suggestion is to add a new constructor to convert from a List to an AttributeList: AttributeList(List list) and an instance method to convert back from an AttributeList to a List: List asList() As long as generic types are not reified, asList() can simply return "this". However, the API is careful not to guarantee this. The name asList() is deliberately short, so that the idiom AttributeList al = ...; for (Attribute a : al.asList()) is as convenient as possible. There is a difference between the two directions. Changes to the List returned by asList() are reflected in the original AttributeList. This is not the case for an AttributeList object constructed with the (List) constructor. By convention, such a constructor is a "copy constructor". The asList() method is also added to RoleList and RoleUnresolvedList. Those classes already had a (List) constructor so this constructor is just generified in the obvious way. Because it is strongly recommended not to include generic methods in "raw" classes, AttributeList etc extend ArrayList. - CompositeData, CompositeDataSupport The interface javax.management.openmbean.CompositeData and its implementation CompositeDataSupport implement a kind of Map where the keys are strings and the values are individually of types controlled by an associated instance of the class CompositeType. CompositeDataSupport has a constructor like this: public CompositeDataSupport(CompositeType compositeType, Map items) A Map is used rather than a Map, because it would not be wrong to pass e.g. a Map here. As a consequence, the method public Collection values() which will return the values that were supplied to the "items" parameter of the constructor has type Collection rather than Collection. The returned value is explicitly described in the API text as unmodifiable so this also gives better compile-time checking. Additionally, a subclass could override this method with a more specific return type like Collection. - TabularData, TabularDataSupport The interface javax.management.openmbean.TabularData and its implementation TabularDataSupport are somewhat quirky. TabularDataSupport implements java.util.Map in what with hindsight was a mistaken attempt to fit into the collections framework. TabularData does not extend Map, however. TabularDataSupport behaves like a Map, CompositeData>. Currently, TabularDataSupport implements plain Map, and in fact we cannot change it to implement Map, CompositeData>. Doing so would change the return type of e.g. get(Object) from Object to CompositeData, which would break any existing subclasses that override "Object get(Object)". We *can* change TabularDataSupport.keySet() so that it returns Set> rather than just Set, and it is worth doing so because it is not very obvious what the contents of the plain Set are. Similarly, we can change values() to return Collection and we can change entrySet() to return Set, CompositeData>>. In fact, it is slightly preferable to use List rather than List in these cases. This means that an attempt by the caller to add or replace an element in the List will be rejected by the compiler. Such an attempt would fail at run-time. A problem arises with the interface TabularData. Somone could have implemented that interface with an implementation different from TabularDataSupport. Potentially the keySet() of that implementation could be different; for example it could be Set rather than Set>. On the other hand, the interest of separating interface from implementation is considerably diminished if we cannot know what methods in the interface will return. Since it is unlikely that anyone has in fact reimplemented TabularData, and even more unlikely that they have done so in a way that is inconsistent with TabularDataSupport, we could clarify the spec to say that keySet() does indeed return Set> (or rather Set>), and that values() returns Collection. That change is tracked by bug 5100192 and it will be proposed to the JSR 255 Expert Group, separately from the rest of the generifcation effort. - MLet The following methods in javax.management.loading.MLet: public Set getMBeansFromURL(String url) public Set getMBeansFromURL(URL url) are specified to return a Set where each element is either an ObjectInstance (if an MBean entry in the MLet file was successfully created) or a Throwable (if not). This is not a great design. Nevertheless, it is already there so we have to live with it. The methods become: public Set getMBeansFromURL(String url) public Set getMBeansFromURL(URL url) We could use Set rather than Set, but there seems to be little gain from doing so. The API would be slightly harder to read for people unfamiliar with the syntax, and callers would be unable to modify the returned Set in ways that they can (and possibly do) today. Although Set would allow a subclass to supply a more specific Set (e.g. Set) it is hard to imagine when this would be useful. Besides, Set implies a homogeneous set, which this is not. These methods are in fact specified in the interface MLetMBean, which MLet implements. The changes are made there too. * OTHER GENERIC CLASSES The class javax.management.openmbean.OpenType acquires a type parameter . This class and its subclasses represent the predefined set of types that can be used in an Open MBean. The new type parameter specifies the corresponding Java type. The immediate use of this parameterization is that the constructors of OpenMBeanAttributeInfoSupport and OpenMBeanParameterInfoSupport can check their parameters for consistency. For example, if an attribute is defined as being of type SimpleType.STRING then a default value specified for it must be a String. We also expect that the parameterization will be useful to code that is dealing with Open MBeans generically (e.g., the MXBean framework). Thus, for example, the basic types specified by instances of the SimpleType class all have a fixed Java type parameter, e.g.: public class SimpleType extends OpenType { public static final SimpleType STRING = ...; public static final SimpleType DATE = ...; } The Java instances associated with CompositeType are always CompositeData, and likewise for TabularType and TabularData, so we have: public class CompositeType extends OpenType public class TabularType extends OpenType The situation is less straightforward for ArrayType. There is unfortunately no way to allow the compiler to deduce the value of a type parameter to a class based on a constructor invocation. We might have liked the class to look like this: public class ArrayType extends OpenType { public ArrayType(OpenType elementType); public OpenType getElementOpenType(); } Then you would be able to write, e.g., ArrayType stringArrayType = new ArrayType(SimpleType.STRING); Notice the redundancy: the compiler knows that SimpleType.STRING is a SimpleType, so E is String, but still requires you to supply the parameter to the constructor. Furthermore, the constructor does not look like the above, since it also has a parameter indicating the number of dimensions. A variable number of dimensions obviously can't be handled by the generics framework. The above constraints could be lifted by adding a static method like this: public class ArrayType extends OpenType { public static ArrayType getInstance(OpenType elementType); } Now the above example would look like this: ArrayType stringArrayType = ArrayType.getInstance(SimpleType.STRING); Multidimensional arrays can be built up by calling this method as many times as necessary. However, we have an RFE (5045358) to allow arrays of primitive type to be used in Open MBeans. Currently, an array of integers can only be expressed using new ArrayType(SimpleType.INTEGER) which implies a Java type of Integer[]. We would like it to be possible to ask for an int[]. But if the type parameter to OpenType is to be then ArrayType cannot be declared as above. The proposal is therefore for ArrayType to look like this: public class ArrayType extends OpenType { // T must be an array type, but we can't express this public ArrayType(int dimension, OpenType elementType); // The existing constructor, e.g.: // ArrayType stringArrayType = // new ArrayType(1, SimpleType.STRING) public static ArrayType getInstance(OpenType elementType); // Factory method, e.g.: // ArrayType stringArrayType = // ArrayType.getInstance(SimpleType.STRING) public static ArrayType primitiveArray(Class arrayClass); // Factory method for int[] etc, e.g.: // ArrayType intArray = // ArrayType.primitiveArray(int[].class) // Checks at run-time that arrayClass is a primitive array // class such as int[].class } In the initial generification being proposed here, only the generified constructor is included. This approach unfortunately does not allow us to declare a type-safe return value for getElementOpenType(). * GENERIC CONSTRUCTORS AND METHODS - OpenMBeanAttributeInfoSupport, OpenMBeanParameterInfoSupport As mentioned above, some of the constructors of OpenMBeanAttributeInfo and OpenMBeanParameterInfo have a type parameter that ensures that default and legal values are consistent with the Open Type of the attribute or parameter. The constructors that have minValue and maxValue parameters now declare them as Comparable rather than plain Comparable. There is nothing to be gained by declaring Comparable consistently with e.g. java.util.Collections. The possible values of T are predefined, and these constructors check that the minValue and maxValue are in fact instances of T or a subclass (via OpenType.isValue). For Comparable to be useful, we would therefore have to have a parameter of a type that is a subclass of T and that implements Comparable where U is a proper supertype of T. This is not possible where T already implements Comparable, which is the case for all the SimpleType values except Character (which is final) and ObjectName (which is planned to implement Comparable). It is also not possible when T is an array type. So this leaves T being CompositeData or TabularData. Both of these have Object as their only supertype, so for Comparable to be useful we would have to have a minValue that implements CompositeData or TabularData and also Comparable. This seems so implausible as not to justify the extra effort for developers to understand as opposed to plain . We considered but rejected making these classes themselves generic rather than their constructors, i.e. moving the parameter to the class. Doing so would get a more precise return type for the methods getDefaultValue(), getLegalValues(), getMaxValue(), getMinValue(), getOpenType(); but it would also mean that callers of the constructor would have to supply an explicit type parameter in order to get the type checking that they get for free when it is the constructor rather than the class that is generic. E.g. they would have to write: new OpenMBeanAttributeInfoSupport(..., SimpleType.INTEGER, ..., defaultIntValue); instead of just: new OpenMBeanAttributeInfoSupport(..., SimpleType.INTEGER, ..., defaultIntValue); - javax.management.StandardMBean The StandardMBean class is used in two ways. It can be used to make an MBean from an MBean interface such as WhatsitMBean and an object that implements that interface, as follows: new StandardMBean(whatsitObject, WhatsitMBean.class) Or, it can be used as the parent class for MBean classes, basically replacing "whatsitObject" with "this" in the above: public class WhatsitImpl extends StandardMBean implements WhatsitMBean { public WhatsitImpl() { super(WhatsitMBean.class); } } Two choices were considered for generification. Either the StandardMBean class itself could be given a type parameter that is the MBean interface (WhatsitMBean in the example). Or, the two-argument constructor could be a given a type parameter that again is the MBean interface. If the class is generified, the two-argument constructor must be invoked as follows to avoid warnings: new StandardMBean(whatsitObject, WhatsitMBean.class) Subclasses must look like this: public class WhatsitImpl extends StandardMBean implements WhatsitMBean { // remainder as before } The advantage of this approach is that the following methods of StandardMBean are more type-safe: public T getImplementation(); public void setImplementation(T implementation); public Class getMBeanInterface(); The main disadvantage is that a type parameter must be explicitly supplied to the two-argument constructor. This could be alleviated by adding a static factory method: public static StandardMBean newInstance(T impl, Class intf) { return new StandardMBean(impl, intf); } Now an instance can be constructed using StandardMBean.newInstance(whatsitObject, WhatsitMBean.class) On the other hand, if only the two-argument constructor is generified then there is no need to add this factory method, since we have: public StandardMBean(T impl, Class intf); with invocation: new StandardMBean(whatsitObject, WhatsitMBean.class) unchanged from existing usage. This constructor still allows the compiler to check that the implementation object does indeed implement the given MBean interface. The main drawback is that it does not allow the compiler to check that an object subsequently given to the setImplementation method also meets this constraint. The fact that a subclass must implement the MBean interface it gives to the one-argument constructor (e.g. WhatsitImpl implementing WhatsitMBean above) cannot be expressed using generics, regardless of whether the type parameter is on the class or on the constructor. The current proposal is to generify the constructor, not the class, which means that existing code does not have to be rewritten to avoid warnings. Much more existing code uses these constructors than uses getImplementation. In the future, we might like to relax the constraint that the first argument to the two-argument constructor must implement the interface named by the second argument. It would be sufficient for it to have all the methods in the interface, without necessarily implementing the interface. (This would allow a StandardMBean to be wrapped around an instance of an existing class to expose some of its methods for management, without requiring the class to be modified to implement an interface.) If this functionality is added, it can be made available through a new constructor or factory method. - javax.management.MBeanServerInvocationHandler The signature of the method MBeanServerInvocationHandler.newProxyInstance is changed in the obvious way in order to declare that the returned value is an instance of the named MBean interface. For example, in the existing code: WhatsitMBean proxy = (WhatsitMBean) MBeanServerInvocationHandler.newProxyInstance(mbeanServer, objectName, WhatsitMBean.class, false); the cast to WhatsitMBean will no longer be necessary. * MISCELLANEOUS Everywhere in the API where a parameter or return value was Class, it has been replaced by Class. This is consistent with the remainder of the J2SE API. It means for example that the result of MLet.loadClass can be passed to a method that is expecting a Class without producing a warning: public class Whatsit { public static T newInstance(Class c) throws ... { return c.newInstance(); } } MLet mlet = ...; Object o = Whatsit.newInstance(mlet.loadClass("Thingy")); This would produce a compiler warning if MLet.loadClass returned plain Class instead of Class.