这个编译过程是如何进行的?

19

我正在编写一个函数,它可以接收一个keyExtractor函数列表并生成一个比较器(想象一下我们有一个具有许多属性的对象,并且想要能够任意按照大量属性进行排序)。

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;
    }
}

我有三个疑惑点:

1). 如果我不定义Extractor类,这段代码将无法编译。我不能直接使用Functions或某种函数式接口。

2). 如果我删除身份函数映射行“.map(e -> e)”,它将无法通过类型检查。

3). 我的IDE说我的函数接受一组类型为Data -> ?的函数列表,这与parseKeysAscending函数的限制不符。


1
这里似乎有一些奇怪的问题。我认为这是值得问的问题,因为我的IDE也会优化映射,但随后会立即拒绝编译。你使用的Java版本是什么? - Makoto
9
这个问题正在元社区上讨论。 - yivi
4个回答

8

如果你使用正确的泛型类型,不需要在流水线中调用Extractor类或者调用map(e -> e)。实际上,如果使用正确的泛型类型,则根本不需要流式处理提取器列表。

至于为什么你的代码不起作用,我不完全确定。泛型是Java中棘手且不稳定的方面……我所做的就是调整parseKeysAscending方法的签名,以使其符合Comparator.comparing期望的格式。

这是parseKeysAscending方法:

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));
    }
}

以下是带调用的演示:

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']]

唯一让代码编译不产生警告的方法是将函数列表声明为List<Function<Data, Integer>>。但这仅适用于返回Integer的getter。我假设您可能想要比较任何混合的Comparables,即上面的代码适用于以下Data类:
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 + "']";
    }
}

这里是演示

编辑:需要注意的是,使用Java 8时,parseKeysAscending方法的最后一行可以是:

return Comparator.comparing(firstSortKey)
        .thenComparing(parseKeysAscending(restOfSortKeys));

对于较新的Java版本,您必须提供明确的泛型类型:

return Comparator.<T, S>comparing(firstSortKey)
        .thenComparing(parseKeysAscending(restOfSortKeys));

3
我仍然不明白问题出在哪里(因为它已经可以使用我现在用的java-11进行编译),但作为一个附注,我认为这个方法可以简化为 public static <T, S extends Comparable<S>> Comparator<T> parseKeysAscending(List<Function<T, S>> keyExtractors) { return keyExtractors.stream() .collect(Collector.of( () -> (Comparator<T>) (x, y) -> 0, Comparator::thenComparing, Comparator::thenComparing)); } - Eugene
这看起来就是我想要的。我认为这也是一个很好的例子,让我通过通配符类型理解Java中的逆变/协变。这可能是特定于IDE的,但按照所写的方法会给我带来未经检查的方法异常。如果像以前一样使用中间的“提取器”类,这个警告就会消失。此外,这是否仍意味着我们的方法接受某些类型的“S extends Comparable <? super S>”?我认为没有任何办法可以调用具有字符串和整数字段的数据而不获取未经检查的警告。 - Billy the Kid
3
@BillytheKid 说实话,我没有遇到那个异常/编译错误,所以无法确定。通常来说,Eclipse编译器在类型推断方面有问题(即协变/逆变、泛型类型、lambda和方法引用混合使用)。至于未经检查的警告,我认为没有办法摆脱它。 - fps
1
@BillytheKid 我在 ideone.com 上尝试了这个,但它没有编译通过,因为 Comparator.thenComparing 需要一个类型为 Function<T, S extends Comparable<? super S>> 的参数,这与 Function<T, ? extends Comparable<?>> 不匹配。 - fps
1
尝试使用各种编译器版本时,javac 8是唯一接受此变体的编译器。更新的版本会拒绝该程序。自Java 8发布以来,Eclipse(ecj)在所有版本中都拒绝了该程序。因此,一些混淆似乎是由于javac中的一个错误导致的,该错误已在9中得到修复。 - Stephan Herrmann
显示剩余3条评论

8

在Federico更正我的错误后(谢谢!),以下是一种你可以使用的单一方法:

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);
}

使用方法如下:

// 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));

3
非常好,这确实有意义,因为原始的递归函数遵循了一个相当标准的折叠模式。 - Billy the Kid
2
我仍然更喜欢 list.stream().map(Comparator::comparing).reduce(Comparator::thenComparing).orElse((Comparator<T>) (a, b) -> 0),但是因为保持所有这些泛型的简单性而加1。 - fps

2
  1. 如果我不定义Extractor类,这个代码将无法编译。我不能直接使用Function或某种函数式接口。

不是的,你可以。你可以通过lambda、方法引用或匿名类定义任何Function<X, Y>

List<Function<Data, Integer>> extractors = List.of(Data::getA, Data::getB);
  1. 如果我删除恒等函数映射行.map(e -> e),这段代码将无法通过类型检查。

即使如此,它仍然可以运行,但结果可能不适合该方法。您可以始终明确定义泛型参数,以确保一切按照您的预期进行。

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

但这里并不需要那样做:
Comparator<Data> test = parseKeysAscending(extractors);

我的IDE显示我的函数接受一个类型为Data, ?List Function列表,这与parseKeysAscending函数的边界不符合。
是的,应该是这样的。你正在传递一个List<Extractor<Data, ?>>,而编译器无法理解?部分。它可能是Comparable,也可能不是,而你的方法明确要求S extends Comparable<S>

1
关于问题中的原始代码:您可以删除 Extractor 并使用原始 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. 但我不知道如何在这里消除原始类型...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接