有没有办法规避 lambda 表达式的类型化?

Posted

技术标签:

【中文标题】有没有办法规避 lambda 表达式的类型化?【英文标题】:Is there any way to circumvent the typedness of lambda expressions? 【发布时间】:2016-07-20 02:38:53 【问题描述】:

这是我在Java中引入lambda之后一直想知道的一个问题,并受到a related question的启发,我想我可能会在这里提出来,看看是否有任何想法。

(旁注:有一个similar question for C#,但我没有找到适用于 Java 的。Java 中关于“将 lambda 存储在变量中”的问题总是提到 type 变量是固定的 - 这正是我试图规避的)


Lambda 表达式通过目标类型推断接收它们需要的类型。这一切都由编译器处理。比如函数

static void useF(Function<Integer, Boolean> f)  ... 
static void useP(Predicate<Integer> p)  ... 

都可以用相同的 lambda 表达式调用:

useF(x -> true);
useP(x -> true);

表达式将一次表现为实现Function&lt;Integer,Boolean&gt;接口的类,一次表现为实现Predicate&lt;Integer&gt;接口的类。

但不幸的是,没有办法将 lambda 表达式存储为适用于这两个函数的类型,例如 in

GenericLambdaTypelambda = x -&gt; true;

这种“通用 lambda 类型”必须编码 可以由给定 lambda 表达式实现的方法的类型。所以在这种情况下,它会是

(Ljava.lang.Integer)Ljava.lang.Booleanlambda = x -&gt; true;

(基于standard type signatures,用于说明)。 (这并非完全不合理:C++ lambda expressions 基本上就是这样做的……)


那么有没有任何方法来防止 lambda 表达式被解析为一种特定类型?

特别是,是否有任何技巧或解决方法允许使用相同的对象调用上面概述的 useFuseP 方法,如

useF(theObject);
useP(theObject);

这不太可能,所以我假设答案很明显是:“否”,但是:是否有 any 方法来编写 generic,魔术适应方法,例如

useF(convertToRequiredTargetType(theObject));
useP(convertToRequiredTargetType(theObject));

?


请注意,这个问题更多是出于好奇。所以我真的在寻找 any 方法来实现这一点(自定义预编译器或字节码操作除外)。

似乎没有简单的解决方法。通过将表达式包装到通用辅助方法中来推迟类型推断的天真尝试,如

static <T> T provide()

    return x -> true;

当然失败了,声明“这个表达式的目标类型必须是一个函数式接口”(这里根本无法推断类型)。但我也考虑了其他选项,例如MethodHandles、残酷的未经检查的演员表或讨厌的反射黑客。一切似乎在编译后立即丢失,其中 lambda 隐藏在匿名类的匿名对象中,其唯一方法通过 InvokeVirtual 调用...

【问题讨论】:

一个CallSite?基本上,这就是 lambda 您是否正在寻找一种将适用的功能接口转换为等效的所需功能接口的方法,例如Function&lt;Integer, Boolean&gt;Predicate&lt;Integer&gt;?这样你就可以将一个 lambda 存储为一种功能接口类型的变量,但你可以将它用作另一种功能接口类型? @fge 我不确定 - 从查看文档来看,我不知道如何在这里应用它。 @rgettman 基本上是的。我知道可以将它包装到另一个 lambda 中,例如 useP(x -&gt; f.apply(x));,但这通常不起作用。 @JornVernee 引用问题:“所以我真的在寻找任何方法来实现这一点(自定义预编译器或字节码操作除外)。” @Marco13 抱歉,看了大约一个小时后,我陷入了沉思:/ 【参考方案1】:

我看不到任何方法可以将解析为一种特定功能接口类型的 lambda 表达式直接解释为等效的功能接口类型。两个函数式接口都没有扩展或可以扩展的超接口或“通用 lambda 类型”,即强制它只接受一个特定类型的参数并返回一个特定类型。

但是您可以编写一个实用程序类,其中包含将一种功能接口类型转换为另一种类型的方法。

此实用程序类将谓词转换为返回布尔值的函数,反之亦然。它包括身份转换,因此您不必担心是否调用转换方法。

public class Convert

    static <T> Predicate<T> toPred(Function<? super T, Boolean> func)
    
        return func::apply;
    

    static <T> Predicate<T> toPred(Predicate<? super T> pred)
    
        return pred::test;
    

    static <T> Function<T, Boolean> toFunc(Predicate<? super T> pred)
    
        return pred::test;
    

    static <T> Function<T, Boolean> toFunc(Function<? super T, Boolean> func)
    
        return func::apply;
    

输入函数和谓词是T 的消费者,因此PECS 指示? super。您还可以添加其他可能采用BooleanSuppliers、Supplier&lt;Boolean&gt; 或任何其他返回booleanBoolean 的功能接口类型的重载。

此测试代码编译。它允许您传递功能接口类型的变量并将其转换为所需的功能接口类型。如果您已经有了确切的功能接口类型,则不必调用转换方法,但如果需要,您可以。

public class Main

    public static void main(String[] args)
    
        Function<Integer, Boolean> func = x -> true;
        useF(func);
        useF(Convert.toFunc(func));
        useP(Convert.toPred(func));

        Predicate<Integer> pred = x -> true;
        useP(pred);
        useP(Convert.toPred(pred));
        useF(Convert.toFunc(pred));
    

    static void useF(Function<Integer, Boolean> f) 
        System.out.println("function: " + f.apply(1));
    
    static void useP(Predicate<Integer> p) 
        System.out.println("predicate: " + p.test(1));
    

输出是:

function: true
function: true
predicate: true
predicate: true
predicate: true
function: true

【讨论】:

是的,这基本上就是我在this comment中提到的。可以通过重新包装来转换 lambda,但没有神奇的 Convert.toTargetType 方法。这里的问题是n*n 转换方法是n 类型所必需的,尽管每种类型都已经满足要求作为其他类型适用...【参考方案2】:

假设在一个闪亮的未来(比如 Java 10 或 11)我们有真正的函数类型,允许指定一个函数而不强制它是特定的传统 Java 类型(并且是某种值而不是对象等) .)。那么我们仍然存在现有方法的问题

static void useF(Function<Integer, Boolean> f)  ... 
static void useP(Predicate<Integer> p)  ... 

期望一个 Java 对象实现传统的 Java interface 并表现得像 Java 对象一样,即不会突然改变 theObject instanceof FunctiontheObject instanceof Predicate 的结果。这意味着它不会是通用函数在传递给这些方法时突然开始实现所需的接口,而是应用某种捕获转换,产生一个实现所需目标接口的对象,就像今天一样,当你将 lambda 表达式传递给这些方法之一,或者当您使用 p::testPredicate 转换为 Function 时(反之亦然,使用 f::apply)。

所以不会发生的情况是您将同一个对象传递给两个方法。您只有一个隐式转换,它将在编译时确定,并且很可能在字节码中显式化,就像今天的 lambda 表达式一样。


convertToRequiredTargetType 这样的泛型方法不能工作,因为它不知道目标类型。使这种事情起作用的唯一解决方案是您排除的那些,预编译器和字节码操作。您可以创建一个接受附加参数的方法,即描述 require interfaceClass 对象,它委托给 LambdaMetaFactory,但该方法必须重做编译器所做的所有事情,确定功能签名、名称实现方法等

没有任何好处,因为调用像convertToRequiredTargetType(theObject)(或实际上是convertToRequiredTargetType(theObject, Function.class))这样的实用方法绝不比例如简单。 theObject::test)。您创建这样一个方法的愿望导致了您奇怪的陈述“编译后似乎一切都立即丢失,其中 lambda 隐藏在匿名类的匿名对象中”,而实际上,您有一个对象实现具有已知功能的接口签名,因此可以像function::methodName 一样简单地转换(如果您忘记了,IDE 可以为您完成方法名称)...

【讨论】:

当然,还有一些技术问题。从高级的“以语言为中心”的角度来看(与以 VM 为中心的观点相反),人们可能想知道为什么 (x instanceof Function)Predicate) 不可能为 true >同一个对象。所以在这方面,我不确定这种隐式转换是否真的是必要的。但可以肯定的是,更进一步,人们可能会问 x.getClass().getMethods() 应该返回什么......(待定) 关于“魔法转换方法”的问题是出于好奇,这是否技术上可能。例如,关于我草拟的provide 方法,可以(天真!!!)询问是否不可能“将类型推断拉入方法中” - 基本上,确定所需的T 调用这个方法的地方确实是一个函数式接口。 (我不能给出一个明确的理由,除了“类型推断非常复杂,而且在某些情况下这是不可能的”)。 当然,让一个对象实现两个接口是没有问题的,即使在今天你也可以通过 lambda 表达式创建这样的东西,想想default 方法和交集类型。该选择仍然在编译时做出,但可以推迟到创建时。但关键是,您不能将其推迟到需要特定类型的时间。请记住,Java 允许动态加载类。在创建 lambda 对象时,可能存在尚未加载的兼容功能接口。 如前所述,由于魔术转换功能的应用,x instanceof Function 的结果不能突然改变为相同的x。我从来没有说过x instanceof Functionx instanceof Predicate 不能同时是true。这是可能的,而且很容易实现。 我不知道你的魔法convertToRequiredTargetType 的目的是什么。有一个技术方案可以做到你所描述的,只是它不适用于方法调用,而是更简单的方法引用表达式。为什么有人要更改 Java 语言只是为了允许以更复杂的方式使用现有功能?

以上是关于有没有办法规避 lambda 表达式的类型化?的主要内容,如果未能解决你的问题,请参考以下文章

有没有办法指定一个“空”的 C# lambda 表达式?

有没有办法在 lambda 表达式树中使用“动态”?

Lambda 表达式练习

Java 8 lambda表达式

C++ lambda表达式(函数指针和function)

有没有办法比较 lambdas?