This question already has answers here:
How to compare objects by multiple fields
(23 answers)
Closed 3 years ago.
I don't think that's the best way to word that title but I can't think of a better way to word it. Here's my problem: I have to write a method that compares in several different ways. If the last names are the same, I then need to compare by first name. If the first names are the same, then I need to sort by section. What would be the most effective way to sort a data structure in this hierarchy? Here's what I've currently got and I think I understand why it doesn't work but I can't come up with a different way to write this function:
//Student class structure, each field has a public get/set method
public class Student implements Comparable<Student>
{
private String fname;
private String lname;
private int section;
}
//My current compareTo method
#Override
public int compareTo(Student s)
{
/*
-compare by last name
-if the same, compare by first name
-if the same, compare by section
*/
String slast = s.getLastName();
if(lname.compareTo(slast) == 0)
{
String sfirst = s.getFirstName();
if(fname.compareTo(sfirst) == 0)
{
int sclass = s.getSection();
return Integer.compare(section, sclass);
}
else
{
return fname.compareTo(sfirst);
}
}
else
{
return lname.compareTo(slast);
}
}
You can create a Comparator for your Student class this way:
Comparator<Student> comparator = Comparator
.comparing(Student::getLastName)
.thenComparing(Student::getFirstName)
.thenComparing(Student::getSection);
And then use this comparator (instead of implementing Comparable interface) to sort a list with Student objects, or to create a TreeMap with these objects:
Collections.sort(listOfStudents, comparator);
TreeMap<Student> mapOfStudents = new TreeMap<>(comparator);
You don't have to use getters or setters if you're overriding compareTo. You can also forgo the else/return statements since they're terminal return statements, and just use return.
#Override
public int compareTo(Student s) {
if (lname.compareTo(s.lname) == 0) {
if (fname.compareTo(s.fname) == 0) {
return section.compareTo(s.section);
}
return fname.compareTo(s.fname);
}
return lname.compareTo(s.lname);
}
Your code looks correct to me.
What would be the most effective way to sort a data structure in this
hierarchy?
Well, it's worth mentioning that you are potentially doing the first two comparisons (first name and last name) multiple times
if(lname.compareTo(slast) == 0)
{
//...
}
else
{
return lname.compareTo(slast);
}
It should be fairly obvious that you are doing lname.compareTo(slast) twice. You can store the result in a variable instead.
int lastNameComparison = lname.compareTo(slast);
if(lastNameComparison == 0)
{
//...
}
else
{
return lastNameComparison;
}
It is a matter of style, but I would not bother to store the result of getters into variables. Just call them when you need them.
Combining both of the above points, you get:
int lastNameComparison = lname.compareTo(s.getLastName();
if (lastNameComparison == 0)
{
int firstNameComparison = fname.compareTo(s.getFirstName());
if (firstNameComparison == 0)
{
return Integer.compare(section, s.getSection());
}
else
{
return firstNameComparison;
}
}
else
{
return lastNameComparison;
}
The nesting is quite ugly and if we need to add another criteria, it would get even worse.
We can solve that by inverting the conditions and using multiple return statements.
int lastNameComparison = lname.compareTo(s.getLastName());
if (lastNameComparison != 0) return lastNameComparison;
// Last names must be equal
int firstNameComparison = fname.compareTo(s.getFirstName());
if (firstNameComparison != 0) return firstNameComparison;
// First names must be equal
return Integer.compare(section, s.getSection());
I would personally use the declarative style of writing this, but if this code is for an assignment, that is likely not what they are expecting.
Related
This question already has answers here:
Comparing two lists and removing duplicates from one
(3 answers)
Closed 7 years ago.
For enterprise reasons I can't override hashCode and I must use Java 6 (but I can use guava)
Whats the bests/simplest/quickest/most efficient/[insert indeterminate adjective equivalent to best] mechanism to remove duplicate beans from a Java collection?
A duplicate is defined by a subset of getters returning same values, e.g.
pojoA.getVal() == pojoB.getVal() && pojoA.getOtherVal() == pojoB.getOtherVal()
Wrap the objects of interest into your own class, and override its hashCode/equals to pay attention to a specific subset of attributes. Make a hash set of wrappers, then harvest the objects from the set to get a duplicate-free subset.
Here is an example:
class ActualData {
public String getAttr1();
public String getAttr2();
public String getAttr3();
public String getAttr4();
}
Let's say you want to pay attention to attributes 1, 2, and 4. Then you can make a wrapper like this:
class Wrapper {
private final ActualData data;
public ActualData getData() {
return data;
}
private final int hash;
public Wrapper(ActualData data) {
this.data = data;
this.has = ... // Compute hash based on data's attr1, 2, and 4
}
#Override
public int hashCode() {
return hashCode;
}
#Override
public boolean equals(Object obj) {
if (!(obj instanceof Wrapper)) return false;
Wrapper other = (Wrapper)obj;
return data.getAttr1().equals(other.getAttr1())
&& data.getAttr2().equals(other.getAttr2())
&& data.getAttr4().equals(other.getAttr4());
}
}
Now you can make a HashSet<Wrapper>:
Set<Wrapper> set = new HashSet<>();
for (ActualData item : listWithDuplicates) {
if (!set.add(new Wrapper(item))) {
System.out.println("Item "+item+" was a duplicate");
}
}
You could use a new TreeSet<Pojo> (comparator) with comparator implemented to reflect your condition (assuming integers here but replace as needed - for non comparable objects you need to find a hack to return some integer).
if (pojoA.getVal() != pojoB.getVal())
return Integer.compare(pojoA.getVal(), pojoB.getVal());
if (pojoA.getOtherVal() != pojoB.getOtherVal())
return Integer.compare(pojoA.getOtherVal(), pojoB.getOtherVal());
return 0;
Not as efficient as a plain HashSet though - #dasblikenlight suggestion is probably better.
I am currently writing a program that prints an ArrayList of books. Each ArrayList of books elements consist of a string (the title of a book) and an ArrayList (the authors of that book). I need to sort my ArrayList of books so that they appear in alphabetical order (sorted by titles). My issue is that when I print the new ArrayList (the list that I call Collections.sort() on) I get the same output as the first time I printed the non-sorted version.
I am calling myLib.sort(); from my driver program which goes to this method in my Library class:
public void sort()
{
Collections.sort(myBooks);
}
myBooks is the ArrayList of books I mentioned earlier. From what I've read, Collections.sort("ArrayList name") should sort my list alphabetically. If that is incorrect and I need to use compareTo() and equals() methods, then here are those methods as they appear in the class Book that I use to construct the books that go into my class Library:
public int compareTo(final Book theOther)
{
int result = 0;
if (myTitle.equals(theOther.myTitle))
{
if (myAuthors.get(0) != theOther.myAuthors.get(0))
{
result = 1;
}
}
else
{
result = 0;
}
return result;
}
public boolean equals(final Object theOther)
{
if (theOther instanceof String)
{
String other = (String) theOther;
return myTitle == other;
}
else
{
return false;
}
}
The only remaining possible issue that I can think of is with my printing method. My driver program prints myLib which is a Library. My Library class has the following toString() method:
public String toString()
{
String result = "";
for (int i = 0; i < myBooks.size(); i++)
{
String tempTitle = myBooks.get(i).getTitle();
ArrayList<String> tempAuthors = myBooks.get(i).getAuthors();
Book tempBook = new Book(tempTitle, tempAuthors);
result += (tempBook + "\n");
}
return result;
}
This gets each book and that book's string from my Book class toString() method which is the following:
public String toString()
{
return "\"" + myTitle + ",\" by " + myAuthors;
}
If this was too little, too much, too confusing, not clear enough, etc... Please let me know in a comment and I will edit the post ASAP. I can also post the entirety of my three classes if need be. I am new to Java and fairly new at posting so I'm still getting used to how things are done in both cases so I'd appreciate it if you'd go easy on me. Thank you!
Your compareTo() method seems to be wrong, note that Collections.sort() uses that method to compare the objects in your list.
You only check if the titles are equals, if they are then you compare the first authors and if they are equal you return 1, else you return 0;
compareTo() is used for check if this object is less, equals or greater than the one you are comparing with(returning 0 is for equals, a negative number for less and positive for greater, you return either a positive number either 0). I recommend you to read the javadoc for compareTo() method.
As an example here is an implementation of Book class where I only compare according to title (I omitted the comparison for the author list).
public class Book implements Comparable<Book> {
private String title;
private List<String> authors;
public Book(String title) {
this.title = title;
}
public int compareTo(Book o) {
return this.title.compareTo(o.title);
}
#Override
public boolean equals(Object b){
if(!(b instanceof Book)){
return false;
}
//authors comparison omitted
return this.title.equals(((Book) b).title);
}
#Override
public String toString(){
return "Title: "+ title; //todo: add authors also if need
}
}
As you see in Book.compareTo() method I rely on the String.compareTo().
it will return -1, 0 or 1; if you need to compare according to author list also you have to thing how will be the logic of the method and think of some issues:
if is enough to rely only on the first authors on the list
if you need to make sure that list of authors is sorted
what happens if the author list is empty
Also NOTE: compareTo should be consistent with equals which means if compareTo returns 0 then equals should return true and vice versa.
According to the documentation, you should also return negative value:
Returns a negative integer, zero, or a positive integer as this object is less
than, equal to, or greater than the specified object.
public int compareTo(final Book theOther) {
int result = myTitle.compareTo(theOther.myTitle);
if (result == 0) {
result = myAuthors.get(0).compareTo(theOther.myAuthors.get(0));
}
return result;
}
check #flowryn for better answer, as he also mention about equals() according to the documentation:
It is strongly recommended, but not strictly required that
(x.compareTo(y)==0) == (x.equals(y)). Generally speaking, any class
that implements the Comparable interface and violates this condition
should clearly indicate this fact. The recommended language is "Note:
this class has a natural ordering that is inconsistent with equals."
I have two arrayLists<myObject>, where myObject is an object of a custom class I've created. I want to be able to compare those arrayLists using the equals() method.
After reading and looking for answers, I've read that certain objects like int[] are only considered equal by the equals() method when they are referencing the same thing.
To fix that, I tried to override the equals method in my custom object. My objects have 3 atributes (all basic types), so my equals method now returns true if all the 3 atributes are equal to those of the object compared, and false otherwise. However, comparing the arraylists still doesn't work. What am I doing wrong?
Excuse me for explaining the code instead of posting it, I do it because the variables and names aren't in English.
EDIT: Ok, here's the code. Compra is my custom class; cantidad,concepto and id are its atributes.
#Override
public boolean equals(Object obj) {
boolean result = true;
if (obj == null) {
result = false;
}else{
Compra comprobada = (Compra) obj;
if(!(this.id == comprobada.getId())){
result = false;
}
if(!(this.cantidad == comprobada.getCantidad())){
result = false;
} if(!this.concepto.equals(comprobada.getConcepto())){
result = false;
}
}
return result;
}
Based on this one :
How can I check if two ArrayList differ, I don't care what's changed
If you have implemented your custom object equals correct (you actually override it and have your one) and the size of the arrayList is the same and each of the pair of the objects is equal then it will return equal. In other words what you are trying to do is totally correct but your arrayLists are not actually having exactly the equal objects in exact order.
Make sure that your equal is called when you check for collection equality by doing a System.out.println() to investigate what is going on.
If you don't mind please send the equals of your object.
I run your code in an isolated example and works fine (outtputs true) - I improved the equals method so it doesn't do so many if checks as if only one of them is not equal it should return false.
class stackoverflow {
public static void main(String args[]){
ArrayList<Compra> array1 = new ArrayList<>();
ArrayList<Compra> array2 = new ArrayList<>();
array1.add(new Compra(1,2,"test"));
array2.add(new Compra(1,2,"test"));
System.out.println(array1.equals(array2));
}
}
class Compra {
int id;
int cantidad;
String concepto;
public Compra(int id, int cantidad, String concepto){
this.id = id;
this.cantidad = cantidad;
this.concepto = concepto;
}
public boolean equals(Object obj) {
if (obj == null) {
return false;
}else{
Compra comprobada = (Compra) obj;
if(!(this.id == comprobada.getId())){
return false;
}
if(!(this.cantidad == comprobada.getCantidad())){
return false;
}
if(!this.concepto.equals(comprobada.getConcepto())){
return false;
}
}
return true;
}
public int getId() {
return id;
}
public int getCantidad() {
return cantidad;
}
public String getConcepto() {
return concepto;
}
}
Some things to check:
Are you sure you don't change the order of the things in ArrayList??:
Do you print to make sure that these equals checks happen and return true or false as expected?
Are you sure that concepto Strings are exactly the same, with the same case and don't contain extra spaces etc?
As you haven't posted code i suggest you to check into Comparable class and method compareTo and how to use it for custom classes.
I am a little confused about how to implement comparators/comparable. I am trying to sort a list (currently an ArrayList, but this is can change...) whereby when Collections.Sort is called, it's objects are sorted by a specific integer field in descending order, and if that integer field is equal then by lexicographic order of the name field.
This is my object:
class Movie implements Comparator<Movie>, Comparable<Movie> {
public String _title;
public int _time;
public Movie(String title, int time) {
this._title = title;
this._time = time;
}
public Movie() {
}
#Override
public int compareTo(Movie o) {
return (this._title.compareTo(o._title));
}
#Override
public int compare(Movie arg0, Movie arg1) {
return (arg0._time > arg1._time) ? -1 : (arg0._time == arg1._time) ? 0 : 1;
}
}
At the moment it only sorts via the number fields. How do I go about making it sort via lexicographic order if the number fields are equal?
I am a little confused by the compareTo method. This defines the natural order right? So what does
(this._title.compareTo(o._title))
actually do?
Thank you so much for your help!
EDIT:
I got my desired output by adding an if statement in the compare method and returning "arg0.compareTo(arg1)". However I am still unsure as to the compareTo method (even after reading about on the net) and a simple explanation would be great.
I am a little confused by the compareTo method. This defines the
natural order right? So what does
(this._title.compareTo(o._title))
actually do?
The natural order of Strings is their lexicographical order.
try:
if (arg0._time > arg1._time){
return -1
} else if (arg0._time < arg1._time){
return 1;
} else {
return arg0._title.compareTo(arg1._title);
}
You can use the following pattern to define compareTo with more and more specific conditions:
#Override
public int compareTo(Movie other) {
int result = Integer.compare( this._time, other._time );
if (result == 0) {
result = this._title.compareTo( other._title );
}
if (result == 0) {
// result = compare even more specific field
}
return result;
}
I've a Vector of objects, and have to search inside for a random attribute of those objects (For example, a Plane class, a Vector containing Plane; and I've to search sometimes for destination, and others to pilotName).
I know I can traverse the Vector using an Iterator, but I've got stuck at how do I change the comparison made between a String and the attribute on the object. I thought of using switch, but a another opinion would be cool.
Update 1:
The code I've written is something like this (Java n00b alert!):
public int search(String whatSearch, String query){
int place = -1;
boolean found = false;
for ( Iterator<Plane> iteraPlane = this.planes.iterator(); iteraPlane.hasNext() && found == false; ) {
Plane temp = (Plane) iteraPlane.next();
/* Here is where I have to search for one of many attributes (delimited by whatSearch */
}
return place;
}
Seems I've to stick to linear search (and that's a price I've able to pay). Anyway, I was thinking if Java had something like variable variable name (ouch!)
I assume that your problem is that you want to have a method that searches for a result based on some property of the collection type. Java is weak on this because it is best expressed in a language which has closures. What you need is something like:
public interface Predicate<T> {
public boolean evaluate(T t);
}
And then your search method looks like:
public static <T> T findFirst(List<T> l, Predicate<T> p) { //use List, not Vector
for (T t : l) { if (p.evaluate(t)) return t; }
return null;
}
Then anyone can use this general-purpose search method. For example, to search for an number in a vector of Integers:
List<Integer> is = ...
findFirst(is, new Predicate<Integer> {
public boolean evaluate(Integer i) { return i % 2 == 0; }
});
But you could implement the predicate in any way you want; for any arbitrary search
Use Collections.binarySearch and provide a Comparator.
EDIT: This assumes that the Vector is sorted. Otherwise, one has to do a linear search.
the equals() method is the best option. For these iterations you could do something like this:
for (Plane plane: planes) {
if ("JFK".equals(plane.getDestination())) {
// do your work in here;
}
}
or you could override the equals() method within Plane to see if the String passed in matches your destination (or pilot). this will allow you to use the indexOf(Object) and indexOf(Object, index) methods on Vector to return you the index(es) of the object(s). Once you have that, you could use Vector.get(index) to return to Object for you.
in Plane.java:
public boolean equals(Object o) {
return o.equals(getDestination()) ||
o.equals(getPilot()) ||
super.equals(o);
}
there is more work to be done with this option, as you will need to override hashCode() as well (see documentation).
See #oxbow_lakes above -- I think what you want isn't to pass a String as whatSearch, it's to pass a little snippet of code that knows how to get the property you're interested in. For a less general version:
public static interface PlaneMatcher {
boolean matches(Plane plane, String query);
}
public int search(PlaneMatcher matcher, String query){
int place = -1;
boolean found = false;
for ( Iterator<Plane> iteraPlane = this.planes.iterator(); iteraPlane.hasNext() && found == false; ) {
Plane temp = (Plane) iteraPlane.next();
if (matcher.matches(temp, query) {
found = true;
}
place++;
}
return place;
}
...
// example
int pilotNameIndex = search(new PlaneMatcher() {
boolean matches(Plane plane, String query) {
// note: assumes query non-null; you probably want to check that earlier
return query.equals(plane.getPilotName());
}
}, "Orville Wright");
(By the way, if it's the index you're interested in rather than the Plane itself, I wouldn't bother with an Iterator -- just use an old-fashioned for (int i = 0; i < planes.size(); i++) loop, and when you have a match, return i.)
Now, the tricky bit here is if what you have to search for is really identified by arbitrary strings at run-time. If that's the case, I can suggest two alternatives:
Don't store these values as object fields -- plane.pilotName, plane.destination -- at all. Just have a Map<String, String> (or better yet, a Map<Field, String> where Field is an Enum of all the valid fields) called something like plane.metadata.
Store them as object fields, but prepopulate a map from the field names to PlaneMatcher instances as described above.
For instance:
private static final Map<String, PlaneMatcher> MATCHERS = Collections.unmodifiableMap(new HashMap<String, PlaneMatcher>() {{
put("pilotName", new PlaneMatcher() {
boolean matches(Plane plane, String query) {
return query.equals(plane.getPilotName());
});
...
put("destination", new PlaneMatcher() {
boolean matches(Plane plane, String query) {
return query.equals(plane.getDestination());
});
}}
...
public int search(String whatSearch, String query){
PlaneMatcher matcher = MATCHERS.get(whatSearch);
int place = -1;
boolean found = false;
for ( Iterator<Plane> iteraPlane = this.planes.iterator(); iteraPlane.hasNext() && found == false; ) {
Plane temp = (Plane) iteraPlane.next();
if (matcher.matches(temp, query) {
found = true;
}
place++;
}
return place;
}
Oh, and you might be tempted to use reflection. Don't. :)
A simple way is to pass a comparison function to your search routine. Or, if you need more speed, use generics.