java 接口需要构造函数方法的惯用方法

nue99wik  于 2023-05-15  发布在  Java
关注(0)|答案(2)|浏览(132)

我正在尝试在泛型方法中初始化泛型类型。从那以后,我了解到你不能在接口中要求构造函数,所以我想出了如何使用泛型来大致伪造相同的行为:

interface Stack<V, T>{
    public V getEmptyStack();  //V is supposed to be the subclass
    public V copy(V s);
    public void push(T e);
    public T pop();
    public boolean isEmpty();
}

class ArrayStack<T> implements Stack<ArrayStack<T>, T>{ //Have to pass subclass

    public ArrayStack<T> getEmptyStack(){ //Constructor returns type of the class.
        return new ArrayStack<T>();
    }
...

有了上面的定义,我可以定义一个泛型方法“reverse”:

public static <V, S extends Stack<S,V> > S reverse(S s){
    s = s.copy();
    S out = s.getEmptyStack(); //Trying to initialize out as simply as possible.
    while(!s.isEmpty()){
        out.push(s.pop());
    }
    return out;
}

我希望接口要求构造函数方法返回实现接口的子类型。如果正确实现了当前接口,则它会这样做;但是它有点复杂。
如果Java不能以更简单的方式做到这一点,我想知道初始化泛型类型的惯用模式。人们对getDeclaredConstructornewInstance的困惑会比我上面写的少吗?还是我写的很好?

nhn9ugyo

nhn9ugyo1#

反射是个错误的选择。一般来说,将Class<?>引用作为“类型范围操作”的位置是错误的。
听起来很诱人,不是吗?让我们讨论一下为什么它感觉是正确的答案,这样我们就知道我们需要找到什么替代方案,因为我会跟进为什么它是错误的。

为什么Class<?>/ static方法感觉不错

类本身 * 是 * 这样的东西应该存在的地方(在arraylist上有意义的操作应该存在于arraylist类型中-这是一个得到 * 非常 * 广泛同意的声明)。
在Java术语中,有两种不同的东西都充当“class-global”函数:构造函数和静态方法。在更务实的层面上,这两个结构之间实际上没有任何区别:它们都不需要接收器(x.foo()-x是接收器),并且它们都完全在类型层次结构之外操作。静态方法不能被继承。语言让它看起来像是你做的,但这是假的--它们不参与动态分派,任何对静态方法的调用都是由javac“链接”的,并且在运行时不会改变(而不是...对示例方法的任何调用,其中JVM将应用动态分派并调用最具体的覆盖)。构造函数也不能。
然而,对于调用任何特定ArrayStack没有意义的操作,但调用“数组堆栈”的概念是有意义的,我们所有的教科书都说:这应该是ArrayStack内部的静态方法。如果我们想要执行的特定操作导致创建一个新的操作,我们需要一个构造函数(这只是一个奇怪的静态方法)。
但是,正如我提到的,静态方法只是不“做”继承或类型层次结构-你不能在接口中定义静态方法(或构造函数),句号。因此,当你想要泛型和类型层次结构的力量时,你只需要去别处看看
理论上,你确实可以只传递Class<?>示例,并使用.getConstructor().newInstance()作为替代“类型层次结构参与,泛化构造函数”,甚至使用例如。.getDeclaredMethod().invoke(null, params-go-here)声明静态方法。
但是我们现在已经完全抛弃了打字系统。不可能使用类型系统来强制无参数构造函数或静态方法的存在。反射是很难处理的(例如,各种各样的异常都会出现,现在的东西都是“字符串类型的”--如果你输入了文档中规定的静态方法,编译器不会告诉你。与此相反,你必须实现一个方法的名称,因为你implements一些接口,在这种情况下,javac会立即告诉你你做错了。该工具还有助于:我可以只输入接口I implements中定义的方法的前几个字母,在eclipse中点击CMD+SPACE,eclipse立即填充所有空白(全名,public,参数类型和合理的名称,甚至是throws子句(如果相关)和@Override注解)。如果您试图使用反射来调用静态方法或构造函数,则这些都不可用。
Class<?>也是‘破碎的泛型’--它们只能深入一个层次。你可以有一个Class<String>,它的.getConstructor().newInstance()方法的类型是String。但是你不能有一个Class<List<String>>--所有字符串只有一个类对象,它不能被泛化。这是一个相当有害的负面影响,甚至尝试。

那你怎么办?

工厂!
我知道我知道这是个迷因哦,那些疯狂的java程序员和他们的BeanFactoryCreatorMachineDoohickeyFactory疯狂。
但是,我想,这只是误导。至少,不要让这阻止你使用这个概念,因为它正是你在这里想要的:它是类型系统参与的,泛型能力的类范围操作**。
这个想法很简单。让我们采用这个堆栈概念,并规定任何Stack概念的实现都必须提供一些类型范围的操作,这包括创建一个新的空操作,但也可能包括更多的操作。例如,考虑一个广义的数字系统(其中您有一些数字概念,并且您有'复数','向量数','真实的','存储为除数和除数的数字'等。你可能需要类型范围的'construct me a new instance of you,with this int value',但也需要一个'construct me a multiplicative unit value of yourself,例如。0表示真实的,0+ 01表示复数,除1对角矩阵外全零,等等。
工厂就是这样做的。您可以创建第二个类型层次结构,该层次结构表示示例(通常每个类型只有一个示例),该示例表示能够执行以下操作的对象:

interface StackFactory {
  <T> Stack<T> create();
  boolean isRandomAccess();
}

// and an example implementation:

public class ArrayStackFactory implements StackFactory {
  <T> public ArrayStack<T> create() {
    return new ArrayStack<T>();
  }

  public boolean isRandomAccess() {
    return true;
  }
}

public interface Stack<T> { ... }
public class ArrayStack<T> implements Stack<T> { ... }

这是你想要的:
给定'某种StackFactory',您可以在其上调用create()并返回Stack<T>。太好了但是,如果你知道stackfactory的具体类型,你会得到一个更具体的类型。这是可行的:

ArrayStackFactory asf = ....;
ArrayStack<String> = asf.create();

这里的一个缺点是java不允许你“编程”你的类型变量。因此,你不能有一个表示堆栈类型的typevar(比如ArrayStack),然后有一个方法将该typevar与一个元素typevar组合,从而从单独的typevar ArrayStackString生成类型ArrayStack<String>。你的反向方法不能完全完成这项工作。
这将是非常整洁的:

interface StackFactory<S extends Stack> {
  <T> S<T> create();
}

但这是一个Java限制,你不能解决,除了'铸造'到泛型(这没有做任何事情,除了使编译器停止抱怨。它实际上并不输入test),而@SuppressWarnings则是当你这样做时编译器向你抛出的警告。
编辑:根据要求,reverse将如何工作。此外,作为奖励,当您的“工厂”只需要暴露一件事(例如,接口将只有一个方法),相反,您可以只使用Supplier<X>(或Function<K, V>,如果操作需要一个参数,等等-来自java.util.function包的东西)。这实际上正是Collection.toArray()所采用的(它的签名是public <T> T[] toArray(IntFunction<T[]> arrayMaker)-其中arraymaker将请求的大小转换为该大小的数组,并且可以像String[]::new一样简单,这是一个IntFunction<String[]>):

public static <T> Stack<T> reverse(Stack<T> in) {
  Stack<T> out = in.factory().create();
  while (!in.isEmpty()) out.push(in.pop());
  return out;
}

这假设Stack接口更新为要求所有堆栈都能够返回其出厂。或者,您可以要求工厂是参数的一部分。这就是toArray的工作方式(以IntFunction<T[]>的形式传入工厂)。
我们可以“一气呵成”,可以说,完全摆脱我们的“util”类(util类有点丑,它们真的不应该存在)。Java现在有默认方法:

public interface Stack<T> {
  StackFactory factory();
  T pop();
  void push(T elem);
  boolean isEmpty();

  default Stack<T> reverse() {
    Stack<T> out = this.factory().create();
    while (!this.isEmpty()) out.push(this.pop());
    return out;
  }
}

或者,使用传入工厂:

public interface Stack<T> {
  T pop();
  void push(T elem);
  boolean isEmpty();

  default <S extends Stack<T>> S reverse(Supplier<S> supplier) {
    S out = supplier.get();
    while (!this.isEmpty()) out.push(this.pop());
    return out;
  }
}

使用最后一个:

ArrayStack<String> myStack = ...;
ArrayStack<String> reversed = someArrayStack.reverse(ArrayStack::new);

这是整洁的,因为它甚至让我们分配给一个变量的类型ArrayStack,其余的这些选项不能管理。

4uqofj5v

4uqofj5v2#

对于初学者来说,Stack不需要在类型上声明第二个泛型类型参数。

interface Stack<V, T> {
    public V getEmptyStack();

可以成为

interface Stack<T> {
    public <V> Stack<V> getEmptyStack();

这应该会简化很多事情。
有了上面的定义,我可以定义一个泛型方法“reverse”:
您可以通过选择不同的实现来实现这一点,而无需使用这种奇怪的方法。您已经在修改输入堆栈。您也可以重用该示例。

public static <T, U extends Stack<T>> U reverse(U s){
    List<T> items = new ArrayList<>();
    while (!s.isEmpty()){
        items.add(s.pop());
    }
    for (int i = items.size() - 1; i >= 0; i--) {
        s.push(items.get(i));
    }
    return s;
}

人们对getDeclaredConstructornewInstance的困惑会比我上面写的少吗?
是的,可能吧。
我希望接口要求构造函数方法返回实现接口的子类型
如果我真实的的需要这样做--我过去就有过--那么我会在界面上放一个JavaDoc来说明这个需求。

相关问题