JavaFX Spinner 空文本空指针异常

JavaFX Spinner empty text nullpointerexception

我遇到一个问题,如果清除编辑器文本并提交然后单击递增或递减按钮,可编辑的 JavaFX 8 Spinner 会导致未捕获 NullPointerException。这是 j8u60j8u77。运气好的话,increment/decrement 按钮会卡在按下状态,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$new5(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$new0(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(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$new3(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.access00(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$handleKeyEvent3(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$null8(WinApplication.java:191)
    at java.lang.Thread.run(Thread.java:745)

我会认为这是一个错误:IntegerSpinnerValueFactory 应该正确处理这种情况。

一种解决方法是向微调器值工厂提供一个 converter,如果文本值无效,该工厂将评估为默认值:

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

我翻遍了 JDK 来源。

NPE 是从侦听器 lambda 中的 if (newValue < getMin()) { 抛出的:

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

我们看到它很乐意 return 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); 获得的值,该值可能为空。在这一点上,我相信微调器 class 应该检查 value 不是 null 并且如果它是将以前的值恢复到编辑器。

我现在相当确定这是一个错误。此外,我认为使用永远不会 return 为 null 的转换器的变通方法不会正常工作,因为它只会掩盖问题以及当值不能为 return 时应该编辑什么值已转换?

编辑:解决方法

将微调器编辑器的 onAction 替换为“return 有效”策略以拒绝无效输入修复了该问题:

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 上的侦听器相反,这避免了使用无效数据触发其他侦听器。然而,这突出了微调器中的另一个问题 class。虽然上面通过 return 在按 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));
}

那么行为将是一致的,并让编辑器有机会在进入值工厂之前处理输入。

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

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

或者您应该处理微调器的值 属性 引发的事件。

下面是一个简单的示例:

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

也可以帮助您,如果您希望使用转换器 class 来帮助更全面地处理更改。

另见 setEditable method;

的官方文档

这是一个已知错误,已在 Java 9 中修复 - 请参阅 https://bugs.openjdk.java.net/browse/JDK-8150962

另一个对我有用的答案,阻止你输入任何非数字值:

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