0

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

With the above defined, I can define a generic method "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;
}

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?

Polymer
  • 1,043
  • 11
  • 13
  • 5
    `getEmptyStack()` looks a lot like a factory method to me, and doesn't prevent me from making actual constructors for that class. You might consider factory methods if you haven't. But also I don't see a perfect "Java does exactly what you want it to" way to do this. What you have now is already too complicated imo, and any maintenance programmer would complain about it being difficult. – markspace May 14 '23 at 22:10
  • 2
    [*What are static factory methods?*](https://stackoverflow.com/q/929021/642706). And https://en.m.wikipedia.org/wiki/Factory_method_pattern – Basil Bourque May 14 '23 at 22:54
  • aside from using factory methods as a means to initiate instances - why would you try to force a force constructors in the first place? If you implement a subclass then you would need to worry about how to instantiate it, and there typically is no need to force a certain way, different implementation might have different requirements for their constructions. And if you want to have a standardized way for dynamic switches without custom code to bind each implementation instantiation to the switch then a separate factory would be what you want typically. – Frank Hopkins May 14 '23 at 23:09
  • I reviewed the Factory method. Are folks suggesting I implement `Stack` as an abstract class and reverse as one of its methods? And then the creation logic goes into child classes of `Stack` (so an `ArrayStack`, and `LinkedStack` would be child classes)? How would I then get reverse to then return a definite instance of the child classes? – Polymer May 14 '23 at 23:28

2 Answers2

2

Reflection is just the wrong bet. In general treating a Class<?> 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<?> / 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<?> 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<?> is also 'broken generics' - they can only go one level deep. You can have a Class<String>, and its .getConstructor().newInstance() method will have type String. But you simply cannot have a Class<List<String>> - 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 {
  <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> { ... }

This does some of what you want:

Given 'some sort of StackFactory', you can just call create() on it and get a Stack<T> 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<String> = 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<String> 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<S extends Stack> {
  <T> S<T> 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<X> (or Function<K, V> 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 <T> T[] toArray(IntFunction<T[]> 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<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;
}

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<T[]>).

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

Or even, using a pass-in factory:

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

to use that last one:

ArrayStack<String> myStack = ...;
ArrayStack<String> 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.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • Either the interface def of `Stack` is updated with a `StackFactory getFactory()` method, or the `reverse` method needs to be passed a stackfactory. The question asked 'how is this done in the java world'. This is how it is done. See also e.g. `list.toArray()` which takes as canonical parameter, a factory (except, because it's just creating new instances, it's a `Supplier` instead of a `ListFactory`). Same for `Collectors.toMap` and friends. – rzwitserloot May 15 '23 at 00:11
  • @rzwitserloot your numerical example speaks to me, and is how I typically think about this stuff (An empty stack being the stack analog of "zero"). I believe I understand what you're spelling out, but could you expand your answer to include how you would implement stack and reverse? It would make the answer more complete I think. – Polymer May 15 '23 at 00:29
  • 1
    @Polymer I've edited it and added a few snippets showing off some more techniques. – rzwitserloot May 15 '23 at 01:42
  • When `reverse` is an instance method, all this factory guff is just pointless indirection. You've just arrived at OP's solution but worse: a Stack still needs to know how to create instances of itself, just now via a factory, which, again, is backwards. `reverse()` may as well be a non-default method with a javadoc stating the requirement to keep the type the same. That's not a compile-time guarantee, but neither is yours (since ArrayStack.factory could return FooStackFactory, in theory). – Michael May 15 '23 at 08:09
  • @Michael no - `reverse` is `default` - any impl of a Stack _does not need to know_. For `reverse()` to exist as a thing that is written only once and no impl of `Stack` needs to write it, you need that factory concept. For example, you can just call `list.sort(comparator)` in java. Nevertheless, `java.util.ArrayList` _does not_ have an implementation of `sort`. – rzwitserloot May 15 '23 at 13:11
  • "*For reverse() to exist as a thing that is written only once and no impl of Stack needs to write it, you need that factory concept*" And now every implementation needs `StackFactory factory();` instead. Congratulations, you've gone from every class needing to implement 1 method to every class needing to implement 1 method *and* 1 factory per class. Like I said, it's OP's solution but worse. – Michael May 15 '23 at 15:42
  • @rzwitserloot I marked this as correct because it carefully explained why and how to avoid static constructs. And gave a defense and technique (default) for adding utility methods to the interface. I can see how to survive without generics between classes, and that this is typical. This has been very helpful. Is there a reason why I need seperate classes (factories) rather than just non static creation methods in my interface? Do the factories enforce a "static quality"? I can see how your solution matches the design pattern from the book. – Polymer May 15 '23 at 15:57
  • It's so hard to ask a good simple question. I'm getting the sense you tried to answer this question by noting my constructor methods "should be static" like some of the comments mentioned. But then emphasized statics don't fit in the type hierarchy. But type factories structurally do, are a standard technique, and therefore should be preferred. This question asked about this as well for example: https://stackoverflow.com/questions/69849/factory-pattern-when-to-use-factory-methods. But a popular answer used statics, which you argue is bad. I'll think about this. – Polymer May 15 '23 at 18:01
  • Okay creation methods have to be static to make an object before you have it - that makes sense. And they're basically simple "smart constructors" so the name they were given was "static factory method." So implicit in yours and others answers was "those should be static factory methods" and you followed up in your answer with "prefer the factory pattern to static factory methods". Which has been discussed before. I think I'm understanding the ideas here. – Polymer May 15 '23 at 18:37
  • @Michael If you dislike having the `factory()` method, the second snippet shows a different tactic (similar to what e.g. collection's `toArray`, and Collectors' `toMap` has chosen. – rzwitserloot May 15 '23 at 21:39
  • @Polymer shoving non-static creating methods in the interface is conflating things a tad; non-static means it's designed to require the context of an instance of the thing, but the task at hand (make me a new empty `ArrayStack`) is an operation that doesn't require that context. Conflating `X` and `XFactory` is not a good idea for this reason - for example, by doing that, you can't actually call `newStack()` without already having a stack lying about, as you need one to call `newStack()` on. – rzwitserloot May 15 '23 at 21:41
  • 1
    Yes, I think you have the basic gist: [1] "These things should be constructors because they construct new instances"... [2] "Oh wait constructors can't have names. Constructors and static methods are very similar, except static methods CAN have names, so let's use those!" [3] "Oh wait, static methods (nor constructors) can't be accessed with interfaces, and I need interfaces here" [4] ... okay, factories. Either explicitly (`class ArrayStackFactory { .. }`) or pass-on-demand (`Supplier` as parameter). That's how the thinking goes :) – rzwitserloot May 15 '23 at 21:43
1

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

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

can become

interface Stack<T> {
    public <V> Stack<V> 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 <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;
}

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.

Michael
  • 41,989
  • 11
  • 82
  • 128
  • If I do this then reverse outputs a `Stack` not an `ArrayStack`, right? – Polymer May 14 '23 at 23:29
  • It doesn't have to. I changed it. But also if your method is just mutating a collection then I'd personally leave it void. Same as, e.g. [`Collections::sort`](https://docs.oracle.com/javase/8/docs/api/java/util/Collections.html#sort-java.util.List-). I only left the return value to keep it more similar to your original. – Michael May 14 '23 at 23:32
  • Thank you, this is very helpful. I realize I wasn't clear enough in the original question, and maybe the question is even ill formed for the stack example. I wanted to leave the input untouched, I realize now that with this example that was silly and wrong. But that was why I was trying to initialize a fresh variable. – Polymer May 14 '23 at 23:48