David R Tribble
David R Tribble

Reputation: 12204

Generic class method returns its own type as a template parameter

I'm designing a set of generic classes that hold Document objects within Folder objects:

// Folder, which holds zero or more documents
public interface Folder<DocType extends Document>
{
    // Locate matching documents within the folder
    public ArrayList<DocType> findDocuments(...);
    ...
}

// Document, contained within a folder
public interface Document
{
    // Retrieve the parent folder
    public Folder getFolder();    // Not correct
    ...
}

These classes are then extended for the actual implementation of the folder and document types. The trouble is that the Document.getFolder() method needs to return an object of type Folder<DocType>, where DocType is the actual implementation type of Document. Which means that the method needs to know what its own concrete class type is.

So my question is, should the Document class be declared instead like this:

// Document, contained within a Folder
public interface Document<DocType extends Document>
{
    // Retrieve the parent folder
    public Folder<DocType> getFolder();
    ...
}

Or is there a simpler way to do this? The code above requires concrete implementations to look like this:

public class MyFolder
    implements Folder<MyDocument>
{ ... }

public class MyDocument
    implements Document<MyDocument>
{ ... }

It's the Document<MyDocument> part that seems a bit strange to me; is it really necessary?

(Apologies if this is a duplicate; I couldn't find the exact answer I was looking for in the archives.)

ADDENDUM

The original code above used ArrayList<DocType>, but like several posters have pointed out, I'd be better off returning a List, e.g.:

    public List<DocType> findDocuments(...);

(That method was not crucial to my problem, and my actual API returns an Iterator, so I just used the first thing that came to mind in order to simplify the question.)

Upvotes: 2

Views: 2458

Answers (2)

meriton
meriton

Reputation: 70564

That seems an aedequate way to adress your requirements, and no, there is no simpler way to express the same (in particular, Java has no syntax to constrain a type parameter to the type of this).

However, since you extend both Document and Folder, perhaps your actually need a mutually recursive type bound. By which I mean that in your solution, the expression

new MyFolder().findDocuments().get(0).getFolder()

is of type Folder<MyDocument>, not MyFolder. If it needs to be MyFolder, the generics get ugly:

//Folder, which holds zero or more documents
interface Folder<D extends Document<D, F>, F extends Folder<D, F>> {
    ArrayList<D> findDocuments();
}

// Document, contained within a folder
interface Document<D extends Document<D, F>, F extends Folder<D, F>> {
    F getFolder();
}

You can then implement:

class MyFolder implements Folder<MyDocument, MyFolder> {
    @Override
    public ArrayList<MyDocument> findDocuments() {
        return new ArrayList<>(Collections.singletonList(new MyDocument()));
    }
}

class MyDocument implements Document<MyDocument, MyFolder> {
    @Override
    public MyFolder getFolder() {
        return new MyFolder();
    }
}

and now,

MyFolder folder = new MyFolder().findDocuments().get(0).getFolder();

compiles, too.

BTW, using ArrayList in an interface is quite restrictive, as the kludge when I wanted to use Collections.singletonList illustrates. You also (inadvertently?) deny yourself the services of Collections.unmodifiableList and friends. Usually, one therefore leaves the List implementation to the discretion of the implementor by declaring

List<D> findDocuments();

instead.

Upvotes: 2

Edwin Dalorzo
Edwin Dalorzo

Reputation: 78589

The way you have it seems to be Ok for me, with the exception of a missing type parameters in the Document and Folder interfaces and the use of ArrayList (an implementation) instead of List (an interface).

interface Folder<T extends Document<T>>
{
    public List<T> findDocuments();

}

interface Document<T extends Document<T>>
{
    public Folder<T> getFolder();
}

Using the API is the best way to know, let's say we have an implementation of Document called SpreadSheet:

class SpreadSheet implements Document<SpreadSheet> {

    @Override
    public Folder<SpreadSheet> getFolder() {
        return new SpreadSheetFolder();
    }
}

And then you have a Folder implementation called SpreadSheetFolder:

class SpreadSheetFolder implements Folder<SpreadSheet>{

    @Override
    public List<SpreadSheet> findDocuments() {
        return asList(new SpreadSheet(), new SpreadSheet());
    }
}

Then when you use your API, you would do:

Document<SpreadSheet> d = new SpreadSheet();
Folder<SpreadSheet> folder = d.getFolder();

It works just fine.

Upvotes: 4

Related Questions