Alternatives to CRTP in Java [closed]
Asked Answered
S

1

8

The CRTP pattern allows to emulate the so called self types in Java, e. g.:

abstract class AbstractFoo<SELF extends AbstractFoo<SELF>> implements Comparable<SELF> {
    @Override
    public final int compareTo(final SELF o) {
        // ...
    }
}

final class C1 extends AbstractFoo<C1> {
    // ...
}

final class C2 extends AbstractFoo<C2> {
    // ...
}

With the above code (the Comparable interface has been chosen just for clarity; of course there're other use cases), I can easily compare two instances of C1:

new C1().compareTo(new C1());

but not AbstractFoo descendants whose concrete types are different:

new C1().compareTo(new C2()); // compilation error

It's easy to abuse the pattern, however:

final class C3 extends AbstractFoo<C1> {
    // ...
}

// ...

new C3().compareTo(new C1()); // compiles cleanly

Also, the type checks are purely compile-time ones, i. e. one can easily mix C1 and C2 instances in a single TreeSet and have them compared with each other.

What alternative to CRTP in Java emulates self types without the potential for abuse as shown above?

P. S. I observe the pattern is not widely used in the standard library -- only EnumSet and its descendants implement it.

Sublimation answered 2/10, 2018 at 15:4 Comment(8)
What measurement determines whether an alternative is better than CRTP? The question sounds subjective.Sfumato
@Sfumato Well, I understand the question may sound opinion-based, but I would appreciate any response descibing possible alternatives. Edited the question accordingly.Sublimation
If any alternative is acceptable, the question becomes too broad: there is no basis for comparing potential answers. FYI, I think the question is interesting, so I'm trying to nudge it towards being less open-ended. Perhaps, "What alternative to CRTP in Java emulates self types without the potential for abuse as shown above?"Sfumato
@Sfumato Thanks, edited as you're suggesting.Sublimation
@Sublimation this may be?Fireback
@Fireback Thank you. Formally, this may qualify as the correct answer to my question. Still, it requires way much more library code to be written if you're an author of the API, duplicating identical implementations. Take a look at the AssertJ library -- its code would have grown twice its current size =)Sublimation
@Fireback Also, your approach works for method return types, but doesn’t for method parameters.Sublimation
@Sublimation I barely read that, it was just a suggestion..Fireback
B
5

I don't think what you showed is "abuse". All the code that uses AbstractFoo and C3 are still perfectly type-safe, as long as they don't do any unsafe casts. The bounds of SELF in AbstractFoo means that the code can rely on the fact that SELF is a subtype of AbstractFoo<SELF>, but the code cannot rely on the fact that AbstractFoo<SELF> is a subtype of SELF. So for example, if AbstractFoo had a method that returned SELF, and it was implemented by returning this (as should be possible if it were really a "self-type"), it would not compile:

abstract class AbstractFoo<SELF extends AbstractFoo<SELF>> {
    public SELF someMethod() {
        return this; // would not compile
    }
}

The compiler doesn't let you compile this because it is unsafe. For example, running this method on C3 would return this (which is really a C3 instance) as type C1, which would cause a class cast exception in the calling code. If you tried to sneak past the compiler by using a cast, like return (SELF)this;, then you get an unchecked cast warning which means you take responsibility for it being unsafe.

And if your AbstractFoo is really used in a way that only relied on the fact that SELF extends AbstractFoo<SELF> (as the bound says), and does not rely on the fact that AbstractFoo<SELF> extends SELF, then why do you care about the "abuse" of C3? You can still write your classes C1 extends AbstractFoo<C1> and C2 extends AbstractFoo<C2> fine. And if someone else decides to write a class C3 extends AbstractFoo<C1>, then as long as they write it in a way without using unsafe casts, the compiler guarantees that it is still type-safe. Perhaps such a class might not be able to do anything useful; I don't know. But it is still safe; so why is that a problem?

The reason why a recursive bound like <SELF extends AbstractFoo<SELF>> is not used much is that, in most cases, it is not any more useful than <SELF>. For example, the Comparable interface's type parameter doesn't have a bound. If someone decides to write a class Foo extends Comparable<Bar>, they can do so, and it is type-safe, though not very useful, because in most classes and methods that use Comparable, they have a type variable <T extends Comparable<? super T>>, which requires that T is comparable to itself, so the Foo class would not be usable as a type argument in any of those places. But it is still fine for someone to write Foo extends Comparable<Bar> if they want.

The only places where a recursive bound like <SELF extends AbstractFoo<SELF>> is in a place which actually makes use of the fact that SELF extends AbstractFoo<SELF>, which is quite rare. One place is in something like the builder pattern, which has methods that return the object itself, which can be chained. So if you have methods like

abstract class AbstractFoo<SELF extends AbstractFoo<SELF>> {
    public SELF foo() { }
    public SELF bar() { }
    public SELF baz() { }
}

and you had a general value of AbstractFoo<?> x, you can do things like x.foo().bar().baz() which you could not do if it were declared as abstract class AbstractFoo<SELF>.

There isn't a way in Java Generics to make a type parameter that must be the same type as the current implementing class. If, hypothetically, there were such a mechanism, that could cause tricky problems with inheritance:

abstract class AbstractFoo<SELF must be own type> {
    public abstract int compareTo(SELF o);
}

class C1 extends AbstractFoo<C1> {
    @Override
    public int compareTo(C1 o) {
        // ...
    }
}

class SubC1 extends C1 {
    @Override
    public int compareTo(/* should it take C1 or SubC1? */) {
        // ...
    }
}

Here, SubC1 implicitly inherits AbstractFoo<C1>, but that breaks the contract that SELF must be the type of the implementing class. If SubC1.compareTo() must take a C1 argument, then it is no longer true that the type of the thing received is the same type as the current object itself. If SubC1.compareTo() can take a SubC1 argument, then it is no longer overrides C1.compareTo(), as it no longer takes as wide a set of the arguments as the method in the superclass takes.

Benn answered 1/2, 2020 at 7:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.