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.
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
This story, "Read-only properties in JavaFX 8" was originally published by JavaWorld.