Given
// base class
class Employee {
// common fields omitted
BigDecimal pay;
public Employee(String name, String ssn, String position, Date dob, BigDecimal pay) {}
// method can be overridden in sub-classes
public BigDecimal calculatePay() {
return pay; // here: the monthly salary
}
}
// part-time (PT) employee inherits from employee
class PTEmployee extends Employee {
// common fields omitted
int nHours;
BigDecimal wagePerHour;
public PTEmployee(String name, String ssn, String position, Date dob, int nHours, BigDecimal wagePerHour) {}
// method is specifically implemented for a part-time employee
@Override
public BigDecimal calulatePay() {
return wagePerHour.multiply(nHours); // hour-based salary-model
}
// method only exists in this generation (this class and sub-classes)
public double timePercentage() {
return Employee.FULL_TIME_HOURS / nHours; // half-time would return 50
}
}
Why does the second assignment downcast?
Inferred type using var
// renamed EV, E and PE to follow
// Java-naming conventions: lower-case variable names
Employee e = .. // initialization omitted
PTEmployee pe = .. // initialization omitted
var ev = e; // type of local variable ev gets inferred as Employee
System.out.println(ev.getClass()); // debug-print the type
// now we assign another instance of the same (Employee) or a sub-type (PTEmployee)
ev = pe; // type of ev is still Employee (the assigned instance is down-casted)
System.out.println(ev.getClass()); // debug-print the type
The Java compiler and your IDE (e.g. method-suggestions or autocompletion) both have the type inferred already with the first and initial assignment.
All further assignments will not change the type of this variable.
See also:
How to declare the variable to accept any sub-class ?
Use-case allowing the shorthand var
Now lets assume the use-case that you have a work-force of 2 employees that should get paid. But the accountant does not care about their time-percentage, only calculating their pay is important. The type of employment is not important.
List<? extends Employee> workforce = List.of(e, pe);
void payWorkforce() {
for (var worker : workforce) { // here the var is useful and a shortcut
System.out.printf("Paying worker %s: %s \n", worker.getName(), worker.calculatePay());
}
}
In the first use-case scenario, the for-each loop, the var
declaration is sufficient and shortens our loop-head. We are only interested to call the methods of base-class Employee
, so we can use var
safely.
Use-case requiring distinct subtypes
For the HR staff, which has to write the contract, the working-hours and contracted salary model however is very important. So the type of employee is accurately defined.
// using different variable-type declarations depending on the type needed
void hireCandidate(.., int preferredHours) {
if (preferredHours <= 0) {
System.out.println("Sorry. We need you to work at least 1 hour.");
return;
} else if (preferredHours < Employee.FULL_TIME_HOURS) {
PTEmployee partTime = new PTEmployee(.., preferredWeeklyHours, HOURLY_PAY); // define sub-type to use
workforce.add(partTime);
System.out.printf("Hired a %d percent partTime employee.\n", partTime.timePercentage()); // then you can use the specific sub-class' method
} else {
Employee fullTime = new Employee(.., MONTHLY_SALARY);
workforce.add(fullTime);
System.out.printf("Hired a fullTime employee.\n");
}
return;
}
In the second use-case scenario, you can see, that because we expect to deal with different types of employees in the hireCandidate
method, we also declared the variables with different types as needed.