05b: OOP Principles
ICS211, Spring 2013
Dr. Zach
(switch view)
Status check
- Exam 1: should be graded by... Mon? Definitely Wed.
- Exam and solutions posted in Laulima
- A04 still on-going
- E01 recording as soon as I and TAs get to it (tonight?)
Encapsulation, Abstraction, and Reuse
lessons learned over time
Quick History of Software (1/3)
- 1940s: computers as hardware only
- 1950s: stored instructions (software)
Quick History of Software (2/3)
- 1960s: structured programming
- ALGOL, blocks, loops
- GOTOs == bad (1968)
- C (1972)
// unstructured programming example
10 sum = 0
20 i = 1
30 if (i > 5) goto 70
35 println i
40 sum = sum + i
50 i = i + 1
60 goto 30
70 print "Total: " sum
Quick History of Software (3/3)
- 1970s: new paradigms
- Functional: Lisp (1958)
- OOP: Simula (1962), Smalltalk (OOP from ground up, public in 1980)
- Logic: Prolog (1972)
- 1980s: modules/namespaces/packages, reuse, abstractions, generics
- 1990s: programmer productivity
- garbage collection, IDEs, scripting languages (web)
- Java
Lessons from Computing History
Trends (what we've learned works well):
- abstraction - from hardware, into blocks, into functions/methods, into class hierarchies
- encapsulation - into blocks, into functions, into objects/classes/modules, into packages
- reuse - across hardware, as functions, as modules and libraries, inheritance, generics
- automation of hard-but-common details (optimizing compilers, garbage collectors)
- protection of the programmer from himself (readability, strongly-typed languages, etc.)
- vs. programmer productivity (writeability, functional and dynamic languages)
- (Others, too, I'm sure...)
Software Engineering Principles --> OOP
- Lessons informed design of Object-Oriented Programming (OOP) languages
- Most important (at least in this lecture):
- encapsulation
- abstraction
- reuse
- These concepts/principles more general than OOP; found elsewhere
Encapsulation
- weak definition: bundling data and methods together in object
- strong definition (what I'll use): "data hiding" or "information hiding"
- How to:
- make instance variables private
- make public only the minimum number of methods that you need to use the object
- Protects state of an object from (accidental) interference
Example: Square, No Encapsulation
public class Square {
public int side;
public int area;
public int perimeter;
public Square(int side) {
this.side = side;
this.area = side * side;
this.perimeter = 4 * side;
}
}
Square sq = new Square(5);
sq.area = 36; //oops! Other fields are now incorrect!
Example: Square, With Encapsulation
public class Square {
private int side;
private int area;
private int perimeter;
public Square(int side) {
this.setSide(side); //call method to do necessary work
}
public int getSide() {
return this.side;
}
public void setSide(int side) {
this.side = (side < 0) ? 0 : side; //side must be 0+
//recalculate all details
this.area = this.side * this.side;
this.perimeter = 4 * this.side;
}
public int getArea() {
return this.area;
}
public int getPerimeter() {
return this.perimeter;
}
}
Encapsulation Advantages
- Robustness/correctness: Defining class has control over state of any instances (always in a valid state)
- Square: can never have a negative side; area and perimeter always correct for current side
- Can't change area or perimeter directly
- Simplicity/abstraction: Hides implementation so user of code doesn't need to think about it
- Can also provide details so user doesn't have to (like recalculating details when Square's side changes)
- Modularity: reduces/controls dependencies and side-effects b/w classes
- Can then change implementation without affecting existing code (on next slide)
Example: Square, with implementation changed
public class Square {
private int side;
public Square(int side) {
this.setSide(side); //call method to do necessary work
}
public int getSide() {
return this.side;
}
public void setSide(int side) {
this.side = (side < 0) ? 0 : side; //side must be 0+
}
public int getArea() {
return this.side * this.side;
}
public int getPerimeter() {
return 4 * this.side;
}
}
If implementation is private, can change it without breaking existing code that uses public interface
Encapsulation Disadvantages
- More code to write as author
- Restricts code user's options (if they want to accept the risk)
Advantages really pay off in large projects; not so much in small ones.
Sidenote: Various meanings of "user"
- person sitting at the keyboard in front of your finished program (end-user)
- another programmer that (re)uses your code (much as you are a user of the Java API)
- customer that originally requested your program
Abstraction
- Very closely tied to encapsulation. Two meanings:
- Make code more general/flexible
- Use <generics> or interfaces, like Comparable (we'll see more of this soon)
- Use parameters to make methods more general (as we did here)
- Simplify mental load by hiding implementation
- Any method call is an example of abstraction: single line hides/replaces all the details of what the method does
Reuse
- If code is more general, can be reused for other similar situations
- Needs to be modular (encapsulated/contained) so can just drop in
- So more code to write to encapsulate, but pays off if you can reuse
- Consider: Java API, 3rd party libraries, open-source projects, etc.
Abstract Data Types (ADTs)
ADT: Stack
What is a data structure?
- A variable -> storage of more than one value
- Can pass around whole structure with all its component data
- Examples:
- Array
- Object (esp. its instance variables)
- (Other languages have more: unions, structs, etc.)
- Generally an implementation-level view
Abstract Data Type (ADT)
- Application of OOP to data structures (Example: java.util.ArrayList)
- It's a type (class, with instances/objects)
- It has public methods you call to use it (together: its interface)
- Details are private/hidden
- Abstraction: ADT defined in terms of behavior only
- often detailed but theoretical: preconditions, post conditions, invariants
- but not in terms of implementation (how it's done)
- Abstraction/Encapsulation: user of ADT doesn't need to even know about inner details
- Encapsulation: easy to control/test because inner details are protected from external misuse
- Reuse: Usually written as general as possible
(In casual discussion, "data structure" and "ADT" often overlap. But sort of like difference between "method" and "algorithm".)
ADT: Stack
- Probably simplest ADT (data structure): just a pile
- Behaviors (LIFO = last-in, first-out):
- create() - new stack is empty (no items)
- add(item) - puts that item on top of the stack (other items still in stack, same order, below)
- remove() - removes (and returns?) item from top the stack (revealing any item below)*
- get() - gets (but does not remove) item current on top of the stack*
- size() - returns the number of items currently in the stack
- isEmpty() - false if there is at least one item in the stack (optional)
- * - But what if stack is empty at the time? Part of behavior, so should be defined here (but we'll come back to it)
ADT: Stack, renamed
- ADTs often have specific names for the behaviors they provide:
- create() - new stack is empty (no items)
- push(item) - puts that item on top of the stack (other items still in stack, same order, below) [add]
- pop() - removes and returns item from top the stack (revealing any item below)* [remove]
- peek() - gets/returns (but does not remove) item current on top of the stack* [get]
- size() - returns the number of items currently in the stack
ADT: Stack, as Java code
To implement, need to know type of contents; use String for now...
public class Stack {
public Stack() { ... }
public void push(String item) {...}
public String pop() {...}
public String peek() {...}
public int size() {...}
}
How do we use this?
Stack s = new Stack();
System.out.println(s.size()); //0
s.push("Alice");
s.push("Bob");
s.push("Carol");
System.out.println(s.size()); //3
System.out.println(s.peek()); //Carol
System.out.println(s.pop()); //Carol
s.pop();
System.out.println(s.peek()); //Alice
System.out.println(s.size()); //1
What would this print?
(Again, as an ADT, should just need to know the defined behavior of the stack, not how it works inside.)
ADT: Stack implementation
- But to actually write a Stack class, need implementation details.
- Need to use a data structure, such as... an array.
- How?
- (And still need to decide what to do when calling pop() or peek() on an empty stack...)
ADT: Stack code
public class Stack {
private static final int SIZE = 5;
private String[] items; //holds contents
private int top; //index of current top element
public Stack() {
this.items = new String[SIZE];
this.top = -1;
}
public void push(String item) {
this.top++;
this.items[this.top] = item; //XXX: What if array is full?
}
public String peek() {
return this.items[this.top]; //XXX: What if stack is empty and top == -1?
}
public String pop() {
String item = this.items[this.top]; //XXX: What if stack is empty?
this.top--;
return item;
}
public int size() {
return this.top + 1;
}
}
//XXX: Problems
- In all //XXX problems, will throw ArrayIndexOutOfBoundsException.
- Bad: Means our stack is (undocumented) source of a crash
- Even if user shouldn't have really be trying to pop or peek at an empty stack in the first place
- Bad: Reveals our carefully hidden implementation details (obviously, we're using an array)
- Would generate different exceptions if we changed implementation (as we will next week)
- Alternative: return
null
instead
- Okay... but nulls just cause problems later.
- Better to fail fast and in a well-documented way
- Alternative: Throw an exception (and document that we do in our interface)
Java: Throwing Exceptions
- If a called method throws an exception, you know how to try/catch
- If you've read A04 FAQ (about loading a file), you know how to properly NOT catch an exception (but let a calling method catch it instead)
- Now: How do you create throw your own exception from a method you are writing?
- For now, we'll just reuse (!) exceptions already in java.lang, such as: IllegalArgumentException or IllegalStateException.
- For stack, IllegalStateException makes sense: stack is empty, so don't try getting elements out of it!
- (There's also java.util.EmptyStackException...)
Java: Throwing Exceptions
- Exception is a (special) object, so just construct it like any other object
- Use
throw
(like return
) to throw it out of the current method
public void foo() {
IllegalStateException err = new IllegalStateException();
throw err;
}
- Or, much more common, in one line:
public void foo() {
throw new IllegalStateException();
}
Java: Throwing Exceptions
- Optionally (for unchecked/runtime exceptions; more on this later) can explicitly document in method signature
- Can also add message (printed in runtime crash stack trace)
/**
* Computes factorial of n.
* n must be positive or else throws IllegalArgumentException.
*/
public int factorial(int n) throws IllegalArgumentException {
if (n < 0) {
throw new IllegalArgumentException("Cannot compute factorial of a negative number.");
}
int fact = //...compute factorial here...
return fact;
}
ADT: Stack code, revised
/**
* A Last-In, First-Out (LIFO) abstract data type.
*/
public class Stack {
private static final int INITIAL_SIZE = 5;
private String[] items; //holds contents
private int top; //index of current top element
/** Creates an empty stack. */
public Stack() {
this.items = new String[INITIAL_SIZE];
this.top = -1;
}
/** Places the given item on the top of this stack. */
public void push(String item) {
this.top++;
if (this.top == items.length) {
//array is full, so replace with a copy that is double current size
this.items = java.util.Arrays.copyOf(this.items, this.items.length * 2);
}
this.items[this.top] = item;
}
/**
* Returns a reference to the item currently on the top of this stack.
* If the stack is empty, throws an IllegalStateException instead.
*/
public String peek() throws IllegalStateException {
if (this.top < 0) {
throw new IllegalStateException("Can't peek() into empty stack.");
}
return this.items[this.top];
}
/**
* Removes and returns the item currently on the top of this stack.
* If there is no top item, throws an IllegalStateException instead.
*/
public String pop() throws IllegalStateException {
if (this.top < 0) {
throw new IllegalStateException("Can't pop() from empty stack.");
}
String item = this.items[this.top];
this.items[this.top] = null; //optional, but allows garbage collection
this.top--;
return item;
}
/** Returns the number of items currently stored in this stack. */
public int size() {
return this.top + 1;
}
}
ADT: Stack, conclusion
- ADT: data storage/manipulation defined in terms of external behavior (only)
- Stack behavior is now well-defined (thanks to exceptions: thrown for pop/peek on empty stack)
- Internally/privately, need to provide a specific implementation to produce ADT's behavior
- Stack does not reveal anything about that private implementation (thanks to encapsulation)
Enums
(quick summary)
One (old) way of handling constants
public class PlayingCard {
//constant class variables
public static final int CLUBS = 0;
public static final int DIAMONDS = 1;
public static final int HEARTS = 2;
public static final int SPADES = 3;
//instance variables
private int value;
private int suit;
public PlayingCard(int value, int suit) {
if (suit < 0 || suit > 3) {
//oops... need to do something about it...
}
}
}
- Correct way to build a PlayingCard:
PlayingCard twoOfClubs = new PlayingCard(2, PlayingCard.CLUBS);
- Possible way to build a PlayingCard:
PlayingCard card = new PlayingCard(-6, 19); //?!?
Defining an enum
- (Better) alternative to static final CONSTANTS when you have a related set of constants
- List all possible values (convention: in ALL_CAPS)
- enum is very much like a class (even in its own .java file)
- listed values are each an instance (object) of that class; cannot create any other new instances
public enum Suit {
SPADES, HEARTS, DIAMONDS, CLUBS;
}
public enum Direction {
N, NE, E, SE, S, SW, W, NW, HERE;
}
Example Use
// a constructor in class PlayingCard
public PlayingCard(int value, Suit suit) {
//...
}
PlayingCard card = new PlayingCard(4, Suit.CLUBS);
- No error checking needed for suit: can ONLY be one of the 4 suit values!
Other advantages
- Just as convenient as using int constants:
- can compare using == (ie:
if (dir == Direction.N)
)
- can use as switch case values
- but better:
- when printed, appear as NAME, not as some numerical value
- can have associated methods (see A04's Direction class; will add FAQ on this)
- can even have internal state/instance variables (beyond our uses here)
Important methods
Every enum can has the following:
- a static
values()
method: returns an array of all values
Direction[] dirs = Direction.values();
for (Direction dir : dirs) {
System.out.print(dir + " "); //prints: N NE E SE S SW W NW HERE
}
System.out.println(dirs[1]); //print NE
- each instance has an
ordinal()
method: returns 0-based position in ordering
Direction dir = Direction.SE;
System.out.println(dir.ordinal()); //prints: 3
Summary
- History of CS -> abstraction, encapsulation, reuse -> OOP
- ADTs = OOP version of data structures
- Stack as an ADT
- Enums
Next time...
- We'll implement stack with the same public interface, but completely different internals (nodes!)
- Recording E01 (codingbat) tonight or so
- Definitely have first couple methods of A04 done (reading in file and printing internal repr.) by Monday
- Quiz 05 to be posted today