Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java: Instantiate a generic type by returning a Supplier vs returning a new instance

I was reading how to instantiate a generic and after reading and applying this answer; I would like to know what would be the differences between expecting a Supplier<T> vs. expecting a new instance of T.

Example:

abstract class AbstractService<T extends AbstractEntity> {
    protected Supplier<T> makeNewThing();  // supplier is expected

    public T myMethod(){
        T object = makeNewThing().get(); // local object by calling supplier
        object.doStuff();
        return object;
    }
}

class CarService extends AbstractService<Car> {
    public Supplier<Car> makeNewThing(){
        return Car::new;
    }
}

vs.

abstract class AbstractService<T extends SomeAbstractEntity> {
    protected T makeNewThing();  // object is expected, newness is assumed

    public T myMethod(){
        T object = makeNewThing(); // local object by calling constructor
        object.doStuff();
        return object;
    }
}

class CarService extends AbstractService<Car> {
    public Car makeNewThing(){
        return new Car();
    }
}

The only thing I can think of is that expecting a supplier ensures that a new object will be created, but when expecting an object we can only assume that the implementing classes are calling the constructor and not re-using an existing instance.

I'd like to know of other objective differences and possible use cases, if any. Thanks in advance.

like image 846
JoseHdez_2 Avatar asked Nov 13 '17 12:11

JoseHdez_2


Video Answer


3 Answers

Using a Supplier postpones the creation of the instance.

This means that you might avoid a creation of an unnecessary instance.

For example, suppose you pass the output of makeNewThing() to some method.

public void makeNewThingSometimes (T newInstance)
{
    if (someCondition) {
        this.instance = newInstance;
    }
}

public void makeNewThingSometimes (Supplier<T> supplier)
{
    if (someCondition) {
        this.instance = supplier.get();
    }
}

Calling the first variant requires creating an instance of T even if you are not going to use it.

Calling the second variant only creates an instance of T when necessary.

Using a Consumer can save both storage (if the create instance requires a significant amount of memory) and time (if the execution of the constructor is expansive).

like image 117
Eran Avatar answered Dec 04 '22 07:12

Eran


The only thing I can think of is that expecting a supplier ensures that a new object will be created,

Not necessarily.
You implement the Supplier in this way :

 return SomeEntityImplementation::new;

But you could have implemented it in this other way :

 if (myCachedObject != null){
    return (()-> myCachedObject);
 }
 return SomeEntityImplementation::new;

Both ways may be used to return a cached object or create a new one.


One of Supplier advantages is the case of Supplier creating an object : this one is actually created only as the Supplier.get() method is invoked.

Note that in your example, using Supplier doesn't bring any advantage as in both cases (with or without Supplier) the object creation is already performed in a lazy way : as the factory method is invoked.
To take advantage of it, you should have a method that provides a Supplier<T> as parameter as in the Eran and Dasblinkenlight examples.


Another Supplier advantage is its ability to implement factory that may return multiple of things.
Using Supplier allows to have a shorter and more readable code and besides that doesn't rely on Java Reflection.

Supposing that you want to create the object from an Enum value, you could so write :

public enum MyBaseClassFactory {

  ENUM_A (A::new),
  ENUM_B (B::new),
  ENUM_C (C::new),
  ENUM_D (D::new);

  private Supplier<BaseClass> supplier;

  MyBaseClassFactory (Supplier<BaseClass> supplier){
    this.supplier = supplier;
  }

  public BaseClass createObject(){
       return supplier.get();
  }
}

You could so use it :

BaseClass base = MyBaseClassFactory.ENUM_A.createObject();

Without Supplier, you will have to use Reflection (that may fail at runtime) or write a verbose and unmaintainable code.

For example with Reflection :

public enum MyEnumFactoryClass {

    ENUM_A(A.class), ENUM_B(B.class), ENUM_C(C.class), ENUM_D(D.class);

    private Class<BaseClass> clazz;

    MyEnumFactoryClass(Class<BaseClass> clazz) {
       this.clazz = clazz;
    }

    public BaseClass createObject() {
       return clazz.newInstance();
    }

}

For example without reflection but with more verbose code :

public enum MyEnumFactoryClass {

  ENUM_A {
     @Override
     public BaseClass createObject() {
        return new A();
     }
    },
    ENUM_B {
     @Override
     public BaseClass createObject() {
        return new B();
     }
    },
    ENUM_C {
    @Override
     public BaseClass createObject() {
        return new C();
     }
    },
    ENUM_D {
    @Override
     public BaseClass createObject() {
        return new D();
     }
    };
    public abstract BaseClass createObject();

}

You could of course take advantage in a close way of Supplier by using it with a Map<String, Supplier<BaseClass>>.

like image 23
davidxxx Avatar answered Dec 04 '22 07:12

davidxxx


The first solution is more flexible, because an extra level of indirection in object creation lets users of your class library change the source of new items independently of ServiceImpl<SomeEntityImplementation> class.

You can make a new Supplier<T> instance without subclassing or recompiling ServiceImpl, because there is an extra level of indirection. ServiceImpl could be implemented as follows:

class ServiceImpl<SomeEntityImplementation> {
    private final Supplier<SomeEntityImplementation> supplier;
    public Supplier<T> makeNewThing(){
        return supplier;
    }
    public ServiceImpl(Supplier<SomeEntityImplementation> s) {
        supplier = s;
    }
}

This makes it possible for users of ServiceImpl to provide their own Supplier<T>, which is not possible using the second approach, in which the source of new items is merged into the implementation of service itself.

like image 26
Sergey Kalinichenko Avatar answered Dec 04 '22 07:12

Sergey Kalinichenko