The Bad Programmer

Swing: Composition (Has-A) vs Inheritance (Is-A)



Introduction

Even a beginning object orientated developer probably knows that when possible you should favor Composition over Inheritance. Some people may also know this as the difference between Has-A (Composition) and Is-A (Inheritance). Annoyingly, for whatever reason, developers don’t seem to apply this principle when writing Java Swing code. Before we cover that let’s have a little composition vs. inheritance primer or recap whatever the case may be for your level of experience.

Composition vs. Inheritance Primer/Recap

It is generally better not to sub-class and instead have a class that has other classes as properties and offers methods to control the behavior of those owned classes. That is probably a poor description so to illustrate the concept let’s consider an example. Let’s say you need to implement a caching class for some data that is expensive performance-wise to retrieve. There are two ways to accomplish this. You could extend a collection like HashMap and then your cache is a HashMap with some custom methods to manipulate the cache. The other way would be to create a class that has a instance of HashMap as a property and then offers methods to manipulate that HashMap. From the description it may sound like these are very similar. However, the implementation and consequences are vastly different. I will let the following examples illustrate.

Example of Composition (Has-A)

public class Cache {
    private Map cache;
    public Cache() {
        this.cache = new HashMap();
    }
    //Methods to manipulate cache here
}

Example of Inheritance (Is-A)

public class Cache extends HashMap {
    public Cache() {
    }

    //Methods to manipulate cache here
}

I don’t think you will find many people that could argue with a straight face that implementing this with inheritance is the preferred way. In this example using composition is clearly the better design.

Apply Composition Over Inheritance to Java Swing Code

There is no reason to not apply the same preference for composition to Swing code. Yet, in my experience it is much more common (practically universal) for Swing code to be implemented with inheritance. I believe this is because all of the Swing example code in the Swing Tutorial shows inheritance rather than composition. However, tutorial code is meant to be a quick example of the demonstrated component and not production quality code. Here is an example of what you most certainly will see if you develop with Swing:

public class ButtonPanel extends JPanel {
    public ButtonPanel() {
        this.add(new JButton("Button1"));
        this.add(new JButton("Button2"));
        this.add(new JButton("Button3"));
    }
}

If you are doing it this way you are doing it wrong. There is no flexibility here. Someone that wants to reuse your ButtonPanel is stuck using it the way you lay it out. What if another developer wants all the functionality of the ButtonPanel but they want the buttons to be stacked vertically but your layout lays them out horizontally? It would make much more sense for ButtonPanel to not be a JPanel and instead have a JPanel with getters for the JPanel and the JButtons. This would let someone reuse ButtonPanel much more easily.

I have a full example here. This example consists of two classes and is a fully functioning Swing application. Note, that I do not inherit JPanel or JFrame instead I have classes that have JPanels and JFrames. The advantage here is that UrlEntryPanel can easily be reused. It offers a default layout method which returns a panel. However, anyone using this class is free to ignore that method and instead use the getters for the buttons and put them in a container of their choice however they need them laid out. Yet they still get all of the functionality of the UrlEntryPanel.

public class UrlDemo {
    private JFrame topLevelFrame;

    public UrlDemo() {
        //Do any non-GUI initialization here
    }

    public void createGui() {
        //GUI initialization goes here
        this.topLevelFrame = new JFrame("Super Duper URL Application");
        UrlEntryPanel panel = new UrlEntryPanel();
        this.topLevelFrame.add(panel.getDefaultLayout(), BorderLayout.CENTER);
        this.topLevelFrame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent ev) {
                System.exit(0);
            }
        });

        this.topLevelFrame.pack();
        this.topLevelFrame.setVisible(true);
    }

    public static void main(String[] args) {
        final UrlDemo demo = new UrlDemo();
        Runnable runnable = new Runnable() {
            public void run() {
                demo.createGui();
            }
        };

        EventQueue.invokeLater(runnable);
    }
}
public class UrlEntryPanel {
    private JTextField textField;
    private JButton button;
    private JButton exitButton;

    public UrlEntryPanel() {
        this.textField = new JTextField(20);
        this.textField.setText("http://www.lp.org");
        this.button = new JButton(new OpenUrlAction());
        this.exitButton = new JButton(new ExitAction());
    }

    public JButton getButton() {
        return button;
    }

    public JButton getExitButton() {
        return exitButton;
    }

    public JTextField getTextField() {
        return textField;
    }

    public JPanel getDefaultLayout() {
        JPanel panel = new JPanel();
        panel.add(this.textField);
        panel.add(this.button);
        panel.add(this.exitButton);
        return panel;
    }

    private class OpenUrlAction extends AbstractAction {
        public OpenUrlAction() {
            super("Open URL");
        }

        public void actionPerformed(ActionEvent actionEvent) {
            String osName = System.getProperty("os.name");
            String url = textField.getText();
            if (osName.toLowerCase().startsWith("windows")) {
                try {
                    Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + url);
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                }
            } else if (osName.startsWith("Mac OS")) {
                try {
                    Class fileMgr = Class.forName("com.apple.eio.FileManager");
                    Method openURL = fileMgr.getDeclaredMethod("openURL", new Class[]{String.class});
                    openURL.invoke(null, url);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static class ExitAction extends AbstractAction {
        public ExitAction() {
            super("Exit");
        }

        public void actionPerformed(ActionEvent e) {
            System.exit(0);
        }
    }
}

When is it OK to sub-class a Swing Component?

Does this mean you should never extend a Swing component? Nope. You simply need to know when it is appropriate to extend a Swing component. My rule-of-thumb is if you need to override the paintComponent() method of a Component then you need to use inheritance. Otherwise, use composition and make a class that has your needed components. If you aren’t over-riding paintComponent() then you really don’t have a new type of a component and inheritance isn’t appropriate.