构造函数重载 - Java 中的最佳实践

Constructor overloading - best practice in Java

构造函数也可以像任何其他方法一样被重载,我知道这一点。由于一项任务,我决定使用具有多个构造函数的抽象超类:

抽象超类:

protected ListSortierer()
{
  this( null, null );
}

protected ListSortierer( List<E> li )
{
  this( li, null );
}

protected ListSortierer( Comparator<E> comp )
{
  this( null, comp );     
}

protected ListSortierer( List<E> li, Comparator<E> com )
{
  this.original = Optional.ofNullable( li );
  this.comp = Optional.ofNullable( com );
}

为了访问这些构造函数中的每一个,我在子类中也需要多个构造函数。

BubbleSort.java:

public ListBubbleSort()
{
  super();
}

public ListBubbleSort( List<E> li )
{
  super( li );
}

public ListBubbleSort( Comparator<E> com )
{
  super( com );
}

public ListBubbleSort( List<E> li, Comparator<E> com )
{
  super( li, com );
}

在这种情况下,子类的每个构造函数都调用超类的构造函数 immediately.It 我想到我可以再次引用自己的构造函数并传递 null 值:

public ListBubbleSort()
{
  this( null, null );
}

public ListBubbleSort( List<E> li )
{
   this( li, null );
}

public ListBubbleSort( Comparator<E> com )
{
   this( null, com );
}

public ListBubbleSort( List<E> li, Comparator<E> com )
{
   super( li, com );
}

这样做可以让我省略抽象超类中的 3 个重载构造函数,但会强制每个子类都遵循相同的模式。

我的问题是:在一致性的情况下,更好的方法是什么?处理抽象超类或子类中的缺失值?它对实例化有影响还是只是一个意见问题?

Doing so would allow me to omit 3 of the overloaded constructors in the abstract superclass but would enforce that every subclass follows the same pattern.

执行合同是通过抽象方法或接口实现的。你不能确定每个子类都会有这些构造函数,或者至少你不能确定每个子类都会正确地添加这些构造函数。

因此,考虑到 Encapsulation,这些构造函数最好放在超类中。

关于问题本身:我认为这两种选择都不理想。

您努力编写尽可能少的代码。您在添加重载时非常小心,因为它们看起来 方便 。实际上,您应该反其道而行之:认真思考您的 真实 用例是什么,并且只支持这些用例。

在您的情况下,整个练习的重点似乎是允许使用不同的实现进行排序。从这个意义上说,您应该查看 strategy pattern 示例。

换句话说:您的第一个想法始终是更喜欢组合而不是继承。当您的设计将您引向此类问题时,更好的 答案可能是从您当前的设计退后一步,并找到一种方法来启用不同的排序,如某种 "service" -而不是将 list 本身一起支持排序。

What is a better approach in case of consistency?

  1. 将所有子构造函数设为私有。
  2. 引入静态工厂方法。

    ListBubbleSort.withList(List<E> list)
    ListBubbleSort.withComparator(Comparator<E> comparator)
    
  3. 调用适当的 super 构造函数。不要传递任何 nulls.

    public static <E> ListBubbleSort withList(List<E> list) {
        return new ListBubbleSort(list);
    }
    
    private ListBubbleSort(List<E>) {
        super(list);
    }
    
    protected ListSortierer(List<E>) {
        // initialise only the list field
        this.origin = list;
    }
    
  4. this.original = Optional.ofNullable(li);

  5. 如果参数超过 3 个,请考虑 the Builder Pattern

Handle missing values in the abstract superclass or in the subclass?

构造函数应该提供初始值。 您没有传递初始值,您只是 表明它们不存在

默认情况下,null 是引用类型的初始值。因此,如果尚未给出字段值,则无需重新分配字段。

Does it make a difference regarding instantiation or is it just a matter of opinion?

可读性,维护。


我会推荐阅读 Effective Java by Joshua Bloch:

创建和销毁对象

  • 项目 1:考虑静态工厂方法而不是构造函数
  • 项目 2:面对许多构造函数参数时考虑构建器

您可以添加一个 final addAll,作为可选的附加项在子构造函数中调用。对于比较器,语义不同,它(几乎)必须在可选时重载。

private final Optional<Comparator<E>> comparatorOption;

public final void addAll(List<E> li) {
    ...;
}

protected ListSortierer() {
    comparatorOption = Optional.empty();     
}

protected ListSortierer(Comparator<E> comp) {
    comparatorOption = Optional.of(comp);     
}

避免空参数似乎更好。

如果您愿意,通过拥有许多构造函数来规定所有子类 具有相同的构造函数也不错。优点是所有 类 的 API 表现相同。然而,这是不必要的样板代码,构建器模式 可以防止。在最初的旧 java 中,许多构造函数是要走的路,但现在人们在 APIs 中看到许多构造函数。

对于列表排序器,使用某种生成器可以流畅地API。

所以最后的答案:更多的是上下文风格决定。

Doing so would allow me to omit 3 of the overloaded constructors in the abstract superclass but would enforce that every subclass follows the same pattern.

子class 没有任何约束来调用父class 的特定构造函数。它可以调用其中任何一个,而这会调用其中一个。
所以你无法达到这样的要求。

关于使用静态工厂方法的解决方案,在某些情况下是可以接受的,甚至还可以,但它不是奇迹,也有一些局限性。
例如:它不允许切换到另一个实现。
对于 JDK classes,我同意这几乎从来都不是问题,但对于自定义 class 我们应该谨慎使用它。
此外,没有什么可以阻止 subclass 扩展父 class 并且不通过工厂来创建此 subclass.
的实例 因此,也不能确保引用的要求。

My question is: What is the the better approach in case of consistency?Handle missing values in the abstract superclass or in the subclass? Does it make a difference regarding instantiation or is it just a matter of opinion?

这不是意见问题。这很重要。
如果子 class 委托给父 class 并且检查总是相同的,那么让父 class 执行检查似乎很有趣。
但是,如果您不能保证父 class 永远不会修改检查,则子 class 可能会在父 class 更改其检查时创建一些不一致的实例。