方法链和继承不能很好地配合使用吗?

40

这个问题曾在C++语境下被问到过,但我对Java也很好奇。由于虚方法的限制不适用(我想),但如果你遇到了这种情况:

abstract class Pet
{
    private String name;
    public Pet setName(String name) { this.name = name; return this; }        
}

class Cat extends Pet
{
    public Cat catchMice() { 
        System.out.println("I caught a mouse!"); 
        return this; 
    }
}

class Dog extends Pet
{
    public Dog catchFrisbee() { 
        System.out.println("I caught a frisbee!"); 
        return this; 
    }
}

class Bird extends Pet
{
    public Bird layEgg() {
        ...
        return this;
    }
}


{
    Cat c = new Cat();
    c.setName("Morris").catchMice(); // error! setName returns Pet, not Cat
    Dog d = new Dog();
    d.setName("Snoopy").catchFrisbee(); // error! setName returns Pet, not Dog
    Bird b = new Bird();
    b.setName("Tweety").layEgg(); // error! setName returns Pet, not Bird
}
在这种类层次结构中,是否有任何方法可以返回this,而不会(实际上)上转换对象类型?

9
现在我明白为什么有那么多人讨厌Java了。 - Ionuț G. Stan
5个回答

59
如果您想避免编译器的未检查转换警告(并且不想使用@SuppressWarnings("unchecked")),则需要进行一些额外的操作:
首先,您定义的Pet必须是自引用的,因为Pet始终是泛型类型:
abstract class Pet <T extends Pet<T>>

其次,在setName中的(T) this强制转换也是未经检查的。为了避免这种情况,可以使用Angelika Langer博士的优秀泛型FAQ中的“getThis”技巧

"getThis"技巧提供了一种方法来恢复this引用的确切类型。

这将导致下面的代码,它编译和运行时没有警告。如果您想扩展您的子类,那么该技术仍然适用(尽管您可能需要泛型化您的中间类)。

结果代码如下:

public class TestClass {

  static abstract class Pet <T extends Pet<T>> {
    private String name;

    protected abstract T getThis();

    public T setName(String name) {
      this.name = name;
      return getThis(); }  
  }

  static class Cat extends Pet<Cat> {
    @Override protected Cat getThis() { return this; }

    public Cat catchMice() {
      System.out.println("I caught a mouse!");
      return getThis();
    }
  }

  static class Dog extends Pet<Dog> {
    @Override protected Dog getThis() { return this; }

    public Dog catchFrisbee() {
      System.out.println("I caught a frisbee!");
      return getThis();
    }
  }

  public static void main(String[] args) {
    Cat c = new Cat();
    c.setName("Morris").catchMice();
    Dog d = new Dog();
    d.setName("Snoopy").catchFrisbee();
  }
}

代码这样变得更加简洁,我会花些时间阅读完整的Angelika文章,非常感谢! - Aquarius Power
2
class Snake extends Pet<Cat> {@Override protected Cat getThis() {return new Cat();}} - user253751
3
当你有非抽象、非最终类并需要创建一个实例时,就会变得有些棘手。例如,假设你有一个static class Poodle extends Dog<Poodle>,并将Dog更改为static class Dog<T extends Dog<T>> extends Pet<T>。此时创建一个原始的Dog实例将会很困难。 - Marcus
有没有可能使用匿名类使其工作?我找不到让它自引用泛型T的方法 :/ - Aquarius Power
我创建了一个可用的东西:class PetAnnon extends Pet<PetAnnon>{},并且在每个匿名类中,我只需像这样使用它:new Pet<PetAnnon>{...,现在像 <T> T get(Class<T> cl){return (T)this.val;} 这样的方法将再次起作用。 - Aquarius Power

20
这个老技巧如何呢:
abstract class Pet<T extends Pet>
{
    private String name;
    public T setName(String name) { this.name = name; return (T) this; }        
}

class Cat extends Pet<Cat>
{
    /* ... */
}

class Dog extends Pet<Dog>
{
    /* ... */
}

+1,表达得比我更简洁。但考虑到Java泛型已经存在了很长时间,这个技巧可能有多老呢? - Steve B.
啊哈,我想到了泛型会有所帮助,只是不知道具体怎么做。谢谢! - Jason S
@Steve B:在Java中并不算老(实际上,我认为我从未见过它在Java中使用),但它在C++中已经被使用了很长时间。 - Rasmus Faber
https://dev59.com/mHVC5IYBdhLWcg3w51ny的实际用途 https://dev59.com/Umox5IYBdhLWcg3wi03F使用Java构建器模式来实现多态对象层次结构是否可行 - Ray Tayek
如果你有一个迭代器,那么可以这样做:public Iterator<T> iterator() { 这样你就不必为猫和狗分别实现一个迭代器,但是你仍然可以循环遍历猫和狗,并且实际上得到的是猫或狗,而不是宠物! - clankill3r
显示剩余2条评论

12

不是真的。你可以通过使用协变返回类型来解决这个问题(感谢 McDowell 提供正确的名称):

@Override
public Cat setName(String name) {
    super.setName(name);
    return this;
}

(如果这是你关心的问题,协变返回类型仅适用于 Java 5 及以上版本。)


5

虽然有些复杂,但您可以使用泛型来完成这个任务:

abstract class Pet< T extends Pet > {
    private String name;

    public T setName( String name ) {
        this.name = name;
        return (T)this;
    }

    public static class Cat extends Pet< Cat > {
        public Cat catchMice() {
            System.out.println( "I caught a mouse!" );
            return this;
        }
    }

    public static class Dog extends Pet< Dog > {
        public Dog catchFrisbee() {
            System.out.println( "I caught a frisbee!" );
            return this;
        }
    }

    public static void main (String[] args){
        Cat c = new Cat();
        c.setName( "Morris" ).catchMice(); // error! setName returns Pet, not Cat
        Dog d = new Dog();
        d.setName( "Snoopy" ).catchFrisbee(); // error! setName returns Pet, not Dog
    }

}

3
public class Pet<AnimalType extends Pet> {

private String name;
    public AnimalType setName(String name) {
       this.name = name; return (AnimalType)this; 
    }        
}

并且

public class Cat extends Pet<Cat> {

    public Cat catchMice() {return this;}

    public static void main(String[] args) {
        Cat c = new Cat().setName("bob").catchMice();
    }

}


@Steve B. - +1,你比我先说了! - Paul Adamson
编辑:复制/粘贴您的代码解决了问题。我意识到我的基类是 X extends Y 而不是 X extends Y<X>。这样就解决了! - fIwJlxSzApHEZIl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接