javagenericsstrategy-patterngeneric-type-parameters

Question on diamond operator for design pattern strategy


Small question regarding the diamond operator and design pattern strategy for Java, please.

I would like to implement a very specific requirement:

Therefore, I went to try with a strategy pattern, where each of the strategies is a different way to store, I think this pattern is quite lovely.

The code is as follows:


public class MyThingToStore {

    private final String name;

    public MyThingToStore(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        MyThingToStore that = (MyThingToStore) o;
        return Objects.equals(name, that.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }

    @Override
    public String toString() {
        return "MyThingToStore{" +
                "name='" + name + '\'' +
                '}';
    }

}

public class MyStorage {

    private final StorageStrategy storageStrategy;

    public MyStorage(StorageStrategy storageStrategy) {
        this.storageStrategy = storageStrategy;
    }

    public void addToStore(MyThingToStore myThingToStore) {
        storageStrategy.addToStore(myThingToStore);
    }

    public int getSize() {
        return storageStrategy.getSize();
    }

}

public interface StorageStrategy {

    void addToStore(MyThingToStore myThingToStore);

    int getSize();

}


public class StorageUsingArrayListStrategy implements StorageStrategy {

    private final List<MyThingToStore> storeUsingArrayList = new ArrayList<>();

    @Override
    public void addToStore(MyThingToStore myThingToStore) {
        storeUsingArrayList.add(myThingToStore);
    }

    @Override
    public int getSize() {
        return storeUsingArrayList.size();
    }

}


public class StorageUsingHashSetStrategy implements StorageStrategy{

    private final Set<MyThingToStore> storeUsingHashSet = new HashSet<>();

    @Override
    public void addToStore(MyThingToStore myThingToStore) {
        storeUsingHashSet.add(myThingToStore);
    }

    @Override
    public int getSize() {
        return storeUsingHashSet.size();
    }

}


public class Main {

    public static void main(String[] args) {
        final StorageStrategy storageStrategy = new StorageUsingArrayListStrategy();
        final MyStorage myStorage = new MyStorage(storageStrategy);
        myStorage.addToStore(new MyThingToStore("firstItem"));
        myStorage.addToStore(new MyThingToStore("duplicatedSecondItem"));
        myStorage.addToStore(new MyThingToStore("duplicatedSecondItem"));
        System.out.println(myStorage.getSize()); //changing strategy will return a different size, working!
    }
}

And this is working fine, very happy, especially tackled the requirement "easy to change the data structure to do the actual store".

(By the way, side question, if there is an even better way to do this, please let me know!)

Now, looking online at different implementations of strategy patterns, I see this diamond operator which I am having a hard time understanding:


MyThingToStore stays the same.


public class MyStorage {

    private final StorageStrategy<MyThingToStore> storageStrategy; //note the diamond here

    public MyStorage(StorageStrategy<MyThingToStore> storageStrategy) {
        this.storageStrategy = storageStrategy;
    }

    public void addToStore(MyThingToStore myThingToStore) {
        storageStrategy.addToStore(myThingToStore);
    }

    public int getSize() {
        return storageStrategy.getSize();
    }

    @Override
    public String toString() {
        return "MyStorage{" +
                "storageStrategy=" + storageStrategy +
                '}';
    }

}


public interface StorageStrategy<MyThingToStore> {
    //note the diamond, and it will be colored differently in IDEs
    void addToStore(MyThingToStore myThingToStore);

    int getSize();

}

public class StorageUsingArrayListStrategy implements StorageStrategy<MyThingToStore> {

    private final List<MyThingToStore> storeUsingArrayList = new ArrayList<>();

    @Override
    public void addToStore(MyThingToStore myThingToStore) {
        storeUsingArrayList.add(myThingToStore);
    }

    @Override
    public int getSize() {
        return storeUsingArrayList.size();
    }

}


public class StorageUsingHashSetStrategy implements StorageStrategy<MyThingToStore> {

    private final Set<MyThingToStore> storeUsingHashSet = new HashSet<>();

    @Override
    public void addToStore(MyThingToStore myThingToStore) {
        storeUsingHashSet.add(myThingToStore);
    }

    @Override
    public int getSize() {
        return storeUsingHashSet.size();
    }

}

public class Main {

    public static void main(String[] args) {
        final StorageStrategy<MyThingToStore> storageStrategy = new StorageUsingArrayListStrategy();
        final MyStorage myStorage = new MyStorage(storageStrategy);
        myStorage.addToStore(new MyThingToStore("firstItem"));
        myStorage.addToStore(new MyThingToStore("duplicatedSecondItem"));
        myStorage.addToStore(new MyThingToStore("duplicatedSecondItem"));
        System.out.println(myStorage.getSize()); //changing strategy will return a different size, working!
    }
}


And both versions will yield the same good result, also be able to answer requirements.

My question is: what are the differences between the version without a diamond operator, and the version with the diamond operator, please?

Which of the two ways are "better" and why?

While this question might appear to be "too vague", I believe there is a reason for a better choice.


Solution

  • I think the confusion comes from how you named type parameter for StorageStrategy in your 2nd example.

    Let's name it T for type instead. T in this case is just a placeholder to express what type of objects your StorageStrategy can work with.

    public interface StorageStrategy<T> {
        void addToStore(T myThingToStore);
        int getSize();
    }
    

    E.g.

    StorageStrategy<MyThingToStore> strategy1 = // Initialization 
    StorageStrategy<String> strategy2 = // Initialization 
    strategy1.addToStore(new MyThingToStore("Apple"));
    // This works fine, because strategy2 accepts "String" instead of "MyThingToStore"
    strategy2.addToStore("Apple");
    // Last line doesn't work, because strategy1 can only handle objects of type "MyThingToStore"
    strategy1.addToStore("Apple");
    

    To make it work properly, you need to change your different StorageStrategy implementations to also include the type parameter.

    public class StorageUsingHashSetStrategy<T> implements StorageStrategy<T> {
    
        private final Set<T> storeUsingHashSet = new HashSet<>();
        @Override
        public void addToStore(T myThingToStore) {
            storeUsingHashSet.add(myThingToStore);
        }
    
        @Override
        public int getSize() {
            return storeUsingHashSet.size();
        }
    
    }
    

    And lastly you also want to have a type paremeter for MyStorage

    public class MyStorage<T> {
    
        private final StorageStrategy<T> storageStrategy;
    
        public MyStorage(StorageStrategy<T> storageStrategy) {
            this.storageStrategy = storageStrategy;
        }
    
        public void addToStore(T myThingToStore) {
            storageStrategy.addToStore(myThingToStore);
        }
    
        public int getSize() {
            return storageStrategy.getSize();
        }
    
    }
    

    Now you can create a MyStorage and can use it to store essentially any object into it and not just MyThingToStore. Whether that is something you want or not is up to you.