JavaFX的Spinner在输入空文本时会引发NullPointerException异常。

11

我有一个问题,涉及可编辑的JavaFX 8 Spinner。如果清除编辑器文本并提交,然后单击增量或减量按钮,则会引发未捕获的NullPointerException。这是j8u60 j8u77版本的问题。运气好的话,增量/减量按钮将一直保持按下状态,NPE将不断流动,导致应用程序锁定。

以下代码可以重现此问题:

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。

有人可以确认这是JavaFX的错误并提供解决方法吗?

编辑:异常堆栈跟踪

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)

这不是一个错误。这是一个整数微调器,里面放了一个非整数值。它只能旋转有效的整数值。因此,这是预期的行为。您需要在代码中处理这个潜在的异常。只需将 Editable 设置为 false,就可以删除更改该值的能力。 - ManoDestra
3
@ManoDestra 看一下堆栈跟踪。 没有任何一点可以捕获它。 它完全是JavaFX内部的。 - VGR
因为上面的代码没有处理控件事件。但它仍然被设置为可编辑。如果您不希望出现异常,请将可编辑设置为false,或者处理控件事件(如果允许编辑)。简单 :) - ManoDestra
@ManoDestra请举个例子,说明你将如何处理这个问题。我同意VGR的看法,认为这个问题没有一个合理的处理点。 - Emily L.
1
@ManoDestra 这是一个bug。IntegerSpinnerValueFactoryconverter属性应该处理这个问题。 - James_D
在我的回答中添加了处理它的代码 :) - ManoDestra
5个回答

6

这是整数型Spinner控件的正确和预期行为。

如果您不希望用户通过工厂设置编辑值,则应将其Editable属性设置为false。

或者,您应该处理Spinner值属性引发的事件。

以下是如何处理此类事件的简单示例:

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;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;

public class Spin extends Application {
    Spinner<Integer> spinner;

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

    @Override
    public void start(Stage aPrimaryStage) throws Exception {
        IntegerSpinnerValueFactory valueFactory = new IntegerSpinnerValueFactory(0, 10);
        spinner = new Spinner<>(valueFactory);
        spinner.setEditable(true);
        spinner.valueProperty().addListener((observableValue, oldValue, newValue) -> handleSpin(observableValue, oldValue, newValue));

        aPrimaryStage.setScene(new Scene(spinner));
        aPrimaryStage.show();
    }

    private void handleSpin(ObservableValue<?> observableValue, Number oldValue, Number newValue) {
        try {
            if (newValue == null) {
                spinner.getValueFactory().setValue((int)oldValue);
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

这个也可能对你有所帮助,如果你想使用转换器类来更全面地处理变化。

此外,还应查看官方文档中关于setEditable方法的说明;


2
我实际上对您提供的文档有不同的理解,具体来说,如果值无效,值工厂应该否决更改。使用侦听器将更改还原为无效值的解决方案存在问题,因为值的其他侦听器将观察到对无效值的更改,然后再次更改回去。这会破坏控件的语义,那些侦听器必须编写以处理(可能是忽略)该情况。 - James_D
可能。我同意,但这就是控件的操作方式。当然,您不必恢复值。这只是一个示例,以突出无效整数输入引起的异常可以处理,然后您可以选择如何处理它。在这里,值的还原纯粹是为了演示目的。这不一定是理想的方法,只是一个例子。OP要求“解决方法”。这是其中之一的例子 :) - ManoDestra
1
是的,话说回来,看起来 IntegerSpinnerValueFactory 使用“恢复到有效值”策略处理“超出范围”的值,我也不太喜欢。 - James_D
1
我尝试了这段代码,它可以工作,但有一件事情让我感到困惑。在将监听器添加到值属性之后,从IntegerSpinnerValueFactory构造函数设置的相同属性上的监听器再也不会被调用了。就好像addListener实际上是replaceAllListeners一样。个人认为回归有效策略是一个明智的选择。可能“保留值并将其标记为红色”也可以起作用。 - Emily L.
@EmilyL。是的,这正是我也在思考的。关于如何实现自己的要求还有待商榷。但是,至少我们有一些可行的方法来解决控件值的失效问题。 - ManoDestra

6

我查看了JDK源代码。

在这里的监听器lambda表达式中,if(newValue < getMin()) { 抛出了NPE:

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);
    }

推测newValuenull,而自动解包null引发了NPE异常。由于输入来自编辑器,我怀疑默认转换器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,这是合理的,因为输入没有有效值。
追踪调用堆栈,我找到了值的来源:

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。此时,我认为spinner类应检查value是否不为null,如果是,则将先前的值恢复到编辑器中。

我现在相当确定这是一个错误。此外,我认为使用永远不会返回null的转换器的解决方法不会正常工作,因为它只会掩盖问题,并且当无法转换值时应返回什么值?

编辑:解决方法

将微调器编辑器的onAction替换为拒绝无效输入并采用“返回有效”策略即可解决此问题:

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键时返回到有效值来解决该问题。但是,如果擦除输入而没有提交(按下Enter键),然后再按增量或减量按钮,将导致相同的NPE,但调用堆栈略有不同。
原因:
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);
}

减少操作与增加操作类似,均需要在以下代码中调用 commitEditorText 方法:
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复制粘贴的内容:
    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,如下所示:
private void commitEditorText() {
    if (!isEditable()) return;
    getEditor().getOnAction().handle(new ActionEvent(this, this));
}

这样做可以保持一致的行为,同时给编辑器处理输入的机会,然后再将其发送到值工厂。


1
我认为这是一个bug:IntegerSpinnerValueFactory应该正确处理这种情况。
一种解决方法是为微调器值工厂提供一个转换器,如果文本值无效,则将其评估为默认值:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory.IntegerSpinnerValueFactory;
import javafx.stage.Stage;
import javafx.util.StringConverter;

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);

        valueFactory.setConverter(new StringConverter<Integer>() {

            @Override
            public String toString(Integer object) {
                return object.toString() ;
            }

            @Override
            public Integer fromString(String string) {
                if (string.matches("-?\\d+")) {
                    return new Integer(string);
                }
                // default to 0:
                return 0 ;
            }

        });

        Spinner<Integer> spinner = new Spinner<>(valueFactory);
        spinner.setEditable(true);
        aPrimaryStage.setScene(new Scene(spinner));
        aPrimaryStage.show();
    }
}

虽然工作正常,但有些情况下默认值不适用(或者可能甚至不在微调器的值域内)。 - Emily L.
在这种情况下,您可以提供一个值工厂的实现...只是想看看我能否让它工作。 - James_D
在JavaFX 11中,此修复程序仅在Spinner失去焦点(文本为空)时第一次有效,从第二次开始似乎不起作用(除非您将值更改为其他有效值,然后将其更改回为空)。 - Venkata Raju

1

我正在使用JavaFX 11,仍然看到这个错误。DoubleSpinnerValueFactory的错误已经修复,但是IntegerSpinnerValueFactory还没有。 - Venkata Raju
IntegerSpinnerValueFactory 中又出现了一个 bug,即“不会将值回绕到最小值”。问题详见 stackoverflow。该 bug 已在 Open JDK Bug System 上报告。 - Venkata Raju

0

另一个对我有效的答案,防止您输入任何非数字值:

spinnerIndex.editorProperty().getValue().textProperty().addListener(new ChangeListener<String>() {

    private static boolean isInteger(final String s) {
        try {
            @SuppressWarnings("unused")
            int d = Integer.parseInt(s);
            return true;
        }
        catch (NumberFormatException e) {
            return false;
        }
    }

    @Override
    public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
        if (!isInteger(newValue)) {
            final StringProperty sp = (StringProperty)observable;
            sp.set(oldValue);
        }
    }
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接