Home > Software engineering >  An issue with Generic type parameters and Inheritance
An issue with Generic type parameters and Inheritance

Time:03-20

Couldn't concretize the title, sorry for that.

For example, we have a Parent class and a Child class:

package test;

public class Parent {
}
package test;

public class Child extends Parent {
}

And a class that can contain Parent's child in its field called a:

package test;

public class Holder<A extends Parent> {
    public A a;
}

Let's create an instance of that class and try to assign a value to a:

package test;

public class Main {
    public static void main(String[] main) {
        Holder<Child> holder = new Holder<>();
        holder.a = new Child();
    }
}

So far, so good. But let's say we want to move this assignation to a Holder's constructor:

package test;

public class Holder<A extends Parent> {
    public A a;

    public Holder() {
        a = new Child();
    }
}

Now we're getting an error: Error:(7, 13) java: incompatible types: test.Child cannot be converted to A.

So, my question is, why doesn't the compiler see that A is a Parent's child? Even if we set <A extends Parent>. And why we can assign values outside of the class, but can't inside of it? Can I somehow fix this problem?

I know we can replace A with Parent and remove that type parameter or pass new Child() to constructor's parameters in Main, but it's because I'm giving a simplified example of the problem.

CodePudding user response:

Upper-bounded generics A extends Something are not writable. Any attempt to assign anything apart from null to the variable a inside the Holder class will fail.

In order to understand why, let's consider the following code:

public class Parent {}
public class Child extends Parent {}
public class GrandChild extends Child {}

public class Holder<A extends Parent> {
    public A a;

    public void setA(A a) {
        this.a = a;
    }
}

A in the Holder class is just a place-holder for the type that will be provided at runtime. It's a way to tell the compiler that we don't know for now what the type is. But it will be a particular type and that has a Parent class in its inheritance chain. And that information compiler will take into account while checking whether the operation done on the variable a safe.

In the code below, the compiler will disallow to assign an object of Child as a value for a, because it's incompatible with a type GrandChild (that happens to be the actual type for A).

public static void main(String[] args) {
    Holder<GrandChild> grandChildHolder = new Holder<>();

    grandChildHolder.setA(new GrandChild()); // no issues
    grandChildHolder.setA(new Child()); // compilation error
}

Similarly, the following assignments inside the Holder class will not succeed because type A will be known only Holder class will get instantiated. And compile doesn't posses information whether it'll be Child, GrandChild, etc., therefore it will not consider these operations to be safe.

public class Holder<A extends Parent> {
    public A a;
    // instance initialither block (runs when Holder object is being created)
    { 
        a = new Child(); // compilation error - type A could be a GrandChild
        a = new GrandChild(); // compilation error - type A could potentially be represented by class incompatible with GrandChild

        a = null; // no issues because null is a valid value for any type
    }

    public void setA(A a) {
        this.a = a;
    }
}

Upper-bounded generic parameters like A extends Parent are useful when want to make a class or method to be able to work with objects of different types and the same time impose a certain restriction on a range of the valid types in to access the behavior of the Parent class (let's assume there are some like work(), goShoping(), etc.).

If you declare the holder without extends clause, just Holder<A> only methods of the Object class (hashCode(), equals(), toString()) will be accessible with variables and parameters of type A.

Also, clause extends Parent will allow the compiler to spot attempts to instantiate the Holder class with an invalid type:

Holder<String> stringHolder; // error: type argument String is not within the bounds of type A

CodePudding user response:

A is a [type] parameter. It's not an actual type. Like most classes, you could initialize the member via a parameter in the method constructor.

public class Main {

    public static void main(String[] args) {
        new Holder<Child>(new Child());
    }
}

class Parent {
    
}

class Child extends Parent {
    
}

class Holder<A extends Parent> {
    public A a;

    public Holder(A a) {
        this.a = a;
    }
}
  • Related