I am developing a framework to integrate scripting languages in java. For Ruby, I am using JRuby (last 9.4.5.0 version).
I have the following Java interfaces:
public interface ContextListener {
public void init(ScriptContext context);
}
and
public interface Script {
public int execute();
}
and
public class APIHelper {
public int getValue() {
return 10;
}
}
and
public class ScriptContext {
Map<String, Object> additionalHelpers = new HashMap<>();
public ScriptContext() {
this.additionalHelpers.put("api", new APIHelper());
}
public Object getAdditionalHelper(String name) {
return additionalHelpers.get(name);
}
}
My Ruby script is:
require 'java'
java_import 'org.scripthelper.context.ScriptContext'
java_import 'org.scripthelper.context.ScriptHelper'
java_import 'org.scripthelper.context.ContextListener'
java_import 'org.scripthelper.ruby.addhelpers.APIHelper'
class ScriptClass
java_implements 'org.scripthelper.context.ContextListener', 'org.scripthelper.ruby.addhelpers.Script'
attr_reader :context
attr_reader :api
def init(ctx)
context = ctx
obj = ctx.getHelper("api")
@api = obj.to_java(Java::org::scripthelper::ruby::addhelpers::APIHelper)
end
def execute()
a = @api.getValue()
return a + 1
end
end
The context returns Object elements. The init method is called before the execute method. I did:
When I try to evaluate the execute method, I have the following exception:
org.jruby.exceptions.NoMethodError: (NoMethodError) undefined method `getValue' for nil:NilClass
I thought I correctly made the cast to a APIHelper interface, which has the getValue() method. What did I do wrong?
For information, this older version of the script (without casting) evaluated correctly and returned a correct result:
require 'java'
java_import 'org.scripthelper.context.ScriptContext'
java_import 'org.scripthelper.context.ScriptHelper'
java_import 'org.scripthelper.context.ContextListener'
class ScriptClass
java_implements 'org.scripthelper.context.ContextListener', 'org.scripthelper.ruby.addhelpers.Script'
attr_reader :context
def init(ctx)
context = ctx
end
def execute()
a = 1
return a + 1
end
end
Does your work if you just get rid of the
.to_javacall and simply set@api = ctx.getHelper("api")?Remember that Ruby is a dynamically typed language, so Ruby variables don't have static types and you can always call any method on an object that it responds to.
In Java, variables and method arguments have static types, and the Java compiler will not let you call a method on an object unless it can tell by its static type that the method should actually be there (or unless you force it with an explicit type cast). So in Java, while you can assign an APIHelper instance to a variable of type Object (since every non-primitive type in Java is a subclass of Object), you can't actually call any APIHelper methods on that Object variable unless you first explicitly cast it back to the APIHelper type (possibly raising a ClassCastException if it's not actually an APIHelper instance).
In Ruby variables have no static type. When you try to call a method on an object, the Ruby interpreter checks at runtime if that object responds to that method and will either invoke the method or raise a NoMethodError if it doesn't. (Actually, before raising an error, it'll first try calling
method_missingon the object just in case it might be able to handle the call after all!) In Java terms, you can sort of think of this like the interpreter always automatically casting every object to its most specific possible type.Of course, in Java you can emulate this kind of dynamic method lookup and casting using reflection, and indeed that's exactly what JRuby does under the hood when you call a method on a Java object.
(It actually does a lot more than that, including translating between Ruby and Java method naming conventions and allowing you to reopen a Java class in Ruby and add Ruby methods to it or even inject entire Ruby modules into the Java class. Of course such modifications will only be visible on the Ruby side of things, since they're only modifying the Ruby wrapper for the Java class.)
Anyway, the take-home message here is that you basically never need to call
to_javain JRuby, except in one specific case: when calling a polymorphic Java method that behaves differently depending on the type of its arguments, sometimes you need to explicitly tell JRuby which Java type the argument should be cast to if the method call would otherwise be ambiguous. Usingto_javacan help there, although in those casesjava_sendis often better.(Also, arrays in JRuby have a special implementation of
to_javathat converts the elements, which can sometimes be useful.)