如何将字符串转换为lambda表达式?

38

我思考了一下,想到了一个有趣的问题,假设我们有一个含有以下内容的配置(输入)文件:

x -> x + 1
x -> x * 2
x -> x * x
x -> -x

而且我们还有一个 Integer 列表:

List<Integer> list = new ArrayList<>();
list.addAll(Arrays.toList(1, 2, 3, 4, 5));

有没有一种方法可以将字符串(如x -> x + 1等)转换为表示lambda表达式的对象?这些对象可以被用作:

Object lambda = getLambdaFromString("x -> x + 1");
if (lambda.getClass().equals(IntFunction.class) {
    list.stream().forEach()
        .mapToInt(x -> x)
        .map(x -> ((IntFunction)lambda).applyAsInt(x))
        .forEach(System.out::println);
}

我应该如何编写名为getLambdaFromString的方法?

  • 有没有JDK/JRE中可以重用的内容?
  • 还需要自己编写吗?
  • 是否可能将Object lambda缩小到仅捕获lambda?

你可以创建一个带有一个公共静态方法的类,该方法返回lambda并即时编译它。或者对于这样简单的表达式,使用脚本引擎在箭头后评估表达式。 - assylias
8
Lambda表达式会被解释为一个单方法接口的实现,该接口取决于词法上下文。因此,您不能指望重用任何功能(来自JDK或其他地方)将字符串从上下文中解析出相应的实例。 - Marko Topolnik
3
您的问题是如何将一个表达式字符串解析为可执行的代码。您只是将“可执行的代码”这个术语替换为“lambda表达式”。 - Holger
2个回答

35

Marko对这个问题的评论是正确的。你无法从文件中读取裸的Java lambda表达式,因为没有提供上下文的目标类型定义这样的表达式是无法被定义的。例如,考虑以下方法声明:

void method1(BiFunction<String,String,String> f) { ... }
void method2(BiFunction<Integer,Integer,Integer> f) { ... }

接下来是以下代码:

method1((x, y) -> x + y);
method2((x, y) -> x + y);

这两个lambda表达式 (x, y) -> x + y 表示完全不同的含义。对于method1,+运算符表示字符串连接,但对于method2,则表示整数相加。

这有点偏离您的问题,但您可以使用动态语言读取和评估lambda或function表达式。在Java 8中,有Nashorn JavaScript引擎。因此,您可以通过从Java调用Nashorn来读取和评估JavaScript函数,而不是尝试读取和评估Java lambda表达式。

以下代码接受arg [0]中的函数并将其应用于每个后续函数,打印结果:

import java.util.function.Function;
import javax.script.*;

public class ScriptFunction {
    public static void main(String[] args) throws Exception {
        ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
        @SuppressWarnings("unchecked")
        Function<Object,Object> f = (Function<Object,Object>)engine.eval(
            String.format("new java.util.function.Function(%s)", args[0]));
        for (int i = 1; i < args.length; i++) {
            System.out.println(f.apply(args[i]));
        }
    }
}

例如,运行以下命令:

java ScriptFunction 'function(x) 3 * x + 1' 17 23 47

给出结果

52.0
70.0
142.0

将函数字符串包装在 new java.util.function.Function 内是为了在 Nashorn 的 JavaScript 函数观念和 Java 的 Function 接口之间创建一个适配器。(可能有更好的方法,但我不知道。) 将 eval 的返回值强制转换为 Function<Object,Object> 导致了未经检查的强制转换警告,这是不可避免的,因为这是 JavaScript(一种动态类型的语言)和静态类型的 Java 之间的边界。最后,没有进行错误检查。如果违反某些假设,比如第一个参数实际上不表示 JavaScript 函数,则我确定这将以各种恶劣的方式引起问题。

尽管如此,如果您需要评估从文件中读取的表达式或函数,您可能会发现这种技术很有用。


这看起来非常有趣,我应该更深入地探索Nashorn引擎!我有一种感觉,在运行时使用Nashorn引擎会比在运行时动态创建Java文件更容易。 - skiwi
1
非常好,斯图尔特。你知道使用Nashorn引擎是否也可以解析筛选谓词吗?比如.filter(u -> u.key.equals("A"))? - biliboc
刚刚意识到我只需要使用 Predicate 而不是 Function。谢谢。 - biliboc

20
我认为在大多数情况下,使用Stuart's answer中提到的Nashorn JavaScript引擎是最好的选择。然而,如果出于某些原因希望留在Java世界中,我最近创建了LambdaFromString库,在运行时将字符串代码转换为lambda。
使用该库执行问题中指定的操作的代码如下:
    List<Integer> list = new ArrayList<>();
    list.addAll(Arrays.asList(1, 2, 3, 4, 5));

    LambdaFactory lambdaFactory = LambdaFactory.get();
    Function<Integer, Integer> lambda = lambdaFactory
            .createLambda("x -> x + 1", new TypeReference<Function<Integer, Integer>>() {});
    list.stream().map(lambda).forEach(System.out::println); //prints 2 to 6

唯一不同的是,必须知道并将lambda类型传递给库,以便编译器知道在此上下文中"+"的含义。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接