JavaFX Spinner 空文本 nullpointerexception [英] JavaFX Spinner empty text nullpointerexception
问题描述
我有一个问题,如果清除编辑器文本并提交,然后单击递增或递减按钮,则可编辑的 JavaFX 8 Spinner
会导致未捕获的 NullPointerException
.这是j8u60 j8u77.运气好的话,递增/递减按钮会卡在按下状态,NPE 会继续锁定应用程序.
I have an issue where an editable JavaFX 8 Spinner
causes an uncaught NullPointerException
if one clears the editor text and commits and then clicks either the increment or decrement button. This is j8u60 j8u77. With some luck the increment/decrement button will get stuck in depressed state and the NPE's keep flowing locking up the application.
以下代码为我重现了该问题:
The following code reproduces the issue for me:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.control.SpinnerValueFactory.IntegerSpinnerValueFactory;
import javafx.stage.Stage;
public class Test extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage aPrimaryStage) throws Exception {
IntegerSpinnerValueFactory valueFactory = new IntegerSpinnerValueFactory(0, 10);
Spinner<Integer> spinner = new Spinner<>(valueFactory);
spinner.setEditable(true);
aPrimaryStage.setScene(new Scene(spinner));
aPrimaryStage.show();
}
}
运行它,清除文本,按回车键(NullPointerException
),点击增加或减少按钮现在也会导致 NPE.
Run it, clear the text, press enter (NullPointerException
), clicking either increment or decrement button will now also cause NPE.
谁能确认这是一个 JavaFX 错误并提出解决方法?
Can any one confirm that this is a JavaFX bug and suggest a workaround?
异常堆栈跟踪
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
at javafx.scene.control.SpinnerValueFactory$IntegerSpinnerValueFactory.lambda$new$215(SpinnerValueFactory.java:475)
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.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.Spinner.lambda$new$210(Spinner.java:139)
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:49)
at javafx.event.Event.fireEvent(Event.java:198)
at javafx.scene.Node.fireEvent(Node.java:8411)
at com.sun.javafx.scene.control.behavior.TextFieldBehavior.fire(TextFieldBehavior.java:179)
at com.sun.javafx.scene.control.behavior.TextInputControlBehavior.callAction(TextInputControlBehavior.java:178)
at com.sun.javafx.scene.control.behavior.BehaviorBase.callActionForEvent(BehaviorBase.java:218)
at com.sun.javafx.scene.control.behavior.TextInputControlBehavior.callActionForEvent(TextInputControlBehavior.java:127)
at com.sun.javafx.scene.control.behavior.BehaviorBase.lambda$new$74(BehaviorBase.java:135)
at com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218)
at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
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:49)
at javafx.event.Event.fireEvent(Event.java:198)
at javafx.scene.Node.fireEvent(Node.java:8411)
at com.sun.javafx.scene.control.skin.SpinnerSkin.lambda$new$473(SpinnerSkin.java:151)
at com.sun.javafx.event.CompositeEventHandler$NormalEventFilterRecord.handleCapturingEvent(CompositeEventHandler.java:282)
at com.sun.javafx.event.CompositeEventHandler.dispatchCapturingEvent(CompositeEventHandler.java:98)
at com.sun.javafx.event.EventHandlerManager.dispatchCapturingEvent(EventHandlerManager.java:223)
at com.sun.javafx.event.EventHandlerManager.dispatchCapturingEvent(EventHandlerManager.java:180)
at com.sun.javafx.event.CompositeEventDispatcher.dispatchCapturingEvent(CompositeEventDispatcher.java:43)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:52)
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$KeyHandler.process(Scene.java:3964)
at javafx.scene.Scene$KeyHandler.access$1800(Scene.java:3910)
at javafx.scene.Scene.impl_processKeyEvent(Scene.java:2040)
at javafx.scene.Scene$ScenePeerListener.keyEvent(Scene.java:2501)
at com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:197)
at com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:147)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleKeyEvent$353(GlassViewEventHandler.java:228)
at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389)
at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleKeyEvent(GlassViewEventHandler.java:227)
at com.sun.glass.ui.View.handleKeyEvent(View.java:546)
at com.sun.glass.ui.View.notifyKey(View.java:966)
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)
推荐答案
我翻遍了 JDK 源代码.
I had a rummage through the JDK source.
NPE 是从 if (newValue < getMin()) {
在侦听器 lambda 中抛出的:
The NPE is thrown from if (newValue < getMin()) {
in the listener lambda here:
javafx.scene.control.SpinnerValueFactory.java
javafx.scene.control.SpinnerValueFactory.java
public IntegerSpinnerValueFactory(@NamedArg("min") int min,
@NamedArg("max") int max,
@NamedArg("initialValue") int initialValue,
@NamedArg("amountToStepBy") int amountToStepBy) {
setMin(min);
setMax(max);
setAmountToStepBy(amountToStepBy);
setConverter(new IntegerStringConverter());
valueProperty().addListener((o, oldValue, newValue) -> {
// when the value is set, we need to react to ensure it is a
// valid value (and if not, blow up appropriately)
if (newValue < getMin()) {
setValue(getMin());
} else if (newValue > getMax()) {
setValue(getMax());
}
});
setValue(initialValue >= min && initialValue <= max ? initialValue : min);
}
大概 newValue
是 null
并且 null
的自动拆箱会抛出 NPE.由于输入来自编辑器,我怀疑 IntegerStringConverter
这是默认转换器.
presumably newValue
is null
and the auto unboxing of null
throws NPE. As the input comes from the editor, I suspect the IntegerStringConverter
which is the default converter.
看这里的实现:
javafx.util.converter.IntegerStringConverter
javafx.util.converter.IntegerStringConverter
public class IntegerStringConverter extends StringConverter<Integer> {
/** {@inheritDoc} */
@Override public Integer fromString(String value) {
// If the specified value is null or zero-length, return null
if (value == null) {
return null;
}
value = value.trim();
if (value.length() < 1) {
return null;
}
return Integer.valueOf(value);
}
/** {@inheritDoc} */
@Override public String toString(Integer value) {
// If the specified value is null, return a zero-length String
if (value == null) {
return "";
}
return (Integer.toString(((Integer)value).intValue()));
}
}
我们看到它很乐意为空字符串返回null
,鉴于输入不存在有效值,这是一种合理的做法.
We see that it will happily return null
for the empty string, which is kind of reasonable given that there exists no valid value for the input.
跟踪调用堆栈我找到了值的来源:
Tracing up the call stack I find where the value is coming from:
javafx.scene.control.Spinner
javafx.scene.control.Spinner
public Spinner() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
setAccessibleRole(AccessibleRole.SPINNER);
getEditor().setOnAction(action -> {
String text = getEditor().getText();
SpinnerValueFactory<T> valueFactory = getValueFactory();
if (valueFactory != null) {
StringConverter<T> converter = valueFactory.getConverter();
if (converter != null) {
T value = converter.fromString(text);
valueFactory.setValue(value);
}
}
});
该值是用从转换器获得的值设置的 T value = converter.fromString(text);
大概是 null.在这一点上,我相信微调器类应该检查 value
是否不是 null
以及是否将以前的值恢复到编辑器.
The value is set with the value obtained from the converter T value = converter.fromString(text);
which presumably is null. At this point I believe that the spinner class should check that value
is not null
and if it is restore the previous value to the editor.
我现在相当确定这是一个错误.此外,我不认为使用从不返回 null 的转换器的解决方法会正常工作,因为它只会掩盖问题以及当值无法转换时应该返回什么值?
I am now fairly sure that this is a bug. Moreover I don't think that a work around with a converter that never returns null is going to work properly as it will only mask the problem and what value should be returned when the value cannot be converted?
替换 Spinner 编辑器的 onAction
以通过返回有效"拒绝无效输入政策解决了这个问题:
Replacing the onAction
of the spinner editor to reject invalid input with a "return to valid" policy fixes the issue:
public static <T> void fixSpinner2(Spinner<T> aSpinner) {
aSpinner.getEditor().setOnAction(action -> {
String text = aSpinner.getEditor().getText();
SpinnerValueFactory<T> factory = aSpinner.getValueFactory();
if (factory != null) {
StringConverter<T> converter = factory.getConverter();
if (converter != null) {
T value = converter.fromString(text);
if (null != value) {
factory.setValue(value);
}
else {
aSpinner.getEditor().setText(converter.toString(factory.getValue()));
}
}
}
action.consume();
});
}
与 valueProperty
上的侦听器相反,这避免了用无效数据触发其他侦听器.然而,这突出了 Spinner 类中的另一个问题.虽然以上通过按回车键返回有效值来解决问题.在不提交的情况下擦除输入(按 Enter)然后按递增或递减将导致相同的 NPE,但调用堆栈略有不同.
As opposed to a listener on the valueProperty
this avoids triggering other listeners with invalid data. However this highlights another issue in the spinner class. While the above fixes the issue by returning to a valid value on pressing enter. Erasing the input without committing (pressing enter) and then pressing increment or decrement will cause the same NPE but with slightly different call stack.
原因:
public void increment(int steps) {
SpinnerValueFactory<T> valueFactory = getValueFactory();
if (valueFactory == null) {
throw new IllegalStateException("Can't increment Spinner with a null SpinnerValueFactory");
}
commitEditorText();
valueFactory.increment(steps);
}
Decrement 类似,都调用下面的commitEditorText
:
Decrement is similar, both call into commitEditorText
below:
private void commitEditorText() {
if (!isEditable()) return;
String text = getEditor().getText();
SpinnerValueFactory<T> valueFactory = getValueFactory();
if (valueFactory != null) {
StringConverter<T> converter = valueFactory.getConverter();
if (converter != null) {
T value = converter.fromString(text);
valueFactory.setValue(value);
}
}
}
注意构造函数中 onAction
的复制粘贴:
Notice the copy-paste from the onAction
in the constructor:
getEditor().setOnAction(action -> {
String text = getEditor().getText();
SpinnerValueFactory<T> valueFactory = getValueFactory();
if (valueFactory != null) {
StringConverter<T> converter = valueFactory.getConverter();
if (converter != null) {
T value = converter.fromString(text);
valueFactory.setValue(value);
}
}
});
我认为 commitEditorText
应该改为在编辑器上触发 onAction
,而不是像这样:
I believe that commitEditorText
should be changed to trigger onAction
on the editor instead like so:
private void commitEditorText() {
if (!isEditable()) return;
getEditor().getOnAction().handle(new ActionEvent(this, this));
}
那么行为就会保持一致,并让编辑器有机会在输入进入值工厂之前对其进行处理.
then the behavior would be consistent and give the editor a chance to handle the input before it goes to the value factory.
这篇关于JavaFX Spinner 空文本 nullpointerexception的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!