This is an automated email from the ASF dual-hosted git repository. paulk pushed a commit to branch GROOVY_2_5_X in repository https://gitbox.apache.org/repos/asf/groovy.git
commit e4e716f3de6200bba48937b0fb0fee7b41897d53 Author: Paul King <pa...@asert.com.au> AuthorDate: Mon May 18 19:42:45 2020 +1000 initial cut of doco for dynamic method selection --- src/spec/doc/core-object-orientation.adoc | 108 ++++++++++++++++++++- src/spec/test/objectorientation/MethodsTest.groovy | 92 ++++++++++++++++++ 2 files changed, 199 insertions(+), 1 deletion(-) diff --git a/src/spec/doc/core-object-orientation.adoc b/src/spec/doc/core-object-orientation.adoc index 0ff7be4..8961ae6 100644 --- a/src/spec/doc/core-object-orientation.adoc +++ b/src/spec/doc/core-object-orientation.adoc @@ -432,7 +432,113 @@ include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=va ==== Method selection algorithm -(TBD) +Dynamic Groovy supports https://en.wikipedia.org/wiki/Multiple_dispatch[multiple dispatch] (aka multimethods). +When calling a method, the actual method invoked is determined +dynamically based on the run-time type of methods arguments. +First the method name and number of arguments will be considered (including allowance for varargs), +and then the type of each argument. +Consider the following method definitions: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=multi_methods,indent=0] +---- + +Perhaps as expected, calling `method` with `String` and `Integer` parameters, +invokes our third method definition. + +[source,groovy] +---- +include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=call_single_method,indent=0] +---- + +Of more interest here is when the types are not known at compile time. +Perhaps the arguments are declared to be of type `Object` (a list of such objects in our case). +Java would determine that the `method(Object, Object)` variant would be selected in all +cases (unless casts were used) but as can be seen in the following example, Groovy uses the runtime type +and will invoke each of our methods once (and normally, no casting is needed): + +[source,groovy] +---- +include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=call_multi_methods,indent=0] +---- + +For each of the first two of our three method invocations an exact match of argument types was found. +For the third invocation, an exact match of `method(Integer, Integer)` wasn't found but `method(Object, Object)` +is still valid and will be selected. + +Method selection then is about finding the _closest fit_ from valid method candidates which have compatible +parameter types. +So, `method(Object, Object)` is also valid for the first two invocations but is not as close a match +as the variants where types exactly match. +To determine the closest fit, the runtime has a notion of the _distance_ an actual argument +type is away from the declared parameter type and tries to minimise the total distance across all parameters. + +The following table illustrates some factors which affect the distance calculation. + +[cols="1,1" options="header"] +|==== +| Aspect +| Example + +| Directly implemented interfaces match more closely than ones from further up the inheritance hierarchy. +a| Given these interface and method definitions: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=multi_method_distance_interfaces,indent=0] +---- + +The directly implemented interface will match: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=multi_method_distance_interfaces_usage,indent=0] +---- + +| An Object array is preferred over an Object. +a| +[source,groovy] +---- +include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=object_array_over_object,indent=0] +---- + +| Non-vararg variants are favored over vararg variants. +a| +[source,groovy] +---- +include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=non_varargs_over_vararg,indent=0] +---- + +| If two vararg variants are applicable, the one which uses the minimum number of vararg arguments is preferred. +a| +[source,groovy] +---- +include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=minimal_varargs,indent=0] +---- + +| Interfaces are preferred over super classes. +a| +[source,groovy] +---- +include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=multi_method_distance_interface_over_super,indent=0] +---- +|==== + +In the case where two variants have exactly the same distance, this is deemed ambiguous and will cause a runtime exception: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=multi_method_ambiguous,indent=0] +---- + +Casting can be used to select the desired method: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=multi_method_ambiguous_cast,indent=0] +---- + ==== Exception declaration diff --git a/src/spec/test/objectorientation/MethodsTest.groovy b/src/spec/test/objectorientation/MethodsTest.groovy index e89d263..0223e76 100644 --- a/src/spec/test/objectorientation/MethodsTest.groovy +++ b/src/spec/test/objectorientation/MethodsTest.groovy @@ -160,4 +160,96 @@ class MethodsTest extends GroovyTestCase { ''' } + void testMultiMethods() { + assertScript ''' + // tag::multi_methods[] + def method(Object o1, Object o2) { 'o/o' } + def method(Integer i, String s) { 'i/s' } + def method(String s, Integer i) { 's/i' } + // end::multi_methods[] + + // tag::call_single_method[] + assert method('foo', 42) == 's/i' + // end::call_single_method[] + + // tag::call_multi_methods[] + List<List<Object>> pairs = [['foo', 1], [2, 'bar'], [3, 4]] + assert pairs.collect { a, b -> method(a, b) } == ['s/i', 'i/s', 'o/o'] + // end::call_multi_methods[] + ''' + + assertScript ''' + import static groovy.test.GroovyAssert.shouldFail + // tag::multi_method_ambiguous[] + def method(Date d, Object o) { 'd/o' } + def method(Object o, String s) { 'o/s' } + + def ex = shouldFail { + println method(new Date(), 'baz') + } + assert ex.message.contains('Ambiguous method overloading') + // end::multi_method_ambiguous[] + // tag::multi_method_ambiguous_cast[] + assert method(new Date(), (Object)'baz') == 'd/o' + assert method((Object)new Date(), 'baz') == 'o/s' + // end::multi_method_ambiguous_cast[] + ''' + + assertScript ''' + // tag::multi_method_distance_interfaces[] + interface I1 {} + interface I2 extends I1 {} + interface I3 {} + class Clazz implements I3, I2 {} + + def method(I1 i1) { 'I1' } + def method(I3 i3) { 'I3' } + // end::multi_method_distance_interfaces[] + + // tag::multi_method_distance_interfaces_usage[] + assert method(new Clazz()) == 'I3' + // end::multi_method_distance_interfaces_usage[] + ''' + + assertScript ''' + // tag::non_varargs_over_vararg[] + def method(String s, Object... vargs) { 'vararg' } + def method(String s) { 'non-vararg' } + + assert method('foo') == 'non-vararg' + // end::non_varargs_over_vararg[] + ''' + + assertScript ''' + // tag::minimal_varargs[] + def method(String s, Object... vargs) { 'two vargs' } + def method(String s, Integer i, Object... vargs) { 'one varg' } + + assert method('foo', 35, new Date()) == 'one varg' + // end::minimal_varargs[] + ''' + + assertScript ''' + // tag::object_array_over_object[] + def method(Object[] arg) { 'array' } + def method(Object arg) { 'object' } + + assert method([] as Object[]) == 'array' + // end::object_array_over_object[] + ''' + + assertScript ''' + // tag::multi_method_distance_interface_over_super[] + interface I {} + class Base {} + class Child extends Base implements I {} + + def method(Base b) { 'superclass' } + def method(I i) { 'interface' } + + assert method(new Child()) == 'interface' + // end::multi_method_distance_interface_over_super[] + ''' + } + }