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

JavaFX跨TableView设置项时触发ConcurrentModificationException问题排查

问题描述

在JavaFX场景中,监听historyTableView.getSelectionModel().selectedItemProperty()获取选中行的Purchase对象,并将其数据设置到purchaseTableView时,触发了ConcurrentModificationException。该异常仅在选中historyTableView中除最后一行外的其他行时出现。

相关代码

public void refreshStock() {
    historyTableView.setItems(FXCollections.observableList(dao.getPurchaseList()));
    historyTableView.refresh();
}

@Override
@FXML
public void initialize(URL location, ResourceBundle resources) {
    historyTableView.refresh();
    purchaseTableView.refresh();

// 异常触发相关代码块
   historyTableView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
        log.debug("Clicked on: " + newValue.toString());
        purchaseTableView.setItems(FXCollections.observableList(newValue.getSoldItemListStream()));

        historyTableView.getSelectionModel().clearSelection();
        purchaseTableView.refresh();
    });
}

异常栈信息

Exception in thread "JavaFX Application Thread" java.util.ConcurrentModificationException
    at java.base/java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1415)
    at java.base/java.util.ArrayList$SubList.size(ArrayList.java:1155)
    at ee.ut.math.tvt.salessystem.dataobjects.Purchase.toString(Purchase.java:66)
    at ee.ut.math.tvt.salessystem.ui.controllers.HistoryController.lambda$initialize$0(HistoryController.java:76)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
    at javafx.base/javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:80)
    at javafx.base/javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)
    at javafx.base/javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:113)
    at javafx.base/javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:147)
    at javafx.controls/javafx.scene.control.SelectionModel.setSelectedItem(SelectionModel.java:105)
    at javafx.controls/javafx.scene.control.MultipleSelectionModelBase.lambda$new$0(MultipleSelectionModelBase.java:67)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
        ...

异常原因分析

  1. 核心触发点:从异常栈可见,Purchase.toString()调用ArrayList$SubList.size()时触发了并发修改异常。这说明Purchase持有的soldItemList是某个原始ArrayList的子列表(SubList),而原始列表正在被修改(比如UI线程或后台线程更新数据源),子列表的修改检测机制因此抛出异常。

  2. 非最后一行才触发的原因:

    • dao.getPurchaseList()返回的列表中,非最后一行的Purchase对象对应的子列表,其关联的原始列表可能在选中操作时正被refreshStock等方法修改(比如重新设置historyTableView的数据源,导致原始列表结构变更)。
    • 最后一行对应的子列表通常是原始列表的末尾片段,当原始列表被重新赋值时,这个子列表的修改计数器不会被触发,或者后续没有操作触发其修改检测逻辑,因此不会抛出异常。
  3. 监听器操作的叠加影响:监听器中调用historyTableView.getSelectionModel().clearSelection()会触发选择模型的属性变更,间接导致historyTableView的数据源列表被遍历或修改,和当前正在执行的newValue.toString()(遍历子列表)形成并发修改,加剧了异常触发的概率。

解决方案

  1. 替换SubList为独立列表:修改Purchase或DAO的实现,避免返回原始列表的子列表,而是创建新的独立列表:

    // 修改Purchase的getSoldItemListStream方法
    public List<SoldItem> getSoldItemListStream() {
        return new ArrayList<>(this.soldItemList); // 将子列表转换为新的ArrayList
    }
    

    或者在refreshStock中预处理列表,确保每个Purchase持有独立的商品列表:

    public void refreshStock() {
        List<Purchase> processedList = dao.getPurchaseList().stream()
            .map(purchase -> {
                Purchase newPurchase = new Purchase();
                // 复制purchase的基础属性
                newPurchase.setSoldItemList(new ArrayList<>(purchase.getSoldItemListStream()));
                return newPurchase;
            })
            .collect(Collectors.toList());
        historyTableView.setItems(FXCollections.observableList(processedList));
    }
    
  2. 延迟执行清除选择操作:用Platform.runLater()包裹clearSelection(),避免在当前事件处理流程中触发列表修改:

    historyTableView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
        log.debug("Clicked on: " + newValue.toString());
        purchaseTableView.setItems(FXCollections.observableList(newValue.getSoldItemListStream()));
        
        // 延迟到当前事件循环结束后执行清除操作
        Platform.runLater(() -> historyTableView.getSelectionModel().clearSelection());
        purchaseTableView.refresh();
    });
    
  3. 设置不可变商品列表:将Purchase中的商品列表设置为不可变,避免后续修改触发异常:

    public class Purchase {
        private final List<SoldItem> soldItemList;
        
        public Purchase(List<SoldItem> soldItems) {
            this.soldItemList = Collections.unmodifiableList(new ArrayList<>(soldItems));
        }
        
        public List<SoldItem> getSoldItemListStream() {
            return soldItemList;
        }
    }
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 14:20:36