How to choose pass instance method to use to a method in Java

220 Views Asked by At

I have a method that sorts a List by different criteria and returns the name (an instance variable) of the one with maximum value. In case more than one instance is having the maximum, all of their names should be concatenated.

Let's say I have Class A as follows.

Class A {
    ...
    String getName(){...}
    int getValue1() {...}
    int getValue2() {...}
    ...
    int getValueN() {...}
    ...
}

I have a List<A> listToSort. I would normally sort this list as listToSort.sort(Comparator.comparing(A::getValue1)) or listToSort.sort(Comparator.comparing(A::getValue2)), so on and so forth. Then get the ones sharing the maximum value.

In a method I believe this should be done as:

String getMaxString (Comparator c) {
    listToSort.sort(c);
    ...
}

and send Comparator.comparing(A.getValueX) as parameter to call it with different methods. (X here indicates an arbitrary number for the getValue function).

However, I need to also return other instances sharing the same values I will need to pass the Class methods to my method and call on instances as:

String getMaxString (Comparator c) {
    listToSort.sort(c);

    int maxValue = listToSort.get(listToSort.size() - 1).getValueX();
    String maxString = listToSort.get(listToSort.size() - 1).getName();

    for (int i = listToSort.size() - 2; i >= 0; i--) {
        if (listToSort.get(i).getValueX()() == maxValue) {
            maxString += ", " + listToSort.get(i).getName();
        }
    }
    return maxString;
}

How would I pass this method to call on instances here? Or do I need to consider another way?

Edit:

I have a list of Courses as List<Course> mylist where a course can be simplified as:

Class Course {
    private String name;
    private int capacity;
    private int students;
    
    ...
    //bunch of getters.
}

My task is to return Strings for the course(es) with maximum capacity, the course(es) with maximum registered students, the course(es) with most difficulty, the maximum filled percentage, the course(es) with the maximum number of TAs etc...

Edit 2: As requested in the comment section.

List of

Course a (name "a", capacity 10, students 5)
Course b (name "b", capacity 20, students 5)
Course c (name "c", capacity 30, students 0)

Sorting based on capacity should return "c"

Sorting based on students should return "a b"

4

There are 4 best solutions below

2
tgdavies On BEST ANSWER

You can pass the getter method and create the Comparator in getMaxString:

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;

public class Foo {
    static class AClass {
        private final String name;
        private final int value1;
        private final int value2;

        String getName() { return name; }
        int getValue1() { return value1; }
        int getValue2() { return value2; }

        public AClass(String name, int value1, int value2) {
            this.name = name;
            this.value1 = value1;
            this.value2 = value2;
        }
    }

    static String getMaxString(Function<AClass,Integer> f, List<AClass> listToSort) {
        listToSort.sort(Comparator.comparing(f));
        int maxValue = f.apply(listToSort.get(listToSort.size() - 1));
        String maxString = listToSort.get(listToSort.size() - 1).getName();

        for (int i = listToSort.size() - 2; i >= 0; i--) {
            if (f.apply(listToSort.get(i)) == maxValue) {
                maxString += ", " + listToSort.get(i).getName();
            }
        }
        return maxString;
    }

    public static void main(String[] args) {
        List<AClass> list = new ArrayList<>();
        list.add(new AClass("a", 1,2));
        list.add(new AClass("b", 1,2));
        list.add(new AClass("c", 2,1));
        list.add(new AClass("d", 2,1));
        System.out.println(getMaxString(AClass::getValue1, list));
        System.out.println(getMaxString(AClass::getValue2, list));
    }
}

As Tim Moore suggested above, it isn't necessary to sort the list (which has cost O(n*log n)), we can traverse it twice:

    static String getMaxString2(ToIntFunction<AClass> f, List<AClass> listToSort) {
        int maxValue = listToSort.stream().mapToInt(f).max().orElseThrow();
        return listToSort.stream()
          .filter(a -> maxValue == f.applyAsInt(a))
          .map(AClass::getName)
          .collect(Collectors.joining(", "));
    }

Note that you should test your code with an empty list.

0
Alexander Ivanchenko On

Time complexity O(n) - only one iteration through the dataset.

Hope it'll help. If something will be unclear fill free to raise a question.

Main

public class MaxClient {
public static void main(String[] args) {
    Comparator<A> comp = Comparator.comparingInt(A::getVal1);

    List<A> items = List.of(new A(1, 8), new A(2, 8), new A(5, 8), new A(5, 27), new A(3, 8));
    items.stream()
            .collect(new GetMax(comp))
            .forEach(System.out::println);
}

}

Custom collector GetMax

public class GetMax implements Collector <A, Deque<A>, Deque<A>> {
private final Comparator<A> comp;

public GetMax(Comparator<A> comp) {
    this.comp = comp;
}

@Override
public Supplier<Deque<A>> supplier() {
    return ArrayDeque::new;
}

@Override
public BiConsumer<Deque<A>, A> accumulator() {
    return (stack, next) -> {
        if (!stack.isEmpty() && comp.compare(next, stack.peekFirst()) > 0) stack.clear();
        if (stack.isEmpty() || comp.compare(next, stack.peekFirst()) == 0) stack.offerLast(next);
    };
}

@Override
public BinaryOperator<Deque<A>> combiner() {
    return (stack1, stack2) -> {
        if (stack1.isEmpty()) return stack2;
        if (stack2.isEmpty()) return stack1;
        if (comp.compare(stack1.peekFirst(), stack2.peekFirst()) == 0) {
            stack1.addAll(stack2);
        }
        return stack1;
    };
}

@Override
public Function<Deque<A>, Deque<A>> finisher() {
    return stack -> stack;
}

@Override
public Set<Characteristics> characteristics() {
    return Set.of(Characteristics.UNORDERED);
}

}

Class A that I used for testing purposes

public class A {
private int val1;
private int val2;

public A(int val1, int val2) {
    this.val1 = val1;
    this.val2 = val2;
}

public int getVal1() {
    return val1;
}

public int getVal2() {
    return val2;
}

@Override
public String toString() {
    return "A val1: " + val1 + " val2: " + val2;
}

}

OUTPUT

A val1: 5 val2: 8
A val1: 5 val2: 27
0
Tim Moore On

It's useful to look at the type signature for Comparator.comparing, because it sounds like you want to do something similar:

static <T,U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T,? extends U> keyExtractor)

The interesting part is the type of keyExtractor. Roughly speaking, it's a function from the type of the object you're comparing, to the type of the field you want to use for the comparison. In our case, these correspond to the A class and Integer. Because these types are fixed in this example, you can declare a method with a signature like this:

String getMaxString(Function<A, Integer> property)

With the existing algorithm, it can be used this way:

String getMaxString(Function<A, Integer> property) {
    listToSort.sort(Comparator.comparing(property));

    int maxValue = property.apply(listToSort.get(listToSort.size() - 1));
    String maxString = listToSort.get(listToSort.size() - 1).getName();

    for (int i = listToSort.size() - 2; i >= 0; i--) {
        if (listToSort.get(i).getValueN() == maxValue) {
            maxString += ", " + listToSort.get(i).getName();
        }
    }
    return maxString;
}

However, it isn't necessary or efficient to sort the entire list in order to determine the maximum elements, as this can be determined by iterating through the list once:

String getMaxString(Function<A, Integer> property) {
    int maxValue = Integer.MIN_VALUE;
    StringBuilder maxString = new StringBuilder();

    for (A element : listToSort) {
        int currentValue = property.apply(element);
        if (currentValue > maxValue) {
            // there is a new maximum, so start the string again
            maxString = new StringBuilder(element.getName());
            maxValue = currentValue;
        } else if (currentValue == maxValue) {
            // equal to the existing maximum, append it to the string
            if (maxString.length() > 0) {
                maxString.append(", ");
            }
            maxString.append(element.getName());
        }
        // otherwise, it's less than the existing maximum and can be  ignored 
    }

    return maxString.toString();
}

Either way, you can call it using the same method reference syntax:

getMaxString(A::getValueN)
0
WJS On

Thanks for posting the information I requested. Here is what I came up with.

Create a list of Course objects

List<Course> list = List.of(
      new Course("a", 10, 5),
      new Course("b", 20, 5),
      new Course("c", 30, 0));

Stream the methods and apply them to the list

List<String> results = Stream.<Function<Course, Integer>>of(
    Course::getCapacity, 
    Course::getStudents)
      .map(fnc-> getMaxString(fnc, list))
      .toList();
    
results.forEach(System.out::println);

print the results

c
a b

I wrote a simple method that takes a method reference and list and finds the maximum. It does not do any sorting.

  • allocate a list to hold the names
  • set the maximum to the lowest possible
  • iterate thru the list applying the method.
  • if the value is greater than the current max replace it and clear the current list of names.
  • otherwise, if equal, add a new name.
  • once done, return the formatted string.

    
static String getMaxString(Function<Course, Integer> fnc,
        List<Course> list) {
    List<String> result = new ArrayList<>();
    int max = Integer.MIN_VALUE;
    for (Course obj : list) {
        int val = fnc.apply(obj);
        if (val >= max) {
            if (val > max) {
                result.clear();
            }
            max = val;
            result.add(obj.getName());
        }
    }
    return String.join(" ", result);
}