Alex
Alex

Reputation: 752

Is it possible to combine these methods? (Java Generics)

I'm working on a homework assignment for University which does not allow me to change the properties of the classes. I came up with the following code for a search method, but I can't help but notice the duplicate code. I am required to build two search methods. One that searches an ArrayList<Person> and one that searches an ArrayList<Module. Both Person and Module have a String name property.

public static <T extends Person> ArrayList<T> findPerson(List<T> list, String query) {
    ArrayList<T> matching = new ArrayList<>();
    list.forEach(s -> {
        if (s.name.contains(query)) matching.add(s);
    });
    return matching;
}

public static <T extends Module> ArrayList<T> findModule(List<T> list, String query) {
    ArrayList<T> matching = new ArrayList<>();
    list.forEach(s -> {
        if (s.name.contains(query)) matching.add(s);
    });
    return matching;
}

Is it possible to combine these methods without changing the class structure? I tried something along these lines but it still doesn't seem to work: (different type argument in method signature)

public static <T extends Person, Module> ArrayList<T> findModule(List<T> list, String query) {
    ArrayList<T> matching = new ArrayList<>();
    list.forEach(s -> {
        if (s.name.contains(query)) matching.add(s);
    });
    return matching;
}

Edit: Here are the classes:

public abstract class Person extends TableItem {

public String name, famName, street, zip, city;
public String[] data = new String[7];

...
}

public class Module extends TableItem {

private Manager manager;
public String[] data = new String[5];
public String name, nr, profID, semester, part;
public ArrayList<String> participants;

...
}

public abstract class TableItem {

public abstract String getString();
public abstract void updateData();
}

Upvotes: 3

Views: 77

Answers (2)

jbx
jbx

Reputation: 22158

Ideally you make both your classes implement the same interface, like NamedItem which would have a function getName(), it would be much simpler, because you make your function take List<? extends NamedItem>. If this is not possible, you will have to provide a Function to determine what String property to match.

public static <T> List<T> find(List<T> list, String query, Function<T, String> keyExtractor) {
    return list
        .stream()
        .filter(s -> keyExtractor.apply(s).contains(query))
        .collect(Collectors.toList());
}

Then you can use it like:

List<Person> matchingPersons = find(allPersons, query1, Person::getName);
List<Module> matchingModules = find(allModules, query1, Module::getName);

You can use it with any property, not just name. You just pass the function that extracts the String you want to match as the 3rd parameter.

Upvotes: 3

Andreas
Andreas

Reputation: 159135

The only way to specify more than one base type for a generic type is with the & syntax:

<T extends Person & Module>

But in that case T must extend both at the same time, so it is not what you're looking for.

To combine your two methods, the types must share a common interface, e.g.

public interface Named {
    String getName();
}

Now, if both Person and Module extends/implements the Named interface, your method can be:

public static <T extends Named> ArrayList<T> findNamed(List<T> list, String query) {
    return list.stream().filter(s -> s.getName().contains(query))
               .collect(Collectors.toCollection(ArrayList::new));
}

Upvotes: 2

Related Questions