JLatenter is a small library which allows you to "interpret" any object of any type as an object that implements an interface you specify, allowing you to call any method on that object without having to cast it to any generic interface or use something like Reflection API or Method Handles API. This can be very helpful when using design patterns like "Observer" or "Chain of Responsibility". To understand how it can be useful, take a look at this example:
import java.util.*;
import java.util.function.*;
import static com.github.justhm228.jlatenter.latent.Latent.*;
public abstract class Handleable {
protected final Set<Object> handlers = new HashSet<>();
public void addHandler(Object handler) {
if (handler != null) {
handlers.add(handler); // Register a handler if it is non-`null`
}
}
public void removeHandler(Object handler) {
if (handler != null) {
handlers.remove(handler); // Unregister a handler if it is non-`null`
}
}
public void update(Object... args) {
// Fire all registered handlers:
for (final Object handler : handlers) {
try {
as(handler, BiConsumer.class).accept(this, args); // "Interpret" it as `BiConsumer`!
} catch (RuntimeException ignored) {
}
}
}
}
This example presents a simple abstract class that implements
the
"Observer"
design pattern. The class can register, unregister and fire any BiConsumer
-like handler.
But here, the registered handlers aren't required to implement BiConsumer
in order to be handlers -
just have an accept()
method that takes 2 arguments, the first of which is a reference to
a Handleable
object, and the last of which is an array of Object
arguments.
This allows you to get rid of the generic interface, which in some cases can be very useful.
And something else important... I don't know how original this thing is, I just came up with this implementation of this idea and decided to do it because why not. The project is open to any ideas that anyone can submit in the "Issues" tab!
There's a little background here. I studied the Java programming language for almost 2 years, mostly using the Internet. In addition to just studying the theory, I simultaneously wrote my programs, encountered problems and found answers to them on StackOverflow or, in the end, in various "no-name" articles. Over the course of my time, I have visited many sites, many of which you may not have even heard of. But besides this, I also read books. One of them was Bruce Eckel's book "Thinking in Java", 4th Edition (an excellent book, by the way, I recommend reading it - it will be at least interesting). I first saw the idea of implementing "duck typing" in Java there. But I wasn't interested in it. Later, when I had already studied Java to a level acceptable to me personally, I began to write various software for myself, which I might later post on GitHub, and, in fact, I started GitHub itself - at a minimum, it definitely won't be superfluous. But then, I simply ran out of ideas, and my study began. And then, during a regular walk, the idea of combining dynamic proxies and "duck typing" from Python came to my mind. This idea could greatly simplify calling methods on objects with a lost type. This is how this implementation was born. I also wanted to make it as user-friendly and take up the least amount of code space as possible. So small that the method call can look like in Python. But in the end, it looks like this:
import com.github.justhm228.jlatenter.latent.*;
import static com.github.justhm228.jlatenter.latent.Latent.*;
public class Main {
// Note: `LatentException` is unchecked, so you don't need to handle it always.
// It's just specified to let you know that it exists.
public static void main(String[] args) throws LatentException {
final Object instance = new Task(); // <-- Losing the type...
as(instance, Runnable.class).run(); // <-- "Interpreting" the object as a `Runnable`!
}
}
class Task { // <-- It doesn't implement `Runnable`!
public void run() {
System.out.println("Hello World!");
}
}
This, of course, isn't particularly similar to Python, but at least it's very short and clear, right?
Well, I think you already guessed how it works. When you pass any object to the as()
method,
it instantiates a new dynamic proxy that implements the specified interface, which is passed
the desired proxy handler. When calling any proxied method, the handler tries to redirect
the call to the same method, but already belonging to the proxied object. First, it gets the class of
that object and tries to find a method in it with the same name and arguments. If it finds one
(even if it is a static method) then it checks the return value for "compatibility".
Return value compatibility in a given context means that any condition in the following list is true:
- The return value of the called method is
void
. - The return value of the called method isn't
void
and the assignment compatibility check of the return values (wrapped in their wrapper types, if at least one of them is primitive) of the called and proxied methods passes successfully.
If none of the above conditions are met, an IncompatibleLatentException
exception is thrown
(which is a subclass of unchecked LatentException
). If at least one of the above conditions is met,
then the next step is to check whether the proxied method is accessible. If a method is inaccessible,
the handler tries its best to make it accessible. If it fails, an InaccessibleLatentException
is thrown (which is also a subclass of the unchecked LatentException
). If successful, the handler
attempts to call the proxied method, passing it a reference to the object being proxied and
the passed arguments. If there is an exception in class initialization (ExceptionInInitializerError
),
it is wrapped in LatentInitException
(which is also a subclass of unchecked LatentException
).
When an exception is thrown by the method (InvocationTargetException
), it's also wrapped in
LatentTargetException
(which is also a subclass of unchecked LatentException
). If the method
invokes successfully, the handler returns the value returned by the method. If the proxied method
wasn't found, the handler tries to find the same method in the superclasses and call it.
If no one was found, it throws the final LatentNotPresentException
(again a subclass of
unchecked LatentException
...). If the one is found and its declaring class is specified as Object
,
the handler's built-in implementation of Object
methods comes into force. If this is a method of
another class, it's called according to the above algorithm.
So that's how in this relatively simple way, any object can be "interpreted" as an object of a completely different type and all method calls to it are redirected to the proxied object, in case you were interested in this.
There is nothing complicated here, because... the only class you will have to work with is Latent
.
The class contains 3 overloaded methods:
as()
isShadowed()
find()
.
as()
takes as the first argument the object whose method you want to call,
and the second argument in the two overloaded versions is different:
- In the first version it's a reference to the interface as which you want to represent the object
In the second version it's an object that implements a single interface, as which an object can be represented (it's a failed experiment, please forget about its existence).
but return value is always the same - a "shadow" of the passed object.
The isShadowed()
method accepts a single object and returns true
if the object is the "shadow" of another object, and false
if not.
The find()
method takes as a required parameter a "shadow" object,
and as an optional parameter the class to which you want to
cast the return value using generics.
The method itself simply returns a reference to an object whose "shadow" is
the argument passed to it.
Methods of the "shadow" object don't throw any exception, except for subclasses of
LatentException
, so all checked exceptions can be suppressed in the catch
block
and all other exceptions can be ignored.
A LatentException
is thrown if the called method isn't found/implemented,
its signature is incompatible with the original method, the method is inaccessible,
an exception has occurred in the initializer, or the method threw an exception.
Also, some built-in stub interfaces have been prepared for you that you can use instead of
creating a new class with your method (there will be more of them in the future):
Startable
- contains astart()
method which accepts no parameters and returnsvoid
Stoppable
- contains astop()
method which accepts no parameters and returnsvoid
Pausable
- contains apause()
method which accepts no parameters and returnsvoid
Formattable
- a parent for all format/print stubs-
SelfPrintable
- contains aprint()
method which accepts no parameters and returnsvoid
-
XPrintable
- contains aprint()
method which acceptsObject
as a parameter and returnsvoid
-
SelfPrintableLN
- contains aprintln()
method which accepts no parameters and returnsvoid
-
XPrintableLN
- contains aprintln()
method which acceptsObject
as a parameter and returnsvoid
-
SelfFormattable
- contains aformat()
method which acceptsObject...
as a parameter and returnsvoid
-
SelfFormatted
- contains aformatted()
method which acceptsObject...
as a parameter and returnsString
-
SelfPrintableF
- contains aprintf()
method which acceptsObject...
as a parameter and returnsvoid
-
XFormattable
- contains aformat()
method which acceptsObject
andObject...
as parameters and returnsvoid
-
XPrintableF
- contains aprintf()
method which acceptsObject
andObject...
as parameters and returnsvoid
@Shadow
- just an annotation which marks all interface stubs (it's optional)Buildable
- a parent for all build stubs-
XBuildable
- contains abuild()
method which accepts no parameters and returnsvoid
-
SelfBuildable
- contains abuild()
method which accepts no parameters and returnsObject
(this
reference)
-
GenericBuildable
- contains abuild()
method which accepts no parameters and returns genericT
(this
reference)
-
SelfBuildableC
- contains abuild()
method which accepts no parameters and returnsObject
(another object, notthis
)
-
GenericBuildableC
- contains abuild()
method which accepts no parameters and returns genericT
(another object, notthis
)
Renderable
- contains arender()
method which accepts no parameters and returnsvoid
Drawable
- contains adraw()
method which accepts no parameters and returnsvoid
Paintable
- contains apaint()
method which accepts no parameters and returnsvoid
Steppable
- contains astep()
method which accepts no parameters and returnsvoid
Gettable
- contains aget()
method which accepts no parameters and returnsObject
Puttable
- contains aput()
method which accepts no parameters and returnsvoid
And that's all. It's very short, isn't it? Also, if you take into account that you can use
static imports to call the as()
method (like in the examples), it turns out that you can
call the method you need in almost one line (possibly even without creating a new interface).
But also note that the garbage collector won't be able to destroy the original object while
its "shadow" exists, because it references original directly, so take that into account
(I won't fix this due to other problems which may be caused by such fixes).
I hope I explained clearly.
I really wanted to finish at 0.1-build.1
initially, but then I realized that I could
turn this spontaneous idea into something more and add a little more functionality.
But first, I need to add more stub interfaces. So much more that a potential user
won't have to write them by yourself - it will be enough to look through the list of built-ins.
This is a very simple task in terms of implementation and time, but however, I alone
don't have enough imagination, so any ideas are welcome. Also, I think I will need to
improve the implementation, optimize it, add better parameter checking (take into account varargs,
wrappers, etc.) and possibly get rid of IncompatibleLatentException
caused by absence of
default null
values. And finally... I want to add some meaning to @Shadow
's annotation,
because right now it's just decoration. If anything, I'm not talking about rewriting the code
so that it only accepts stubs with this annotation. Still, it isn't for nothing that I decided to
use
dynamic proxies
to solve the problem of call redirection. I can add some additional, optional parameters to
the annotation, which, for example, will redirect a call to the manually-specified method,
change the search algorithm, or even control the built-in profiler (yes, I know, it's corny,
but why not). The possibilities in this regard are endless! That's why I decided not to complete
this idea that spontaneously came to me with 0.1-build.1
- I want to understand how I can "play" with
this implementation, how to expand it, what I can basically do with it. So I promise that I will
at least try to do something similar.
- Come up with an idea.
- Implement the idea.
- Add more stub interfaces.
- Improve/optimize the current implementation.
-
- Refactor the code.
-
- Migrate from Reflection API to Method Handles API.
- Improve parameter checking system (for better redirecting).
-
- Take into account varargs
(to allow such redirections:
example() -> example(Object...) // <-- 0 elements
).
- Take into account varargs
(to allow such redirections:
-
- Take into account wrapper types
(to allow such redirections:
example(int) -> example(Integer) // <-- if there's no other methods
orexample(Integer) -> example(int) // <-- if there's no other methods
).
- Take into account wrapper types
(to allow such redirections:
-
- And etc.
- Possibly get rid of
IncompatibleLatentException
caused byvoid
(just returnnull
-like values in cases of conflict - it will take very much space) - Do something with
@Shadow
- Publish to Maven
- Abandon the project?..
Well, something like this.