课程内容来自于UC Berkley CS 61B Data Structure, Spring 2018
Extends, Casting, Higher Order Functions
The Extends Keyword
Because of extends, RotatingSLList inherits all members of SLList:
- All instance and static variables
- All methods
- All nested classes
Constructor Behavior Is Slightly Weird
Constructors are not inherited. However, the rules of Java say that all constructors must start with a call to one of the super class’s constructors.
The Object Class
As it happens, every type in Java is a descendant of the Object class.
- VengefulSLList extends SLList.
- SLList extends Object (implicitly).
- Interface don’t extends Object.
Complexity: The Enemy
When building large programs, our enemy is complexity.
Some tools for managing complexity:
-
Hierarchical abstraction.
- Create layers of abstraction, with clear abstraction barriers!
-
“Design for change” (D. Parnas)
- Organize program around objects.
- Let objects decide how things are done.
- Hide information others don’t need.
Managing complexity supremely important for large projects (e.g. project 2).
Implementation Inheritance Breaks Encapsulation
//super class
public void bark() {
barkMany(1);
}
public void barkMany(int N) {
for (int i = 0; i < N; i++) {
System.out.println("bark");
}
}
@Override
public void barkMany(int N) {
System.out.println("As a dog, I say: ");
for (int i = 0; i < N; i++) {
bark();
}
}
Dynamic Method Selection and Type Checking Puzzle
public static void main(String[] args) {
VengefulSLList<Integer> vsl = new VengefulSLList<Integer>(9);
SLList<Integer> sl = vsl;
sl.addLast(50);//VengefulSLList doesn't override, uses SLList's
sl.removeLast();//Uses VengefulSLList's
sl.printLostItems();//Compilation error
VengefulSLList<Integer> vsl2 = sl;//Compilation error
}
If overriden, decide which method to call based on run-time (dynamic) type of variable (Dynamic Method Selection) .
Compiler allows method calls based on compile-time (static) type of variable.
Compiler also allows assignments based on compile-time types.
- Even though sl’s runtime-type is VengefulSLList, cannot assign to vsl2.
- Compiler plays it as safe as possible with type checking
Compile-Time Types and Expressions
Expressions have compile-time types:
- Method calls have compile-time type equal to their declared type.
public static Dog maxDog(Dog d1, Dog d2) { ... }
- Any call to maxDog will have compile-time type Dog!
Casting
Java has a special syntax for forcing the compile-time type of any expression.
- Put desired type in parenthesis before the expression
- Think of it as a way to trick the compiler
Casting is a powerful but dangerous tool.
- Tells Java to treat an expression as having a different compile-time type.
- Effectively tells the compiler to ignore its type checking duties.
Higher Order Functions
Higher Order Function: A function that treats another function as data.
- e.g. takes a function as imput
Old School (Java 7 and earlier)
- Fundamental issue: Memory boxes (variables) cannot contain pointers to functions.
- Can use an interface instead
public interface IntUnaryFunction {
int apply(int x);
}
public class TenX implements IntUnaryFunction {
public int apply(int x) {
return 10 * x;
}
}
public class HoFDemo {
public static int do_twice(IntUnaryFunction f, int x) {
return f.apply(f.apply(x));
}
public static void main(String[] args) {
System.out.println(do_twice(new TenX(), 2));
}
}
Implementation Inheritance Cheatsheet
VengefulSLList extends SLList means a VenglefulSLList is-an SLList. Inherits all members!
- Variables, methods, nested classes.
- Not constructors.
- Subclass constructor must invoke superclass constructor first.
- Use super to invoke overridden superclass methods and constructors.
Invocation of overridden methods follows two simple rules:
- Compiler plays it safe and only lets us do things allowed by static type.
- Compiler chooses 2
- For overridden methods the actual method invoked is based on dynamic type of invoking expression, e.g. Dog.maxDog(d1, d2).bark();
- Can use casting to overrule compiler type checking.
Subtype Polymorphism vs. HoFs
A Typing Puzzle
Suppose we have two classes:
-
Dog: Implements bark() method.
-
ShowDog: Extends Dog, overrides bark method.
Summarizing is-a relationships, we have:
-
Every ShowDog is-a Dog
-
Every Dog is-an Object.
- All types in Java are a subtype of Object.
For each assignment, decide if it causes a compile error.
For each call to bark, decide whether: 1. Dog.bark() is called, 2. ShowDog.bark() is called, or 3. A syntax error results.
Object o2 = new ShowDog("Mortimer", "Corgi", 25, 512.2);//no problem
ShowDog sdx = ((ShowDog)o2);//no problem
sdx.bark();//ShowDog.bark() is called
Dog dx = ((Dog) o2);//no problem, (Dog)o2's static type is Dog
dx.bark();//ShowDog.bark() is called, because o2's dynamic type is still ShowDog
((Dog) o2).bark();//ShowDog.bark() is called
Object o3 = (Dog) o2;//no problem
o3.bark();//compile error
The rules:
-
Compiler allows memory box to hold any subtype.
-
Compiler allows calls based on static type.
-
Overridden non-static methods are selected at run time based on dynamic type.
- Everything else is based on static type, including overloaded methods.
Static Methods, Variables, and Inheritance
You may find questions on old 61B exams, worksheets, etc. that consider:
-
What if a subclass has variables with the same name as a superclass?
-
What if subclass has a static method with the same signature as a superclass method?
- For static methods, we do not use the term overriding for this.
These two practices above are called “hiding”.
- It is bad style.
- There is no good reason to ever do this.
- The rules for resolving the conflict are a bit confusing to learn.
- I decided last year to stop teaching it in 61B.
But if you want to learn it, see docs.oracle.com/javase/tuto…
Subtype Polymorphism
The biggest idea of the last couple of lectures: Subtype Polymorphism
- Polymorphism: “providing a single interface to entities of different types”
Consider a variable deque of static type Deque:
- When you call deque.addFirst(), the actual behavior is based on the dynamic type.
- Java automatically selects the right behavior using what is sometimes called “dynamic method selection”.
The Fundamental Problem
Objects cannot be compared to other objects with >
- How could we fix our Maximizer class using inheritance / HoFs?
/**
* Maximizer.java
*/
public static Object max(Object[] items) {
int maxDex = 0;
for (int i = 0; i < items.length; i += 1) {
if (items[i] > items[maxDex]) {
maxDex = i;
}
}
return items[maxDex];
}
/**
* DogLauncher.java
*/
public static void main(String[] args) {
Dog[] dogs = {new Dog("Elyse", 3), new Dog("Sture", 9),
new Dog("Benjamin", 15)};
Dog maxDog = (Dog) max(dogs);
maxDog.bark();
}
Solution
Create an interface that guarantees a comparison method.
- Have Dog implement this interface.
- Write Maximizer class in terms of this interface.
The OurComparable Interface
public interface OurComparable {
int compareTo(Object obj);
}
Specification, returns:
- Negative number if this is less than obj.
- 0 if this is equal to object.
Positive number if this is greater than obj.
General Maximization Function Through Inheritance
Benefits of this approach:
- No need for array maximization code in every custom type (i.e. no Dog.maxDog(Dog[]) function required).
- Code that operates on multiple types (mostly) gracefully.
The Issues With OurComparable
Two issues:
-
Awkward casting to/from Objects.
-
We made it up.
- No existing classes implement OurComparable (e.g. String, etc).
- No existing classes use OurComparable (e.g. no built-in max function that uses OurComparable)
The industrial strength approach: Use the built-in Comparable interface.
- Already defined and used by tons of libraries. Uses generics.
Comparable Advantages
- Lots of built in classes implement Comparable (e.g. String).
- Lots of libraries use the Comparable interface (e.g. Arrays.sort / Collection.max)
- Avoids need for cast.
Natural Order
The term “Natural Order” is sometimes used to refer to the ordering implied by a Comparable’s compareTo method.
Additional Orders in Java
The standard Java approach: Create sizeComparator and nameComparator classes that implement the Comparator interface.
- Requires methods that also take Comparator arguments (see project 1B).
Example: NameComparator
public class Dog implements Comparable<Dog> {
private String name;
private int size;
public static class NameComparator implements Comparator<Dog> {
public int compare(Dog d1, Dog d2) {
return d1.name.compareTo(d2.name);
}
}
...
}
Comparator<Dog> cd = new Dog.NameComparator();
if (cd.compare(d1, d3) > 0) {
d1.bark();
} else {
d3.bark();
}
Comparable and Comparator Summary
Interfaces provide us with the ability to make callbacks:
-
Sometimes a function needs the help of another function that might not have been written yet.
- Example: max needs compareTo
- The helping function is sometimes called a “callback”.
-
Some languages handle this using explicit function passing.
-
In Java, we do this by wrapping up the needed function in an interface (e.g. Arrays.sort needs compare which lives inside the comparator interface)
-
Arrays.sort “calls back” whenever it needs a comparison.
- Similar to giving your number to someone if they need information.
- See Project 1B to explore how to write code that uses comparators.
Libraries, Abstract Classes, Packages
Collections
Among the most important interfaces in the java.util library are those that extend the Collection interface (btw interfaces can extend other interfaces).
-
Lists of things.
-
Sets of things.
-
Mappings between items, e.g. jhug’s grade is 88.4.
- Maps also known as associative arrays, associative lists (in Lisp), symbol tables, dictionaries (in Python).
Java Libraries
The built-in java.util package provides a number of useful:
- Interfaces: ADTs (lists, sets, maps, priority queues, etc.) and other stuff.
- Implementations: Concrete classes you can use.
Inheritance Summary (So Far)
In the last three lectures we’ve seen how implements and extends can be used to enable interface inheritance and implementation inheritance.
- Interface inheritance: What (the class can do).
- Implementation inheritance: How (the class does it).
A Closer Look at Interfaces
Interfaces may combine a mix of abstract and default methods.
- Abstract methods are what. And must be overridden by subclass.
- Default methods are how.
- Every method in an interface must be public, so public is redundant.
- Unless you use the keyword default, a method will be abstract.
- Can provide variables, but they are public static final.
- A class can implement multiple interfaces.
Interface Summary
-
Cannot be instantiated.
-
Can provide either abstract or concrete methods.
- Use no keyword for abstract methods.
- Use default keyword for concrete methods.
-
Can provide only public static final variables.
-
Can provide only public methods.
Introducing: Abstract Classes
Abstract classes are an intermediate level between interfaces and classes.
-
Cannot be instantiated.
-
Can provide either abstract or concrete methods.
- Use abstract keyword for abstract methods.
- Use no keyword for concrete methods.
-
Can provide variables (any kind).
-
Can provide methods (any kind).
Summary: Abstract Classes vs. Interfaces
Interfaces:
- Primarily for interface inheritance. Limited implementation inheritance.
- Classes can implement multiple interfaces.
Abstract Classes:
- Can do anything an interface can do, and more.
- Subclasses only extends one abstract classes.
In my opinion, you should try to use interfaces whenever possible.
- Why? More powerful programming language constructs introduce complexity.
Where Abstract Classes Are Used in Java Standard Libraries
A more accurate hierarchy for lists is shown below.
- AbstractList provides default implementations for methods.
- Why not just put them in List itself? No default methods in Java interfaces until 2014, and the AbstractList was public so can’t just throw it away.
Canonicalization
What we’d really like is the ability to provide a canonical name for everything.
- Canonical representation: A unique representation for a thing.
- Not-canonical: License plate number (can be reused, can change).
- Canonical: The VIN number JYA3AWC04VA071286 (refers to a specific motorcycle).
In Java, we (attempt to) provide canonicity through by giving every a class a “package name”.
- A package is a namespace that organizes classes and interfaces.
- Today is just a brief look. We’ll talk more about packages in two weeks.
- In HW1, after the midterm, we’ll make our own.
Packages
To address the fact that classes might share names:
- A package is a namespace that organizes classes and interfaces.
- Naming convention: Package name starts with website address (backwards).
Coming up Next: The Syntax Lectures
In the next three lectures, we’ll build an Array based implementation of a Map, and along the way, learn some new syntax.
- Syntax1: Autoboxing, promotion, immutability, generics
- Syntax2: Exceptions, Iterables/Iterators
- Syntax3: Access control, equals, other loose ends
- Syntax4 (optional): Wildcards, type upper bounds, covariance (not in the scope of the class).
Syntax 1: Generics, Autoboxing
Generics
For the most part, using generics is pretty straightforward.
- Generic classes require us to provide one or more actual type arguments.
We cannot use primitive types as actual type arguments.
- Code below causes a compile time error.
Autoboxing and Unboxing
Wrapper types and primitives can be used almost interchangeably.
- If Java code expects a wrapper type and gets a primitive, it is autoboxed.
- If the code expects a primitive and gets a wrapper, it is unboxed.
Some notes:
- Arrays are never autoboxed/unboxed, e.g. an Integer[] cannot be used in place of an int[] (or vice versa).
- Autoboxing / unboxing incurs a measurable performance impact!
- Wrapper types use MUCH more memory than primitive types.
Another Type of Conversion: Primitive Widening
A similar thing happens when moving from a primitive type with a narrower range to a wider range.
- In this case, we say the value is “widened”.
- Code below is fine since double is wider than int.
To move from a wider type to a narrower type, must use casting.
Immutable Data Types
An immutable data type is one for which an instance cannot change in any observable way after instantiation.
Examples:
- Mutable: ArrayDeque, Planet.
- Immutable: Integer, String, Date.
The final keyword will help the compiler ensure immutability.
- final variable means you will assign a value once (either in constructor of class or in initializer).
- Not necessary to have final to be immutable (e.g. Dog with private variables).
Immutability
Advantage: Less to think about: Avoids bugs and makes debugging easier.
- Analogy: Immutable classes have some buttons you can press / windows you can look inside. Results are ALWAYS the same, no matter what.
Disadvantage: Must create a new object anytime anything changes.
Warning: Declaring a reference as Final does not make object immutable.
- Example: public final ArrayDeque d = new ArrayDeque();
The d variable can never change, but the referenced deque can!
Generic Summary
We’ve now seen four new features of Java that make Generics more powerful:
- Autoboxing and auto-unboxing of primitive wrapper types.
- Promotion between primitive types.
- Specification of generic types for methods (before return type).
- Type upper bounds (e.g. K extends Comparable)
- In syntax4, you can also see another feature called “wildcards”.
A true understand of Java generics takes a long time and lots of practice.
- You won’t know all the details by the end of 61B.
- I promise not to ask questions about bounded wildcards, type erasure, or covariance (see bonus lecture entitled syntax4).
Syntax 2: Exceptions and Iteration
Exceptions
Basic idea:
- When something goes really wrong, break the normal flow of control.
- So far, we’ve only seen implicit exceptions.
Explicit Exceptions
We can also throw our own exceptions using the throw keyword.
- Can provide more informative message to a user.
- Can provide more information to some sort of error handling code.
Handling Errors
The Java approach to handling exceptional events is to throw an exception.
- Disrupts normal flow of the program.
- So far in 61B, exceptions just cause the program to crash, printing out a helpful (?) message for the user.
What has been Thrown, can be Caught
So far, thrown exceptions cause code to crash.
- Can ‘catch’ exceptions instead, prventing program from crashing. Use keywords try and catch to break normal flow.
Dog d = new Dog("Lucy", "Retriever", 80);
d.becomeAngry();
try {
d.receivePat();
} catch (Exception e) {
System.out.println(
"Tried to pat: " + e);
}
System.out.println(d);
$ java ExceptionDemo
Tried to pat: java.lang.RuntimeException: grrr... snarl snarl
Lucy is a displeased Retriever weighing 80.0 standard lb units.
Can take Corrective Action in Catch Blocks
Catch blocks can execute arbitrary code.
- May include corrective action.
Dog d = new Dog("Lucy", "Retriever", 80);
d.becomeAngry();
try {
d.receivePat();
} catch (Exception e) {
System.out.println(
"Tried to pat: " + e);
d.eatTreat("banana");
}
d.receivePat();
System.out.println(d);
$ java ExceptionDemo
Tried to pat: java.lang.RuntimeException: grrr... snarl snarl
Lucy munches the banana
Lucy enjoys the pat.
Lucy is a happy Retriever weighing 80.0 standard lb units.
Why Exceptions?
Allows you to keep error handling code separate from ‘real’ code.
- Consider pseudocode that reads a file:
One naive approach to the right.
- Clearly a bad idea.
func readFile: {
open the file;
if (theFileIsOpen) {
determine its size;
if (gotTheFileLength) {
allocate that much memory;
} else {
return error("fileLengthError");
}
if (gotEnoughMemory) {
read the file into memory;
if (readFailed) {
return error("readError");
}
...
} else {
return error("memoryError");
}
} else {
return error("fileOpenError")
}
}
With Exceptions:
func readFile: {
try {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
} catch (fileOpenFailed) {
doSomething;
} catch (sizeDeterminationFailed) {
doSomething;
} catch (memoryAllocationFailed) {
doSomething;
} catch (readFailed) {
doSomething;
} catch (fileCloseFailed) {
doSomething;
}
}
Exceptions and the Call Stack
When an exception is thrown, it descends the call stack.
-
If exceptions reaches the bottom of the stack, the program crashes and Java provides a message for the user.
- Ideally the user is a programmer with the power to do something about it.
java.lang.RuntimeException in thread “main”:
at ArrayRingBuffer.peek:63
at GuitarString.sample:48
at GuitarHeroLite.java:110
“Must be Caught or Declared to be Thrown”
Occasionally, you’ll find that your code won’t even compile, for the mysterious reason that an exception “must be caught or declared to be thrown”.
- The basic idea: Some exceptions are considered so disgusting by the compiler that you MUST handle them somehow.
- We call these “checked” exceptions.
Checked Exceptions
Examples so far have been unchecked exceptions. There are also checked exceptions:
-
Compiler requires that these be “caught” or “specified”.
- Goal: Disallow compilation to prevent avoidable program crashes.
Unchecked Exceptions
By contrast unchecked exceptions have no such restrictions.
- Code below will compile just fine (but will crash at runtime).
Checked vs. Unchecked Exceptions
Any subclass of RuntimeException or Error is unchecked, all other Throwables are checked.
-
Compiles fine, because the possibility of unchecked exceptions is allowed:
public class UncheckedExceptionDemo { public static void main(String[] args) { if (today == “Thursday”) { throw new RuntimeException("as a joke"); } } } -
Won’t compile, because there exists possibility of checked exception.
public class Eagle { public static void gulgate() { if (today == “Thursday”) { throw new IOException("hi"); } } }
Checking Exceptions
Compiler requires that all checked exceptions be caught or specified.
Two ways to satisfy compiler:
-
Catch: Use a catch block after potential exception.
public static void gulgate() { try { if (today == “Thursday”) { throw new IOException("hi"); } } catch (Exception e) { System.out.println("psych!"); } } -
Specify method as dangerous with throws keyword.
- Defers to someone else to handle exception.
public static void gulgate() throws IOException { ... throw new IOException("hi"); ... }
The Secret of the Enhanced For Loop
For code on the right to work:
-
Compiler checks that Lists have a method called iterator() that returns an Iterator.
-
Then, compiler checks that Iterators have:
- hasNext()
- next()
List<Integer> friends = new ArrayList<Integer>();
Iterator<Integer> seer = friends.iterator();
while (seer.hasNext()) {
System.out.println(seer.next());
}
The Iterable Iterface
Compiler checks that Lists have a method called iterator() that returns an Iterator.
- How: The List interface extends the Iterable interface, inheriting the abstract iterator() method*.
public interface Iterable<T> {
Iterator<T> iterator();
}
public interface List<T> extends Iterable<T>{
...
}
The Iterator Interface
Then, compiler checks that Iterators have hasNext and next()
- How: The Iterator interface specifies these abstract methods explicitly.
package java.util;
public interface Iterator<T> {
boolean hasNext();
T next();
}
Iteration Using A Nested Class
public class KeyIterator {
private int ptr;
public KeyIterator() {
ptr = 0;
}
public boolean hasNext() {
return (ptr != size);
}
public K next() {
K returnItem = keys[ptr];
ptr = ptr + 1;
return returnItem;
}
}
ArrayMap<String, Integer> am = new ArrayMap<String, Integer>();
am.put("hello", 5);
am.put("syrups", 10);
ArrayMap.KeyIterator ami = am.new KeyIterator();
//Instantiating nested classes requires dot notation.
while (ami.hasNext()) {
System.out.println(ami.next());
}
For-each Iteration And ArrayMaps
To support the enhanced for loop, we need to make ArrayMap implement the Iterable interface.
To complete our task, simply make KeyIterator extend Iterator.
Iteration Summary
Implement iterable interface to support enhanced for loop.
- iterator() method must return an object that implements the Iterator interface.
Syntax 2: Packages, Access Control, Object Methods
Packages
To address the fact that classes might share names:
- A package is a namespace that organizes classes and interfaces.
- Naming convention: Package name starts with website address (backwards).
JAR Files
Suppose you’ve written a program that you want to share.
-
Sharing dozens of .class files in special directories is annoying.
-
Can instead share a single .jar file that contains all of your .class files.
- JAR files are really just zip files, but with some extra info added.
To create a JAR file: 1. Go to File → Project Structure, and select the option below:
Step 2: Click OK a couple of times.
Step 3: Click Build -> Build Artifacts.
- The jar file will be generated in a folder called “Artifacts”.
Step 4: Give to other Java programmers, who can use your jar by importing it into IntelliJ (or otherwise).
- Also possible to run a “Main class” of a package from command line. Not discussed in lecture. See Google for more.
Note on JAR Files
JAR files are just zip files.
- They do not keep your code safe!
- Easy to unzip and transform back into .Java files.
- Important: Do not share .jar files of your projects with other students.
Build Systems
To avoid the need to import a bunch of libraries, put files into the appropriate place, and so forth, there exist a number of “Build Systems”.
Popular build systems include:
- Ant
- Maven
- Gradle (ascendant)
Very useful for large teams and large projects. Advantages are fairly minimal in 61B. We’ll use Maven in Project 3.
A Point to Ponder
Access Is Based Only on Static Types
package universe;
public interface BlackHole {
void add(Object x);
}
package universe;
public class CreationUtils {
public static BlackHole hirsute() {
return new HasHair();
}
}
package universe;
class HasHair implements BlackHole {
Object[] items;
public void add(Object o) { ... }
public Object get(int k) { ... }
}
Try to predict if the compiler will allow each line of Client.
- Not part of the universe package!
import static CreationUtils.hirsute;
class Client {
void demoAccess() {
BlackHole b = hirsute();//YES
b.add("horse");//YES
b.get(0);//NO
HasHair hb = (HasHair) b;//NO
}
}
Access Control at the Top Level
So far we’ve discussed how to control access to members.
- Also possible to control access at the top level (i.e. an entire interface or class).
Two levels:
- public
- no modifier: package-private
Determines who can see the existence of the class.
- Choices: Entire world vs. just members of same package.
Purpose of the Access Modifiers
Access Levels:
- Private declarations ****are parts of the implementation of a class that only that class needs.
- Package-private declarations are parts of the implementation of a package that other members of the package will need to complete the implementation.
- Protected declarations are things that subtypes might need, but subtype clients will not.
- Public declarations are declarations of the specification for the package, i.e. what clients of the package can rely on. Once deployed, these should not change.
toString()
If you want a custom String representation of an object, create a toString() method.
- Nothing particularly interesting about it, except that you can rely on it to generate a (nice?) String representation.
Equals vs. ==
As mentioned in an offhand manner previously, == and .equals() behave differently.
== checks that two variables reference the same object (compares bits in boxes):
To test equality in the sense we usually mean it, use:
-
Arrays.equal or Arrays.deepEquals for arrays.
-
.equals for classes. Requires writing a .equals method for your classes.
- Default implementation of .equals uses == (probably not what you want).
Rules for Equals in Java
Java convention is that equals must be an equivalence relation:
- Reflexive: x.equals(x) is true.
- Symmetric: x.equals(y) is true iff y.equals(x)
- Transitive: x.equals(y) and y.equals(z) implies x.equals(z).
Must also:
- Take an Object argument.
- Be consistent: If x.equals(y), then x must continue to equal y as long as neither changes.
- Never true for null, i.e. x.equals(null) must be false.