JavaBeans

Elisha Peterson

Abstract: This describes "JavaBeans" and how to use them. A Java "bean" is essentially a class that adopts a kind of contract regarding the names and nature of its methods. This contract allows the class to be easily handled when it comes to adjusting its properties dynamically or through property sheets, serializing (i.e. saving the class to a file and re-loading it), event handling, and many other situations. See also this official guide.


Properties are single data values that have get and set methods. Beans contain several properties. The JavaBeans specification consists of design patterns that allow beans to be made easily portable. While a single property might use ChangeEvents to notify listeners when a value has changed, a bean may use PropertyChangeEvents to notify listeners of changes to a specific property. This event contains additional information about the old and new values of a property.

The basic requirements for a Java "bean" are:

  • must be a public class
  • must implement the Serializable interface;
  • must have a public no-argument constructor.

Introspection allows properties and event handling routines that are specified by particular code templates to be "discovered" by external classes. In particular, the Netbeans GUI builder can "detect" the bean properties and allow for customization. For properties of the standard types (boolean, short, int, long, float, double, String, Color), property editing is automatically supported within the GUI's property sheet. The code generated by the builder first calls the no-argument constructor, and then calls various set methods for the properties that have been customized.

Code Patterns

Besides the no-argument constructor, JavaBeans use several other code patterns.

Properties

To allow introspection, properties should conform to get/set patterns:

public C getXyz();
public void setXyz(C newValue);

where C is the class type and Xyz is the name of the property, e.g. "Value". The lowercase version "value" is the official property identifier. Note: for boolean properties, the getter may take the form public boolean isXyz();.

Indexed properties, typically arrays, support indexed get/set patterns also:

C[] getXyz()
void setXyz(C[] array)
C getXyz(int index)
void setXyz(int index, C element)

Events

Similarly, event handling should conform to add/remove patterns:

public void addXyzListener(XyzListener l)
public void removeXyzListener(XyzListener l)

where Xyz is replaced by the type of listener… e.g. ChangeListener or ActionListener.

Bound Properties

Bound properties fire PropertyChangeEvents when their value changes. This event includes (i) the String name of the property, (ii) the old value, and (iii) the new value. Any registered PropertyChangeListeners may respond to the change accordingly. The java.beans.PropertyChangeSupport class contains the necessary event handling. A typical set method differs for bound properties, as in the following example:

public void setTitle( String title )
{
    if ( ! this.title.equals(title) ) {
        String old = this.title;
        this.title = title;
        firePropertyChange( "title", old, title );
    }
}

Constrained Properties

Constrained properties fire VetoableChangeEvents when their value changes.

Persistence

Persistence refers to the ability to save and reload classes. JavaBean code patterns allow for automated persistence through the use of the XMLEncoder class. For full details, see http://java.sun.com/products/jfc/tsc/articles/persistence4/.

Automatic Persistence

The underlying procedure in saving and reloading is using the existing properties of a class, as defined via the above bean patterns, to store the class. Only attributes that are accessible through properties are stored, so the properties MUST reflect the entire state of the object. In summary, for automatic persistence to work:

  • The class must have a no argument constructor.
  • The entire state of the class must be reflected in its properties (get/set methods).

Here is an example of a class that uses bean patterns to store the file, together with the output:

CODE
/** This is the class definition for the object we wish to save. */
public class Dog {
    String name;                        // the state variables
    float weight;
 
    public Dog() { this("Spot", 20); }            // the public no-argument constructor (required)
    public Dog(String name, float weight) { this.name = name; this.weight = weight; }
 
                                    // the bean properties
    public void setName(String name) { this.name = name; }
    public String getName() { return name; }
    public void setWeight(float weight) { this.weight = weight; }
    public float getWeight() { return weight; }
}
 
/** This is how to create and use the XMLEncoder */
public static void main(String[] args) {
    // create an object to save
    Dog dog = new Dog();
    dog.name="Spotty";
 
    // create the encoder and print the object
    XMLEncoder e = new XMLEncoder(System.out);
    e.writeObject(dog);
    e.close();
}
OUTPUT
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.5.0_21" class="java.beans.XMLDecoder"> 
 <object class="Dog"> 
  <void property="name"> 
   <string>Spotty</string> 
  </void> 
 </object> 
</java>

Some comments:

  • It is easy to write to a file instead of System.out. One can use for example new FileOutputStream("dog.xml").
  • Notice that only the name property is written to the file. That's because the weight is still the default value. If one changes the weight to be something other than 20, then it will show up in the xml. So if we had written instead Dog dog = new Dog{"Curly", 15}; the XML output would contain both the name and weight properties.

Decoding

The decoding process is similarly easy. The code below illustrates this. Note that this code acts as a sort of test to ensure that whatever is written to a file also reads in properly.

XMLEncoder e;
XMLDecoder ed;
Dog dog = new Dog(); dog.name = "Spotty";
try {
    e = new XMLEncoder(new FileOutputStream("test1.xml"));
    ed = new XMLDecoder(new FileInputStream("test1.xml"));
    System.out.println(dog);
    e.writeObject(dog); e.close();
    dog = (Dog) ed.readObject(); ed.close();
    System.out.println(dog);
} catch (FileNotFoundException ex) {
    Logger.getLogger(Dog.class.getName()).log(Level.SEVERE, null, ex);
}

Persistence with Collections and Arrays

Persistence with Java's Lists and arrays does not require much extra, but the resulting XML looks different. Here is another example below. Note particularly how the Vector's add method is being encoded directly.

CODE
public static class DogPack {
    Vector<Dog> dogs;
    public DogPack() { dogs = new Vector<Dog>(); }
    public Vector<Dog> getDogs() { return dogs; }
    public void setDogs(Vector<Dog> dogs) { this.dogs = dogs; }
    @Override public String toString() { return dogs.toString(); }
}
 
public static void main(String[] args) {
    Dog dog = new Dog();    
    dog.name="Spotty";
    DogPack pack = new DogPack();
    pack.dogs.add(new Dog("Curly", 15));
    pack.dogs.add(dog);
    XMLEncoder e = new XMLEncoder(System.out);
    e.writeObject(pack);
    e.close();
}
OUTPUT
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.5.0_21" class="java.beans.XMLDecoder"> 
 <object class="testpersist.Dog$DogPack"> 
  <void property="dogs"> 
   <void method="add"> 
    <object class="testpersist.Dog"> 
     <void property="name"> 
      <string>Curly</string> 
     </void> 
     <void property="weight"> 
      <float>15.0</float> 
     </void> 
    </object> 
   </void> 
   <void method="add"> 
    <object class="testpersist.Dog"> 
     <void property="name"> 
      <string>Spotty</string> 
     </void> 
    </object> 
   </void> 
  </void> 
 </object> 
</java>

The case of writing arrays is similar, although the xml looks different. Here note the indexing properties, and also note the use of the idref attribute to refer to a previously-defined object (ensuring proper re-creation of pointers).

CODE
public static class DogPack2 {
    Dog[] dogs;
    public DogPack2() { dogs = new Dog[3]; }
    public Dog[] getDogs() { return dogs; }
    public void setDogs(Dog[] dogs) { this.dogs = dogs; }
    @Override public String toString() { return Arrays.toString(dogs); }
}
 
public static void main(String[] args) {
    Dog dog = new Dog();    
    dog.name="Spotty";
    DogPack2 pack2 = new DogPack2();
    pack2.setDogs(new Dog[]{new Dog("Moe", 12), dog, new Dog("Mic", 4), dog});
    XMLEncoder e = new XMLEncoder(System.out);
    e.writeObject(pack2);
    e.close();
}
OUTPUT
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.5.0_21" class="java.beans.XMLDecoder"> 
 <object class="testpersist.Dog$DogPack2"> 
  <void property="dogs"> 
   <array class="testpersist.Dog" length="4"> 
    <void index="0"> 
     <object class="testpersist.Dog"> 
      <void property="name"> 
       <string>Moe</string> 
      </void> 
      <void property="weight"> 
       <float>12.0</float> 
      </void> 
     </object> 
    </void> 
    <void index="1"> 
     <object id="Dog0" class="testpersist.Dog"> 
      <void property="name"> 
       <string>Spotty</string> 
      </void> 
     </object> 
    </void> 
    <void index="2"> 
     <object class="testpersist.Dog"> 
      <void property="name"> 
       <string>Mic</string> 
      </void> 
      <void property="weight"> 
       <float>4.0</float> 
      </void> 
     </object> 
    </void> 
    <void index="3"> 
     <object idref="Dog0"/> 
    </void> 
   </array> 
  </void> 
 </object> 
</java>

Custom Persistence

There is a standard procedure for "delegating" persistence to another object. This is particularly useful when the base class does not support the required bean patterns. One object of this type is java.awt.geom.Point2D.Double, which has two parameters x and y, and get methods for these parameters, but no corresponding set methods. Here is an example of a something that might go wrong.

CODE
Point2D.Double p = new Point2D.Double(3,2);
XMLEncoder e = new XMLEncoder(System.out);
e.writeObject(p);
e.close();
OUTPUT
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.5.0_21" class="java.beans.XMLDecoder"> 
 <object class="java.awt.geom.Point2D$Double"/> 
</java>

If we try to save and reload the file, then we end up with the coordinate (0,0) instead of (3,2). The work-around is to create a custom persistence delegate, which is actually really easy, so instead use:

CODE
Point2D.Double p = new Point2D.Double(3,2);
XMLEncoder e = new XMLEncoder(System.out);
e.setPersistenceDelegate(Point2D.Double.class, new DefaultPersistenceDelegate(new String[]{"x","y"}));
e.writeObject(p);
e.close();
OUTPUT
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.5.0_21" class="java.beans.XMLDecoder"> 
 <object class="java.awt.geom.Point2D$Double"> 
  <double>3.0</double> 
  <double>2.0</double> 
 </object> 
</java>

This tells the encoder to store the values obtained by getX() and getY(), and then to use them in the default constructor when the object is restored using the XMLDecoder.
There is no need to set the persistence delegate for the decoder as well… I think the decoder puts the values saved in the xml directly into the constructor. (So if you switch the order of "x" and "y" the values read in incorrectly.)

Bugs and Difficulties

Arrays

I have found occasional problems when storing a class with a Point2D.Double[] property. For some reason, the value of the object is not always stored, even if the old array differs from the new array, unless the array is first recreated. I suspect this is a system bug, partly due to the equals() method in Point2D.Double, which is defined in the abstract class Point2D.Double, since an equivalent class with this method redefined works properly. This may also happen in some other circumstances… I'm not quite sure. Here are possible solutions:

  • In the class with the point array, DO NOT SET UP any initial values for the points, or set them up with null values. Things seem to work properly when initial values are null.
  • Create new get and set methods that will handle the persistence, e.g. use a double[][] type to get and set values instead. Then ensure that the persistence delegate only uses this value.
  • Create a subtype of Point2D.Double that overrides the "equals" method and use objects of this type instead… this works even if you only use it for the default values.

Enums

Java's enums also do not work properly with XMLEncoder, at least in earlier versions (see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5015403). I have found that the workaround specified there tends to work for enums that are declared with just constants, but not if they are declared with custom methods or any non-standard methods. A quick workaround is to use the String value returned by the name() method to serialize rather than the enum itself. So in the bean, instead of using

public YourEnumName getYourEnum() {
    return yourEnum;
}
public void setYourEnum(YourEnumName newValue) {
    this.yourEnum = newValue;
}

use the following:
public String getYourEnum() {
    return yourEnum==null ? "" : yourEnum.name();
}
 
public void setYourEnum(String newValue) {
    this.yourEnum = YourEnumName.valueOf(newValue);
}

Property Editors

PropertyEditors are intended to "provide support for GUIs that want to allow users to edit a property value of a given type". In doing so, they handle PropertyChangeEvents for underlying beans. When the value is changed, the class fires an event notifying any registered listeners of the change.

The PropertyEditor API encapsulates a single bean, with getValue and setValue methods, and PropertyChangeEvent handling.

The API also includes getJavaInitializationString, which provides Java constructor code to get the value underlying the component. This can be used by GUI builders to automatically generate the underlying object, or a copy of that object. I'm not entirely sure about its purpose.

Display and Editing Options

There are three display options:

  1. Textual display, via getAsText.
  2. Combobox display, via getAsText and String[] getTags.
  3. Custom component display, via isPaintable and paintRectangle.

Similarly, there are three editing options:

  1. Textual editing, via setAsText.
  2. Combobox editing, via setAsText and String[] getTags.
  3. Custom component editing, via supportsCustomEditor and Component getCustomEditor.

Standard Support Class

The default implementation is the PropertyEditorSupport class, which has simple text editing support.

  • Allows customizing the source of PropertyChangeEvents: this defaults to the class, but also may be set directly via the "source" bean property.
  • Supports getAsText and setAsText, but an exception is thrown in the set case unless the value is a String.
  • Does not support custom editors or tags.

Package Implementations

Several editors are already included within the sun.beans.editors package. Most of these support textual editing only. Exceptions are noted.

  • BooleanEditor (tags)
  • ByteEditor
  • ColorEditor (panel)
  • DoubleEditor
  • EnumEditor (tags)
  • FloatEditor
  • FontEditor (panel)
  • IntegerEditor
  • LongEditor
  • NumberEditor
  • ShortEditor
  • StringEditor

Customizers

The Customizer interface has basic PropertyChangeEvent support, as well as a setObject(Object bean) method that sets the argument based on the value within the customizer. These are intended to provide for more advanced customization capabilities.


Please up-vote this page if you like its contents!

rating: +1+x

Leave a comment or a question below:

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License