var a = 1;
var b = 10;
var m = 4;
def c = bind for (x in [a..b] where x < m) { x * x };
프로퍼티와 바인딩
JavaFX 바인딩의 선구자들
The need for exposing attributes of Java components directly to client code, allowing them to observe and to manipulate such attributes and to take action when their values change, was recognized early in Java’s life.
The JavaBeans framework in Java 1.1 provided support for properties through the now familiar getter and setter convention. It also supported the propagations of property changes through its PropertyChangeEvent and PropertyChangeListener mechanism. Although the JavaBeans framework is used in many Swing applications, its use is quite cumbersome and requires quite a bit of boilerplate code. Several higher level data binding frameworks were created over the years with various levels of success.
The heritage of the JavaBeans in the JavaFX properties and bindings framework lies mainly in the JavaFX Beans getter, setter, and property getter naming convention when defining JavaFX components. We talk about the JavaFX Beans getter, setter, and property getter naming convention later in this chapter, after we have covered the key concepts and interfaces of the JavaFX properties and bindings framework.
Another strand of heritage of the JavaFX properties and bindings framework comes from the JavaFX Script language that was part of the JavaFX 1.x platform. Although the JavaFX Script language was deprecated in the JavaFX platform in favor of a Java-based API, one of the goals of the transition was to preserve most of the powers of the JavaFX Script’s bind keyword, the expressive power of which has delighted many JavaFX enthusiasts. As an example, JavaFX Script supports the binding to complex expressions:
This code will automatically recalculate the value of c whenever the values of a, b, or m are changed.
Although the JavaFX properties and bindings framework does not support all of the binding constructs of JavaFX Script, it supports the binding of many useful expressions. We talk more about constructing compound binding expressions after we cover the key concepts and interfaces of the framework.
A Motivating Example
Let’s start with an example in Listing 4-1 that shows off the capabilities of the Property interface through the use of a couple of instances of the SimpleIntegerProperty class.
*Listing 4-1. * MotivatingExample.java
public class MotivatingExample {
private static IntegerProperty intProperty;
public static void main(String[] args) {
createProperty();
addAndRemoveInvalidationListener();
addAndRemoveChangeListener();
bindAndUnbindOnePropertyToAnother();
}
private static void createProperty() {
System.out.println();
intProperty = new SimpleIntegerProperty(1024);
System.out.println("intProperty = " + intProperty);
System.out.println("intProperty.get() = " + intProperty.get());
System.out.println("intProperty.getValue() = " + intProperty.getValue().intValue());
}
private static void addAndRemoveInvalidationListener() {
System.out.println();
final InvalidationListener invalidationListener = observable ->
System.out.println("The observable has been invalidated: " + observable + ".");
intProperty.addListener(invalidationListener);
System.out.println("Added invalidation listener.");
System.out.println("Calling intProperty.set(2048).");
intProperty.set(2048);
System.out.println("Calling intProperty.setValue(3072).");
intProperty.setValue(Integer.valueOf(3072));
intProperty.removeListener(invalidationListener);
System.out.println("Removed invalidation listener.");
System.out.println("Calling intProperty.set(4096).");
intProperty.set(4096);
}
private static void addAndRemoveChangeListener() {
System.out.println();
final ChangeListener changeListener = (ObservableValue observableValue, Object oldValue, Object newValue) ->
System.out.println("The observableValue has changed: oldValue = " + oldValue + ", newValue = " + newValue);
intProperty.addListener(changeListener);
System.out.println("Added change listener.");
System.out.println("Calling intProperty.set(5120).");
intProperty.set(5120);
intProperty.removeListener(changeListener);
System.out.println("Removed change listener.");
System.out.println("Calling intProperty.set(6144).");
intProperty.set(6144);
}
private static void bindAndUnbindOnePropertyToAnother() {
System.out.println();
IntegerProperty otherProperty = new SimpleIntegerProperty(0);
System.out.println("otherProperty.get() = " + otherProperty.get());
System.out.println("Binding otherProperty to intProperty.");
otherProperty.bind(intProperty);
System.out.println("otherProperty.get() = " + otherProperty.get());
System.out.println("Calling intProperty.set(7168).");
intProperty.set(7168);
System.out.println("otherProperty.get() = " + otherProperty.get());
System.out.println("Unbinding otherProperty from intProperty.");
otherProperty.unbind();
System.out.println("otherProperty.get() = " + otherProperty.get());
System.out.println("Calling intProperty.set(8192).");
intProperty.set(8192);
System.out.println("otherProperty.get() = " + otherProperty.get());
}
}
In this example we created a SimpleIntegerProperty object called intProperty with an initial value of 1024. We then updated its value through a series of different integers while we added and then removed an InvalidationListener, added and then removed a ChangeListener, and finally created another SimpleIntegerProperty named otherProperty, bound it to and then unbound it from intProperty. We have taken advantage of the Java 8 Lambda syntax in defining our listeners. The sample program used a generous amount of println calls to show what is happening inside the program.
By correlating the output lines with the program source code (or by stepping through the code in the debugger of your favorite IDE), we can draw the following conclusions.
-
A SimpleIntegerProperty object such as intProperty and otherProperty holds an int value. The value can be manipulated with the get(), set(), getValue(), and setValue() methods. The get() and set() methods perform their operation with the primitive int type. The getValue() and setValue() methods use the Integer wrapper type.
-
You can add and remove InvalidationListener objects to and from intProperty.
-
You can add and remove ChangeListener objects to and from intProperty.
-
Another Property object such as otherProperty can bind itself to intProperty. When that happens, otherProperty receives the value of intProperty.
-
When a new value is set on intProperty, whatever object that is attached to it is notified. The notification is not sent if the object is removed.
-
When notified, InvalidationListener objects are only informed of which object is sending out the notification and that object is only known as an Observable.
-
When notified, ChangeListener objects are informed on two more pieces of information—the oldValue and the newValue—in addition to the object sending the notification. The sending object is known as an ObservableValue.
-
In the case of a binding property such as otherProperty, we cannot tell from the output when or how it is notified of the change of value in intProperty. However, we can infer that it must have known of the change because when we asked otherProperty for its value we got back the latest value of intProperty.
This example brings to our attention some of the key interfaces and concepts of the JavaFX properties and bindings framework: including the Observable and the associated InvalidationListener interfaces, the ObservableValue and the associated ChangeListener interfaces, the get(), set(), getValue(), and setValue() methods that allow us to manipulate the values of a SimpleIntegerProperty object directly, and the bind() method that allows us to relinquish direct manipulation of the value of a SimpleIntegerProperty object by subordinating it to another SimpleIntegerProperty object.
In the next section we show you these and some other key interfaces and concepts of the JavaFX properties and bindings framework in more detail.
핵심 인터페이스와 개념
Observable 인터페이스
가장 상단에 Observable 인터페이스가 있다. InvalidationListener 객체를 Observable 객체에 등록하고 무효화 이벤트를 받도록 할 수 있다. 여러분은 이미 Observable 객체로부터 무효화 이벤트를 받는 것을 경험하였다. (SimpleIntegerProperty object intProperty). It is fired when the set() or setValue() methods are called to change the underlying value from one int to a different int.
무효화 이벤트는 Binding 객체로부터 발사될 수도 있다. Binding객체가 무효화되면 invalidate() 메소드가 불리거나, 의존 객체들 중에 하나로부터 발사된다.
ObservableValue 인터페이스
다음 계층이 ObservableValue 인터페이스이다. 이것은 값을 가진 Observable이다. getValue() 메소드가 값을 반환한다. ChangeListener 객체를 ObservableValue 객체에 등록하면 변경 이벤트를 받을 수 있다.
변경 이벤트가 발사되면 ChangeListener는 ObservableValue의 기존 값과 새로운 값을 받는다.
Note: 같은 값을 한 로우에 여러번 불러도 변경 이벤트는 ObservableValue 인터페이스의 구현에서 딱 한번만 발사된다.
무효화 이벤트와 변경 이벤트는 lazy evaluation 지원에서 차이가 난다.
otherProperty.bind(intProperty);
intProperty.set(7168);
System.out.println("otherProperty.get() = " + otherProperty.get());
intProperty.set(7168)를 부르면 무효화 이벤트가 otherProperty에게 발사된다. 이 이벤트를 받으면 otherProperty는 이 값이 더 이상 유효하지 않음을 기록한다. 그 값을 즉시 재계산하지는 않는다. 재계산은 otherProperty.get()이 불릴때 수행된다. intProperty.set() 을 부르는 단 한번 부르는 대신 intProperty.set()을 여러번 부르는 것을 상상해보라. otherProperty는 여전히 재계산을 한번만 수행한다.
WritableValue 인터페이스
WritableValue인터페이스의 목적은 getValue()와 setValue() 메소드를 구현에 넣도록 하는데 있다. WritableValue를 구현한 모든 클래스는 ObservableValue를 구현하므로 ObservableValue의 값을 setValue()의 인자로 사용할 수 있다.
You have seen the setValue() method at work in the motivating example.
ReadOnlyProperty Interface
ReadOnlyProperty인터페이스는 두 개의 메소드를 구현하도록 한다. getBean()은 ReadOnlyRroperty 객체를 담은 객체 또는 null을 리턴한다. getName()은 ReadOnlyProperty의 이름 또는 이름이 없는 경우엔 비어있는 스트링을 리턴한다.
담은 객체와 이름이 ReadOnlyProperty의 문맥적 정보를 제공한다. 문맥적 정보가 무효화 이벤트의 전파나 값 재계산에서 직접적인 역할을 하지 않는다. 다만 어떤 주변적 계산에서 고려사항이 된다.
예제에서는 intProperty는 구문적 정보 없이 만들었다. 만일 완전한 생성자를 사용한다면 다음과 같다.
intProperty = new SimpleIntegerProperty(null, "intProperty", 1024);
출력하면 프로퍼티 이름이 나올 것이다.
intProperty = IntegerProperty [name: intProperty, value: 1024]
Property 인터페이스
이제 계층도 맨 아래이다. Property 인터페이스는 앞에서 설명한 4개의 인터페이스 모두를 갖는 수퍼인터페이스이다. 따라서 이들의 모든 메소드를 상속하며 자체적으로 5개의 메소드를 갖는다.
void bind(ObservableValue<? extends T> observableValue);
void unbind();
boolean isBound();
void bindBidirectional(Property<T> tProperty);
void unbindBidirectional(Property<T> tProperty);
You have seen two of the methods at work in the motivating example in the last section: bind() and unbind().
마지막 절의 예제에서 두개의 메소드 bind(), unbind()를 본적이 있다.
bind()는 일방의 바인딩 또는 Property객체와 ObservableValue 인자간의 의존을 만든다. 일단 관계가 설정되면 Property객체에서 set()이나 setValue()를 부르면 Property객체는 RuntimeException을 던진다. get()이나 getValue()는 ObservableValue객체의 값을 리턴한다. 그리고 ObservableValue 객체의 값을 바꾸면 Property객체가 무효화된다.
unbind()를 부르면 Property객체의 일방의 바인딩이 풀린다. 일방향 바인딩이 유효 여부는 isBound()를 검사할 수 있다.
bindBidirectional()는 양방의 바인딩을 만든다. bind()의 인자는 ObservableValue이지만 bindBidirectional()은 Property를 인자로 받는다. 단지 두 Property객체만 함께 양방향으로 바인딩 된다. 일단 설정되면 어느 Property든 set(), setValue()가 불리면 두 객체의 값이 모두 변경된다. unbindBidirectional()를 부르면 기존의 양방향 바인딩이 풀린다.
Listing 4-2. BidirectionalBindingExample.java
public class BidirectionalBindingExample {
public static void main(String[] args) {
System.out.println("Constructing two StringProperty objects.");
StringProperty prop1 = new SimpleStringProperty("");
StringProperty prop2 = new SimpleStringProperty("");
System.out.println("Calling bindBidirectional.");
prop2.bindBidirectional(prop1);
System.out.println("prop1.isBound() = " + prop1.isBound());
System.out.println("prop2.isBound() = " + prop2.isBound());
System.out.println("Calling prop1.set(\"prop1 says: Hi!\")");
prop1.set("prop1 says: Hi!");
System.out.println("prop2.get() returned:");
System.out.println(prop2.get());
System.out.println("Calling prop2.set(prop2.get() + \"\\nprop2 says: Bye!\")");
prop2.set(prop2.get() + "\nprop2 says: Bye!");
System.out.println("prop1.get() returned:");
System.out.println(prop1.get());
}
}
두 개의 SimpleStringProperty 객체인 prop1과 prop2를 만들어 양방향 바인딩한다. 그리고 set()과 get()을 부르고 출력한다. 결과는
Constructing two StringProperty objects.
Calling bindBidirectional.
prop1.isBound() = false
prop2.isBound() = false
Calling prop1.set("prop1 says: Hi!")
prop2.get() returned:
prop1 says: Hi!
Calling prop2.set(prop2.get() + "\nprop2 says: Bye!")
prop1.get() returned:
prop1 says: Hi!
prop2 says: Bye!
Binding Interface
Binding 인터페이스는 4개의 메소드를 정의한다. 이 객체는 ObservableValue이며 유효성은 isValid()로 질의할 수 있으며 invalidate()로 값을 설정한다. getDependencies()로 얻을 수 있는 의존 목록을 갖는다. dispose()는 바인딩이 더 이상 사용되지 않으며 사용한 리소스는 정리될 수 있음을 알려준다.
From this brief description of the Binding interface, we can infer that it represents a unidirectional binding with multiple dependencies. Each dependency, we imagine, could be an ObservableValue to which the Binding is registered to receive invalidation events. When the get() or getValue() method is called, if the binding is invalidated, its value is recalculated.
JavaFX 프로퍼티와 바인딩 프레임워크는 Binding인터페이스를 구현하는 클래스를 제공하지 않는다. 하지만 여러분이 쉽게 Binding객체를 만들 수 있도록 여러 방식을 제공한다. 프레임워크의 추상 클래스를 상속하거나 유틸리티 클래스의 정적 메소드로 기존의 통상의 자바 값, 속성, 바인딩으로부터 새로운 바인딩을 만들 수 있다.
you can also use a set of methods that are provided in the various properties and bindings classes and form a fluent interface API to create new bindings. We go through the utility methods and the fluent interface API in the “Creating Bindings” section later in this chapter.
For now, we show you the first example of a binding by extending the DoubleBinding abstract class. The program in Listing 4-3 uses a binding to calculate the area of a rectangle.
Listing 4-3. RectangleAreaExample.java
public class RectangleAreaExample {
public static void main(String[] args) {
System.out.println("Constructing x with initial value of 2.0.");
final DoubleProperty x = new SimpleDoubleProperty(null, "x", 2.0);
System.out.println("Constructing y with initial value of 3.0.");
final DoubleProperty y = new SimpleDoubleProperty(null, "y", 3.0);
System.out.println("Creating binding area with dependencies x and y.");
DoubleBinding area = new DoubleBinding() {
private double value;
{
super.bind(x, y);
}
@Override
protected double computeValue() {
System.out.println("computeValue() is called.");
return x.get() * y.get();
}
};
System.out.println("area.get() = " + area.get());
System.out.println("area.get() = " + area.get());
System.out.println("Setting x to 5");
x.set(5);
System.out.println("Setting y to 7");
y.set(7);
System.out.println("area.get() = " + area.get());
}
}
In the anonymous inner class, we called the protected bind() method in the superclass DoubleBinding, informing the superclass that we would like to listen to invalidation events from the DoubleProperty objects x and y. We finally implemented the protected abstract computeValue() method in the superclass DoubleBinding to do the actual calculation when a recalculation is needed.
When we run the program in Listing 4-3, the following output is printed to the console:
Constructing x with initial value of 2.0.
Constructing y with initial value of 3.0.
Creating binding area with dependencies x and y.
computeValue() is called.
area.get() = 6.0
area.get() = 6.0
Setting x to 5
Setting y to 7
computeValue() is called.
area.get() = 35.0
Notice that computeValue() is called only once when we call area.get() twice in a row. Now that you have a firm grasp of the key interfaces and concepts of the JavaFX properties and bindings framework, we show you how these generic interfaces are specialized to type-specific interfaces and implemented in type-specific abstract and concrete classes.
Type-Specific Specializations of Key Interfaces
We did not emphasize this fact in the last section because we believe its omission does not hurt the explanations there, but except for Observable and InvalidationListener, the rest of the interfaces are generic interfaces with a type parameter <T>. In this section we examine how these generic interfaces are specialized to the specific types of interest: Boolean, Integer, Long, Float, Double, String, and Object. We also examine some of the abstract and concrete classes of the framework and explore typical usage scenarios of each class.
Image Note Specializations of these interfaces also exist for List, Map, and Set. They are designed for working with observable collections. We cover observable collections in Chapter 7.
A Common Theme for Type-Specific Interfaces
Although the generic interfaces are not all specialized in exactly the same way, a common theme exists:
• The Boolean type is specialized directly.
• The Integer, Long, Float, and Double are specialized through the Number supertype.
• The String type is specialized through the Object type.
This theme exists in the type-specific specializations of all the key interfaces. As an example, we examine the subinterfaces of the ObservableValue<T> interface:
-
ObservableBooleanValue extends ObservableValue<Boolean>, and it offers one additional method.
• boolean get(); -
ObservableNumberValue extends ObservableValue<Number>, and it offers four additional methods.
• int intValue(); -
long longValue();
-
float floatValue();
-
double doubleValue();
-
ObservableObjectValue<T> extends ObservableValue<t>, and it offers one additional method. •T get();
-
ObservableIntegerValue, ObservableLongValue, ObservableFloatValue, and ObservableDoubleValue extend ObservableNumberValue and each offers an additional get() method that returns the appropriate primitive type value.
-
ObservableStringValue extends ObservableObjectValue<String> and inherits its get() method that returns String.
Notice that the get() method that we have been using in the examples is defined in the type-specific ObservableValue subinterfaces. A similar examination reveals that the set() method that we have been using in the examples is defined in the type-specific WritableValue subinterfaces.
A practical consequence of this derivation hierarchy is that any numerical property can call bind() on any other numerical property or binding. Indeed, the signature of the bind() method on any numerical property is
void bind(ObservableValue<? extends Number> observable);
and any numerical property and binding is assignable to the generic parameter type. The program in Listing 4-4 shows that any numerical properties of different specific types can be bound to each other.
Listing 4-4. NumericPropertiesExample.java
public class NumericPropertiesExample {
public static void main(String[] args) {
IntegerProperty i = new SimpleIntegerProperty(null, "i", 1024);
LongProperty l = new SimpleLongProperty(null, "l", 0L);
FloatProperty f = new SimpleFloatProperty(null, "f", 0.0F);
DoubleProperty d = new SimpleDoubleProperty(null, "d", 0.0);
System.out.println("Constructed numerical properties i, l, f, d.");
System.out.println("i.get() = " + i.get());
System.out.println("l.get() = " + l.get());
System.out.println("f.get() = " + f.get());
System.out.println("d.get() = " + d.get());
l.bind(i);
f.bind(l);
d.bind(f);
System.out.println("Bound l to i, f to l, d to f.");
System.out.println("i.get() = " + i.get());
System.out.println("l.get() = " + l.get());
System.out.println("f.get() = " + f.get());
System.out.println("d.get() = " + d.get());
System.out.println("Calling i.set(2048).");
i.set(2048);
System.out.println("i.get() = " + i.get());
System.out.println("l.get() = " + l.get());
System.out.println("f.get() = " + f.get());
System.out.println("d.get() = " + d.get());
d.unbind();
f.unbind();
l.unbind();
System.out.println("Unbound l to i, f to l, d to f.");
f.bind(d);
l.bind(f);
i.bind(l);
System.out.println("Bound f to d, l to f, i to l.");
System.out.println("Calling d.set(10000000000L).");
d.set(10000000000L);
System.out.println("d.get() = " + d.get());
System.out.println("f.get() = " + f.get());
System.out.println("l.get() = " + l.get());
System.out.println("i.get() = " + i.get());
}
}
In this example we created four numeric properties and bound them into a chain in decreasing size to demonstrate that the bindings work as expected. We then reversed the order of the chain and set the double property’s value to a number that would overflow the integer property to highlight the fact that even though you can bind different sizes of numeric properties together, when the value of the dependent property is outside the range of the binding property, normal Java numeric conversion applies.
When we run the program in Listing 4-4, the following is printed to the console:
Constructed numerical properties i, l, f, d.
i.get() = 1024
l.get() = 0
f.get() = 0.0
d.get() = 0.0
Bound l to i, f to l, d to f.
i.get() = 1024
l.get() = 1024
f.get() = 1024.0
d.get() = 1024.0
Calling i.set(2048).
i.get() = 2048
l.get() = 2048
f.get() = 2048.0
d.get() = 2048.0
Unbound l to i, f to l, d to f.
Bound f to d, l to f, i to l.
Calling d.set(10000000000L).
d.get() = 1.0E10
f.get() = 1.0E10
l.get() = 10000000000
i.get() = 1410065408
Commonly Used Classes
We now give a survey of the content of the four packages javafx.beans, javafx.beans.binding, javafx.beans.property, and javafx.beans.value. In this section, “the SimpleIntegerProperty series of classes” refers to the classes extrapolated over the Boolean, Integer, Long, Float, Double, String, and Object types. Therefore what is said also applies to SimpleBooleanProperty, and so on.
-
The most often used classes in the JavaFX properties and bindings framework are the SimpleIntegerProperty series of classes. They provide all the functionalities of the Property interface including lazy evaluation. They are used in all the examples of this chapter up to this point.
-
Another set of concrete classes in the JavaFX properties and bindings framework is the ReadOnlyIntegerWrapper series of classes. These classes implement the Property interface but also have a getReadOnlyProperty() method that returns a ReadOnlyProperty that is synchronized with the main Property. They are very handy to use when you need a full-blown Property for the implementation of a component but you only want to hand out a ReadOnlyProperty to the client of the component.
-
The IntegerPropertyBase series of abstract classes can be extended to provide implementations of full Property classes, although in practice the SimpleIntegerProperty series of classes is easier to use. The only abstract methods in the IntegerPropertyBase series of classes are getBean() and getName().
-
The ReadOnlyIntegerPropertyBase series of abstract classes can be extended to provide implementations of ReadOnlyProperty classes. This is rarely necessary. The only abstract methods in the ReadOnlyIntegerPropertyBase series of classes are get(), getBean(), and getName().
-
The WeakInvalidationListener and WeakChangeListener classes can be used to wrap InvalidationListener and ChangeListener instances before addListener() is called. They hold weak references of the wrapped listener instances. As long as you hold a reference to the wrapped listener on your side, the weak references will be kept alive and you will receive events. When you are done with the wrapped listener and have unreferenced it from your side, the weak references will be eligible for garbage collection and later garbage collected. All the JavaFX properties and bindings framework Observable objects know how to clean up a weak listener after its weak reference has been garbage collected. This prevents memory leaks when the listeners are not removed after use. The WeakInvalidationListener and WeakListener classes implement the WeakListener interface, whose wasGarbageCollected() method will return true if the wrapped listener instance was garbage collected.
That covers all the JavaFX properties and bindings APIs that reside in the javafx.beans, javafx.beans.property, and javafx.beans.value packages and some but not all of the APIs in the javafx.beans.binding package. The javafx.beans.property.adapters package provides adapters between old-style JavaBeans properties and JavaFX properties. We will cover these adapters in the “Adapting JavaBeans Properties to JavaFX Properties” section. The remaining classes of the javafx.beans.binding package are APIs that help you to create new bindings out of existing properties and bindings. That is the focus of the next section.
Creating Bindings
이제 기존 프로퍼티에서 새로운 바운딩을 만들어 보자. "핵심 인터페이스와 개념"에서 다룬 것으로 바인딩은 관찰가능한 값이며 관찰 가능한 값은 의존 목록을 갖는다. 의존 값 역시 관찰 가능한 값이다.
바인딩을 만드는데 3가지 방법을 제공한다.
• 추상클래스의 IntegerBinding 시리즈를 확장
• Bindings 유킬리티 클래스의 정적 bindings-creating 메소드를 사용
• 추상클래스의 IntegerExpression 시리즈가 제공하는 fluent interface API를 사용.
You saw the direct extension approach in the “Binding Interface” section earlier in this chapter. We explore the Bindings utility class next.
Bindings Utility Class
Bindings클래스는 기존의 관찰가능한 값과 보통 값들로 부터 새로운 바운딩을 만드는 팩토리 메소드가 들어 있다. 메소드 대부분은 관찰가능한 값과 보통의 자바(관찰불가능한) 값 모두를 사용해서 새로운 바운딩을 만들기 위해 오버로드되어 있다. 최소한 인자 중 하나는 관찰가능한 값이다.
public static NumberBinding add(ObservableNumberValue n1, ObservableNumberValue n2)
public static DoubleBinding add(ObservableNumberValue n, double d)
public static DoubleBinding add(double d, ObservableNumberValue n)
public static NumberBinding add(ObservableNumberValue n, float f)
public static NumberBinding add(float f, ObservableNumberValue n)
public static NumberBinding add(ObservableNumberValue n, long l)
public static NumberBinding add(long l, ObservableNumberValue n)
public static NumberBinding add(ObservableNumberValue n, int i)
public static NumberBinding add(int i, ObservableNumberValue n)
add()가 호출되면 모든 관찰가능한 값 인자를 포함하는 의존을 지니며 그 값은 두 인자의 값을 합한 NumberBinding을 리턴한다. 마찬가지로 subtract(), multiply(), divide()에 대해서도 오버로드된 메소드가 있다.
Note: Recall from the last section that ObservableIntegerValue, ObservableLongValue, ObservableFloatValue, and ObservableDoubleValue are subclasses of ObservableNumberValue. Therefore the four arithmetic methods just mentioned can take any combinations of these observable numeric values as well as any unobservable values.
The program in Listing 4-5 uses the arithmetic methods in Bindings to calculate the area of a triangle in the Cartesian plane with vertices (x1, y1), (x2, y2), (x3, y3) using this formula:
Area = (x1*y2 + x2*y3 + x3*y1 – x1*y3 – x2*y1 – x3*y2) / 2
Listing 4-5. TriangleAreaExample.java
public class TriangleAreaExample {
public static void main(String[] args) {
IntegerProperty x1 = new SimpleIntegerProperty(0);
IntegerProperty y1 = new SimpleIntegerProperty(0);
IntegerProperty x2 = new SimpleIntegerProperty(0);
IntegerProperty y2 = new SimpleIntegerProperty(0);
IntegerProperty x3 = new SimpleIntegerProperty(0);
IntegerProperty y3 = new SimpleIntegerProperty(0);
final NumberBinding x1y2 = Bindings.multiply(x1, y2);
final NumberBinding x2y3 = Bindings.multiply(x2, y3);
final NumberBinding x3y1 = Bindings.multiply(x3, y1);
final NumberBinding x1y3 = Bindings.multiply(x1, y3);
final NumberBinding x2y1 = Bindings.multiply(x2, y1);
final NumberBinding x3y2 = Bindings.multiply(x3, y2);
final NumberBinding sum1 = Bindings.add(x1y2, x2y3);
final NumberBinding sum2 = Bindings.add(sum1, x3y1);
final NumberBinding sum3 = Bindings.add(sum2, x3y1);
final NumberBinding diff1 = Bindings.subtract(sum3, x1y3);
final NumberBinding diff2 = Bindings.subtract(diff1, x2y1);
final NumberBinding determinant = Bindings.subtract(diff2, x3y2);
final NumberBinding area = Bindings.divide(determinant, 2.0D);
x1.set(0); y1.set(0);
x2.set(6); y2.set(0);
x3.set(4); y3.set(3);
printResult(x1, y1, x2, y2, x3, y3, area);
x1.set(1); y1.set(0);
x2.set(2); y2.set(2);
x3.set(0); y3.set(1);
printResult(x1, y1, x2, y2, x3, y3, area);
}
private static void printResult(IntegerProperty x1, IntegerProperty y1,
IntegerProperty x2, IntegerProperty y2,
IntegerProperty x3, IntegerProperty y3,
NumberBinding area) {
System.out.println("For A(" +
x1.get() + "," + y1.get() + "), B(" +
x2.get() + "," + y2.get() + "), C(" +
x3.get() + "," + y3.get() + "), the area of triangle ABC is " + area.getValue());
}
}
좌표는 IntegerProperty로 나타낸다. NumberBinding 넓이는 Bindings의 사칙연산 팩토리 메소드를 사용하여 계산한다. IntegerProperty객체로 시작하므로 사칙연산의 결과가 NumberBindings라 하더라도 실제로 반환된 객체는 행렬식까지는 IntegerBinding 객체이다.
We used 2.0D rather than a mere 2 in the divide() call to force the division to be done as a double division, not as int division. All the properties and bindings that we build up form a tree structure with area as the root, the intermediate bindings as internal nodes, and the properties x1, y1, x2, y2, x3, y3 as leaves. This tree is similar to the parse tree we will get if we parse the mathematical expression for the area formula using grammar for the regular arithmetic expressions.
When we run the program in Listing 4-5, the following output is printed to the console:
For A(0,0), B(6,0), C(4,3), the area of triangle ABC is 9.0
For A(1,0), B(2,2), C(0,1), the area of triangle ABC is 1.5
Aside from the arithmetic methods, the Bindings class also has the following factory methods.
• Logical operators: and, or, not
• Numeric operators: min, max, negate
• Object operators: isNull, isNotNull
• String operators: length, isEmpty, isNotEmpty
• opRelationalerators:
- equal, equalIgnoreCase, greaterThan, greaterThanOrEqual
- lessThan, lessThanOrEqual, notEqual, notEqualIgnoreCase
• Creation operators:
• Selection operators:select
생성 연산자와 선택 연산자를 제외하면 예상하는대로 작동을 할 것이다. 객체 연산자는 관찰가능한 스트링 값과 관찰가능한 객체 값에 대해서만 의미있다. 스트링 연산자는 관찰가능한 스트링 값에 대해서만 의미가 있다. 모든 관계 연산자는 산술 값에만 적용된다.
There are versions of the equal and notEqual operators for numeric values that have a third double parameter for the tolerance when comparing float or double values. The equal and notEqual operators also apply to boolean, string, and object values. For string and object values, the equal and notEqual operator compares their values using the equals() method.
The creation operators provide a convenient way of creating a binding without directly extending the abstract base class. It takes a Callable and any number of dependencies as an argument. The area double binding in Listing 4-3 can be rewritten as follows, using a lambda expression as the Callable:
생성 연산자는 추상베이스 클래스를 직접 확장하지 않고도 바인딩을 만드는 편한 방법을 제공한다.
DoubleBinding area = Bindings.createDoubleBinding(() -> {
return x.get() * y.get();
}, x, y);
선택연산자는 JavaFX 빈, JavaFX 빈 명세에 따라 생성된 자바클래스에 대해 작동한다. 나. 관찰가능한 컬렉션을 다루는 Bindings에는 여러가지 메소드가 있다.
That covers all methods in Bindings that return a binding object. There are 18 methods in Bindings that do not return a binding object. The various bindBidirectional() and unbindBidirectional() methods create bidirectional bindings. As a matter of fact, the bindBidirectional() and unbindBidirectional() methods in the various properties classes simply call the corresponding ones in the Bindings class. The bindContent() and unbindContent() methods bind an ordinary collection to an observable collection. The convert(), concat(), and a pair of overloaded format() methods return StringExpression objects. And finally the when() method returns a When object.
When과 StringExpression 클래스들은 바인딩을 만드는 유연한 인터페이스 API의 부분이다.
Fluent Interface API
If you asked the questions “Why would anybody name a method when()? And what kind of information would the When class encapsulate?,” welcome to the club.
While you were not looking, the object-oriented programming community invented a brand new method of API design that totally disregards the decades-old principles of object-oriented practices.
데이터를 내포하고 비즈니스 로직을 관련 도메인 객체로 배포하는 대신에 이 새로운 방법론은 메소드 체이닝을 권장하고 한 메소드의 리턴 타입으로 다음 번에 사용할 수 있는 메소드를 결정하는 API 스타일을 만들어낸다. 메소드 이름은 완벽한 의미를 전달하지 않도록 선정되지만 전체적인 메소드 체인은 유창한 문장처럼 읽혀진다. 이러한 스타일의 API를 fluent 인터페이스 API라고 한다.
Note: You can find a more thorough exposition of fluent interfaces on Martin Fowler’s web site, referenced at the end of this chapter.
The fluent interface APIs for creating bindings are defined in the IntegerExpression series of classes. IntegerExpression is a superclass of both IntegerProperty and IntegerBinding, making the methods of IntegerExpression also available in the IntegerProperty and IntegerBinding classes. The four numeric expression classes share a common superinterface NumberExpression, where all the methods are defined. The type-specific expression classes override some of the methods that yield a NumberBinding to return a more appropriate type of binding.
바인디을 만드는 fluent인터페이스 API는 IntegerExpression 클래스 시리즈에서 정의된다. IntegerExpression은 IntegerProperty와 IntegerBinding 모두의 상위클래스이며 IntegerExpression의 메소드를 두 클래스에서 사용할 수 있다. 4가지 산술 표현식 클래스는 NumberExpression을 공통의 상위 인터페이스를 공유한다. 특정타입 표현식 클래스들은 NumberBinding이 좀 더 적당한 타입의 바인딩을 리턴하는 몇가지 메소드를 오버라이딩 한다.
The methods thus made available for the seven kinds of properties and bindings are listed here:
• For BooleanProperty and BooleanBinding
-
BooleanBinding and(ObservableBooleanValue b)
-
BooleanBinding or(ObservableBooleanValue b)
-
BooleanBinding not()
-
BooleanBinding isEqualTo(ObservableBooleanValue b)
-
BooleanBinding isNotEqualTo(ObservableBooleanValue b)
-
StringBinding asString()
• Common for all numeric properties and bindings
-
BooleanBinding isEqualTo(ObservableNumberValue m)
-
BooleanBinding isNotEqualTo(ObservableNumberValue m)
-
BooleanBinding greaterThan(ObservableNumberValue m)
-
BooleanBinding lessThan(ObservableNumberValue m)
-
BooleanBinding greaterThanOrEqualTo(ObservableNumberValue m)
-
BooleanBinding greaterThanOrEqualTo(double d)
-
BooleanBinding greaterThanOrEqualTo(float f)
-
BooleanBinding greaterThanOrEqualTo(long l)
-
BooleanBinding greaterThanOrEqualTo(int i)
-
BooleanBinding lessThanOrEqualTo(ObservableNumberValue m)
-
StringBinding asString()
-
StringBinding asString(String str)
-
StringBinding asString(Locale locale, String str)
• For IntegerProperty and IntegerBinding
• For LongProperty and LongBinding
• For FloatProperty and FloatBinding
• For DoubleProperty and DoubleBinding
• For StringProperty and StringBinding
• For ObjectProperty and ObjectBinding
With these methods, you can create an infinite variety of bindings by starting with a property and calling one of the methods that is appropriate for the type of the property to get a binding, and calling one of the methods that is appropriate for the type of the binding to get another binding, and so on. One fact that is worth pointing out here is that all the methods for the type-specific numeric expressions are defined in the NumberExpression base interface with a return type of NumberBinding, and are overridden in the type-specific expression classes with an identical parameter signature but a more specific return type. This way of overriding a method in a subclass with an identical parameter signature but a more specific return type is called covariant return-type overriding, and has been a Java language feature since Java 5. One of the consequences of this fact is that numeric bindings built with the fluent interface API have more specific types than those built with factory methods in the Bindings class.
Sometimes it is necessary to convert a type-specific expression into an object expression holding the same type of value. This can be done with the asObject() method in the type-specific expression class. The conversion back can be done using static methods in the expressions class. For IntegerExpression these static methods are static IntegerExpression integerExpression(ObservableIntegerValue value) static <T extends java.lang.Number> IntegerExpression integerExpression(ObservableValue<T> value)
The program in Listing 4-6 is a modification of the triangle area example in Listing 4-5 that uses the fluent interface API instead of calling factory methods in the Bindings class.
Listing 4-6. TriangleAreaFluentExample.java
import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.beans.binding.StringExpression;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class TriangleAreaFluentExample {
public static void main(String[] args) {
IntegerProperty x1 = new SimpleIntegerProperty(0);
IntegerProperty y1 = new SimpleIntegerProperty(0);
IntegerProperty x2 = new SimpleIntegerProperty(0);
IntegerProperty y2 = new SimpleIntegerProperty(0);
IntegerProperty x3 = new SimpleIntegerProperty(0);
IntegerProperty y3 = new SimpleIntegerProperty(0);
final NumberBinding area = x1.multiply(y2)
.add(x2.multiply(y3))
.add(x3.multiply(y1))
.subtract(x1.multiply(y3))
.subtract(x2.multiply(y1))
.subtract(x3.multiply(y2))
.divide(2.0D);
StringExpression output = Bindings.format(
"For A(%d,%d), B(%d,%d), C(%d,%d), the area of triangle ABC is %3.1f",
x1, y1, x2, y2, x3, y3, area);
x1.set(0); y1.set(0);
x2.set(6); y2.set(0);
x3.set(4); y3.set(3);
System.out.println(output.get());
x1.set(1); y1.set(0);
x2.set(2); y2.set(2);
x3.set(0); y3.set(1);
System.out.println(output.get());
}
}
다음엔 When클래스와 바인딩을 만들때의 역할에 대한 미스테리를 풀겠다. When클래스는 ObservableBooleanValue을 인자로 갖는다.
public When(ObservableBooleanValue b)
It has the following 11 overloaded then() methods.
When.NumberConditionBuilder then(ObservableNumberValue n)
When.NumberConditionBuilder then(double d)
When.NumberConditionBuilder then(float f)
When.NumberConditionBuilder then(long l)
When.NumberConditionBuilder then(int i)
When.BooleanConditionBuilder then(ObservableBooleanValue b)
When.BooleanConditionBuilder then(boolean b)
When.StringConditionBuilder then(ObservableStringValue str)
When.StringConditionBuilder then(String str)
When.ObjectConditionBuilder<T> then(ObservableObjectValue<T> obj)
When.ObjectConditionBuilder<T> then(T obj)
then()에서 반환하는 객체의 타입은 인자의 타입에 달려있다. 인자가 수치형이면 관찰가능 또는 관찰 불가능에 이든지간에 리턴 타입은 중첩 클래스 When.NumberConditionBuilder이다. 마찬가지로 Boolean인자이면 리턴 타입은 hen.BooleanConditionBuilder이다. 등등
These condition builders in turn have the following otherwise() methods.
• For When.NumberConditionBuilder •NumberBinding otherwise(ObservableNumberValue n)
-
DoubleBinding otherwise(double d)
-
NumberBinding otherwise(float f)
-
NumberBinding otherwise(long l)
-
NumberBinding otherwise(int i)
• For When.BooleanConditionBuilder •BooleanBinding otherwise(ObservableBooleanValue b)
-
BooleanBinding otherwise(boolean b)
• For When.StringConditionBuilder •StringBinding otherwise(ObservableStringValue str)
-
StringBinding otherwise(String str)
• For When.ObjectConditionBuilder
-
ObjectBinding<T> otherwise(ObservableObjectValue<T> obj)
-
ObjectBinding<T> otherwise(T obj)
The net effect of these method signatures is that you can build up a binding that resembles an if/then/else expression this way: new When(b).then(x).otherwise(y)
where b is an ObservableBooleanValue, and x and y are of similar types and can be either observable or unobservable. The resulting binding will be of a type similar to that of x and y.
The program in Listing 4-7 uses the fluent interface API from the When class to calculate the area of a triangle with given sides a, b, and c. Recall that to form a triangle, the three sides must satisfy the following conditions:
a + b > c, b + c > a, c + a > b.
When the preceding conditions are satisfied, the area of the triangle can be calculated using Heron’s formula:
Area = sqrt(s * (s – a) * (s – b) * (s – c))
where s is the semiperimeter:
s = (a + b + c) / 2.
Listing 4-7. HeronsFormulaExample.java
public class HeronsFormulaExample {
public static void main(String[] args) {
DoubleProperty a = new SimpleDoubleProperty(0);
DoubleProperty b = new SimpleDoubleProperty(0);
DoubleProperty c = new SimpleDoubleProperty(0);
DoubleBinding s = a.add(b).add(c).divide(2.0D);
final DoubleBinding areaSquared = new When(
a.add(b).greaterThan(c)
.and(b.add(c).greaterThan(a))
.and(c.add(a).greaterThan(b)))
.then(s.multiply(s.subtract(a))
.multiply(s.subtract(b))
.multiply(s.subtract(c)))
.otherwise(0.0D);
a.set(3);
b.set(4);
c.set(5);
System.out.printf("Given sides a = %1.0f, b = %1.0f, and c = %1.0f," +
" the area of the triangle is %3.2f\n", a.get(), b.get(), c.get(),
Math.sqrt(areaSquared.get()));
a.set(2);
b.set(2);
c.set(2);
System.out.printf("Given sides a = %1.0f, b = %1.0f, and c = %1.0f," +
" the area of the triangle is %3.2f\n", a.get(), b.get(), c.get(),
Math.sqrt(areaSquared.get()));
}
}
Inasmuch as there is no ready-made binding method in DoubleExpression that calculates the square root, we create a DoubleBinding for areaSquared instead. The constructor argument for When() is a BooleanBinding built out of the three conditions on a, b, and c. The argument for the then() method is a DoubleBinding that calculates the square of the area of the triangle. And because the then() argument is numeric, the otherwise() argument also has to be numeric. We choose to use 0.0D to signal that an invalid triangle is encountered.
Inasmuch as there is no ready-made binding method in DoubleExpression that calculates the square root, we create a DoubleBinding for areaSquared instead. The constructor argument for When() is a BooleanBinding built out of the three conditions on a, b, and c. The argument for the then() method is a DoubleBinding that calculates the square of the area of the triangle. And because the then() argument is numeric, the otherwise() argument also has to be numeric. We choose to use 0.0D to signal that an invalid triangle is encountered.
Listing 4-8. HeronsFormulaDirectExtensionExample.java
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class HeronsFormulaDirectExtensionExample {
public static void main(String[] args) {
final DoubleProperty a = new SimpleDoubleProperty(0);
final DoubleProperty b = new SimpleDoubleProperty(0);
final DoubleProperty c = new SimpleDoubleProperty(0);
DoubleBinding area = new DoubleBinding() {
{
super.bind(a, b, c);
}
@Override
protected double computeValue() {
double a0 = a.get();
double b0 = b.get();
double c0 = c.get();
if ((a0 + b0 > c0) && (b0 + c0 > a0) && (c0 + a0 > b0)) {
double s = (a0 + b0 + c0) / 2.0D;
return Math.sqrt(s * (s - a0) * (s - b0) * (s - c0));
} else {
return 0.0D;
}
}
};
a.set(3);
b.set(4);
c.set(5);
System.out.printf("Given sides a = %1.0f, b = %1.0f, and c = %1.0f," +
" the area of the triangle is %3.2f\n", a.get(), b.get(), c.get(),
area.get());
a.set(2);
b.set(2);
c.set(2);
System.out.printf("Given sides a = %1.0f, b = %1.0f, and c = %1.0f," +
" the area of the triangle is %3.2f\n", a.get(), b.get(), c.get(),
area.get());
}
}
The direct extension method is preferred for complicated expressions and for expressions that go beyond the available operators.
Now that you have mastered all the APIs in the javafx.beans, javafx.beans.binding, javafx.beans.property, and javafx.beans.value packages, you are ready to step beyond the details of the JavaFX properties and bindings framework and learn how these properties are organized into bigger components called JavaFX Beans.