How can an interface include a method that referen

2020-02-01 03:41发布

Suppose I am designing something like the following interface:

public interface MyInterface{
  public MyInterface method1();
  public void method2(MyInterface mi);
}

However, there is the caveat that the return type for method1 and the parameter for method2 match the concrete implementation and not just MyInterface. That is, if I have MyInterfaceImpl that implements MyInterface, it needs to have the following:

public class MyInterfaceImpl implements MyInterface{
  @Override
  public MyInterfaceImpl method1(){...}

  @Override
  public void method2(MyInterfaceImpl mi){...}
}

As written above, method1 won't cause any compile errors, but there is nothing guaranteeing that the return type matches in all implementations. Of course method2 won't even compile because the signature does not match the interface.

One candidate solution is to use self-referential or recursive bounds in generics:

public interface MyInterface<T extends MyInterface<T>>{
  public T method1();
  public void method2(T mi);
}

public class MyInterfaceImpl implements MyInterface<MyInterfaceImpl>{
  @Override
  public MyInterfaceImpl method1();

  @Override
  public void method2(MyInterfaceImpl mi);
}

This would get me what I want with one exception: other implementations might pass the wrong generic type (nothing forces T to match the concrete type). So potentially someone else could implement the following:

public class NotMyInterfaceImpl implements MyInterface<MyInterfaceImpl>{
  @Override
  public MyInterfaceImpl method1();

  @Override
  public void method2(MyInterfaceImpl mi);
} 

That would compile just fine even though NotMyInterfaceImpl should implement MyInterface<NotMyInterfaceImpl>.* That makes me think I need something else.

*Note that I don't think I'm trying to violate LSP; I'm OK with the return type/parameter being subclasses of NotMyInterfaceImpl.

So I don't know of a clean way to do this. That leads me to believe that I might be focusing too much on implementation details in the interface, but it doesn't seem that way to me. Is there any way to do the type of thing I described, or is this some kind of smell that I'm putting something in an interface that doesn't belong there?

5条回答
Viruses.
2楼-- · 2020-02-01 04:00

Is this what you are looking for?

public interface MyInterface {

    static abstract class MyInterfaceImpl implements MyInterface {

        @Override
        public abstract MyInterfaceImpl method1();

        @Override
        public abstract void method2(MyInterfaceImpl mi);

    }    

    MyInterfaceImpl method1();
    void method2(MyInterfaceImpl mi);

}

And you could even implement method 1 or 2 instead of making them abstract.

查看更多
对你真心纯属浪费
3楼-- · 2020-02-01 04:01

What you are trying to do is not legal because you are trying to narrow the parameter of the implemented type, and this "does not make sense". You are tryint to use "covariant" parameters, and only covariant return types are allowed (and even logic, and only supported from Java 5).

I mean, if it was possible to use covariant parameter types, you could do things like:

MyInterface instance = new MyInterfaceImpl();

And then, invoke on "instance" the method with another implementation supported by the interface but not supported by the MyInterfaceImpl class this way:

instance.method2(new MyInterfaceImpl_2());

Java cannot convert MyInterfaceImpl_2 to MyInterfaceImpl, so it prevents you from doing so at compilation time.

What you could do is to widen the parameter, using "contravariant" parameter, which would be logic. For more detail on this, check this anser:

Demonstrate covariance and contravariance in Java?

The only workaround that I can think of, is to solve the problem at runtime, I mean, doing something like this:

public class MyInterfaceImpl implements MyInterface{
  @Override
  public void method2(MyInterface mi){
        realMethod((MyInterfaceImpl) mi);
  }
  public void realMethod(MyInterfaceImpl) {...}
}

But you could get ClassCast exception, of course.

查看更多
▲ chillily
4楼-- · 2020-02-01 04:03

The point of returning the interface is such that the method does not care the actual implementation of the returned object. In your case you actually want to mandate the type to be a particular sub-implementation of that interface.

To apply the constraints that you described above, IMHO the design should probably be a base class instead of an interface. This allows you to control the implementation, for example a top-level flow, and leave low-level strategy to sub-classes to implement:

class MyBaseImpl {
    public final void fixedFlow() {
        MyBaseImpl obj = method1();
        obj.method2(this);
    }
    protected abstract MyBaseImpl method1();
    ....
}

There has to be other methods to make it interesting...; perhaps you have good reasons to want to do this...

Hope this helps!

查看更多
狗以群分
5楼-- · 2020-02-01 04:12

I believe that this cannot be done. There is simply no way to refer to an object's implementation class in the framework of generics, nor, as far as i know, any way to construct a cage out of pure generics which is capable of constraining the implementation class to match a type parameter.

The most useful thing i can suggest is using a self-referential parameter, and then always acquiring instances of implementations from factory methods which look like:

public <T extends MyInterface<T>> T newInstance();

It is easier for a camel to pass through the eye of a needle than for an instance of NotMyInterfaceImpl to pass through that return type. So, although troublemakers could write classes which do not conform to your masterplan, they couldn't return them from factories. Unless NotMyInterfaceImpl extended MyInterfaceImpl; but then, in a sense, it would also be a MyInterfaceImpl, so perhaps that would be kosher?

EDIT: A slightly more useful version of that idea is to always pass instances of implementations of the interface around in a suitably restrictive holder, like:

class Holder<T extends MyInterface<T>> {
    public final T value;
}

If someone gives you a Holder<Q>, then you know that Q must be a version of MyInterface bound to itself, which is what you're after.

查看更多
看我几分像从前
6楼-- · 2020-02-01 04:15

This is the exact situation faced by the Comparable interface (its compareTo method wants to take an argument the same type as the object it is called on). So what does it do? It's simply defined as Comparable<T>. The idea is that an implementing class "should" implement Comparable with itself as the parameter (allowing it to "compare to" itself); but this is not enforced (since there is no way to do it).

Yes, as you noted, this will allow any class to implement Comparable with a parameter of any other class: class Foo implements Comparable<Bar> where Foo and Bar have no relation to each other. However, this is not really a problem.

All the methods and classes (sorting, maximum, etc.) that require Comparable objects have the following generic type constraint <T extends Comparable<? super T>>. This ensures that objects of type T are comparable with themselves. That way, it is completely type-safe. So the enforcement is not made in the declaration of the Comparable interface, but in the places that use it.

(I notice that you use <T extends MyInterface<T>> while Comparable uses simply <T>. Although <T extends MyInterface<T>> will exclude cases where the type parameter does not implement MyInterface, it will not exclude cases where the type parameter does implement MyInterface, but is different than the class. So what's the point of half-excluding some cases? If you adopt Comparable's way of restricting it where they are used, it's type-safe anyway, so there is no point in adding more restrictions.)

查看更多
登录 后发表回答