java - Llamar a métodos invalidables como Swing''s add() en el constructor
java super constructor (4)
Netbeans está generando la función privada.
private initializeComponents() {...}
Por lo tanto, el método no es reemplazable. Solo los métodos protegidos y públicos son invalidables.
Una función adicional mantiene su código mucho más limpio para la expansión de Netbeans. Pero, en general, puede usar métodos privados para inicializar clases.
Además, si tiene múltiples constructores, es práctico usar un método adicional para la inicialización.
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";
}
}
Sé que llamar métodos invalidables de los constructores es una mala idea. Pero también veo que se está haciendo en todas partes con Swing, donde código como add(new JLabel("Something"));
ocurre en constructores todo el tiempo.
Tome NetBeans IDE, por ejemplo. Es muy exigente con las llamadas anulables en los constructores. Y, sin embargo, cuando genera el código Swing, pone todas esas llamadas al método add()
en un método initializeComponents()
... ¡que luego se llama desde el constructor! Una buena manera de ocultar un problema y deshabilitar la advertencia (NetBeans no tiene un "método privado que invoca métodos llamables desde un constructor"). Pero no es realmente una forma de resolver el problema.
¿Que está pasando aqui? Lo he estado haciendo durante años, pero siempre tuve una sensación incómoda sobre esto. ¿Hay una mejor manera de inicializar los contenedores Swing, a excepción de hacer un método init()
adicional (y no olvidar llamarlo siempre, lo cual es un poco aburrido)?
Ejemplo
Aquí hay un ejemplo extremadamente artificial de cómo las cosas pueden salir mal:
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
}
}
}
Para evitar el cableado de los componentes Swing en el constructor, simplemente podría delegar la responsabilidad del cableado a otro objeto. Por ejemplo, podría otorgar derechos de cableado a una fábrica:
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);
}
}
O bien, podría sacar todo y crear una instancia de todos sus componentes Swing con un contenedor de inyección de dependencia y hacer que el contenedor active el cableado. Aquí hay un ejemplo con 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");
}
}
Uno de los principios de OOP es: Prefiere la composición sobre la herencia. Cuando creo una GUI de Swing, nunca extiendo los componentes de Swing, excepto que creo un nuevo componente Swing de propósito general (como JTreeTable, JGraph, JCalendar, etc.).
Entonces mi código se ve así:
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;
}
}
De esta manera tiene una desventaja: no hay un constructor de GUI que lo soporte;)
Volviendo después de un rato y leyendo la respuesta aceptada, me di cuenta de que hay una forma aún más simple de resolver este problema. Si la responsabilidad de llamar a métodos invalidables se puede mover a otra clase, también se puede mover a un método estático, utilizando el patrón de método de fábrica:
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
}
}
}
Por supuesto, todavía hay que recordar llamar a initializeComponents
al crear subclases, ¡pero al menos no cada vez que se crea una instancia! Correctamente documentado, este enfoque puede ser simple y confiable.