Guava: How to create an explicit Ordering from a List and a single element?

主宰稳场 提交于 2019-12-03 16:42:54

问题


In Guava, given a Collection<E> and an element e of type E that I know is in the collection, I'd like to create a custom Ordering<E> that sorts e first and then the rest of the collection. However, the way to get there seems awfully complicated:

Collection<String> values = ImmutableList.of("apples", "oranges", "pears");
String first = "oranges";

List<String> remainingValues = newArrayList(values);  // this
remainingValues.remove(first);                        // seems
Ordering<String> myOrdering =                         // very
    Ordering.explicit(first, remainingValues.toArray( // complicated!
        new String[remainingValues.size()]));         // is there an easier way?

What I'm wishing for is either something like this:

Ordering.explicit(first);

(I'd like this to sort first to the beginning and retain the order of all other elements, but the docs say the resulting Ordering will throw a ClassCastException for elements not explicitly listed.)

Or like this:

Ordering.explicit(first, values.toArray(/* etc */));

(But this would fail because first would be a duplicate value)

Can anybody come up with a concise way of doing what I want?

BTW, it doesn't have to be an Ordering, it could also be a workaround for creating an Iterable in the specified Order, but again, this is very complicated:

Iterable<String> sorted = Iterables.concat(
                             ImmutableList.of(first),
                             Iterables.filter(values, not(equalTo(first))));

回答1:


Well, here's one way to do it, but you may not find it much better.

final String special = "oranges";
Collections.sort(
    list,
    new Comparator<String>() {
      public int compare(String left, String right) {
        return ComparisonChain.start()
            .compareTrueFirst(left.equals(special), right.equals(special))
            .compare(left, right)
            .result();
      }
    });

ComparisonChain docs

Relevant Guava feature request -- please add any details.




回答2:


Perhaps this answer isn't easier/less complicated than what you already have but at least it can be re-used :)

class FirstOrdering<T extends Comparable> extends Ordering<T> {

    private T first;

    public FirstOrdering(T first) {
        this.first = first;
    }
    @Override
    public int compare(@Nullable T left, @Nullable T right) {
        // TODO Nullchecks...
        if (first.equals(left)) return -1;
        if (first.equals(right)) return 1;
        return left.compareTo(right);
    }
}

final String first = "B";
    new FirstOrdering(first).
            sortedCopy(Arrays.asList("A", "D", "E", first));



回答3:


Simply use NullsFirstOrdering as your template and create a ordering that sorts the first element, delegating to another ordering for everything else:

public class ItemFirstComparator<T> implements Comparator<T> implements Serializable {
  private final Comparator<? super T> comparator;
  private final Object item;

  ItemFirstComparator(Object item, Comparator<? super T> comparator) {
    this.item = item;
    comparator = checkNotNull(comparator);
  }

  @Override public int compare(@Nullable T left, @Nullable T right) {
    if (left == right) {
      return 0;
    }
    if (Objects.equals(left, item)) {
      return -1;
    }
    if (Objects.equals(right, item)) {
      return 1;
    }
    return comparator.compare(left, right);
  }
}

You then can chain orderings easily: Ordering.from(new ItemFirstComparator("oranges", Ordering.allEqual())).

Edit

Changed the code to use Comparator instead of Ordering, the rest stays the same.




回答4:


If you look at the source for com.google.common.collect.ExplicitOrdering, it maintains a map holding the rank of each item, and compare simply compares the ranks. You can do the same thing yourself, but forcing the designated first item's rank to -1, which is before all other items.

If you have a List (as the question's title states), Java 8 streams make building the map moderately convenient:

Map<T, Integer> rankMap = IntStream.range(0, list.size()).boxed().collect(
    Collectors.toMap(list::get, i -> list.get(i).equals(first) ? -1 : i));
Comparator<T> cmp = Comparator.comparing(rankMap::get);

If you have only a Collection (as the question's body states), you'll need to use a for loop to build the map:

Map<T, Integer> rankMap = new HashMap<>(coll.size());
int rank = 0;
for (T t : coll)
    rankMap.put(t, t.equals(first) ? -1 : rank++);
Comparator<T> cmp = Comparator.comparing(rankMap::get);

You can turn the Comparator into an Ordering with Ordering.from, as usual.




回答5:


This is more convenient and less repetitive if you have more special values:

class PriorityComparator<T> implements Comparator<T> {
    private final List<T> values;

    public PriorityComparator(T... values) {
        this.values = Arrays.asList(values);
    }

    @Override public int compare(T o1, T o2) {
        int idx1 = values.indexOf(o1);
        int idx2 = values.indexOf(o2);
        if (idx1 > -1) {
            return idx2 > -1 ? idx1 - idx2 : -1;
        }
        return idx2 > -1 ? 1 : 0;
    }
}

You can use it in a comparison chain like

return ComparisonChain.start()
    .compare(left, right, new PriorityComparator<>("oranges", "apples"))
    .compare(left, right)
    .result();

It will sort the elements as specified in the PriorityComparator, other elements are reported as being equal.

It's also easy to require T to be comparable and use that as the default value instead:

class PriorityComparator2<T extends Comparable<T>> implements Comparator<T> {
    private final List<T> values;

    public PriorityComparator2(T... values) {
        this.values = Arrays.asList(values);
    }

    @Override public int compare(T o1, T o2) {
        int idx1 = values.indexOf(o1);
        int idx2 = values.indexOf(o2);
        if (idx1 > -1) {
            return idx2 > -1 ? idx1 - idx2 : -1;
        }
        return idx2 > -1 ? 1 : o1.compareTo(o2);
    }
}



回答6:


If you were considering using explicit ordering to begin with, it kind of assumes your list did not have duplicates. At which point, FluentIterable and .toSet() can make this trivial. Duplicates will be simply ignored (as opposed to erroring out).

Iterable<String> sorted = FluentIterable.of(first).append(values).toSet();
    or
ImmutableList<String> sorted =
    FluentIterable.of(first).append(values).toSet().toList();

IMO, your first suggestion is actually not that bad, and also works if your list has duplicate values non-first. If you use a FluentIterable though, it looks better, as you can concat mixed Iterable and elements types:

Iterable<String> others = Iterables.filter(values, not(equalTo(first)));
Iterable<String> sorted = FluentIterable.of(first).append(others);

The catch here though is that if you have more than 1 "first" element, you'll lose the copies.

The fix is trivial though:

Iterable<String> firsts = Iterables.filter(values, equalTo(first)));
Iterable<String> others = Iterables.filter(values, not(equalTo(first));
Iterable<String> sorted = FluentIterable.from(firsts).append(others);

This requires iterating your collection twice, but the algorithm is trivial, and probably faster than anything Comparator based. If I had to code review such an implementation, I'd accept this without batting an eye, because it's super readable/maintainable, and I'd have 100% faith it works as intended.

If all else fails though, manual iteration never hurt anyone:

List<String> firsts = new ArrayList<>();
List<String> others = new ArrayList<>();
values.forEach(element -> (first.equal(element) ? firsts : others).add(element));
Iterable<String> sorted = FluentIterable.from(firsts).append(others);

Finally, note that since these use FluentIterable, getting a collection (ImmutableList) out of these is as trivial as appending .toList() to you FluentIterable.




回答7:


This also sounds like a "ranking" sort, where objects that "are first" have a higher weigh: So the 1-liner ordering would be:

Ordering.explicit(true, false).onResultOf(first::equals);
   or the more general
Ordering.natural().reverse().onResultOf(rankFunction);


来源:https://stackoverflow.com/questions/14403114/guava-how-to-create-an-explicit-ordering-from-a-list-and-a-single-element

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!