diff --git a/guava-tests/test/com/google/common/collect/ComparisonChainTest.java b/guava-tests/test/com/google/common/collect/ComparisonChainTest.java index b2bb7efcac00..d697ebb79790 100644 --- a/guava-tests/test/com/google/common/collect/ComparisonChainTest.java +++ b/guava-tests/test/com/google/common/collect/ComparisonChainTest.java @@ -16,11 +16,20 @@ package com.google.common.collect; +import static com.google.common.base.MoreObjects.toStringHelper; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.lang.Integer.signum; +import static java.util.Comparator.comparing; +import static java.util.Comparator.naturalOrder; +import static java.util.Comparator.nullsLast; import com.google.common.annotations.GwtCompatible; +import com.google.common.primitives.Booleans; +import java.util.Comparator; import junit.framework.AssertionFailedError; import junit.framework.TestCase; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Unit test for {@link ComparisonChain}. @@ -118,4 +127,95 @@ public void testCompareTrueFirst() { assertThat(ComparisonChain.start().compareTrueFirst(false, true).result()).isGreaterThan(0); assertThat(ComparisonChain.start().compareTrueFirst(false, false).result()).isEqualTo(0); } + + enum TriState { + FALSE, + MAYBE, + TRUE, + } + + static class Foo { + private final String aString; + private final int anInt; + private final @Nullable TriState anEnum; + + Foo(String aString, int anInt, @Nullable TriState anEnum) { + this.aString = aString; + this.anInt = anInt; + this.anEnum = anEnum; + } + + @Override + public String toString() { + return toStringHelper(this) + .add("aString", aString) + .add("anInt", anInt) + .add("anEnum", anEnum) + .toString(); + } + } + + /** Validates that the Comparator equivalent we document is correct. */ + public void testComparatorEquivalent() { + Comparator comparatorUsingComparisonChain = + (a, b) -> + ComparisonChain.start() + .compare(a.aString, b.aString) + .compare(a.anInt, b.anInt) + .compare(a.anEnum, b.anEnum, Ordering.natural().nullsLast()) + .result(); + Comparator comparatorUsingComparatorMethods = + comparing((Foo foo) -> foo.aString) + .thenComparing(foo -> foo.anInt) + .thenComparing(foo -> foo.anEnum, nullsLast(naturalOrder())); + ImmutableList instances = + ImmutableList.of( + new Foo("a", 1, TriState.TRUE), + new Foo("a", 2, TriState.TRUE), + new Foo("b", 1, TriState.FALSE), + new Foo("b", 1, TriState.TRUE), + new Foo("b", 1, null)); + for (Foo a : instances) { + for (Foo b : instances) { + int comparedUsingComparisonChain = signum(comparatorUsingComparisonChain.compare(a, b)); + int comparedUsingComparatorMethods = signum(comparatorUsingComparatorMethods.compare(a, b)); + assertWithMessage("%s vs %s", a, b) + .that(comparedUsingComparatorMethods) + .isEqualTo(comparedUsingComparisonChain); + } + } + } + + static class Bar { + private final boolean isBaz; + + Bar(boolean isBaz) { + this.isBaz = isBaz; + } + + boolean isBaz() { + return isBaz; + } + } + + /** + * Validates that {@link Booleans#trueFirst()} and {@link Booleans#falseFirst()} can be used with + * {@link Comparator} when replacing {@link ComparisonChain#compareTrueFirst} and {@link + * ComparisonChain#compareFalseFirst}, as we document. + */ + public void testTrueFirstFalseFirst() { + Bar trueBar = new Bar(true); + Bar falseBar = new Bar(false); + + assertThat(ComparisonChain.start().compareTrueFirst(trueBar.isBaz(), falseBar.isBaz()).result()) + .isLessThan(0); + Comparator trueFirstComparator = comparing(Bar::isBaz, Booleans.trueFirst()); + assertThat(trueFirstComparator.compare(trueBar, falseBar)).isLessThan(0); + + assertThat( + ComparisonChain.start().compareFalseFirst(falseBar.isBaz(), trueBar.isBaz()).result()) + .isLessThan(0); + Comparator falseFirstComparator = comparing(Bar::isBaz, Booleans.falseFirst()); + assertThat(falseFirstComparator.compare(falseBar, trueBar)).isLessThan(0); + } } diff --git a/guava/src/com/google/common/collect/ComparisonChain.java b/guava/src/com/google/common/collect/ComparisonChain.java index 9c05205ec01d..79b7c6cc8906 100644 --- a/guava/src/com/google/common/collect/ComparisonChain.java +++ b/guava/src/com/google/common/collect/ComparisonChain.java @@ -24,7 +24,10 @@ import org.checkerframework.checker.nullness.qual.Nullable; /** - * A utility for performing a chained comparison statement. For example: + * A utility for performing a chained comparison statement. Note: Java 8+ users should + * generally prefer the methods in {@link Comparator}; see below. + * + *

Example usage of {@code ComparisonChain}: * *

{@code
  * public int compareTo(Foo that) {
@@ -52,6 +55,37 @@
  * "https://github.com/google/guava/wiki/CommonObjectUtilitiesExplained#comparecompareto">{@code
  * ComparisonChain}.
  *
+ * 

Java 8+ equivalents

+ * + * If you are using Java version 8 or greater, you should generally use the static methods in {@link + * Comparator} instead of {@code ComparisonChain}. The example above can be implemented like this: + * + *
{@code
+ * import static java.util.Comparator.comparing;
+ * import static java.util.Comparator.nullsLast;
+ * import static java.util.Comparator.naturalOrder;
+ *
+ * ...
+ *   private static final Comparator COMPARATOR =
+ *       comparing((Foo foo) -> foo.aString)
+ *           .thenComparing(foo -> foo.anInt)
+ *           .thenComparing(foo -> foo.anEnum, nullsLast(naturalOrder()));}
+ *
+ *   {@code @Override}{@code
+ *   public int compareTo(Foo that) {
+ *     return COMPARATOR.compare(this, that);
+ *   }
+ * }
+ * + *

With method references it is more succinct: {@code comparing(Foo::aString)} for example. + * + *

Using {@link Comparator} avoids certain types of bugs, for example when you meant to write + * {@code .compare(a.foo, b.foo)} but you actually wrote {@code .compare(a.foo, a.foo)} or {@code + * .compare(a.foo, b.bar)}. {@code ComparisonChain} also has a potential performance problem that + * {@code Comparator} doesn't: it evaluates all the parameters of all the {@code .compare} calls, + * even when the result of the comparison is already known from previous {@code .compare} calls. + * That can be expensive. + * * @author Mark Davis * @author Kevin Bourrillion * @since 2.0 @@ -243,6 +277,12 @@ public final ComparisonChain compare(Boolean left, Boolean right) { * Compares two {@code boolean} values, considering {@code true} to be less than {@code false}, * if the result of this comparison chain has not already been determined. * + *

Java 8+ users: you can get the equivalent from {@link Booleans#trueFirst()}. For example: + * + *

+   * Comparator.comparing(Foo::isBar, {@link Booleans#trueFirst()})
+   * 
+ * * @since 12.0 */ public abstract ComparisonChain compareTrueFirst(boolean left, boolean right); @@ -251,6 +291,12 @@ public final ComparisonChain compare(Boolean left, Boolean right) { * Compares two {@code boolean} values, considering {@code false} to be less than {@code true}, * if the result of this comparison chain has not already been determined. * + *

Java 8+ users: you can get the equivalent from {@link Booleans#falseFirst()}. For example: + * + *

+   * Comparator.comparing(Foo::isBar, {@link Booleans#falseFirst()})
+   * 
+ * * @since 12.0 (present as {@code compare} since 2.0) */ public abstract ComparisonChain compareFalseFirst(boolean left, boolean right);