在构造函数中调用可覆盖的方法,例如 Swing 的 add()

Calling overridable methods like Swing's add() in constructor

我知道从构造函数中调用可重写的方法是个坏主意。但我也看到它在 Swing 中无处不在,其中像 add(new JLabel("Something")); 这样的代码一直出现在构造函数中。

以 NetBeans IDE 为例。它对构造函数中的可覆盖调用非常挑剔。然而,当它生成 Swing 代码时,它会将所有这些 add() 方法调用放入一个 initializeComponents() 方法中……然后从构造函数中调用该方法!隐藏问题和禁用警告的好方法(NetBeans 没有“调用可覆盖方法的私有方法是从构造函数调用的”警告)。但不是真正解决问题的方法。

这是怎么回事?我已经这样做了很多年,但对此总是有一种不安的感觉。有没有更好的初始化 Swing 容器的方法,除了制作一个额外的 init() 方法(并且不要忘记每次都调用它,这有点无聊)?

例子

这是一个极其人为的例子,说明事情是如何出错的:

public class MyBasePanel extends JPanel {
    public MyBasePanel() {
        initializeComponents();
    }

    private void initializeComponents() {
        // layout setup omitted
        // overridable call
        add(new JLabel("My label"), BorderLayout.CENTER);
    }
}

public class MyDerivedPanel extends MyBasePanel {
    private final List<JLabel> addedLabels = new ArrayList<>();

    @Override
    public void add(Component comp, Object constraints) {
        super.add(comp);
        if (comp instanceof JLabel) {
            JLabel label = (JLabel) comp;
            addedLabels.add(label); // NPE here
        }
    }
}

Netbeans 正在生成私有函数。

private initializeComponents() {...}

因此该方法不可重写。只有受保护和 public 方法是可覆盖的。

一个额外的函数使您的代码对于 Netbeans 示例更加清晰。 但一般来说,你可以节省地使用私有方法来初始化 类.

此外,如果您有多个构造函数,使用一种额外的方法进行初始化是可行的。

class Foo {

   int x,y;
   String bar;

   public Foo(x) {
      this.x = x;
      init();
   }

   public Foo(y) {
      this.y = y;
      init();
   }
   private void init() {
      // .. something complicated or much to do
      bar = "bla";
   }
}

为了避免在构造函数中将 Swing 组件连接在一起,您可以简单地将连接的责任交给另一个对象。例如,您可以将布线职责交给工厂:

public class MyPanelFactory {
    public MyBasePanel myBasePanel() {
        MyBasePanel myBasePanel = new MyBasePanel();
        initMyBasePanel(myBasePanel);
        return myBasePanel;
    }

    public MyDerivedPanel myDerivedPanel() {
        MyDerivedPanel myDerivedPanel = new MyDerivedPanel();
        initMyBasePanel(myDerivedPanel);
        return myDerivedPanel;
    }

    private void initMyBasePanel(MyBasePanel myBasePanel) {
        myBasePanel.add(new JLabel("My label"), BorderLayout.CENTER);
    }
}

或者您可以全力以赴,使用依赖项注入容器实例化所有 Swing 组件,并让容器触发连接。这是 Dagger 的示例:

@Module
public class MyPanelModule {
    static class MyBasePanel extends JPanel {
        private final JLabel myLabel;

        MyBasePanel(JLabel myLabel) {
            this.myLabel = myLabel;
        }

        void initComponents() {
            this.add(myLabel, BorderLayout.CENTER);
        }
    }

    static class MyDerivedPanel extends MyBasePanel {
        private final List<JLabel> addedLabels = new ArrayList<>();

        MyDerivedPanel(JLabel myLabel) {
            super(myLabel);
        }

        @Override
        public void add(Component comp, Object constraints) {
            super.add(comp);
            if (comp instanceof JLabel) {
                JLabel label = (JLabel) comp;
                addedLabels.add(label);
            }
        }
    }

    @Provides MyBasePanel myBasePanel(@Named("myLabel") JLabel myLabel) {
        MyBasePanel myBasePanel = new MyBasePanel(myLabel);
        myBasePanel.initComponents();
        return myBasePanel;
    }

    @Provides MyDerivedPanel myDerivedPanel(@Named("myLabel") JLabel myLabel) {
        MyDerivedPanel myDerivedPanel = new MyDerivedPanel(myLabel);
        myDerivedPanel.initComponents();
        return myDerivedPanel;
    }

    @Provides @Named("myLabel") JLabel myLabel() {
        return new JLabel("My label");
    }
}

OOP 原则之一是:优先组合而不是继承。当我创建一个 Swing GUI 时,我从不扩展 Swing 组件,除非我创建一个新的通用 Swing 组件(如 JTreeTable、JGraph、JCalendar 等)。

所以我的代码看起来像:

public class MyPanel {
     private JPanel mainPanel;
     public MyPanel() {
         init();
     }
     private void init() {
          mainPanel = new JPanel();
     }
     public Component getComponent() {
         return mainPanel;
     }
}

public class MyComposedPanel {
     private JPanel mainPanel;
     public MyComposedPanel() {
         init();
     }
     private void init() {
          mainPanel = new JPanel();
          mainPanel.add(new MyPanel().getComponent());
     }
     public Component getComponent() {
         return mainPanel;
     }
}

这种方式有一个缺点:没有支持它的 GUI 生成器 ;)

一段时间后回来阅读接受的答案,我意识到有一种更简单的方法可以解决这个问题。如果调用可覆盖方法的责任可以转移到另一个 class,也可以使用工厂方法模式转移到静态方法:

class MyBasePanel extends JPanel {

    public static MyBasePanel create() {
        MyBasePanel panel = new MyBasePanel();
        panel.initializeComponents();
        return panel;
    }

    protected MyBasePanel() {
    }

    protected void initializeComponents() {
        // layout setup omitted
        // overridable call
        add(new JLabel("My label"), BorderLayout.CENTER);
    }
}

class MyDerivedPanel extends MyBasePanel {

    private final List<JLabel> addedLabels = new ArrayList<>();

    public static MyDerivedPanel create() {
        MyDerivedPanel panel = new MyDerivedPanel();
        panel.initializeComponents();
        return panel;
    }

    protected MyDerivedPanel() {
    }

    @Override
    public void add(Component comp, Object constraints) {
        super.add(comp);
        if (comp instanceof JLabel) {
            JLabel label = (JLabel) comp;
            addedLabels.add(label); // no more NPE here
        }
    }
}

当然,在subclassing的时候还是要记得调用initializeComponents,但至少不是每次创建实例的时候!适当记录,这种方法既简单又可靠。