) process" /> ) process" /> ) process"/>

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

500 Views Asked by At

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

6

There are 6 best solutions below

0
alalalala On BEST ANSWER

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;
}
1
rioswarawan On

List type is only contains 1 class such us List<String> means the list of Strings. Same as your example, List<T> means the list of Ts.

I can suggest to create a super class Animal then add inheritance to Dog and Cat.

public class Animal {
    Integer legCount;
    Integer age;
}

public class Dog extends Animal {

    String name;

    public Dog(String name, int legCount, int age) {
        super(legCount, age);
        this.name = name;
    }
}

public class Cat extends Animal {
    String name;

    public Cat(String name, int legCount, int age) {
        super(legCount, age);
        this.name = name;
    }
}

add dog and cat variable into list of animals

Dog dog1 = new Dog("Joni", 4, 2);
Cat cat1 = new Cat("Ron", 4, 3);

List<Animal> animals = new ArrayList();
animals.add(dog1);
animals.add(cat1);

then in the foo function become

List<Puppy> puppies = new ArrayList();
List<Kitten> kittens = new ArrayList();

public void foo(List<Animal> animals) {
    for(animal in animals) {
        if(animals insntanceOf Dog) {
            Puppy puppy = ...
            puppies.add(puppy);
        }

        if(animals insntanceOf Kitten) {
            Kitten kitten = ...
            kittens.add(kitten);
        }
    }
}

Do one loop for all animals

0
tgdavies On

This is one way of doing something similar -- converting a list of a particular animal to a list of its young, in a type safe way.

It requires that you designate the type of an animals young as a type parameter when you declare the class:


import java.util.List;
public class Main {
    public static void main(String[] args) {
        List<Dog> dogs = List.of(new Dog(), new Dog());
        List<Cat> cats = List.of(new Cat(), new Cat());
        List<Puppy> puppies = createYoungList(dogs);
        List<Kitten> kittens = createYoungList(cats);
        //List<Puppy> kittens = createYoung(cats); compilation error
    }

    static <P extends Animal<Y>,Y> List<Y> createYoungList(List<P> parents) {
        return parents.stream().map(Animal::createYoung).toList();
    }

}

interface Animal<Y> {
    Y createYoung();
}

class Puppy {}
class Kitten {}

class Dog implements Animal<Puppy> {

    @Override
    public Puppy createYoung() {
        return new Puppy();
    }
}

class Cat implements Animal<Kitten> {

    @Override
    public Kitten createYoung() {
        return new Kitten();
    }
}

Adding a new implementation of Animal doesn't require any changes to createYoungList.

0
Phil Ku On

I tried to replicate your question. Here is one possible solution:

Create a Animal superclass and declare a map between classes:

private Map<Class<Animal>, Class<Animal>> classMap = Map.ofEntries(
  new AbstractMap.SimpleEntry(Cat.class, Kitten.class),
  new AbstractMap.SimpleEntry(Dog.class, Puppy.class)
);

Looping through map entries to handle the mapping of classes in foo:

for(Entry<Class<Animal>, Class<Animal>> entry : classMap.entrySet()){
    if (items.get(0).getClass() == entry.getKey()) {
        List list = items.stream().map(item -> {
            Animal animal = new Animal();
            try {
                // instantiate animal young
                animal = entry.getValue().getConstructor().newInstance();
            } catch (Exception e) {
                e.printStackTrace();
            }
            animal.setName(item.getName());
            // mapping values ...
            return animal;
        }).collect(Collectors.toList());

        return list;
    }
}

Whenever you need to add more animal classes, just add them to the classmap:

private Map<Class<Animal>, Class<Animal>> classMap = Map.ofEntries(
  new AbstractMap.SimpleEntry(Cat.class, Kitten.class),
  new AbstractMap.SimpleEntry(Dog.class, Puppy.class),
  new AbstractMap.SimpleEntry(Cow.class, Calf.class)
);

Hope it is of some help.

0
Stone On

I have an idea: register the mapper function by source class first. For a new source class, you only need to register a new mapper function. One disadvantage is: there is an explicit Class Cast for getting the target list. I have no idea how to remove it.

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

public class Test {

    public static void main(String[] args) {
        List<Dog> dogs = Arrays.asList(new Dog("d1"), new Dog("d2"));
        Map<Class, Function> register = new HashMap();
        register.put(Dog.class, (Function<Dog, Puppy>) dog -> new Puppy(dog.getName()));
        List<Puppy> puppies = (List<Puppy>) dogs.stream().map(register.get(Dog.class)).collect(Collectors.toList());
        System.out.println("puppies:" + puppies);
    }
}

class Dog {
    private String name;

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

    public String getName() {
        return name;
    }
}

class Puppy {
    private String name;

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

    public void setName(String name) {
        this.name = name;
    }

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

0
Bohemian On

I'm not sure what your intention is, but hopefully this will help you get there.

One way to return a typed list from a typed method is to pass a type token:

private List<Object> items;

public <T> List<T> getAll(Class<T> clazz) {
    List<T> result = new ArrayList<>();
    for (Object obj : items) {
        if (clazz.isInstance(obj)) {
            result.add((T)obj);
        }
    }
    return result;
}

The type is inferred when called:

List<Dog> dogs = myClass.getAll(Dog.class);