CS61B Week 1~3 | Java自学笔记

610 阅读10分钟

课程内容来自于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

compilation.PNG

  • 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()

IntList.PNG

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

SLList.PNG

/** 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.