Read-only properties in JavaFX 8

Learn how to expose internal modifiable properties as external read-only properties in JavaFX 8

You're building a JavaFX library with properties that must appear read-only to external clients while remaining updatable to library code. How do you accomplish this duality? This post presents JavaFX 8's answer to this question.

The need for read-only exposure

Before I show you how to create updatable properties that are read-only to external clients, let's review how to define a simple property. Listing 1 presents the source code to a class that implements a counter property.

Listing 1. Implementing a counter property

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public final class Counter
{
   private IntegerProperty counter = new SimpleIntegerProperty();

   public IntegerProperty counterProperty()
   {
      return counter;
   }

   public int getCounter() 
   {
      return counter.get();
   }

   public void increment()
   {
      counter.set(counter.get() + 1);
   }
}

Counter first introduces a javafx.beans.property.IntegerProperty field that defines a counter property wrapping a 32-bit integer value. Because IntegerProperty is abstract, I initialize this field to an instance of the concrete javafx.beans.property.SimpleIntegerProperty class, which implements the property. Furthermore, its noargument constructor initializes the property value to 0.

The counterProperty() method follows the JavaFX pattern for returning a property so that a client can bind to the property, install a change listener, and so on. The getCounter() method follows the JavaFX pattern for returning the property's current value. However, there is no equivalent setCounter() method because counter isn't to be set to an arbitrary integer value. Instead, the task of updating this property is delegated to the increment() method.

Listing 2 presents the source code to a class that demonstrates Counter.

Listing 2. Using the counter property

public class UseCounter
{
   public static void main(String[] args)
   {
      Counter c = new Counter();
      c.counterProperty()
       .addListener((o, ov, nv) -> 
                    System.out.printf("old val = %d, new val = %d%n", ov, nv));
      for (int i = 1; i <= 10; i++)
         c.increment();
   }
}

UseCounter's main() method first instantiates Counter and then attaches a change listener to this property. Whenever counter's value changes, the listener will be called with this property's old and new values, which it will output.

main() exercises the counter property by invoking Counter's increment() method 10 times. After compiling both source files (javac *.java) and running the application (java UseCounter), you should observe the following output:

old val = 0, new val = 1
old val = 1, new val = 2
old val = 2, new val = 3
old val = 3, new val = 4
old val = 4, new val = 5
old val = 5, new val = 6
old val = 6, new val = 7
old val = 7, new val = 8
old val = 8, new val = 9
old val = 9, new val = 10

There's a problem with the counter property's implementation. Despite no setCounter() method, it's easy for an external client to bypass the increment() method and assign an arbitrary integer value to counter. Prove this to yourself by inserting c.counterProperty().set(-10); after Counter c = new Counter();, recompile the source code, and run the application. This time, you'll see the following output:

old val = -10, new val = -9
old val = -9, new val = -8
old val = -8, new val = -7
old val = -7, new val = -6
old val = -6, new val = -5
old val = -5, new val = -4
old val = -4, new val = -3
old val = -3, new val = -2
old val = -2, new val = -1
old val = -1, new val = 0

Earlier, I stated that there's no setCounter() method because the counter property isn't to be set to an arbitrary integer value. However, I just showed you how to violate this requirement. In the next section, I'll correct this problem.

Enforcing read-only exposure

Listing 1's counterProperty() method is problematic because it breaks Counter's encapsulation by exposing the counter property to external clients. You could fix the problem by removing this method, but that would prevent external clients from binding to or installing a change listener on the counter property. Fortunately for us, JavaFX's designers foresaw this dilemma and devised an elegant solution.

The javafx.beans.property package includes various classes that begin with the ReadOnly prefix. This prefix is followed by a type name such as Boolean, Integer, List, or Map. A suffix consisting of Property, PropertyBase, or Wrapper terminates the class's name. For example, you'll discover ReadOnlyIntegerProperty, ReadOnlyIntegerPropertyBase, and ReadOnlyIntegerWrapper classes in this package.

ReadOnlyIntegerProperty and similar concrete classes define read-only properties for values of the indicated types (e.g., 32-bit integer values). ReadOnlyIntegerWrapper and similar concrete classes create two properties that are synchronized. One property is read-only and can be passed to external users. The other property is updatable and shouldn't be exposed to external clients.

Listing 3 presents the source code to a class that uses ReadOnlyIntegerProperty and ReadOnlyIntegerWrapper to implement an updatable counter property that's read-only to external clients.

Listing 3. Implementing an updatable counter property that's read-only to external clients

import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;

public final class Counter
{
   private ReadOnlyIntegerWrapper counter =
      new ReadOnlyIntegerWrapper();

   public ReadOnlyIntegerProperty counterProperty()
   {
      return counter.getReadOnlyProperty();
   }

   public int getCounter() 
   {
      return counter.get();
   }

   public void increment()
   {
      counter.set(counter.get() + 1);
   }
}

There are two differences between Listing 3 and Listing 1. First, counter is declared to be of type ReadOnlyIntegerWrapper instead of type IntegerProperty. Also, it's initialized to a ReadOnlyIntegerWrapper instance instead of to a SimpleIntegerProperty instance. The ReadOnlyIntegerWrapper() constructor initializes the wrapped integer to 0.

The second difference is the counterProperty() method. Its return type is set to ReadOnlyIntegerProperty instead of IntegerProperty. Also, it executes return counter.getReadOnlyProperty(); instead of return counter;.

ReadOnlyIntegerWrapper's ReadOnlyIntegerProperty getReadOnlyProperty() method returns a ReadOnlyIntegerProperty object that's synchronized with the invoking ReadOnlyIntegerWrapper object. An update to the ReadOnlyIntegerWrapper's updatable property is immediately reflected in the returned read-only property. Encapsulation isn't violated because counter isn't returned.

If you attempt to compile the modified UseCounter class that includes the c.counterProperty().set(-10); expression, the compiler will report an error stating that it cannot find the set() method: ReadOnlyIntegerProperty doesn't declare a set() method. This time, an external client won't be able to set the counter property to an arbitrary integer value. It can update this property only via Counter's increment() method.

Conclusion

Although it demonstrates a read-only property, this post's example is far from useful. I've created a comic book viewer application and library as a second and more useful example of a read-only property. This JavaFX-based example lets you load, view, and scroll through the pages of comic books that are stored in CBZ archives. To obtain the example, check out the Comic Book Viewer project, which is advertised below.

download
Get the source code for this post's applications. Created by Jeff Friesen for JavaWorld

The following software was used to develop the post's code:

  • 64-bit JDK 8u60

The post's code was tested on the following platform(s):

  • JVM on 64-bit Windows 8.1
Notice to our Readers
We're now using social media to take your comments and feedback. Learn more about this here.