简体   繁体   中英

Java: Generics in interface and factory

I have a base class Plant from which there are two subtype paths. Fruit extends Plant and Vegetable extends Plant. Fruit and Vegetable are also abstract classes and then they further have other classes like Banana extends Fruit and Spinach extends Vegetable .

Now lets say for example, an operation has to be operation via these classes and there is an interface for the operation. The current design is as follows:

I have an interface:

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

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

Then I have:

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

Then further I have the concrete class:

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

Now, as the user who just understands Plant, I would like to get the correct instance of the Weigher and then perform the operation.

So the Factory is as follows:

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

So in the user class, I call the following function:

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

Could some one please help me understand what is wrong with the design here and also could some one please advice a solution.

Thanks.

Your error is presumably something like The method weight(capture#2-of ? extends Plant) in the type PlantWeigher<capture#2-of ? extends Plant> is not applicable for the arguments (Banana) The method weight(capture#2-of ? extends Plant) in the type PlantWeigher<capture#2-of ? extends Plant> is not applicable for the arguments (Banana) .

You can't use a Banana with a PlantWeigher<? extends Plant> PlantWeigher<? extends Plant> which has a method int weight(T fruit) . The PlantWeigher<? extends Plant> PlantWeigher<? extends Plant> could in fact be a PlantWeigher<? extends Orange> PlantWeigher<? extends Orange> , which presumably can't weigh Banana s.

Your simplest solution is to change your factory method to:

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

and use it like this:

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

This is really all about the PECS principle . I recommend you read The Java Tutorials > Guidelines for Wildcard Use and Java Generics: What is PECS .

In general:

  • Producers return data from a method and can use ? extends X ? extends X
  • Consumers accept data into a method ( via a method parameter) and can use ? super X ? super X

In general you cannot use an upper-bounded wildcard type argument ( extends ) with a method that has a parameter with the type of the type argument. The only valid value you can send to your instance.weight in this case is null .

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

A type parameter which is invoked with a wildcard with an upper (extends) bound is an "out" variable. This is because the compiler can, at best, guarantee it is some subtype of its bound. In this case the bound is Plant . You could therefore get a Banana from a method T getTestPortion() on that same PlantWeigher<? extends Plant> PlantWeigher<? extends Plant> , as long as it was originally set with a PlantWeigher<Banana> .

If you were to use a lower bounded wildcard, then the compiler could guarantee that the type argument is some super type of Plant , and so it could weigh either Banana or Orange .

Unfortunately for you a PlantWeigher<Banana> and PlantWeigher<Orange> don't share some common subtype that you can use - but this does in itself indicate a problem with your design.

For the type of the method return variable

For the type of the method parameter

The line :

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

means, "I have a weigher that can weight some subclass of Plant , NOT all kinds of plants". For example, if the factory would return a BananaWeigher , it wouldn't make sense to try to weight a Tomato with this. To achieve what you want you should change the Factory as follows:

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

Then the calling code would return be like:

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

Now you have a specific PlantWeigher and you can call it with a specific type

OR , if you want to be able to weigh any kind of Plant with any kind of PlantWeigher , you should then change the latter:

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

This ensures that a PlantWeigher can weight any plan, wether that makes sens or not.

The problem is in some parts of your code you use generics, and in other parts you do not. You can go the unsafe way and have one factory method that returns different types of weigher from one method. However, you'll lose the type safety you get from making your weighers generic. eg.

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

The other way is to make WeigherFactory have different methods for each concrete Plant class and to make the code that uses the WeigherFactory generic as well. eg

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

When you don't know the actual type of the Plant (the Visitor Pattern)

We will need one PlantWeigher that knows how to weigh all types of Plant . It can delegate to specialised weighers if need be. But we need a central weigher as you will not be able to return a specific weigher and use it in a type-safe way. The central weigher will use the Visitor Pattern to dispatch the correct method for weighing each type of plant.

First you need a Visitor interface and an implementation of that class that knows what to do with each concrete plant class. eg.

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

Secondly, you will need to modify the Plant base class to define an abstract method that accepts a Visitor . Only concrete implementations of Plant need to implement this method.

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

Finally, how to use all 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

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