Object-Oriented Programming Languages
Java is an object-oriented programming language. An "object" is a
programming construct which is used to represent both data and actions
to work on that data in a computer program. These actions are known
as "methods". Methods sometimes need more knowledge than what an
object has in order to do their job effectively. We can give this
knowledge by passing information to our methods in the form of
"parameters". Our methods can also give feed back about the action it
performed as return values.
We define the characteristics of an object by creating a "class". A
class contains all of the things available to an object. These
"things" are commonly referred to as "members" of the class, and are
either data members ("variables") or method members. A class serves
as a blue-print for an object: the class only shows what an object can
do. However, nothing has been created yet: to make the class
effective we often have to create an instance of a class, which is
what an object is. A primary goal of OOP is code reusability: once we
have a class defined, we create instances of that class in use them in
many different programs.
A Java class
Let us look at an example Java class: a class used to represent a
point in Cartesian coordinates:
class Point {
private double x, y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public void move(double dx, double dy) {
x += dx;
y += dy;
}
public String toString() {
return "(" + x + ", " + y + ")";
}
public void equals(Point p) {
return (p.x == x && p.y == y);
}
}
The class here is called "Point". It has two data members,
x and y. It also has three methods: move,
toString, and equals. There is also something which
is used to create a new Point, a "constructor".
The data members of this class are of a primitive data type, known as
double. A primitive data type is different from an object in
how it is represented in your computer. There are a total of 8
primitive data types in Java (byte, short,
int, long, float, double,
char, and boolean). The Java reserved word
"private" in front of the declaration of x and
y is known as an "access modifier": it is used to control
what is allowed to use the variables x and y. The
word private means that x and y can only be accessed
inside of this class. This concept is part of "encapsulation" which
deals with keeping members of a class which a user-programmer does not
need to know about in order to make the class useful. A member which
is "public", however, is accessible outside of the class.
The next line (public Point(double x, double y) {) is known
as the constructor for the class. This will be used to create new
Point objects. For instance, to create a Point at (1.0,
-1.0), the following statement would be used:
Point p = new Point(1.0, -1.0);
This statement creates a reference p and assigns it to refer
to a location in memory which is a point containing the specified
values, like the picture below.
The constructor is public because we want other classes to be
able to create an instance of a Point. The constructor also
uses the "self-referencing pointer" this to avoid name-scope
confusion.
The move is a method. Because it is public, a
programmer will be able to call this method directly on a
Point object by using the dot operator. This method takes
two parameters, and returns nothing. Because the method does not
return anything, the reserved-word void is used as the return
type.
The toString method, on the other hand, does return a value.
In this case, it returns a String object. Because the method
returns a value, the last statement executed must be of the form
return <expression>;
where <expression> is the value we want this method to
return. Note that the type of this expression must match the type
specified in the method header.
Our final method has both a parameter and a return type. This method
is used to determine if a given Point is equal to another
Point. It is necessary because the built-in Java operator
== may not go what we want it to do. For example, consider
the following situation:
Point p = new Point(1.0, -1.0);
Point q = p;
Point r = new Point(1.0, -1.0);
The memory diagram for this situation is given below.
The == bases equality on whether two references refer to the
same location in memory. If they do, they are equal. If not, then
the references are not ==, no matter the contents of either memory
location. Hence, p == q is true, while p ==
r is false. If we care about the values in memory, we need an
equals method.
Note that this method seemingly does something illegal: it directly
accesses the x and y members of a Point,
even those are declared as private. This is actually
acceptable: private members are used to hide implementation
details of a class that a user does not need to know. However, since
the the parameter is a Point object, we are allowed to
directly use the members of the object.
Inheritance
One of the advantage of the Point class above is that it
could be used in a variety of applications. For instance, we could
use a Point object for information in a chart, or for paddle
and ball positions in Pong, ...
However, the Point class is not good enough to keep track of
a player's position in Quake III: we need a third dimension. See we
could re-define an entirely new class called Point3d which
has many similar features to a Point object, but adds a third
dimension. Instead, though, we would prefer to build upon the
existing Point class, using the parts of the class we know
work (the x and y) and add a z-coordinate.
There is a mechanism in objected-oriented programming languages known
as "inheritance" which allows this to happen. Inheritance allows us
to build upon an existing class, use the things that work in that
class, change the things that do not ("overriding"), and also add
completely different functionality.
We often draw pictures like the following:
We say that Point is a "super class" of Point3d, and
that Point3d is a "sub class" of Point. Another
common phrase is that "Point3d is-a Point." One
useful consequence of this is that we can assign a Point3d
object into a Point. This will come in handy later.
As sub class inherits an initial set (with the caveats discussed
below) of methods and data members from the super class, which can be
used as if they were defined in the sub-class.
Inheritance in Java
In Java, we have a sub class inherit from a super class by using the
Java reserved word extends:
class Subclass extends Superclass {
// class contents
}
For example, we could create a Point3d as below:
class Point3d extends Point {
private double z;
public Point3d(double x, double y, double z) {
super(x, y);
this.z = z;
}
public double getZ() {
return z;
}
public void move(double dx, double dy, double dz) {
super.move(dx, dy);
z += dz;
}
public boolean equals(Point3d p) {
return super.equals(p) && p.z == z;
}
public String toString() {
// This will only work if x and y are protected in the super class
return "(" + x +", " + y + ", " + z + ")";
}
}
The subclass then has direct access to any member (data or method) of
the super class, provided that the member is declared with either a
public or protected access modifier. The
protected modifier is likely new to you. A
protected member is similar to private in that a
protected member is not accessible by anybody. However, a
protected member is directly accessible in any sub class,
while a private member is not.
It is important to point out that private members are still inherited
in a sub class, they are simply not directly accessible in the sub
class. So why are they inherited? Because the super class may be
depending upon those values to do its job. To use the super class in
a sub class, you need to use the super reserved word. These
issues come into play in the move and equals methods above.
x and y remain as private in the
Point class, it becomes necessary to use the super class'
move method. Similarly, the only way we can effectively write the
equals method is to pass our parameter to the super's equals method.
Note we are allowed to pass a Point3d object into a method
which takes a Point object because a Point3d object
is-a Point object.
The super reference may be used in other ways, as well. In
the above example, it is used in the constructor. When we make a call
to
super(...);
in a sub classes constructor, we are calling the super's corresponding
constructor. Generally, the super constructor is called with a subset
of the sub class' constructor's parameters. When making a call to the
super constructor, it must be the first line in the sub class'
constructor. Indeed, if you do not make a call to a super
constructor, Java automatically calls the default constructor
super() for you. Note that this "first line" rule is only
enforced when calling a super constructor.
One final important note is that in Java all classes, either directly
or indirectly, automatically extend the Object class. The
Object has many methods defined in it, including a
toString() method and an equals(...) method. These
will be discussed further below. That is, the inheritance hierarchy
looks like:
Polymorphism
I stated above that it is possible to assign a Point3d object
into a Point reference because a Point3d object is-a
Point object:
Point p = new Point3d(1.0, -1.0, 1.0);
This is allowed because a Point3d object has all of the
characteristics of a Point object; it just happens to have
more as well.
On the other hand, the reverse is not the case: because there is no
z-component to a Point and most Point3d methods
depend upon that z-component, the following is not legal:
Point3d p = new Point(1.0, -1.0);
An interesting question to ask here is given the following
declaration:
Point p = new Point3d(1.0, -1.0, 1.0);
what does the following code print when executed?
System.out.println(p.toString());
(1.0, -1.0) or (1.0, -1.0, 1.0)?
More formally, when executing code, does the Java interpreter run the
code for the reference's class or the object's class? The answer is
the object's class: a polymorphic reference uses the type of the
object, not the type of the reference to determine which method to
call. This may not seem correct, but in fact it is what we want to
have happen. You should know it is possible to print object's by
calling System.out.println(...) with the object as a
parameter. If you look at the interface for the PrintStream class
(out is a PrintStream object), you will see the
following method headers:
public void println(String x)
public void println(Object x)
The code for the version which takes an Object likely looks
like:
public void println(Object x) {
println(x.toString());
}
(As an aside, this is also interesting because even though the
String returned by toString() is-an Object,
the String version of println is called). The version of
toString() in the Object class does not produce a
String which is very useful: it simply returns the memory
location of the Object. By over-riding the toString()
method, println(...) is able to do its job correctly.
Knowing this, another artifact of polymorphism may seem unusual: the
following set of statements are illegal:
Point3d p3 = new Point3d(1.0, -1.0, 1.0);
Point p = p3;
System.out.println(p.getZ());
It is illegal because of the call to the getZ() method: even
though the definition of the class of the object pointed to by
p contains a getZ() method, we are only allowed to
call the methods defined for what the reference is. No
getZ() method in the Point class means that the
statement is illegal.
Once we have the situation above, it is possible to assign p
into a Point3d reference, even though p is a Point
reference: we simply have to perform a cast:
Point3d another = (Point3d) p;
The Java interpreter will have no problem with the above statement
because p actually refers to a Point3d object. If
there were some sort of problem, a ClassCastException would
have been issued.
The equals method and instanceof
public boolean equals(Object o) {
return o.x == x && o.y == y;
}
A problem with this is that we are treating o as if it is a
Point: there are no x and y members in the
Object class. We fix this problem by casting:
public boolean equals(Object o) {
Point p = (Point) o;
return p.x == x && p.y == y;
}
This works so long as we actually passed a Point to the
method: if we did not, we would get a ClassCastException. We
need some way to make sure that the parameter is actually a
Point. Clearly, if it is not there is no way equality can
hold. There is an operator in Java which can tell us whether or not
our parameter is a Point: instanceof
The instanceof operator is a boolean operator which
takes two operands:
<obj> instanceof <Class name>
which returns true if <obj> can be assigned into a reference of
type <Class name>. That is, instanceof checks if the
<obj> is-an instance of the specified class. Therefore, any
reference o shows o instanceof Object to be true,
provided ois not null.
Therefore, we should write our equals method as:
public boolean equals(Object o) {
if (o instanceof Point) {
Point p = (Point) o;
return p.x == x && p.y == y;
}
else {
return super.equals(o);
}
}
Abstract classes
We turn our discussion now to Java abstract classes. An "abstract
class" is an incomplete class: not all of the methods have been
completely written, and will be left to the derived class to
implement. An abstract class can have data, methods, and constructors
just like normal, however abstract classes can not be explicitly
created using the new operator. However, we can still
perform up-casting on them: that is, we can create an object of a
class which is derived from an abstract class and then assign that
object into a reference of the type of the abstract class. Basically,
you can safely make all constructors in an abstract class
protected so that they may only be called from the
implementing class. If the sub class does not fully define all of the
abstract methods of the super class, the sub class must be declared as
abstract as well.
To create an abstract class, you use the Java reserved word
abstract:
abstract class <Class name> {
You create abstract methods in a similar manner, by using the word
abstract:
abstract <access> <return type> <identifier>(<parameters>);
Notice the use of a semi-colon (;) at the end of the method header.
Below is an example of the use of an abstract class:
abstract class Food {
abstract public void isTasty();
}
class Lutefisk extends Food {
public Lutefisk() {
}
// completing the abstract class Food
public void isTasty() {
System.out.println("Lutefisk looks disgusting, but tastes great!");
}
}
class Pizza extends Food {
public Pizza() {
}
// completing the abstract class Food
public void isTasty() {
System.out.println("I never met a pizza I didn't like!");
}
}
class Dinner {
public static void main(String [] args) {
Food dinner;
Pizza pizza = new Pizza();
Lutefisk lute = new Lutefisk();
if (Math.random() < .05) {
dinner = pizza;
}
else {
dinner = lute;
}
dinner.isTasty();
}
}
Interfaces
We have discussed the word "interface" before: an interface describes
how to use a class. In Java, it is possible to create something known
as an interface which is simply a collection of constants and abstract
methods. In fact, you might consider an interface to be a "really
abstract abstract class." Just like with abstract classes, it is not
possible to create an instance of an interface with new, but
you can perform up-casting.
To define an interface, we use the following syntax:
interface <interface name> {
// variables (constants)
// these are declared as they "normally" would be: no need for
// final or static
// Methods: no need for abtract (it is redundant)
<access> %lt;return type> <identifier>(<parameters>);
}
We then have classes implement this interface by completing all of the
methods described in the interface definition:
class <class name> implements <interface name> {
Interfaces are useful because they allow us to "extend" more than one
class: according to the rules of inheritance, it was only possible to
directly extend one class. This limits the number of up-casts we can
make with this class. However, a single class is allowed to both
extend a super class and implement an interface.
Indeed, a class is allowed to implement multiple interfaces: just list
all of the interfaces to be implemented by a comma-separated list.
Below we see an example of how interfaces can be used.
import java.util.*;
public class Dinner2 {
public static void main(String [] args) {
Pizza pizza = new Pizza();
Lutefisk lute = new Lutefisk();
Enumeration e = pizza;
Food f = lute;
f.isTasty();
pizza.isTasty();
System.out.println("Grabbing slices from that enumeration:");
while (e.hasMoreElements()) {
System.out.println(e.nextElement());
}
System.out.println("Is there any pizza left?");
System.out.println(pizza.nextElement());
}
}
abstract class Food {
abstract public void isTasty();
}
class Lutefisk extends Food {
public void isTasty() {
System.out.println("Lutefisk looks disgusting, but tastes great!");
}
}
class Pizza extends Food implements Enumeration {
private int slices_remaining = 8;
// Completing the isTasty abstract method from the Food class
public void isTasty() {
System.out.println("I've never met a pizza I didn't like!");
}
// Completing the Enumeration interface
public boolean hasMoreElements() {
return slices_remaining != 0;
}
// Completing the Enumeration interface
public Object nextElement() {
if (slices_remaining > 0) {
slices_remaining--;
return "Another slice gone!";
}
else {
return "No more slices!";
}
}
}
Exceptions
Notice that in the above example if there are no slices left, we
simply return an error message. In a more sophisticated example, we
would prefer to warn the user of this in a different way: by issuing
an exception.
Recall exceptions are used to a notify a programmer or program when
something bad happens. We can issue an exception by using the java
reserved word throw:
throw <exception object>;
For instance, to issue a NoSuchElementException when there
are no more slices, we might write the following:
public Object nextElement() {
if (slices_remaining > 0) {
slices_remaining--;
return "Another slice gone!";
}
else {
throw new NoSuchElementException("No more slices!");
}
}
A NoSuchElementException is a runtime (or unchecked)
exception. This means that there is no need for the exception to be
handled when we compile our program, like a checked exception requires
you to (with throws or a try-catch block).
It is also possible to define your own exceptions: you simply need to
make your exception the sub class of the correct super class. For
checked exceptions, your class should extend Exception. For
un-checked exceptions, your class should extend RuntimeException.
© 2000 Michael Wade
All rights reserved