RFR: 8274771: Map, FlatMap and OrElse fluent bindings for ObservableValue [v18]
Michael Strauß
mstrauss at openjdk.org
Tue Jul 5 17:18:18 UTC 2022
On Fri, 1 Jul 2022 15:16:24 GMT, John Hendrikx <jhendrikx at openjdk.org> wrote:
>> This is an implementation of the proposal in https://bugs.openjdk.java.net/browse/JDK-8274771 that me and Nir Lisker (@nlisker) have been working on. It's a complete implementation including good test coverage.
>>
>> This was based on https://github.com/openjdk/jfx/pull/434 but with a smaller API footprint. Compared to the PoC this is lacking public API for subscriptions, and does not include `orElseGet` or the `conditionOn` conditional mapping.
>>
>> **Flexible Mappings**
>> Map the contents of a property any way you like with `map`, or map nested properties with `flatMap`.
>>
>> **Lazy**
>> The bindings created are lazy, which means they are always _invalid_ when not themselves observed. This allows for easier garbage collection (once the last observer is removed, a chain of bindings will stop observing their parents) and less listener management when dealing with nested properties. Furthermore, this allows inclusion of such bindings in classes such as `Node` without listeners being created when the binding itself is not used (this would allow for the inclusion of a `treeShowingProperty` in `Node` without creating excessive listeners, see this fix I did in an earlier PR: https://github.com/openjdk/jfx/pull/185)
>>
>> **Null Safe**
>> The `map` and `flatMap` methods are skipped, similar to `java.util.Optional` when the value they would be mapping is `null`. This makes mapping nested properties with `flatMap` trivial as the `null` case does not need to be taken into account in a chain like this: `node.sceneProperty().flatMap(Scene::windowProperty).flatMap(Window::showingProperty)`. Instead a default can be provided with `orElse`.
>>
>> Some examples:
>>
>> void mapProperty() {
>> // Standard JavaFX:
>> label.textProperty().bind(Bindings.createStringBinding(() -> text.getValueSafe().toUpperCase(), text));
>>
>> // Fluent: much more compact, no need to handle null
>> label.textProperty().bind(text.map(String::toUpperCase));
>> }
>>
>> void calculateCharactersLeft() {
>> // Standard JavaFX:
>> label.textProperty().bind(text.length().negate().add(100).asString().concat(" characters left"));
>>
>> // Fluent: slightly more compact and more clear (no negate needed)
>> label.textProperty().bind(text.orElse("").map(v -> 100 - v.length() + " characters left"));
>> }
>>
>> void mapNestedValue() {
>> // Standard JavaFX:
>> label.textProperty().bind(Bindings.createStringBinding(
>> () -> employee.get() == null ? ""
>> : employee.get().getCompany() == null ? ""
>> : employee.get().getCompany().getName(),
>> employee
>> ));
>>
>> // Standard JavaFX + Optional:
>> label.textProperty().bind(Bindings.createStringBinding(
>> () -> Optinal.ofNullable(employee.get())
>> .map(Employee::getCompany)
>> .map(Company::getName)
>> .orElse(""),
>> employee
>> ));
>>
>> // Fluent: no need to handle nulls everywhere
>> label.textProperty().bind(
>> employee.map(Employee::getCompany)
>> .map(Company::getName)
>> .orElse("")
>> );
>> }
>>
>> void mapNestedProperty() {
>> // Standard JavaFX:
>> label.textProperty().bind(
>> Bindings.when(Bindings.selectBoolean(label.sceneProperty(), "window", "showing"))
>> .then("Visible")
>> .otherwise("Not Visible")
>> );
>>
>> // Fluent: type safe
>> label.textProperty().bind(label.sceneProperty()
>> .flatMap(Scene::windowProperty)
>> .flatMap(Window::showingProperty)
>> .orElse(false)
>> .map(showing -> showing ? "Visible" : "Not Visible")
>> );
>> }
>>
>> Note that this is based on ideas in ReactFX and my own experiments in https://github.com/hjohn/hs.jfx.eventstream. I've come to the conclusion that this is much better directly integrated into JavaFX, and I'm hoping this proof of concept will be able to move such an effort forward.
>
> John Hendrikx has updated the pull request with a new target base due to a merge or a rebase. The incremental webrev excludes the unrelated changes brought in by the merge/rebase. The pull request contains 27 additional commits since the last revision:
>
> - Merge branch 'openjdk:master' into feature/fluent-bindings
> - Add null checks in Subscription
> - Update copyrights
> - Move private binding classes to com.sun.javafx.binding package
> - Add note to Bindings#select to consider ObservableValue#flatMap
> - Fix bug invalidation bug in FlatMappedBinding
>
> Also fixed a secondary issue where the indirect source of the binding
> was unsubscribed and resubscribed each time its value was recomputed.
>
> Add additional comments to clarify how FlatMappedBinding works.
>
> Added test cases for these newly discovered issues.
> - Fix typos in LazyObjectBinding
> - Rename observeInputs to observeSources
> - Expand flatMap javadoc with additional wording from Optional#flatMap
> - Add since tags to all new API
> - ... and 17 more: https://git.openjdk.org/jfx/compare/b8ce56f8...d66f2ba6
I have yet another question. The following test passes for `Bindings.select`, but fails for `ObservableValue.flatMap`:
JMemoryBuddy.memoryTest(test -> {
class ValueHolder {
final StringProperty value = new SimpleStringProperty(this, "value");
StringProperty valueProperty() { return value; }
}
ObjectProperty<ValueHolder> valueHolderProperty = new SimpleObjectProperty<>();
valueHolderProperty.set(new ValueHolder());
// Map the nested property value
ObservableValue<String> mapped = valueHolderProperty.flatMap(ValueHolder::valueProperty);
// Note: the test passes when using the following alternative to flatMap:
// ObservableValue<String> mapped = Bindings.selectString(valueHolderProperty, "value");
// Bind the mapped value to a property that will soon be GC'ed.
ObjectProperty<String> otherProperty = new SimpleObjectProperty<>();
otherProperty.bind(mapped);
test.setAsReferenced(valueHolderProperty);
test.assertCollectable(otherProperty);
test.assertCollectable(mapped); // expectation: the mapped value is eligible for GC
});
My observation is that a flat-mapped value that was once observed is not eligible for garbage-collection even when the observer itself is collected. This seems to be quite unexpected to me, because it means that a bound property that is collected without being manually unbound will cause a memory leak in the mapped binding.
Is this by design? If so, I think this can lead to subtle and hard to diagnose bugs, and should be documented at the very least.
-------------
PR: https://git.openjdk.org/jfx/pull/675
More information about the openjfx-dev
mailing list