1

I have a task to compare between two objects in java. One is a Date that has days, months and years. the second one extends it, DateT with hours and minutes. In order to to that:

  • I can't use any function from Objects Class.
  • I also can't use the Name of one Class in the other Class.
  • But I can use instanceof.

The problem is that when I compare between Date date and Date dateT(the subclass object) I need to return false.

The problem is that date.equals(dateT) returns true. Do you have some suggestions?

public boolean equals(Object other) 
    if (!(other instanceof Date))
        return false;
    Date d = (Date) other;
    return day == d.day && month == d.month && year == d.year;
WJS
  • 36,363
  • 4
  • 24
  • 39
nelad
  • 11
  • 3
  • Does this answer your question? [how do I correctly override equals for inheritance in java?](https://stackoverflow.com/questions/9885773/how-do-i-correctly-override-equals-for-inheritance-in-java) – orhtej2 May 29 '23 at 11:38
  • 1
    This generally tends to be solved by `if (other.getClass != Date.class) return false` instead of the instanceof check, but it has its own problems (read Effective Java article 3.10). In general, please think about the symmetry, what should happen when date.equals(dateT), or dateT.eqauls(date) and if they should ever be equal, e.g. when somebody daoes `Date haha = dateT;` and then expects `date.equals(haha)` when the date components match. – Petr Janeček May 29 '23 at 11:43
  • I can't use getClass in the assignment – nelad May 29 '23 at 11:52
  • @nelad *`"I can't use getClass in the assignment"`* You didn't list that as one of your constraints. Can you be more general about what is or is not permitted. Otherwise we are just guessing. – WJS May 29 '23 at 15:40

4 Answers4

1

The answer should be that you shouldn’t design your software that way. Don’t look at bad examples, like Timestamp extending Date in an incompatible manner. Rather, look at examples like LocalDate and YearMonth, which implement common interfaces but are not subclasses of each other.


But since this is an assignment, here are two ways to solve this:

The problem is that the overridden method is invoked on the first object, which does not take into account that the second object might be a subclass. Therefore, split the comparison operation into two methods and invoke the second method on the second object:

class Date {
    …

    @Override
    public boolean equals(Object obj) {
        return obj instanceof Date && ((Date)obj).sameDate(this);
    }

    protected boolean sameDate(Date d) {
        return day == d.day && month == d.month && year == d.year;
    }
}
class DateT extends Date {
    …

    @Override
    public boolean equals(Object obj) {
        return obj instanceof DateT && ((DateT)obj).sameDate(this);
    }
  
    @Override
    protected boolean sameDate(Date d) {
        return d instanceof DateT && month == d.month && year == d.year;
    }
}

The overridable method equals delegates to the overridable method of the method argument, so if either this or the argument object is a DateT, at least one overridden method will be invoked, enforcing that both objects must be DateT to be equal.

There’s a small redundancy when both objects are DateT, as the this passed to sameDate is already known to be of type DateT but to avoid this check, the comparison logic would have to be extracted into a third method which is not worth the effort here.


The alternative is to consider that even if DateT doesn’t use the day field, each instance will have it, inherited from Day. So, initialize it with a value that can never be equal to a valid value of a Day instance and you’re done. E.g.

public class Date {
    final int day, month, year;

    public Date(int day, int month, int year) {
        if(day < 0) {
            throw new IllegalArgumentException("day = " + day);
        }
        this.day = day;
        this.month = month;
        this.year = year;
    }

    // only for subclasses, might be even more restricted than protected,
    // package-private or even private when using nested classes
    protected Date(int month, int year) {
        this.day = -1;
        this.month = month;
        this.year = year;
    }

    @Override
    public boolean equals(Object obj) {
        return obj instanceof Date d
            && day == d.day && month == d.month && year == d.year;
    }
}

public class DateT extends Date {
    public DateT(int month, int year) {
        super(month, year);
    }
}

This assumes that a day can never be negative and enforces this property in the constructor. Only the special constructor, which should only be accessible to subclasses, will initialize the day to -1 which can never match a valid day of a Date instance. This way, Date instances are never equal to DateT instances without the need for a special handling in the equals method.

If you have a mutable day field, additional measures must be taken to ensure that no method can change these invariants.

Holger
  • 285,553
  • 42
  • 434
  • 765
0

Assuming I understand the issue, I believe the following should enforce the symmetric and transitive requirements of your equals implementations.

You need to make sure that you only properly compare Date to Date and DateT to DateT. So have them each return their own class name in toString() and check to see that it matches the passed object's toString(). This is only needed in the Date class.

The following should meet your requirements as no Date* class mentions the other.

So in your DateT class, put

@Override
pubic String toString() { 
    return "DateT";
}
public boolean equals(Object date) {
        if (this == date) { // same object
           return true;
        if (date instanceof DateT d) { // d is now cast as dateT
            return <comparison using DateT values>
        }
        return false;
}

But since DateT is a subtype of Date, put this in your Date class

@Override
public String toString() {
    return "Date";
}

public boolean equals( Object date) {
  if( this == date) {  // same object
     return true;
  }
  if ((date instanceof Date d) && date.toString().equals("Date")) {
        return <comparison using Date values>
  }
  return false;
}

Note: As of Java 14 implied casting can be used as follows:

public boolean equals(Object foo) {
  if (foo instanceOf Foo f) { // if true f is now an instance of Foo and
                                  // can be used directly.
   ...
} 
WJS
  • 36,363
  • 4
  • 24
  • 39
0

This line if (!(other instanceof Date)) will always be false when you supply DateT type as the argument to equals method because DateT is a subclass of Date. Thus the return false; will not be executed and the custom comparison will be checked.

You need to do two things before your custom comparison

  1. Check the argument is not null. A null can be passed into any derived data type variable.
  2. Return false if the argument is not the Canonical Name doesn't match with your class name (I'm guessing it is java.util.Date).

This is how the equality should be checked.

public boolean equals(Object date) {

    if(date == null)
        return false;
    
    else if(date.getClass().getCanonicalName() != "java.util.Date")
        return false;
    
    else {
        Date d = (Date) other;
        return day == d.day && month == d.month && year == d.year;
    }
}
Arun Sudhakaran
  • 2,167
  • 4
  • 27
  • 52
0

To negate the comparison using the DateT class, you can first check if the obj parameter is an instance of DateT.

Additionally, here you can use a pattern variable within the instanceof statement.

class Date {
    int years, months, days;

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof DateT) return false;
        if (obj instanceof Date date) {
            return years == date.years
                && months == date.months
                && days == date.days;
        }
        return false;
    }
}

The sub-class equals can be implemented in two ways, I'm not sure which is more valid.
Essentially, it could be equivalent if the hours and minutes of the DateT class are 0.

In this case, a DateT will not equal a Date.

class DateT extends Date {
    int hours, minutes;

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof DateT date)) return false;
        return years == date.years
            && months == date.months
            && days == date.days
            && hours == date.hours
            && minutes == date.minutes;
    }
}
Reilas
  • 3,297
  • 2
  • 4
  • 17
  • You cannot use the the name of one class in the other. Check the second bullet in the question. `if (obj instanceof DateT) return false;` is not allowed in the `Date` class. – WJS May 29 '23 at 22:32
  • @WJS, interesting, I overlooked that. I thought they were referring to _Object.class.getName()_. – Reilas May 29 '23 at 23:53
  • I made the same mistake in an earlier answer and the OP pointed it out. – WJS May 30 '23 at 00:00