Keeping mutable objects sorted in TreeSets at all times - java

It came to my notice that a TreeSet doesn't keep the mutable objects in sorted order if object attribute values are changed later on. For example,
public class Wrap {
static TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>(){
#Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
});
public static void main(String []args){
Student s = new Student(10);
ts.add(s);
ts.add(new Student(50));
ts.add(new Student(30));
ts.add(new Student(15));
System.out.println(ts);
s.age = 24; //Here I change the age of a student in the TreeSet
System.out.println(ts);
}
}
class Student{
int age;
Student(int age){
this.age = age;
}
#Override
public String toString() {
return "Student [age=" + age + "]";
}
}
The output is :
[Student [age=10], Student [age=15], Student [age=30], Student [age=50]]
[Student [age=24], Student [age=15], Student [age=30], Student [age=50]]
After I change the age of a particular student, and then print the TreeSet, the Set seems no longer in sorted order. Why does this happen? and how to keep it sorted always?

Why does this happen?
Because the set cannot monitor all its objects for changes... How would it be able to do that?!
Same problem arises for HashSets. You can't change values affecting an objects hash-code when a HashSet holds the object.
and how to keep it sorted always?
You typically remove the element from the set, modify it, and then reinsert it. In other words, change
s.age = 24; //Here I change the age of a student in the TreeSet
to
ts.remove(s);
s.age = 24; //Here I change the age of a student in the TreeSet
ts.add(s);
You can also use for example a list, and call Collections.sort on the list each time you've modified an object.

You could make use of the observer pattern. Let your TreeSet implement Observer and let your Student extend Observable. The only change you need to make is to hide the age field by encapsulation so that you have more internal control over the change.
Here's a kickoff example:
public class ObservableTreeSet<O extends Observable> extends TreeSet<O> implements Observer {
public ObservableTreeSet(Comparator<O> comparator) {
super(comparator);
}
#Override
public boolean add(O element) {
element.addObserver(this);
return super.add(element);
}
#Override
#SuppressWarnings("unchecked")
public void update(Observable element, Object arg) {
remove(element);
add((O) element);
}
}
and
public class Student extends Observable {
private int age;
Student(int age) {
this.age = age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (this.age != age) {
setChanged();
}
this.age = age;
if (hasChanged()) {
notifyObservers();
}
}
#Override
public String toString() {
return "Student [age=" + age + "]";
}
}
Now do a new ObservableTreeSet instead of new TreeSet.
static TreeSet<Student> ts = new ObservableTreeSet<Student>(new Comparator<Student>() {
#Override
public int compare(Student o1, Student o2) {
return o1.getAge() - o2.getAge();
}
});
It's ugly at first sight, but you end up with no changes in the main code. Just do a s.setAge(24) and the TreeSet will "reorder" itself.

This is a generic problem with Maps and Sets. The values are inserted using the hashCode/equals/compare at the moment of insertion, and if the values on which these methods are based change, then the structures can screw up.
One way would be to remove the item from the set and re-add it after the value has been changed. Then it would be correct.

Glazed Lists can help: http://www.glazedlists.com/
I use it for its EventList and haven't tried sorting. But on their home page they list the main features:
Live Sorting means your table stays sorted as your data changes.

Generally, it is best to manually keep your sorted Set/Map continuously consistent (see the strategy mentioned by #aioobe).
However, sometimes this is not an option. In these cases we can try this:
if (treeSet.contains(item)) {
treeSet.remove(item);
treeSet.add(item);
}
or with a map:
if (treeMap.containsKey(key)) {
Value value = treeMap.get(key);
treeMap.remove(key);
treeMap.put(key, value);
}
But this will not work correctly, because even containsKey can result with an incorrect result.
So what can we do with a dirty map? How can we refresh a single key without having to rebuild the entire map? Here is a utility class to solve this problem (can be easily converted to handle sets):
public class MapUtil {
/**
* Rearranges a mutable key in a (potentially sorted) map
*
* #param map
* #param key
*/
public static <K, V> void refreshItem(Map<K, V> map, K key) {
SearchResult<K, V> result = MapUtil.searchMutableKey(map, key);
if (result.found) {
result.iterator.remove();
map.put(key, result.value);
}
}
/**
* Searches a mutable key in a (potentially sorted) map
*
* Warning: currently this method uses equals() to check equality.
* The returned object contains three fields:
* - `found`: true iff the key found
* - `value`: the value under the key or null if `key` not found
* - `iterator`: an iterator pointed to the key or null if `key` not found
*
* #param map
* #param key
* #return
*/
public static <K, V> SearchResult<K, V> searchMutableKey(Map<K, V> map, K key) {
Iterator<Map.Entry<K, V>> entryIterator = map.entrySet().iterator();
while (entryIterator.hasNext()) {
Map.Entry<K, V> entry = entryIterator.next();
if (key.equals(entry.getKey())) {
return new SearchResult<K, V>(true, entry.getValue(), entryIterator);
}
}
return new SearchResult<K, V>(false, null, null);
}
public static class SearchResult<K, V> {
final public boolean found;
final public V value;
final public Iterator<Map.Entry<K, V>> iterator;
public SearchResult(boolean found, V value, Iterator<Map.Entry<K, V>> iterator) {
this.found = found;
this.value = value;
this.iterator = iterator;
}
}
}

If your problem is the iteration order, and you don't want to use the extra functionality of TreeSet (headSet() etc.), then use HashSet with custom iterator. Also, there is a major problem with your example: two students of the same age (often it happens) make conflict.
A possible solution:
public class Main {
public static void main(final String[] args) {
MagicSet<Student> ts = new MagicSet<Student>(new Comparator<Student>() {
#Override
public int compare(Student student1, Student student2) {
return student1.age - student2.age;
}
});
Student s = new Student(10);
ts.add(s);
ts.add(new Student(50));
ts.add(new Student(30));
ts.add(new Student(15));
System.out.println(ts); // 10, 15, 30, 50
s.age = 24;
System.out.println(ts); // 15, 24, 30, 50
}
public static class Student {
public int age;
public Student(int age) {
this.age = age;
}
#Override
public String toString() {
return "Student [age=" + age + "]";
}
}
public static class MagicSet<T> extends HashSet<T> {
private static final long serialVersionUID = -2736789057225925894L;
private final Comparator<T> comparator;
public MagicSet(Comparator<T> comparator) {
this.comparator = comparator;
}
#Override
public Iterator<T> iterator() {
List<T> sortedList = new ArrayList<T>();
Iterator<T> superIterator = super.iterator();
while (superIterator.hasNext()) {
sortedList.add(superIterator.next());
}
Collections.sort(sortedList, comparator);
return sortedList.iterator();
}
}
}

Related

Set ordered by "add() count"

I'm trying to implement a Set which is ordered by the count of additions like this:
public class App {
public static void main(String args[]) {
FrequencyOrderedTreeSet<String> set = new FrequencyOrderedTreeSet<String>();
set.add("bar");
set.add("foo");
set.add("foo");
Iterator<String> i = set.iterator();
while (i.hasNext()) {
System.out.print(i.next());
}
// prints "foobar"
}
}
I've created a protected class FrequencyOrderedTreeSet.Element which implements Comparable and has a T entry and an int frequency property and extended TreeSet<FrequencyOrderedTreeSet.Element> with FrequencyOrderedTreeSet<T> and overrode the compareTo and equals methods on the Element.
One problem is that I can't override the add() method because of type erasure problems and also I can't call instanceof Element in the equals method, because in case object given to it is an Element, I have to compare their entries, but if it's not, I have to compare the object itself to this.entry.
In the add method I create a new element, find the element with the same entry in the set, set the frequency on the new element to "old+1", remove the old one and add the new one. I'm not even sure this is the best way to do this or if it would work even because the other problems I described.
The question is: what's the best way to implement such data structure? In case I'm somehow on the right track - how can I circumvent the problems I've mentioned above?
Here's a basic implementation. It's not the most optimal and will take some more work if you want to implement the full Set interface.
public class FrequencySet<T> implements Iterable<T>
{
private TreeSet<T> set;
private HashMap<T, Integer> elements = new HashMap<T, Integer>();
public FrequencySet()
{
set = new TreeSet<T>(new Comparator<T>()
{
public int compare(T o1, T o2)
{
return elements.get(o2)-elements.get(o1);
}
});
}
public void add(T t)
{
Integer i = elements.get(t);
elements.put(t, i == null ? 1 : i+1);
set.remove(t);
set.add(t);
}
public Iterator<T> iterator() {return set.iterator();}
public static void main(String [] args)
{
FrequencySet<String> fset = new FrequencySet<String>();
fset.add("foo");
fset.add("bar");
fset.add("foo");
for (String s : fset)
System.out.print(s);
System.out.println();
fset.add("bar");
fset.add("bar");
for (String s : fset)
System.out.print(s);
}
}
The key is in the add method. We change the counter for the given object (which changes the relation order), remove it from the backing set and put it back in.
This works the other way (count is increased when you use GET)
#SuppressWarnings("rawtypes")
final class Cache implements Comparable {
private String key;
private String value;
private int counter;
public String getValue() {
counter++;
return value;
}
private void setValue(String value) { this.value = value; }
public String getKey() { return key; }
private void setKey(String key) { this.key = key; }
public int getCounter() { return counter; }
public void setCounter(int counter) { this.counter = counter; }
public Cache(String key, String value) {
this.setKey(key);
this.setValue(value);
setCounter(0);
}
#Override
public int compareTo(Object arg0) {
if(!(arg0 instanceof Cache)) {
throw new ClassCastException();
}
return this.getCounter() - ((Cache) arg0).getCounter();
}
}

Customizing equals method as per the use case

I have a class defined as
class Book{
String author;
String title;
int id;
public boolean equals(Object o){
return id == ((Book)o).id;
}
public int hashCode{...}
}
In most of the cases uniqueness of the Books is determined by id, hence works properly. In one particular case, I want to merge two lists based on author and title value. I cannot directly use a Set and add the second list to the Set as comparison will happen on ids and not on author/title value. Only way for me is to have two nested for loops to compare each object's value.
List<Book> list1=...;
List<Book> list2 = ...;
Iterator<Book> iterator = list1.iterator();
while(iterator.hasNext()){
Book b1 = iterator.next();
for(Book b2:list2){
if(b1.getAuthor().equals(b2.getAuthor()) && b1.getTitle().equals(b2.getTitle())){
iterator.remove();
}
}
}
list2.addAll(list1);
Is there any way where we can override the equals method as per the use case (similar to Comparator where we can change the sort algorithm)?
Instead just have customized equals method that will check the author value and somehow following works
set.addAll(list2);
You can do something liek that with closures, but it is not overriding as such. The problem you have is this is a O(N*M) time complexity which is not idea. A better approach is O(N) is
Map<String, Book> books = new LinkedHashMap<>();
for (Book book : list1) books.put(book.author+"/"+book.title, book);
for (Book book : list2) books.remove(book.author+"/"+book.title);
list2.addAll(books.values());
For closures, you need a few functions I couldn't find.
static class MapStream<K, V> {
final Map<K, V> map;
final Function<V, K> func;
MapStream(Iterable<V> values, Function<V, K> func) {
map = new LinkedHashMap<>();
this.func = func;
addAll(values);
}
private void addAll(Iterable<V> values) {
for (V value : values)
map.put(func.apply(value), value);
}
public MapStream<K, V> removeAll(Iterable<V> values) {
for (V value : values) {
map.remove(func.apply(value));
}
return this;
}
public Collection<V> values() {
return map.values();
}
}
public static <T> Function<T, String> and(Function<T, String> func1, Function<T, String> func2) {
return (T t) -> func1.apply(t) + "\uffff" + func2.apply(t);
}
public static void main(String... ignored) {
List<Book> list1 = new ArrayList<>();
List<Book> list2 = new ArrayList<>();
Function<Book, String> commonKey = and((Book b) -> b.author, (Book b) -> b.title);
list2.addAll(new MapStream<>(list1, commonKey).removeAll(list2).values());
}
You can see that with some support you can see something with closures.
In general you need to externalize equals and hashCode methods. Therefore you could have something like:
class MyModelClass {
private EqualsImpl<MyModelClass> equalsImpl;
public MyModelClass(EqualsImpl<MyModelClass> equalsImpl) {
super();
this.equalsImpl = equalsImpl;
}
#Override
public boolean equals(Object obj) {
return equalsImpl.equals(this, obj);
}
#Override
public int hashCode() {
return equalsImpl.hashCode(this);
}
}
interface EqualsImpl<C> {
public boolean equals(C obj1, Object obj2);
public int hashCode(C obj);
}
If I understand your need correctly, you can use a TreeSet with a custom comparator:
TreeSet<Book> set = new TreeSet<Book>(new Comparator<Book>() {
public int compare(Book o1, Book o2) {
int r = o1.author.compareTo(o2.author);
if (r != 0)
return r;
return o1.title.compareTo(o2.title);
}
});
set.add(...);
SortedSet guarantees uniqueness, but it doesn't make use of equals() or hashcode(). Instead, it uses the comparator (or natural ordering) for determining equality.
Create a custom List which decorates the List.add() (or List.addAll()) using a Comparator like the TreeSet
Just in addition to answer of #Aniket Thakur (+1).
Yes, I also recommend you to use Comparator. You should define Comparator per your use case and use collections that work with comparators, e.g. TreeSet, TreeMap. This is the clearest way to achieve what you need: separation of comparison logic from model class itself.
public boolean equals(Object o){
return (author.equals(((Book)o).author) && title.equals(((Book)o).title));
}
why can't you override the equals method as above.
You can do
import java.util.Comparator;
public class Book {
String author;
String title;
int id;
public boolean equals(Object o) {
return id == ((Book) o).id;
}
//getters and setters
//other methods
}
class BookComparator implements Comparator<Book>{
#Override
public int compare(Book o1, Book o2) {
if(o1.getAuthor() == o2.getAuthor() && o1.getTitle() == o2.getTitle())
return 0;
return 1;
}
}
and then you can do something like
Set<Book> lSet = new TreeSet<Book>(new BookComparator());
lSet.addAll(list1);
lSet.addAll(list2);

How to sort ArrayList<myobject> on the basis of myobject's properties

I want to sort some objects which are in an ArrayList on the basis of the objects properties.
the object has:
public class Minterm
{
String minTerm;
char flagTick;
String minTermDerive;
int groupNo;
String adjGroup;
static int MaxLiterals;
then i have this in the main method:
ArrayList<Minterm> column =new ArrayList<Minterm>();
then i add some objects of type Minterm in the list. but at the end i want to organise them and sort them on the member variable groupNo(Ascending order).
i searched and came up with the comparable and comparator interfaces that i tried but didnt succeed. is there any other method to do this? or am i doing the comparator implemnting wrong.
EDIT :
Following is the code i wrote for Comparator. Please confirm if it will sort in ascending?
package backEnd;
import java.util.Comparator;
public class Comp implements Comparator<Minterm>
{
#Override
public int compare(Minterm a, Minterm b)
{
return a.getgroupOne().compareTo(b.getgroupOne());
}
}
i run it as:
Collections.sort(column , new Comp());
seems to be working fine. but i dont have a sound understanding of it.
Please confirm if it will sort in ascending?
You should let Minterm implement Comparable<MinTerm> or write a custom Comparator for MinTerm and then use Collections.sort.
Using a comparator it would look like this:
Collections.sort(column, new Comparator<Minterm>() {
#Override
public int compare(Minterm o1, Minterm o2) {
return Integer.valueOf(o1.groupNo).compareTo(o2.groupNo);
}
});
Regarding your edit:
Yes. that sorts Minterms based on the groups in ascending order.
Collections.sort() and the Comparator interface are precisely the right tool for this job.
Something along the following lines should do it (untested):
Collections.sort(column, new Comparator<Minterm>() {
public int compare(Minterm o1, Minterm o2) {
return Integer.valueOf(o1.groupNo).compareTo(o2.groupNo);
}
});
Two ways, using Collections.sort(..):
make your object implement Comparable. Involves changing the original class, which may not be possible
supply custom Comparator. It takes instances of your object and compares them. Doesn't require a change to the class.
Either way, make sure you conform to the interfaces' contracts.
Here is the sample code (for more examples refer http://java2novice.com/java-collections-and-util/arraylist/sort-comparator/ ):
public class MyArrayListSort {
public static void main(String a[]){
List<Empl> list = new ArrayList<Empl>();
list.add(new Empl("Ram",3000));
list.add(new Empl("John",6000));
list.add(new Empl("Crish",2000));
list.add(new Empl("Tom",2400));
Collections.sort(list,new MySalaryComp());
System.out.println("Sorted list entries: ");
for(Empl e:list){
System.out.println(e);
}
}
}
class MySalaryComp implements Comparator<Empl> {
#Override
public int compare(Empl e1, Empl e2) {
if(e1.getSalary() < e2.getSalary()){
return 1;
} else {
return -1;
}
}
}
class Empl{
private String name;
private int salary;
public Empl(String n, int s){
this.name = n;
this.salary = s;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
public String toString(){
return "Name: "+this.name+"-- Salary: "+this.salary;
}
}

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

"Exception in thread "main" java.lang.NullPointerException when adding to a HashMapList

HashMapList keeps its elements inside a HashMap) and when I call add method this error message will be shown in the concole "Exception in thread "main" java.lang.NullPointerException
public class HashMapList<K, V extends Product> extends AbstractList<Product> {
public V element;
public int index;
Map<Integer, V> map;
public HashMapList() {
super();
new HashMap<Integer, V>();
}
// Override
public void add(int index, V element) {
map.put(new Integer(index), element);
}
}
thanks,I have solved the first problem but when I call add method like==>
HashMapList<Integer, Book> list = new HashMapList<Integer, Book>();
list.add(0, new Book("physics"));
and Book class is==>
public class Book extends Product {
public String name = null;
public Book(String name) {
super(name);
}
}
and Product class is==>
public class Product implements Comparable {
/**
*
*/
private static final long serialVersionUID = 1L;
private String name = null;
public Product(String name) {
if (name == null)
throw new NullPointerException();
this.name = name;
}
public String getName() {
return name;
}
// Override
public int compareTo(Object o) {
Product product = (Product) o;
int compare = getName().compareTo(product.name);
return compare;
}
}
And when I want to print this list basically with System.out.println(list);
this sentence will be shown in the concole:[org.bihe.com1112.Book#1fb8ee3, org.bihe.com1112.Book#61de33, org.bihe.com1112.Book#14318bb]
You are not assigning anything to map
public HashMapList() {
super();
map = new HashMap<Integer, V>();
}
whenever you get a null pointer exception look for where you assign a value to the variable to you are using. Here look for anywhere in your code where you say "map = ...".
For your second question, you should really start another thread. It is correctly printing the string representation of your object. Your Book class does not provide a custom overridden toString() method. So it uses the one inherited from Object, which just returns a string made of the full name of the class and the hashCode of the object, which is what you are seeing. You should override the toString() method if you want to see something different.
look at your constructor.
new HashMap<Integer, V>();
should be
map = new HashMap<Integer, V>();

Categories