23.07.2011
Übersicht
Im folgenden wird erklärt, wie die Performance beim Aufklappen von großen JTree
s 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 JTree
s 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."); }