-
-
Notifications
You must be signed in to change notification settings - Fork 194
Introduction to Mixins Resolving Method Signature Conflicts
Mixins provide us with a great deal of power to manipulate existing classes, one of the most useful of which is to monkey-patch new interfaces onto existing classes as described in the first part of this series.
However, a problem arises when the interfaces we wish to monkey-patch contain method declarations which conflict with existing methods in the target class or its superclasses. Let's take a look at a simple example to see where the problems occur:
In our example application, we want to tag certain objects so we can keep track of their instances. We define a new interface called Identifyable
with a getID()
method which we will mixin to target classes that we want to identify.
public interface Identifyable {
/**
* Get the ID for this object
*/
public abstract UUID getID();
}
We choose to use Java's UUID
class as our identifier type and intend to generate a unique UUID
instance for every object we mix into.
However there's a problem, if the superclass of one of our targets already defines a method with a signature which differs on return type then the Java compiler will not let us compile our mixin. To see why, let's take a look at the structure we're trying to create. I have decorated each accessor with its return type to illustrate the problem:
The method getID()
in the parent class, and the getID()
method we are trying to define differ only on return type. This type of overload is not supported by Java and the compiler will raise an error when we try to compile our mixin:
However, there is a silver lining hidden in the engine which powers Java, the Java Virtual Machine (JVM): the JVM itself does support this kind of overload, it is only the Java language which does not. This means that if we can somehow convince the compiler to compile our code, then the actual class will work just fine.
So how do we leverage this hidden functionality in the JVM if Java won't let us access it? Simple: we use a fake method to compile the mixin, and switch it out with the actual method we want when we come to apply the mixin. The first phase of our solution looks like this:
In this example, we prefix an underscore (_
) to the method, and apply a rename operation to strip the underscore from the method name when we apply the mixin. We also remove the interface declaration from our mixin because the compiler is still smart enough to spot the conflict even when the method is defined on an interface.
So now we know the solution to compiling our conflicting method, we have two new problems:
-
How does the mixin processor know what methods need to have a prefix stripped from them, and what the prefix is?
-
How do we go about implementing the interface on the mixin if doing so will immediately cause a conflict with the target?
Fortunately both of these new problems can be quite easily solved!
To resolve these problems, we will introduce a new concept, the idea of soft implementing an interface.
With our soft implementation, we will define both a prefix to be used for methods in the interface, which solves the first problem, as well as a way of declaring an interface implementation without actually using the implements
keyword, which solves the second.
In terms of the end result, soft implementation provides exactly the same capabilities as having a mixin directly implement an interface, in other words:
- Soft-implemented interfaces are still monkey-patched onto the target class in the same way that regular interfaces are
- Prefixed methods are still mixed in (including overwrite semantics - see the next section) to the target class and will simply have any prefix stripped by the mixin processor.
NOTE It is also possible to mix "hard" and "soft" implementation clauses on the same mixin.
As you might expect, soft implementations are declared using annotations. Let's take a look at how the example above would translate into Java code.
@Mixin(Bar.class)
@Implements(@Interface(iface = Indentifyable.class, prefix = "ident$"))
public abstract class MixinBar extends Foo {
private final UUID id = UUID.randomUUID();
public UUID ident$getID() {
return this.id;
}
}
Notice that in the @Implements
clause we can specify one or more @Interface
annotations to describe the interfaces we want to implement. The prefix you choose is entirely up to you, however I recommend that the following guidelines be used when choosing prefixes:
-
End the prefix with a
$
sign.$
is commonly used as a separator in synthetic and structural parts of java classes and helps visually separate the prefix from the method name being prefixed. For examplefoo$getID()
is more easily parsable as two parts thanfoogetID()
. If you choose not to use$
, then I recommend using underscore (_
) as a reasonable alternative. -
Use a short string which evokes understanding of the interface to which the prefix refers, this makes it easier to relate prefixed methods to their soft-implemented interface. For example, the choice of
ident$
tips a hat to theIndentifyable
interface,id$
orifbl$
might also work, however using longer names is allowed, for example it's perfectly legal to useidentifyable$
, but leads to code which is awkward to read. Likewise using overly short or unrelated names likefoo$
ora$
, whilst legal, is discouraged.
Note that methods which soft-implement an interface do not have to use the prefix, in fact only methods which conflict as shown above actually need to use the prefix. However using the prefix is beneficial because it allows the mixin processor to do some additional verification at application time. Prefixed methods are checked for membership in the declared interface making it a detectable error if a method is later removed or changed in the interface.
So signature conflicts can occur when implenting an interface where a conflicting method exists in a superclass, but signature conflicts can also occur with methods in target class. You might ask yourself, "but how? surely a conflict can't occur with the target class because the compiler doesn't know about methods in the target class when compiling the mixin?"
The answer, of course, is shadow methods.
You may recall from the first part of this series that we can tell the mixin processor about methods and fields in the target class by "shadowing" them, this of course becomes a problem when a shadow method we want to add has a signature conflict with an interface method we are adding.
Let's modify our example above and remove Bar
and assume we're mixing directly into Foo
:
We can see fairly quickly that we can't add the shadow for getID()
in the target class because of a signature conflict.
Fortunately, the mixin processor gives us two options for dealing with this scenario:
- Firstly, we could use a soft implementation, just like above we can change the interface implemenation to soft to work around the compiler limitation
However, this can be inconvenient when implementing a large interface where only a single (or at least, a small number of) shadow method(s) are the source of the conflict.
- Alternatively, Mixin allows us to prefix the shadow method.
This is extremely useful when a small number of shadows are causing the problem, we can simply rename the shadow itself in order to avoid the conflict.
Unlike soft-implements, there is no separate place to define a prefix for a shadow method. Instead, the prefix can be defined directly in the shadow annotation, like so:
public abstract class MixinFoo implements Identifyable {
@Shadow(prefix = "conflict$")
public abstract int conflict$getID();
public UUID getID() {
// return the unique ID
}
}
We defined the prefix conflict$
for the shadow, in order to avoid the signature conflict. Mixin also provides a default value for prefix
which can be used without being explicitly specified:
@Shadow
public abstract int shadow$getID();
Using the prefix shadow$
allows the renamed shadow to be specified without needing to explicitly define the prefix. However, in order to aid readability it is recommended that prefix
is always explicity defined in the annotation, even if the default is being used.
Prefixes and soft implementations also play an important part in Intrinsic Proxy Methods, which we will discuss in the next section.