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

Nir Lisker nlisker at openjdk.org
Tue Feb 14 17:39:55 UTC 2023


On Sun, 5 Feb 2023 20:11:35 GMT, John Hendrikx <jhendrikx at openjdk.org> wrote:

> I don't think we should care about depth-first, breadth-first. The only thing that I think is important here is that the contract of ChangeListener is respected. I think that that contract should be:
...

I'll be more concrete. Here is my test program:


public class ListenersTest {

    private static int inv = 0;

    public static void main(String[] args) {
        with1Change();
    }

    static void with1Change() {
        inv = 0;
        var property = new SimpleIntegerProperty(0);

        ChangeListener<? super Number> listenerA = (obs, ov, nv) -> {
            inv++;
            String spaces = IntStream.range(1, inv).mapToObj(i -> "  ").reduce(String::concat).orElse("") + inv;
            System.out.println(spaces + " bA " + ov + "->" + nv + " (" + property.get() + ")");
            property.set(5);
            System.out.println(spaces + " aA " + ov + "->" + nv + " (" + property.get() + ")");
        };
        property.addListener(listenerA);

        ChangeListener<? super Number> listenerB = (obs, ov, nv) -> {
            inv++;
            String spaces = IntStream.range(1, inv).mapToObj(i -> "  ").reduce(String::concat).orElse("") + inv;
            System.out.println(spaces + " bB "  + ov + "->" + nv + " (" + property.get() + ")");
            System.out.println(spaces + " aB " + ov + "->" + nv + " (" + property.get() + ")");
        };
        property.addListener(listenerB);

        property.set(1);
        System.out.println("---------\n");
    }
}


With the patch:

1 bA 0->1 (1)
1 aA 0->1 (5)
  2 bB 0->1 (5)
  2 aB 0->1 (5)
    3 bA 1->5 (5)
    3 aA 1->5 (5)
      4 bB 1->5 (5)
      4 aB 1->5 (5)

With your patch, each event finishes its run and only then the next event happens. This is the "breadth-first" approach.
However, there is another one:

1 bA 0->1 (1)
  2 bA 1->5 (5)
  2 aA 1->5 (5)
    3 bB 1->5 (5)
    3 aB 1->5 (5)
1 aA 0->1 (5)
      4 bB 0->1 (5)
      4 aB 0->1 (5)

This approach starts events before the previous ones finished, and goes back to the original event later. This is the "depth-first" approach. I don't think that either is wrong. This one makes sense and it's a behavior I can reason about: the listener is loyal to the event at the time it happened (and the "real" value is accessible with `get`).

Without the patch:

1 bA 0->1 (1)
  2 bA 1->5 (5)
  2 aA 1->5 (5)
    3 bB 1->5 (5)
    3 aB 1->5 (5)
1 aA 0->1 (5)
      4 bB 0->5 (5)
      4 aB 0->5 (5)

I agree that at step 4 the 0->5 event is wrong because the events are only 0->1 and 1->5.

If you comment out the line `property.addListener(listenerB);` (only register A), then both with and without the patch I get

1 bA 0->1 (1)
  2 bA 1->5 (5)
  2 aA 1->5 (5)
1 aA 0->1 (5)

while with delaying nested events I would expect:

1 bA 0->1 (1)
1 aA 0->1 (5)
  2 bA 1->5 (5)
  2 aA 1->5 (5)

So this looks inconsistent to me.

The fix for the lock being released is good regardless, it's the behavioral change that I'm not sold on.

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

PR: https://git.openjdk.org/jfx/pull/837


More information about the openjfx-dev mailing list