Programming with Abstract Classes and Interfaces - Wrox Press Java Programming 24-Hour Trainer 2nd (2015)

Wrox Press Java Programming 24-Hour Trainer 2nd (2015)

Lesson 7. Programming with Abstract Classes and Interfaces

In this lesson you learn about abstract classes, and then you build a complete application that illustrates how to design and implement programs with abstract classes and interfaces. You also learn about the notion of polymorphism.

Abstract Classes

If a class is declared abstract it can’t be instantiated. The keyword abstract has to be placed in the declaration of a class. The abstract class may have abstract method(s). The question is, “Who needs a class that can’t be instantiated?”

It’s easiest to answer this question by showing you how to use abstract classes while designing an application. Previous lessons ended with assignments, but this lesson starts with one.

Assignment

A company has employees and contractors. Design the classes without using interfaces to represent the people who work for this company. The classes should have the following methods:

changeAddress()

promote()

giveDayOff()

increasePay()

A one-time promotion means giving one day off and raising the salary by a specified percentage. The method increasePay() should raise the yearly salary for employees and increase the hourly rate for contractors.

Solution with an Abstract Class

Classes Employee and Contractor should have some common functionality, but because increasePay() has to be implemented differently for Employee and Contractor, let’s declare a superclass Person for them with an abstract (not implemented) method increasePay(). The class Person also has three concrete (implemented) methods. The fact that the abstract class Person cannot be instantiated forces you, the developer, to implement abstract methods in its subclasses.

Start by redesigning the class Person from the “Try It” section of Listing 7-1).

This is a different approach from the one in Chapter 6, which used interfaces. In this case some methods are implemented in the superclass and some are not. As per the assignment, this solution won’t be using any interfaces.

Listing 7-1: Abstract class Person

package com.practicaljava.lesson7;

public abstract class Person {

private String name;

private String address;

int INCREASE_CAP = 20; // cap on pay increase

public Person(String name){

this.name=name;

}

public String getName(){

return "Person's name is " + name;

}

public void changeAddress(String address){

this.address = address;

System.out.println("New address is" + address);

}

private void giveDayOff(){

System.out.println("Giving a day off to " + name);

}

public void promote(int percent){

System.out.println(" Promoting a worker...");

giveDayOff();

//calling an abstract method

increasePay(percent);

}

// an abstract method to be implemented in subclasses

public abstract boolean increasePay(int percent);

}

The method increasePay() is abstract, and the author of the class Person doesn’t have to know the specifics of implementing raising pay. The subclasses may even be programmed by other developers. But the author of Person can write code that even invokes increasePay(), as in the methodpromote(). This is allowed because by the time the concrete class is instantiated, the method increasePay() will definitely have been implemented. For simplicity, I didn’t write any code that looks like an actual increase of pay — this is irrelevant for understanding the concept of abstract classes.

The next step is to create the subclasses Employee and Contractor, implementing the method increasePay() in two different ways, as shown in Listing 7-2 and Listing 7-3.

Listing 7-2: Class Employee

package com.practicaljava.lesson7;

public class Employee extends Person{

public Employee(String name){

super(name);

}

public boolean increasePay(int percent) {

System.out.println("Increasing salary by " +

percent + "%. "+ getName());

return true;

}

}

Listing 7-3: Class Contractor

package com.practicaljava.lesson7;

public class Contractor extends Person {

public Contractor(String name){

super(name);

}

public boolean increasePay(int percent) {

if(percent < INCREASE_CAP){

System.out.println("Increasing hourly rate by " +

percent + "%. "+ getName());

return true;

} else {

System.out.println("Sorry, can't increase hourly rate by more

than " + INCREASE_CAP + "%. "+ getName());

return false;

}

}

}

Programmers writing subclasses are forced to write an implementation of increasePay() according to its signature, declared in the abstract class. If they declare a method increasing pay that has a different name or argument list, their classes remain abstract. So they don’t have a choice and have to play by the rules dictated in the abstract class.

The class TestPayIncrease in Listing 7-4 shows how to use the classes Employee and Contractor for promoting workers.

Listing 7-4: Class TestPayincrease

import com.practicaljava.lesson7.Person;

import com.practicaljava.lesson7.Contractor;

import com.practicaljava.lesson7.Employee;

public class TestPayIncrease {

public static void main(String[] args) {

Person workers[] = new Person[3];

workers[0] = new Employee("John");

workers[1] = new Contractor("Mary");

workers[2] = new Employee("Steve");

for (Person p: workers){

p.promote(30);

}

}

}

Compare the code of the preceding class TestPayIncrease with the one from the “Try It” section of Chapter 6. Which one do you like better? I like this version better; it exhibits polymorphic behavior, explained next.

Polymorphism

Polymorphism is easier to understand through an example. Let’s look at the classes Person, Employee, and Contractor from a different angle. The code in Listing 7-4 populates an array, mixing up the instances of the classes Employee and Contractor with hard-coded names. In real life, the data about workers usually comes from an external data source. For example, a program could get a person’s work status from the database and instantiate an appropriate concrete class. The earlier class TestPayIncrease gives an additional vacation day and attempts to increase the salary or hourly rate of every worker by 30 percent.

Note that even though the loop variable p is of its ancestor’s type Person in Listing 7-4, at every iteration it actually points at either an Employee or a Contractor instance. The actual object type will be evaluated only during the run time. This feature of object-oriented languages is called run-time binding or late binding.

The output of the class TestPayIncrease looks like the following:

Promoting a worker...

Giving a day off to John

Increasing salary by 30%. Person's name is John

Promoting a worker...

Giving a day off to Mary

Sorry, can't increase hourly rate by more than 20%. Person's name

is Mary

Promoting a worker...

Giving a day off to Steve

Increasing salary by 30%. Person's name is Steve

Both classes, Employee and Contractor, were inherited from the same base class, Person. Instead of having different methods for increasing the worker’s compensation based on the worker’s type, you give a polymorphic behavior to the method increasePay(), which applies different business logic depending on the type of the object.

You’re calling the same method, promote(), on every object from the array workers, but because the actual object type is evaluated during run time, the pay is raised properly according to this particular object’s implementation of the method increasePay(). This is polymorphism in action.

The for loop in the class TestPayIncrease remains the same even if you add some other types of workers inherited from the class Person. For example, to add a new category of worker — a foreign contractor — you have to create a class called ForeignContractor derived from the class Personand implement yet another version of the method increasePay() there. The class TestPayIncrease keeps evaluating the actual type of Person’s descendants during run time and calls the proper implementation of the method increasePay().

Polymorphism enables you to avoid using switch or if statements with the rather slow operator instanceof, which you used in Chapter 6. Would you agree that even though TestPayIncrease from Lesson 6 is producing the same results, its code looks pretty ugly compared to this version of this class? The code in the Lesson 6 version of TestPayIncrease works more slowly than the polymorphic version, and its if statement will have to be modified every time a new type of worker is added.

Making the Interface Solution Polymorphic

After discussing the abstract class version of the assignment’s solution, it’s time to modify its interface version from Chapter 6 with the polymorphic solution. Note that the array workers has been declared of type Payable . It is populated by objects of types Employee and Contractor, which implement the Payable interface.

You’ve eliminated not only the need of using instanceof, but even the casting to Payable is not required. The array is of type Payable, and you use only the behavior defined in Payable (that is, the increasePay() method) without worrying too much about whether the current worker is an employee or a contractor (see Listing 7-5).

Listing 7-5: Class TestPayincreasePoly

// For reusing the interface version of Employee and Contractor

// let's keep this sample in the code for Lesson 6

import com.practicaljava.lesson6.*;

public class TestPayInceasePoly {

public static void main(String[] args) {

Payable workers[] = new Payable[3];

workers[0] = new Employee("John");

workers[1] = new Contractor("Mary");

workers[2] = new Employee("Steve");

for (Payable p: workers){

p.increasePay(30);

}

}

}

Note that the variable p can “see” only the methods declared in Payable. The variable p could not be used to invoke any methods from Person regardless of the fact that both Employee and Contractor are inherited from this class.

What can go wrong during the execution of the code from Listing 7-5? What if a developer creates a class called ForeignContractor without implementing the Payable interface, and by mistake tries to add its instance to the array workers? You get a compile-time error “Cannot convert fromForeignContractor to Payable." Compiler errors are easy to fix. In the “Try It” section you purposely create a situation that causes a run-time casting error.

Interfaces Versus Abstract Classes

The next question is, “When should you use interfaces and when should you use abstract classes?" If two or more classes have lots of common functionality, but some methods should be implemented differently, you can create a common abstract ancestor and as many subclasses inheriting this common behavior as needed. Declare those methods abstract so subclasses implement them differently, and implement these methods in subclasses.

If several classes don’t have common functionality but need to exhibit some common behavior, do not create a common ancestor; have them implement an interface that declares the required behavior. This scenario was not presented in the “Interfaces” section of Chapter 6, but it’s going to be a part of the hands-on exercise in the “Try It” section of this lesson.

Interfaces and abstract classes are similar in that they ensure that required methods are implemented according to required method signatures. But they differ in how the program is designed. Whereas abstract classes require you to provide a common ancestor for the classes, interfaces don’t.

Interfaces could be your only option if a class already has an ancestor that cannot be changed. Java doesn’t support multiple inheritance; a class can have only one ancestor. For example, to write Java applets you must inherit your class from the class Applet, or, in the case of Swing applets, from JApplet. Using your own abstract ancestor here is not an option.

Although using abstract classes, interfaces, and polymorphism is not a must, it certainly improves the design of Java code by making it more readable and understandable to others who may need to work on programs written by you. In general, it’s a good habit to think over the behavior of the classes you’re about to write and list it in separate interfaces.

In Lesson 11 I give you another reason for using interfaces in the note titled "Programming to Interfaces“.

Try It

In the first part of the assignment your goal is to break the code from Listing 7-5 to produce the run-time error ClassCastException. You create a situation when the array workers will be of type Person, which can store any Person or its descendants. Then, you purposely add its subclass that doesn’t implement Payable, but will try to cast it to Payable anyway to generate a run-time exception. In the second part of the assignment you need to rewrite the assignment from Chapter 6 to keep the Payable interface but remove the common ancestor Person.

Lesson Requirements

For this lesson you should have Java installed.

NOTE You can download the code and resources for this “Try It” from the book’s web page at www.wrox.com/go/javaprog24hr2e. You can find them in Lesson6.zip and in Lesson7.zip.

Step-by-Step

Part 1

1. In Eclipse, open the project Lesson6 — yes, the one you’ve imported in the previous lesson.

2. Create a new class called ForeignContractor, as shown in the following code. Note that this class doesn’t implement the Payable interface:

3. package com.practicaljava.lesson6;

4. public class ForeignContractor extends Person {

5. public ForeignContractor(String name){

6. super(name);

7. }

8. public boolean increasePay(int percent) {

9. System.out.println("I'm just a foreign worker");

10. return true;

11. }

}

12. Create the class TestPayIncreasePolyError, adding an instance of the ForeignContractor class. Note that you’re casting every element of the array to Payable:

13. import com.practicaljava.lesson6.*;

14. public class TestPayIncreasePolyError {

15. public static void main(String[] args) {

16. Person workers[] = new Person[3];

17. workers[0] = new Employee("John");

18. workers[1] = new Contractor("Mary");

19. workers[2] = new ForeignContractor("Boris");

20. for (Person p: workers){

21. ((Payable)p).increasePay(30);

22. }

23. }

}

24. Run the program TestPayIncreasePolyError. Observe the output in the console view. You get the run-time error java.lang.ClassCastException on the third element of the array. Note the number 14 — this is the line number of TestPayIncreasePolyError program, which casts each object to the Payable interface:

25. Increasing salary by 30%. Person's name is John

26. Sorry, can't increase hourly rate by more than 20%. Person's name is Mary

27. Exception in thread "main" java.lang.ClassCastException:

28. com.practicaljava.lesson6.ForeignContractor cannot be cast to

29. com.practicaljava.lesson6.Payable

at TestPayInceasePolyError.main(TestPayInceasePolyError.java:14)

30. Modify the code of TestPayIncreasePolyError, changing the type of the array from Person to Payable and changing the type of the loop variable accordingly:

31. Payable workers[] = new Payable [3];

32. workers[0] = new Employee("John");

33. workers[1] = new Contractor("Mary");

34. workers[2] = new ForeignContractor("Boris");

35. for (Payable p: workers){

36. p.increasePay(30);

}

37.Observe that now you are getting a Java compiler error preventing you from even adding to the array the instance of ForeignContractor because it doesn’t implement Payable. Predicting and preventing run-time errors is a very important task for every software developer, and this subject is covered in detail in Chapter 10.

Part 2

1. Open the the project Lesson7 in Eclipse, select the menu File → New → Package, and create the new package com.practicaljava.lesson7.tryit.

2. Using Ctrl+C/Ctrl+V copy the Payable interface from Eclipse project Lesson6 to the package com.practicaljava.lesson7.tryit. Change the package name to be com.practicaljava.lesson7.tryit.

3. In the same package create the class Employee as follows:

4. package com.practicaljava.lesson7.tryit;

5. public class Employee implements Payable{

6. private String name;

7. public Employee(String name){

8. this.name=name;

9. }

10. public boolean increasePay(int percent) {

11. System.out.println("Increasing salary by " + percent

12. + "%: " + name);

13. return true;

14. }

}

15. In the same package create the class Contractor as follows:

16. package com.practicaljava.lesson7.tryit;

17. public class Contractor implements Payable {

18. private String name;

19. public Contractor(String name){

20. this.name=name;

21. }

22. public boolean increasePay(int percent) {

23. if(percent < Payable.INCREASE_CAP){

24. System.out.println("Increasing hourly rate by " +

25. percent + "%. ");

26. return true;

27. } else {

28. System.out.println(

29. "Sorry,can't increase hourly rate by more than "

30. + Payable.INCREASE_CAP + "%: " + name);

31. return false;

32. }

33. }

34. }

35. Create a class called TestPayIncreaseInterface:

36. public class TestPayIncreaseInterface {

37. public static void main(String[] args) {

38. Payable workers[] = new Payable [3];

39. workers[0] = new Employee("John");

40. workers[1] = new Contractor("Mary");

41. workers[2] = new Employee("Steve");

42. for (Payable p: workers){

43. ((Payable)p).increasePay(30);

44. }

45. }

}

46. Run this program. It should produce the following output:

47. Increasing salary by 30%: John

48. Sorry, can't increase hourly rate by more than 20%: Mary

49. Increasing salary by 30%: Steve

Note that neither Employee nor Contractor extends Person any longer. Both classes are free to extend any other classes now, but on the other hand, each of them has to declare the variable name and the method getName(), which was done once in the class Person before.

TIP Please select the videos for Lesson 7 online at www.wrox.com/go/javaprog24hr2e. You will also be able to download the code and resources for this lesson from the website.