javaarraysoopdesign-patternsvisitor-pattern

How do I make my method return different types of list base on the input type of list?


I want to create a mapping from input List<T> to outputList<S>, for the method named foo(List<T>), but in a "smart" way . The way foo(List<T>) processes different types of list are very similar in nature since the input list shares the same attributes, but are from different classes.

My intention is to reuse as much of the foo() implementation with just a minor check of the input type before returning the output.

One way to do this is to implement something like the following in foo

if (items.get(0) instanceOf Cat) {
    List<Kitten> kittens = items.stream().map(cat -> Kitten.builder().name(cat.getName()).build().toList();
    return kittens;
}
if (items.get(0) instanceOf Dog) {
    List<Puppy> puppies = items.stream().map(dog -> Puppy.builder().name(dog.getName()).build().toList();
    return puppies;
}

, but it feels wrong since if I added another type like Bird I would have to add another if condition.

I suppose that another way to accomplish this if I wanted to have different return types for different types of input is by creating custom classes for a list of a specific type, i.e.

class DogList {
    private List<Dog> dogs;
}

class CatList {
    private List<Cat> cats;
}

class KittenList {
    private List<Kitten> kittens;
}

class PuppyList {
    private List<Puppy> puppies;
}

// And creating method for each type that's sth like
public KittenList foo(CatList cats) {
    List<Kitten> kittens = cats.getCats().stream().map(cat -> 
    Kitten.builder().name(cat.getName()).build().toList();
    return kittens;
}
public PuppyList foo(DogList dogs) {
    List<Puppy> puppies = dogs.getCats().stream().map(cat -> 
    Puppy.builder().name(dogs.getName()).build().toList();
    return puppies;
}

But it feels weird doing it this way since I'm creating custom classes just to wrap a list. I also am duplicating 99% of the implementation of foo.. and the implementation is almost identical here, so I would prefer to reuse the same method..


Solution

  • First, we need to ensure that Cat, Dog, Kitten and Puppy have a common parent class Animal

    class Animal {
        private String name;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
    class Dog extends Animal {
        private String name;
        public Dog(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
    class Cat extends Animal {
        private String name;
        public Cat(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
    class Kitten extends Animal {
        private String name;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            return "Kitten{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    class Puppy extends Animal {
        private String name;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            return "Puppy{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    

    Then use generic methods like this:

    public static <R extends Animal, T extends Animal> List<T> foo(List<R> source, Class<T> target) {
    
        return source.stream().map(v -> {
            T t = null;
            try {
                t = target.newInstance();
                t.setName(v.getName());
            } catch (InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
            }
            return t;
        }).collect(Collectors.toList());
    }
    

    Specify two parameters, the source list List<R> source, and the target type Class<T> target.

    We need to constrain the type T and type R, because inside the method, we need to call the corresponding setName, getName method, if it is any type, then we cannot be sure that they have setName, getName method, how to ensure this? Very simple, if T and R are required to be subtypes of Animal, they can be guaranteed to have setName and getName methods.So we add constraints to the generic method <R extends Animal, T extends Animal>.

    In the method implementation, we convert each element (R) in the source list to the target type (T).

    If you add a new type, you only need to add the corresponding type description class.

    The following is the complete test code, you need to understand and deal with the details of the code yourself:

    public class StackOverflow {
    
        public static void main(String[] args) {
            List<Dog> dogs = new ArrayList<Dog>(){{
                add(new Dog("dog 1"));
                add(new Dog("dog 2"));
                add(new Dog("dog 3"));
                add(new Dog("dog 4"));
            }};
            List<Cat> cats = new ArrayList<Cat>(){{
                add(new Cat("cat 1"));
                add(new Cat("cat 2"));
                add(new Cat("cat 3"));
            }};
            DogList dogList = new DogList();
            dogList.setDogs(dogs);
            CatList catList = new CatList();
            catList.setCats(cats);
    
            List<Puppy> puppies = foo(dogList.getDogs(), Puppy.class);
            System.out.println(puppies);
    
            List<Kitten> kittens = foo(catList.getCats(), Kitten.class);
            System.out.println(kittens);
        }
    
        public static <R extends Animal, T extends Animal> List<T> foo(List<R> source, Class<T> target) {
    
            return source.stream().map(v -> {
                T t = null;
                try {
                    t = target.newInstance();
                    t.setName(v.getName());
                } catch (InstantiationException | IllegalAccessException e) {
                    e.printStackTrace();
                }
                return t;
            }).collect(Collectors.toList());
        }
    }
    
    class Animal {
        private String name;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
    class Dog extends Animal {
        private String name;
        public Dog(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
    class Cat extends Animal {
        private String name;
        public Cat(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
    class Kitten extends Animal {
        private String name;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            return "Kitten{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    class Puppy extends Animal {
        private String name;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            return "Puppy{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    
    class DogList {
        private List<Dog> dogs;
    
        public List<Dog> getDogs() {
            return dogs;
        }
        public void setDogs(List<Dog> dogs) {
            this.dogs = dogs;
        }
    }
    
    class CatList {
        public List<Cat> getCats() {
            return cats;
        }
        public void setCats(List<Cat> cats) {
            this.cats = cats;
        }
        private List<Cat> cats;
    }