Search code examples
javamethodssyntaxparameter-passingvariadic-functions

How can I define a vararg with three types in Java?


Since I need a Java method to receive any number of objects of three specific classes I decided to use a vararg ... to implement it. However, I have the restriction that these three classes are:

  • java.lang.String
  • java.lang.Class
  • com.company.MyWidget

In my imaginary syntax my method could look like:

  public void myMethod(<String|Class|MyWidget>... elements)) {
    // do something
  }

I was thinking I could use a superclass or interface, but the first two classes belong to the JVM and I cannot touch them.

How can I do this in Java?


Solution

  • Variadic varargs aren't supported. What you want with the hypothetical syntax isn't in Java, at all. There is no way to write a method that accepts an arbitrary number of arguments such that the compiler marks that call off as invalid if any of the args aren't typed Class, String, or MyWidget.

    Solution 1 – 3 methods

    Make 3 methods each with args MyWidget..., Class<?>..., etc. Now you can call this method with all mywidgets, or all classes, etc, but you still can't call this method with a hybridized mix of types.

    Solution 2 – runtime checks only

    Just make one method with Object... and use instanceof to check theem all before continuing. This has the downside that I can call your method passing, say, a bunch of InputStream objects, and the compiler will let it happen; you won't know that's invalid until you actually run the code.

    Solution 3 - builder

    Something like this:

    static class MyMethodBuilder {
      List<Class<?>> classes = new ArrayList<>();
      List<MyWidget> widgets = new ArrayList<>();
      List<String> strings = new ArrayList<>();
    
      MyMethodBuilder add(Class<?> f) {
        classes.add(f);
        return this;
      }
    
      MyMethodBuilder add(MyWidget f) {
        widgets.add(f);
        return this;
      }
    
      MyMethodBuilder add(String f) {
        strings.add(f);
        return this;
      }
    
      void go() {
        // body of method here
      }
    }
    

    Called as:

    myMethod()
      .add(SomeWidget.class)
      .add(someInstanceOfWidget)
      .add("someNameOfWidget")
      .go();
    

    Note that this solution doesn't preserve order (you can't tell the difference between first adding a string and then adding a widget, vs the other way around); if you need order preserved you'll need to do more work, such as storing them all in a single List<Object> and using something like:

    for (Object v : elems) {
      if (v instanceof MyWidget mw) process(mw);
      else if (v instanceof Class<?> v) process(v.getConstructor().newInstance());
      else if (v instanceof String) processViaString(...);
      else throw new IllegalStateException("BUG: arrived here with a " + v.getClass());
    }
    

    Solution 4 - wrapper

    Make a wrapper type that accepts any of the 3, then make a method that accepts varargs amount of that wrapper:

    sealed interface MyWrapper {
      static StringWrapper of(String in) {
        return new StringWrapper(in);
      }
      static ClassWrapper of(Class<?> in) {
        return new ClassWrapper(in);
      }
      static WidgetWrapper of(MyWidget in) {
        return new WidgetWrapper(in);
      }
    }
    
    final class StringWrapper implements MyWrapper {
      final String value;
    
      StringWrapper(String value) {
        this.value = value;
      }
    }
    
    final class WidgetWrapper implements MyWrapper {
      final MyWidget value;
    
      ClassWrapper(String value) {
        this.value = value;
      }
    }
    
    // same for ClassWrapper
    
    // and then:
    
    void myMethod(MyWrapper... wrappers)
    

    which can be called as:

    myMethod(Wrapper.of("Hi"), Wrapper.of(myWidget), Wrapper.of(Foo.class));
    

    Solution 5 - take a step back

    A method that accepts either instances of MyWidget, or a class object, or a string sounds like bad design. Accepting Class<?> is almost always wrong; presumably you are going to invoke .newInstance() on that, which is bad, because now you no longer get compile checks checking that the type provided has a public no-args constructor in the first place.

    What you probably want is a varargs set of Supplier<MyWidget>. Then you can call:

    MyWidget mw = makeAWidget();
    myMethod(
      () -> mw, 
      SomeWidgetType::new,
      loadDynamic("foo.bar.fullyqualifiednameof.SomeCustomWidget"));
    

    Where the idea of creating these with a string is kinda icky, that's, obviously, not compile time checkable. loadDynamic is a method that finds that class, checks that it has a no-args constructor that is public and callable. It fails immediately if those things aren't set up properly; if all is well, it returns a supplier that calls .newInstance() each time you get() on it.

    And now your varargs method is simple. It takes any number of Supplier<MyWidget>. This is compile-time checked; SomeWidgetClass::new is a valid expression of type Supplier<MyWidget> if it has a public no-args constructor, and isn't if it isn't.