- 1. Introduction
- 2. Interfaces
-
- 2.1. Defining Interfaces
- 2.2. Implementing Interfaces
- 2.3. Using Interfaces
- 3. Abstract Classes
-
- 3.1. Defining Abstract Classes
- 3.2. Extending Abstract Classes
- 3.3. Using Abstract Classes
- 4. A Worked Example – Payments System
-
- 4.1. The Payee Interface
- 4.2. The Payment System
- 4.3. The Employee Classes
- 4.4. The Application
- 4.5. Handling Bonuses
- 4.6. Contracting Companies
- 4.7. Advanced Functionality: Taxation
- 5. Conclusion
1. Introduction
In this tutorial we will give an introduction to Abstraction in Java and define a simple Payroll System using Interfaces, Abstract Classes and Concrete Classes.
There are two levels of abstraction in Java – Interfaces, used to define expected behaviour and Abstract Classes, used to define incomplete functionality.
We will now look at these two different types of abstraction in detail.
2. Interfaces
An interface is like a contract. It is a promise to provide certain behaviours and all classes which implement the interface guarantee to also implement those behaviours. To define the expected behaviours the interface lists a number of method signatures. Any class which uses the interface can rely on those methods being implemented in the runtime class which implements the interface. This allows anyone using the interface to know what functionality will be provided without having to worry about how that functionality will actually be achieved. The implementation details are hidden from the client, this is a crucial benefit of abstraction.
2.1. Defining Interfaces
You can use the keyword interface to define an interface:
1 | public interface MyInterface { |
7 | String methodC(double x, double y); |
Here we see an interface called MyInterface defined, note that you should use the same case conventions for Interfaces that you do for Classes. MyInterface defines 3 methods, each with different return types and parameters. You can see that none of these methods have a body; when working with interfaces we are only interested in defining the expected behaviour, not with it’s implementation. Note: Java 8 introduced the ability to create a default implementation for interface methods, however we will not cover that functionality in this tutorial.
Interfaces can also contain state data by the use of member variables:
1 | public interface MyInterfaceWithState { |
All the methods in an interface are public by default and in fact you can’t create a method in an interface with an access level other than public.
2.2. Implementing Interfaces
Now we have defined an interface we want to create a class which will provide the implementation details of the behaviour we have defined. We do this by writing a new class and using the implements keyword to tell the compiler what interface this class should implement.
01 | public class MyClass implements MyInterface { |
03 | public void methodA() { |
04 | System.out.println("Method A called!"); |
07 | public int methodB() { |
11 | public String methodC(double x, double y) { |
12 | return "x = " + x + ", y = " y; |
We took the method signatures which we defined in MyInterface and gave them bodies to implement them. We just did some arbitrary silliness in the implementations but it’s important to note that we could have done anything in those bodies, as long as they satisfied the method signatures. We could also create as many implementing classes as we want, each with different implementation bodies of the methods from MyInterface.
We implemented all the methods from MyInterface in MyClass and if we failed to implement any of them the compiler would have given an error. This is because the fact that MyClass implements MyInterface means that MyClass is guaranteeing to provide an implementation for each of the methods from MyInterface. This lets any clients using the interface rely on the fact that at runtime there will be an implementation in place for the method it wants to call, guaranteed.
2.3. Using Interfaces
To call the methods of the interface from a client we just need to use the dot (.) operator, just like with the methods of classes:
1 | MyInterface object1 = new MyClass(); |
We see something unusual above, instead of something like MyClass object1 = new MyClass(); (which is perfectly acceptable) we declare object1 to be of type MyInterface. This works because MyClass is an implementation of MyInterface, wherever we want to call a method defined in MyInterface we know that MyClass will provide the implementation. object1 is a reference to any runtime object which implements MyInterface, in this case it’s an instance of MyClass. If we tried to do MyInterface object1 = new MyInterface() we’d get a compiler error because you can’t instantiate an interface, which makes sense because there’s no implementation details in the interface – no code to execute.
When we make the call to object1.methodA() we are executing the method body defined in MyClass because the runtime type of object1 is MyClass, even though the reference is of type MyInterface. We can only call methods on object1 that are defined in MyInterface, for all intents and purposes we can refer to object1 as being of type MyInterface even though the runtime type is MyClass. In fact if MyClass defined another method called methodD() we couldn’t call it on object1, because the compiler only knows that object1 is a reference to a MyInterface, not that it is specifically a MyClass.
This important distinction is what lets us create different implementation classes for our interfaces without worrying which specific one is being called at runtime.
Take the following interface:
1 | public interface OneMethodInterface { |
It defines one void method which takes no parameters.
Let’s implement it:
1 | public class ClassA implements OneMethodInterface { |
3 | public void oneMethod() { |
4 | System.out.println("Runtime type is ClassA."); |
We can use this in a client just like before:
1 | OneMethodInterface myObject = new ClassA(); |
Output:
1 | public class ClassB implements OneMethodInterface { |
3 | public void oneMethod() { |
4 | System.out.println("The runtime type of this class is ClassB."); |
And modify our code above:
1 | OneMethodInterface myObject = new ClassA(); |
3 | myObject = new ClassB(); |
Output:
2 | The runtime type of this class is ClassB. |
We have successfully used the same Reference (myObject) to refer to instances of two different runtime types. The actual implementation is completely unimportant to the compiler, it just cares that OneMethodInterface is implemented, by anything, and in any way. As far as the compiler is concerned myObject is a OneMethodInterface, and oneMethod() is available, even if it’s reassigned to a different instance object of a different class type. This ability to provide more than one runtime type and have it resolved at run time, rather than compile time, is called polymorphism.
Interfaces define behaviour without any implementation details (ignoring Java 8) and implementing classes define all the implementation details for the classes they define, but what happens if we want a mix of the two concepts? If we want to mix some behaviour definition and some implementation together in the same place we can use an abstract class.
3. Abstract Classes
An abstract class is like an incomplete blueprint, it defines some of the implementation details of the class while leaving others as simple behaviour definitions to be implemented later.
Imagine a blueprint of a house where the house is drawn in completely but there is a big empty square where the garage will go. We know there will be a garage but we don’t know what it will look like. Somebody else will have to take a copy of our blueprint and draw in the garage. In fact several different people may take copies of our blueprint and draw in different types of garage. Houses built using these blueprints will all be recognizable variants of our house; the front door, the room layouts and the windows will all be identical, however the garages will all be different.
Much like the blueprints above our abstract classes will define some methods completely, and these method implementations will be the same in all implementations of the abstract class. The abstract class will define only the signature for other methods, in much the same way that interfaces do. The method implementations for these methods will vary in the implementing classes. An implementing class of an abstract class is commonly referred to as a concrete class. Due to the inheritance relationship between the concrete class and the abstract class we generally say that a concrete classextends an abstract class, rather than implements it as we say with interfaces.
Just like with interfaces any client code knows that if a concrete class is extending an abstract class the concrete class guarantees to provide method bodies for the abstract methods of the abstract class (the abstract class provides it’s own method bodies for non-abstract methods, of course).
Again, just like interfaces, there can be several different concrete classes of a given abstract class, each may define very different behaviours for the abstract methods of the abstract class while satisfying the contract of the abstract class. The implementation details are hidden from the client.
3.1. Defining Abstract Classes
The keyword abstract is used to define both a class and its methods as abstract.
01 | public abstract class MyAbstractClass { |
03 | protected int someNumber = 0; |
05 | public void increment() { |
09 | public abstract void doSomethingWithNumber(); |
Here we have defined an abstract class called MyAbstractClass which contains an integer and provides a method for incrementing that integer. We also define an abstract method called doSomethingWithNumber(). We don’t yet know what this method will do, it will be defined in any concrete classes which extend MyAbstractClass. doSomethingWithNumber()doesn’t have a method body because it is abstract.
In interfaces all the methods are public by default, however the scope of an abstract method may be public, package or protected.
You can see that some behavioural implementation in increment() is mixed with some behavioural definition indoSomethingWithNumber() in this abstract class. Abstract classes mix some implementation with some definition. Concrete classes which extend Abstract class will reuse the implementation of increment() while guaranteeing to provide their own implementations of doSomethingWithNumber().
3.2. Extending Abstract Classes
Now that we have created an abstract class let’s make a concrete implementation for it. We make concrete implementations from abstract classes by using the extends keyword in a class which itself is not abstract.
01 | public class MyConcreteClass extends MyAbstractClass { |
03 | public void sayHello() { |
04 | System.out.println("Hello there!"); |
07 | public void doSomethingWithNumber() { |
08 | System.out.println("The number is " + someNumber); |
We have created a concrete class called MyConcreteClass and extended MyAbstractClass. We only needed to provide an implementation for the abstract method doSomethingWithNumber() because we inherit the non private member variables and methods from MyAbstractClass. If any client calls increment() on MyConcreteClass the implementation defined in MyAbstractClass will be executed. We also created a new method called sayHello() which is unique to MyConcreteClass and wouldn’t be available from any other concrete class which implements MyAbstractClass.
We can also extend MyAbstractClass with another abstract class where we don’t implement doSomethingWithNumber – this means that another concrete class will have to be defined, which extends this new class in order to implementdoSomethingWithNumber().
1 | public abstract class MyOtherAbstractClass extends MyAbstractClass { |
3 | public void sayHello() { |
4 | System.out.println("Hello there!"); |
We didn’t have to make any reference to doSomethingWithNumber() here, whenever we create a concrete class for MyOtherAbstractClass we will provide the implementation for doSomethingWithNumber().
Lastly, abstract classes can themselves implement interfaces. Because the abstract class can’t be instantiated itself it does not have to provide an implementation for all (or any) of the interface methods. If the abstract class does not provide an implementation for an interface method, the concrete class which extends the abstract class will have to provide the implementation.
1 | public abstract MyImplementingAbstractClass implements MyInterface { |
3 | public void methodA() { |
4 | System.out.println("Method A has been implemented in this abstract class"); |
Here we see that MyImplementingAbstractClass implements MyInterface but only provides an implementation for methodA(). If any concrete class extends MyImplementingAbstractClass it will have to provide an implementation for methodB() and methodC() as defined in MyInterface.
3.3. Using Abstract Classes
Just like with Interfaces and regular classes, to call the methods of an abstract class you use the dot (.) operator.
1 | MyAbstractClass object1 = new MyConcreteClass(); |
3 | object1.doSomethingWithNumber(); |
Again we see that object1 is a reference to an instance that provides a concrete implementation for MyAbstractClass and the runtime type of that instance is MyConcreteClass. For all intents and purposes object1 is treated by the compiler as if it is a MyAbstractClass instance. If you were to try to call the sayHello() method defined in MyConcreteClass you would get a compiler error. This method is not visible to the compiler through object1 because object1 is a MyAbstractClass reference. The only guarantee object1 provides is that it will have implementations for the methods defined in MyAbstractClass, any other methods provided by the runtime type are not visible.
As with interfaces we can provide different runtime types and use them through the same reference.
Lets define a new abstract class
1 | public abstract class TwoMethodAbstractClass { |
3 | public void oneMethod() { |
4 | System.out.prinln("oneMethod is implemented in TwoMethodAbstractClass."); |
7 | public abstract void twoMethod(); |
It defines one implemented method an another abstract method.
Let’s extend it with a concrete class
1 | public class ConcreteClassA extends TwoMethodAbstractClass { |
3 | public void twoMethod() { |
4 | System.out.println("twoMethod is implemented in ConcreteClassA."); |
We can use it in a client just like before:
1 | TwoMethodAbstractClass myObject = new ConcreteClassA(); |
Output:
1 | oneMethod is implemented in TwoMethodAbstractClass. |
2 | twoMethod is implemented in ConcreteClassA. |
Now let’s make a different concrete class which extends TwoMethodClass:
1 | public class ConcreteClassB extends TwoMethodAbstractClass { |
3 | public void twoMethod() { |
4 | System.out.println("ConcreteClassB implements its own twoMethod."); |
And modify our code above:
1 | TwoMethodAbstractClass myObject = new ConcreteClassA(); |
4 | myObject = new ConcreteClassB(); |
Output:
1 | oneMethod is implemented in TwoMethodAbstractClass. |
2 | twoMethod is implemented in ConcreteClassA. |
3 | oneMethod is implemented in TwoMethodAbstractClass. |
4 | ConcreteClassB implements its own twoMethod. |
We have used the same reference (myObject) to refer to instances of two different runtime types. When the runtime type of myObject is ConcreteClassA it uses the implementation of twoMethod from ConcreteClassA. When the runtime type of myObject is ConcreteClassB it uses the implementation of twoMethod from ConcreteClassB. In both cases the common implementation of oneMethod from TwoMethodAbstractClass is used.
Abstract classes are used to define common behaviours while providing contracts or promises that other behaviours will be available from a concrete class later. This allows us to build object models where we can reuse common functionality and capture differing requirements in different concrete classes.
4. A Worked Example – Payments System
We have been asked to build a payments system for a company. We know that the company has different types of employees that can be paid in different ways; salaried and with commission. The boss of the company understands that his needs will change and the system may be changed later to accommodate different types of entities which will receive payments.
4.1. The Payee Interface
Let’s start by considering the employees. They must receive payments, but we also know that later on we may need to have different entities also receive payments. Let’s create an interface, Payee, which will define the sort of behaviour we’d expect from entities which can receive payments.
1 | public interface Payee { |
Here we have a Payee interface which guarantees three behaviours; the ability to provide a name for the Payee, the ability to provide a gross payment amount which should be paid and the ability to provide a bank account number where the funds should be deposited.
4.2. The Payment System
Now that we have a Payee defined let’s write some code that uses it. We’ll create a PaymentSystem class which maintains a list of Payees and on demand will cycle through them and transfer the requested amount into the appropriate bank account.
01 | public class PaymentSystem { |
03 | private List<Payee> payees; |
05 | public PaymentSystem() { |
06 | payees = new ArrayList<>(); |
09 | public void addPayee(Payee payee) { |
10 | if (!payees.contains(payee)) { |
15 | public void processPayments() { |
16 | for (Payee payee : payees) { |
17 | Double grossPayment = payee.grossPayment(); |
19 | System.out.println("Paying to " + payee.name()); |
20 | System.out.println("\tGross\t" + grossPayment); |
21 | System.out.println("\tTransferred to Account: " + payee.bankAccount()); |
You can see that the PaymentSystem is totally agnostic as to the runtime types of the Payees, it doesn’t care and doesn’t have to care. It knows that no matter what the runtime type the Payee being process is guaranteed to implement name(),grossPayment() and bankAccount(). Given that knowledge it’s simply a matter of executing a for loop across all the Payees and processing their payments using these methods.
4.3. The Employee Classes
We have been told by the boss that he has two different types of Employee – Salaried Employees and Commissioned Employees. Salaried Employees have a base salary which doesn’t change while Commissioned Employees have a base salary and also can be given sales commissions for successful sales.
No comments:
Post a Comment