JDK-8146325 : Spinner throws a ClassCastException under Linux
  • Type: Bug
  • Component: javafx
  • Sub-Component: controls
  • Affected Version: 8u66
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: linux
  • CPU: x86_64
  • Submitted: 2015-12-04
  • Updated: 2020-01-31
  • Resolved: 2016-03-01
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 8 JDK 9
8u102Fixed 9Fixed
Description
FULL PRODUCT VERSION :
java version "1.8.0_66"
Java(TM) SE Runtime Environment (build 1.8.0_66-b17)
Java HotSpot(TM) 64-Bit Server VM (build 25.66-b17, mixed mode)


ADDITIONAL OS VERSION INFORMATION :
Linux pc682 3.13.0-68-generic #111-Ubuntu SMP Fri Nov 6 18:17:06 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux


A DESCRIPTION OF THE PROBLEM :
An JavaFX Spinner<Integer> throws a "java.lang.ClassCastException: java.lang.Double cannot be cast to java.lang.Integer" exception under Linux whereas it perfectly works under Windows.
The problem seems to be that the JVM uses the order of the constructors of Spinner to decide whether to instantiate a Spinner<Double> or Spinner<Integer> and this constructor order seems to be interpreted differently in the Windows JVM and the Linux JVM.
That's why a Java program with a Spinner<Integer> that runs nicely under Windows throws an exception under Linux.


STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Use the example code from the description, start the application and change the spinner value.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The Spinner should not throw an exception.
ACTUAL -
The Spinner throws the following exception (under Linux):
Exception in thread "JavaFX Application Thread" java.lang.ClassCastException: java.lang.Double cannot be cast to java.lang.Integer
	at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:361)
	at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
	at javafx.beans.property.ReadOnlyObjectWrapper$ReadOnlyPropertyImpl.fireValueChangedEvent(ReadOnlyObjectWrapper.java:176)
	at javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:142)
	at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
	at javafx.beans.property.ObjectPropertyBase.access$000(ObjectPropertyBase.java:51)
	at javafx.beans.property.ObjectPropertyBase$Listener.invalidated(ObjectPropertyBase.java:233)
	at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:349)
	at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
	at javafx.beans.property.ObjectPropertyBase.fireValueChangedEvent(ObjectPropertyBase.java:105)
	at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
	at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146)
	at javafx.scene.control.SpinnerValueFactory.setValue(SpinnerValueFactory.java:150)
	at javafx.scene.control.SpinnerValueFactory$DoubleSpinnerValueFactory.increment(SpinnerValueFactory.java:850)
	at javafx.scene.control.Spinner.increment(Spinner.java:394)
	at com.sun.javafx.scene.control.behavior.SpinnerBehavior.increment(SpinnerBehavior.java:132)
	at com.sun.javafx.scene.control.behavior.SpinnerBehavior.lambda$new$209(SpinnerBehavior.java:62)
	at com.sun.javafx.scene.control.behavior.SpinnerBehavior.startSpinning(SpinnerBehavior.java:151)
	at com.sun.javafx.scene.control.skin.SpinnerSkin.lambda$new$468(SpinnerSkin.java:99)
	at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)
	at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
	at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
	at com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
	at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
	at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
	at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
	at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
	at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
	at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
	at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
	at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
	at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
	at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
	at javafx.event.Event.fireEvent(Event.java:198)
	at javafx.scene.Scene$MouseHandler.process(Scene.java:3757)
	at javafx.scene.Scene$MouseHandler.access$1500(Scene.java:3485)
	at javafx.scene.Scene.impl_processMouseEvent(Scene.java:1762)
	at javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2494)
	at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:352)
	at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:275)
	at java.security.AccessController.doPrivileged(Native Method)
	at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$354(GlassViewEventHandler.java:388)
	at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389)
	at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:387)
	at com.sun.glass.ui.View.handleMouseEvent(View.java:555)
	at com.sun.glass.ui.View.notifyMouse(View.java:937)
	at com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
	at com.sun.glass.ui.gtk.GtkApplication.lambda$null$49(GtkApplication.java:139)
	at java.lang.Thread.run(Thread.java:745)


REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
Here is a minimal example that shows the bug when you change the Spinner value:

TestApplication.java
--------------------
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.net.URL;

public class TestApplication extends Application {

	@Override
	public void start(Stage stage) throws Exception {
		URL resource = getClass().getResource("test.fxml");
		FXMLLoader loader = new FXMLLoader(resource);
		Parent root = loader.load();

		stage.setScene(new Scene(root));

		stage.show();
	}

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

}

TestController.java
-------------------
import javafx.fxml.Initializable;
import javafx.scene.control.Spinner;

import java.net.URL;
import java.util.ResourceBundle;

public class TestController implements Initializable {

	public Spinner<Integer> smoothingSpinner;

	@Override
	public void initialize(URL location, ResourceBundle resources) {
		smoothingSpinner.valueProperty().addListener((observable, oldValue, newValue) -> {
			System.out.println(newValue);
		});
	}

}

test.fxml
---------
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Spinner?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.VBox?>
<VBox prefHeight="200.0" prefWidth="300.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="TestController">
  <Spinner fx:id="smoothingSpinner" editable="true" min="0" max="100" initialValue="3" />
</VBox>

---------- END SOURCE ----------


Comments
We can still consider that in JDK 9, although the need is less urgent. The current FXML-only fix is still a good fix, since we were able to backport that to 8 (whereas we would not backport the deprecation of the constructors / new factory methods). If you like, you can file a new RFE to deprecate the public constructors and add the factory methods.
02-03-2016

So only the FXML changes are added? I still think the public constructors are a bad API and should be deprecated and replaced by static factory methods.
02-03-2016

We should backport this to JDK 8. Approved for backport to 8u-dev (8u82).
01-03-2016

Changeset: d0ab046c39bb Author: vadim Date: 2016-03-01 17:37 +0300 URL: http://hg.openjdk.java.net/openjfx/9-dev/rt/rev/d0ab046c39bb
01-03-2016

+1
01-03-2016

Kevin, More correctly, it will sort the constructors so that if argument types are different and one of it is int and another is double then the constructor with int type will come first, otherwise it will sort them by names. So if arguments are int it will try int constructor first and will succeed. But if the type is double it will still try to coerce it to the int, will fail and then try double constructor. So this is very specific fix for this specific class. BTW, I botched the test (argument types were float) and reworked it: http://cr.openjdk.java.net/~vadim/8146325/webrev.01/ I tried to mitigate the fact that running on different platforms result in different order of methods by testing two classes with different order (which seems to work at least on Windows and Linux). Also added a test for two types of construction: with exact arguments and with additional setter. And verified that both tests (integer) fail without the fix.
01-03-2016

I'll take a closer look, but I very much like the concept of the fix. If I understand correctly this change will first try the int constructor, which will work iff all 3 args are integers, and failing that, will call the double constructor, right?
29-02-2016

Vadim, Thanks for tackling this from the FXML side. I think your webrev looks pretty good.
29-02-2016

Jonathan, Unfortunately, new static methods will not help in case of FXML, they need to be no-arg in order to work with fx:factory declarations. Here's my attempt to create a very specific hack which imposes an order for the constructors so that int constructors always goes before float. I've verified that Spinner class is the only class with identical annotated constructors with different types. http://cr.openjdk.java.net/~vadim/8146325/webrev.00/
29-02-2016

The changes look ok for me. From a Java-API user point of view I think they are the way to move forward, the only other solution for FXML would be to have IntegerSpinner, DoubleSpinner, ... which feel so wrong if you use them from Java code that I would not consider them valid options.
25-02-2016

I'd like to discuss this further before approving the API change. It might be the right thing to do to deprecate these constructors, but we should consider all the options once Vadim has looked into the best FXML solution.
24-02-2016

Reassigning to myself - not entirely sure why I assigned this to Leif. Must have been a case of too many tabs being opened!
24-02-2016

Attaching patch that deprecates most constructors of Spinner and introduce static methods. I have not added logging however. Separately, Vadim is looking into how best to progress handling inside FXML, and he has one option he is exploring.
24-02-2016

The actual workaround is based on Tom's idea but a little simpler since FXML can in fact load inner classes, one only needs to import whole package: <?import javafx.scene.control.*?> ... <Spinner fx:id="smoothingSpinner" editable="true"> <valueFactory> <SpinnerValueFactory.IntegerSpinnerValueFactory min="0" max="100" initialValue="3" /> </valueFactory> </Spinner>
18-02-2016

Let me start with the fact that I think the Spinner(int,int,int), Spinner(double,double,double) violate every typesafte I've seen in java since a long long time. They are convenience for the SpinnerValueFactory and so should have provided as static factory methods. Spinner.createInteger() : Spinner<Integer>, Spinner#createDouble() : Spinner<Double>, ... . In general the problem we see here is that the order in which reflection APIs return constructors is arbitary hence it's pure luck that it works on other platforms today! The work around for Java8 is to use the valueFactory for init unfortunately the decision was made to make the default value factories inner-classes of SpinnerValueFactory so unless mistaken you can not use them in FXML easily but you have to provide you custom class who extends eg IntegerSpinnerValueFactory. So the Java8 work around is to use <?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.layout.VBox?> <?import javafx.scene.control.Spinner?> <?import application.MyWrapperFactory?> <VBox xmlns:fx="http://javafx.com/fxml/1"> <Spinner> <valueFactory> <MyWrapperFactory min="0" max="100" initialValue="3" /> </valueFactory> </Spinner> </VBox> For Java9 I would suggest: * deprecate the constructors ===> and log an error if used so that application who work today will work today are informed about that they work by luck * provide static helper methods create(int,int,int) : Spinner<Integer> ... * make the default factories standalone classes
17-02-2016

The test application works properly on OS X with both Java 9-ea+104 and Java 8u72.
17-02-2016

Leif - could you please take a look at this and let me know if you can reproduce it? Thanks
05-01-2016