如何调试NullPointerException,其中stacktrace不引用我的类?

时间:2018-01-08 05:38:44

标签: java debugging javafx stack-trace

我有一个表,其行根据文本输入进行过滤。

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

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

Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
    at javafx.collections.transformation.SortedList$Element.access$200(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$0(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$3(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$11(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$5(GtkApplication.java:139)
    at java.lang.Thread.run(Thread.java:748)

我最近添加的代码就是这个,取代了对表进行过滤的常规方法。基本思想是避免应用不必要的谓词。

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毫秒似乎会破坏我计算机上的错误。这让我非常紧张,因为我必须想象&#34;对&#34;不同的速度系统的价值是不同的。

1 个答案:

答案 0 :(得分:2)

这绝对是并发问题。

TableView正在尝试在渲染脉冲期间渲染,并且支持列表在发生这种情况时会发生变化。 NullPointerException被抛出,因为持有实际元素的Element对象“神秘地”消失了。

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

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

您可以这样做,除非您将filteredList.setPredicate()电话打包在Platform.runLater()中。

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

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

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

执行此操作将在后台线程中卸载Predicate的生成,而实际更新在UI线程上完成。我会说这会导致很大一部分处理转移回UI线程,但这可能是不可避免的。

但是,您仍然可以跳过一些谓词更改,因为您在线程中的Runnable对象中编写了代码。我认为这符合你“避免应用不必要的谓词”的要求。

使用TimeLine

JavaFX有一个非常方便的类TimeLine,它的作用就像一个计时器,它在UI线程上运行。

不要使用其他线程,而是在类中创建一个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线程上运行,因此您不会有这些奇怪的例外。

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

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