I just screamed at Java again. This time my ire has been raised by the lack of self-types in the language.
Self-types
So what is a self-type. Well, I could point you at some links, but I'll try and have a go at defining it myself...
A self-type is a special type that always refers to the current class. Its used mostly with generics, and if you've never needed it then you won't understand what a pain it is that it isn't in Java.
Here's my problem to help explain the issue. Consider two immutable classes - ZonedDate and LocalDate - both declaring effectively the same method:
public final class ZonedDate { ZonedDate withYear(int year) { ... } } public final class LocalDate { LocalDate withYear(int year) { ... } }
The implementations only differ in the return type. This is essential for immutable classes, as you need the returned instance to be the same type as the original in order to call other methods (like withMonthOfYear).
Now consider that both methods actually contain the same code, and I want to abstract that out into a shared abstract superclass. This is where we get stuck:
public abstract class BaseDate { BaseDate withYear(int year) { ... } } public final class ZonedDate extends BaseDate { // withYear removed as now in superclass, or is it? } public final class LocalDate extends BaseDate { // withYear removed as now in superclass, or is it? }
The problem is that we've changed the effect of the method as far as ZonedDate and LocalDate are concerned, because they no longer return the correct type, but now return the type of the superclass.
One solution is to override the abstract implementation in the subclass, just to get the correct return type (via covariance):
public abstract class BaseDate { BaseDate withYear(int year) { ... } } public final class ZonedDate extends BaseDate { ZonedDate withYear(int year) { return (ZonedDate) super.withYear(year); } } public final class LocalDate extends BaseDate { LocalDate withYear(int year) { return (LocalDate) super.withYear(year); } }
What a mess. Imagine doing that for many methods on many subclasses. Its a large amount of pointless boilerplate code for no good reason. What we really want is self-types, with a syntax such as <this>:
public abstract class BaseDate { <this> withYear(int year); } public final class ZonedDate extends BaseDate { // withYear removed as now correctly in superclass } public final class LocalDate extends BaseDate { // withYear removed as now correctly in superclass }
The simple device of the self-type means that the withYear
method will now appear to have the correct return type in each of the subclasses - LocalDate in LocalDate, ZonedDate in ZonedDate and so on. And without reams of dubious boilerplate.
Summary
Self-types are a necessary companion for abstract classes where the subclass is to be immutable (and there are probably other good uses too...).
Opinions welcome as always!
This is especially annoying with creating more fluent interfaces...
ReplyDeleteWith Java 5.0 with generics there is a work around though. Declare a self referencing parameter on the abstract class. Such as:
public abstract class BaseDate>
{
E withYear(int year)
{
...;
// This will raise an unchecked warning...
return (E)this;
}
}
public final class ZonedDate extends BaseDate
{
// Same effect as self type...
}
public final class LocalDate extends BaseDate
{
// Same effect as self type...
}
Although I do admit that this is not the prettiest syntax (..it also lends itself to abuse), its a bit better than tons of covariant overrides.
Hope this helps.
Cheers,
Herman van Hovell
Why wouldn't the following piece of code work?
ReplyDeletepublic abstract class BaseDate {
T withYear(int year) { return (T) this; }
}
public final class ZonedDate extends BaseDate {
ZonedDate withYear(int year) {
return super.withYear(year);
}
}
public final class LocalDate extends BaseDate {
LocalDate withYear(int year) {
return super.withYear(year);
}
}
Am I missing something?
You're correct, that would work too. But, now you've exposed an implementation detail to all users of the API. Specifically, you can't hold a reference to a BaseDate without specifying the implementing subclass, which is a little daft.
ReplyDeleteThe biggest problem with John's solution is that if BaseDate is concrete, then you need to refer to it as BaseDate everywhere or face a raw-type warning.
ReplyDeleteI'd like to see an implementation of the first solution (the one with typecasts in) that won't throw a ClassCastException.
ReplyDelete@Ricky: getClass().newInstance() ?
ReplyDelete@Stephen: I'm desperate for self-types. Current Generics force exposing implementation details in cases like mentioned, which is a pain in the butt. This would also solve the Object.clone() problem. Could also help with implementing Comparable.
It's quite a problem to build a class thinking ahead of what subtypes may need beforehand.
@Ricky, you have a single abstract factory method in the superclass implemented by each subclass, or use getClass().newInstance() ;-)
ReplyDeleteI think self types are a good idea, though I wonder if they are among the low hanging fruit for improving the language. The keyword "this" could be used to mean the current type because of Java's separation of namespaces. Few people have written about what the spec would look like; self types would have to be restricted to covariant contexts (i.e. not allowed as an argument type).
ReplyDeleteI submitted a comment, however I kept getting the message "Your comment was marked as spam and will not be displayed." What gives?
ReplyDeleteThis seems pretty questionable to me. It doesn't make much sense to have a base class that refers to the actual class of its children. OO would suggest that if a subclass wants to constrain the interface of its base class it should override the behavior. If you have a situation where you're doing this a lot then you probably aren't dealing with a subclass at all.
ReplyDeletegetClass().newInstance() is clearly flawed in the common case that there is no public no-arg constructor (it places a contract on subclasses that is not expressible in the type system). The abstract factory is the one way that works.
ReplyDeleteI haven't used subclassing in Java for a few years now, initially as an experiment to see whether I could do without it, and now because I can't see any cases where I'd want it. Does the self-typing issue manifest itself without subclassing?
@Neal, I deliberately used the angle brackets around this to mark out the self-type as related to generics. Personally, I'd like to see a self-type usable in method parameters too, but I've heard before that there is some deep and dark reason why it doesn't work :-)
ReplyDelete@Matthew, I didn't mark your comment as spam, JRoller did. I've no idea why, but I've found it now and let it through.
@Ocean, The superclass isn't referring directly to the class of the subclass, its simply telling the compiler to auto-implement covariant types. I don't believe that it has anything to do with OO. I have noticed that for me, this is a problem primarily around designing APIs with immutable classes. Immutables are still pretty rare in Java, but with the forthcoming multi-processor world, immutables will become more common.
@Ricky, I believe self-types are only useful wrt subclasses.
> Personally, I'd like to see a self-type usable in method
ReplyDelete> parameters too, but I've heard before that there is some deep
> and dark reason why it doesn't work :-)
Isn't it basically the same reason why there aren't "covariant parameter types"? If BaseDate could have a foo() method, and you just have a reference to a BaseDate, what value could you safely pass to it, not knowing the receiver's actual class? It would seem that foo could only be invoked if the type of the target expression were a final class.
@Anonymous, Yes, that sounds correct:
ReplyDeleteBaseDate date = ...
date.foo(new ZonedDate());
We can't tell if this is valid, because we don't know that date is z ZonedDate instance. One solution would be to throw a ClassCastException at runtime if it was invalid, which isn't ideal.
If you want to use runtime features at compile time, than don't try to extend Java with a new cryptical syntax. Change the language of implementation, resp. the language needs to change.
ReplyDeleteNowadays it should be the job of the IDE to validate the structure of the code at coding time, rather than the compiler. The problem above wouldn't bother you, if you (and the user's of your API, of course) got the correct IntelliSense when needed.
IMHO ...
I can't post code on this site - it gets marked as spam! Can you fix that?
ReplyDeleteSelf types have some uses but they can also be a problem. When they were introduced in Eiffel v3 the example given was a ski camp were you don't wan't the boys to share rooms with the girls. The classes were something like:
ReplyDeleteabstract class Skier {
this roomMate;
this getRoomMate() { return roomMate; }
void setRoomMate( this roomMate ) { this.roomMate = roomMate; }
}
class Boy extends Skier {}
class Girl extends Skier {}
The disadvantage is that when the example was extended to include ranked skiers then a ranked boy couldn't share with a none ranked boy. If self referencing generic types are used instead then this isn't a problem, e.g.:
abstract class Skier< S extends Skier< S > > {
S roomMate;
S getRoomMate() { return roomMate; }
void setRoomMate( S roomMate ) { this.roomMate = roomMate; }
}
class Boy extends Skier< Boy > {}
class Girl extends Skier< Girl > {}
class RankedBoyextends Skier< Boy > {}
class RankedGirl extends Skier< Girl > {}
And a ranked skier can share with a none ranked skier. Therefore self types introduce other problems. Is there a way of getting the best of the two systems?
> We can't tell if this is valid, because we don't
ReplyDelete> know that date is z ZonedDate instance. One
> solution would be to throw a ClassCastException at
> runtime if it was invalid, which isn't ideal.
On method parameters, only contra-variance would be possible, not sure if that's of any use in current Java. Otherwise, one would need runtime binding of methods, I think.
Hi stephen, peter ahé wrote a blog about self type
ReplyDeletehttp://blogs.sun.com/ahe/entry/selftypes
last year.
In you case, you can hide implementation details
by using a non-public abstract class between
BaseDate and its subclasses.
Rémi
I think what is missing are mixins. You would still need the self-type facility, but a mixin models this problem better than a base class, IMHO.
ReplyDelete@Stefan, I suspect I was thinking of erased method parameters, so only the subclasses would be real method overrides.
ReplyDelete@Remi, yes you are probably correct that a hidden class between BaseDate and its subclasses would work. However, it won't be acceptable if BaseDate is a class that is intended to be subclassed and added to the JDK!
This can be achieved using recursive generic types. See below. Note that I have used square brackets in place of angle brackets, as they did not render correctly.
ReplyDeletepublic class DateTest
{
@Test
public void testDate()
{
ZonedDate withYear = new ZonedDate().withYear(0);
}
}
abstract class BaseDate[T extends BaseDate[T]]
{
public T withYear(final int year)
{
return null;
}
}
final class ZonedDate extends BaseDate[ZonedDate]
{
// some other logic
}
final class LocalDate extends BaseDate[LocalDate]
{
// some other logic
}
The Manifold open source project provides the self type for Java via the @Self annotation. You can read more about it here.
ReplyDeleteThe simple case:
public class Foo {
public @Self Foo getMe() {
return this;
}
}
public class Bar extends Foo {
}
Bar bar = new Bar().getMe(); // Voila!
Use with generics:
public class Tree {
private List<Tree> children;
public List<@Self Tree> getChildren() {
return children;
}
}
public class MyTree extends Tree {
}
MyTree tree = new MyTree();
...
List<MyTree> children = tree.getChildren(); // :)
Use with extension methods:
package extensions.java.util.Map;
import java.util.Map;
public class MyMapExtensions {
public static <K,V> @Self Map<K,V> add(@This Map<K,V> thiz, K key, V value) {
thiz.put(key, value);
return thiz;
}
}
var map = new HashMap<String, String>()
.add("bob", "fishspread")
.add("alec", "taco")
.add("miles", "mustard");