Search code examples
javamultithreadingswinganonymous-inner-class

Accessing member variable of enclosing scope in anonymous inner class


EDIT: I have fixed it thanks to rsutormin's link about making the simplest program possible that exhibits the same error. I did this, and it kept working. I added more and more things from the original that I could imagine would contribute and it just kept on working. At first was annoyed but then I realised that if it kept on working, eventually I'd just have a copy of my program, except working. So that was fine. Eventually I found the thing that when added caused the same behaviour as before. In the action event listener method the other cases (besides the button) were for sorting (alphabetical, by size, etc). After the switch statement I had

setTableRows();
update();

and it was only after adding this back (update() fixes the column widths and setTableRows() redoes the content in the new order in case it has been resorted) that the problem reappeared. Specifically setTableRows(). I still don't know what it is exactly that would cause a problem with the new thread there that isn't there when it isn't (I am thinking something to do with the fact that the thread is running WHILE the point of execution continues onto this bit - but the method does not edit thingList, only read it...?), but here is the method in case someone else has a similar problem and can identify something in common with their own code.

private void setTableRows()
{
    DefaultTableModel dtm = new DefaultTableModel(0, 0);
    String[] header = new String[]{"Name", "Size"};
    dtm.setColumnIdentifiers(header);
    for(Thing thing : thingList.getList())
    {
        String sizeColumn = Integer.toString(thing.getSize()) + "MB";
        if(sizeColumn.equals("0MB"))
        {
            sizeColumn = "?";
        }
        dtm.addRow(new Object[]{thing.getTitle(), sizeColumn});
    }
    thingTable.setModel(dtm);
}

Summary: I have a Java Swing program that has a slow, long-running job to do on the click of a button. The UI was freezing so I thought I should do it in a new Thread. However, the member variables of the main window are not getting passed properly to the thread. Only the parts that get created on construction are available, nothing else changed afterwards is available.

Most relevant section:

public class MainWindow extends JPanel implements ActionListener
{

    ......

    @Override
    public void actionPerformed(ActionEvent e)
    {

        switch(e.getActionCommand())
        {
            ...
          case "go":
          Thread t = new Thread()
            {
                @Override
                public void run()
                {
                    try
                    {
                        DoStuff d = new DoStuff(statusLabel);
                        d.doStuff(thingList);
                    }
                    catch (IOException | InterruptedException e1)
                    {
                        e1.printStackTrace();
                    }
                }
            };
            t.start();

            break;
        }
    ....
}

The MainWindow (extends JPanel implements ActionListener) has these member variables:

ThingList thingList

and

JTextArea statusLabel

(and many others that aren't relevant)

ThingList is a class with two relevant member variables:

ArrayList<Thing> list

and

ArrayList<Thing> selectedList

list is filled in the constructor of ThingList, and thingList is created in MainWindow's constructor. selectList is filled when the user clicks on a JTable in its listener, and things are added like this in ThingList:

public void setSelection(int[] rows)
{
    selectedList.clear();
    for(int i = 0; i < rows.length; i ++)
    {
        selectedList.add(list.get(rows[i]));
    }
}

Where the rows passed are the ones clicked on.

Thing is just a data-holding class with getters and setters and that's all.

...

The actual behaviour

  • In doStuff, the passed-in ThingList has a correctly populated list but an empty selectedList

  • If you breakpoint inside run(), and hover over one of the variables, they appear bold like if you hovered over a variable that was out of scope at the point of execution (this is in Eclipse).

  • If you make a local variable just before the Thread definition and use that instead, it doesn't appear bolded when hovered over (and it has an id and you can go into the two lists and look at their data), but list is still populated while selectedList is empty

  • If you give Thread a name and its own member variables, and pass the real ones in in a constructor, it behaves as above.

  • If you add a dummy Thing to selectedList inside the method that populates list, that appears in the passed-in ThingList to doStuff. But I can't know in advance what the user will click on so I can't populate selectedList in the constructor as a solution.

I have been trying to read a lot about inner classes and everything seems to say that they should be able to use the enclosing class' member variables without issue, but just that local variables might have to be final. I am guessing it is different because of being a Thread not just a regular run-of-the-mill inner class. But I can't seem to find anyone with this same issue.

Thanks in advance for any help.

Edit: Just had an idea to try and print out the memory addresses of the objects and see if they are the same ones (I had assumed not). I get this result:

thingList address outside of the new thread: main.ThingList@332f9531
selectedList address outside of the new thread: [main.Thing@3f12d523]
thingList address inside run(): main.ThingList@332f9531
selectedList address inside run(): []

From having

System.out.println("thingList address outside of the new thread: " + thingList);
            System.out.println("selectedList address outside of the new thread: " + thingList.getSelectedList());

Just before Thread t = new Thread(), and

System.out.println("thingList address inside run(): " + thingList);
                    System.out.println("selectedList address inside run(): " + thingList.getSelectedList());

just inside run() (before try{ )

So they are the same object (I am assuming that's what the hash code represents - that it has the same memory location?) but selectedList is blank (or whatever [] signifies) by the time it is inside the thread.


Solution

  • Maybe you need "SwingUtilities.invokeLater()" to be able to correctly change properties of Swing-related components? Here is my way of doing it:

    import java.awt.Color;
    
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.SwingUtilities;
    
    public class ColorFrame extends JFrame {
        private JPanel innerPanel = new JPanel();
        
        public static void main(String[] args) {
            new ColorFrame().setVisible(true);
        }
        
        public ColorFrame() {
            this.add(innerPanel);
            this.setSize(500, 500);
            this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            new Thread() {
                @Override
                public void run() {
                    while(true) {
                        try {
                            Thread.sleep(1000);
                        } catch(Exception ex) {
                            break;
                        }
                        SwingUtilities.invokeLater(new Runnable() {
                            @Override
                            public void run() {
                                int r = (int)(255 * Math.random());
                                int g = (int)(255 * Math.random());
                                int b = (int)(255 * Math.random());
                                ColorFrame.this.innerPanel.setBackground(new Color(r, g, b));
                            }
                        });
                    }
                }
            }.start();
        }
    }