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.