19

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.

fps
  • 33,623
  • 8
  • 55
  • 110
Billy the Kid
  • 271
  • 2
  • 12
  • 1
    There *does* seem to be some wonky stuff going on here. Question worth asking to me, since my IDE also optimizes out the mapping but then immediately refuses to compile. What version of Java are you using? – Makoto Oct 01 '18 at 17:42
  • 9
    This question is being discussed on [meta](https://meta.stackoverflow.com/questions/375454/why-is-this-a-good-question) – yivi Oct 17 '18 at 17:38

4 Answers4

8

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 Comparables, 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));
fps
  • 33,623
  • 8
  • 55
  • 110
  • 3
    I still don't understand where the problem was or is (it compiled with java-11 that I have and use), but as a side note I think that method can be shorten to `public static > Comparator parseKeysAscending(List> keyExtractors) { return keyExtractors.stream() .collect(Collector.of( () -> (Comparator) (x, y) -> 0, Comparator::thenComparing, Comparator::thenComparing)); }` – Eugene Oct 01 '18 at 19:02
  • Two interesting observations for your code Federico: 1). This doesn't work for me if I remove the stream in the call 2). I am getting a "cannot resolve method" in the recursive call yet it still successfully runs. – Billy the Kid Oct 01 '18 at 19:03
  • This looks like what I am going for. I think this is also a great example for me to understand contravariance/covariance in Java through the wildcard type. This may be IDE specific, but using the method as written gives me an unchecked method exception. If I use the intermediate "extractor" class as before, this warning goes away. Also, doesn't this still mean our method is accepting some type "S extends Comparable super S>"? I don't think there should be any way to call this with data that has both a string and integer field without getting the unchecked warning. – Billy the Kid Oct 01 '18 at 20:32
  • 3
    @BillytheKid Honestly, I'm not getting that exception/compilation error, so I cannot tell. Usually, Eclipse compiler is known to have issues with type inference (i.e. covariance/contravariance, generic types. lambdas and method references all mixed together). As to the unchecked warning, I don't think there's a way to get rid of it. – fps Oct 01 '18 at 20:58
  • I'm using Intellij, but I think the issue is really in the type definition: public static > Comparator parseKeysAscending(List> keyExtractors) We actually don't care at all about the type S at all, we just care that we can compare it. So we would need a way to represent a type which is any comparable object which I believe would need be ? extends Comparable> – Billy the Kid Oct 01 '18 at 22:56
  • 1
    @BillytheKid I tried that in ideone.com and it didn't compile because `Comparator.thenComparing` needs an argument of type `Function>`, which doesn't match to `Function>`. – fps Oct 01 '18 at 22:59
  • 1
    When trying various versions of compilers, javac 8 is the only one that accepts this variant. Newer versions reject the program. Eclipse (ecj) has been rejecting the program in all versions since Java 8 was released. Ergo, some of the confusion seems to result from a bug in javac, that was fixed in 9. – Stephan Herrmann Oct 03 '18 at 09:42
  • @StephanHerrmann I've edited my answer as per your comment, thanks for your feedback – fps Oct 03 '18 at 18:51
8

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));
Eugene
  • 117,005
  • 15
  • 201
  • 306
  • 3
    Very nice, this does make sense as the original recursive function follows a pretty standard fold-like pattern. – Billy the Kid Oct 01 '18 at 20:56
  • 2
    I still like `list.stream().map(Comparator::comparing).reduce(Comparator::thenComparing).orElse((Comparator) (a, b) -> 0)` more than this, but +1 for keeping all this generics nightmare simple. – fps Oct 01 '18 at 21:03
2
  1. If I don't define the Extractor class, this will not compile. I cannot directly have Functions or some sort of functional interface.

No, you can. You can define any Function<X, Y> by a lambda, or method reference, or anonymous class.

List<Function<Data, Integer>> extractors = List.of(Data::getA, Data::getB);
  1. If I remove the identity function mapping line .map(e -> e), this will not type check.

It still will, but the result may not be suitable for the method. You can always define generic parameters explicitly to make sure that everything goes as you expect.

extractors.<Function<Data, Integer>>stream().collect(Collectors.toList())

But there is no need for that here:

Comparator<Data> test = parseKeysAscending(extractors);
  1. 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.

Yes, it should. You are passing a List<Extractor<Data, ?>> and the compiler can't understand the ? part. It may or may not be a Comparable while your method clearly requires S extends Comparable<S>.

Andrew Tobilko
  • 48,120
  • 14
  • 91
  • 142
1

Regarding original code in the question: you can remove Extractor and use a raw Comparable:

List<Function<Data, Comparable>> extractors = new ArrayList<>();

extractors.add(Data::getA);
extractors.add(Data::getB);

@SuppressWarnings("unchecked")
Comparator<Data> test = parseKeysAscending(extractors);

P.S. Yet I don't see how to get rid of the raw type here...

Oleksandr Pyrohov
  • 14,685
  • 6
  • 61
  • 90