ADDITIONAL SYSTEM INFORMATION :
I run on microsoft windows & Java 17 / JavaFX17.
But the error is also in older version comibinations like Java 11 / JavaFX 16.
A DESCRIPTION OF THE PROBLEM :
The error can be located in a wrong implementation of javafx.collections.transformation.SortedList.sourceChanged() under certain conditions.
If the element in the source list is replaced by set(index, element), the standard ObservableList creates a ListChangeEvent with 1 Remove and 1 Add. The SortedList does not replace the element at the same position if there is another element which compares to 0, as it is first replaced and then added after the other element.
This can be very problematic e.g. in a table, if the column based comparator compares different records to 0, and we update a record by using set(index, element) .
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
JUnit test below:
* Create an ObservableList
* Create a SortedList on that
* Bind a comparator to the sorted list, that two elements have a 0 comparison result
* Change the first of the two elements by an exact copy of itself
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The sorted list should not have a different sorting.
If the comparator returns 0, the order in the original list should then be the comparison result.
ACTUAL -
The sort order changes
---------- BEGIN SOURCE ----------
package de.heitec.empic.centralsuite.gui.javafx;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.junit.Assert;
import org.junit.Test;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
public class SortedListTest
{
@Test
public void testSorting()
{
// Create a list.
ObservableList<TestDto> originalList = FXCollections.observableArrayList(List.of(
new TestDto('C', 2),
new TestDto('C', 1),
new TestDto('A', 1)));
// Create a sorted list. The comparator cannot distinguish between the first and the second element
SortedList<TestDto> sortedList = new SortedList<>(originalList, Comparator.comparing(TestDto::getValue));
Assert.assertEquals("C2;C1;A1", toDebugString(originalList));
// The comparator result of C1 and C2 is the same, but C2 is in front of C1 in the origin list
Assert.assertEquals("A1;C2;C1", toDebugString(sortedList));
// We update the value of the second element of the original list with a copy of the same value
originalList.set(1, new TestDto('C', 1));
Assert.assertEquals("C2;C1;A1", toDebugString(originalList));
// HERE COMES THE ERROR:
// We expect the sorted list to be identical to the status above
// but instead of that the two results C2 and C1 are mixed
Assert.assertEquals("A1;C2;C1", toDebugString(sortedList)); // In JavaFX 16 & 17.0.6 "A1;C1;C2" is the result
}
private String toDebugString(List<TestDto> testDtos)
{
return testDtos.stream()
.map(TestDto::toString)
.collect(Collectors.joining(";"));
}
private static class TestDto
{
private final Character value;
private final int otherIdentifier;
TestDto(Character value, int otherIdentifier)
{
this.value = value;
this.otherIdentifier = otherIdentifier;
}
public Character getValue()
{
return value;
}
public int getOtherIdentifier()
{
return otherIdentifier;
}
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (o == null || getClass() != o.getClass())
{
return false;
}
TestDto testDto = (TestDto) o;
return value.equals(testDto.value);
}
@Override
public int hashCode()
{
return Objects.hash(value);
}
@Override
public String toString()
{
return value.toString() + otherIdentifier;
}
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
No usable workaround,
Maybe copy the whole base list, change it on the copy and use setAll() would help. But using that on tables would have enormous consequences.
FREQUENCY : always
I run on microsoft windows & Java 17 / JavaFX17.
But the error is also in older version comibinations like Java 11 / JavaFX 16.
A DESCRIPTION OF THE PROBLEM :
The error can be located in a wrong implementation of javafx.collections.transformation.SortedList.sourceChanged() under certain conditions.
If the element in the source list is replaced by set(index, element), the standard ObservableList creates a ListChangeEvent with 1 Remove and 1 Add. The SortedList does not replace the element at the same position if there is another element which compares to 0, as it is first replaced and then added after the other element.
This can be very problematic e.g. in a table, if the column based comparator compares different records to 0, and we update a record by using set(index, element) .
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
JUnit test below:
* Create an ObservableList
* Create a SortedList on that
* Bind a comparator to the sorted list, that two elements have a 0 comparison result
* Change the first of the two elements by an exact copy of itself
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The sorted list should not have a different sorting.
If the comparator returns 0, the order in the original list should then be the comparison result.
ACTUAL -
The sort order changes
---------- BEGIN SOURCE ----------
package de.heitec.empic.centralsuite.gui.javafx;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.junit.Assert;
import org.junit.Test;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
public class SortedListTest
{
@Test
public void testSorting()
{
// Create a list.
ObservableList<TestDto> originalList = FXCollections.observableArrayList(List.of(
new TestDto('C', 2),
new TestDto('C', 1),
new TestDto('A', 1)));
// Create a sorted list. The comparator cannot distinguish between the first and the second element
SortedList<TestDto> sortedList = new SortedList<>(originalList, Comparator.comparing(TestDto::getValue));
Assert.assertEquals("C2;C1;A1", toDebugString(originalList));
// The comparator result of C1 and C2 is the same, but C2 is in front of C1 in the origin list
Assert.assertEquals("A1;C2;C1", toDebugString(sortedList));
// We update the value of the second element of the original list with a copy of the same value
originalList.set(1, new TestDto('C', 1));
Assert.assertEquals("C2;C1;A1", toDebugString(originalList));
// HERE COMES THE ERROR:
// We expect the sorted list to be identical to the status above
// but instead of that the two results C2 and C1 are mixed
Assert.assertEquals("A1;C2;C1", toDebugString(sortedList)); // In JavaFX 16 & 17.0.6 "A1;C1;C2" is the result
}
private String toDebugString(List<TestDto> testDtos)
{
return testDtos.stream()
.map(TestDto::toString)
.collect(Collectors.joining(";"));
}
private static class TestDto
{
private final Character value;
private final int otherIdentifier;
TestDto(Character value, int otherIdentifier)
{
this.value = value;
this.otherIdentifier = otherIdentifier;
}
public Character getValue()
{
return value;
}
public int getOtherIdentifier()
{
return otherIdentifier;
}
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (o == null || getClass() != o.getClass())
{
return false;
}
TestDto testDto = (TestDto) o;
return value.equals(testDto.value);
}
@Override
public int hashCode()
{
return Objects.hash(value);
}
@Override
public String toString()
{
return value.toString() + otherIdentifier;
}
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
No usable workaround,
Maybe copy the whole base list, change it on the copy and use setAll() would help. But using that on tables would have enormous consequences.
FREQUENCY : always
- links to
-
Review(master) openjdk/jfx/1519