如何调试堆栈跟踪未引用我的 类 的 NullPointerException?

How can I debug a NullPointerException where the stacktrace doesn't reference my classes?

我有一个 table 其行是根据文本输入过滤的。

我最近将谓词放在延迟系统中(下面的完整代码)以避免在过滤大型数据集时冻结 UI。

我可以通过在程序启动时向过滤器输入文本框发送垃圾邮件来生成以下异常。正如您将看到的,整个异常都发生在 Oracle 的代码库中。我在堆栈跟踪中没有看到我项目的任何 类。

Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
    at javafx.collections.transformation.SortedList$Element.access0(SortedList.java:272)
    at javafx.collections.transformation.SortedList.get(SortedList.java:170)
    at javafx.scene.control.TableColumn.getCellObservableValue(TableColumn.java:562)
    at javafx.scene.control.TableCell.updateItem(TableCell.java:644)
    at javafx.scene.control.TableCell.indexChanged(TableCell.java:468)
    at javafx.scene.control.IndexedCell.updateIndex(IndexedCell.java:116)
    at com.sun.javafx.scene.control.skin.TableRowSkinBase.requestCellUpdate(TableRowSkinBase.java:659)
    at com.sun.javafx.scene.control.skin.TableRowSkinBase.lambda$init[=11=](TableRowSkinBase.java:159)
    at com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:137)
    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.Cell.setItem(Cell.java:403)
    at javafx.scene.control.Cell.updateItem(Cell.java:670)
    at javafx.scene.control.TableRow.updateItem(TableRow.java:259)
    at javafx.scene.control.TableRow.indexChanged(TableRow.java:225)
    at javafx.scene.control.IndexedCell.updateIndex(IndexedCell.java:116)
    at com.sun.javafx.scene.control.skin.VirtualFlow.setCellIndex(VirtualFlow.java:1957)
    at com.sun.javafx.scene.control.skin.VirtualFlow.addTrailingCells(VirtualFlow.java:1344)
    at com.sun.javafx.scene.control.skin.VirtualFlow.layoutChildren(VirtualFlow.java:1197)
    at com.sun.javafx.scene.control.skin.VirtualFlow.setCellCount(VirtualFlow.java:231)
    at com.sun.javafx.scene.control.skin.TableViewSkinBase.updateRowCount(TableViewSkinBase.java:567)
    at com.sun.javafx.scene.control.skin.VirtualContainerBase.checkState(VirtualContainerBase.java:113)
    at com.sun.javafx.scene.control.skin.VirtualContainerBase.layoutChildren(VirtualContainerBase.java:108)
    at com.sun.javafx.scene.control.skin.TableViewSkinBase.layoutChildren(TableViewSkinBase.java:696)
    at javafx.scene.control.Control.layoutChildren(Control.java:578)
    at javafx.scene.Parent.layout(Parent.java:1087)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Scene.doLayoutPass(Scene.java:552)
    at javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2397)
    at com.sun.javafx.tk.Toolkit.lambda$runPulse(Toolkit.java:355)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:354)
    at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:381)
    at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:510)
    at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:490)
    at com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit(QuantumToolkit.java:319)
    at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
    at com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
    at com.sun.glass.ui.gtk.GtkApplication.lambda$null(GtkApplication.java:139)
    at java.lang.Thread.run(Thread.java:748)

我最近添加的代码是这样的,取代了 table 的正常过滤方式。基本思想是避免应用不必要的谓词。

import java.text.Normalizer;
import java.util.ArrayList;

import javafx.collections.transformation.FilteredList;
import net.joshuad.hypnos.Album;

public class ThrottledAlbumFilter {
    private String requestedFilter = "";
    private long timeRequestMadeMS = 0;

    private Thread filterThread;
    private boolean interruptFiltering = false;

    private String currentAppliedFilter = "";

    private FilteredList <Album> filteredList;

    public ThrottledAlbumFilter ( FilteredList <Album> filteredList ) {
        this.filteredList = filteredList;

        filterThread = new Thread ( () -> {
            while ( true ) {
                String filter = requestedFilter;

                if ( !filter.equals( currentAppliedFilter ) ) {
                    if ( System.currentTimeMillis() >= timeRequestMadeMS + 100 ) {
                        interruptFiltering = false;
                        setPredicate( filter );
                        currentAppliedFilter = filter;
                    }
                }

                try { Thread.sleep( 25 ); } catch ( InterruptedException e ) {} 
            }
        });

        filterThread.setDaemon( true );
        filterThread.start();
    }

    public void setFilter ( String filter ) {
        if ( filter == null ) filter = "";
        timeRequestMadeMS = System.currentTimeMillis();
        this.requestedFilter = filter;
        interruptFiltering = true;
    }

    private void setPredicate ( String filterText ) {
        filteredList.setPredicate( album -> {
            if ( interruptFiltering ) return true;
            if ( filterText.isEmpty() ) return true;

            ArrayList <String> matchableText = new ArrayList <String>();

            matchableText.add( album.getAlbumArtist().toLowerCase() );
            matchableText.add( album.getYear().toLowerCase() );
            matchableText.add( album.getFullAlbumTitle().toLowerCase() );

            matchableText.add( Normalizer.normalize( album.getFullAlbumTitle(), Normalizer.Form.NFD )
                .replaceAll( "[^\p{ASCII}]", "" ).toLowerCase() 
            );

            matchableText.add( Normalizer.normalize( album.getYear(), Normalizer.Form.NFD )
                .replaceAll( "[^\p{ASCII}]", "" ).toLowerCase()
            );

            matchableText.add( Normalizer.normalize( album.getAlbumArtist(), Normalizer.Form.NFD )
                .replaceAll( "[^\p{ASCII}]", "" ).toLowerCase() 
            );

            String[] lowerCaseFilterTokens = filterText.toLowerCase().split( "\s+" );
            for ( String token : lowerCaseFilterTokens ) {
                boolean tokenMatches = false;
                for ( String test : matchableText ) {
                    if ( test.contains( token ) ) {
                        tokenMatches = true;
                    }
                }

                if ( !tokenMatches ) {
                    return false;
                }
            }

            return true;
        });
    }
}

之前的版本经过大量测试,没有任何问题。现在,我可以通过在程序启动时快速大量更改过滤器文本来非常可靠地生成它。我不得不假设崩溃是由于此更改的代码引起的,但由于我的堆栈跟踪根本没有引用我的代码库,我不确定从哪里开始。

编辑:有趣的是,将睡眠时间从 25 毫秒更改为 50 毫秒似乎消除了我计算机上的错误。这让我非常紧张,因为我不得不想象 "right" 值对于不同的速度系统是不同的。

这肯定是并发问题。

TableView 正尝试在渲染脉冲期间进行渲染,并且当发生这种情况时支持列表正在更改。 NullPointerException 被抛出,因为持有实际元素的 Element 对象 "mysteriously" 消失了。

依靠睡眠时间是一个非常糟糕的主意 - 我相信您也已经意识到这一点。解决这个问题主要有两种方法:

在 UI 线程(即 JavaFX 应用程序线程)上修改 UI

您也可以这样做,只是您将 filteredList.setPredicate() 调用包装在 Platform.runLater().

换句话说,它应该是这样的:

final Predicate<Album> predicate = album -> {
    // Whatever you have
};

Platform.runLater(() -> filteredList.setPredicate(predicate));

这样做会在后台线程中卸载 Predicate 的生成,而实际的更新是在 UI 线程上完成的。我会说这也会导致大量处理转移回 UI 线程,但这可能是不可避免的。

但是,由于您在线程中的 Runnable 对象中编写的代码,您仍然可以跳过一些谓词更改。我认为这可以满足您 "avoiding applying predicates that aren't necessary."

的要求

使用时间轴

JavaFX 有一个非常方便的 class,称为 TimeLine,它像计时器一样工作,它在 UI 线程上 运行s。

不要使用另一个线程,而是在 class 中创建一个 TimeLine 对象。

private String filter;

private final Timeline timeline = new Timeline(
    new KeyFrame(Duration.millis(100),
                 ae -> setPredicate()
    ));

public void setFilter ( String filter ) {
    if ( filter == null ) filter = "";
    if ( !this.filter.equals( filter ) ) {
        this.filter = filter;
        this.timeline.playFromStart();
    }
}

private void setPredicate() {
    final String filterText = this.filter;

    // The rest remains pretty much the same.
}

使用这种方法会导致所有代码在 UI 线程上 运行,所以你不会有那些奇怪的异常。

另一个好处是,您不必管理线程。尽管您已将线程设置为守护进程,但线程仍将每 25 毫秒 运行 Runnable,直到您的程序完全终止。

最后,这提供了从过滤器字符串的最后一次更改开始的统一延迟。这将提供稍微更好的用户体验。