RFR: 8290310: ChangeListener events are incorrect or misleading when a nested change occurs

Michael Strauß mstrauss at openjdk.org
Thu Jun 27 17:31:55 UTC 2024


On Tue, 4 Apr 2023 15:22:48 GMT, John Hendrikx <jhendrikx at openjdk.org> wrote:

> This provides and uses a new implementation of `ExpressionHelper`, called `ListenerManager` with improved semantics.
> 
> # Behavior
> 
> |Listener...|ExpressionHelper|ListenerManager|
> |---|---|---|
> |Invocation Order|In order they were registered, invalidation listeners always before change listeners|(unchanged)|
> |Removal during Notification|All listeners present when notification started are notified, but excluded for any nested changes|Listeners are removed immediately regardless of nesting|
> |Addition during Notification|Only listeners present when notification started are notified, but included for any nested changes|New listeners are never called during the current notification regardless of nesting|
> 
> ## Nested notifications:
> 
> | |ExpressionHelper|ListenerManager|
> |---|---|---|
> |Type|Depth first (call stack increases for each nested level)|(same)|
> |# of Calls|Listeners * Depth (using incorrect old values)|Collapses nested changes, skipping non-changes|
> |Vetoing Possible?|No|Yes|
> |Old Value correctness|Only for listeners called before listeners making nested changes|Always|
> 
> # Performance
> 
> |Listener|ExpressionHelper|ListenerManager|
> |---|---|---|
> |Addition|Array based, append in empty slot, resize as needed|(same)|
> |Removal|Array based, shift array, resize as needed|(same)|
> |Addition during notification|Array is copied, removing collected WeakListeners in the process|Appended when notification finishes|
> |Removal during notification|As above|Entry is `null`ed (to avoid moving elements in array that is being iterated)|
> |Notification completion with changes|-|Null entries (and collected WeakListeners) are removed|
> |Notifying Invalidation Listeners|1 ns each|(same)|
> |Notifying Change Listeners|1 ns each (*)|2-3 ns each|
> 
> (*) a simple for loop is close to optimal, but unfortunately does not provide correct old values
> 
> # Memory Use 
> 
> Does not include alignment, and assumes a 32-bit VM or one that is using compressed oops.
> 
> |Listener|ExpressionHelper|ListenerManager|OldValueCaching ListenerManager|
> |---|---|---|---|
> |No Listeners|none|none|none|
> |Single InvalidationListener|16 bytes overhead|none|none|
> |Single ChangeListener|20 bytes overhead|none|16 bytes overhead|
> |Multiple listeners|57 + 4 per listener (excluding unused slots)|57 + 4 per listener (excluding unused slots)|61 + 4 per listener (excluding unused slots)|
> 
> # About nested changes
> 
> Nested changes are simply changes that are made to a property that is currently in the process of notifying its listeners. This...

This change is a great improvement over the current implementation, I'm looking forward to it.
Some comments below:

`ListenerManager` is an obvious improvement, as it fixes incorrect behavior and allows listeners to veto changes. However, the behavior of `ListenerManager` is also an implementation detail and not documented anywhere. This leads me to the following questions:

1. How will users know that they can now do all of the things that were previously broken? Do we need a specification for what is allowed and what's not allowed?
2. Should this behavior be required for all valid `ObservableValue` implementations? (This would render many existing implementations defective.)
3. If `ObservableValue` implementations are not required to replicate the `ListenerManager` behavior, we should probably make it easily discoverable whether any particular implementation (most of them are properties) supports nested changes/vetoing. In most of the public API, there's no obvious way to see (without looking at the source code) whether a property implementation extends one of the `*PropertyBase` classes.

modules/javafx.base/src/main/java/com/sun/javafx/binding/ArrayManager.java line 58:

> 56:      * Constructs a new instance.
> 57:      *
> 58:      * @param accessor an {@link Accessor}, cannot be {@code null}

There is no `accessor` parameter.

modules/javafx.base/src/main/java/com/sun/javafx/binding/ArrayManager.java line 94:

> 92:      *
> 93:      * @param instance the instance it is located in, cannot be {@code null}
> 94:      * @param array the occupied slots of the array to set

The parameter is named `occupiedSlots`, not `array`.

modules/javafx.base/src/main/java/com/sun/javafx/binding/ArrayManager.java line 168:

> 166:      * @param instance a reference to the instance where the array is stored, cannot be {@code null}
> 167:      * @param index an index to remove, cannot be negative, or greater than or equal to the number of occupied slots
> 168:      * @returns the element that was removed, can be {@code null} if the element at the given index was {@code null}

Should be `@return`.

modules/javafx.base/src/main/java/com/sun/javafx/binding/ArrayManager.java line 217:

> 215:      * @param instance a reference to the instance where the array is stored, cannot be {@code null}
> 216:      * @param index an index, cannot be negative, or greater than or equal to the number of occupied slots
> 217:      * @returns the element at the given index, can be {@code null} if the element at the given index was {@code null}

Should be `@return`.

modules/javafx.base/src/main/java/com/sun/javafx/binding/ArrayManager.java line 238:

> 236:      * @param index an index to set, cannot be negative, or greater than or equal to the number of occupied slots
> 237:      * @param element an element to set, can be {@code null}
> 238:      * @returns the element that was previously at the given index, can be {@code null} if the element at the given index was {@code null}

Should be `@return`.

modules/javafx.base/src/main/java/com/sun/javafx/binding/ArrayManager.java line 374:

> 372:         while (needed > max) {
> 373:             min = mid;
> 374:             mid = max;

These two lines don't seem to be useful, as neither `min` nor `mid` are ever accessed after this point.

modules/javafx.base/src/main/java/com/sun/javafx/binding/ListenerListBase.java line 243:

> 241: 
> 242:     /**
> 243:      * Gets the {@link ChangeLisener} at the given index. This can be {@code null} if the

Typo: `ChangeListener`

modules/javafx.base/src/main/java/com/sun/javafx/binding/ListenerManager.java line 84:

> 82:      * @throws NullPointerException when listener is {@code null}
> 83:      */
> 84:     public void addListener(I instance, ChangeListener<? super T> listener) {

The code of this method is a duplicate of `addListener(I, InvalidationListener)`.
Maybe you could use a common implementation for both overloads.

modules/javafx.base/src/main/java/com/sun/javafx/binding/ListenerManager.java line 125:

> 123: 
> 124:     /**
> 125:      * Notifies the listeners managed in the given instance.<p>

Minor: unncessary `<p>`

modules/javafx.base/src/main/java/com/sun/javafx/binding/ListenerManager.java line 143:

> 141:      */
> 142:     public void fireValueChanged(I instance, T oldValue) {
> 143:         Object data = getData(instance);

The `data` value could be passed into this method, which would save a (potentially not devirtualized) method call.

modules/javafx.base/src/main/java/com/sun/javafx/binding/ListenerManager.java line 145:

> 143:         Object data = getData(instance);
> 144: 
> 145:         if (data instanceof ListenerList) {

Why is `ListenerList` checked first, when most observables only have a single `InvalidationListener`?

modules/javafx.base/src/main/java/com/sun/javafx/binding/OldValueCachingListenerList.java line 101:

> 99:      *     notification otherwise {@code false}
> 100:      */
> 101:     public boolean notifyListeners(ObservableValue<T> observableValue) {

The code in this method is _almost_ identical to `ListenerList.notifyListeners(ObservableValue<T>, T)`.
Given that this method is somewhat complex, I think it would be good to use a common implementation.
This will help with code review, and decrease the chance that both methods diverge further with future modifications.

modules/javafx.base/src/main/java/com/sun/javafx/binding/OldValueCachingListenerList.java line 164:

> 162:     }
> 163: 
> 164:     private void callInvalidationListener(ObservableValue<?> instance, InvalidationListener listener) {

This method is identical to `ListenerList.callInvalidationListener`.

modules/javafx.base/src/main/java/com/sun/javafx/binding/OldValueCachingListenerList.java line 173:

> 171:     }
> 172: 
> 173:     private void callChangeListener(ObservableValue<T> instance, ChangeListener<T> changeListener, T oldValue, T newValue) {

Again, _almost_ identical to `ListenerList.callChangeListener`.

modules/javafx.base/src/main/java/javafx/beans/property/ObjectPropertyBase.java line 91:

> 89:     @Override
> 90:     public void addListener(InvalidationListener listener) {
> 91:         LISTENER_MANAGER.addListener((ObjectPropertyBase<Object>) this, listener);

I think the unchecked casts here can be removed if `ListenerManagerBase` is declared as `ListenerManagerBase<T, I extends ObservableValue<? extends T>>`, and `OldValueCachingListenerManager` accordingly. Then the `LISTENER_MANAGER` instance can be parameterized as `OldValueCachingListenerManager<Object, ObjectPropertyBase<?>>`.

modules/javafx.base/src/main/java/javafx/beans/property/ReadOnlyObjectPropertyBase.java line 66:

> 64:     @Override
> 65:     public void addListener(InvalidationListener listener) {
> 66:         LISTENER_MANAGER.addListener((ReadOnlyObjectPropertyBase<Object>) this, listener);

See the comment for `ObjectPropertyBase`.

modules/javafx.base/src/main/java/javafx/beans/value/ObservableValueBase.java line 70:

> 68:     @Override
> 69:     public void addListener(InvalidationListener listener) {
> 70:         LISTENER_MANAGER.addListener((ObservableValueBase<Object>) this, listener);

See the comment for `ObjectPropertyBase`.

modules/javafx.base/src/test/java/test/com/sun/javafx/binding/ArrayManagerTest.java line 3:

> 1: package test.com.sun.javafx.binding;
> 2: 
> 3: import static org.junit.Assert.assertNull;

Wrong import

modules/javafx.base/src/test/java/test/com/sun/javafx/binding/ArrayManagerTest.java line 91:

> 89: 
> 90:         @Test
> 91:         void setSshouldRejectSettingIllegalIndices() {

Typo: `Sshould`

-------------

PR Review: https://git.openjdk.org/jfx/pull/1081#pullrequestreview-1386650763
PR Comment: https://git.openjdk.org/jfx/pull/1081#issuecomment-2018017130
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1167686027
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1167686153
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1167686217
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1167686291
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1167686304
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1167686565
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1167686624
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1167687366
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1167687470
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1272690289
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1272692011
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1167688825
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1167688937
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1167688953
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1167690228
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1167690295
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1167690187
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1167690735
PR Review Comment: https://git.openjdk.org/jfx/pull/1081#discussion_r1167690436


More information about the openjfx-dev mailing list