I'm writing a function that takes a list of keyExtractor functions to produce a Comparator (imagine we had an object with many many properties and wanted to be able to arbitrarily compare by a large number of properties in any order).
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
class Test {
public static <T, S extends Comparable<S>> Comparator<T> parseKeysAscending(List<Function<T, S>> keyExtractors) {
if (keyExtractors.isEmpty()) {
return (a, b) -> 0;
} else {
Function<T, S> firstSortKey = keyExtractors.get(0);
List<Function<T, S>> restOfSortKeys = keyExtractors.subList(1, keyExtractors.size());
return Comparator.comparing(firstSortKey).thenComparing(parseKeysAscending(restOfSortKeys));
}
}
public static void main(String[] args) {
List<Extractor<Data, ?>> extractors = new ArrayList<>();
extractors.add(new Extractor<>(Data::getA));
extractors.add(new Extractor<>(Data::getB));
Comparator<Data> test = parseKeysAscending(
extractors.stream()
.map(e -> e)
.collect(Collectors.toList()));
}
}
class Extractor<T, S extends Comparable<S>> implements Function<T, S> {
private final Function<T, S> extractor;
Extractor(Function<T, S> extractor) {
this.extractor = extractor;
}
@Override
public S apply(T t) {
return extractor.apply(t);
}
}
class Data {
private final Integer a;
private final Integer b;
private Data(int a, int b) {
this.a = a;
this.b = b;
}
public Integer getA() {
return a;
}
public Integer getB() {
return b;
}
}
There are three main points of confusion for me:
1). If I don't define the Extractor class, this will not compile. I cannot directly have Functions or some sort of functional interface.
2). If I remove the identity function mapping line ".map(e -> e)", this will not type check.
3). My IDE says my function is accepting a List of Functions of the type Data -> ? which doesn't comply with the bounds of the parseKeysAscending function.
How compilers work. Compilers are utility programs that take your code and transform it into executable machine code files. When you run a compiler on your code, first, the preprocessor reads the source code (the C++ file you just wrote).
Compile refers to the act of converting programs written in high level programming language, which is understandable and written by humans, into a low level binary language understood only by the computer.
A compiler takes the program code (source code) and converts the source code to a machine language module (called an object file). Another specialized program, called a linker, combines this object file with other previously compiled object files (in particular run-time modules) to create an executable file.
It works for me without the Extractor
class and also without calling map(e -> e)
in the stream pipeline. Actually, streaming the list of extractors isn't needed at all if you use the correct generic types.
As to why your code doesn't work, I'm not completely sure. Generics is a tough and flaky aspect of Java... All I did was adjusting the signature of the parseKeysAscending
method, so that it conforms to what Comparator.comparing
actually expects.
Here's the parseKeysAscending
method:
public static <T, S extends Comparable<? super S>> Comparator<T> parseKeysAscending(
List<Function<? super T, ? extends S>> keyExtractors) {
if (keyExtractors.isEmpty()) {
return (a, b) -> 0;
} else {
Function<? super T, ? extends S> firstSortKey = keyExtractors.get(0);
List<Function<? super T, ? extends S>> restOfSortKeys =
keyExtractors.subList(1, keyExtractors.size());
return Comparator.<T, S>comparing(firstSortKey)
.thenComparing(parseKeysAscending(restOfSortKeys));
}
}
And here's a demo with the call:
List<Function<? super Data, ? extends Comparable>> extractors = new ArrayList<>();
extractors.add(Data::getA);
extractors.add(Data::getB);
Comparator<Data> test = parseKeysAscending(extractors);
List<Data> data = new ArrayList<>(Arrays.asList(
new Data(1, "z"),
new Data(2, "b"),
new Data(1, "a")));
System.out.println(data); // [[1, 'z'], [2, 'b'], [1, 'a']]
data.sort(test);
System.out.println(data); // [[1, 'a'], [1, 'z'], [2, 'b']]
The only way to make the code compile without warnings was to declare the list of functions as List<Function<Data, Integer>>
. But this works only with getters that return Integer
. I'm assuming that you might want to compare any mix of Comparable
s, i.e. the code above works with the following Data
class:
public class Data {
private final Integer a;
private final String b;
private Data(int a, String b) {
this.a = a;
this.b = b;
}
public Integer getA() {
return a;
}
public String getB() {
return b;
}
@Override
public String toString() {
return "[" + a + ", '" + b + "']";
}
}
Here's the demo.
EDIT: Note that with Java 8, the last line of the parseKeysAscending
method can be:
return Comparator.comparing(firstSortKey)
.thenComparing(parseKeysAscending(restOfSortKeys));
While for newer versions of Java, you must provide explicit generic types:
return Comparator.<T, S>comparing(firstSortKey)
.thenComparing(parseKeysAscending(restOfSortKeys));
After Federico corrected me (thank you!) this is a single method you could do it with:
public static <T, S extends Comparable<? super S>> Comparator<T> test(List<Function<T, S>> list) {
return list.stream()
.reduce((x, y) -> 0,
Comparator::thenComparing,
Comparator::thenComparing);
}
And usage would be:
// I still don't know how to avoid this raw type here
List<Function<Data, Comparable>> extractors = new ArrayList<>();
extractors.add(Data::getA); // getA returns an Integer
extractors.add(Data::getB); // getB returns a String
listOfSomeDatas.sort(test(extractors));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With