Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Issue with Java type parameters for return values

I've tried to condense this issue down to the smallest amount of code that I could.

I have defined a table structure, like a database table, with the Row and Table classes below. A Table is essentially a list of Row. I want subclasses to define their particular flavor of Table and Row and I want the compiler to catch inappropriate attempts to put rows of one type into tables of an incompatible type.

The abstract Agent class provides a method to take parameters and return a table that takes rows of type T. I have defined three methods to illustrate the problem that I am having.

The FinalAgent, FinalTable, and FinalRow classes define implementations of the Agent, Table, and Row classes. Ultimately, what I want is method2a, which takes a list of parameters and returns a table of type FinalTable.

public abstract class Row {}
public abstract class Table<T extends Row> {}
public abstract class Agent {
    public <T extends Row> Table<T> method1(List<String> parameter) {
        return null;
    }
    public <T extends Row> Table<T> method2a(List<String> parameter) {
        return null;
    }
    public <T extends Row> Table<T> method2b(String parameter) {
        return null;
    }
}

public class FinalRow extends Row {}
public class FinalTable extends Table<FinalRow> {}
public class FinalAgent extends Agent {
    @Override
    public <T extends Row> Table<T> method1(List<String> parameter) {
        return null;
    }
    @Override
    public FinalTable method2a(List<String> parameter) {
        return null;
    }
    @Override
    public FinalTable method2b(String parameter) {
        return null;
    }
}

At the bottom:

  • method1 of FinalAgent compiles, but I have to write Table<FinalRow> t1 = new FinalAgent().method1(null); in order to call the method.
  • method2a of FinalAgent I changed the return type to FinalTable to reflect what I am actually returning (I want to write FinalTable t2a = new FinalAgent().method2a(null);), but the compiler produces the error: The method method2a(List) of type FinalAgent must override or implement a supertype method
  • method3 I changed the parameter from a List to a String. The method compiles OK but gives me a type safety warning, which I can at least work with.

So, finally, the question: Is it a compiler bug that method2a in FinalAgent does not compile yet method2b does compile?

I might as well also ask, is there a better way to do what I am doing?

like image 575
John Jackson Avatar asked Oct 05 '16 21:10

John Jackson


People also ask

Can a type parameter have multiple upper bounds in Java?

Core Java bootcamp program with Hands on practiceA type parameter can have multiple bounds.

How do you restrict the types used as type arguments in generic classes and methods?

Whenever you want to restrict the type parameter to subtypes of a particular class you can use the bounded type parameter. If you just specify a type (class) as bounded parameter, only sub types of that particular class are accepted by the current generic class. These are known as bounded-types in generics in Java.

What is type parameter in Java?

A type parameter, also known as a type variable, is an identifier that specifies a generic type name. The type parameters can be used to declare the return type and act as placeholders for the types of the arguments passed to the generic method, which are known as actual type arguments.

How many type parameters can be used in a generic class?

Multiple parameters You can also use more than one type parameter in generics in Java, you just need to pass specify another type parameter in the angle brackets separated by comma.


3 Answers

It's not clear why you're parameterizing the methods. You're promising to return a Table<T>, but your method can't identify T at runtime because of type erasure. You probably want to parameterize the entire Agent class instead:

public abstract class Agent<T extends Table<?>> {
    public T method1(List<String> parameter) {
        return null;
    }
    public T method2a(List<String> parameter) {
        return null;
    }
    public T method2b(String parameter) {
        return null;
    }
}

public class FinalAgent extends Agent<FinalTable> {
    @Override
    public FinalTable method1(List<String> parameter) {
        return null;
    }
    @Override
    public FinalTable method2a(List<String> parameter) {
        return null;
    }
    @Override
    public FinalTable method2b(String parameter) {
        return null;
    }
}

As for your initial question, I can't make sense of the discrepancy or the warning message on method2b.

like image 188
shmosel Avatar answered Nov 06 '22 02:11

shmosel


Using a method parameter at a single place means the parameter is usually unnecessary

With methods like that in Agent, FinalAgent compiles fine:

public Table<? extends Row> method2a(List<String> parameter) {
    return null;
}
public Table<? extends Row>  method2b(String parameter) {
    return null;
}
like image 44
k5_ Avatar answered Nov 06 '22 02:11

k5_


This is direct consequence of raw type.

  • method1 is still a generic method : no issue.
  • method2b is not anymore a generic method but override the method in Agent : no issue (except the safety warning though, due to the unchecked conversion)
  • method2a is not anymore a generic method but does not override anything

method2a is seen declared like this from FinalAgent, after compilation :

public Table method2a(List parameter) 

But defined like this in FinalAgent :

public FinalTable method2a(List<String> parameter)

And now you are stuck. Same erasure but does not override correctly method2a

Here is a simple example :

public interface Foo
{
    public void bar(List a);
}

public class FooChild implements Foo
{
    @Override
    public void bar(List<String> a)
    {
    }
}

It gives The method bar(List<String>) of type FooChild must override or implement a supertype method

public interface Foo
{
    public void bar(List a);
}

public class FooChild implements Foo
{
    public void bar(List<String> a)
    {
    }
}

It gives Name clash: The method bar(List<String>) of type FooChild has the same erasure as bar(List) of type Foo but does not override it

Another example. This code compiles fine :

public interface Foo
{
    public void bar(List<String> a);
}

public class FooChild implements Foo
{
    public void bar(List<String> a)
    {   
    }   
}

This one does not :

public interface Foo
{
    public <T> void bar(List<String> a);
}

public class FooChild implements Foo
{
    public void bar(List<String> a)
    {   
    }   
}

It gives Name clash: The method bar(List<String>) of type FooChild has the same erasure as bar(List<String>) of type Foo but does not override it

As soon as bar becomes a generic method, it is seen with raw types and we can notice the same issue.

A different example of raw type issue : Name clash when overriding method of generic class

like image 40
ToYonos Avatar answered Nov 06 '22 02:11

ToYonos