【问题标题】:How to fully expand a large JTree efficiently如何有效地充分扩展大型 JTree
【发布时间】:2016-04-24 04:52:32
【问题描述】:

有几个相关的问题,关于auto-expanding a JTree when a new TreeModel is set,或者关于expanding a JTree in general,其中一些也是针对expanding many paths in a JTree的性能。

但是,所提出的解决方案似乎都没有涵盖人们认为的“简单”应用案例:我有一棵大树(即,一棵非常深、非常宽或两者兼有的树),而我希望以编程方式完全扩展它。

以下是显示问题的MCVE:它创建了一个具有 100k 节点的树模型。按下该按钮会触发对expandAll 的调用,它会尝试使用从相关问题的答案中得出的方法来扩展所有节点。

问题在于扩展这 10 万个节点大约需要 13 秒(在普通机器上,使用最新的 JVM)。

import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.util.function.Consumer;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;


public class TreeExpansionPerformanceProblem
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(
            () -> createAndShowGUI());
    }

    private static void createAndShowGUI()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().setLayout(new GridLayout(1,0));

        f.getContentPane().add(createTestPanel(
            TreeExpansionPerformanceProblem::expandAll));

        f.setSize(800,600);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    private static JPanel createTestPanel(Consumer<JTree> expansionMethod)
    {
        JPanel panel = new JPanel(new BorderLayout());
        JTree tree = new JTree(createTestTreeModel());
        panel.add(new JScrollPane(tree), BorderLayout.CENTER);

        JPanel buttonPanel = new JPanel();
        JButton expandAllButton = new JButton("Expand all");
        expandAllButton.addActionListener( e -> 
        {
            System.out.println("Expanding...");
            long before = System.nanoTime();
            expansionMethod.accept(tree);
            long after = System.nanoTime();
            System.out.println("Expanding took "+(after-before)/1e6);

        });
        buttonPanel.add(expandAllButton);
        panel.add(buttonPanel, BorderLayout.SOUTH);
        return panel;
    }

    private static void expandAll(JTree tree)
    {
        int r = 0;
        while (r < tree.getRowCount())
        {
            tree.expandRow(r);
            r++;
        }
    }

    private static TreeModel createTestTreeModel() 
    {
        DefaultMutableTreeNode root = new DefaultMutableTreeNode("JTree");
        addNodes(root, 0, 6, 6, 10);
        return new DefaultTreeModel(root);
    }

    private static void addNodes(DefaultMutableTreeNode node, 
        int depth, int maxDepth, int count, int leafCount)
    {
        if (depth == maxDepth)
        {
            return;
        }
        for (int i=0; i<leafCount; i++)
        {
            DefaultMutableTreeNode leaf = 
                new DefaultMutableTreeNode("depth_"+depth+"_leaf_"+i);
            node.add(leaf);
        }
        if (depth < maxDepth - 1)
        {
            for (int i=0; i<count; i++)
            {
                DefaultMutableTreeNode child = 
                    new DefaultMutableTreeNode("depth_"+depth+"_node_"+i);
                node.add(child);
                addNodes(child, depth+1, maxDepth, count, leafCount);
            }
        }

    }
}

是否有任何选项可以更有效地扩展多个节点?

【问题讨论】:

    标签: java performance swing jtree


    【解决方案1】:

    完全扩展一棵大树时会遇到各种瓶颈,并且有不同的方法来规避这些瓶颈。

    有趣的是,收集TreePath 对象以进行扩展并遍历树通常并不是最昂贵的部分。根据在VisualVMJava Flight Recorder 中运行的分析器,大部分时间都花在计算模型状态(TreeModel)和视图(JTree)之间的“映射”上。这主要是指

    • 计算JTree 的行高
    • 计算TreeCellRenderer中标签的边界

    并非所有这些计算都可以避免。但是,通过

    可以显着加快树的扩展速度
    • 设置固定的行高,用JTree#setRowHeight
    • 暂时禁用树的TreeExpansionListeners

    下面是一个MCVE,它比较了问题中的“幼稚”方法,它需要 13 秒 来扩展具有 100k 个节点的树,而稍微快一点的方法,只需要 1 秒 用于扩展同一棵树。

    import java.awt.BorderLayout;
    import java.awt.Component;
    import java.awt.GridLayout;
    import java.util.Arrays;
    import java.util.List;
    import java.util.function.Consumer;
    
    import javax.swing.JButton;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.JScrollPane;
    import javax.swing.JTree;
    import javax.swing.SwingUtilities;
    import javax.swing.event.TreeExpansionListener;
    import javax.swing.tree.DefaultMutableTreeNode;
    import javax.swing.tree.DefaultTreeModel;
    import javax.swing.tree.TreeCellRenderer;
    import javax.swing.tree.TreeModel;
    import javax.swing.tree.TreePath;
    
    
    public class TreeExpansionPerformanceSolution
    {
        public static void main(String[] args)
        {
            SwingUtilities.invokeLater(
                () -> createAndShowGUI());
        }
    
        private static void createAndShowGUI()
        {
            JFrame f = new JFrame();
            f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            f.getContentPane().setLayout(new GridLayout(1,0));
    
            f.getContentPane().add(createTestPanel(
                TreeExpansionPerformanceSolution::expandAll));
    
            f.getContentPane().add(createTestPanel(
                TreeExpansionPerformanceSolution::expandAllFaster));
    
            f.setSize(800,600);
            f.setLocationRelativeTo(null);
            f.setVisible(true);
        }
    
        private static JPanel createTestPanel(Consumer<JTree> expansionMethod)
        {
            JPanel panel = new JPanel(new BorderLayout());
            JTree tree = new JTree(createTestTreeModel());
            panel.add(new JScrollPane(tree), BorderLayout.CENTER);
    
            JPanel buttonPanel = new JPanel();
            JButton expandAllButton = new JButton("Expand all");
            expandAllButton.addActionListener( e -> 
            {
                System.out.println("Expanding...");
                long before = System.nanoTime();
                expansionMethod.accept(tree);
                long after = System.nanoTime();
                System.out.println("Expanding took "+(after-before)/1e6);
    
            });
            buttonPanel.add(expandAllButton);
            panel.add(buttonPanel, BorderLayout.SOUTH);
            return panel;
        }
    
        private static void expandAll(JTree tree)
        {
            int r = 0;
            while (r < tree.getRowCount())
            {
                tree.expandRow(r);
                r++;
            }
        }
    
        private static void expandAllFaster(JTree tree)
        {
            // Determine a suitable row height for the tree, based on the 
            // size of the component that is used for rendering the root 
            TreeCellRenderer cellRenderer = tree.getCellRenderer();
            Component treeCellRendererComponent = 
                cellRenderer.getTreeCellRendererComponent(
                    tree, tree.getModel().getRoot(), false, false, false, 1, false);
            int rowHeight = treeCellRendererComponent.getPreferredSize().height + 2;
            tree.setRowHeight(rowHeight);
    
            // Temporarily remove all listeners that would otherwise
            // be flooded with TreeExpansionEvents
            List<TreeExpansionListener> expansionListeners =
                Arrays.asList(tree.getTreeExpansionListeners());
            for (TreeExpansionListener expansionListener : expansionListeners)
            {
                tree.removeTreeExpansionListener(expansionListener);
            }
    
            // Recursively expand all nodes of the tree
            TreePath rootPath = new TreePath(tree.getModel().getRoot());
            expandAllRecursively(tree, rootPath);
    
            // Restore the listeners that the tree originally had
            for (TreeExpansionListener expansionListener : expansionListeners)
            {
                tree.addTreeExpansionListener(expansionListener);
            }
    
            // Trigger an update for the TreeExpansionListeners
            tree.collapsePath(rootPath);
            tree.expandPath(rootPath);
        }
    
        // Recursively expand the given path and its child paths in the given tree
        private static void expandAllRecursively(JTree tree, TreePath treePath)
        {
            TreeModel model = tree.getModel();
            Object lastPathComponent = treePath.getLastPathComponent();
            int childCount = model.getChildCount(lastPathComponent);
            if (childCount == 0)
            {
                return;
            }
            tree.expandPath(treePath);
            for (int i=0; i<childCount; i++)
            {
                Object child = model.getChild(lastPathComponent, i);
                int grandChildCount = model.getChildCount(child);
                if (grandChildCount > 0)
                {
                    class LocalTreePath extends TreePath
                    {
                        private static final long serialVersionUID = 0;
                        public LocalTreePath(
                            TreePath parent, Object lastPathComponent)
                        {
                            super(parent, lastPathComponent);
                        }
                    }
                    TreePath nextTreePath = new LocalTreePath(treePath, child);
                    expandAllRecursively(tree, nextTreePath);
                }
            }
        }
    
    
        private static TreeModel createTestTreeModel() 
        {
            DefaultMutableTreeNode root = new DefaultMutableTreeNode("JTree");
            addNodes(root, 0, 6, 6, 10);
            return new DefaultTreeModel(root);
        }
    
        private static void addNodes(DefaultMutableTreeNode node, 
            int depth, int maxDepth, int count, int leafCount)
        {
            if (depth == maxDepth)
            {
                return;
            }
            for (int i=0; i<leafCount; i++)
            {
                DefaultMutableTreeNode leaf = 
                    new DefaultMutableTreeNode("depth_"+depth+"_leaf_"+i);
                node.add(leaf);
            }
            if (depth < maxDepth - 1)
            {
                for (int i=0; i<count; i++)
                {
                    DefaultMutableTreeNode child = 
                        new DefaultMutableTreeNode("depth_"+depth+"_node_"+i);
                    node.add(child);
                    addNodes(child, depth+1, maxDepth, count, leafCount);
                }
            }
    
        }
    }
    

    注意事项:

    • 这是一个自我回答的问题,我希望这个答案可能对其他人有所帮助。尽管如此,1 秒还是比较慢。我也尝试了其他一些东西,例如设置tree.setLargeModel(true),但这并没有产生积极的影响(事实上,它甚至更慢!)。大部分时间仍然花在树的视觉状态的最终更新上,我很高兴在这里看到进一步的改进。

    • expandAllRecursively 方法可以用涉及DefaultMutableTreeNode#breadthFirstEnumerationDefaultTreeModel#getPathToRoot 的几行代码替换,而不会牺牲很多性能。但在目前的形式中,代码仅在TreeModel 接口上运行,应该适用于任何类型的节点。

    【讨论】:

    • 因为 UI 委托拥有 CellRendererPane,讨论了 here,我看到 L&F 实现之间存在一些差异;我看到更好的绝对性能,但在 Mac OS X (~8:2) 上的相对改进较低。
    • @trashgod 当然,这里会有一些变化。诚然,我没有系统地尝试不同的 L&F,但也许我会相应地扩展 Q/A,这可能会很有趣。如果您对如何加快速度有任何想法(甚至猜测),我也很想听听。
    【解决方案2】:

    正如here 所讨论的,JTree 已经使用享元模式来优化渲染。我认为您在expandAllFaster() 中的方法就足够了。展开所有 >105 叶子充其量是笨拙的。生成的树很难有意义地浏览,尽管合适的搜索控件可以缓解这种情况。

    在 Mac OS X TreeUI 委托 com.apple.laf.AquaTreeUI 中可以看到一个有趣的妥协。当按下 option 键时,它递归地扩展选定节点其子节点,由MouseEvent::isAltDown() 确定。有关详细信息,请参阅名为 "aquaFullyExpandNode"Action

    最后,对于example,将用户的展开保存为首选项可能是值得的。

    我正在处理...过滤 >100k-node-JTree

    按照here 的建议,专注于基于模型的方法,将搜索移动到一个单独的、可能是无模式的对话框中。概括地说,

    • 根据要用作dictionary 的树模型构造一个prefix tree,可能使用建议的here 方法之一。

    • DocumentListener 监控搜索字段并设置自定义TableModel 以在用户键入时显示匹配项。

    • 在输入最少字符数之前不显示匹配项;三是大型模型的常见选择。

    • TableModelListener展开选定行对应的树节点;或者,在 Expand 按钮处理程序中展开选定的行;在无模式的上下文中,触发一个合适的PropertyChangeEvent,树应该监听它。

    【讨论】:

    • 感谢您的提示。快速浏览一下源代码,似乎aquaFullyExpandNode 操作最终以expandAllNodes 方法结束,这与问题中的方法相似。 (待续……)
    • 当然,显示 100k 个节点几乎没有意义,但这正是这个问题的“根本原因”:我正在研究一个“过滤的 JTree”,它允许过滤 >100k-nodes- JTree on the fly - 但是当第一个字母输入到过滤器文本字段中时,它仍然可能需要扩展 >100k 节点(甚至 许多 节点 - 这就是我想在第一名)。 (顺便说一句:对于过滤树,似乎没有很好的、通用的、高效的基于模型的解决方案,但这是一个不同的问题——我考虑为这个树添加一个赏金stackoverflow.com/questions/9234297...)
    • 而且,关于保存首选项的另一个(抱歉):据我所知,主要问题是在 JTree 中构建视觉结构(例如确定边界所有单元格中的)是昂贵的部分 - “无论如何”都必须这样做(除非有真正棘手的解决方法)。因此,即使它对单元格使用享元模式,它仍然必须计算它们的 bounds 以确定树的大小。当然,所有这些都隐藏在 TreeUI 实现中......
    • @Marco13:我在上面概述了一种方法。
    • 这个想法是,只要有一个非null 过滤器,所有匹配的节点都应该被扩展 - 这导致了问题。 (过滤器设置为非null时,保存展开状态,过滤器再次变为null时恢复)。从应用程序的角度来看,您的第三点可能是一种合理的解决方法,但总的来说,从库的角度来看,人们根本无法说出特定过滤器的作用。 (上下文:github.com/javagl/CommonUI/blob/master/src/main/java/de/javagl/… - 只是在那里玩)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-06-07
    • 2011-08-20
    • 2011-08-05
    • 1970-01-01
    • 2019-08-13
    • 2016-02-12
    • 2011-10-31
    相关资源
    最近更新 更多