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