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.