Lesson 7: Calling Methods
Introduction
Calling methods is as easy as accessing fields, which we learned about in the previous lesson.
The proxy function names are as close to the Java names as possible. It is not always possible to have exactly the
same name because certain names clash with C++ keywords like delete
or with commonly used preprocessor
definitions like min
and max
. Java also allows the declaration of methods with the same name
as the class, even though they are not constructors. C++ does not allow that. In all these cases the proxy methods will (most likely) have an
underscore appended to their Java name (though the exact details are configurable in the JunC++ion code generator).
Passing Arguments
The most important thing to understand is that proxy type instances are passed by reference and primitive arguments
are passed by value. The use of reference type arguments allows the polymorphic passing of arguments, i.e. instances of
derived types can be passed in place of the declared type, for example a String
or a Hashtable
can be passed through an Object
reference.
C++ will also perform
any legal conversions on the passed arguments so you can for example pass a
C string literal in any place where a Java string is expected. null
can be passed in any place that
expects a non-primitive argument. The snippet below shows several Hashtable
method invocations.
Hashtable ht( _use_java_ctor ); String key = "foo"; Object value( _use_java_ctor ); // put a key/value pair in the hashtable // the key (String) is passed polymorphically through an Object reference ht.put( key, value ); // put another key/value pair in the hashtable // both key and value are created through conversion constructors ht.put( "FOO", "BAR" );
Returning Results
In Java, what you regard as an object is actually always a reference to an object.
All non-primitive method results are therefore implicitly returned by reference. When a Java method is declared
to return a java.lang.Object
it actually rarely means that
you will get back an instance of type java.lang.Object
. That declaration just establishes that you
will get back an object of a type that is assignable to java.lang.Object
.
Please see the following Java code snippet
where we use our expert knowledge about the type of the stored value even though the get()
function is
declared to return Object
:
Hashtable ht = new Hashtable();
ht.put( "key", "value" );
String result = (String)ht.get("key");
This works because the result is returned by reference, allowing a String
to be passed in place of
an Object
.
C++ proxy methods on the other hand always return results by value. This is necessary to ensure that lifecycle management works even when the caller ignores the result. It also means that the function does not have to perform a potentially very expensive search for the best proxy type to use for the returned object. We leave it to the caller to reinterpret the returned object as a differently typed object (more on that in the lesson on casting).
Return by value vs. return by reference is a very important difference. The proxy method corresponding with the above
Java method will be declared to return an instance of java::lang::Object
, and
that is exactly what it has to do: it has to return an actual instance of type java::lang::Object
.
Even if the Java method actually returns a String
, the C++ call will return a java::lang::Object
.
The polymorphism is maintained purely via the reference to the Java object.
Your returned value of type java::lang::Object
will internally maintain a reference to a Java Object that is assignable to java.lang.Object
or null
.
You should always check return values for null
before you use them in a way that expects a valid
Java object reference.
Let's take a look at the C++ code snippet that corresponds to the Java code above.
Hashtable ht( _use_java_ctor ); ht.put( "key", "value" ); String result = String::dyna_cast(ht.get("key"));
As the get()
function returns an actual java::lang::Object
we cannot directly cast it to
a java::lang::String
. But we know that the Java object inside the result is a String
,
so we can use our special dyna_cast()
function to reinterpret the result. Basically, this takes the
Java object reference owned by the returned java::lang::Object
, performs a type check on it and, if the
type is compatible to what we're trying to reinterpret to, returns a proxy instance of that type.
The key take-away from this discussion should be that you cannot simply cast results to a different type, you have to
use our special dyna_cast()
function which really creates a new C++ object that holds the same Java object
reference as the value it was given.
Exceptions
Most of the operations you can perform in Java can result in an exception or an error being thrown. We're introducing the topic of exceptions here even though it also technically applies to accessing fields and invoking constructors. We're discussing the topic in greater detail in its own lesson but we quickly want to touch on it here.
The runtime library will check all Java calls to see whether an exception was thrown on the Java side. If so, it will look for the C++ proxy exception type that matches the Java exception type most closely and throw an instance of that type into C++. This means that your exact C++ exception type depends not just on the Java exception type but also on the complete set of C++ exception types that are available.
Catching exceptions by reference allows you to catch exception types polymorphically, something that is highly advised so your code does not break if, after having written your code, you somehow gain access to a new proxy library that contains additional exception types and now the runtime will throw an exception of a different type than what you are handling.