The justly famed Curiously Recurring Template Pattern (CRTP) is one of the cooler consequences of the design of templates in C++. But Java generics supports the same idiom:
public interface Bob<T extends Bob<T>> { T doWop(T t); } public class Fred implements Bob<Fred> { public Fred doWop(final Fred f); }
Which is particularly nice if you are writing base classes.
Good for something
However, what about a case like this — lets try to rationalize my favorite pariah class, java.io.File
. For starters, files and directories should be different types extending a common superclass. It makes no sense to list children on a file, nor to open an input stream on a directory. Starting simply:
public interface Entity<T extends Entity<T>> { void create() throws IOException; void delete() throws IOException; void renameTo(final T newLocation) throws IOException; }
renameTo
is the first interesting method. I do not want renaming files to directories and vice versa to even compile if possible, so I require the arguments to be of the same type.
Problem the first
Now a file:
public interface File<T extends Entity<T>> { InputStream getInputStream() throws IOException; OutputStream getOutputStream() throws IOException; }
In the generic specification, why not T extends File<T>
? Ah, my first chance to get bit by generics. You see, when I tried that the first time, I could then no longer implement Entity
and File
separately with FileImpl
extending EntityImpl
. Why not? Then there are two distinct superclasses, Entity<EntityImpl>
and Entity<FileImpl>
for my eventual concrete class tying it all together, and the compile does not like that.
(This is a great spot to point out that languages with multiple inheritance or mixins such as C++ and Scala do no suffer this limitation. It is a direct fallout of single inheritance with generics. You should try it yourself a few times to convince yourself; it took me a while to accept the problem at face value.)
And a directory:
public interface Directory<T extends Entity<T>> { boolean isRoot() throws IOException; }
(By now you are probably wondering about throwing IOException
from everywhere. My practical use for reimplementing java.io.File
includes networked files so I must accept that all operations potentially fail. Throwing is so much better than the broken example of java.io.File
, some methods returning false
on failure. And just look at the horror that is createNewFile()
, both throwing IOException
and returning false
depending on the exact failure.)
Problem the second
What about some implementations?
public abstract class LocalEntity <T extends LocalEntity<T>> implements Entity<T>, Comparable<LocalEntity<T>> { protected final File backing; // Constructor, implementations }
And yet another gotcha! See that backing is marked protected
? This is not of necessity because I access backing from classes which extend LocalEntity
. It is required because I access backing from within LocalEntity
. Here:
public void delete() throws IOException { if (!backing.delete()) throw new IOException(); } public void renameTo(final T newLocation) throws IOException { if (!backing.rename(newLocation.backing)) throw new IOException(); }
The delete()
method compiles fine if I change backing from protected
to private
. The renameTo(T)
method, however, does not: specifically, newLocation.backing
is no longer accessible. That is because T is not the same type as myself; the generic declaration declares that it is some type which extends myself. Hence, I need protected
access. That one took a while for me to grok as well.
The final problem
One last final trick. What about the final concrete classes? I want to be able to write Directory dir = new LocalDirectory("/home/binkley")
and not worry about generics. There takes quite a bit more magic for that to happen. Watch:
public abstract class LocalDirectoryType <T extends LocalDirectoryType<T>> extends LocalEntity<T> implements Directory<T> { // Constructors forwarding to super; no methods or members } public class LocalDirectory extends LocalDirectoryType<LocalDirectory> { // Constructors, and unimplemented directory-specific methods }
Wow! The chicanery is necessary so that the end product, LocalDirectory
has no generics. The implementor should do all the work, not the user. I make one last use of CRTP, extend an implementation superclass and add on the Directory
interface.
Conclusion
That is a lot of typing. I hope it is all worth it. In C++-land this sort of thing is considered high sport, but most Java folks I know get a sour expression at the sight of generics. Suum cuique pulchrum est.
If you would like to see a fuller example, please drop me a line.
UPDATE: I add an important correction in the following post.
No comments:
Post a Comment