【问题标题】:Why does stream.max() result differ when using comparator in ascending vs descending order?为什么以升序和降序使用比较器时,stream.max() 结果会有所不同?
【发布时间】:2021-09-04 16:26:04
【问题描述】:

我有下面的代码设置,我有点困惑为什么当 Comparator 设置为按升序或降序排序时 stream.max() 返回不同的结果。我知道比较器如何对数组进行排序,但不太清楚 max 和比较器如何处理流。

public class Main
{
    public static void main(String[] args) {
        
        Cat c1 = new Cat(5);
        Cat c2 = new Cat(6);
        Cat c3 = new Cat(7);

        ArrayList<Cat> arr = new ArrayList<Cat>();
        arr.add(c1);
        arr.add(c2);
        arr.add(c3);
        
        Optional<Cat> cMax = findMax(arr);
        System.out.println(cMax.get().age);

    }
    
    public static Optional<Cat> findMax(ArrayList<Cat> arr) {
        return arr.stream()
          .max((Cat a, Cat b) -> {
            return b.age - a.age;
            // versus a.age - b.age
      });
    }
}

class Cat{
    int age;
    Cat(int age){
        this.age = age;
    }
}

【问题讨论】:

  • 我没试过,但我的期望是max(a-b)min(b-a)
  • b.age - a.age 返回 5 和 a.age - b.age 返回 7
  • 你可以把它想象成比较器回答了“a大于b吗?”这个问题。通过返回正数(是)或负数(否)。如果你系统地“撒谎”,寻找最大值实际上会找到最小值。
  • max 函数和比较器之间的操作顺序令人困惑。 max 函数在这里跟踪流中比较的每对之间的最大值吗?

标签: java java-stream comparator


【解决方案1】:

问题是,当你给方法提供两个不同的比较器时,为什么你期望得到相同的结果,一个是自然顺序,另一个是相反的?

如果您使用比较器调用操作max,您将使用比较器隐式定义哪个对象应该是最大的。换句话说,您希望流按比较器排序,并且您需要最后一个元素。如果它是一个列表,当你说排序列表并给我最后一个对象时,很容易理解为什么会出现不同的结果。最后一个对象取决于您是按升序还是降序排序。

也许该方法的名称具有误导性。它应该被称为maxBy 而不仅仅是max。在你的例子中,随着年龄的增长,它会导致误解。 Max 不是数学上的最大数字,而是根据您的定义/比较器的最大值。理论上你也可以传递一个名称比较器,假设你的类有一个字段名:

List<Cat> myCatList = List.of(new Cat("Jane"), new Cat("Dave"), new Cat("Susane"),);

....

public static Optional<Cat> findMax(ArrayList<Cat> arr) {
    return arr.stream()
      .max((Cat a, Cat b) -> {
        return a.name.compareTo(b.name);
  });
}

你会得到 Susane 作为结果,尽管在没有上下文的情况下说名为 Susane 的猫是最大的猫可能没有意义。

我不想说倒序排序没有意义。以学校成绩为例。在许多国家,学校成绩是字母,A 是最好的,F 是最差的。但在自然顺序中,字母 F 更大。在这种情况下,举个例子,如果您想从学生对象流中获得成绩最好的对象,即max(grades_comparator),您不想让最差的学生获得 f,而是让最好的学生获得a级。在那里你传递了一个反向比较器。

除此之外,您应该更喜欢现有的比较方法,而不是使用数学技巧来实现排序。在您使用简单数字的示例中,没有必要,但是如果您的值接近 Integer.MAX_VALUEInteger.MIN_VALUE 并且您的程序提供了奇怪的结果,那么您可能会无意中出现溢出。更好:

public static Optional<Cat> findMax(ArrayList<Cat> arr) {
    return arr.stream()
      .max((Cat a, Cat b) -> {
        return Integer.compare(a.age, b.age);
  });
}

或者,如果你还是要尝试使用流 api,你还应该使用比较器接口和方法引用,这使得整个事情更具可读性:

public static Optional<Cat> findMax(ArrayList<Cat> arr) {
    return arr.stream()
              .max(Comparator.comparingInt(Cat::getAge));
}

【讨论】:

    【解决方案2】:

    你身边可能有一点神奇的想法,但Comparator.compare()有一个非常简单的一句话总结:

    比较它的两个参数的顺序。返回负整数、零或正整数,因为第一个参数小于、等于或大于 第二个。

    这是max()min() 期望从您提供给它们的比较器中得到的,因此(Cat a, Cat b) -&gt; a.age - b.age 将在提供给它们时提供“自然”最大值和最小值。但是,您可以在比较器中很好地执行其他任何操作,然后结果将反映这种比较,例如使用 b-a 变体进行与名称完全相反的操作。

    接受任何类型的整数只是为了方便,但实际上只使用它们与 0 的关系。

    ReferencePipeline.java 有实际的实现,它确实使用reduce() 作为documentation promises (“这是一个缩减的特殊情况。”)。

    @Override
    public final Optional<P_OUT> max(Comparator<? super P_OUT> comparator) {
        return reduce(BinaryOperator.maxBy(comparator));
    }
    

    BinaryOperator.javamaxBy(),创建一个函数,根据提供的比较器选择较大的值:

    public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-12-03
      • 2018-11-17
      • 2019-02-24
      相关资源
      最近更新 更多