Java: 接口和工厂中的泛型

Java: Generics in interface and factory

我有一个基础 class Plant,其中有两个子类型路径。 Fruit extends PlantVegetable extends Plant. FruitVegetable 也是抽象的 classes,然后它们还有其他 classes,如 Banana extends FruitSpinach extends Vegetable

现在让我们举个例子,一个操作必须通过这些classes来操作,并且有一个操作接口。 目前设计如下:

我有一个界面:

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> { }

然后我有具体的 class:

public class BananaWeigher extends FruitWeigher<Banana> {
  public int weight(Banana fruit);    
}

现在,作为刚刚了解Plant的用户,我想获取正确的Weigher实例,然后进行操作。

所以Factory如下:

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();    
   }
}

所以在用户class中,我调用了下面的函数:

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

有人可以帮助我理解这里的设计有什么问题吗,也有人可以建议一个解决方案。

谢谢。

您的错误大概类似于 The method weight(capture#2-of ? extends Plant) in the type PlantWeigher<capture#2-of ? extends Plant> is not applicable for the arguments (Banana).

您不能将 Banana 与具有方法 int weight(T fruit)PlantWeigher<? extends Plant> 一起使用。 PlantWeigher<? extends Plant> 实际上可能是一个 PlantWeigher<? extends Orange>,大概不能称重 Bananas。

最简单的解决方案是将工厂方法更改为:

public static <T extends Plant> PlantWeigher<T> getInstance(T plant) {

并像这样使用它:

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

这就是 PECS 原则。我建议您阅读 The Java Tutorials > Guidelines for Wildcard Use and Java Generics: What is PECS

总的来说:

  • Producers return data from a method and can use ? extends X
  • Consumers 将数据接受到方法中(通过 方法参数)并且可以使用 ? super X

通常,您不能将上限通配符类型参数 (extends) 与具有类型参数类型的参数的方法一起使用。在这种情况下,您可以发送到 instance.weight 的唯一有效值是 null.

instance.weight(null);  // No more error

使用带上(扩展)边界的通配符调用的类型参数是一个 "out" 变量。这是因为编译器充其量只能保证它是其绑定的某个子类型。在这种情况下,边界是 Plant。因此,您可以从同一个 PlantWeigher<? extends Plant> 上的方法 T getTestPortion() get Banana,只要它最初设置为 PlantWeigher<Banana> ].

如果您要使用下界通配符,那么编译器可以保证类型参数是 Plant 的某种超类型,因此它可以权衡 BananaOrange.

不幸的是,PlantWeigher<Banana>PlantWeigher<Orange> 不共享一些您可以使用的公共子类型 - 但这本身表明您的设计存在问题。

对于方法的类型return变量

对于方法参数的类型

行:

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

表示,"I have a weigher that can weight some subclass of Plant, NOT all kinds of plants"。例如,如果工厂将 return a 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 ...
  }
}

那么调用代码 return 就像:

 PlantWeigher<Banana> instance =WeigherFactory.getInstance(banana);
 instance.weigh(banana)

现在你有一个特定的 PlantWeigher 并且你可以用特定的类型调用它

,如果你想权衡任何一种Plant和任何一种PlantWeigher,你应该改变后者:

 public interface PlantWeigher {
    public int weight(Plant p);
}

这确保 PlantWeigher 可以对任何计划进行加权,无论是否有意义。

问题在于您的代码的某些部分使用了泛型,而在其他部分则没有。您可以采取不安全的方式,并使用一种工厂方法 return 从一种方法获得不同类型的称重器。但是,您将失去通过使您的称重器通用而获得的类型安全。例如

Plant banana = ...;
Plant spinach = ...;
Weigher<Plant> bananaWeigher = WeigherFactory.getInstance(banana);
bananaWeigher.weigh(spinach); // Compiles, but will produce a runtime error!

另一种方法是使 WeigherFactory 对每个具体 Plant class 具有不同的方法,并使使用 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);    
    }
}

当您不知道 Plant 的实际类型(访问者模式)时

我们需要一个知道如何权衡所有类型 PlantPlantWeigher。如果需要,它可以委托专门的称重人员。但是我们需要一个中央称重器,因为您将无法 return 一个特定的称重器并以类型安全的方式使用它。中央称重器将使用 Visitor Pattern 发送正确的方法称量每种植物。

首先,您需要一个 Visitor 接口和一个 class 的实现,它知道如何处理每个混凝土厂 class。例如

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 基础 class 以定义接受 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