课程内容来自于UC Berkley CS 61B Data Structure, Spring 2018
Defining and Using Classes
Compilation
- The standard tools for executing Java program use a two step process
-
Why make a class file at all
- .class file has been type checked. Distributed code is safer
- .class file are ‘simpler’ for machine to execute. Distributed code is faster
- Minor benefit: Protects your intellectual property. No need to give out source.
Defining and Instantiating Classes
-
Every method (a.k.a function) is associated with some class
-
To run a class, we must define a main method
- Not all classes have a main method
//Can't be run directly, since there is no main method
public class Dog {
public static void makeNoise() {
System.out.println("Bark!);
}
}
//Calls a method from another class
public class DogLauncher {
public static void main(String[] args){
Dog.makeNoise();
}
}
Arrays of Objects
To create an array of objects:
- First use the new keyword to create the array
- Then use new again for each object that you want to put in the array
Static vs Instance Methods
Key differences between static and non-static (a.k.a instance) methods:
- Static methods are invoked using the class name, e.g. Dog.makeNoise();
- Instance methods are invoked using an instance name, e.g. maya.makeNoise();
- Static methods can’t access “my” instance variables, because there is no “me”
Why Static Methods?
Some classes are never instatiated. For example, Math.
- x = Math.round(5.6);
A class may have a mix of static and non-static members.
- A variable or method defined in a class is also called a member of that class
- Static members are accessed using class name, e.g. Dog.binomen
- Non-static members cannot be invoked using class name:
Dog.makeNoise() - Static mehods must access instance variables via a specific instance, e.g. d1
Using Libraries
There are tons of Java libraries out there.
-
In 61B, we will provide all needed libraries. These include (but are not limited to):
- The built-in Java libraries (e.g. Math, String, Integer, List, Map)
- The Princeton standard library (e.g. StdDraw, StdAudio, In)
As a programmer, you’ll want to leverage existing libraries whenever possible
- Saves you the trouble of writing code
- Existing widely used libraries are (probably) will probably be less buggy
- … but you have to spend some time getting acquainted with the library
We’ll be using a great library courtesy of my old colleagues at Princeton, mostly Kevin Wayne: introcs.cs.princeton.edu/java/stdlib…
Makes various things much easier:
- Getting user input
- Reading from files
- Making sounds
- Drawing to the screen
- Getting random numbers
References, Recursion, and Lists
Primitive Types
When you declare a variable of a certain type in Java:
-
Your computer sets aside exactly enough bits to hold a thing of that type
- Example: Declaring an int sets aside a “box” of 32 bits
- Example: Declaring a double sets aside a “box” of 64 bits
-
Java creates an internal table that maps each variable name to a location
-
Java does NOT write anything into the reserved boxes
- For safety, Java will not let access a variable that is unintialized
The Golden Rule of Equals (GRoE)
Given variables y and x:
- y = x copies all the bits from x into y
Refernce Types
There are 8 primitive types in Java:
- byte, short , int, long, float, double, boolean, char
Everything else, including arrays, is a reference type.
Class Instantiations
When we instantiate an Object (e.g. Dog, Walrus, Planet):
- Java first allocates a box of bits for each instance variable of the class and fills them with a default value (e.g. 0, null)
- The constructor then usually fills every such box with some other value
Can think of new as returning the address of the newly created object
- Addresses in Java are 64 bits
- Example (rough picture): If object is created in memory location 2384723423, then new returns 2384723423
Reference Type Variable Declarations
When we declare a variable of any reference type (Walrus, Dog, Planet):
-
Java allocates exactly a box of size 64 bits, no matter what type of object
-
These bits can be either set to:
- Null (all zeros)
- The 64 bit “address” of a specific instance of that class (returned by new)
Reference Types Obey the Golden Rule of Equals
Just as with primitive types, the equals sign copies the bits
- In terms of our visual metaphor, we “copy” the arrow by making the arrow in the b box point at the same instance as a
Parameter Passing
Given variables b and a:
- b = a copies all the bits from a into b
Passing parameters obeys the same rule: Simply cope the bits to the new scope
The Golden Rule: Summery
There are 9 types of variables in Java:
- 8 primitives types (byte, short, int, long, float, double, boolean, char)
- The 9th type is references to Objects (an arrow). References may be null
In box-and-pointer notaion, each variable is drawn as a labeled box and values are shown in the box
- Addresses are represented by arrows to object instances
The golden rule:
- b = a copies the bits from a into b
- Passing parameters copies the bits
Instantiation of Arrays
Arrays are also Objects. As we’ve seen, objects are (ususlly) instatiated using the new keyword
- Planet p = new Planet(0, 0, 0, 0, 0, “blah.png”);
- int[] x = new int[]{0, 1, 2, 95, 4}
int[] a;
- Declaration creates a 64 bit box intended only for storing a reference to an int array. No object is instantiated
new int[]{0, 1, 2, 95, 4};
- Instantiates a new Object, in this case an int array
- Object is anonymous
int[] a = new int[]{0, 1, 2, 95, 4};
- Creates a 64 bit box for storing an int array address. (declaration)
- Creates a new Object, in this case an int array. (instantiation)
- Puts the address of this new Object into the 64 bit box named a. (assignment)
IntList and Linked Data Structures
IntList
Let’s define an IntList as an object containing two member variables:
- int first;
- IntList rest;
And define two versions of the same method:
- size()
- iterativeSize()
public class IntList {
public int first;
public IntList rest;
public IntList(int f, IntList r) {
first = f;
rest = r;
}
/** Return the size of the list using... recursion! */
public int size() {
if (rest == null) {
return 1;
}
return 1 + this.rest.size();
}
/** Return the size of the list using no recursion! */
public int iterativeSize() {
IntList p = this;
int totalSize = 0;
while (p != null) {
totalSize += 1;
p = p.rest;
}
return totalSize;
}
/** Returns the ith item of this IntList. */
public int get(int i) {
if (i == 0){
return this.first;
}
return this.rest.get(i - 1);
}
public static void main(String[] args) {
IntList L = new IntList(15, null);
L = new IntList(10, L);
L = new IntList(5, L);
System.out.println(L.size());
}
}
SLLists, Nested Classes, Sentinel Nodes
Last Time in 61B: Recursive Implementation of a List
While functional, “naked” linked lists like the one above are hard to use
- Users of this class are probably going to need to know references very well, and be able to think recursively. Let’s make our users’ lives easier
public class IntNode {
public int item;
public IntNode next;
public IntNode(int i, IntNode n) {
item = i;
next = n;
}
}
/** An SLList is a list of integers, which hides the terrible truth
* of the nakedness within. */
public class SLList {
public IntNode first;
public SLList (int x) {
first = new IntNode(x, null);
}
/** Adds x to the front of the list. */
public void addaFirst(int x) {
first = new IntNode(x, first);
}
/** Returns the first item in the list. */
public int getFirst(){
return first.item;
}
public static void main(String[] args) {
/* Creates a list of one integer, namely 10 */
SLList L = new SLList(10);
L.addaFirst(10);
L.addaFirst(5);
System.out.println(L.getFirst());
}
}
While functional, “naked” linked lists like the IntList class are hard to use
- Users of IntList are need to know Java references well, and be able to think recursively
- SLList is much simpler to use. Simply use the provided methods
Why Restrict Access?
Hide implementation details from users of your class
- Less for user of class to understand
- Safe for you to change private methods (implementation)
Why Nested Classes?
Nested Classes are useful when a class doesn’t stand on its own and is obviously subordinate to another class
- Make the nested class private if other classes should never use the nested class
In my opnion, probably makes sense to make IntNode a nested private class.
- Hard to imagine other classes having a need to manipulate IntNodes
Static Nested Classes
If the nested class never uses any instance variables or methods of the outer class, declare it static
- Static classes cannot access outer class’s instance variables or methods
- Results in a minor savings of memory
/** An SLList is a list of integers, which hides the terrible truth
* of the nakedness within. */
public class SLList {
public IntNode first;
private static class IntNode {
public int item;
public IntNode next;
public IntNode(int i, IntNode n) {
item = i;
next = n;
}
}
public SLList (int x) {
first = new IntNode(x, null);
}
/** Adds x to the front of the list. */
public void addaFirst(int x) {
first = new IntNode(x, first);
}
/** Returns the first item in the list. */
public int getFirst(){
return first.item;
}
/** Adds an item to the end of the list. */
public void addLast(int x) {
IntNode p = first;
/* Move p until it reaches the end of the list. */
while (p.next != null) {
p = p.next;
}
p.next = new IntNode(x, null);
}
public static void main(String[] args) {
/* Creates a list of one integer, namely 10 */
SLList L = new SLList(10);
L.addaFirst(10);
L.addaFirst(5);
L.addLast(20);
System.out.println(L.getFirst());
}
}
Tips For Being a Good Programmer: Keep Code Simple
As a human programmer, you only have so much working memory
-
You want to restrict the amount of complexity in your life
-
Simple code is (usually) good code
- Special cases are not ‘simple’
Invariants
An invariant is a condition that is guaranteed to be true during code execution (assuming there are no bugs in your code).
An SLList with a sentinel node has at least the following invariants:
- The sentinel reference always points to a sentinel node.
- The first node (if it exists), is always at sentinel.next.
- The size variable is always the total number of items that have been added.
Invariants make it easier to reason about code:
- Can assume they are true to simplify code (e.g. addLast doesn’t need to worry about nulls).
- Must ensure that methods preserve invariants.
Generic SLLists
/** An SLList is a list of integers, which hides the terrible truth
* of the nakedness within. */
public class SLList<LochNess> {
private class StuffNode {
public LochNess item;
public StuffNode next;
public StuffNode(LochNess i, StuffNode n) {
item = i;
next = n;
}
}
/** The first item (if it exists) is at sentinal.next. */
private StuffNode first;
private int size;
public SLList (LochNess x) {
first = new StuffNode(x, null);
size = 1;
}
/** Adds x to the front of the list. */
public void addFirst(LochNess x) {
first = new StuffNode(x, first.next);
size++;
}
/** Returns the first item in the list. */
public LochNess getFirst() {
return first.item;
}
/** Adds an item to the end of the list. */
public void addLast(LochNess x) {
size++;
StuffNode p = first;
/* Move p until it reaches the end of the list. */
while (p.next != null) {
p = p.next;
}
p.next = new StuffNode(x, null);
}
public int size() {
return size;
}
}
public class SLListLauncher {
public static void main(String[] args) {
SLList<String> s1 = new SLList<>("bone");
s1.addFirst("thugs");
}
}
Arrays
Arrays consisit of:
-
A fixed integer length (cannot change!)
-
A sequence of N memory boxes where N=length, such that:
- All of the boxes hold the same type of value (and have same # of bits)
- The boxes are nunbered 0 through length-1
Like instances of classes:
- You get one reference when its created
- If you reassign all variables containing that reference, you can never get the array back
Unlike classes, arrays do not hava methods
Like classes, arrays are (almost always) instantiated with new.
Three valid notations:
- y = new int[3];
- x = new int[]{1, 2, 3, 4, 5};
- int[] w = {9, 10, 11, 12, 13}; ← can omit the new if you are also declaring a variable
All three notations create an array, which comprises:
- A length field
- A sequence of N boxes, where N = length
Arraycopy
Two ways to copy arrays:
-
Item by item using a loop
-
Using arraycopy. Takes 5 parameters:
- System.arraycopy(b, 0, x, 3, 2)
- Source array
- Start position in source
- Target array
- Start position in target
- Number to copy
arraycopy is (likely to be) faster, particularly for large arrays. More compact code
2D Arrays
//Array of int array references
int[][] pascalsTriangle;
//Create four boxes, each can store an int array reference
pascalsTriangle = new int[4][];
int[] rowZero = pascalsTriangle[0];
pascalsTriangle[0] = new int[]{1};
pascalsTriangle[1] = new int[]{1, 1};
/** Create a new array with three boxes, storing integers 1, 2, 1, respectively.
Store a reference to this array in pascalsTriangle box #2. */
pascalsTriangle[2] = new int[]{1, 2, 1};
pascalsTriangle[3] = new int[]{1, 3, 3, 1};
int[] rowTwo = pascalsTriangle[2];
rowTwo[1] = -5;
int[][] matrix;
//Creates 1 total array
matrix = new int[4][];
//Creates 5 total arrays
matrix = new int[4][4];
int[][] pascalAgain = new int[][]{{1}, {1, 1},{1, 2, 1}, {1, 3, 3, 1}};
Arrays vs. Classes
Arrays and Classes can both be used to organize a bunch of memory boxes
- Array boxes are accessed using [] notation
- Class boxes are accessed using dot notation
- Array boxes must all be the same type
- Class boxes may be of different types
- Both have a fixed number of boxes
ALists, Resizing, vs. SLists
Random Access in Arrays
Retrieval from any position of an array is very fast
Our Goal: AList.java
Want to figure out how to build an array version of a list
- In lecture we’ll only do back operations. Project 1A is the front operations
Naive AList Code
AList Invariants:
- The position of the next item to be inserted is always size
- size is always the number of items in the AList
- The last item in the list is always in position size - 1
public class AList {
int[] items;
int size;
/** Creates an empty list. */
public AList() {
items = new int[100];
size = 0;
}
/** Inserts X into the back of the list. */
public void addLast(int x) {
items[size] = x;
size = size + 1;
}
/** Returns the item from the back of the list. */
public int getLast() {
return items[size - 1];
}
/** Gets the ith item in the list (0 is the front). */
public int get(int i) {
return items[i];
}
/** Returns the number of items in the list. */
public int size() {
return size;
}
/** Deletes item from back of the list and
* returns deleted item. */
public int removeLast() {
int x = getLast();
size = size - 1;
return x;
}
}
Array Resizing
When the array gets too full, just make a new array
- int[] a = new int[size+1];
- System.arraycopy(…)
- a[size] = 11;
- items = a; size+=1;
Resizing Slowness
Inserting 100,000 items requires roughly 5,000,000,000 new containers.
- Computers operate at the speed of GHz (due billions of things per second)
- No huge surprise that 100,000 items took seconds
Suprising Fact
Geomatric resizing is much faster: Just how much better will have to wait
public void addLast(int x) {
if (size == items.length) {
resize(size * RFACTOR);
}
items[size] = x;
size += 1;
}
Memory Efficiency
An AList should not only be efficient in time, but also efficient in space.
- Define the “usage ratio” R=size / items.length;
- Typical solution: Half array size when R < 0.25
Later we will consider tradeoffs between time and space efficiency for a variety of algorithms and structures
Generic ALists
When creating an array of references to Glorps:
- (Glorp []) new Object[cap]
- Causes a compiler warning, which you should ignore
Why not just new Glorp[cap]
- Will cause a “generic array creation” error
- Will discuss in a few weeks
public class AList<Item> {
Item[] items;
int size;
/** Creates an empty list. */
public AList() {
items = (Item[]) new Object[100];
size = 0;
}
/** Resizes the underlying array to the target capacity */
private void resize(int capacity) {
Item[] a = (Item[]) new Object[capacity];
System.arraycopy(items, 0, a, 0, size);
items = a;
}
/** Inserts X into the back of the list. */
public void addLast(Item x) {
if (size == items.length) {
resize(size + 1);
}
items[size] = x;
size = size + 1;
}
/** Returns the item from the back of the list. */
public Item getLast() {
return items[size - 1];
}
/** Gets the ith item in the list (0 is the front). */
public Item get(int i) {
return items[i];
}
/** Returns the number of items in the list. */
public int size() {
return size;
}
/** Deletes item from back of the list and
* returns deleted item. */
public Item removeLast() {
Item x = getLast();
size = size - 1;
return x;
}
}
Nulling Out Deleted Items
Unlike integer based ALists, we actually want to null out deleted items
- Java only destroys unwanted objects when the last reference has been lost
- Keeping references to unneeded objects is sometimes called loitering
- Save memory. Don’t loiter
Simple JUnit
New Syntax #1: org.junit.Assert.assertEquals(expected, actual);
- Tests that expected equals actual
- If not, program terminates with verbose message
JUnit does much more:
- Other methods like assertEquals include assertFalse, assertNotNull, etc.
- Other more complex behavior to support more sophisticated testing.
Better JUnit
New Syntax #2
-
Annotate each test with @org.junit.Test
-
Change all test methods to non- static
-
Use a JUnit runner to run all tests and tabulate results
- IntelliJ provides a default runner/renderer. OK to delete main
- If you want to use the command line instead, see the jh61b runner in the lab 3 supplement. Not preferred
- Rendered output is easier to read, no need to manually invoke tests
Even Better JUnit
New Syntax #3: To avoid this we’ll start every test file with:
import org.junit.Test;
import static org.junit.Assert.* ;
This will magically eliminate the need to type ‘org.junit’ or ‘org.junit.Assert’
Correctness Tool #1: Autograder
Correctness Tool #2: Unit Tests
Idea:Write tests for every “unit”
- JUnit makes this easy!
Why?
- Build confidence in basic modules
- Decrease debugging time
- Clarify the task
Why not?
- Building tests takes time
- May provide false confidence
- Hard to test units that rely on others
Test-Driven Development (TDD)
Steps to developing according to TDD:
-
Identify a new feature
-
Write a unit test for that feature
-
Run the test. It should fail. (RED)
-
Write code that passes test. (GREEN)
- Implementation is certifiably good!
-
Optional: Refactor code to make it faster, cleaner, etc.
Inheritance, Implements
Simple Hyponymic Relationships in Java
SLLists and ALists are both clearly some kind of “list”
- List is a hypernym of SLList and AList
Expressing this in Java is a two-step process:
- Step1: Define a reference type for our hypernym (List61B.java)
- Step2: Specify that SLLists and ALists are hyponyms of that type
Step1: Defining a List61B
We’ll use the new keyword interface instead of class to define a List61B
- Idea: Interface is a specification of what a List is able to do, not how to do it
Step2: Implementing the List61B Interface
We’ll now:
- Use the new implements keyword to tell the Java compiler that SLList and AList are hyponyms of List61B
Method Overriding
If a “subclass” has a method with the exact same signature as in the “superclass”, we say the subclass overrides the method
Optional Step 2B: Adding the @Override Annotation
In 61B, we’ll always make every overriding method with the @Override annotation
- Example: Mark AList.java’s overriding method with @Override
- The only effect of this tag is that the code won’t compile if it is not actually an overriding method
Why use @Override?
-
Main reason: Protects against typos
- If you say @Override, but it the method isn’t actually overiding anything, you’ll get a compile error
-
Reminds programmer that method definition came from somewhere higher up in the inheritance hierarchy
Interface Inheritance
Specifying the capabilities of a subclass using the implements keyword is known as interface inheritance
-
Interface: The list of all method signatures
-
Inheritance: The subclass “inherits” the interface from a superclass
-
Specifies what the subclass can do, but not how
-
Subclasses must override all of these methods!
- Will fail to compile otherwise
Implementation Inheritance
Interface inheritance:
- Subclass inherits signatures, but NOT implementation
For better or worse, Java also allows implementation inheritance
- Subclasses can inherit signatures AND implementation
Use the default keyword to specify a method that subclasses should inherit from an interface
- Example: Let’s add a default print() method to List61B.java
Static Type vs. Dynamic Type
Every variable in Java has a “compile-time type”, a.k.a. “static type”
- This is the type specified at declaration. Never changes!
Variables also have a “run-time type”, a.k.a. “dynamic type”
- This is the type specified at instantiation (e.g. when using new)
- Equal to the type of the object being pointed at
Dynamic Method Selection For Overridden Methods
Suppose we call a method of an object using a variable with:
- compile-time type X
- run-time type Y
Then if Y overrides the method, Y’s method is used instead
- This is known as “dynamic method selection”
Dynamic Method Selection Puzzle
Suppose we have classes defined below. Try to predict the results
public interface Animal {
default void greet(Animal a) {
print("hello animal"); }
default void sniff(Animal a) {
print("sniff animal"); }
default void flatter(Animal a) {
print("u r cool animal"); }
}
public class Dog implements Animal {
void sniff(Animal a) {
print("dog sniff animal"); }
void flatter(Dog a) {
print("u r cool dog"); }
}
Animal a = new Dog();
Dog d = new Dog();
a.greet(d); // "hello animal"
a.sniff(d); // "dog sniff animal"
d.flatter(d); // "u r cool dog"
a.flatter(d); // “u r cool animal”
flatter is overloaded, not overriden!
The Method Selection Algorithm
Consider the function call foo.bar(x1), where foo has static type TPrime, and x1 has static type T1
At compile time, the compiler verifies that TPrime has a method that can handle T1. It then records the signature of this method.
- Note: If there are multiple methods that can handle T1, the compiler records the “most specific” one. For example, if T1=Dog, and TPrime has bar(Dog) and bar(Animal), it will record bar(Dog)
At runtime, if foo’s dynamic type overrides the recorded signature, use the overriden method. Othervise, use TPrime’s version of the method.
Interface vs. Implementation Inheritance
Interface Inheritance (a.k.a. what):
- Allows you to generalize code in a powerful, simple way
Implementation Inheritance (a.k.a. how):
-
Allows code-reuse: Subclasses can rely on superclass or interfaces
- Example: print() implemented in List61B.java
- Gives another dimension of control to subclass designers: Can decide whether or not to override default implementations
Important: In both cases, we specify “is-a” relationships, not “has-a”.
- Good: Dog implements Animal, SLList implements List61B
- Bad: Cat implements Claw, Set implements SLList
The Dangers of Implementation Inheritance
Particular Dangers of Implementation Inheritance
-
Makes it harder to keep track of where something was actually implemented (though a good IDE makes this better)
-
Rules for resolving conflicts can be arcane. Won’t cover in 61B
- Example: What if two interfaces both give conflicting default methods?
-
Encourages overly complex code (especially with novices)
- Common mistake: Has-a vs. Is-a!
-
Breaks encapsulation!
- What is encapsulation? See next week.