sort and group a java collection - java

I have an object which has a name and a score. I would like to sort a collection of such objects so that they are grouped by name and sorted by maximum score in each group (and within the group by descending score as well).
let me demonstrate what I intend to achieve. assume I have these objects(name, score):
(a, 3)
(a, 9)
(b, 7)
(b, 10)
(c, 8)
(c, 3)
then I would like them to be sorted like this:
(b, 10)
(b, 7)
(a, 9)
(a, 3)
(c, 8)
(c, 3)
is this feasible with a Comparator? I can't figure it out, so any hints would be appreciated.

No, you can't do it with a single sort with a single Comparator.
You have to:
group by name
sort the groups, by highest score in group
Then you need to flatten the groups back to a list.
With Java 8
Edit: Since i wrote this answer, Java 8 has come out, which simplifies the problem a lot:
import java.util.*;
import static java.util.Comparator.*;
import static java.util.stream.Collectors.*;
List<Record> result = records.stream()
.sorted(comparingInt(Record::getScore).reversed())
.collect(groupingBy(Record::getName, LinkedHashMap::new, toList()))
.values().stream()
.flatMap(Collection::stream)
.collect(toList());
First we sort by score reversed, and then we group using a LinkedHashMap, which will preserve the insertion order for the keys, so keys with higher score will come first.
Sorting first is OK if the groups are small, so the redundant compares between objects in different groups don't hurt so much.
Also, with this method, duplicates are preserved.
Alternatively, if you don't care about preserving duplicates, you can:
Comparator<Record> highestScoreFirst = comparingInt(Record::getScore).reversed();
List<Record> result = records.stream()
.collect(groupingBy(Record::getName,
toCollection(() -> new TreeSet<>(highestScoreFirst))))
.values().stream()
.sorted(comparing(SortedSet::first, highestScoreFirst))
.flatMap(Collection::stream)
.collect(toList());
Where the records are grouped into sorted TreeSets, instead of sorting the values as the first operation of the stream, and then the sets are sorted by their first, highest value.
Grouping before sorting is appropriate if the groups are big, to cut down on redundant compares.
Implementing Comparable:
And you can make it shorter by having your record implement Comparable
public class Record implements Comparable<Record> {
#Override
public int compareTo(Record other) {
// Highest first
return -Integer.compare(getScore(), other.getScore());
/* Or equivalently:
return Integer.compare(other.getScore(), getScore());
*/
}
...
}
List<Record> result = records.stream()
.collect(groupingBy(Record::getName, toCollection(TreeSet::new)))
.values().stream()
.sorted(comparing(SortedSet::first))
.flatMap(Collection::stream)
.collect(toList());
Before Java 8
Edit: Here is a really rough unit test that demonstrates one way to do it. I haven't cleaned it up as much as i would have liked.
Stuff like this is painful in Java, and i would normally use Google Guava for this.
import org.junit.Test;
import java.util.*;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
public class GroupSortTest {
#Test
public void testGroupSort() {
List<Record> records = asList(
new Record("a", 3),
new Record("a", 9),
new Record("b", 7),
new Record("b", 10),
new Record("c", 8),
new Record("c", 3));
List<SortedMap<Integer, Record>> recordsGroupedByName = groupRecordsByNameAndSortedByScoreDescending(records);
Collections.sort(recordsGroupedByName, byHighestScoreInGroupDescending());
List<Record> result = flattenGroups(recordsGroupedByName);
List<Record> expected = asList(
new Record("b", 10),
new Record("b", 7),
new Record("a", 9),
new Record("a", 3),
new Record("c", 8),
new Record("c", 3));
assertEquals(expected, result);
}
private List<Record> flattenGroups(List<SortedMap<Integer, Record>> recordGroups) {
List<Record> result = new ArrayList<Record>();
for (SortedMap<Integer, Record> group : recordGroups) {
result.addAll(group.values());
}
return result;
}
private List<SortedMap<Integer, Record>> groupRecordsByNameAndSortedByScoreDescending(List<Record> records) {
Map<String, SortedMap<Integer, Record>> groupsByName = new HashMap<String, SortedMap<Integer, Record>>();
for (Record record : records) {
SortedMap<Integer, Record> group = groupsByName.get(record.getName());
if (null == group) {
group = new TreeMap<Integer, Record>(descending());
groupsByName.put(record.getName(), group);
}
group.put(record.getScore(), record);
}
return new ArrayList<SortedMap<Integer, Record>>(groupsByName.values());
}
private DescendingSortComparator descending() {
return new DescendingSortComparator();
}
private ByFirstKeyDescending byHighestScoreInGroupDescending() {
return new ByFirstKeyDescending();
}
private static class ByFirstKeyDescending implements Comparator<SortedMap<Integer, Record>> {
public int compare(SortedMap<Integer, Record> o1, SortedMap<Integer, Record> o2) {
return o2.firstKey().compareTo(o1.firstKey());
}
}
private static class DescendingSortComparator implements Comparator<Comparable> {
public int compare(Comparable o1, Comparable o2) {
return o2.compareTo(o1);
}
}
}

Foreach over the collection, and put the objects into a Map<String, SortedSet<YourObject>>, keyed by name, where the SortedSet is a TreeSet with a custom comparator that compares by score.
Then foreach over the map's values() collection, and put the groups into a SortedSet<SortedSet<YourObject>>, with a second custom comparator that compares SortedSets according to their largest element. Actually, instead of foreaching, you can simply use addAll().
Here's the code:
public class SortThings {
static class Thing {
public final String name;
public final int score;
public Thing(String name, int score) {
this.name = name;
this.score = score;
}
#Override
public String toString() {
return "(" + name + ", " + score + ")";
}
}
public static void main(String[] args) {
Collection<Thing> things = Arrays.asList(
new Thing("a", 3),
new Thing("a", 9),
new Thing("b", 7),
new Thing("b", 10),
new Thing("c", 8),
new Thing("c", 3)
);
SortedSet<SortedSet<Thing>> sortedGroups = sortThings(things);
System.out.println(sortedGroups);
}
private static SortedSet<SortedSet<Thing>> sortThings(Collection<Thing> things) {
final Comparator<Thing> compareThings = new Comparator<Thing>() {
public int compare(Thing a, Thing b) {
Integer aScore = a.score;
Integer bScore = b.score;
return aScore.compareTo(bScore);
}
};
// first pass
Map<String, SortedSet<Thing>> groups = new HashMap<String, SortedSet<Thing>>();
for (Thing obj: things) {
SortedSet<Thing> group = groups.get(obj.name);
if (group == null) {
group = new TreeSet<Thing>(compareThings);
groups.put(obj.name, group);
}
group.add(obj);
}
// second pass
SortedSet<SortedSet<Thing>> sortedGroups = new TreeSet<SortedSet<Thing>>(new Comparator<SortedSet<Thing>>() {
public int compare(SortedSet<Thing> a, SortedSet<Thing> b) {
return compareThings.compare(a.last(), b.last());
}
});
sortedGroups.addAll(groups.values());
return sortedGroups;
}
}
Note that the output is in smallest-to-largest order. That's the natural order with Java's collections; it would be trivial to modify this to sort the other way if that's what you need.

public class ScoreComparator implements Comparator<Item>
{
public int compare(Item a, Item b){
if (a.name.equals(b.name){
return a.score.compareTo(b.score);
}
return a.name.compareTo(b.Name);
}
}

I think you can do this. First check to see if the group is equal. If it is then compare on score. Otherwise return which group you want to be more on top. Let me code it up.
class Item{
String name;
int score;
}
new Comparator<Item>(){
#Override
public int compare(Item o1, Item o2) {
if (o1.name.equals(o2.name)) {
return o1.score > o2.score ? 1 : -1; // might have to flip this. I didn't test
}else {
return o1.name.compareTo(o2.name);
}
}
};

Yes Go for Comparator
Give first preference to name in comparison and then to score. it will be grouped up with sorted score also
List<Score> scores = new ArrayList<Score>();
scores.add(new Score("a", 58));
scores.add(new Score("a", 10));
scores.add(new Score("b", 165));
scores.add(new Score("a", 1));
scores.add(new Score("b", 1658));
scores.add(new Score("c", 1));
scores.add(new Score("c", 10));
scores.add(new Score("c", 0));
Collections.sort(scores, new Comparator<Score>() {
public int compare(Score o1, Score o2) {
if (o1.getName().compareTo(o2.getName()) == 0) {
return o2.getScore() - o1.getScore();
} else {
return o1.getName().compareTo(o2.getName());
}
}
});
System.out.println(scores);
Update
As Chris pointed out.
import java.util.*;
/**
*
* #author Jigar
*/
class Score {
private String name;
private List<Integer> scores;
public Score() {
}
public Score(String name, List<Integer> scores) {
this.name = name;
this.scores = scores;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Integer> getScores() {
return scores;
}
public void setScores(List<Integer> scores) {
this.scores = scores;
}
#Override
public String toString() {
return name + " , " + scores + "\n";
}
}
public class ScoreDemo {
public static void main(String[] args) {
List<Score> scores = new ArrayList<Score>();
List<Integer> lstA = new ArrayList<Integer>();
lstA.add(3);
lstA.add(9);
lstA.add(7);
Collections.sort(lstA);
Collections.reverse(lstA);
List<Integer> lstB = new ArrayList<Integer>();
lstB.add(10);
lstB.add(8);
lstB.add(3);
Collections.sort(lstB);
Collections.reverse(lstB);
List<Integer> lstC = new ArrayList<Integer>();
lstC.add(8);
lstC.add(3);
Collections.sort(lstC);
Collections.reverse(lstC);
scores.add(new Score("a", lstA));
scores.add(new Score("b", lstB));
scores.add(new Score("c", lstC));
Collections.sort(scores, new Comparator<Score>() {
public int compare(Score o1, Score o2) {
return o2.getScores().get(0).compareTo(o1.getScores().get(0));
}
});
System.out.println(scores);
}
}
Here is working ideone demo
UPDATE: Here is working ideone demo

Related

Sorting a Map on First by value then by Key [duplicate]

This question already has answers here:
Sort a Map<Key, Value> by values
(64 answers)
Closed 4 years ago.
I am actually able to sort my Map on the basis of both Key and value alone, I even tried to sort them like below:
I sorted the students on the basis of country , and if two students happen to have same states then sort by StudentID only among the matched country.
What i have tried so far:
final Map<Integer, String> studentMaster = new HashMap<>() {{
put(146, "Sweden");
put(148, "Sweden");
put(110, "Orebro");
put(6, "Malmo");
put(14, "Orebro");
put(26, "Malmo");
}
};
studentMaster.entrySet().stream()
.sorted((i1,i2)->i1.getValue().compareTo(i2.getValue()))
.sorted((j1,j2)->j1.getKey().compareTo(j2.getKey()))
.forEach(System.out::println);
The result that I am getting**( actual output )**
14=Orebro
26=Malmo
110=Orebro
146=Sweden
148=Sweden
Expected Output:
26=Malmo
14=Orebro
110=Orebro
146=Sweden
148=Sweden
Note: Your expected and actual outputs don't match up with the keys that you added to your Map.
The reason that your code doesn't work is because you're calling Stream#sorted twice with two separate Comparators, so the first call to Stream#sorted is useless in your case (as it's overridden by the second call).
I was able to achieve your expected output by passing a custom Comparator to Stream#sorted:
Map.Entry.<Integer, String>comparingByValue()
.thenComparing(Map.Entry.comparingByKey())
Output:
6=Malmo
26=Malmo
14=Orebro
110=Orebro
146=Sweden
148=Sweden
Sometime back i gave answer to How to sort the name along with age in java , Many similarities to your question apart from data-structure used for storage.
To traverse in each key and sort it and then again in value and then sort it is quite tedious and can get you hell a lot confused. just remember how you used to traverse in Map when you did not used to use Stream :
for (Map.Entry<String,String> entry : somemap.entrySet()){..Some Statements..};
studentMaster.entrySet().stream()
.sorted(Comparator.comparing((Map.Entry<Integer, String> m) -> m.getValue())
.thenComparing(Map.Entry::getKey)).forEach(System.out::println);
Output
6=Malmo
26=Malmo
14=Orebro
110=Orebro
146=Sweden
148=Sweden
The Comparator should look like this:
Comparator<Entry<Integer, String>> comparator = (o1, o2) -> {
int i = o1.getValue().compareTo(o2.getValue());
if (i == 0) {
return o1.getKey().compareTo(o2.getKey());
} else {
return i;
}
};
And then pass it to the Stream#sorted method: studentMaster.entrySet().stream().sorted(comparator).forEach(System.out::println);
Output:
6=Malmo
26=Malmo
14=Orebro
110=Orebro
146=Sweden
148=Sweden
2 ways:
Use TreeSet with Comparable pojo.
Use TreeSet with customized Comparator.
Code
Tmp.java
(Use TreeSet with Comparable pojo.)
import java.util.*;
public class Tmp {
static class StudentMaster implements Comparable<StudentMaster> {
private Integer id;
private String master;
public StudentMaster(Integer id, String master) {
this.id = id;
this.master = master;
}
#Override
public int compareTo(StudentMaster other) {
int masterFlag = master.compareTo(other.master);
return (masterFlag == 0) ? id.compareTo(other.id) : masterFlag;
}
#Override
public boolean equals(Object o) {
StudentMaster osm = (StudentMaster) o;
return id == osm.id && master.equals(osm.master);
}
#Override
public int hashCode() {
return Objects.hash(id, master);
}
public String toString() {
StringBuilder sb = new StringBuilder();
Formatter fm = new Formatter(sb);
fm.format("id = %d, master = %s\n", id, master);
fm.close();
return sb.toString();
}
}
public static void test() {
final Set<StudentMaster> smSet = new TreeSet<>();
smSet.add(new StudentMaster(146, "Sweden"));
smSet.add(new StudentMaster(148, "Sweden"));
smSet.add(new StudentMaster(110, "Orebro"));
smSet.add(new StudentMaster(6, "Malmo"));
smSet.add(new StudentMaster(14, "Orebro"));
smSet.add(new StudentMaster(26, "Malmo"));
for (StudentMaster sm : smSet) {
System.out.print(sm);
}
}
public static void main(String[] args) {
test();
}
}
TmpComparator.java
(Use TreeSet with customized Comparator.)
import java.util.*;
public class TmpComparator {
static Comparator<StudentMaster> smc = new Comparator() {
#Override
public int compare(Object o1, Object o2) {
StudentMaster sm1 = (StudentMaster) o1, sm2 = (StudentMaster) o2;
int masterFlag = sm1.master.compareTo(sm2.master);
return (masterFlag == 0) ? sm1.id.compareTo(sm2.id) : masterFlag;
}
};
static class StudentMaster {
private Integer id;
private String master;
public StudentMaster(Integer id, String master) {
this.id = id;
this.master = master;
}
#Override
public boolean equals(Object o) {
StudentMaster osm = (StudentMaster) o;
return id == osm.id && master.equals(osm.master);
}
#Override
public int hashCode() {
return Objects.hash(id, master);
}
public String toString() {
StringBuilder sb = new StringBuilder();
Formatter fm = new Formatter(sb);
fm.format("id = %d, master = %s\n", id, master);
fm.close();
return sb.toString();
}
}
public static void test() {
final Set<StudentMaster> smSet = new TreeSet<>(smc);
smSet.add(new StudentMaster(146, "Sweden"));
smSet.add(new StudentMaster(148, "Sweden"));
smSet.add(new StudentMaster(110, "Orebro"));
smSet.add(new StudentMaster(6, "Malmo"));
smSet.add(new StudentMaster(14, "Orebro"));
smSet.add(new StudentMaster(26, "Malmo"));
for (StudentMaster sm : smSet) {
System.out.print(sm);
}
}
public static void main(String[] args) {
test();
}
}
Just run the main() method.
Output of both are the same:
id = 6, master = Malmo
id = 26, master = Malmo
id = 14, master = Orebro
id = 110, master = Orebro
id = 146, master = Sweden
id = 148, master = Sweden
Tips
In production code, the equals() need to be improved, this is a simplified version for test only.

LinkedHashMap<double[], Integer>, cannot access Integer with .get or .containsKey

I have a series of small arrays (consisting of two doubles), many of which are
the same. E.g.
{5.0, 15.0}
{5.0, 15.0}
{5.0, 15.0}
{12.0, 8.0}
{10.0, 8.0}
{10.0, 8.0}
I want to be able to count the number of each arrays, i.e.
3 of {5.0, 15.0}
1 of {12.0, 8.0}
2 of {10.0, 8.0}
To do this, I tried making use of a LinkedHashMap (linked, because the order
might come into use later on):
import java.util.Map;
import java.util.LinkedHashMap;
public class MapArrayInt {
Map<double[], Integer> arrays = new LinkedHashMap<double[], Integer>();
public static void main(String[] args) {
MapArrayInt mapArrayInt = new MapArrayInt();
mapArrayInt.addArray(5.0, 15.0);
mapArrayInt.addArray(5.0, 15.0);
mapArrayInt.addArray(5.0, 15.0);
mapArrayInt.addArray(12.0, 8.0);
mapArrayInt.addArray(10.0, 8.0);
mapArrayInt.addArray(10.0, 8.0);
System.out.println(String.valueOf(mapArrayInt.arrays.get(new double[]{5.0, 15.0})));
System.out.println(String.valueOf(mapArrayInt.arrays.get(new double[]{12.0, 8.0})));
System.out.println(String.valueOf(mapArrayInt.arrays.get(new double[]{10.0, 8.0})));
}
void addArray(double val1, double val2) {
double[] newArray = new double[]{val1, val2};
if (!arrays.containsKey(newArray)) {
arrays.put(newArray, 1);
} else {
arrays.put(newArray, arrays.get(newArray) + 1);
}
}
}
I expected this output,
3
1
2
but got,
null
null
null
I'm quite new to Java, but I suspect this might be because each double[] counts as a unique because they are different instances, even though they contain the same two doubles.
How can I fix this, if I should at all (is there a better way)? I just need a data structure that allows me to
Add doubles[]
Preserves order of doubles[]
Easily iterate through to get doubles[] and number of said doubles[]
As I stated in my comment, with new you're creating a new instance of an object. Which means that the arrays you added with mapArrayInt.addArray(5.0, 15.0); and the arrays in mapArrayInt.arrays.get(new double[]{5.0, 15.0}) reference different objects. That's why you get null, because for the map those are different keys.
In order to circumvent this, you could create a custom wrapper class
import java.util.Arrays;
public class Exercise {
private final double[] array;
public Exercise(double first, double second) {
this.array = new double[]{first, second};
}
public boolean equals(Object obj) {
if(!(obj instanceof Exercise)) {
return false;
}
Exercise other = (Exercise)obj;
return Arrays.equals(this.array, other.array);
}
public int hashCode() {
return Arrays.hashCode(array);
}
}
The equals and hashCode methods are important, when you want to use this class in collections like Map, otherwise the hashcode of Object is used for checking equality and you'd have the same problem as you have now.
Then, in your main class you can use it like so:
void addArray(double val1, double val2) {
Exercise exercise = new Exercise(val1, val2);
if (!arrays.containsKey(exercise)) {
arrays.put(exercise, 1);
} else {
arrays.put(exercise, arrays.get(exercise) + 1);
}
}
And System.out.println(String.valueOf(mapArrayInt.arrays.get(new Exercise(5.0, 15.0))));
EDIT:
I changed one of the doubles to int (you said you're representing reps and weight ... and reps can only be a natural number right?)
You could build create an Exercise-Class like below and use the static method "of" to create the instances:
package somepackage;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
public class Exercise
{
private static final Map<Integer, Map<Double, WeakReference<Exercise>>> instances = new HashMap<>();
private final int reps;
private final double weight;
private Exercise(int reps, double weight)
{
this.reps = reps;
this.weight = weight;
}
public static Exercise of(int reps, double weight)
{
if (!instances.containsKey(reps))
{
instances.put(reps, new HashMap<>());
}
Map<Double, WeakReference<Exercise>> innerMap = instances.get(reps);
WeakReference<Exercise> weakRef = innerMap.get(weight);
Exercise instance = null;
if (weakRef != null)
{
instance = weakRef.get();
}
if (weakRef == null || instance == null || weakRef.isEnqueued())
{
instance = new Exercise(reps, weight);
innerMap.put(weight, new WeakReference<>(instance));
}
return instance;
}
public int getReps()
{
return this.reps;
}
public double getWeight()
{
return this.weight;
}
}
And then you could put those exercises in a map like below:
public void addArray(int reps, double weight)
{
Exercise exercise = Exercise.of(reps, weight);
if (!arrays.containsKey(exercise))
{
arrays.put(exercise, 1);
}
else
{
arrays.put(exercise, arrays.get(exercise) + 1);
}
}
OR:
Instead of an double[] as key you can use the a Map<Double, Integer> as your value for 2 values:
package somepackage;
import java.util.HashMap;
import java.util.Map;
public class MapArrayInt
{
private final Map<Double, Map<Double, Integer>> values;
public MapArrayInt()
{
this.values = new HashMap<>();
}
public void addArray(double val1, double val2)
{
if (!this.values.containsKey(val1))
{
this.values.put(val1, new HashMap<>());
}
Map<Double, Integer> innerValues = this.values.get(val1);
if (innerValues.containsKey(val2))
{
innerValues.put(val2, innerValues.get(val2) + 1);
}
else
{
innerValues.put(val2, 1);
}
}
public int getArrayValue(double val1, double val2)
{
Map<Double, Integer> innerValues = this.values.get(val1);
if (innerValues == null)
{
// you may also throw an Exception here
return 0;
}
Integer value = innerValues.get(val2);
if (value == null)
{
// also here you may throw an Exception
return 0;
}
return value;
}
public int getArrayValue(double[] values)
{
return getArrayValue(values[0], values[1]);
}
}

How to create 2 depends Comparators in java?

Lets say I have a Product class in Java and 2 Comparators:
1st is price Comparator for asc order.
2nd is price Comparator for desc order.
It can be that if I changed the 1st to be product name Comparator, so, the 2nd will change automatic to name Comparator as well?
Thanks alot!
Exmaple:
class ProductComparatorByPriceDesc implements Comparator<Customer> {
#Override
public int compare(Product o1, Product o2) {
return o1.getPrice() - o2.getPrice();
}
}
Class ProductComparatorByPriceAsc implements Comparator<Customer> {
#Override
public int compare(Customer o1, Customer o2) {
return o2.getPrice() - o1.getPrice();
}
}
So if i changed the 1st comparator to sort by name, not price, the 2nd will changed as well, but not the opposite!
One way would be:
import java.util.*;
class SomeClass {
public int price;
public String name;
public SomeClass(String name, int price) {
this.name = name;
this.price = price;
}
}
class PriceOrNameComparator implements Comparator<SomeClass> {
boolean compareByPrice;
public PriceOrNameComparator byPrice() {
this.compareByPrice = true;
return this;
}
public PriceOrNameComparator byName() {
this.compareByPrice = false;
return this;
}
public int compare(SomeClass a, SomeClass b) {
if (compareByPrice) {
return a.price - b.price;
} else {
return a.name.compareTo(b.name);
}
}
public Comparator<SomeClass> reverseComparator() {
return new Comparator<SomeClass>() {
public int compare(SomeClass a, SomeClass b) {
int res = PriceOrNameComparator.this.compare(a, b);
if (res == 0) {
return 0;
} else {
return (res > 0) ? -1 : 1;
}
}
};
}
}
class Test {
public static void main(String[] args) {
SomeClass s1 = new SomeClass("a", 5);
SomeClass s2 = new SomeClass("b", 4);
PriceOrNameComparator c = new PriceOrNameComparator().byPrice();
Comparator<SomeClass> r = c.reverseComparator();
System.out.println(c.compare(s1, s2)); // 1
System.out.println(r.compare(s1, s2)); // -1
c.byName();
System.out.println(c.compare(s1, s2)); // -1
System.out.println(r.compare(s1, s2)); // 1
}
}
Basically, the outer comparator is configurable, and the inner, reverse order, comparator, being an anonymous inner class, has an implicit reference to the outer comparator and can observe changes in its state.
I would suggest only having a single comparator class for comparing by price, and a separate comparator class to compare by name (or no classes - see the end of my answer). Each class does one thing, and does it well.
Then you can reverse any comparator using the Comparator.reversed default method... and likewise you can chain them together using Comparator.thenComparing, should you wish to order by name and then price, for example:
Comparator<Product> nameThenPrice =
new NameComparator().thenComparing(new PriceComparator());
(If you're not using Java 8, it's easy enough to write a ReversingComparator which takes an existing one, and a CompoundComparator which takes two existing ones.)
You can also use Java 8's static methods in Comparator:
Comparator<Product> byName = Comparator.comparing(p -> p.getName());
Comparator<Product> byPrice = Comparator.comparing(p -> p.getPrice());
Comparator<Product> nameThenPrice = byName.thenComparing(byPrice);
That way you often don't need to implement Comparator at all manually.

Using Super Class and Extend From It

I want to sort some team base on their points (win: 3 points, draw: 1 points and defeat: 0 points). I have an array which contains some teams' names and their results, I have extracted their scores and then sorted them descending. Here is the code I have used:
import java.util.*;
import java.util.Map.Entry;
public class SCAN{
public static void main(String[] args) {
Map<String, Integer> points = new HashMap<>();
String[] challenges = new String[]{
"Team(A) 2 - 1 Team(C)",
"Team(D) 3 - 1 Team(A)",
"Team(B) 3 - 3 Team(D)",
"Team(C) 0 - 4 Team(B)",
"Team(B) 2 - 0 Team(A)"
};
calcualte(challenges, points);
List<Entry<String, Integer>> entries = new ArrayList<Entry<String, Integer>>(
points.entrySet());
Collections.sort(entries, new Comparator<Entry<String, Integer>>() {
public int compare(Entry<String, Integer> e1,
Entry<String, Integer> e2) {
return e2.getValue().compareTo(e1.getValue()); // Sorts
// descending.
}
});
Map<String, Integer> orderedMap = new LinkedHashMap<String, Integer>();
for (Entry<String, Integer> entry : entries) {
orderedMap.put(entry.getKey(), entry.getValue());
}
for (Entry<String, Integer> element : entries) {
System.out.println(element);
}
}
private static void calcualte(String[] challenges, Map<String, Integer> points) {
List<String> challengeList = new ArrayList<>();
for (String str : challenges) {
String[] bits = str.trim().split(" ");
String firstTeam = bits[0];
String lastTeam = bits[bits.length - 1];
if (!challengeList.contains(firstTeam)) {
challengeList.add(firstTeam);
}
if (!challengeList.contains(lastTeam)) {
challengeList.add(lastTeam);
}
int firstScore = Integer.parseInt(bits[1]);
int lastScore = Integer.parseInt(bits[3]);
if (firstScore > lastScore) {
insert(3, points, firstTeam);
insert(0, points, lastTeam);
} else if (firstScore < lastScore) {
insert(3, points, lastTeam);
insert(0, points, firstTeam);
} else {
insert(1, points, firstTeam);
insert(1, points, lastTeam);
}
{
}
}
}
private static void insert(int i, Map<String, Integer> points, String team) {
int score = points.containsKey(team) ? points.get(team) : 0;
score += i;
points.put(team, score);
}
}
I know this, but I should do it by using a superclass So I need a subclass which its name is Plays and should be extended from a super class which its name is Playclass . The Playclass should not be changed. This class needs three abstracted method which should be overridden, they are won(),drawn() and defeated(). The other method which should to be overridden, is Print() which shows each team's information, like bellow:
public abstract class Playclass {
private String name;
Playclass(String name){
this.name=name;
}
public String getName() {
return name;
}
public abstract void print();
public abstract void won();
public abstract void drawn();
public abstract void defeated();
}
The main class name could be Playleage and it should contain the String[] challenges = new String[]{…} which I showed in the first code. I know how to sort them base on their scores (like the first code) but I don’t know how to use the classes which I said. May someone help me? thanks

Collections.sort with multiple fields

I have a list of "Report" objects with three fields (All String type)-
ReportKey
StudentNumber
School
I have a sort code goes like-
Collections.sort(reportList, new Comparator<Report>() {
#Override
public int compare(final Report record1, final Report record2) {
return (record1.getReportKey() + record1.getStudentNumber() + record1.getSchool())
.compareTo(record2.getReportKey() + record2.getStudentNumber() + record2.getSchool());
}
});
For some reason, I don't have the sorted order. One advised to put spaces in between fields, but why?
Do you see anything wrong with the code?
(originally from Ways to sort lists of objects in Java based on multiple fields)
Original working code in this gist
Using Java 8 lambda's (added April 10, 2019)
Java 8 solves this nicely by lambda's (though Guava and Apache Commons might still offer more flexibility):
Collections.sort(reportList, Comparator.comparing(Report::getReportKey)
.thenComparing(Report::getStudentNumber)
.thenComparing(Report::getSchool));
Thanks to #gaoagong's answer below.
Note that one advantage here is that the getters are evaluated lazily (eg. getSchool() is only evaluated if relevant).
Messy and convoluted: Sorting by hand
Collections.sort(pizzas, new Comparator<Pizza>() {
#Override
public int compare(Pizza p1, Pizza p2) {
int sizeCmp = p1.size.compareTo(p2.size);
if (sizeCmp != 0) {
return sizeCmp;
}
int nrOfToppingsCmp = p1.nrOfToppings.compareTo(p2.nrOfToppings);
if (nrOfToppingsCmp != 0) {
return nrOfToppingsCmp;
}
return p1.name.compareTo(p2.name);
}
});
This requires a lot of typing, maintenance and is error prone. The only advantage is that getters are only invoked when relevant.
The reflective way: Sorting with BeanComparator
ComparatorChain chain = new ComparatorChain(Arrays.asList(
new BeanComparator("size"),
new BeanComparator("nrOfToppings"),
new BeanComparator("name")));
Collections.sort(pizzas, chain);
Obviously this is more concise, but even more error prone as you lose your direct reference to the fields by using Strings instead (no typesafety, auto-refactorings). Now if a field is renamed, the compiler won’t even report a problem. Moreover, because this solution uses reflection, the sorting is much slower.
Getting there: Sorting with Google Guava’s ComparisonChain
Collections.sort(pizzas, new Comparator<Pizza>() {
#Override
public int compare(Pizza p1, Pizza p2) {
return ComparisonChain.start().compare(p1.size, p2.size).compare(p1.nrOfToppings, p2.nrOfToppings).compare(p1.name, p2.name).result();
// or in case the fields can be null:
/*
return ComparisonChain.start()
.compare(p1.size, p2.size, Ordering.natural().nullsLast())
.compare(p1.nrOfToppings, p2.nrOfToppings, Ordering.natural().nullsLast())
.compare(p1.name, p2.name, Ordering.natural().nullsLast())
.result();
*/
}
});
This is much better, but requires some boiler plate code for the most common use case: null-values should be valued less by default. For null-fields, you have to provide an extra directive to Guava what to do in that case. This is a flexible mechanism if you want to do something specific, but often you want the default case (ie. 1, a, b, z, null).
And as noted in the comments below, these getters are all evaluated immediately for each comparison.
Sorting with Apache Commons CompareToBuilder
Collections.sort(pizzas, new Comparator<Pizza>() {
#Override
public int compare(Pizza p1, Pizza p2) {
return new CompareToBuilder().append(p1.size, p2.size).append(p1.nrOfToppings, p2.nrOfToppings).append(p1.name, p2.name).toComparison();
}
});
Like Guava’s ComparisonChain, this library class sorts easily on multiple fields, but also defines default behavior for null values (ie. 1, a, b, z, null). However, you can’t specify anything else either, unless you provide your own Comparator.
Again, as noted in the comments below, these getters are all evaluated immediately for each comparison.
Thus
Ultimately it comes down to flavor and the need for flexibility (Guava’s ComparisonChain) vs. concise code (Apache’s CompareToBuilder).
Bonus method
I found a nice solution that combines multiple comparators in order of priority on CodeReview in a MultiComparator:
class MultiComparator<T> implements Comparator<T> {
private final List<Comparator<T>> comparators;
public MultiComparator(List<Comparator<? super T>> comparators) {
this.comparators = comparators;
}
public MultiComparator(Comparator<? super T>... comparators) {
this(Arrays.asList(comparators));
}
public int compare(T o1, T o2) {
for (Comparator<T> c : comparators) {
int result = c.compare(o1, o2);
if (result != 0) {
return result;
}
}
return 0;
}
public static <T> void sort(List<T> list, Comparator<? super T>... comparators) {
Collections.sort(list, new MultiComparator<T>(comparators));
}
}
Ofcourse Apache Commons Collections has a util for this already:
ComparatorUtils.chainedComparator(comparatorCollection)
Collections.sort(list, ComparatorUtils.chainedComparator(comparators));
Do you see anything wrong with the code?
Yes. Why are you adding the three fields together before you compare them?
I would probably do something like this: (assuming the fields are in the order you wish to sort them in)
#Override public int compare(final Report record1, final Report record2) {
int c;
c = record1.getReportKey().compareTo(record2.getReportKey());
if (c == 0)
c = record1.getStudentNumber().compareTo(record2.getStudentNumber());
if (c == 0)
c = record1.getSchool().compareTo(record2.getSchool());
return c;
}
I'd make a comparator using Guava's ComparisonChain:
public class ReportComparator implements Comparator<Report> {
public int compare(Report r1, Report r2) {
return ComparisonChain.start()
.compare(r1.getReportKey(), r2.getReportKey())
.compare(r1.getStudentNumber(), r2.getStudentNumber())
.compare(r1.getSchool(), r2.getSchool())
.result();
}
}
This is an old question so I don't see a Java 8 equivalent. Here is an example for this specific case.
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Compares multiple parts of the Report object.
*/
public class SimpleJava8ComparatorClass {
public static void main(String[] args) {
List<Report> reportList = new ArrayList<>();
reportList.add(new Report("reportKey2", "studentNumber2", "school1"));
reportList.add(new Report("reportKey4", "studentNumber4", "school6"));
reportList.add(new Report("reportKey1", "studentNumber1", "school1"));
reportList.add(new Report("reportKey3", "studentNumber2", "school4"));
reportList.add(new Report("reportKey2", "studentNumber2", "school3"));
System.out.println("pre-sorting");
System.out.println(reportList);
System.out.println();
Collections.sort(reportList, Comparator.comparing(Report::getReportKey)
.thenComparing(Report::getStudentNumber)
.thenComparing(Report::getSchool));
System.out.println("post-sorting");
System.out.println(reportList);
}
private static class Report {
private String reportKey;
private String studentNumber;
private String school;
public Report(String reportKey, String studentNumber, String school) {
this.reportKey = reportKey;
this.studentNumber = studentNumber;
this.school = school;
}
public String getReportKey() {
return reportKey;
}
public void setReportKey(String reportKey) {
this.reportKey = reportKey;
}
public String getStudentNumber() {
return studentNumber;
}
public void setStudentNumber(String studentNumber) {
this.studentNumber = studentNumber;
}
public String getSchool() {
return school;
}
public void setSchool(String school) {
this.school = school;
}
#Override
public String toString() {
return "Report{" +
"reportKey='" + reportKey + '\'' +
", studentNumber='" + studentNumber + '\'' +
", school='" + school + '\'' +
'}';
}
}
}
If you want to sort by report key, then student number, then school, you should do something like this:
public class ReportComparator implements Comparator<Report>
{
public int compare(Report r1, Report r2)
{
int result = r1.getReportKey().compareTo(r2.getReportKey());
if (result != 0)
{
return result;
}
result = r1.getStudentNumber().compareTo(r2.getStudentNumber());
if (result != 0)
{
return result;
}
return r1.getSchool().compareTo(r2.getSchool());
}
}
This assumes none of the values can be null, of course - it gets more complicated if you need to allow for null values for the report, report key, student number or school.
While you could get the string concatenation version to work using spaces, it would still fail in strange cases if you had odd data which itself included spaces etc. The above code is the logical code you want... compare by report key first, then only bother with the student number if the report keys are the same, etc.
I suggest to use Java 8 Lambda approach:
List<Report> reportList = new ArrayList<Report>();
reportList.sort(Comparator.comparing(Report::getRecord1).thenComparing(Report::getRecord2));
Sorting with multiple fields in Java8
package com.java8.chapter1;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import static java.util.Comparator.*;
public class Example1 {
public static void main(String[] args) {
List<Employee> empList = getEmpList();
// Before Java 8
empList.sort(new Comparator<Employee>() {
#Override
public int compare(Employee o1, Employee o2) {
int res = o1.getDesignation().compareTo(o2.getDesignation());
if (res == 0) {
return o1.getSalary() > o2.getSalary() ? 1 : o1.getSalary() < o2.getSalary() ? -1 : 0;
} else {
return res;
}
}
});
for (Employee emp : empList) {
System.out.println(emp);
}
System.out.println("---------------------------------------------------------------------------");
// In Java 8
empList.sort(comparing(Employee::getDesignation).thenComparing(Employee::getSalary));
empList.stream().forEach(System.out::println);
}
private static List<Employee> getEmpList() {
return Arrays.asList(new Employee("Lakshman A", "Consultent", 450000),
new Employee("Chaitra S", "Developer", 250000), new Employee("Manoj PVN", "Developer", 250000),
new Employee("Ramesh R", "Developer", 280000), new Employee("Suresh S", "Developer", 270000),
new Employee("Jaishree", "Opearations HR", 350000));
}
}
class Employee {
private String fullName;
private String designation;
private double salary;
public Employee(String fullName, String designation, double salary) {
super();
this.fullName = fullName;
this.designation = designation;
this.salary = salary;
}
public String getFullName() {
return fullName;
}
public String getDesignation() {
return designation;
}
public double getSalary() {
return salary;
}
#Override
public String toString() {
return "Employee [fullName=" + fullName + ", designation=" + designation + ", salary=" + salary + "]";
}
}
If the StudentNumber is numeric it will not be sorted numeric but alphanumeric.
Do not expect
"2" < "11"
it will be:
"11" < "2"
Use Comparator interface with methods introduced in JDK1.8: comparing and thenComparing, or more concrete methods: comparingXXX and thenComparingXXX.
For example, if we wanna sort a list of persons by their id firstly, then age, then name:
Comparator<Person> comparator = Comparator.comparingLong(Person::getId)
.thenComparingInt(Person::getAge)
.thenComparing(Person::getName);
personList.sort(comparator);
If you want to sort based on ReportKey first then Student Number then School, you need to compare each String instead of concatenating them. Your method might work if you pad the strings with spaces so that each ReportKey is the same length and so on, but it is not really worth the effort. Instead just change the compare method to compare the ReportKeys, if compareTo returns 0 then try StudentNumber, then School.
I had the same issue and I needed an algorithm using a config file. In This way you can use multiple fields define by a configuration file (simulate just by a List<String) config)
public static void test() {
// Associate your configName with your Comparator
Map<String, Comparator<DocumentDto>> map = new HashMap<>();
map.put("id", new IdSort());
map.put("createUser", new DocumentUserSort());
map.put("documentType", new DocumentTypeSort());
/**
In your config.yml file, you'll have something like
sortlist:
- documentType
- createUser
- id
*/
List<String> config = new ArrayList<>();
config.add("documentType");
config.add("createUser");
config.add("id");
List<Comparator<DocumentDto>> sorts = new ArrayList<>();
for (String comparator : config) {
sorts.add(map.get(comparator));
}
// Begin creation of the list
DocumentDto d1 = new DocumentDto();
d1.setDocumentType(new DocumentTypeDto());
d1.getDocumentType().setCode("A");
d1.setId(1);
d1.setCreateUser("Djory");
DocumentDto d2 = new DocumentDto();
d2.setDocumentType(new DocumentTypeDto());
d2.getDocumentType().setCode("A");
d2.setId(2);
d2.setCreateUser("Alex");
DocumentDto d3 = new DocumentDto();
d3.setDocumentType(new DocumentTypeDto());
d3.getDocumentType().setCode("A");
d3.setId(3);
d3.setCreateUser("Djory");
DocumentDto d4 = new DocumentDto();
d4.setDocumentType(new DocumentTypeDto());
d4.getDocumentType().setCode("A");
d4.setId(4);
d4.setCreateUser("Alex");
DocumentDto d5 = new DocumentDto();
d5.setDocumentType(new DocumentTypeDto());
d5.getDocumentType().setCode("D");
d5.setId(5);
d5.setCreateUser("Djory");
DocumentDto d6 = new DocumentDto();
d6.setDocumentType(new DocumentTypeDto());
d6.getDocumentType().setCode("B");
d6.setId(6);
d6.setCreateUser("Alex");
DocumentDto d7 = new DocumentDto();
d7.setDocumentType(new DocumentTypeDto());
d7.getDocumentType().setCode("B");
d7.setId(7);
d7.setCreateUser("Alex");
List<DocumentDto> documents = new ArrayList<>();
documents.add(d1);
documents.add(d2);
documents.add(d3);
documents.add(d4);
documents.add(d5);
documents.add(d6);
documents.add(d7);
// End creation of the list
// The Sort
Stream<DocumentDto> docStream = documents.stream();
// we need to reverse this list in order to sort by documentType first because stream are pull-based, last sorted() will have the priority
Collections.reverse(sorts);
for(Comparator<DocumentDto> entitySort : sorts){
docStream = docStream.sorted(entitySort);
}
documents = docStream.collect(Collectors.toList());
// documents has been sorted has you configured
// in case of equality second sort will be used.
System.out.println(documents);
}
Comparator objects are really simple.
public class IdSort implements Comparator<DocumentDto> {
#Override
public int compare(DocumentDto o1, DocumentDto o2) {
return o1.getId().compareTo(o2.getId());
}
}
public class DocumentUserSort implements Comparator<DocumentDto> {
#Override
public int compare(DocumentDto o1, DocumentDto o2) {
return o1.getCreateUser().compareTo(o2.getCreateUser());
}
}
public class DocumentTypeSort implements Comparator<DocumentDto> {
#Override
public int compare(DocumentDto o1, DocumentDto o2) {
return o1.getDocumentType().getCode().compareTo(o2.getDocumentType().getCode());
}
}
Conclusion : this method isn't has efficient but you can create generic sort using a file configuration in this way.
Here is a full example comparing 2 fields in an object, one String and one int, also using Collator to sort.
public class Test {
public static void main(String[] args) {
Collator myCollator;
myCollator = Collator.getInstance(Locale.US);
List<Item> items = new ArrayList<Item>();
items.add(new Item("costrels", 1039737, ""));
items.add(new Item("Costs", 1570019, ""));
items.add(new Item("costs", 310831, ""));
items.add(new Item("costs", 310832, ""));
Collections.sort(items, new Comparator<Item>() {
#Override
public int compare(final Item record1, final Item record2) {
int c;
//c = record1.item1.compareTo(record2.item1); //optional comparison without Collator
c = myCollator.compare(record1.item1, record2.item1);
if (c == 0)
{
return record1.item2 < record2.item2 ? -1
: record1.item2 > record2.item2 ? 1
: 0;
}
return c;
}
});
for (Item item : items)
{
System.out.println(item.item1);
System.out.println(item.item2);
}
}
public static class Item
{
public String item1;
public int item2;
public String item3;
public Item(String item1, int item2, String item3)
{
this.item1 = item1;
this.item2 = item2;
this.item3 = item3;
}
}
}
Output:
costrels
1039737
costs
310831
costs
310832
Costs
1570019
A lot of answers above have fields compared in single comparator method which is not actually working. There are some answers though with different comparators implemented for each field, I am posting this because this example would be much more clearer and simple to understand I am believing.
class Student{
Integer bornYear;
Integer bornMonth;
Integer bornDay;
public Student(int bornYear, int bornMonth, int bornDay) {
this.bornYear = bornYear;
this.bornMonth = bornMonth;
this.bornDay = bornDay;
}
public Student(int bornYear, int bornMonth) {
this.bornYear = bornYear;
this.bornMonth = bornMonth;
}
public Student(int bornYear) {
this.bornYear = bornYear;
}
public Integer getBornYear() {
return bornYear;
}
public void setBornYear(int bornYear) {
this.bornYear = bornYear;
}
public Integer getBornMonth() {
return bornMonth;
}
public void setBornMonth(int bornMonth) {
this.bornMonth = bornMonth;
}
public Integer getBornDay() {
return bornDay;
}
public void setBornDay(int bornDay) {
this.bornDay = bornDay;
}
#Override
public String toString() {
return "Student [bornYear=" + bornYear + ", bornMonth=" + bornMonth + ", bornDay=" + bornDay + "]";
}
}
class TestClass
{
// Comparator problem in JAVA for sorting objects based on multiple fields
public static void main(String[] args)
{
int N,c;// Number of threads
Student s1=new Student(2018,12);
Student s2=new Student(2018,12);
Student s3=new Student(2018,11);
Student s4=new Student(2017,6);
Student s5=new Student(2017,4);
Student s6=new Student(2016,8);
Student s7=new Student(2018);
Student s8=new Student(2017,8);
Student s9=new Student(2017,2);
Student s10=new Student(2017,9);
List<Student> studentList=new ArrayList<>();
studentList.add(s1);
studentList.add(s2);
studentList.add(s3);
studentList.add(s4);
studentList.add(s5);
studentList.add(s6);
studentList.add(s7);
studentList.add(s8);
studentList.add(s9);
studentList.add(s10);
Comparator<Student> byMonth=new Comparator<Student>() {
#Override
public int compare(Student st1,Student st2) {
if(st1.getBornMonth()!=null && st2.getBornMonth()!=null) {
return st2.getBornMonth()-st1.getBornMonth();
}
else if(st1.getBornMonth()!=null) {
return 1;
}
else {
return -1;
}
}};
Collections.sort(studentList, new Comparator<Student>() {
#Override
public int compare(Student st1,Student st2) {
return st2.getBornYear()-st1.getBornYear();
}}.thenComparing(byMonth));
System.out.println("The sorted students list in descending is"+Arrays.deepToString(studentList.toArray()));
}
}
OUTPUT
The sorted students list in descending is[Student [bornYear=2018, bornMonth=null, bornDay=null], Student [bornYear=2018, bornMonth=12, bornDay=null], Student [bornYear=2018, bornMonth=12, bornDay=null], Student [bornYear=2018, bornMonth=11, bornDay=null], Student [bornYear=2017, bornMonth=9, bornDay=null], Student [bornYear=2017, bornMonth=8, bornDay=null], Student [bornYear=2017, bornMonth=6, bornDay=null], Student [bornYear=2017, bornMonth=4, bornDay=null], Student [bornYear=2017, bornMonth=2, bornDay=null], Student [bornYear=2016, bornMonth=8, bornDay=null]]
im my case List of Lists (in the approximation examle):
List<T>.steam
.map(Class1.StaticInnerClass1::Field1)
.flatMap(x -> x.getField11ListStaticInnerClass2OfField1.stream())
.max(Comparator.comparing(Class1.StaticInnerClass2::Field21,Collections.reverseOrder())
.thenCompare(Class1.StaticInnerClass2::Field22));
For my case, I had 3 fields (For example - int index, bool isArchive ,bool isClassPrivate)
and I summed their comparison result like this-
Collections.sort(getData(), (o1, o2) ->
Integer.compare(o1.getIndex(getContext()), o2.getIndex(getContext()))
+ Boolean.compare(o1.isArchive(), o2.isArchive())
+ Boolean.compare(o1.isClassPrivate(), o2.isClassPrivate()
));

Categories