Fwd: Wrappers feature proposal
Brian Goetz
brian.goetz at oracle.com
Wed Jan 13 15:55:31 UTC 2021
The follows language feature proposal was received on the
amber-spec-experts list.
Comments from the Legislative Analyst:
As always, language features should be evaluated through the lens of
"why" (what problem do they solve) before dwelling on the pros and cons
of a specific solution.
The underlying problem that this proposal attempts to solve is that
writing a class C that implements interface I, delegating most methods
of I to an instance of I, and overriding only a few (or none), requires
a lot of low-value boilerplate to implement. But the proposal seems to
conflate the boilerplate reduction of this kind of inheritance, with a
new semantics for lightweight wrappers, which I think would be better
off separated.
The boilerplate problem illustrated here has been well known in Java for
a long time; RFEs along these lines were circulated as early as 1998
(e.g., https://bugs.openjdk.java.net/browse/JDK-4104202), if not
earlier. These go under the general category of "inheritance by
delegation". (Kotlin has more recently offered up its answer for this
long-standing Java RFE:
class Derived(b: Base) : Base by b { ... }
which says that methods of `Base` not overriden by `Derived` are
delegated to `b`.)
The specific solution proposed here doesn't really add to our
understanding of the problem; the problem is a fairly well-understood
one, and is ultimately "syntactic" at heart -- the language already
provides us with a way to say what we want, it is just that the code for
doing so is overly verbose, duplicative, and error-prone. (The number
of "no" answers in the Q&A belies the fact that it is an attempt to
solve a very specific syntactic problem, which is often a clue that the
problem is being solved at the wrong level.)
From my perspective, were we to prioritize such a feature (which I
don't think merits moving up on the priority list, even though it is a
nice-to-have), I would prefer to see it as a pure implementation detail
rather than a new kind of entity (as Wrappers suggests) or a new kind of
delegation (as Kotlin's approach suggests.) If C implements Foo, from
the outside, we shouldn't care how it implements Foo, or constraint it
to do so in a specific way. Something like
class C implements I {
private final I underlying;
implements I via underlying;
... more methods
}
gets the job done without exposing this information where it doesn't
belong.
A secondary problem with all the solutions I've seen for this problem is
that they rely on some sort of build-time expansion of the interface,
which means that default methods that are known at runtime but not at
build time will not be delegated, even when they are in fact implemented
by the class of the underlying delegee. (This is a limitation of the
JVM; there's no "method missing" suppport, so someone somewhere has to
do the expansion of the interface.) While this is not a fatal flaw, it
is unfortunate.
-------- Forwarded Message --------
Subject: Wrappers feature proposal
Date: Wed, 13 Jan 2021 15:22:08 +0200
From: Dimitris Paltatzidis <dcrystalmails at gmail.com>
To: amber-spec-comments at openjdk.java.net
We use wrappers to override 1 or more instance methods of an instance.
*The wrappers that you and I have in our mind might not be the same. Bear
with me, for the concept.
Suppose we have an instance of class A and we want to override 1 of its
methods.
Currently, to achieve that we have to write:
class Wrapper extends A {//here A could be an interface (implements), but
we'll touch that later
private final A a;
Wrapper(A a) {
//We hope that A has a no args constructor
if (a == null) throw new NullPointerException();
this.a = a;
}
@Override public void method1() {this.a.method1();}
@Override public int method2() {return this.a.method2();}
.
.
.
@Override public void methodN() {this.a.methodN();}
@Override public void methodToOverride() {/*We override this method*/}
}
A few problems with the above implementation are:
1. Most of the time we want to override 1 or a small portion of all the
methods of A,
but we have to override all of them (all they way up the inheritance
hierarchy),
delegating to the originals of our A instance, just to reach the few we
actually
need to override. That is, we have to write a lot of boilerplate.
2. The class Wrapper is a child of A, so it possibly has instance fields
(hidden in A
and above in the inheritance hierarchy) that adds up to the memory
footprint of Wrapper,
even though there is no need for a Wrapper instance to have its own
state. Its state
is the state of its underlying A instance. It's only purpose is to
override or delegate.
3. There is no guarantee that there is an appropriate constructor in A to
call with super()
in the Wrapper constructor. So either you add one in A (if you have
access to it) that
initializes it in a "default for wrappers" state or you call one with
garbage values,
to hopefully pass any guards there might be. Either case, you end up
with a state that
could be illegal (or meaningless), just to pass through the Wrapper
constructor. All
that with additional work, and again to create a state that shouldn't be
there as it
eats memory and could be illegal and prone to bugs. Yes, interfaces
won't have the
constructor and state problems.
Ok, so how do these wrappers look like then?
wrapper Wrapper extends A {
@Override public void methodToOverride() {/*We override this method*/}
}
That's it. It is equivalent to the above verbose one.
Every single method that is not overridden will be delegated.
And to create it:
A a = new A(..);
Wrapper w = new Wrapper(a);
We can also have anonymous wrappers:
public static A unmodifiable(A a) {
return new wrapper A(a) {/*Override only the necessary methods*/};
}
But why? How can we benefit from wrappers?
- Unmodifiable collections is a good candidate in the JDK. They are the
so-called lightweight
wrappers that throw UnsupportedOperationException in any attempt to
change the underlying
collection through them.
- Spy wrappers. Suppose you have a window, and you want to get notified
every time its size
changes. You can just wrap it and override its getWidth() and getHeight()
methods, giving
the observer pattern another look.
- Possibly more..
What about interfaces?
- Sure, wrappers can work with them too, no matter how many you implement.
All non overridden
methods will be delegated to the underlying instance. Of course you must
supply the wrapper
construction with an instance that implements all of them.
What about anonymous wrappers and multiple interfaces?
- Well, it's a No here, to stay consistent with anonymous classes.
Should you be able to access the wrapped instance from a wrapper? And if
so, how?
- Yes. I'm not sure if the keyword is "super" or something else here
though..
Are wrappers just like classes?
- I like to think of wrappers as enhanced object references. They really do
reference you
to their underlying wrapped instance and sometimes you take a side trip
when methods are
overridden.
Can I add instance fields in wrappers?
- That defeats one of their purposes, No. Wrappers should be treated as if
they are their
underlying instance, not on their own. I'm not sure about static finals
though.
Can I add methods in wrappers?
- Just like the previous answer, wrappers shouldn't be viewed as
independent entities, so
it probably doesn't make sense to add methods. I'm not sure about statics
though.
Can I extend wrappers?
- No. They are implicitly final.
Are wrappers just a child class of the class they extend?
- So, I want the answer to be mostly No, because the semantics of wrappers
is to actually
"mutate" methods of a given instance of the class they extend (yes, that
is a subset
attribute of child classes too). They actually Are the class they extend
and not a child.
But, without breaking major rules here and to honor a lot of the Java
terms, they
technically are a child class.
Can I wrap a wrapper with another wrapper?
- No. One layer is sufficient. That is tricky to implement though, without
letting technical
dept into the game.
Can wrappers wrap a class that may have final methods?
- Sure, you just won't be able to override those final methods. Now, if the
class is not
final, but for some reason all of its methods are, then you would end up
with a wrapper
that basically just delegates. There is nothing illegal here, just
pointless.
Can wrappers wrap final classes?
- No.
Should wrappers be referenced by their type or by the type they wrap?
- I'm not sure if wrappers should be used as public API. I think they can
thrive as hidden
implementations making the API flatter, e.g.
Collections.unmodifiableCollection() returns
a Collection, even though the actual implementation is
UnmodifiableCollection, which is
a private static nested class. But, no one expects UnmodifiableCollection
as the returned
type. The same goes for wrappers too. Wrappers will mostly be returned by
methods as the
type they wrap, and if used in statements, it's better to have A w = new
Wrapper(a); as
opposed to Wrapper w = new Wrapper(a); If wrappers can't have methods of
their own, what's
the point of the latter statement? Also, the latter hides the actual type
of a (yes, good
naming conventions could help).
Can sealed classes and interfaces permit wrappers?
- Sure. Anonymous wrappers are the exception here just like anonymous
classes.
Can wrappers wrap sealed classes that don't permit them?
- No.
What about Serialization?
- Well, wrappers don't have a state of their own, so that's a good thing.
But, I can't tell
if it'll be a nightmare to get them to play together with it or not.
Ok, so .equals() is delegated, but what about == ?
- Normally you would expect wrappers to be instances of their own, having
an identity, but
I'm not sure what that identity is (or maybe they don't need identity
*caught* future
inline from project Valhalla). Are 2 wrappers that wrap the same instance
and come from
the same wrapper "class" different as per == or equal?
So, wrappers are just syntactic sugar.?
- No, but at their most degraded version yes. They can be so much more than
that, and here's how:
1. You can remove their state completely, making them truly lightweight.
Their state is the
state of their underlying instance. The class they wrap doesn't need
to have an appropriate
constructor, as they don't have a state of their own to initialize.
2. Now, to push things a little bit more, wrappers could wrap classes
that have package-private
abstract methods (outside the package). You would be able to override
only the protected/public
methods. Wrappers do not provide implementations, they just override
the already implemented
methods of their underlying instance, so there is a guarantee that the
wrapped instance will
have that package-private method overridden.
The meaning of wrappers is that: Any method that needs to be overridden
(and can be), should be
without any hassle and per instance. e.g. I have this instance and I want
to override 2 of its
methods and only for that instance.
Wrappers should be as close to their underlying wrapped instance as
possible. It's like having a
reference to that instance that magically has that method you want to
override, overridden.
Wrappers and their underlying instances shouldn't be visualized as
different objects.
Who can benefit from wrappers?
- Application authors: Yes, especially using them as spy wrappers, for
events.
- Library authors: Yes. Immutability will be easier to achieve and more
accessible.
- Compiler authors: Maybe?
What about other languages, do they have wrappers?
- Yes, Kotlin has them, under the term Delegation. Although, they are not
quite the same as here,
as they can have a state of their own.
More information about the amber-dev
mailing list