如何避免在子类中创建无用的传递构造函数,只是为了将参数传递给“super()”?

12
在Java中,据我所知,子类不会继承带有参数的构造函数。
例如:
public class Parent {
    public Parent(int x) {
        DoSomethingWithX(x);
    }
}

public class Child extends Parent {
    // Compile fails with "Implicit super constructor Parent() is undefined 
    // for default constructor. Must define an explicit constructor
}
在Child类中创建一个无用的传递构造函数是唯一的修复方法:
public class Child extends Parent {
    public Child(int x) {
         super(x);
    }
}

问题:

如果我有一个包含 6-10 个子类的复杂继承体系,给每个子类都加上这样一个无意义的传递参数的构造函数似乎不是一个好主意!

  • 代码看起来很愚蠢(10 个类中都有相同的方法副本)。
  • 更糟糕的是,与任何重复代码一样,代码变得容易出错——任何更改(例如添加一个参数)都需要在 10 个地方进行修改,而不是一个地方。

问题:

有没有一种方法可以避免在大型类层次结构中出现这种问题?

注意:

我知道一种解决方案是为参数设置一个 setter(必须从构造函数单独调用)。但这种解决方案有几个很大的缺点,因此不能接受。


1
@Zar - 接口或抽象类如何帮助我解决我所描述的问题? - DVK
1
乍一看,我对设计有所疑虑,但让我们假设您无法对其进行任何更改...如果您的构造函数接收一个Map或CustomClass类,其中包含您可能需要的每个参数,那该怎么办呢? 这样,如果您需要更改参数和参数,您只需更改您感兴趣的类即可。 - psabbate
4
简短回答:不行。详细回答:不,不可能。 - Louis Wasserman
1
@psabbate - 是的。目前您建议的似乎是最不糟糕的解决方案 :) - DVK
@DVK 不是你来解决它的选择吗?那么谁来呢?请看我的答案,这是解决这个问题的标准方案,仍然保证初始化,只是重新排列了一下。 - Bohemian
显示剩余9条评论
3个回答

4
大约15年前,我遇到了与你类似的问题,这个问题涉及一个非常广泛(但仅稍微深入)的层次结构(是的,朋友们,它是这样的,有一个原因)。但由于它们都源自我们需要定期添加更多信息的基础,很快就明显添加参数到基础构造函数是非常痛苦的。
默认情况下不继承构造函数是一件好事,但有时您想要继承它们。我有时想要一个编译时注释,可以添加到类中,告诉编译器“自动为任何未明确实现的超级构造函数生成构造函数”。但是我们没有它,如果成功的话,JSR将需要数年时间才能通过...
没有那个神奇的注释,我们使用的解决方案正是@psabbate 在他/她的评论中提到的

如果您的构造函数接收Map或CustomClass类,该类包含您可能需要的每个参数。这样,如果您需要更改参数和参数,则只需更改您感兴趣的类即可。

...但是参数类有特定的类型层次结构(而不是Map)。

为了完整起见,这里举个例子:

// The standard parameters needed
class StandardParams {
    private String thisArg;

    public StandardParams(String thisArg) {
        this.thisArg = thisArg;
    }

    public String getThisArg() {
        return this.thisArg;
    }
}

// The base class
class Base {
    public Base(StandardParams args) {
        System.out.println("Base: " + args.getThisArg());
    }
}

// A standard subclass
class Sub1 extends Base {
    public Sub1(StandardParams args) {
        super(args);
        System.out.println("Sub1 thisArg: " + args.getThisArg());
    }
}

对我来说,这甚至不是“最小的坏处”。拥有代表层次结构所需基本信息的类型是具有表现力的。
如果子类需要比标准参数更多的信息(这在我们这里出现了),您可以选择使用第二个参数类类型作为第二个参数(但是在树中则有两种类型的构造函数)或者在参数类层次结构中使用继承;我们选择了后者:
// Extended parameters (naturally you make these names meaningful)
class ExtendedParams extends StandardParams {
    private String thatArg;

    public ExtendedParams(String thisArg, String thatArg) {
        super(thisArg);
        this.thatArg = thatArg;
    }

    public String getThatArg() {
        return this.thatArg;
    }
}

// A subclass requiring extended parameter information
class Sub2 extends Base {
    public Sub2(ExtendedParams args) {
        super(args);
        System.out.println("Sub2 thisArg: " + args.getThisArg());
        System.out.println("Sub2 thatArg: " + args.getThatArg());
    }
}

在我的情况下,我们在继承结构中的大约30个主类中只有三个参数类(标准类和两个子类,用于树中特定分支)。最后需要注意的是:对于我们来说,在构建参数类时,我们需要提供一组核心内容,这些内容不会变化,并且有大约八个选项具有合理的基本默认值,但您可以进行覆盖。为了避免参数类构造函数数量的激增,我们最终采用了一种简单的生成器模式(“简单”的原因是我们将类作为其自身的生成器,而不是将其分离出来;这并不是必要的,因为更改的内容具有廉价的默认值,因此即使在构建时实例也始终处于有效状态)。因此,我们的构建过程看起来像这样:
Thingy t = new Thingy(
    new ThingyParams(basic, construction, info)
    .withAnswer(42)
    .withQuestion("Life, the Universe, and Everything")
);

这正是我昨天最终做的事情。不确定为什么你标记为 CW :) - DVK
@DVK: :-) 仅为避免出现任何不当行为的外观,因为psabbate在评论中几乎提到了同样的事情。 - T.J. Crowder

4

使用工厂方法和反射:

public class A {
    protected A() {
        // No longer do initialization in constructor
        // initialize in init(), which is guaranteed to be called
    }

    protected A init(int i) {
        // do all initialization with i in here
        return this;
    };

    public static <T extends A> T create(Class<T> clazz, int i) {
        try {
            return (T) clazz.newInstance().init(i);
        } catch (InstantiationException | IllegalAccessException e) {
            throw new IllegalArgumentException("not going to happen", e);
        }
    }
}

创建实例的步骤如下:
B b = A.create(B.class, 86);

根据您所需的学科级别,子类可以很简单,例如:

public class B extends A {
    // nothing special needed
}

但是允许直接实例化B b = new B()而不需要int,或者您可以像这样收紧所有内容以防止此类情况:

public class B extends A {
    protected B() {
        super();
    }
    // nothing else special needed
}

这需要所有实例化都通过工厂方法进行。

使用你的集成开发环境,更改调用点的代码可以轻松地进行重构。

如果您的基类更改需要更多/不同的初始化变量,则只需更改工厂方法(子类中不会发生任何更改),或者您可以添加一个新的工厂方法,保留旧的工厂方法以实现向后兼容。


太好了!我实际上在选择@T.J. Crowder的方案之前尝试过这个解决方案(它是我最喜欢的),但在编写构建器时无法快速弄清楚泛型。+1,非常好的方法。 - DVK
@ dvk np。您知道您可以随时更改接受的答案。只需单击其他答案的接受勾选即可。 - Bohemian
这是我希望能接受两个的罕见情况之一。如果我没有忘记的话,我可能会给你一个赏金。 - DVK

0

你在Parent类中只有一个带参数的构造函数,这告诉我你不能创建一个没有该整数的Parent对象。通过创建一个Child对象,你也会继承创建一个Parent对象。因为我们需要一个整数来创建Parent对象,所以Child对象必须从某个地方提供它,可以是它自己的构造函数参数或其他静态值。

如果你不想在每个Parent子类中都提供一个传递构造函数,那么你需要在其中提供一个无参构造函数并定义其含义。这可以是你已经拥有的构造函数的补充,也可以替换它,这取决于你。但是无参构造函数对于你想要做的事情是必要的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接