Idiomatic way to have constructor methods required by interfaces

huangapple go评论45阅读模式
英文:

Idiomatic way to have constructor methods required by interfaces

问题

I'm trying to initialize generic types in generic methods. I've since learned you can't require constructors in interfaces, so I figured out how to use generics to roughly fake the same behavior:

我正在尝试在泛型方法中初始化泛型类型。后来我了解到在接口中无法要求构造函数,所以我想出了如何使用泛型来粗略模仿相同行为的方法:

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&lt;T&gt; getEmptyStack(){ //Constructor returns type of the class.
    return new ArrayStack&lt;T&gt;();
}

...


With the above defined, I can define a generic method &quot;reverse&quot;:

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


public static &lt;V, S extends Stack&lt;S,V&gt; &gt; 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;
}

I want the interface to require constructor methods which return the subtype implementing the interface. The current interface does this if it's implemented correctly; however it is a bit complicated.

If Java can't do this in a simpler way, I'd like to know the idiomatic patterns for initializing generic types. Would folks generally be less confused by getDeclaredConstructor and newInstance than what I wrote above? Or is what I wrote above fine?

英文:

I'm trying to initialize generic types in generic methods. I've since learned you can't require constructors in interfaces, so I figured out how to use generics to roughly fake the same behavior:

interface Stack&lt;V, T&gt;{
    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&lt;T&gt; implements Stack&lt;ArrayStack&lt;T&gt;, T&gt;{ //Have to pass subclass

    public ArrayStack&lt;T&gt; getEmptyStack(){ //Constructor returns type of the class.
        return new ArrayStack&lt;T&gt;();
    }
...

With the above defined, I can define a generic method "reverse":

public static &lt;V, S extends Stack&lt;S,V&gt; &gt; 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;
}

I want the interface to require constructor methods which return the subtype implementing the interface. The current interface does this if it's implemented correctly; however it is a bit complicated.

If Java can't do this in a simpler way, I'd like to know the idiomatic patterns for initializing generic types. Would folks generally be less confused by getDeclaredConstructor and newInstance than what I wrote above? Or is what I wrote above fine?

答案1

得分: 2

Reflection is just the wrong bet. In general treating a Class&lt;?&gt; reference as a place for 'type-wide operations' is wrong.

这只是错误的选择。总的来说,将 Class&lt;?&gt; 引用视为进行'类型范围操作'的地方是错误的。

It sounds enticing, though, doesn't it? Let's cover why it feels like the right answer, so that we know what alternative we need to find, because I'll follow this up why it's wrong.

尽管听起来很吸引人,但为什么它似乎是正确的答案呢?让我们来看看为什么它看起来是正确的答案,这样我们就知道需要找到什么替代方案,因为我将会解释为什么它是错误的。

Why Class&lt;?&gt; / static methods feels right

为什么 Class&lt;?&gt; / 静态方法感觉是正确的

The class itself is the place where such things should live (operations that make sense on an arraylist should live in the arraylist type - that's a statement that would get very broad agreement).

类本身确实是这些操作应该存在的地方(对数组列表上有意义的操作应该存在于数组列表类型中 - 这是一个会得到广泛认同的说法)。

In java terminology, there are 2 different things that both serve as 'class-global' functions: Constructors, and static methods. At a more pragmatic level there is effectively zero difference between those two constructs: They both do not require a receiver (x.foo() - x is the receiver), and they both completely operate outside the type hierarchy. static methods cannot be inherited. The language makes it look like you do, but this is fake - they don't engage in dynamic dispatch, any call to a static method is 'linked' by javac and isn't going to change at runtime (vs. any call to an instance method, where the JVM will apply dynamic dispatch and call the most specific override). Constructors also cannot be.

在Java术语中,有两种不同的东西都作为'类全局'函数:构造函数和静态方法。从更实际的角度来看,这两种构造之间实际上没有任何区别:它们都不需要接收者(x.foo() - x 是接收者),它们都__完全在类型层次结构之外操作__。静态方法不能被继承。语言使它看起来像是可以继承的,但这是假的 - 它们不参与动态分派,对静态方法的任何调用都是由javac链接的,不会在运行时更改(与对实例方法的任何调用不同,在这种情况下JVM会应用动态分派并调用最具体的重写)。构造函数也不能被继承。

Nevertheless, for an operation that doesn't make sense to call on any specific ArrayStack, but does make sense to call on the concept of 'array stacks', all our textbooks say: That should be a static method inside ArrayStack. And if the specific operation we want to perform results in the creation of a new one, we'd want a constructor (which is just a weird static method).

尽管如此,对于在任何特定的 ArrayStack 上调用没有意义的操作,但对于调用'数组栈'概念上有意义的操作,我们所有的教材都说:那应该是在 ArrayStack 内部的静态方法。如果我们要执行的具体操作导致创建新的操作,我们会需要一个构造函数(它只是一个奇怪的静态方法)。

But, as I mentioned, static methods just do not 'do' inheritance or a type hierarchy - you cannot define static methods (or constructors) in an interface, period. So when you want the power of generics and type hierarchy, you just have to look elsewhere.

但是,正如我提到的,静态方法根本不支持继承或类型层次结构 - 你不能在接口中定义静态方法(或构造函数),没有例外。所以当你想要泛型和类型层次结构的功能时,你__必须寻找其他地方__。

In theory you can indeed just pass Class&lt;?&gt; instances around and use .getConstructor().newInstance() as ersatz 'type hierarchy-engaging, generified constructors' and even use e.g. .getDeclaredMethod().invoke(null, params-go-here) to declare static methods.

从理论上讲,你确实可以传递 Class&lt;?&gt; 实例,并使用 .getConstructor().newInstance() 作为伪造的'类型层次结构参与,通用的构造函数',甚至可以使用 .getDeclaredMethod().invoke(null, params-go-here) 声明静态方法。

But we've now left the typing system behind completely. It is not possible to use the type system to enforce a no-args constructor, or the existence of a static method. And reflection is very hard to work with (all sorts of exceptions that fall out, for example, and things are now 'stringly typed' - if you typo that static method the docs decree you must have, the compiler won't tell you. In constrast to fat-fingering the name of a method you must implement because you implements some interface, in which case javac will tell you immediately you did it wrong. The tooling also helps: I can just type the fist few letters of a method defined in an interface I implements, hit CMD+SPACE in eclipse, and eclipse instantly fills in all the blanks (the full name, public, the argument types and sensible names, even a throws clause if relevant and an @Override annotation). None of that is available if you attempt to use reflection to call into static methods or constructors.

但是,我们现在已经完全离开了类型系统。不可能使用类型系统来强制执行无参数构造函数,或静态方法的存在。并且使用反射非常难以处理(例如,会出现各种异常,而且现在事情都是'字符串类型' - 如果你拼错了那个必须要有的

英文:

Reflection is just the wrong bet. In general treating a Class&lt;?&gt; reference as a place for 'type-wide operations' is wrong.

It sounds enticing, though, doesn't it? Let's cover why it feels like the right answer, so that we know what alternative we need to find, because I'll follow this up why it's wrong.

Why Class&lt;?&gt; / static methods feels right

The class itself is the place where such things should live (operations that make sense on arraylist should live in the arraylist type - that's a statement that would get very broad agreement).

In java terminology, there are 2 different things that both serve as 'class-global' functions: Constructors, and static methods. At a more pragmatic level there is effectively zero difference between those two constructs: They both do not require a receiver (x.foo() - x is the receiver), and they both completely operate outside the type hierarchy. static methods cannot be inherited. The language makes it look like you do, but this is fake - they don't engage in dynamic dispatch, any call to a static method is 'linked' by javac and isn't going to change at runtime (vs. any call to an instance method, where the JVM will apply dynamic dispatch and call the most specific override). Constructors also cannot be.

Nevertheless, for an operation that doesn't make sense to call on any specific ArrayStack, but does make sense to call on the concept of 'array stacks', all our textbooks say: That should be a static method inside ArrayStack. And if the specific operation we want to perform results in the creation of a new one, we'd want a constructor (which is just a weird static method).

But, as I mentioned, static methods just do not 'do' inheritance or a type hierarchy - you cannot define static methods (or constructors) in an interface, period. So when you want the power of generics and type hierarchy, you just have to look elsewhere.

In theory you can indeed just pass Class&lt;?&gt; instances around and use .getConstructor().newInstance() as ersatz 'type hierarchy-engaging, generified constructors' and even use e.g. .getDeclaredMethod().invoke(null, params-go-here) to declare static methods.

But we've now left the typing system behind completely. It is not possible to use the type system to enforce a no-args constructor, or the existence of a static method. And reflection is very hard to work with (all sorts of exceptions that fall out, for example, and things are now 'stringly typed' - if you typo that static method the docs decree you must have, the compiler won't tell you. In constrast to fat-fingering the name of a method you must implement because you implements some interface, in which case javac will tell you immediately you did it wrong. The tooling also helps: I can just type the fist few letters of a method defined in an interface I implements, hit CMD+SPACE in eclipse, and eclipse instantly fills in all the blanks (the full name, public, the argument types and sensible names, even a throws clause if relevant and an @Override annotation). None of that is available if you attempt to use reflection to call into static methods or constructors.

Class&lt;?&gt; is also 'broken generics' - they can only go one level deep. You can have a Class&lt;String&gt;, and its .getConstructor().newInstance() method will have type String. But you simply cannot have a Class&lt;List&lt;String&gt;&gt; - there is only one class object for all strings, it cannot be generified. That's quite a deleterious downside of even trying.

So what do you do?

FACTORIES!

I know, I know. It's a meme. Oh, those crazy java programmers and their BeanFactoryCreatorMachineDoohickeyFactory craziness.

But, that's just, I guess, misleading. At least, don't let that deter you from using the concept, because it is precisely what you want here: It's type-system-engaged, generics-capable class-wide operations.

The idea is simple. Let's take this stack notion and decree that any implementation of the concept Stack must provide some type-wide operations, which includes making a new empty one, but may also include more operations. For example, consider a generalized number system (where you have some Number concept, and you have 'complex number', 'vector number', 'real number', 'number stored as divisor and dividend', and so on. You may want type-wide 'construct me a new instance of you, with this int value' but also a 'construct me a multiplicative unit value of yourself, e.g. 0 for real numbers, 0+0i for complex, an all-zeroes-except-for-a-diagonal-with-ones matrix, and so on' operation.

Factory is how you do that. You create a second type hierarchy that represents instances (generally, only one instance per type) that represents that object that is capable of doing these operations:

interface StackFactory {
  &lt;T&gt; Stack&lt;T&gt; create();
  boolean isRandomAccess();
}

// and an example implementation:

public class ArrayStackFactory implements StackFactory {
  &lt;T&gt; public ArrayStack&lt;T&gt; create() {
    return new ArrayStack&lt;T&gt;();
  }

  public boolean isRandomAccess() {
    return true;
  }
}

public interface Stack&lt;T&gt; { ... }
public class ArrayStack&lt;T&gt; implements Stack&lt;T&gt; { ... }

This does some of what you want:

Given 'some sort of StackFactory', you can just call create() on it and get a Stack&lt;T&gt; back. That's perfect. However, if you know the specific type of the stackfactory you get a more specific type back. This works:

ArrayStackFactory asf = ....;
ArrayStack&lt;String&gt; = asf.create();

One downside is here is that java doesn't let you 'program' your type vars. You therefore can't have a typevar representing the stack type (say, ArrayStack), and then have a method that combines that typevar with an element typevar and thus produce the type ArrayStack&lt;String&gt; from separate typevars ArrayStack and String. e.g. your reverse method can't quite get the job done.

This would have been very neat:

interface StackFactory&lt;S extends Stack&gt; {
  &lt;T&gt; S&lt;T&gt; create();
}

But that's a java limitation that you can't work around, other than by 'casting' to generics (which does nothing, other than make the compiler stop complaining. It doesn't actually type test), and @SuppressWarnings the warning the compiler throws at you when you do that.


EDIT: Upon request, how reverse would work. Also, as a bonus, when your 'factory' only needs to expose one thing (e.g. the interface would have just the one method), instead you can just use Supplier&lt;X&gt; (or Function&lt;K, V&gt; if the operation takes an argument, and so on - something from the java.util.function package). This is in fact exactly what Collection.toArray() takes (its signature is public &lt;T&gt; T[] toArray(IntFunction&lt;T[]&gt; arrayMaker) - where arraymaker turns a requested size into an array of that size, and can be as simple as String[]::new which is an IntFunction&lt;String[]&gt;):

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

This assumes the Stack interface is updated to require that all stacks are capable of returning their factory. Alternatively, you can instead require that the factory is part of the argument. This is how toArray works (you pass the factory in, in the form of an IntFunction&lt;T[]&gt;).

We can do that 'in one go', so to speak, and get rid of our 'util' class entirely (util classes are kinda ugly, they really shouldn't exist). Java has default methods now:

public interface Stack&lt;T&gt; {
  StackFactory factory();
  T pop();
  void push(T elem);
  boolean isEmpty();

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

Or even, using a pass-in factory:

public interface Stack&lt;T&gt; {
  T pop();
  void push(T elem);
  boolean isEmpty();

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

to use that last one:

ArrayStack&lt;String&gt; myStack = ...;
ArrayStack&lt;String&gt; reversed = someArrayStack.reverse(ArrayStack::new);

Which is neat in that it even lets us assign to a variable of type ArrayStack which the rest of these options can't manage.

答案2

得分: 1

以下是翻译好的部分:

起始时,Stack 类型不需要在类型上声明第二个泛型参数。

接口 Stack 可以改成:

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;
}

相比我上面写的,使用 "getDeclaredConstructor" 和 "newInstance" 是否会让人们更少困惑?

是的,可能会。

我希望接口要求构造方法返回实现接口的子类型:

如果我真的需要这个 - 过去我确实需要过 - 那么我会在接口上放一个 JavaDoc,说明这个要求。

英文:

For starters, Stack doesn't need a 2nd generic type parameter declared on the type.

interface Stack&lt;V, T&gt; {
    public V getEmptyStack();

can become

interface Stack&lt;T&gt; {
    public &lt;V&gt; Stack&lt;V&gt; getEmptyStack();

That should simplify things quite a lot.

> With the above defined, I can define a generic method "reverse":

You can do that without that weird method by choosing a different implementation. You're already modifying the input stack. You may as well reuse that instance.

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

> Would folks generally be less confused by getDeclaredConstructor and
> newInstance than what I wrote above?

Yes, probably.

> I want the interface to require constructor methods which return the
> subtype implementing the interface

If I had a real need for this - and I have in the past - then I'd just put a JavaDoc on the interface stating that requirement.

huangapple
  • 本文由 发表于 2023年5月15日 05:59:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/76249853.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定