Java:接口和工厂中的泛型

4

我有一个基类Plant,从中分为两个子类型路径。 Fruit extends PlantVegetable extends PlantFruitVegetable也是抽象类,然后它们进一步有其他类,比如Banana extends FruitSpinach extends Vegetable

现在假设需要通过这些类进行操作,并且有一个用于操作的接口。 当前的设计如下:

我有一个接口:

abstract class Plant {}
abstract class Fruit extends Plant {}
class Banana extends Fruit {}

public interface PlantWeigher<T extends Plant> {    
    public int weight(T fruit);    
}

接下来我有:


public abstract class FruitWeigher<Y extends Fruit> implements PlantWeigher<Y> { }

然后我有一个具体的类:
public class BananaWeigher extends FruitWeigher<Banana> {
  public int weight(Banana fruit);    
}

现在,作为一个只懂Plant的用户,我希望获取正确的Weigher实例并执行操作。

因此,工厂如下:

public class WeigherFactory {    
   public static PlantWeigher<? extends Plant> getInstance(Plant plant){
     // based on some logic on Plant argument, I return, lets say:
      return new BananaWeigher();    
   }
}

因此,在用户类中,我调用了以下函数:

PlantWeigher<? extends Plant> instance = WeigherFactory.getInstance(userPlant);
instance.weight(plant);  // THIS LINE GIVES AN ERROR

请问有人能帮我理解这里的设计问题,并给出解决方案吗?

谢谢。


7
请问是什么异常? - HungPV
1
“plant”和“userPlant”是如何定义的? - Ian2thedv
1
这段代码能编译吗? - raduation
1
为什么这个问题有7个赞?它的表述非常不清楚。 - Keppil
我已经踩了,这个无法编译。 - Derrops
显示剩余4条评论
3个回答

3

这句话的意思是:

 PlantWeigher<? extends Plant> instance = WeigherFactory.getInstance(userPlant);

意思是,“我有一个可以称量某些子类Plant的秤,而不是所有种类的植物。”例如,如果工厂返回一个BananaWeigher,尝试用它来称量Tomato是没有意义的。 要实现您想要的结果,应按以下方式更改工厂:
public class WeigherFactory{

   public static <P extends Plant> PlantWeigher<P> getInstance(P plant){
    // should return a correct Weigher for the subttype of Plant P
            return ...
  }
}

此时,调用代码将会返回如下:
 PlantWeigher<Banana> instance =WeigherFactory.getInstance(banana);
 instance.weigh(banana)

现在你有一个特定的 PlantWeigher,你可以使用特定类型来调用它。 或者,如果您希望能够称量任何类型的PlantPlantWeigher,那么您应该更改后者:
 public interface PlantWeigher {
    public int weight(Plant p);
}

这确保了 PlantWeigher 可以称量任何计划,无论它是否合理。

但问题在于用户不知道它是什么类型的植物。它只有类型为Plant的对象。 - Sunny
1
不幸的是,WeigherFactory 仍然无法编译,因为 BananaWeigher 不能保证是 PlantWeigher<P> 的子类。例如,如果 PSpinach - Dunes
@Sunny,如果他想这样做,我添加了一个“OR”。 - Chirlo
@Dunes,由于他获得了一个P的实例,他无法确定P的确切类型并进行安全转换,但代码可以编译通过。 - Chirlo
@Chirlo 但是第二种方法的问题在于当函数被调用weight(plant)时。这里的对象只能看到与植物类相关的细节,为了访问香蕉函数,我必须进行类型转换。 - Sunny
@sunny,是的,但他需要选择其中之一。要么使用所有可用信息称量特定种类的植物,要么使用通用信息称量通用植物。 - Chirlo

2
您的错误可能类似于类型PlantWeigher<capture#2-of ? extends Plant>中的方法weight(capture#2-of ? extends Plant)不适用于参数(Banana)
您不能使用一个 Banana 与具有方法 int weight(T fruit)PlantWeigher<? extends Plant>。实际上,PlantWeigher<? extends Plant> 可能是 PlantWeigher<? extends Orange>,而这显然无法称量香蕉。
最简单的解决方法是将您的工厂方法更改为:
public static <T extends Plant> PlantWeigher<T> getInstance(T plant) {

并像这样使用它:

PlantWeigher<Plant> instance = WeigherFactory.getInstance(myBanana);
instance.weight(myBanana); // No more error

这实际上都涉及到了“PECS原则”。我建议您阅读《Java教程》中的通配符用法指南以及Java泛型:什么是PECS
一般而言:
  • 生产者从方法返回数据并可以使用? extends X
  • 消费者通过方法参数接受数据并可以使用? super X
一般而言,您不能在具有类型参数的参数的方法中使用上限通配符类型参数(extends)。在这种情况下,您可以向instance.weight发送的唯一有效值是null
instance.weight(null);  // No more error

具有上界(extends)通配符的类型参数是“out”变量。这是因为编译器最多只能保证它是其边界的某个子类型。在这种情况下,边界是Plant。因此,您可以通过在同一PlantWeigher<? extends Plant>上使用T getTestPortion()方法来获取Banana,只要它最初设置为PlantWeigher<Banana>
如果您使用下限通配符,则编译器可以保证类型参数是Plant的某个超类型,因此它可以称重BananaOrange
不幸的是,PlantWeigher<Banana>PlantWeigher<Orange>没有共同的子类型可用于您 - 但这本身就表明您的设计存在问题。
†对于方法返回变量的类型
‡对于方法参数的类型

1
问题在于你的代码中有些部分使用了泛型,而其他部分则没有。你可以采用不安全的方式,从一个方法返回不同类型的称重器。然而,这样做会失去使用泛型时得到的类型安全性。例如:
Plant banana = ...;
Plant spinach = ...;
Weigher<Plant> bananaWeigher = WeigherFactory.getInstance(banana);
bananaWeigher.weigh(spinach); // Compiles, but will produce a runtime error!

另一种方法是使WeigherFactory针对每个具体的Plant类具有不同的方法,并使使用WeigherFactory的代码也具有通用性。例如:
public class WeigherFactory {
   public static BananaWeigher getInstance(Banana banana) {
      return new BananaWeigher();
   }
   public static SpinachWeigher getInstance(Spinach spinach) {
      return new SpinachWeigher();
   }
}

public abstract class FactoryUser<P extends Plant> {
    /**
     * Method that knows how to call the appropriate WeigherFactory.getInstance method
     * Defined by concrete implementations that know the full plant type.
     */
    public abstract Weigher<P> getWeigher(P plant);

    // common code for all FactoryUsers
    public void run(P plant) {
        Weigher<P> weigher = getWeigher(plant);
        int weight = weigher.weigh(plant);
        System.out.println("the plant weighs: " + weight);
    }
}

public class Main {
    public static void main(String[] args) {
        Banana banana = new Banana();
        FactoryUser<Banana> factoryUser = new FactoryUser<Banana>() {
            // We know the generic type to be Banana now, so we can call the 
            // appropriate WeigherFactory.getInstance method.
            // This technique is known as double dispatch
            @Override
            public BananaWeigher getWeigher(Banana banana) {
                return WeigherFactory.getInstance(banana);
            }
        };
        factoryUser.run(banana);    
    }
}

当你不知道植物的实际类型时(访问者模式)

我们需要一个PlantWeigher来称量所有类型的Plant。如果需要,它可以委托给专门的称量器。但是我们需要一个中央称量器,因为您将无法返回特定的称量器并以类型安全的方式使用它。中央称量器将使用访问者模式来分派每种植物的正确称量方法。

首先,您需要一个Visitor接口和该类的实现,该实现知道如何处理每个具体的植物类。例如:

public interface Visitor<T> {
    public T visit(Banana banana);
    public T visit(Spinach spinach);
}

public class WeighingVisitor implements Visitor<Integer> {
    @Override
    public Integer visit(Banana banana) {
        return banana.getBananaWeight();
    }
    @Override
    public Integer visit(Spinach spinach) {
        return spinach.getSpinachWeight();
    }   
}

public class PlantWeigher {    
    public int weigh(Plant plant) {
        return plant.accept(new WeighingVisitor());
    }
}

其次,您需要修改 Plant 基类以定义一个抽象方法,该方法接受一个 Visitor。只有 Plant 的具体实现需要实现此方法。
public interface Plant {
    public <T> T accept(Visitor<T> visitor);
}

public class Banana implements Fruit {
    private int weight;
    public Banana(int weight) {
        this.weight = weight;
    }
    public int getBananaWeight() {
        return weight;
    }
    @Override
    public <T> T accept(Visitor<T> visitor) {
        return visitor.visit(this); // the implementation is always exactly this
    }
}

最后,如何使用所有这些:

List<Plant> plants = asList(new Banana(10), new Spinach(20));
PlantWeigher weigher = new PlantWeigher();

int weight = 0;
for (Plant p : plants) {
    weight += weigher.weigh(p);
}

System.out.println("total weight: " + weight); // expect 30

感谢您的答复。唯一的问题是用户不知道确切的类型,只知道对象是超类型植物。因此,我希望工厂以植物类型作为输入,然后根据其返回称重器。 - Sunny
问题在于您需要以类型安全的方式使用 Weigher。但是,您需要它接受一个 Plant 实例,这不是类型安全的。这就是问题所在。因此,您必须编写一个 weigher,在调度正确的方法称重植物之前对植物进行运行时检查。请参见 https://sites.google.com/site/steveyegge2/when-polymorphism-fails -- "当多态行为确实是目标行为时,多态性才有意义。当它是观察者 [weigher] 的行为时,您需要运行时类型。" - Dunes
虽然您可以考虑使用访问者模式,但这需要您更改各种Plant实现--这可能是不可取的或不可能的。https://en.wikipedia.org/wiki/Visitor_pattern - Dunes

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