Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8154784

ObservableList wrapped in FilteredList wrapped in SortedList crashes given some sequence of changes

XMLWordPrintable

    • Icon: Bug Bug
    • Resolution: Duplicate
    • Icon: P4 P4
    • None
    • 8u91
    • javafx
    • x86
    • other

      FULL PRODUCT VERSION :
      java version "1.8.0_91"
      Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
      Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)

      ADDITIONAL OS VERSION INFORMATION :
      Microsoft Windows [version 10.0.10586]

      A DESCRIPTION OF THE PROBLEM :
      To get filtering and sorting to work properly on a TableView, it is generally advised to wrap the ObservableList of items into a FilteredList, itself wrapped in a SortedList

      But if a number of subsequent changes are made to the items (namely property updates and item removals), then either the SortedList or the FilteredList ends up throwing Exceptions, starting with java.lang.ArrayIndexOutOfBoundsException

      If an extractor was registered with the ObservableList, then it happens upon property update ; otherwise it happens upon removal.

      This happens even if the SortedList isn't bound to any Node as items

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      1. Construct an ObservableList of any class with properties (with or without extractor)
      2. Wrap this list into a FilteredList with a predicate that filters out based on one property
      3. Wrap this FilteredList into a SortedList with a comparator
      4. Bind the SortedList to a Node (say as items for a TableView), or don't
      5. Make a random mix of additions, removals and property updates on the original ObservableList

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      It should keep on working indefinitely with no exceptions
      ACTUAL -
      You will end up with an ArrayIndexOutOfBoundsException, followed by other types of Exceptions, eventually rendering any control bound to the SortedList unresponsive or buggy

      As a bonus, your application won't shut down when you close the main window as an uncaught exception has been thrown

      Sometimes it takes a lot of changes to occur, but it usually happens quite fast

      ERROR MESSAGES/STACK TRACES THAT OCCUR :
      Uncaught exception:
      java.lang.ArrayIndexOutOfBoundsException: -13
      at javafx.collections.transformation.SortedList.findPosition(SortedList.java:318)
      at javafx.collections.transformation.SortedList.removeFromMapping(SortedList.java:359)
      at javafx.collections.transformation.SortedList.addRemove(SortedList.java:389)
      at javafx.collections.transformation.SortedList.sourceChanged(SortedList.java:105)
      at javafx.collections.transformation.TransformationList.lambda$getListener$15(TransformationList.java:106)
      at javafx.collections.WeakListChangeListener.onChanged(WeakListChangeListener.java:88)
      at com.sun.javafx.collections.ListListenerHelper$SingleChange.fireValueChangedEvent(ListListenerHelper.java:164)
      at com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(ListListenerHelper.java:73)
      at javafx.collections.ObservableListBase.fireChange(ObservableListBase.java:233)
      at javafx.collections.ListChangeBuilder.commit(ListChangeBuilder.java:524)
      at javafx.collections.ListChangeBuilder.endChange(ListChangeBuilder.java:541)
      at javafx.collections.ObservableListBase.endChange(ObservableListBase.java:205)
      at javafx.collections.transformation.FilteredList.sourceChanged(FilteredList.java:147)
      at javafx.collections.transformation.TransformationList.lambda$getListener$15(TransformationList.java:106)
      at javafx.collections.WeakListChangeListener.onChanged(WeakListChangeListener.java:88)
      at com.sun.javafx.collections.ListListenerHelper$SingleChange.fireValueChangedEvent(ListListenerHelper.java:164)
      at com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(ListListenerHelper.java:73)
      at javafx.collections.ObservableListBase.fireChange(ObservableListBase.java:233)
      at javafx.collections.ListChangeBuilder.commit(ListChangeBuilder.java:524)
      at javafx.collections.ListChangeBuilder.endChange(ListChangeBuilder.java:541)
      at javafx.collections.ObservableListBase.endChange(ObservableListBase.java:205)
      at com.sun.javafx.collections.ObservableListWrapper.removeAll(ObservableListWrapper.java:185)
      at application.Main2.lambda$2(Main2.java:200)
      at com.sun.javafx.application.PlatformImpl.lambda$null$173(PlatformImpl.java:295)
      at java.security.AccessController.doPrivileged(Native Method)
      at com.sun.javafx.application.PlatformImpl.lambda$runLater$174(PlatformImpl.java:294)
      at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
      at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
      at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
      at java.lang.Thread.run(Thread.java:745)
      (12 items in list)

      REPRODUCIBILITY :
      This bug can be reproduced often.

      ---------- BEGIN SOURCE ----------
      package application;

      import java.lang.Thread.UncaughtExceptionHandler;
      import java.util.ArrayList;
      import java.util.HashSet;
      import java.util.List;
      import java.util.Random;
      import java.util.Set;
      import java.util.Timer;
      import java.util.TimerTask;

      import javafx.application.Application;
      import javafx.application.Platform;
      import javafx.beans.Observable;
      import javafx.beans.property.ReadOnlyIntegerProperty;
      import javafx.beans.property.SimpleIntegerProperty;
      import javafx.beans.property.SimpleStringProperty;
      import javafx.beans.property.StringProperty;
      import javafx.collections.FXCollections;
      import javafx.collections.ObservableList;
      import javafx.collections.transformation.FilteredList;
      import javafx.collections.transformation.SortedList;
      import javafx.stage.Stage;

      public class Main2 extends Application {

      public static void main(String[] args) {
      System.out.println(Runtime.class.getPackage().getImplementationVendor() + " " + Runtime.class.getPackage().getImplementationTitle() + " "
      + Runtime.class.getPackage().getImplementationVersion());
      launch(args);
      }

      // A Class representing any kind of object with properties
      public static class AnyThing {
      private static int used_id = 0; // Unique id

      // Autoincrement id
      private final ReadOnlyIntegerProperty id = new SimpleIntegerProperty(used_id++);
      // String properties
      private final StringProperty first = new SimpleStringProperty();
      private final StringProperty second = new SimpleStringProperty();
      private final StringProperty third = new SimpleStringProperty();

      public AnyThing() {
      super();
      }

      public final ReadOnlyIntegerProperty idProperty() {
      return this.id;
      }

      public final int getId() {
      return this.idProperty().get();
      }

      public final StringProperty firstProperty() {
      return this.first;
      }

      public final java.lang.String getFirst() {
      return this.firstProperty().get();
      }

      public final void setFirst(final java.lang.String first) {
      this.firstProperty().set(first);
      }

      public final StringProperty secondProperty() {
      return this.second;
      }

      public final java.lang.String getSecond() {
      return this.secondProperty().get();
      }

      public final void setSecond(final java.lang.String second) {
      this.secondProperty().set(second);
      }

      public final StringProperty thirdProperty() {
      return this.third;
      }

      public final java.lang.String getThird() {
      return this.thirdProperty().get();
      }

      public final void setThird(final java.lang.String third) {
      this.thirdProperty().set(third);
      }

      }

      // Used to randomize everything
      private final Random rnd = new Random();
      // Primary stage
      private Stage stage;
      // Main list of items
      private ObservableList<AnyThing> items;
      // Uncaught exception stop trigger
      private volatile boolean stop = false;

      private Timer t = new Timer();

      @Override
      public void start(Stage primaryStage) throws Exception {
      // Title and memorize primary stage
      stage = primaryStage;
      stage.setTitle("Filtered and Sorted table test");
      items = FXCollections.observableList(makeAnyThings(10)
      // Uncomment this to enable auto filter/sort on update by registering an extractor
      // , an -> {
      // return new Observable[] { an.firstProperty(), an.secondProperty(), an.thirdProperty() };
      // }
      );
      final FilteredList<AnyThing> fl = new FilteredList<>(items, an -> !an.getFirst().toLowerCase().startsWith("z"));
      final SortedList<AnyThing> sl = new SortedList<>(fl);
      sl.setComparator((x, y) -> {
      if (x == null)
      return y == null ? 0 : -1;
      if (y == null)
      return 1;
      if (x.getFirst() == null)
      return y.getFirst() == null ? 0 : -1;
      if (y.getFirst() == null)
      return 1;
      return x.getFirst().compareTo(y.getFirst());
      });

      // Handle uncaught exceptions so we immediately display them to Standard Error and stop processing
      Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
      @Override
      public void uncaughtException(Thread t, Throwable e) {
      System.err.println("Uncaught exception:");
      e.printStackTrace();
      stop = true;
      }
      });

      System.out.println("Starting");
      System.out.println(" (" + items.size() + " items in list)");
      doRandomAction();

      }

      //Perform a random action in JavaFX Thread and reschedule
      private void doRandomAction() {
      if (stop) {
      t.cancel();
      System.exit(0);
      return;
      }
      Platform.runLater(() -> {
      int rd = rnd.nextInt(100);
      if (rd < 50) {
      if (items.size() > 0) {
      // Modify a random number of objects between 1 and
      // items.size() in table
      int max = rnd.nextInt(Math.max(1, items.size() - 1)) + 1;
      System.out.println("~ Making " + max + " changes:");
      for (int i = 0; i < max; i++) {
      // Each change is a complete rewrite of either of the
      // string fields
      int pos = rnd.nextInt(items.size());
      AnyThing an = items.get(pos);
      int prop = rnd.nextInt(3);
      String s = makeRandomString();
      switch (prop) {
      case 0:
      System.out.println(" - Changing " + an.getId() + ".first from " + an.getFirst() + " to " + s);
      an.setFirst(s);
      break;
      case 1:
      System.out.println(" - Changing " + an.getId() + ".second from " + an.getSecond() + " to " + s);
      an.setSecond(s);
      break;
      default:
      System.out.println(" - Changing " + an.getId() + ".third from " + an.getThird() + " to " + s);
      an.setThird(s);
      break;
      }
      }
      }
      } else if (rd < 70) {
      // Add 5 randomly generated items to the table
      System.out.println("+ Adding 5 items");
      items.addAll(makeAnyThings(5));
      System.out.println(" (" + items.size() + " items in list)");
      } else {
      if (items.size() > 0) {
      // Remove a random number of objects between 1 and
      // items.size() / 2 from table
      Set<AnyThing> removes = new HashSet<>(items.size());
      int max = rnd.nextInt(Math.max(1, items.size() / 2)) + 1;
      System.out.println("- Removing " + max + " items");
      for (int i = 0; i < max; i++) {
      int pos = rnd.nextInt(items.size());
      removes.add(items.get(pos));
      }
      items.removeAll(removes);
      System.out.println(" (" + items.size() + " items in list)");
      }
      }
      });
      t.schedule(new TimerTask() {
      @Override
      public void run() {
      doRandomAction();
      }
      }, (rnd.nextInt(10) + 1) * 100);
      }

      // Randomly generate a given number of AnyThing objects
      private List<AnyThing> makeAnyThings(final int number) {
      final List<AnyThing> lst = new ArrayList<>(number);
      for (int i = 0; i < number; i++) {
      AnyThing at = new AnyThing();
      at.setFirst(makeRandomString());
      at.setSecond(makeRandomString());
      at.setThird(makeRandomString());
      lst.add(at);
      }
      return lst;
      }

      // Make a random string as a combination of [ a-zA-Z] (1 to 30 characters
      // trimmed)
      private String makeRandomString() {
      final StringBuilder sb = new StringBuilder();
      for (int i = 0, max = rnd.nextInt(30) + 1; i < max; i++) {
      int c = rnd.nextInt(56);
      if (c < 4)
      c = 32;
      else
      c = c - 4 + 65;
      if (c > 90)
      c += 6;
      sb.append((char) c);
      }
      return sb.toString().trim();
      }
      }
      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      Nothing very conclusive : just stop using ObservableList -> FilteredList -> SortedList as TableView items, and make my own implementation of Filtered / Sorted TableView, which would be slow and messy, unless I spend a lot of time on it.

            vadim Vadim Pakhnushev
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated:
              Resolved: