Java Mapping for Operations
On this page:
Basic Java Mapping for Operations
As we saw in the mapping for interfaces, for each operation on an interface, the proxy interface contains a corresponding method with the same name. To invoke an operation, you call it via the proxy. For example, here is part of the definitions for our file system:
module Filesystem { interface Node { idempotent string name(); } // ... }
The name
operation returns a value of type string
. Given a proxy to an object of type Node
, the client can invoke the operation as follows:
NodePrx node = ...; // Initialize proxy String name = node.name(); // Get name via RPC
Normal and idempotent
Operations in Java
You can add an idempotent
qualifier to a Slice operation. As far as the signature for the corresponding proxy method is concerned, idempotent
has no effect. For example, consider the following interface:
interface Example { string op1(); idempotent string op2(); }
The proxy interface for this is:
public interface ExamplePrx extends ObjectPrx { String op1(); String op2(); }
Because idempotent
affects an aspect of operation invocation, not interface, it makes sense for the two methods to be mapped the same.
Passing Parameters in Java
In Parameters in Java
The parameter passing rules for the Java mapping are very simple: each in
parameter of a Slice operation is mapped to a corresponding parameter in the mapped Java method.
Here is an interface with operations that pass parameters of various types from client to server:
struct NumberAndString { int x; string str; } sequence<string> StringSeq; dictionary<long, StringSeq> StringTable; interface ClientToServer { void op1(int i, float f, bool b, string s); void op2(NumberAndString ns, StringSeq ss, StringTable st); void op3(ClientToServer* proxy); }
The Slice compiler generates the following proxy for these definitions:
public interface ClientToServerPrx extends ObjectPrx { void op1(int i, float f, boolean b, String s); void op2(NumberAndString ns, String[] ss, java.util.Map<Long, String[]> st); void op3(ClientToServerPrx proxy); }
Given a proxy to a ClientToServer
interface, the client code can pass parameters as in the following example:
ClientToServerPrx p = ...; // Get proxy... p.op1(42, 3.14f, true, "Hello world!"); // Pass simple literals int i = 42; float f = 3.14f; boolean b = true; String s = "Hello world!"; p.op1(i, f, b, s); // Pass simple variables NumberAndString ns = new NumberAndString(); ns.x = 42; ns.str = "The Answer"; String[] ss = { "Hello world!" }; java.util.Map<Long, String[]> st = new java.util.HashMap<Long, String[]>(); st.put(0, ns); p.op2(ns, ss, st); // Pass complex variables p.op3(p); // Pass proxy
Out Parameters in Java
The mapping for an operation depends on how many values it returns, including out parameters and a non-void
return value:
- Zero values
The corresponding Java method returnsvoid
. For the purposes of this discussion, we're not interested in these operations.
One value
The corresponding Java method returns the mapped type, regardless of whether the Slice definition of the operation declared it as a return value or as an out parameter. Consider this example:Sliceinterface I { string op1(); void op2(out string name); }
The mapping generates corresponding methods with identical signatures:
Javainterface IPrx extends ObjectPrx { String op1(); String op2(); }
- Multiple values
The Slice compiler generates an extra nested class to hold the results of an operation that returns multiple values. The class is nested in the mapped interface (not the proxy interface) and has the nameOpResult
, whereOp
represents the name of the operation. The leading character of the class name for a "result class" is always capitalized. The values of out parameters are provided in corresponding public fields of the same names. If the operation declares a return value, its value is provided in the field namedreturnValue
. If an out parameter is also namedreturnValue
, the field to hold the operation's return value is named_returnValue
instead. The result class defines an empty constructor as well as a "one-shot" constructor that accepts and assigns a value for each of its fields. The corresponding Java method returns the result class type.
Here are the same Slice definitions we saw earlier, but this time with all parameters being passed in the out
direction, along with one additional operation to better demonstrate the mapping:
struct NumberAndString { int x; string str; } sequence<string> StringSeq; dictionary<long, StringSeq> StringTable; interface ServerToClient { void op1(out int i, out float f, out bool b, out string s); void op2(out NumberAndString ns, out StringSeq ss, out StringTable st); void op3(out ServerToClient* proxy); StringSeq op4(out string returnValue); }
The Slice compiler generates the following code for these definitions:
public interface ServerToClientPrx extends ObjectPrx { ServerToClient.Op1Result op1(); ServerToClient.Op2Result op2(); ServerToClientPrx op3(); ServerToClient.Op4Result op4(); } public interface ServerToClient extends ... { public static class Op1Result { public Op1Result() {} public Op1Result(int i, float f, boolean b, String s) { this.i = i; this.f = f; this.b = b; this.s = s; } public int i; public float f; public boolean b; public String s; } public static class Op2Result { public Op2Result() {} public Op2Result(NumberAndString ns, String[] ss, java.util.Map<java.lang.Long, String[]> st) { this.ns = ns; this.ss = ss; this.st = st; } public NumberAndString ns; public String[] ss; public java.util.Map<java.lang.Long, String[]> st; } public static class Op4Result { public Op4Result() {} public Op4Result(String[] _returnValue, String returnValue) { this._returnValue = _returnValue; this.returnValue = returnValue; } public String[] _returnValue; public String returnValue; } }
We need to point out several things here:
- Result classes are generated for
op1
,op2
andop4
because they return multiple values - The result classes are generated as nested classes of interface
ServerToClient
, and notServerToClientPrx
op4
declares an out parameter namedreturnValue
, thereforeOp4Result
declares a field named_returnValue
to hold the operation's return value- No result class is necessary for
op3
because it only returns one value; the mapped Java method declares a return type ofServerToClientPrx
even though the Slice operation declared it as an out parameter
Null Parameters in Java
Some Slice types naturally have "empty" or "not there" semantics. Specifically, sequences, dictionaries, and strings all can be null
, but the corresponding Slice types do not have the concept of a null value. To make life with these types easier, whenever you pass null
as a parameter or return value of type sequence, dictionary, or string, the Ice run time automatically sends an empty sequence, dictionary, or string to the receiver.
This behavior is useful as a convenience feature: especially for deeply-nested data types, fields that are sequences, dictionaries, or strings automatically arrive as an empty value at the receiving end. This saves you having to explicitly initialize, for example, every string element in a large sequence before sending the sequence in order to avoid NullPointerException
. Note that using null parameters in this way does not create null semantics for Slice sequences, dictionaries, or strings. As far as the object model is concerned, these do not exist (only empty sequences, dictionaries, and strings do). For example, whether you send a string as null
or as an empty string makes no difference to the receiver: either way, the receiver sees an empty string.
Optional Parameters in Java
The mapping uses standard Java types to encapsulate optional parameters:
java.util.OptionalDouble
The mapped type for an optionaldouble
.
java.util.OptionalInt
The mapped type for an optionalint
.
java.util.OptionalLong
The mapped type for an optionallong
.
java.util.Optional<T>
The mapped type for all other Slice types.
Optional return values and output parameters are mapped to instances of the above classes, depending on their types. For operations with optional in parameters, the proxy provides a set of overloaded methods that accept them as optional values, and another set of methods that accept them as required values. Consider the following operation:
optional(1) int execute(optional(2) string params);
The mapping for this operation is shown below:
java.util.OptionalInt execute(String params); java.util.OptionalInt execute(java.util.Optional<String> params);
For cases where you are passing values for all of the optional in parameters, it is more efficient to use the required mapping and avoid creating temporary optional values.
A client can invoke execute
as shown below:
java.util.OptionalInt i; i = proxy.execute("--file log.txt"); // required mapping i = proxy.execute(java.util.Optional.of("--file log.txt")); // optional mapping i = proxy.execute(java.util.Optional.empty()); // params is unset if(i.isPresent()) { System.out.println("value = " + i.get()); }
Passing null
where an optional value is expected is equivalent to passing an instance whose value is unset.
Java's optional classes do not consider null
to be a legal value. Consider this example:
class Data { ... } interface Repository { void addOptional(optional(1) Data d); void addRequired(Data d); }
The Ice encoding allows null
values for class instances, so you can pass null
to addRequired
and the server will receive it as null
. However, there's no way to pass an "optional value set to null" in the Java mapping. Passing null
to addOptional
is equivalent to passing the value of java.util.Optional.ofNullable((T)null)
, which is equivalent to passing the value of java.util.Optional.empty()
. In either case, the server will receive it as an optional whose value is not present.
A well-behaved program must not assume that an optional parameter always has a value. Calling get
on an optional instance for which no value is set raises java.util.NoSuchElementException
.
Exception Handling in Java
Any operation invocation may throw a run-time exception and, if the operation has an exception specification, may also throw user exceptions. Suppose we have the following simple interface:
exception Tantrum { string reason; } interface Child { void askToCleanUp() throws Tantrum; }
Slice exceptions are thrown as Java exceptions, so you can simply enclose one or more operation invocations in a try
-catch
block:
ChildPrx child = ...; // Get child proxy... try { child.askToCleanUp(); } catch(Tantrum t) { System.out.write("The child says: "); System.out.writeln(t.reason); }
Typically, you will catch only a few exceptions of specific interest around an operation invocation; other exceptions, such as unexpected run-time errors, will typically be handled by exception handlers higher in the hierarchy. For example:
public class Client { public static void main(String[] args) { try { ChildPrx child = ...; // Get child proxy... try { child.askToCleanUp(); child.praise(); // Give positive feedback... } catch(Tantrum t) { System.out.print("The child says: "); System.out.println(t.reason); child.scold(); // Recover from error... } } catch(com.zeroc.Ice.LocalException e) { e.printStackTrace(); } } }