CISC 3115
Modern Programming Techniques
Lecture 1
Prep
Overview
We introduce the notion of a class. The basic idea is to bundle together state (i.e., variables) and
behavior (methods) into a logical entity that presents or models specific logical behavior. Our initial fundamental
change will be to take the principal values of our proposed object and place them at the class level rather than as local variables;
in effect making them available to all the methods in the class (assuming we remove the static keyword from their header — see below).
Most of the other changes are then a consequence of this change.
class BankAccount {
public void deposit(int amount) {balance += amount;}
public void withdraw(int amount) {balance -= amount;}
public String toString() {return "$" + balance;}
private int balance = 0;
public static void main(String [] args) {
BankAccount bankAccount = new BankAccount();
System.out.println("After creation: " + bankAccount);
bankAccount.deposit(200);
System.out.println("After deposit of $200: " + bankAccount);
bankAccount.withdraw(50);
System.out.println("After withdrawal of $50: " + bankAccount);
bankAccount.withdraw(70);
System.out.println("After withdrawal of $70: " + bankAccount);
}
}
Notes
The primary (actually only in this case) value is the balance; we move it out of main (where it was a local variable)
to the level of the class body.
Such variables are called instance variables (we will explain why shortly)
Instance variables are visible to all the methods of the class, and can be manipulated by them
As such, they do not need to be passed as parameters to the methods; they are directly accessible
Notice the massive reduction in the number of arguments, often going to 0
Again, that's because the state of the object is being maintained in the instance variable, there's no need
to pass anything to the method (other than external information, e.g. the amount of a deposit or withdrawal; that
is NOT part of the state of the object)
Creating an object or instance of the class is a familiar operation:
classnameidentifier = new classname(…);
for example:
BankAccount bankAccount = new BankAccount();
This creates a new instance of the class with its own copy of the variables (which is why they're called instance variables —
each instance has its own set of them.
Note the absence of static on the method headers; these are known as instance methods and
will operate on a particular instance's variables.
Invoking (calling) a method on a particular instance is also familiar:
bankAccount.deposit(amount);
The instance variable (balance that is used in the method is the one associated with the instance referenced by bankAccount
We call the object upon which we are invoking the method the receiver (of the method).
We do not want to allow any outside code to directly manipulated the value of balance so we declare it private
Only the instance methods of the class can access private instance variables
The instance methods of the class are declared public so that they can be invoked from outside the class.
We introduce a method named toString. This method returns a String representation of our object
The exact nature of the representation is up to the class designer (us in this case).
For this class, we choose to return balance preceded by a $
For reasons that will become clear later on, toStringmust be declared public
The method main does not implement an operation of the class; rather it is a method that uses the class
In particular, we would not invoke main in the following manner:
Rather main a class that we are using to illustrate use of our BankAccount class.
There is no receiver, so in some sense, main is 'separate' from the class (i.e., it does not operate directly on the instance variables
of the class), and to indicate that, we declare it static
This is an imprecise explanation of what is happening; a better explanation will be forthcoming shortly
class BankAccount {
public int deposit(int amount) {balance += amount;}
public int withdraw(int amount) {if (balance >= amount) balance -= amount;}
private int balance = 0;
}
Notes
Note the change in the logic of the operation; this is all isolated within the 'behavior' of the class.
Summary
Some Things to Think About
What if you wanted two bank accounts?
What about twenty of them?
What about a savings account and a checking account, i.e., with and without overdraft?
Does the introduction of the class address any of these issues?
Modelling Things with Several (Logically-Related) Elements: A Container (02-Container)
Write an app that reads values from a file and places them into a container, i.e., a data entity that contains other items of data. The
methods to be provided are:
add — adds a value to the container
find — searches the container for a value and returns the location at which the value was found and -1 otherwise
get — returns the value at a specified position in the container
size — returns the number of elements in the container
isEmpty — returns true if the container has no elements; false otherwise
toString — returns a string-representation of the container with surrounding {}'s and commas between the elements
class Container {
public int add(int value) {
values[size] = value;
size++;
return size;
}
public int size() {return size;}
public boolean isEmpty() {return size() == 0;}
public int find(int value) {
for (int i = 0; i < size; i++)
if (values[i] == value) return i;
return -1;
}
public int get(int index) {return values[index];}
public String toString() {
String result = "{";
for (int i = 0; i < size; i++)
result += values[i] + (i < size-1 ? ", " : "");
return result + "}";
}
private static final int CAPACITY = 100;
private int [] values = new int[CAPACITY];
private int size = 0;
}
Notes
Note how the array and size instance variables work in concert with each other
And again, note the signatures of the class' methods; they correspond much more closely to the operation on a container
(i.e., add a value rather than add a value to the array containing 'size' elements
As before, in order to eliminate magic numbers in the code proper, we declare a constant (final) named CAPACITY,
initialize it to the capacity (physical size) to be used for all array instance variables, and use it exclusively wherever the capacity value is
required
However, we do not need (or want) a CAPACITY variable created with every instance we created (they're all going to contain 100),
so we declare it static, (lacking change) — which means only one CAPACITY variable is created and shared
by the entire class (i.e., all instances).
Such variables are known as class variables (or static), as they operate at the class (rather than instance) level
This is somewhat similar to the notion of static in method headers. There also, static meant that the method is not tied to any
particular set of instance variables
We declared CAPACITY to be private as we have decided that it is nobody's business what the capacity of the array is. Were we to decide differently,
we have a couple of choices:
Make CAPACITYpublic
To access this variable from outside the class, one would write Container.CAPACITY
Keep it private and introduce a method named getCapacity (thus an outside caller can obtain the value, but not modify it).
Since this method is limited to accessing the value of CAPACITY — a static variable, it is also declared static
as it does not access or interact with instance variables.
To call this method from outside the class, one would write Container.getCapacity()
import java.io.File;
import java.util.Scanner;
class ContainerApp {
public static void main(String [] args) throws Exception {
Scanner scanner = new Scanner(new File("container_actions.text"));
Container container = new Container();
while (scanner.hasNext()) {
String action = scanner.next();
int value;
int index;
switch (action) {
case "A":
value = scanner.nextInt();
container.add(value);
System.out.print("After adding " + value + ": " + container);
break;
case "F":
value = scanner.nextInt();
int pos = container.find(value);
if (pos >= 0)
System.out.println(value + " is at position " + pos + " of the container");
else
System.out.println(value + " is not in the container");
break;
case "G":
index = scanner.nextInt();
value = container.get(index);
System.out.println("The value at location " + index + " is " + value);
break;
default:
System.out.println("*** Error *** unknown action: " + action);
}
}
}
}
Model a point in 2-dimensional space (i.e., the 2D plane). A point is represented as a pair of values (we'll stick with integers … at least for now) representing
the x and y components of the point.
We will support the methods:
quadrant: returns a numeric value between 0 and 8 corresponding to the quadrant or
axis the point lies on
quadrantStr: returns a String value corresponding to the quadrant/axis the point lies on
public class Point {
Point(int x, int y) {this.x = x;this.y = x;}
public String quadrantStr() {
switch (quadrant()) {
case 0: return "Origin";
case 1: return "Quadrant 1";
case 2: return "Quadrant 2";
case 3: return "Quadrant 3";
case 4: return "Quadrant 4";
case 5: return "Positive x axis";
case 6: return "Positive y axis";
case 7: return "Negative x axis";
case 8: return "Negative y axis";
default: return "???";
}
}
public int quadrant() {
if (x == 0 && y == 0) return 0;
if (x > 0 && y > 0) return 1;
if (x < 0 && y > 0) return 2;
if (x < 0 && y < 0) return 3;
if (x > 0 && y < 0) return 4;
if (x > 0 && y == 0) return 5;
if (x == 0 && y > 0) return 6;
if (x < 0 && y == 0) return 7;
if (x == 0 && y < 0) return 8;
return -1;
}
public String toString() {return "(" + x + ", " + y + ")";}
private int x, y;
public static final Point ORIGIN = new Point(0, 0);
}
Notes
Most of this should now be familiar, but there is one new issue introduced by this class: object initialization
Unlike the previous two classes, where the instance variables were initialized with the same value
for all instances (0 for balance and size, and an array of sizee
100 for values), each Point instance will have its own individual values
for x and y; furthermore x and y need to be
initialized with these values upon object creation.
We accomplish this with a method that is invoked when the object is created; we call such a method a
constructor
These methods are not new; you've seen them before:
Scanner scanner = new Scanner(System.in);
Random r = new Random(12345);
PrintStream ps = new PrintStream("outputfile.data");
The constructor is only called when an object of the class is created, and is not directly called by the
programmer; but rather automatically as part of the new operation. As such, it has no return type
(not called, so doesn't return anywhere), nor is its name a choice of the programmer (since they don't call it).
As such, the usual name assigned to the constructor is the name of the class; this works out well in the
syntax and semantics of the constructor, as we will see.
Not every OOP language follows this naming convention; Python for examples does something a bit different.
In our Point class, we need a method that accepts two integers which are then assigned to
x and y; given the above naming discussion we have:
Point(int …, int …) {
x = …;
y = …;
}
Note the absence of a return type,and the name of the constructor method (Point).
Regarding the naming of the parameters, and the use of this see below
A point to be made: the variables a and b are parameters to the method and thus
local variables, while x and y are instance variables and declared in the class-body.
class Point {
Point(int …, int …) {
…
}
…
private int x, y;
}
The body of the constructor assigns the values of the parameters to the instance variables.
As seen below in the app, the parameters receive their values from the corresponding arguments passed to the constructor in the new
expression.
Point p = new Point(12, 14); // create the Point (12, 14)
Notice the class variable ORIGIN. We wish to have a 'well-known' / predefined object for the origin (it IS special after all), so we create
an Point object corresponding to it and assign its reference to a well-named variable. There's no need for more than one such object, i.e., we don't want to
have a new origin object for each instance, so we declare it static.
This is a common technique … to declare and create 'well-know', i.e., useful object of the class and make them class variables of the class as well.
import java.io.*;
import java.util.*;
public class PointApp {
public static void main(String [] args) throws Exception {
Scanner scanner = new Scanner(new File("points.data"));
System.out.println(" --- From the file");
System.out.println();
while (scanner.hasNextInt()) {
int
x = scanner.nextInt(),
y = scanner.nextInt();
doIt(x, y);
}
Random r = new Random(12345);
System.out.println();
System.out.println(" --- Some random points between 0 and 9");
System.out.println();
for (int i = 1; i <= 20; i++) {
int
x = r.nextInt(19)-9,
y = r.nextInt(19)-9;
doIt(x, y);
}
}
public static void doIt(int x, int y) throws Exception{
System.out.println(x + " " + y);
Point p = new Point(x, y);
System.out.println("p: " + p);
System.out.println("Quadrant: " + p.quadrantStr() + " (" + p.quadrant() + ")");
}
}
More on Constructors
The Names of the Constructor Parameters
Note the names of the constructor parameters (a and b are the same as the instance variables (x and y)
they are being used to initialize.
This follows a school of thought (accepted and prevalent in Java) whose goal is to keep the code clean, readable and consistent. Rather than coming up with alternative names that 'mean the same', we use the same names.
The problem is distinguishing between the instance variables and the parameters that are local to the method.
Java uses the keyword this to refer to the receiving object within a method
In our context, it is used to 'bypass' the reference to the parameters
The resolution of which variable we are referring to works outwards: in a method we start at the locals, then outwards to the instance (and possibly class) variables.
Using this informs the compiler (and us) to skip the locals and begin the resolution process at the receiving object.
this has other uses as well, we encounter another one below
Overloaded Constructors
There are often several ways of creating an object:
Point: we create Point objects by specifying their x and y values. One could imagine that
not specifying any x/y might create a point at the origin (0, 0):
Point() {
this.x = 0;
this.y = 0;
}
Like other methods, constructors may be overloaded, i.e., two or more methods of the same name
may be defined as long as their signatures differ.
Leveraged Constructors
Even further, it is often the case that the overloaded constructors contain much of the same logic. Rather than repeating the logic,
we can often place the logic in one constructor (sometimes called the workhorse and then leverage
that logic from the other constructors (again, leveraging and delegation are related: when one method delegates to another, it is leveraging the other's logic).
Point() {this(0, 0);}
The above constructor delegates it task of initializing the x and y to 0 by calling the two-argument constructor (the one we
placed in the Point class above)
We use this (rather than Point to indicate that we are invoking another constructor of the object we are initializing.
Default Constructor
A 0-parameter constructor is called a default constructor
This is because one relies on the class definition to supply default initial values to the state of the class.
This is useful for new users of the class and classes with complex configurations / values
Copy Constructor
When we write:
Point p = new Point(10, 20);
Point p2 = p;
p2 become an alias of p, i.e., the two reference variables are now referring to the same object.
If we want two separate copies (i.e., p and p2 refer to separate objects), we can write:
Point p2 = new Point(p.getX(), p.getY());
but that requires:
We have the getter methods getX and getY, and
We know how to build Point objects
What we really want to do is copy the object referred to by p to a new object and have p2 refer to it,
i.e.:
Point p2 = new Point(p);
the above constructor is called a copy constructor and its implementation is:
Model a line in 2-dimensional space (i.e., the 2D plane). A line is represented as two pair of values (i.e., points)
We will support the methods:
quadrant: returns a numeric value between 0 and 8 corresponding to the quadrant or
axis the point lies on
printQuadrant: returns a descriptive string corresponding to the quadrant or axis of a point
length: returns the length of a line
isVertical: returns whether a line is vrtical (infinite slope)
slope: returns the slope of a line
public class Line {
Line(Point endPoint1, Point endPoint2) {
this.endPoint1 = endPoint1;
this.endPoint2 = endPoint2;
}
Line(int x1, int y1, int x2, int y2) {this(new Point(x1, y1), new Point(x2, y2));}
public double length() {return Math.sqrt((Math.pow(endPoint1.getX() - endPoint2.getX(), 2) + Math.pow(endPoint1.getY() - endPoint2.getY(), 2)));}
public boolean isVertical() {return endPoint1.getX() == endPoint2.getX();}
public double slope() {return (endPoint1.getY() - endPoint2.getY()) / (double)(endPoint2.getX() - endPoint1.getX());}
public String toString() {return endPoint1 + " - " + endPoint2;}
private Point endPoint1, endPoint2;
}
Notes
Note the overloaded constructors; one can create a line object wither using a pair
of Point objects, or by providing the x/y values for the two endpoints
whether the second constructor is useful is really up to the designer of the class
and the eventual users of the class.
There doesn't seem to be any need or application for a default constructor in this class
(what would it mean to say Line line = new Line())?
To calculate the length of the line, we need the x and y values of the Point object,
however, they are in accessible being declared private in that class.
We're not about to make them public but we are willing to provide access to their
values (but not the ability to modify them).
We therefore introduce getter (or accessor) methods
getX and getY providing such access.
To do this we to go back to our Point class and add these two methods (see below)
Again, an alternative is to make them public but that opens the values up tp modificationm
There is a third alternative we'll discuss later when we talk about packages
What about getEndPoint1 and getEndPoint2?
public class Point {
…
public int getX() {return x;}public int getY() {return y;}
…
private int x, y;
}
import java.io.*;
import java.util.*;
public class LineApp {
public static void main(String [] args) throws Exception {
Scanner scanner = new Scanner(new File("lines.data"));
while (scanner.hasNextInt()) {
int
x1 = scanner.nextInt(),
y1 = scanner.nextInt(),
x2 = scanner.nextInt(),
y2 = scanner.nextInt();
doIt(x1, y1, x2, y2);
}
Random r = new Random(12345);
System.out.println();
System.out.println(" --- Some random points between 0 and 9");
System.out.println();
for (int i = 1; i <= 20; i++) {
int
x1 = r.nextInt(19)-9,
y1 = r.nextInt(19)-9,
x2 = r.nextInt(19)-9,
y2 = r.nextInt(19)-9;
doIt(x1, y1, x2, y2);
}
}
public static void doIt(int x1, int y1, int x2, int y2) throws Exception {
System.out.println(x1 + ", " + y1 + ", " + x2 + ", " + y2);
Point p1 = new Point(x1, y1);Point p2 = new Point(x2, y2);Line line = new Line(p1, p2);
System.out.println("p1: " + p1 + " p2: " + p2);
System.out.println("line: " + line);
printQuadrantInfo(p1);
printQuadrantInfo(p2);
System.out.println("The length of the line is: " + line.length());
if (line.isVertical())
System.out.println("Vertical line, no slope");
else
System.out.println("The slope of the line is: " + line.slope());
System.out.println();
}
public static void printQuadrantInfo(Point p) {
System.out.println(p + " Quadrant: " + p.quadrantStr() + " (" + p.quadrant() + ")");
}
}
Notes
We create the Line object in doIt usinf the two-argument (endpoints)
constructor. We could have done it using the four-argument (x/y/ values) constructor:
Line line = new Line(x1, y1, x2, y2);
Notice, however, that doing so does not provide us with variables p1 and p2
for the endpoints.
The only reason our app is able to print out quadrant info or even just the poins is because it created the
Point objects. Had we used the four-arg constructor (where the x/y values are passed), we would not have
access to the Point objects (created in that Line constructor.
Model a pair of lines in 2-dimensional space (i.e., the 2D plane). A pair of lines line is represented as four pair of values (i.e., two lines of two endpoints)
We will support the methods:
quadrant: returns a numeric value between 0 and 8 corresponding to the quadrant or
axis the point lies on
printQuadrant: returns a descriptive string corresponding to the quadrant or axis of a point
length: returns the length of a line
yIntercept: returns whether a line is vrtical (infinite slope)
slope: returns the slope of a line
public class LinePair {
LinePair(Line l1, Line l2) {
line1 = l1;
line2 = l2;
}
public boolean isParallel() {return line1.slope() == line2.slope();}
public boolean isPerpendicular() {return line1.slope() == -1/line2.slope();}
public String toString() {return "line1: " + line1 + "/" + "line2: " + line2;}
Line line1, line2;
}
Notes
There really is no other intuitive constructor; there would be no reason to create a
LinePair from anything other than two Lines (i.e, on would not
create a pair of lines by supplying the four endpoints or the eight x/y values)
import java.io.*;
import java.util.*;
public class LinePairApp {
public static void main(String [] args) throws Exception {
Scanner scanner = new Scanner(new File("line_pairs.data"));
while (scanner.hasNextInt()) {
int
x11 = scanner.nextInt(),
y11 = scanner.nextInt(),
x12 = scanner.nextInt(),
y12 = scanner.nextInt(),
x21 = scanner.nextInt(),
y21 = scanner.nextInt(),
x22 = scanner.nextInt(),
y22 = scanner.nextInt();
doIt(x11, y11, x12, y12, x21, y21, x22, y22);
}
}
public static void doIt(int x11, int y11, int x12, int y12, int x21, int y21, int x22, int y22) throws Exception {
System.out.println("x11: " + x11 + " y11: " + y11 + " x12: " + x12 + " y12: " + y12 + " / " +
" x21: " + x21 + " y21: " + y21 + "x22: " + x22 + " y22: " + y22);
Point p1 = new Point(x11, y11);
Point p2 = new Point(x12, y12);
Line line1 = new Line(p1, p2);
Point p3 = new Point(x21, y21);
Point p4 = new Point(x22, y22);
Line line2 = new Line(p2, p2);
LinePair linePair = new LinePair(line1, line2);
System.out.println("linr pair: " + linePair);
printQuadrantInfo(p1);
printQuadrantInfo(p2);
printQuadrantInfo(p3);
printQuadrantInfo(p4);
System.out.println("The length of the first line is: " + line1.length());
System.out.println("The length of the second line is: " + line2.length());
double m1 = line1.slope();
double m2 = line2.slope();
System.out.println("The slope of the first line is: " + m1);
System.out.println("The slope of the second line is: " + m2);
if (linePair.isParallel()) System.out.println("The two lines are parallel");
else if (linePair.isPerpendicular()) System.out.println("The two lines are perpendicular");
System.out.println();
}
public static void printQuadrantInfo(Point p) {
System.out.println(p + ") Quadrant: " + p.quadrantStr() + " (" + p.quadrant() + ")");
}
}
Notes
Not much to say … basically same as LineApp
Modelling Many Thing with Many Elements: Phonebook (Phonebook)
Given the file, "phonebook.text" containing phone entries in the format last-namephone-number, write an application that
allows the user to look up a phone number via the last name. You can assume no two entries have the same last name.
An Overview
This example goes a step further than the previous ones in that there are two 'multiple's here: the multiple attributes of the entries of the phone book
(name and number), and multiple entries forming the phonebook.
The multiple attributes of an entry (name and number) will form a class we'll call PhonebookEntry
The multiple entries will form an array / container
What follows is a standard way of structuring and implementing such a situation. The will be three classes:
A PhonebookEntry class that will model the entry, i.e., a name and number
A Phonebook class. This will model the collection of entries and will essentially be a partially-populated
array.
PhonebookEntry is the element type of the array
This class will bear a striking resemblance to our Container class above. In fact, we COULD make use of that class
but at this stage of our OOP knowledge it would prove to be more of a distraction than anything else
The PhonebookApp class. As with the earlier app classes in this lecture, this app class will illustrate use of the
Phonebook object.
not much of note; nothing new here … we've got some instance variables, their corresponding getter
methods, and a toString
This is a simply class modelling a simple object with a couple of attributes and provides the lement type of our phonebook proper
Phonebook
import java.io.*;
import java.util.*;
public class Phonebook {
public void read(String filename) throws IOException {
Scanner scanner = new Scanner(new File(filename));
while (scanner.hasNext()) {
if (size == CAPACITY) {
System.out.println("Phonebook capacity exceeded - increase size of underlying array");
System.exit(1);
}
String name = scanner.next();
String number = scanner.next();
PhonebookEntry entry = new PhonebookEntry(name, number);entries[size] = entry;
size++;
}
}
public String lookup(String name) {
for (int i = 0; i < size; i++)
if (entries[i].getName().equals(name)) return entries[i].getNumber();return null;
}
public String toString() {
String result = "{";
for (int < = 0; i size; i++)
result += entries[i] + (i < size-1 ? ", " : "");
result += "}";
return result;
}
private static final int CAPACITY = 100;
private PhonebookEntry [] entries = new PhonebookEntry[CAPACITY];
private int size = 0;
}
Notes
There are no values that need to be initialized when the object is created; they can all be specified as initializations
in the declarations of the instance variables
Were we to write a constructor it would
be a default constructor since it requires no arguments, and
contain no logic (its all in the declarations)
Phonebook() {} // nothing to do
Under such circumstances, the class designed can omit the constructor
Note that our first couple of classes (Counter and Container
fell into this category as well. It wasn't until Point, which required a per-instance x/y
value that we needed to introduce constructors.
We place the read method here as we are populating the phonebook and a general rule is to place methods in the class
they operate on (are responsible for).
There are some enhancements we could make, but they involve a bit more familiarity with designing classes; so we'll stick
with this approach, it's simple and intuitive
The read method reads in the values (name and number strings) that will be used to construct an entry in the phone book, creates the
entry object with them, and inserts it into the array.
The lookup method iterates through the array, comparing the name again the name being searched for and if found, returns the corresponding number.
If the number cant be found, it returns the value null.
null is the value representing the lack of a reference to an object. It is assigned to a variable that
is currently not referencing anything. It is often used as a 'failure' value for searches in the same sense that -1, i.e.,
representing a non-existent value.
Why not act like find and return the position in the array?
The toString method leverages the toString of the PhonebookEntry
Again, notice the comma-logic in toString
PhonebookEntry
import java.io.*;
import java.util.*;
public class PhonebookApp {
public static void main(String [] args) throws Exception {
final String FILENAME = "phonebook.text";
Phonebook phonebook = new Phonebook();
phonebook.read(FILENAME);
System.out.println(phonebook);
Scanner scanner = new Scanner(System.in);
System.out.print("name? ");
while (scanner.hasNext()) {
String name = scanner.next();
String number = phonebook.lookup(name);
if (!number.equals(""))
System.out.println(number);
else
System.out.println("Not found");
System.out.print("name? ");
}
}
}
Notes
even though there's no explicit (default) constructor, we still create the object in the usual fashion.
notice the printing of the phone book right after the read; that's more so that we
get to use the toString of Phonebook.
Finally, notice the overall elegance of our implementation; the code is minimal and quite readable -- a far cry from the Lecture 1 version.
the movement to class (eliminating all those arguments/parameters), together with breaking the app up into three classes, really
streamlines things, and provides a 'template' for other apps of this sort.