This discussion is archived
6 Replies Latest reply: Nov 18, 2010 11:05 AM by 815773 RSS

JTree disappears while expanding node. Possibly a hashCode() issue.

815773 Newbie
Currently Being Moderated
Hi, I have a JTree with custom model. I test it with the following structure:

+root
----+category
---------+file

When the app starts, root and category are visible, file is hidden. When I try to expand the category node, the whole JTree disappears. Any further clicks in the place previously occupied by the JTree result in a java.lang.NullPointerException at javax.swing.plaf.basic.BasicTreeUI$Handler.handleSelection().

The nodes I use represent a simple tree, where every node has its value and a LinkedList of its children. The model simply translates this to the "language" of TreeModel.

It is essential for me to correctly define equals() and hashCode() for the nodes. I found out that if the nodes' hashCode() doesn't rely on its list of children, everything works well. But I want it to rely on the children!

I use JRE 1.6.0_22 on Win7.

Where do I do something wrong, please?

Thanks very much for any help ;)

PS: I managed to workaround this - in a TreeSelectionListener I call tree.setModel(tree.getModel()) and then I restore the expanded and selected nodes. This works, but of course I'd rather have a cleaner solution. Moreover, this seems as a bug in JRE for me.

SSCCE included:
import java.awt.Dimension;
import java.util.LinkedList;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

public class JTreeTest
{
    public static void main(String[] args)
    {
        // setup the tree
        final File list = new File();
        list.setValue("root");

        File category = new File();
        list.getFiles().add(category);
        category.setValue("category");

        File file = new File();
        category.getFiles().add(file);
        file.setValue("file");

        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run()
            {
                JFrame frame = new JFrame();

                // setup the JTree
                JTree tree = new JTree(new MyModel(list));
                frame.add(tree);

                frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
                frame.setSize(new Dimension(200, 100));
                frame.setVisible(true);
            }
        });
    }

    // this model is very very basic and I don't see much space here for errors...
    static class MyModel implements TreeModel
    {
        protected File                    root;

        protected List<TreeModelListener> treeModelListeners = new LinkedList<TreeModelListener>();

        public MyModel(File root)
        {
            this.root = root;
        }

        @Override
        public File getRoot()
        {
            return root;
        }

        @Override
        public Object getChild(Object parent, int index)
        {
            if (index < 0)
                return null;

            if (!(parent instanceof File))
                return null;

            File fParent = (File) parent;

            try {
                return fParent.getFiles().get(index);
            } catch (ArrayIndexOutOfBoundsException e) {
                return null;
            }
        }

        @Override
        public int getChildCount(Object parent)
        {
            if (!(parent instanceof File))
                return 0;

            File fParent = (File) parent;

            return fParent.getFiles().size();
        }

        @Override
        public boolean isLeaf(Object node)
        {
            return getChildCount(node) == 0;
        }

        @Override
        public void valueForPathChanged(TreePath path, Object newValue)
        {
            if (!(newValue instanceof File))
                return;

            if (path.getParentPath() == null) {
                fireTreeNodesChanged(new TreeModelEvent(this, path, null, null));
            } else {
                fireTreeNodesChanged(new TreeModelEvent(this, path.getParentPath(), new int[] { getIndexOfChild(path
                        .getParentPath().getLastPathComponent(), newValue) }, new Object[] { newValue }));
            }
        }

        @Override
        public int getIndexOfChild(Object parent, Object child)
        {
            if (parent == null || child == null)
                return -1;

            if (!(parent instanceof File) || !(child instanceof File))
                return -1;

            File fParent = (File) parent;

            return fParent.getFiles().indexOf(child);
        }

        @Override
        public void addTreeModelListener(TreeModelListener l)
        {
            treeModelListeners.add(l);
        }

        @Override
        public void removeTreeModelListener(TreeModelListener l)
        {
            treeModelListeners.remove(l);
        }

        public void fireTreeNodesChanged(TreeModelEvent e)
        {
            for (TreeModelListener listener : treeModelListeners) {
                listener.treeNodesChanged(e);
            }
        }
    }

    static class File
    {
        static int fileId = 0;
        int        id;
        List<File> files  = null;
        String     value  = null;

        public File()
        {
            // ensure each item will have a really unique identifier, so no equals() collisions should occur
            id = fileId++;
        }

        public List<File> getFiles()
        {
            if (files == null)
                files = new LinkedList<File>();
            return files;
        }

        public String getValue()
        {
            return value;
        }

        public void setValue(String value)
        {
            this.value = value;
        }

        // generated by Eclipse code helpers
        @Override
        public int hashCode()
        {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((value == null) ? 0 : value.hashCode());
            // ///////HERE IT IS////////
            // if you uncomment the following line, the list will get empty when you expand the second category and
            // any following clicks in any place that should be occupied by an item will result in a
            // NullPointerException
            //
            result = prime * result + ((files == null) ? 0 : files.hashCode());
            //
            result = prime * result + id;
            return result;
        }

        @Override
        public boolean equals(Object obj)
        {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            File other = (File) obj;
            if (value == null) {
                if (other.value != null)
                    return false;
            } else if (!value.equals(other.value))
                return false;
            if (files == null) {
                if (other.files != null)
                    return false;
            } else if (!files.equals(other.files))
                return false;
            if (id != other.id)
                return false;
            return true;
        }

        @Override
        public String toString()
        {
            return "File [value=" + value + "]";
        }
    }

}
  • 1. Re: JTree disappears while expanding node. Possibly a hashCode() issue.
    800268 Expert
    Currently Being Moderated
    The problem is your lazy initialization of File.files. A File before calling getFiles() on it is not equal to itself after calling it (null is not equal to an empty List). TreePath#equals() (used by JTree) is based on equals() not identity so the UI get confused.

    TreeModel javadoc:
    JTree and its related classes make extensive use of TreePaths for indentifying nodes in the TreeModel. If a TreeModel returns the same object, as compared by equals, at two different indices under the same parent than the resulting TreePath objects will be considered equal as well. Some implementations may assume that if two TreePaths are equal, they identify the same node. If this condition is not met, painting problems and other oddities may result. In other words, if getChild for a given parent returns the same Object (as determined by equals) problems may result, and it is recommended you avoid doing this.
    Similarly JTree and its related classes place TreePaths in Maps. As such if a node is requested twice, the return values must be equal (using the equals method) and have the same hashCode.
  • 2. Re: JTree disappears while expanding node. Possibly a hashCode() issue.
    815773 Newbie
    Currently Being Moderated
    Thank you much for your quick reply!

    Now I see that I've done too much simplifications in my SSCCE. The basic idea of my use of JTree is that some nodes reload their values when selected. So I need the JTree to handle nodes with changing hashcode.

    I've added a selection listener to the SSCCE that changes the value of the selected node and then fires a treeStructureChanged event in the model.

    But if I click the last node, the JTree freezes and any further clicking on it results in the same NPE as above.

    I think the problem will be in the valueForPathChanged method of the model, but I don't know how to write it better. (I've read that a TreePath is identified by its last path component, which is the node that was changed - so I think the JTree loses control over the whole TreePath, too).

    Here is the edited SSCCE:
    import java.awt.Dimension;
    import java.util.LinkedList;
    import java.util.List;
    
    import javax.swing.JFrame;
    import javax.swing.JTree;
    import javax.swing.SwingUtilities;
    import javax.swing.WindowConstants;
    import javax.swing.event.TreeModelEvent;
    import javax.swing.event.TreeModelListener;
    import javax.swing.event.TreeSelectionEvent;
    import javax.swing.event.TreeSelectionListener;
    import javax.swing.tree.TreeModel;
    import javax.swing.tree.TreePath;
    
    public class JTreeTest
    {
        public static void main(String[] args)
        {
            // setup the tree
            final File list = new File();
            list.setValue("root");
    
            File category = new File();
            list.getFiles().add(category);
            category.setValue("category");
    
            File file = new File();
            category.getFiles().add(file);
            file.setValue("file");
    
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run()
                {
                    JFrame frame = new JFrame();
    
                    // setup the JTree
                    JTree tree = new JTree(new MyModel(list));
                    tree.addTreeSelectionListener(new MyTreeSelectionListener());
                    frame.add(tree);
    
                    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
                    frame.setSize(new Dimension(200, 100));
                    frame.setVisible(true);
                }
            });
        }
    
        // this model is very very basic and I don't see much space here for errors...
        static class MyModel implements TreeModel
        {
            protected File                    root;
    
            protected List<TreeModelListener> treeModelListeners = new LinkedList<TreeModelListener>();
    
            public MyModel(File root)
            {
                this.root = root;
            }
    
            @Override
            public File getRoot()
            {
                return root;
            }
    
            @Override
            public Object getChild(Object parent, int index)
            {
                if (index < 0)
                    return null;
    
                if (!(parent instanceof File))
                    return null;
    
                File fParent = (File) parent;
    
                try {
                    return fParent.getFiles().get(index);
                } catch (ArrayIndexOutOfBoundsException e) {
                    return null;
                }
            }
    
            @Override
            public int getChildCount(Object parent)
            {
                if (!(parent instanceof File))
                    return 0;
    
                File fParent = (File) parent;
    
                return fParent.getFiles().size();
            }
    
            @Override
            public boolean isLeaf(Object node)
            {
                return getChildCount(node) == 0;
            }
    
            @Override
            public void valueForPathChanged(TreePath path, Object newValue)
            {
                if (!(newValue instanceof File))
                    return;
    
                if (path.getParentPath() == null) {
                    fireTreeStructureChanged(new TreeModelEvent(this, path, null, null));
                } else {
                    fireTreeStructureChanged(new TreeModelEvent(this, path.getParentPath(), null, null));
                }
            }
    
            @Override
            public int getIndexOfChild(Object parent, Object child)
            {
                if (parent == null || child == null)
                    return -1;
    
                if (!(parent instanceof File) || !(child instanceof File))
                    return -1;
    
                File fParent = (File) parent;
    
                return fParent.getFiles().indexOf(child);
            }
    
            @Override
            public void addTreeModelListener(TreeModelListener l)
            {
                treeModelListeners.add(l);
            }
    
            @Override
            public void removeTreeModelListener(TreeModelListener l)
            {
                treeModelListeners.remove(l);
            }
    
            public void fireTreeNodesChanged(TreeModelEvent e)
            {
                for (TreeModelListener listener : treeModelListeners) {
                    listener.treeNodesChanged(e);
                }
            }
    
            public void fireTreeStructureChanged(TreeModelEvent e)
            {
                for (TreeModelListener listener : treeModelListeners) {
                    listener.treeStructureChanged(e);
                }
            }
        }
    
        static class File
        {
            static int       fileId = 0;
            int              id;
            final List<File> files  = new LinkedList<File>();
            String           value  = null;
    
            public File()
            {
                // ensure each item will have a really unique identifier, so no equals() collisions should occur
                id = fileId++;
            }
    
            public List<File> getFiles()
            {
                return files;
            }
    
            public String getValue()
            {
                return value;
            }
    
            public void setValue(String value)
            {
                this.value = value;
            }
    
            // generated by Eclipse code helpers
            @Override
            public int hashCode()
            {
                final int prime = 31;
                int result = 1;
                result = prime * result + ((value == null) ? 0 : value.hashCode());
                // ///////HERE IT IS////////
                // if you uncomment the following line, the list will get empty when you expand the second category and
                // any following clicks in any place that should be occupied by an item will result in a
                // NullPointerException
                //
                result = prime * result + ((files == null) ? 0 : files.hashCode());
                //
                result = prime * result + id;
                return result;
            }
    
            @Override
            public boolean equals(Object obj)
            {
                if (this == obj)
                    return true;
                if (obj == null)
                    return false;
                if (getClass() != obj.getClass())
                    return false;
                File other = (File) obj;
                if (value == null) {
                    if (other.value != null)
                        return false;
                } else if (!value.equals(other.value))
                    return false;
                if (files == null) {
                    if (other.files != null)
                        return false;
                } else if (!files.equals(other.files))
                    return false;
                if (id != other.id)
                    return false;
                return true;
            }
    
            @Override
            public String toString()
            {
                return "File [value=" + value + "]";
            }
        }
    
        static class MyTreeSelectionListener implements TreeSelectionListener
        {
    
            @Override
            public void valueChanged(final TreeSelectionEvent e)
            {
                if (!e.isAddedPath()) {
                    return;
                }
    
                final Object selected = e.getPath().getLastPathComponent();
                if (selected instanceof File) {
                    final File file = (File) selected;
                    final JTree tree = (JTree) e.getSource();
                    new Thread() {
                        @Override
                        public void run()
                        {
                            file.setValue("newVal");
                            tree.getModel().valueForPathChanged(e.getPath(), file);
                            SwingUtilities.invokeLater(new Runnable() {
                                @Override
                                public void run()
                                {
                                    tree.invalidate();
                                }
                            });
                        }
                    }.start();
                }
            }
    
        }
    
    }
  • 3. Re: JTree disappears while expanding node. Possibly a hashCode() issue.
    800268 Expert
    Currently Being Moderated
    The problem is still that you're changing the hashCode/equals result of the tree nodes.

    Why not use DefaultTreeModel with DefaultMutableTreeNodes and use you File class as the user objects?

    Use a TreeExpandListener or sub class DefaultTreeModel to lazy load the values.

    Note that changes to a TreeModel which is showing in the GUI must be updated on the EDT. See the Swing concurrency tutorial.
  • 4. Re: JTree disappears while expanding node. Possibly a hashCode() issue.
    815773 Newbie
    Currently Being Moderated
    So you think it isn't possible to have directly implemented nodes with changing hashcodes in a JTree?

    I hoped that the treeStructureChanged event would help here... I even tried generating a treeNodeRemoved and treeNodeInserted event, but this failed on the fact, that I don't have a TreePath representing the old node after changing its hashcode... And I don't want to remove it before the value reload happens (it may take a long time in my case, so the node would disappear, which I don't want to happen).

    I looked at DefaultMutableTreeNode, and I see that it does it's "magic" by not overriding hashCode().

    The reason why I don't use DefaultMutableTreeNode is that I don't want to mess the code with unnecessary classes. But it seems that DefaultMutableTreeNode could be necessary...
  • 5. Re: JTree disappears while expanding node. Possibly a hashCode() issue.
    800268 Expert
    Currently Being Moderated
    peci1 wrote:
    So you think it isn't possible to have directly implemented nodes with changing hashcodes in a JTree?
    If know so because the javadoc says it. Again:
    Similarly JTree and its related classes place TreePaths in Maps. As such if a node is requested twice, the return values must be equal (using the equals method) and have the same hashCode.
    I hoped that the treeStructureChanged event would help here...
    It might work if you fire the structure changed event one node up because you changed the hashcode of the actual parent when you changed the children of it. But catching all those gotchas (restoring expand state, etc) seems way too much work just to avoid a wrapper node class.
  • 6. Re: JTree disappears while expanding node. Possibly a hashCode() issue.
    815773 Newbie
    Currently Being Moderated
    Ok, thank you very much again for a helpful discussion ;)

Legend

  • Correct Answers - 10 points
  • Helpful Answers - 5 points