With this lesson we start learning Object Oriented Programming (OOP). Object Oriented Programming as a paradigm has four (or three, depending on how you count) fundamental tenants: encapsulation, data-hiding, inheritance, and polymorphism. Today we kick things off with encapsulation.
So far we've seen classes used in two ways: collections of functions and collections of data. Of course, we could mix the member data and the static member functions we've been dealing with into a single class, but that wouldn't get us quite to the "objects" of Object Oriented Programming. To get there, we need to understand a new mechanism: non-static member functions, which in Java parlance are called instance methods (as opposed to the static member functions which are called static methods in Java parlance).
First let's recall how instance data works. Imagine you have a class Point
defined as follows:
public class Point {
public double x, y;
}
... and suppose in some other class file you create two instances like this:
Point A = new Point();
Point B = new Point();
A key observation is that at this point, the expression x
or the expression y
or an expression like Math.sqrt(x*x + y*y)
makes no sense. Why? because nobody knows which x
or y
you mean! x's and y's belong to instances of Points. So you need to say something like "the x that belongs to the Point that A refers to" (which we write as A.x
), or "the x that belongs to the Point that B refers to" (which
we write as B.x
).
A function defined without the modifier static
works the same way. If we add to the class Point like this
public class Point {
public double x, y;
public double distance() {
return Math.sqrt(x*x + y*y);
}
}
and imagine the same two instances
Point A = new Point();
Point B = new Point();
... the expression distance()
makes no sense. Why? Because nobody knows whether you mean the distance()
that belongs to the Point A refers to or the distance()
that belongs to
the Point B refers to. We think of instance methods belonging to instances just like instance data, and we specify which instance is intended in exactly the same way:
A.distance()
versus B.distance()
. Looking inside the definition of distance
, you see the expression Math.sqrt(x*x + y*y)
, and you might be tempted to think that it violates what I said earlier, i.e.
which x?, which y?. The answer is the x and y that belong to the same instance of
Point that this distance() belongs to. In other words, when you make the call
A.distance()
... the x and y in Math.sqrt(x*x + y*y)
are the ones belonging to A. When you make the call
B.distance()
... the x and y in Math.sqrt(x*x + y*y)
are the ones belonging to B.
Conceptually, each instance of class Point in the above example has its own distance() function, just like it has its own x and its own y. In reality, the way Java handles instance methods works a bit differently. In the implementation, the call
A.distance()
really acts like a call to
distance(A)
, and this is always the case.
A call to instance method foo
of the form
obj.foo(arg1,arg2,...,argk)
... is actually a call to a static function
foo(obj,arg1,arg2,...,argk)
In fact, this is so literally true, that this implicit parameter that is the object on which the method was called (i.e. what's before the dot), which has the name this
, can be used inside the function definition. For example:
public void addToMe(Point B) { x += B.x; y += B.y; }
same as
public void addToMe(Point B) { this.x += B.x; this.y += B.y; }
So, to clarify, when we make a call to a non-static member function (instance method in Java parlance), the object we call with, i.e. the object before the ".", is implicitly an extra parameter named "this". Hopefully the illustration below clarifies things.
public class Point {
double x, y;
public void addToMe(Point p) {
this.x += p.x;
this.y += p.y;
}
public static void main(String[] args) {
Point a = new Point();
Point b = new Point();
a.x = 2;
a.y = 5;
b.x = 3;
b.y = 1;
a.addToMe(b);
System.out.println(a.x + "," + a.y);
}
}
Following the Object Oriented Programming paradigm, a program consists of object instances communicating by calling each others' methods. Thus instead of the function being the fundamental unit of a program, as it is in procedural programming,
the object instance is the fundamental unit of a program. Each instance of a class has a well-defined interface — the collection of its method prototypes (i.e. the first line of the method declaration) and, hopefully, a bit of documentation
— and a well-defined implementation — the definitions of its methods and its data members (more properly called fields is Java parlance). So what's the difference? Well, object instances have data-members/fields, so they have memory
or, as computer scientists more formally would say, they have state. The upshot of that is that calls to the same method for the same instance with the same arguments can give different results over time, because the instance has memory/state
that can evolve as the program executes. This matches the way things work in the real-world (if p
is an instance of class Person
, p.weight()
give a different answer after a big dinner than it did before), and matches
the way we like to think of many abstractions in software systems.
So, when sitting down to design a program in Java, instead of asking yourself "what functions will I need?", you ask yourself "what classes will I need?"; and answering that question will require you think about collections of methods you want to be able to call for each different type of "thing" in your program.
Let's consider a simple example. I'd like to write a program that keeps track of batting results in baseball, in order to report players' batting averages. For simplicity, we'll assume that the result of a player stepping up to the plate will either be a walk, a hit, or an out. The formula for batting average is hits/atbats, where an at-bat is an appearance that resulted in a hit or an out — i.e. walks are ignored. In my program, I'd like to be able to handle many players.
In considering an OOP approach, we would identify that a player should be an object in our program. We should have a method that allows us to record the results of one or more plate appearances, and we should have a method that reports the player's current batting average. This leads us to an interface like this:
class Batter {
void record(String outcomes) // records outcomes of plate appearances; outcomes is a string of h/w/o's like "hoowh"
double average() // returns the current batting average, no rounding
}
If we had this kind of interface, we could write code like this:
Batter b = new Batter();
b.record("owhoowoowhhwoohoo");
System.out.println(b.average());
b.record("hhowowohhwohoohoowoooh");
System.out.println(b.average());
Of course, whole teams worth of batters would work the same way. We'd just have arrays or linked lists of Batter objects, each recording and reporting their own batting averages.
So what about implementing this? The implementation has to remember things in order to be able to report a batting average. This means it has to have data-members/fields. What we want to remember is an implementation decision. One option is to keep a count of at-bats and a count of hits. That leaves us with something like the following
public class Batter {
int hits;
int atBats;
public void record(String outcomes) {
for (int i = 0; i < outcomes.length(); i++) {
if (outcomes.charAt(i) == 'h') {
hits++;
}
if (outcomes.charAt(i) != 'w') {
atBats++;
}
}
}
public double average() {
return (double)hits / atBats;
}
public static void main(String[] args) {
Batter b = new Batter();
b.record("owhoowoowhhwoohoo");
System.out.println(b.average());
b.record("hhowowohhwohoohoowoooh");
System.out.println(b.average());
}
}
public class Team {
public static void main(String[] args) {
Batter A = new Batter();
Batter B = new Batter();
A.hits = 0;
String[][] res = { { "oohw", "ohoh" }, { "howoo", "woho" } };
for (int i = 0; i < res.length; i++) {
A.record(res[i][0]);
B.record(res[i][1]);
System.out.println("A: " + A.average()
+ ", B: " + B.average());
}
}
}
~/$ java Team A: 0.3333333333333333, B: 0.5 A: 0.2857142857142857, B: 0.42857142857142855