【问题标题】:Java 8: Lambda with variable argumentsJava 8:带有可变参数的 Lambda
【发布时间】:2015-11-19 11:44:35
【问题描述】:

我正在寻找一种调用多个参数方法但使用lambda 构造的方法。在文档中说,lambda 只有在可以映射到功能接口时才可用。

我想做这样的事情:

test((arg0, arg1) -> me.call(arg0, arg1));
test((arg0, arg1, arg2) -> me.call(arg0, arg1, arg2));
...

有没有什么方法可以优雅地做到这一点,而无需定义 10 个接口,每个参数计数一个?

更新

我使用从非方法接口扩展而来的多个接口,并且重载了该方法。

两个参数的示例:

interface Invoker {}
interface Invoker2 extends Invoker { void invoke(Object arg0, Object arg1);}
void test(Invoker2 invoker, Object ... arguments) {
    test((Invoker)invoker, Object ... arguments);
}

void test(Invoker invoker, Object ... arguments) {
    //Use Reflection or whatever to access the provided invoker
}

我希望有可能用一个解决方案替换 10 个调用程序接口和 10 个重载方法。

我有一个合理的用例,请不要问诸如“你为什么要做这样的事情?”之类的问题。和“你要解决的问题是什么?”或类似的东西。只要知道我已经考虑过了,这是我正在尝试解决的合理问题。

很抱歉添加混淆调用它,但它实际上是在我当前的用例(测试构造函数合同)中调用的。

基本上,如上所述,请考虑一种方法,该方法适用于lambda 中不同数量的属性。

【问题讨论】:

  • 简短回答:不。长答案:nooooooooo。 (但是,无论如何,你会怎么处理这样的事情呢?)
  • 你所有的论点都是同一类型吗?如果是这样,您是否考虑过使用可变参数?有什么具体的例子可以说明你正在尝试做什么?
  • Louis 对不起,我为你更新了(update2)。
  • 您使用可变数量的参数但不能使用可变参数是否有任何特殊原因?
  • 公平地说,yak shaving 是一回事,我想问“你真正想做什么?”总是一个公平的问题......

标签: java lambda java-8


【解决方案1】:

Java 中你需要使用这样的数组。

test((Object[] args) -> me.call(args));

如果call 采用数组变量args,这将起作用。如果没有,您可以使用反射来进行调用。

【讨论】:

  • 我添加了一个更新来告诉你我目前的状态。 Object [] args 正是我遇到的墙,我在周围工作。现在我想尽可能减少 Java 8 允许的解决方法。
  • @MartinKersten Java 有一个静态编译器,这意味着它只能针对编译时已知的方法调用进行编译。如果直到运行时才知道类型是什么,则必须使用反射。
  • 我想使用 lambdas 变量参数。我确切地知道反射是什么,但是在您的示例中,您必须为每个冗长的调用提供一个新的 Object [] {arg0, arg1} ,我想避免这种情况。所以用 {arg0, arg1} 给出你的解决方案是我正在寻找的真正交易。反射对此无济于事,但它是解决方案的一部分:-)。
  • @MartinKersten 处理可变长度参数的方法是使用数组,因为它可以是不同的长度。
  • 这会破坏使用 lambdas 来减少冗长和样板的目的。目前我可以做类似 testNullPointer((a, b) -> new TestClass(to(a), to(b)), "a", null);以及 testNullPointer((a, b, c, d) -> new TestClass(to(a), to(b), to(c), to(d)), "a", null, 3, new Date ());这正是我想用更少的接口(目前使用重载 10 次的方法和 11 个不同的接口来支持零到 10 个参数)但使用默认方法我不再需要反射(这非常好)。跨度>
【解决方案2】:

我目前使用的最终解决方案是定义接口的层次结构(如问题中所述)并使用默认方法来避免失败。伪代码如下:

interface VarArgsRunnable {
     default void run(Object ... arguments) {
          throw new UnsupportedOperationException("not possible");
     }
     default int getNumberOfArguments() {
          throw new UnsupportedOperationException("unknown");
     }
}

以及四个参数的接口,例如:

@FunctionalInterface
interface VarArgsRunnable4 extends VarArgsRunnable {
     @Override
     default void run(Object ... arguments) {
          assert(arguments.length == 4);
          run(arguments[0], arguments[1], arguments[2], arguments[3]);
     }

     void run(Object arg0, Object arg1, Object arg2, Object arg3, Object arg4);

     @Override
     default int getNumberOfArguments() {
          return 4;
     }
}

定义了从 VarArgsRunnable0 到 VarArgsRunnable10 的 11 个接口,重载一个方法变得非常容易。

public void myMethod(VarArgsRunnable runnable, Object ... arguments) {
     runnable.run(arguments);
}

由于 Java 无法通过使用 instance.myMethod((index, value) -> doSomething(to(index), to(value)), 10, "value") 之类的东西找到 VarArgsRunnable 的正确扩展功能接口来组成 Lambda,因此需要使用正确的接口重载方法。

public void myMethod(VarArgsRunnable2 runnable, Object arg0, Object arg1) {
    myMethod((VarArgsRunnable)runnable, combine(arg0, arg1));
}

private static Object [] combine(Object ... values) {
    return values;
}

由于这需要使用 to(...) 将 Object 转换为任何适当的类型,因此可以使用泛型进行参数化以避免这种用法。

to-方法如下所示: 公共静态 T 到(对象值){ 返回(T)值; //禁止这个警告 }

这个例子很蹩脚,但我用它来调用一个具有多个参数的方法,这些参数是所有潜在组合的排列(用于测试目的),例如:

run((index, value) -> doTheTestSequence(index, value), values(10, 11, 12), values("A", "B", "C"));

所以这条小线运行了 6 次调用。所以你看到这是一个简洁的助手,能够在一行中测试多个东西,而不是定义更多或在 TestNG 中使用多种方法等等......。

PS:不需要使用反射是一件好事,因为它不会失败并且非常节省参数计数。

【讨论】:

  • 这是一个实用的解决方案,但同时它也说明了 Java 只能被扭曲来做这种事情。我想知道是否有一种优雅的方式。
【解决方案3】:

我为自己的用例所做的是定义一个辅助方法,它接受可变参数,然后调用 lambda。我的目标是 1) 能够在方法中定义一个函数以保持简洁和范围(即 lambda)和 2) 对该 lambda 的调用非常简洁。最初的发布者可能有类似的目标,因为他在上面的一个 cmets 中提到,希望避免为每个调用编写 Object[] {...} 的冗长。也许这对其他人有用。

步骤#1:定义辅助方法:

public static void accept(Consumer<Object[]> invokeMe, Object... args) {
    invokeMe.accept(args);
}

第 2 步:定义一个可以使用不同数量参数的 lambda:

Consumer<Object[]> add = args -> {
    int sum = 0;
    for (Object arg : args)
        sum += (int) arg;
    System.out.println(sum);
};

第 3 步:多次调用 lambda - 这种简洁性就是我想要语法糖的原因:

accept(add, 1);
accept(add, 1, 2);
accept(add, 1, 2, 3);
accept(add, 1, 2, 3, 4);
accept(add, 1, 2, 3, 4, 5);
accept(add, 1, 2, 3, 4, 5, 6);

【讨论】:

    【解决方案4】:

    我很好奇用于可变数量参数的 lambda 是否可以工作。确实如此。见:

    public class Example {
        public interface Action<T> {
            public void accept(T... ts);
        }
    
        public static void main(String args[]) {
            // Action<String> a = (String... x) -> { also works
            Action<String> a = (x) -> {
                for(String s : x) {
                    System.out.println(s);
                }
            };
    
            a.accept("Hello", "World");
        }
    }
    

    我知道问题已得到解答。这主要是为那些好奇并看到这篇文章的人准备的。

    【讨论】:

      【解决方案5】:

      我相信下面的代码应该可以适应你想要的:

      public class Main {
          interface Invoker {
            void invoke(Object ... args);
          }
      
          public static void main(String[] strs) {
              Invoker printer = new Invoker() {
                  public void invoke(Object ... args){
                      for (Object arg: args) {
                          System.out.println(arg);
                      }
                  }
              };
      
              printer.invoke("I", "am", "printing");
              invokeInvoker(printer, "Also", "printing");
              applyWithStillAndPrinting(printer);
              applyWithStillAndPrinting((Object ... args) -> System.out.println("Not done"));
              applyWithStillAndPrinting(printer::invoke);
          }
      
          public static void invokeInvoker(Invoker invoker, Object ... args) {
              invoker.invoke(args);
          }
      
          public static void applyWithStillAndPrinting(Invoker invoker) {
              invoker.invoke("Still", "Printing"); 
          }
      }
      

      请注意,您不必创建 lambda 并将其传递给 me.call,因为您已经拥有对该方法的引用。你可以打电话给test(me::call),就像我打电话给applyWithStillAndPrinting(printer::invoke)一样。

      【讨论】:

      • 感谢您的代码,但我正在寻找使用 lambdas。调用程序示例就是我所说的,因为它是关于调用测试方法生成测试场景的。
      • 请注意,lambda 只是一个调用站点构造,请参阅stackoverflow.com/questions/13604703/… 上面的调用者是 一个 lambda。打印机只是为了方便而存在,除非您遇到的问题是如何定义 lambda。
      • 调用者不是 lambda。 lambda 是 (arg0, arg1) -> doSomething(); 形式的东西。一个 lambda 被转换(映射)为一个函数式接口——通常被认为是用 @FunctionalInterface 注释的,但它变成了可选的,所以如果可能的话,任何接口都可以使用。因此,尝试将您的调用程序作为方法的参数并使用 (arg0, arg1) -> System.println("message " + arg0 + ", " +arg1);。对 (arg0, arg1, arg2) 执行相同操作 -> System.println("message " + arg0 + ", " + arg1 + ", " + arg2)。
      • 上面的printer 声明完全等同于Invoker printLambda = (Object ... args) -&gt; { for (Object arg: args) { System.out.println(arg); } };。用 -&gt; 定义的 Lambda 只是语法糖;两者是等价的。
      【解决方案6】:

      是的

      您不需要重载方法或多个接口。可以使用带有可变参数列表方法的单个功能接口来完成。

      但是,由于 Java 可变参数是使用隐式数组实现的,因此您的 lambda 将采用单个通用数组参数,并且必须处理从数组中解包参数。

      如果您的函数的参数并非全部属于同一个类,那么您还必须处理类型转换,这会带来所有固有的危险。

      示例

      package example;
      import java.util.Arrays;
      import java.util.List;
      
      public class Main {
          @FunctionalInterface
          public interface Invoker<T, R> {
              R invoke(T... args);
          }
      
          @SafeVarargs
          public static <T, R> void test(Invoker<T, R> invoker, T...args) {
              System.out.println("Test result: " + invoker.invoke(args));
          }
      
          public static Double divide(Integer a, Integer b) {
              return a / (double)b;
          }
      
          public static String heterogeneousFunc(Double a, Boolean b, List<String> c) {
              return a.toString() + ", " + b.hashCode() + ", " + c.size();
          }
      
          public static void main(String[] args) {
              Invoker<Integer, Double> divideInvoker = argArray -> Main.divide(
                      argArray[0], argArray[1]
              );
      
              test(divideInvoker, 22, 7);
      
              Invoker<Object, String> weirdInvoker = argArray -> heterogeneousFunc(
                      (Double) argArray[0], (Boolean) argArray[1], (List<String>) argArray[2]
              );
      
              test(weirdInvoker, 1.23456d, Boolean.TRUE, Arrays.asList("a", "b", "c", "d"));
      
              test(weirdInvoker, Boolean.FALSE, Arrays.asList(1, 2, 3), 9.999999d);
          }
      }
      
      
      

      输出:

      Test result: 3.142857142857143
      Test result: 1.23456, 1231, 4
      Exception in thread "main" java.lang.ClassCastException: class java.lang.Boolean cannot be cast to class java.lang.Double
          at example.Main.lambda$main$1(Main.java:27)
          at example.Main.test(Main.java:13)
          at example.Main.main(Main.java:32)
      

      【讨论】:

      • 我使用 Object ... arguments 版本能够以通用方式执行方法。实际的接口使用不同的类型定义,因此您可以拥有 Test3 的接口实现,允许您使用所有这些实现的类型安全版本。
      • 您说辅助方法(如我的回答)是不必要的,但在您给出的示例中,似乎 test() 是辅助方法。
      • @twm 你是对的,我应该写“重载方法”来指代 OP 的解决方案,而不是“帮助方法”指的是你的解决方案。我已编辑我的答案以更正它。
      猜你喜欢
      • 2015-05-12
      • 2020-03-21
      • 2015-07-08
      • 2014-05-19
      • 1970-01-01
      • 1970-01-01
      • 2019-05-11
      • 2021-03-10
      • 1970-01-01
      相关资源
      最近更新 更多