Java Swing JTree Performance beim Aufklappen (Expand) von großen Bäumen

23.07.2011

Übersicht

Im folgenden wird erklärt, wie die Performance beim Aufklappen von großen JTrees unter Java Swing beschleunigt werden kann. Dazu wird ein Beispiel mit einem JTree implementiert, der sich über ein Popup-Menü aufklappen (expand) läßt. In dem Beispiel wird gezeigt, wie durch das Überschreiben der Methode public Enumeration getExpandedDescendants(final TreePath parent) der Klasse JTree eine Performance-Steigerung erreicht werden kann.

Performance-Steigerung durch Überschreiben

Um eine Performance-Steigerung beim Aufklappen eines JTrees zu erreichen, muß eine neue Klasse angelegt werden, die JTree erweitert, d.h. von ihr erbt. Deshalb wird in dem Beispiel die Klasse JHTree erstellt. Das Wichtigste an der Klasse ist die Methode public Enumeration getExpandedDescendants(final TreePath parent). Dabei handelt es sich um eine Methode des JTree, welche aufgerufen wird, wenn der Baum aufgeklappt wird. Diese Methode tut meines Wissens nichts, was im Normalfall benötigt wird. Allerdings benötigt sie eine Menge Rechenzeit und beansprucht Speicherplatz, wenn der Baum aufgeklappt werden soll. Deshalb wurde sie überschrieben und liefert einfach den Wert null zurück. Außerdem wird noch die Methode expandAll ergänzt, die den Baum rekusiv aufklappt.

import java.util.Enumeration;

import javax.swing.JTree;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;

/**
 * @author hameister
 * 
 */
public class JHTree extends JTree {

	public JHTree(TreeNode treenode) {
		super(treenode);
	}

	public void expandAll() {
		int row = 0;
		while (row < getRowCount()) {
			expandRow(row);
			row++;
		}
	}

	/**
	 * Overwrite the standard method for performance reasons.
	 * 
	 * @see javax.swing.JTree#getExpandedDescendants(javax.swing.tree.TreePath)
	 */
	@Override
	public Enumeration getExpandedDescendants(final TreePath parent) {
		return null;
	}
}

Diese neue Klasse kann nun einfach verwendet werden, um ein performantes und speicherschonendes Aufklappen von JTrees zu erreichen.

Performance-Untersuchung

Im folgenden findet man noch ein paar Performance-Untersuchungen, die ich auf einem Mac 2.4 GHz Intel Core2Duo mit 2GB Ram gemacht habe. Den Beispielcode von oben habe ich innerhalb von Eclipse 3.6.2 ausgeführt. Für die Tests habe ich die Variablen deep und numberOfNodes verändert (siehe Abschnitt Vollständige Beispiel-Implementierung weiter unten) und jeweils einen Test mit Optimierung und einen ohne Optimierung durchgeführt und die Werte verglichen. Alle Zeiten werden in Millisekunden angegeben.

Knotenanzahl
(n, deep, numberOfNodes)
mit Optimierung
(ms)
ohne Optimierung
(ms)
14 / 2 / 2 2 3
39 / 2 / 3 4 6
120 / 3 / 3 10 13
340 / 3 / 4 26 30
1022 / 8 / 2 86 127
1364 / 4 / 4 81 106
9330 / 4 / 6 395 662
21844 / 6 / 4 742 3753
29523 / 8 / 3 895 13520
55986 / 5 / 6 2390 10496
87380 / 7 / 4 1684 77892
299592 / 5 / 8 6153 201645
335922 / 6 / 6 9919 663451
349524 / 8 / 4 14514 1717839

Es ist ganz klar zu sehen, daß die Optimierung ab Baumgrößen von über 20000 Knoten einen erheblichen Einfluß auf die Zeit beim Aufklappen eines Baumes hat.

Aus wissenschaftlicher Sicht müßte man zwar weitere Untersuchungen machen, allerdings glaube ich, daß die Werte für die Praxis ausreichend sind. ;-)

Vollständige Beispiel-Implementierung

Der Vollständigkeit halber findet man im folgenden Abschnitt noch eine Beispiel-Implementierung, mit der die Perfomance-Untersuchungen durchgeführt wurden.

Als erstes wird eine Hauptklasse benötigt, die ein JPanel erstellt, welches den JTree enthält.

import java.awt.Dimension;
import javax.swing.JFrame;

public class MyJTreeExample {

    private static void createAndShowGUI() {

        //Create and set up the window.
        JFrame frame = new JFrame("JHTreeDemo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        //Add content to the window.
        frame.add(new JHTreePanel());

        frame.setMinimumSize(new Dimension(800, 600));
        
        //Display the window.
        frame.pack();
        frame.setVisible(true);
    }
	
	public static void main(String[] args) {
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGUI();
            }
        });
    }
}

Die Klasse enthält keine großen Geheimnisse und ist in gleicher Form in vielen Tutorials und Beispielen zu Java Swing zu finden.

Interessanter wird es in der Klasse JHTreePanel. Sie erweitert JPanel und implementiert das ActionListener-Interface. Dieses wird benötigt, um auf das Ereignis (ActionEvent) Expand zu reagieren.

Im Konstruktor wird als erstes ein Wurzelknoten top erstellt. Unter diesen Knoten wird durch die Methode createRecursiveTree ein Baum gehängt. Entsprechend der gewählten Parameter deep und numberOfNodes wird ein Baum erstellt. Die Variable deep legt die Tiefe des Baumes fest. Mittels der Variable numberOfNodes wird die Anzahl der Kindknoten pro Knoten angegeben.

Danach wird der JTree erstellt, welcher einer JScrollPane hinzugefügt wird und anschließend in das JPanel eingefügt wird.

Zum Schluß muß noch das Popup-Menü zum Aufklappen erzeugt werden und dem JTree mittels der Klasse JHPopupListener hinzugefügt werden.

import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeSelectionModel;



/**
 * @author hameister
 *
 */
public class JHTreePanel extends JPanel  implements  ActionListener {
    private JHTree tree;

    private int nodeCount = 0;
    private int deep = 7;
    private int numberOfNodes = 4;
  
	public JHTreePanel() {
		super(new GridLayout(1,0));
		//Create the nodes.
        DefaultMutableTreeNode top =new DefaultMutableTreeNode("Root");
        createRecursiveTree(top, deep, numberOfNodes);
        
        //Create a tree that allows one selection at a time.
        tree = new JHTree(top);
        tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);

        //Create the scroll pane and add the tree to it. 
        JScrollPane treeView = new JScrollPane(tree);

        add(treeView);
        
        JPopupMenu popup = new JPopupMenu();
        JMenuItem menuItem = new JMenuItem("Expand");
        menuItem.addActionListener(this);
        popup.add(menuItem);
        
        tree.addMouseListener(new JHPopupListener(popup));
	}

Die Klasse PopupListener kann als Inner-Class in der Datei JHTreePanel.java hinzugefügt werden. Diese Klasse sorgt einfach dafür, daß der JTree auf Mouse-Events reagiert. (Die Klasse ist aus dem Beispiel PopupMenuDemo übernommen.)

    class JHPopupListener extends MouseAdapter {
        JPopupMenu popup;

        JHPopupListener(JPopupMenu popupMenu) {
            popup = popupMenu;
        }

        public void mousePressed(MouseEvent e) {
            maybeShowPopup(e);
        }

        public void mouseReleased(MouseEvent e) {
            maybeShowPopup(e);
        }

        private void maybeShowPopup(MouseEvent e) {
            if (e.isPopupTrigger()) {
                popup.show(e.getComponent(),
                           e.getX(), e.getY());
            }
        }
    }

Um den JTree zu erzeugen wird eine rekursive Methode verwendet, die den Baum aufbaut.

private void createRecursiveTree(DefaultMutableTreeNode root, int deep, int numberOfNodes) {
	DefaultMutableTreeNode firstLevel = null;

	for(int firstL = 0 ; firstL < numberOfNodes ; firstL++) {
		firstLevel = new DefaultMutableTreeNode("N"+firstL+" "+deep);
		nodeCount++;
		root.add(firstLevel);
		if(deep>0) {
			createRecursiveTree(firstLevel, deep-1, numberOfNodes);
		}
	}
}

Als letztes muß noch die Methode actionPerformed des ActionListener-Interfaces implementiert werden. Die Methode wird aufgerufen, wenn im Popup-Menü Expand ausgewählt wird. Sie berechnet die Zeit, die zwischen dem Aufruf von tree.expandeAll() vergeht und gibt den Wert auf der Konsole aus.

@Override
public void actionPerformed(ActionEvent e) {
	System.out.println("Expand");
	long startTime = System.currentTimeMillis();
	tree.expandAll();
	long endTime = System.currentTimeMillis();
	System.out.println(deep+" "+numberOfNodes+" Time: "+(endTime-startTime)+" for "+nodeCount+" nodes.");
}