You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TableCell自定义Skin运行14小时后触发NPE问题排查求助

问题排查:TableView长时间运行后TableCell Skin空指针异常

我在应用中实现了TableView,为TableCell创建了自定义Skin,并在TableCell的createDefaultSkin()方法中初始化该自定义Skin,前期运行正常。但应用连续运行14小时后崩溃,抛出如下NullPointerException:

java.lang.NullPointerException: Cannot invoke "javafx.scene.control.Skin.dispose()" because the return value of "javafx.scene.control.IndexedCell.getSkin()" is null
    at javafx.scene.control.skin.TableRowSkinBase.recreateCells(TableRowSkinBase.java:713)
    at javafx.scene.control.skin.TableRowSkinBase.updateCells(TableRowSkinBase.java:496)
    at javafx.scene.control.skin.TableRowSkinBase.checkState(TableRowSkinBase.java:640)
    at javafx.scene.control.skin.TableRowSkinBase.layoutChildren(TableRowSkinBase.java:247)
    at javafx.scene.control.Control.layoutChildren(Control.java:601)
    at javafx.scene.control.Cell.layoutChildren(Cell.java:636)
    at javafx.scene.Parent.layout(Parent.java:1207)
    at javafx.scene.Parent.layout(Parent.java:1214)
    at javafx.scene.Parent.layout(Parent.java:1214)
    at javafx.scene.Parent.layout(Parent.java:1214)
    at javafx.scene.Parent.layout(Parent.java:1214)
    at javafx.scene.Parent.layout(Parent.java:1214)
    at javafx.scene.Parent.layout(Parent.java:1214)
    at javafx.scene.Parent.layout(Parent.java:1214)
    at javafx.scene.Parent.layout(Parent.java:1214)
    at javafx.scene.Scene.doLayoutPass(Scene.java:579)
    at javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2499)
    at com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:405)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:404)
    at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:434)
    at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:575)
    at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:555)
    at com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue(QuantumToolkit.java:548)
    at com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$11(QuantumToolkit.java:352)
    at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
    at com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
    at com.sun.glass.ui.gtk.GtkApplication.lambda$runLoop$11(GtkApplication.java:316)
    at java.base/java.lang.Thread.run(Thread.java:833)

TableRowSkinBase中recreateCells()方法的相关代码如下:

/*
     * A map that maps from TableColumn to TableCell (i.e. model to view).
     * This is recreated whenever the leaf columns change, however to increase
     * efficiency we create cells for all columns, even if they aren't visible,
     * and we only create new cells if we don't already have it cached in this
     * map.
     *
     * Note that this means that it is possible for this map to therefore be
     * a memory leak if an application uses TableView and is creating and removing
     * a large number of tableColumns. This is mitigated in the recreateCells()
     * function below - refer to that to learn more.
     */
    WeakHashMap<TableColumnBase, Reference<R>> cellsMap;


    private void recreateCells() {
            if (cellsMap != null) {
                Collection<Reference<R>> cells = cellsMap.values();
                Iterator<Reference<R>> cellsIter = cells.iterator();
                while (cellsIter.hasNext()) {
                    Reference<R> cellRef = cellsIter.next();
                    R cell = cellRef.get();
                    if (cell != null) {
                        cell.updateIndex(-1);
                        cell.getSkin().dispose(); // THIS IS WHERE THE ISSUE OCCURS
                        cell.setSkin(null);
                    }
                }
                cellsMap.clear();
            }

            ObservableList<? extends TableColumnBase/*<T,?>*/> columns = getVisibleLeafColumns();

            cellsMap = new WeakHashMap<>(columns.size());
            fullRefreshCounter = DEFAULT_FULL_REFRESH_COUNTER;
            getChildren().clear();

            for (TableColumnBase col : columns) {
                if (cellsMap.containsKey(col)) {
                    continue;
                }

                // create a TableCell for this column and store it in the cellsMap
                // for future use
                createCellAndCache(col);
            }
        }

自定义Skin逻辑简单,仅包含自定义行为实现:

import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumnBase;
import javafx.scene.control.skin.TableCellSkinBase;

/**
 * Custom table cell skin.
 *
 * @param <S> The type of the TableView generic type (i.e. S == TableView&lt;S&gt;)
 * @param <T> The type of the content in all cells in this TableColumn
 * @param <C> the cell type
 */
public final class CustomTableCellSkin<S, T, C extends IndexedCell<T>> extends TableCellSkinBase<S, T, C> {

    /** Cell behaviour. */
    private final CustomTableCellBehavior<S, T> behaviour;

    /**
     * Constructor.
     *
     * @param cell skinnable cell
     */
    @SuppressWarnings("unchecked")
    public CustomTableCellSkin(final TableCell<S, T> cell) {
        super((C) cell);
        behaviour = new CustomTableCellBehavior<>(cell);
    }

    /**
     * Return the cell behaviour.
     *
     * @return behaviour
     */
    public final CustomTableCellBehavior<S, T> getBehaviour() {
        return behaviour;
    }

    @Override
    public final ReadOnlyObjectProperty<? extends TableColumnBase<S, T>> tableColumnProperty() {
        @SuppressWarnings("unchecked")
        final TableCell<S, T> cell = (TableCell<S, T>) getSkinnable();
        return cell.tableColumnProperty();
    }
}

排查线索

1. 单元格Skin会在哪些情况下被清空?

  • 手动调用cell.setSkin(null):业务代码或第三方逻辑中若存在该调用,会直接清空Skin
  • Skin被垃圾回收:若Skin仅被弱引用持有,且无其他强引用关联,GC时可能被回收,导致getSkin()返回null
  • JavaFX内部Skin替换:控件皮肤被重新创建(如主题切换、控件重新初始化)时,旧Skin会被清理,极端场景下可能出现状态不一致
  • 单元格复用异常:TableView的单元格复用机制在极端场景(大量数据滚动、列频繁调整)下,可能出现Skin状态异常,导致Skin被意外清空

2. 为何recreateCells()方法中未在调用dispose()前做空检查?

这是JavaFX控件库的潜在bug:

  • 开发人员默认认为cell != null时Skin必然存在,但长时间运行的复杂场景中,可能出现Skin被提前释放的情况
  • cellsMap使用WeakHashMap弱引用存储单元格,单元格对象被GC前,Skin可能已被意外清空
  • 该场景属于边缘case,常规测试难以复现,因此官方代码未添加空判断

临时修复建议

  • 在自定义TableCell中重写setSkin()方法,确保Skin被正确持有
  • 自定义TableRowSkinBase子类,在调用dispose()前添加空检查(需注意JavaFX内部API的访问限制)
  • 避免在业务代码中直接操作单元格的Skin对象

内容的提问来源于stack exchange,提问作者Sai Dandem

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 18:51:17