简体   繁体   中英

Understanding generics: Incompatible types when class has generic type and implements one of its parametrised superclass

I am implementing a simple app with MVP architecture.

Here are my MvpView and MvpPresenter interfaces (nothing interesting about MvpModel , so I am skipping it):

/// MvpView.java
public interface MvpView {
}

/// MvpPresenter.java
public interface MvpPresenter<V extends MvpView> {
    void attachView(V view);

    void detachView();
}

Now I have a basic MvpView implementation, which is an Activity :

// BaseActivity.java
public abstract class BaseActivity<V extends MvpView, P extends MvpPresenter<V>> 
        extends AppCompatActivity implements MvpView {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getPresenter().attachView(this);
    }

    public abstract P getPresenter();

    // other logic
}

As for me everything looks correct, but there is a compile error in line:

    getPresenter().attachView(this);

不兼容的类型

If I add cast to V then project compiles and everything works fine:

    getPresenter().attachView((V) this);

1. The question is why I need this cast or why I am experiencing this incompatible types error without casting?

(1 is already answered by Eran )

2. How I can link V to BaseActivity in this example or how it is better to implement this MVP approach?

It is strange, as for me, because my BaseActivity is extending MvpView as it is defined by this generic parameter: V extends MvpView !

getPresenter() is an instance of type P , which extends MvpPresenter<V> . Therefore getPresenter.attachView() expects an argument of type V .

Now, we know that V must implement MvpView , and we also know that BaseActivity implements MvpView , but this implementations don't necessarily match.

For example, you can create a concrete sub-class SubBaseActivity and instantiate it with:

SubBaseActivity<MvpViewImpl, MvpPresenterImpl<MvpViewImpl>>
    activity = new SubBaseActivity<> (); // let's ignore the fact that you are not suppose
                                         // to instantiate Android activities this way

Now getPresenter() returns a MvpPresenterImpl and getPresenter().attachView() expects an argument of type MvpViewImpl . But this is not of type MvpViewImpl .

When you make the unsafe cast from BaseActivity<V,P> to V , you are telling the compiler BaseActivity<V,P> can be cast to V . However, the reason this works at runtime is that the compiler erases the generic type parameters V and P . Since the type bound of V is MvpView , the casting to V becomes a casting to MvpView , which BaseActivity implements.

The problem is that you are assuming that V makes reference to BaseActivity<V,?> because you are probably using it only this way in practice.

Unfortunately currently there is no way to make reference to the class you are declaring in the type arguments bounds as to force such a constraint.

The simplest fix is to use an unchecked cast (masking the warning if you like):

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    @SupressWarning("unchecked")
    final V myView = (V) this;
    getPresenter().attachView(myView);
}

You don't need to use the local myView variable but declaring it explicitly allows you to silence only that cast and not any other "unchecked" warning in that method.

In any case you must make sure that any extending class would set their V type-parameter to themselves as to not break contract:

public class ConcreteActivity<V extends MvpView, P extends MvpPresenter<V>> extends BaseActivity<ConcreteActivity<V,P>, P> {
...
} 

There is no way you can enforce that at compilation time, instead your test code should use reflection to verify that each extending class complies with such a restriction.

You can go a bit further in order to avoid the unchecked cast warning but that would require for you to add a field typed V pointing to this set in the BasicActivity constructor passed as a parameter by the extending class constructor....

public abstract class BaseActivity<V extends MvpView, P extends MvpPresenter<V>> 
        extends AppCompatActivity implements MvpView {

    private final V myView;

    protected BaseActivity(final V myView) {
       if (myView != this) { throw new IllegalArgumentException("you must pass this object"); } 
       this.myView = myView;
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getPresenter().attachView(myView);
    }

    public abstract P getPresenter();

    // other logic
}


public class ConcreteActivity<V extends MvpView, P extends MvpPresenter<V> extends BaseActivity<BaseActivity<V, P>, P> {

    public ConcreteActivity() {
       super(this);
    }
    ...
}

Notice that we double check that myView is in fact this in the constructor in order to fail early if the extending class does not comply; you can leave that out if your test code makes sure that this is going to be always the case.

Despite that is preferable to avoid any kind of warning... I would say that in this situation the first alternative is quite acceptable as it needs less code and is more efficient memory-wise.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM