Java 8:带有可变参数的 Lambda

Posted

技术标签:

【中文标题】Java 8:带有可变参数的 Lambda【英文标题】:Java 8: Lambda with variable arguments 【发布时间】: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 是一回事,我想问“你真正想做什么?”总是一个公平的问题...... 【参考方案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 只是一个调用站点构造,请参阅***.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 的解决方案,而不是“帮助方法”指的是你的解决方案。我已编辑我的答案以更正它。

以上是关于Java 8:带有可变参数的 Lambda的主要内容,如果未能解决你的问题,请参考以下文章

如何创建可变参数泛型 lambda?

如何编写一个丢弃其参数的通用可变参数 lambda?

如何使用 SWIG 包装带有可变参数的 C 函数

c ++ lambdas如何从上层范围捕获可变参数包

java 可变参数

如何创建一个可变的通用lambda?