JDK-8154784 : ObservableList wrapped in FilteredList wrapped in SortedList crashes given some sequence of changes
  • Type: Bug
  • Component: javafx
  • Sub-Component: base
  • Affected Version: 8u91
  • Priority: P4
  • Status: Closed
  • Resolution: Duplicate
  • OS: other
  • CPU: x86
  • Submitted: 2016-04-20
  • Updated: 2016-04-26
  • Resolved: 2016-04-26
Related Reports
Duplicate :  
Description
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.