This presentation introduces the abstraction and encapsulation
capabilities offered by Java.
Generally,
- Encapsulation is the separation of how a class
is used from how its data is represented
- Abstraction is the related idea of hiding the details
of how a class is implemented from its users
Examples of these concepts will be given through an application that
performs matrix algebra.
The general idea of an
interface comes from hardware. For example:
- A USB (universal serial bus) interface allows keyboards, mice,
etc to be connected to a computer
- Advantage: USB interfaces allow multiple kinds
of devices to be connected through the same kind of
physical input, increasing the computer's flexibility
- Requirement: Each device must "talk" to the computer through a common
communication protocol
The idea of an interface in software, in particular object-oriented
software, can be found in
Applications Programming Interfaces, or
APIs.
APIs specify how classes can be used, particularly what their public
methods are and how they should be called.
A Java type is specified by indicating the kinds of operations that can
be performed on values of the type.
Here are some operations we would expect to be able to perform
on a matrix, and which therefore should be part of a matrix API:
- Get a matrix's number of rows
- Set a matrix's number of rows
- Get a matrix's number of columns
- Set a matrix's number of columns
- Get the value at a particular row and column
- Set the value at a particular row and column
- Add a matrix object to another, similarly dimensioned
- Multiply a matrix object by another, appropriately dimensioned
Java has the concept of an
interface built into the
language. Interfaces describe methods by giving their signatures (name,
return type and parameters types) but not their bodies.
A Java interface is a little like a C++ class with only pure virtual
methods. However, interfaces do not have constructors, and they do not
define instance fields.
Interfaces are intended to be
implemented by classes with
constructors and instance fields.
When a class implements an interface, it is obligated to provide method bodies
(also called implementations) of all methods in the interface (with
the exception of
abstract classes, discussed later).
Below is a
Matrix interface type definition that reflects how we
would like to use matrices. Note:
- None of the methods has an implementation
- Some of the methods have comments indicating conditions under
which a MatrixException occurs
As of Java 8, interface definitions can include
default methods,
i.e. methods with implementations.
- The body of a default method can use other methods in the interface
even if they don't have implementations, because those implementations
will be provided by implementing classes.
-
Default methods add to the API for for the type in question.
When a method implementation makes use of methods whose own
implementations are provided by another class, it is called a
"
template method."
Here are two things we might expect a matrix type to do for us:
- Check row bounds
- Clear the matrix, or initialize all matrix elements to zero
In the examples that follow, note the use of the "
default"
keyword and the method bodies.
The code below shows default methods that support matrix bounds checking.
- Since the checkRowBounds method uses the getNumRows
method, which will be provided by an implementing class, it is both a
default and a template method
- The checkColumnBounds method is also a default template
method
- The convenience method checkBounds is a default method,
but not a template method
The code below shows a default method that clears a matrix. It is also
a template method.
The default methods have been added to the end of the interface definition:
The Unified Modeling Language (UML) can be used to represent interfaces
graphically.
You will learn how to use UML in a lab exercise, but below is a UML
representation of the
Matrix interface as currently defined.
Also shown (using the
dotted arrow) is the
interface's dependence on the
MatrixException class.
Interfaces are intended to be implemented by classes. In our example,
we are going to provide two implementations: one based on arrays (the
class
ArrayImplementation), and
one based on lists (the class
ArrayListImplementation).
If each of these classes were defined from scratch, they would have
code in common; for example, the
getNumRows
and
getNumColumns would be the same.
We will instead define a class that partially implements
the
Matrix interface, and from which
both
ArrayImplementation and
ArrayListImplementation can
inherit.
In Java, such a class is called
abstract. We will call this
class
AbstractMatrix.
The
AbstractMatrix class will implement most of the abstract
(non-default) methods in the
Matrix interface, but it will not
provide an underlying representation of matrix elements.
AbstractMatrix has instance fields to store the number of rows and columns in the
matrix, and implementations of the following methods:
- The getters and setters getNumRows, getNumColumns, setNumRows,
and setNumColumns
- The add and multiply methods
- The generally useful toString
and equals methods. These cannot be in
the Matrix interface because they are part of the Object
superclass.
The
get and
set methods are not implemented, since they
must be provided by an underlying data representation class. This is
why this class is abstract.
Also note that
equals,
add, and
multiply
are intended to be written by students, so their implementations
are not shown here.
Below is the source for
AbstractMatrix. Note the use of the
"
abstract" keyword in the class header.
When any class, abstract or not, implements an interface, it is shown
in UML with the dotted line ending with a triangle, as shown below.
Note that the class icon in UML provides a location for showing
attributes (instance fields), along with the new methods defined.
At this point, it is worth noting that we have not yet considered how
actual elements of a matrix are to be represented. That is, we have not
decided on a
data representation for matrices.
Yet, through the
Matrix
interface and the
AbstractMatrix abstract class, we have
provided usable code that stores matrix dimension information and
implements the following operations:
- Get and set the dimension information
- Check row and column bounds
- Clear a matrix
- Perform matrix addition and multiplication
- Check for matrix equality
- Produce a string representation of a matrix
We have managed to do this because we have strictly separated the
details of how matrices are
represented from how matrices
are
used.
That is, we have enforced the principle of
data abstraction.
The only matrix API operations we have described and not implemented are
those to
get and
set actual array elements, because they
require a data representation.
Of course, to have code that we can run, we must come up with a data
representation. For this, we introduce the concept of a concrete class.
A class that is not abstract is
concrete. If it implements an
interface or extends a class that does, it completes the full
implementation of the interface.
In our example, we have two concrete classes that complete the
implementation of the
Matrix
interface by subclassing (extending) the
AbstractMatrix
class:
ArrayImplementation and
ArrayListImplementation.
The first is complete, while the second is intended to be completed by
students.
- Both classes provide constructors that can be called directly using
the new operator.
- Both classes also provide implementations of the get
and set methods, but they benefit from the methods provided
by AbstractMatrix, and the amount of new code that they
introduce is minimized.
Shown below is the UML depiction of these classes extending
the
AbstractMatrix class, using the solid line ending in a
triangle.
See the menu to the left for their source code.
An important operation that so far is not part of our matrix API
is
creating a new matrix given the number of rows and columns.
We will consider two approaches to remedying this.
One way is to add a
create method to the API
along with
get,
set,
add, etc., like this:
In fact, now that we have a data representation for matrices and
constructors for them, we could use a default method:
The problem with this approach is that in order to create a new matrix,
we would need an already created matrix object to do it. For example,
suppose
m1 is an existing matrix. Then we can create
m2
like this:
This has two problems:
- It is unnatural to ask a matrix to create another matrix,
i.e. it makes for a clumsy API, and
- How was m1 created?
The solution is to make
create a
static method, so that
it can be created like this:
Since Java 8, we can introduce static methods in interfaces simply by
replacing the "
default" keyword with "
static":
Not only is this more natural, it also allows us to easily change our
data representation choice without affecting any other code in the
API. Note how the following "swaps out" the array implementation for
the array list implementation:
The inclusion of the static
create method introduces
dependencies on the concrete implementation files, as shown below.
While in general dependencies in UML models should be minimized, in
this case their use does not impede the benefits of
polymorphism, as
described next.
One of the dictates of effective object-oriented design is:
Code to
interfaces. That is, whenever possible define interface types for
your program variables rather than class types.
The reason is that code using interface types for variables is easier
to extend and modify.
Without the
Matrix interface type and
AbstractMatrix
abstract class, we would have the following disadvantages:
- The ArrayImplementation
and ArrayListImplementation classes would have to be written
from scratch
- There would be a lot of repetitive code between them.
- High-level changes in one implementation would also have to be
made in the other
- Each would need to have its own test class
An example is found in the
MatrixTest class used in our
application, whose code is shown in the menu to the left:
- This class exists to test the features of our Matrix API that
have been implemented so far.
- Since the main test variables are of interface type Matrix, the
same code that uses and tests the implementation can be used,
regardless of whether the objects referred to by the variables
are ArrayImplementation objects
or ArrayListImplementation objects.
The fact that we can change the
Matrix interface's
create method from
one implementation to another without having to change any
MatrixTest code is an effect of
polymorphism:
- Polymorphism is the ability of code to dynamically change its behavior depending on the
types of the objects it uses.
- Polymorphism can only be achieved when, as in MatrixTest,
program variables are of generic interface types (like Matrix),
rather than concrete class types (like ArrayImplementation
or ArrayListImplementation).
Here are all the files from this presentation collected together.