Software Construction 4
Software Construction 4
static typing
the big three properties of good software
Hailstone Sequence
2, 1
3, 10, 5, 16, 8, 4, 2, 1
4, 2, 1
2n, 2n-1 , ... , 4, 2, 1
5, 16, 8, 4, 2, 1
7, 22, 11, 34, 17, 52, 26, 13, 40, ...? (where does
this stop?)
Computing Hailstones
Here’s some code for computing and printing the hailstone sequence
for some starting n. We’ll write Java and Python side by side for
comparison:
// Java # Python
int n = 3; n = 3
while (n != 1) { while n != 1:
System.out.println(n); print(n)
if (n % 2 == 0) { if n % 2 == 0:
n = n / 2; n = n / 2
} else { else:
n = 3 * n + 1; n = 3 * n + 1
}
}
System.out.println(n); print(n)
Types
int (for integers like 5 and -200, but limited to the range ± 231, or
roughly ± 2 billion)
long (for larger integers up to ± 263)
boolean (for true or false)
double (for floating-point numbers, which represent a subset of the
real numbers)
char (for single characters like 'A' and '$' )
Java also has object types, for example:
Operations are functions that take inputs and produce outputs (and
sometimes change the values themselves). The syntax for operations
varies, but we still think of them as functions no matter how they’re
written. Here are three different syntaxes for an operation in Python
or Java:
Static Typing
are declared as int s, then the compiler concludes that a+b is also an
int . The Eclipse environment does this while you’re writing the code,
in fact, so you find out about many errors while you’re still typing.
"5" * "6"
that tries to multiply two strings, then static typing will catch this error
while you’re still programming, rather than waiting until the line is
reached during execution.
Here are some rules of thumb for what errors you can expect to be
caught at each of these times.
Integer overflow. The int and long types are actually finite sets
of integers, with maximum and minimum values. What happens
when you do a computation whose answer is too positive or too
negative to fit in that finite range? The computation quietly
overflows (wraps around), and returns an integer from
somewhere in the legal range but not the right answer.
Let’s try some examples of buggy code and see how they behave in
Java. Are these bugs caught statically, dynamically, or not at all?
int n = 5;
if (n) {
n = n + 1;
}
static error
dynamic error
no error, wrong answer
(missing explanation)
check
static error
dynamic error
no error, wrong answer
(missing explanation)
check
(missing explanation)
check
int sum = 0;
int n = 0;
int average = sum/n;
static error
dynamic error
no error, wrong answer
(missing explanation)
check
double sum = 7;
double n = 0;
double average = sum/n;
static error
dynamic error
no error, wrong answer
(missing explanation)
check
Arrays and Collections
The int[] array type includes all possible array values, but a
particular array value, once created, can never change its length.
Operations on array types include:
indexing: a[2]
assignment: a[2]=0
Instead of a fixed-length array, let’s use the List type. Lists are
variable-length sequences of another type T . Here’s how we can
declare a List variable and make a list value:
indexing: list.get(2)
assignment: list.set(2, 0)
length: list.size()
Note that List is an interface, a type that can’t be constructed directly
with new, but that instead specifies the operations that a List must
provide. We’ll talk about this notion in a future class on abstract data
types. ArrayList is a class, a concrete type that provides
implementations of those operations. ArrayList isn’t the only
implementation of the List type, though it’s the most commonly used
one. LinkedList is another. Check them out in the Java API
documentation, which you can find by searching the web for “Java 8
API”. Get to know the Java API docs, they’re your friend. (“API”
means “application programmer interface,” and is commonly used as
a synonym for “library.”)
Not only simpler but safer too, because the List automatically
enlarges itself to fit as many numbers as you add to it (until you run
out of memory, of course).
Iterating
A for loop steps through the elements of an array or a list, just as in
Python, though the syntax looks a little different. For example:
You can iterate through arrays as well as lists. The same code would
work if the list were replaced by an array.
Math.max() is a handy function from the Java API. The Math class is full
of useful functions like this – search for “java 8 Math” on the web to
find its documentation.
Methods
public means that any code, anywhere in your program, can refer to
the class or method. Other access modifiers, like private, are used to
get more safety in a program, and to guarantee immutability for
immutable types. We’ll talk more about them in an upcoming class.
static means that the method doesn’t take a self parameter – which
in Java is implicit anyway, you won’t ever see it as a method
parameter. Static methods can’t be called on an object. Contrast that
with the List add() method or the String length() method, for example,
which require an object to come first. Instead, the right way to call a
static method uses the class name instead of an object reference:
Hailstone.hailstoneSequence(83)
Take note also of the comment before the method, because it’s very
important. This comment is a specification of the method, describing
the inputs and outputs of the operation. The specification should be
concise and clear and precise. The comment provides information
that is not already clear from the method types. It doesn’t say, for
example, that n is an integer, because the int n declaration just below
already says that. But it does say that n must be positive, which is
not captured by the type declaration but is very important for the
caller to know.
We’ll have a lot more to say about how to write good specifications in
a few classes, but you’ll have to start reading them and using them
right away.
final int n = 5;
If the Java compiler isn’t convinced that your final variable will only be
assigned once at runtime, then it will produce a compiler error. So
final gives you static checking for immutable references.
Documenting Assumptions
Safety is the first reason. Java has static checking (primarily type
checking, but other kinds of static checks too, like that your code
returns values from methods declared to do so). We’re studying
software engineering in this course, and safety from bugs is a key
tenet of that approach. Java dials safety up to 11, which makes it a
good language for learning about good software engineering
practices. It’s certainly possible to write safe code in dynamic
languages like Python, but it’s easier to understand what you need to
do if you learn how in a safe, statically-checked language.
Ubiquity is another reason. Java is widely used in research,
education, and industry. Java runs on many platforms, not just
Windows/Mac/Linux. Java can be used for web programming (both
on the server and in the client), and native Android programming is
done in Java. Although other programming languages are far better
suited to teaching programming (Scheme and ML come to mind),
regrettably these languages aren’t as widespread in the real world.
Java on your resume will be recognized as a marketable skill. But
don’t get us wrong: the real skills you’ll get from this course are not
Java-specific, but carry over to any language that you might program
in. The most important lessons from this course will survive language
fads: safety, clarity, abstraction, engineering instincts.
Summary
The main idea we introduced today is static checking. Here’s how
this idea relates to the goals of the course:
Objectives
Software in 6.005
Ready for
Safe from bugs Easy to understand
change
int a = 5; // (1)
if (a > 10) { // (2)
int b = 2; // (3)
} else { // (4)
int b = 4; // (5)
} // (6)
b *= 3; // (7)
(missing explanation)
check
(missing explanation)
check
(missing explanation)
check
Don’t worry if you find the Number wrapper classes confusing. They are.
Questions: Numbers
Questions: Characters, Strings
reading exercises
fahrenheit = 212.0
celsius = (fahrenheit - 32) * 5/9
Yes
No: integer arithmetic will cause celsius to be zero
No: integer arithmetic will cause celsius to be rounded down
(missing explanation)
check
Double shot
Rewrite the first line in Java:
int fahrenheit = 212.0;
Integer fahrenheit = 212.0;
float fahrenheit = 212.0;
Float fahrenheit = 212.0;
double fahrenheit = 212.0;
Double fahrenheit = 212.0;
And the second line, where ??? is the same type you selected above:
??? celsius = (fahrenheit - 32) * 5/9;
??? celsius = (fahrenheit - 32) * (5 / 9);
??? celsius = (fahrenheit - 32) * (5. / 9);
(missing explanation)
check
Fit to print
How should we print the result?
System.out.println(fahrenheit, " -> ", celsius);
System.out.println(fahrenheit + " -> " + celsius);
System.out.println("%s -> %s" % (fahrenheit, celsius));
(missing explanation)
check
You should be able to answer the questions on the first two Questions and
Exercises pages.
Questions: Classes
Questions: Objects
Don’t worry if you don’t understand everything in Nested Classes and Enum
Types right now. You can go back to those constructs later in the semester
when we see them in class.
reading exercises
class Tortoise:
def __init__(self):
self.position = 0
def forward(self):
self.position += 1
pokey = Tortoise()
pokey.forward()
print pokey.position
(missing explanation)
check
Under construction
(missing explanation)
check
Methodical
What’s the appropriate line of code for the body of the method? (check all
that apply)
position += 1;
self.position += 1;
this.position += 1;
Tortoise.position += 1;
(missing explanation)
check
On your mark
public Tortoise() {
int position = 0; // (3)
int self.position = 0; // (4)
int this.position = 0; // (5)
int Tortoise.position = 0; // (6)
}
// ...
}
1
2
3
4
5
6
… or in a combination of lines:
public Tortoise() {
self.position = 0; // (3)
this.position = 0; // (4)
Tortoise.position = 0; // (5)
}
// ...
}
1
2
3
4
5
(missing explanation)
check
Hello, world!
Read Hello World!
You should be able to create a new HelloWorldApp.java file, enter the code
from that tutorial page, and compile and run the program to see Hello World!
on the console.
Snapshot diagrams
Many readings include optional videos from the MITx version of 6.005.
More info about the videos
To talk to each other through pictures (in class and in team meetings)
To illustrate concepts like primitive types vs. object types, immutable
values vs. immutable references, pointer aliasing, stack vs. heap,
abstractions vs. concrete representations.
To help explain your design for your team project (with each other and
with your TA).
To pave the way for richer design notations in subsequent courses. For
example, snapshot diagrams generalize into object models in 6.170.
Although the diagrams in this course use examples from Java, the notation
can be applied to any modern programming language, e.g., Python,
Javascript, C++, Ruby.
Primitive values
Object values
An object value is a circle labeled by its type. When we want to show more
detail, we write field names inside it, with arrows pointing out to their values.
For still more detail, the fields can include their declared types. Some
people prefer to write x:int instead of int x , but both are fine.
String s = "a";
s = s + "b";
Mutable values
Immutable references
Java also gives us immutable references: variables that are assigned once
and never reassigned. To make a reference immutable, declare it with the
keyword final :
final int n = 5;
If the Java compiler isn’t convinced that your final variable will only be
assigned once at runtime, then it will produce a compiler error. So final
This list of cities might represent a trip from Boston to Bogotá to Barcelona.
s1.difference_update(s2)
s1.removeAll(s2) remove s2 from s1 s1 -= s2
Here we have a set of integers, in no particular order: 42, 1024, and -7.
Validation
Testing is an example of a more general process called validation. The purpose of validation is to
uncover problems in a program and thereby increase your confidence in the program’s correctness.
Validation includes:
Formal reasoning about a program, usually called verification. Verification constructs a formal
proof that a program is correct. Verification is tedious to do by hand, and automated tool support
for verification is still an active area of research. Nevertheless, small, crucial pieces of a program
may be formally verified, such as the scheduler in an operating system, or the bytecode interpreter
in a virtual machine, or the filesystem in an operating system.
Code review. Having somebody else carefully read your code, and reason informally about it, can
be a good way to uncover bugs. It’s much like having somebody else proofread an essay you have
written. We’ll talk more about code review in the next reading.
Testing. Running the program on carefully selected inputs and checking the results.
Even with the best validation, it’s very hard to achieve perfect quality in software. Here are some
typical residual defect rates (bugs left over after the software has shipped) per kloc (one thousand
lines of source code):
This can be discouraging for large systems. For example, if you have shipped a million lines of typical
industry source code (1 defect/kloc), it means you missed 1000 bugs!
Here are some approaches that unfortunately don’t work well in the world of software.
Exhaustive testing is infeasible. The space of possible test cases is generally too big to cover
exhaustively. Imagine exhaustively testing a 32-bit floating-point multiply operation, a*b . There are 2^64
test cases!
Haphazard testing (“just try it and see if it works”) is less likely to find bugs, unless the program is so
buggy that an arbitrarily-chosen input is more likely to fail than to succeed. It also doesn’t increase our
confidence in program correctness.
Random or statistical testing doesn’t work well for software. Other engineering disciplines can test
small random samples (e.g. 1% of hard drives manufactured) and infer the defect rate for the whole
production lot. Physical systems can use many tricks to speed up time, like opening a refrigerator
1000 times in 24 hours instead of 10 years. These tricks give known failure rates (e.g. mean lifetime of
a hard drive), but they assume continuity or uniformity across the space of defects. This is true for
physical artifacts.
But it’s not true for software. Software behavior varies discontinuously and discretely across the space
of possible inputs. The system may seem to work fine across a broad range of inputs, and then
abruptly fail at a single boundary point. The famous Pentium division bug affected approximately 1 in 9
billion divisions. Stack overflows, out of memory errors, and numeric overflow bugs tend to happen
abruptly, and always in the same way, not with probabilistic variation. That’s different from physical
systems, where there is often visible evidence that the system is approaching a failure point (cracks in
a bridge) or failures are distributed probabilistically near the failure point (so that statistical testing will
observe some failures even before the point is reached).
Instead, test cases must be chosen carefully and systematically, and that’s what we’ll look at next.
reading exercises
Testing basics
In the 1990s, the Ariane 5 launch vehicle, designed and built for the European Space Agency, self-
destructed 37 seconds after its first launch.
The reason was a control software bug that went undetected. The Ariane 5’s guidance software was
reused from the Ariane 4, which was a slower rocket. When the velocity calculation converted from a
64-bit floating point number (a double in Java terminology, though this software wasn’t written in Java)
to a 16-bit signed integer (a short ), it overflowed the small integer and caused an exception to be
thrown. The exception handler had been disabled for efficiency reasons, so the guidance software
crashed. Without guidance, the rocket crashed too. The cost of the failure was $1 billion.
(missing explanation)
check
Testing requires having the right attitude. When you’re coding, your goal is to make the program work,
but as a tester, you want to make it fail.
That’s a subtle but important difference. It is all too tempting to treat code you’ve just written as a
precious thing, a fragile eggshell, and test it very lightly just to see it work.
Instead, you have to be brutal. A good tester wields a sledgehammer and beats the program
everywhere it might be vulnerable, so that those vulnerabilities can be eliminated.
Test-first Programming
Test early and often. Don’t leave testing until the end, when you have a big pile of unvalidated code.
Leaving testing until the end only makes debugging longer and more painful, because bugs may be
anywhere in your code. It’s far more pleasant to test your code as you develop it.
In test-first-programming, you write tests before you even write any code. The development of a single
function proceeds in this order:
The specification describes the input and output behavior of the function. It gives the types of the
parameters and any additional constraints on them (e.g. sqrt ’s parameter must be nonnegative). It also
gives the type of the return value and how the return value relates to the inputs. You’ve already seen
and used specifications on your problem sets in this class. In code, the specification consists of the
method signature and the comment above it that describes what it does. We’ll have much more to say
about specifications a few classes from now.
Writing tests first is a good way to understand the specification. The specification can be buggy, too —
incorrect, incomplete, ambiguous, missing corner cases. Trying to write tests can uncover these
problems early, before you’ve wasted time writing an implementation of a buggy spec.
To do this, we divide the input space into subdomains, each consisting of a set of inputs. Taken
together the subdomains completely cover the input space, so that every input lies in at least one
subdomain. Then we choose one test case from each subdomain, and that’s our test suite.
The idea behind subdomains is to partition the input space into sets of similar inputs on which the
program has similar behavior. Then we use one representative of each set. This approach makes the
best use of limited testing resources by choosing dissimilar test cases, and forcing the testing to
explore parts of the input space that random testing might not reach.
We can also partition the output space into subdomains (similar outputs on which the program has
similar behavior) if we need to ensure our tests will explore different parts of the output space. Most of
the time, partitioning the input space is sufficient.
Example: BigInteger.multiply()
Let’s look at an example. BigInteger is a class built into the Java library that can represent integers of
any size, unlike the primitive types int and long that have only limited ranges. BigInteger has a method
multiply that multiplies two BigInteger values together:
/**
* @param val another BigIntger
* @return a BigInteger whose value is (this * val).
*/
public BigInteger multiply(BigInteger val)
BigInteger a = ...;
BigInteger b = ...;
BigInteger ab = a.multiply(b);
This example shows that even though only one parameter is explicitly shown in the method’s
declaration, multiply is actually a function of two arguments: the object you’re calling the method on ( a
in the example above), and the parameter that you’re passing in the parentheses ( b in this example). In
Python, the object receiving the method call would be explicitly named as a parameter called self in
the method declaration. In Java, you don’t mention the receiving object in the parameters, and it’s
called this instead of self .
So we should think of multiply as a function taking two inputs, each of type BigInteger , and producing
one output of type BigInteger :
So we have a two-dimensional input space, consisting of all the pairs of integers (a,b). Now let’s
partition it. Thinking about how multiplication works, we might start with these partitions:
There are also some special cases for multiplication that we should check: 0, 1, and -1.
a or b is 0, 1, or -1
Finally, as a suspicious tester trying to find bugs, we might suspect that the implementor of BigInteger
might try to make it faster by using int or long internally when possible, and only fall back to an
expensive general representation (like a list of digits) when the value is too big. So we should definitely
also try integers that are very big, bigger than the biggest long .
a or b is small
the absolute value of a or b is bigger than Long.MAX_VALUE , the biggest possible primitive integer in
Java, which is roughly 2^63.
Let’s bring all these observations together into a straightforward partition of the whole (a,b) space.
We’ll choose a and b independently from:
0
1
-1
small positive integer
small negative integer
huge positive integer
huge negative integer
So this will produce 7 × 7 = 49 partitions that completely cover the space of pairs of integers.
To produce the test suite, we would pick an arbitrary pair (a,b) from each square of the grid, for
example:
The figure at the right shows how the two-dimensional (a,b) space is divided by this partition, and the
points are test cases that we might choose to completely cover the partition.
Example: max()
Let’s look at another example from the Java library: the integer max() function, found in the Math class.
/**
* @param a an argument
* @param b another argument
* @return the larger of a and b.
*/
public static int max(int a, int b)
a<b
a=b
a>b
emptiness (the empty string, empty list, empty array) for collection types
the first and last element of a collection
Why do bugs often happen at boundaries? One reason is that programmers often make off-by-one
mistakes (like writing <= instead of < , or initializing a counter to 0 instead of 1). Another is that some
boundaries may need to be handled as special cases in the code. Another is that boundaries may be
places of discontinuity in the code’s behavior. When an int variable grows beyond its maximum positive
value, for example, it abruptly becomes a negative number.
It’s important to include boundaries as subdomains in your partition, so that you’re choosing an input
from the boundary.
Now let’s pick test values that cover all these classes:
After partitioning the input space, we can choose how exhaustive we want the test suite to be:
reading exercises
Partitioning
/**
* Reverses the end of a string.
*
* 012345 012345
* For example: reverseEnd("Hello, world", 5) returns "Hellodlrow ,"
* <-----> <----->
*
* With start == 0, reverses the entire text.
* With start == text.length(), reverses nothing.
*
* @param text non-null String that will have its end reversed
* @param start the index at which the remainder of the input is reversed,
* requires 0 <= start <= text.length()
* @return input text with the substring from start to the end of the string reversed
*/
public static String reverseEnd(String text, int start)
Which of the following are reasonable partitions for the start parameter?
(missing explanation)
check
Partitioning a String
Which of the following are reasonable partitions for the text parameter?
text contains some letters; text contains no letters, but some numbers; text contains neither letters
nor numbers
text.length() = 0; text.length() > 0
text.length() = 0; text.length()-start is odd; text.length()-start is even
text is every possible string from length 0 to 100
(missing explanation)
check
Blackbox testing means choosing test cases only from the specification, not the implementation of
the function. That’s what we’ve been doing in our examples so far. We partitioned and looked for
boundaries in multiply and max without looking at the actual code for these functions.
Whitebox testing (also called glass box testing) means choosing test cases with knowledge of how
the function is actually implemented. For example, if the implementation selects different algorithms
depending on the input, then you should partition according to those domains. If the implementation
keeps an internal cache that remembers the answers to previous inputs, then you should test repeated
inputs.
When doing whitebox testing, you must take care that your test cases don’t require specific
implementation behavior that isn’t specifically called for by the spec. For example, if the spec says
“throws an exception if the input is poorly formatted,” then your test shouldn’t check specifically for a
NullPointerException just because that’s what the current implementation does. The specification in this
case allows any exception to be thrown, so your test case should likewise be general to preserve the
implementor’s freedom. We’ll have much more to say about this in the class on specs.
reading exercises
/**
* Sort a list of integers in nondecreasing order. Modifies the list so that
* values.get(i) <= values.get(i+1) for all 0<=i<values.length()-1
*/
public static void sort(List<Integer> values) {
// choose a good algorithm for the size of the list
if (values.length() < 10) {
radixSort(values);
} else if (values.length() < 1000*1000*1000) {
quickSort(values);
} else {
mergeSort(values);
}
}
Which of the following test cases are likely to be boundary values produced by white box testing?
(missing explanation)
check
Documenting Your Testing Strategy
For the example function on the left, on the right is how we can document the testing strategy we
worked on in the partitioning exercises above. The strategy also addresses some boundary values we
didn’t consider before.
/*
/** * Testing strategy
* Reverses the end of a string. *
* * Partition the inputs as follows:
* For example: * text.length(): 0, 1, > 1
* reverseEnd("Hello, world", 5) * start: 0, 1, 1 < start < text.length(),
* returns "Hellodlrow ," * text.length() - 1, text.length()
* * text.length()-start: 0, 1, even > 1, odd > 1
* With start == 0, reverses the entire text. *
* With start == text.length(), reverses nothing. * Include even- and odd-length reversals because
* * only odd has a middle element that doesn't move.
* @param text non-null String that will have *
* its end reversed * Exhaustive Cartesian coverage of partitions.
* @param start the index at which the */
* remainder of the input is
* reversed, requires 0 <= Document how each test case was chosen,
* start <= text.length()
* @return input text with the substring from
including white box tests:
* start to the end of the string
* reversed // covers test.length() = 0,
*/ // start = 0 = text.length(),
static String reverseEnd(String text, int start) // text.length()-start = 0
@Test public void testEmpty() {
assertEquals("", reverseEnd("", 0));
}
Coverage
One way to judge a test suite is to ask how thoroughly it exercises the program. This notion is called
coverage. Here are three common kinds of coverage:
Branch coverage is stronger (requires more tests to achieve) than statement coverage, and path
coverage is stronger than branch coverage. In industry, 100% statement coverage is a common goal,
but even that is rarely achieved due to unreachable defensive code (like “should never get here”
assertions). 100% branch coverage is highly desirable, and safety critical industry code has even more
arduous criteria (e.g., “MCDC,” modified decision/condition coverage). Unfortunately 100% path
coverage is infeasible, requiring exponential-size test suites to achieve.
A standard approach to testing is to add tests until the test suite achieves adequate statement
coverage: i.e., so that every reachable statement in the program is executed by at least one test case.
In practice, statement coverage is usually measured by a code coverage tool, which counts the
number of times each statement is run by your test suite. With such a tool, white box testing is easy;
you just measure the coverage of your black box tests, and add more test cases until all important
statements are logged as executed.
A good code coverage tool for Eclipse is EclEmma, shown on the right.
Lines that have been executed by the test suite are colored green, and lines not yet covered are red. If
you saw this result from your coverage tool, your next step would be to come up with a test case that
causes the body of the while loop to execute, and add it to your test suite so that the red lines become
green.
reading exercises
Install EclEmma in Eclipse on your laptop. Use your laptop, because you’ll need it for testing exercises
in class, too.
Then create a new Java class called Hailstone.java (you can make a new project for it, or just put it in
the project from class 2 exercises) containing this code:
Run this class with EclEmma code coverage highlighting turned on, by choosing Run → Coverage As
→ Java Application.
By changing the initial value of n , you can observe how EclEmma highlights different lines of code
differently.
When n=3 initially, what color is the line n = n/2 after execution?
(missing explanation)
(missing explanation)
What initial value of n would make the line while (n != 1) yellow after execution?
(missing explanation)
check
A well-tested program will have tests for every individual module (where a module is a method or a
class) that it contains. A test that tests an individual module, in isolation if possible, is called a unit
test. Testing modules in isolation leads to much easier debugging. When a unit test for a module fails,
you can be more confident that the bug is found in that module, rather than anywhere in the program.
The opposite of a unit test is an integration test, which tests a combination of modules, or even the
entire program. If all you have are integration tests, then when a test fails, you have to hunt for the
bug. It might be anywhere in the program. Integration tests are still important, because a program can
fail at the connections between modules. For example, one module may be expecting different inputs
than it’s actually getting from another module. But if you have a thorough set of unit tests that give you
confidence in the correctness of individual modules, then you’ll have much less searching to do to find
the bug.
Suppose you’re building a web search engine. Two of your modules might be getWebPage() , which
downloads web pages, and extractWords() , which splits a page into its component words:
/** @return the contents of the web page downloaded from url
*/
public static String getWebPage(URL url) {...}
These methods might be used by another module makeIndex() as part of the web crawler that makes
the search engine’s index:
One mistake that programmers sometimes make is writing test cases for extractWords() in such a way
that the test cases depend on getWebPage() to be correct. It’s better to think about and test
extractWords() in isolation, and partition it. Using test partitions that involve web page content might be
reasonable, because that’s how extractWords() is actually used in the program. But don’t actually call
getWebPage() from the test case, because getWebPage() may be buggy! Instead, store web page content
as a literal string, and pass it directly to extractWords() . That way you’re writing an isolated unit test,
and if it fails, you can be more confident that the bug is in the module it’s actually testing, extractWords() .
Note that the unit tests for makeIndex() can’t easily be isolated in this way. When a test case calls
makeIndex() , it is testing the correctness of not only the code inside makeIndex() , but also all the methods
called by makeIndex() . If the test fails, the bug might be in any of those methods. That’s why we want
separate tests for getWebPage() and extractWords() , to increase our confidence in those modules
individually and localize the problem to the makeIndex() code that connects them together.
Isolating a higher-level module like makeIndex() is possible if we write stub versions of the modules that
it calls. For example, a stub for getWebPage() wouldn’t access the internet at all, but instead would return
mock web page content no matter what URL was passed to it. A stub for a class is often called a
mock object.
Automated Testing and Regression Testing
Nothing makes tests easier to run, and more likely to be run, than complete automation. Automated
testing means running the tests and checking their results automatically. A test driver should not be an
interactive program that prompts you for inputs and prints out results for you to manually check.
Instead, a test driver should invoke the module itself on fixed test cases and automatically check that
the results are correct. The result of the test driver should be either “all tests OK” or “these tests
failed: …” A good testing framework, like JUnit, helps you build automated test suites.
Note that automated testing frameworks like JUnit make it easy to run the tests, but you still have to
come up with good test cases yourself. Automatic test generation is a hard problem, still a subject of
active computer science research.
Once you have test automation, it’s very important to rerun your tests when you modify your code.
This prevents your program from regressing — introducing other bugs when you fix new bugs or add
new features. Running all your tests after every change is called regression testing.
Whenever you find and fix a bug, take the input that elicited the bug and add it to your automated test
suite as a test case. This kind of test case is called a regression test. This helps to populate your test
suite with good test cases. Remember that a test is good if it elicits a bug — and every regression
test did in one version of your code! Saving regression tests also protects against reversions that
reintroduce the bug. The bug may be an easy error to make, since it happened once already.
This idea also leads to test-first debugging. When a bug arises, immediately write a test case for it
that elicits it, and immediately add it to your test suite. Once you find and fix the bug, all your test
cases will be passing, and you’ll be done with debugging and have a regression test for that bug.
In practice, these two ideas, automated testing and regression testing, are almost always used in
combination.
Regression testing is only practical if the tests can be run often, automatically. Conversely, if you
already have automated testing in place for your project, then you might as well use it to prevent
regressions. So automated regression testing is a best-practice of modern software engineering.
reading exercises
Regression testing
Changes should be tested against all inputs that elicited bugs in earlier versions of the code.
Every component in your code should have an associated set of tests that exercises all the corner
cases in its specification.
Tests should be written before you write the code as a way of checking your understanding of the
specification.
When a new test exposes a bug, you should run it on all previous versions of the code until you find
the version where the bug was introduced.
check
Which of the following are good times to rerun all your JUnit tests?
(missing explanation)
check
Testing techniques
Which of these techniques are useful for choosing test cases in test-first programming, before any
code is written?
black box
regression
static typing
partitioning
boundaries
white box
coverage
(missing explanation)
check
Summary
In this reading, we saw these ideas:
The topics of today’s reading connect to our three key properties of good software as follows:
Safe from bugs. Testing is about finding bugs in your code, and test-first programming is about
finding them as early as possible, immediately after you introduced them.
Easy to understand. Testing doesn’t help with this as much as code review does.
Ready for change. Readiness for change was considered by writing tests that only depend on
behavior in the spec. We also talked about automated regression testing, which helps keep bugs
from coming back when changes are made to code.
Reading 4: Code Review
Code Review
Style Standards
Most companies and large projects have coding style standards (for
example, Google Java Style). These can get pretty detailed, even to
the point of specifying whitespace (how deep to indent) and where
curly braces and parentheses should go. These kinds of questions
often lead to holy wars since they end up being a matter of taste and
style.
For Java, there’s a general style guide (unfortunately not updated for
the latest versions of Java). Some of its advice gets very specific:
The opening brace should be at the end of the line that begins the
compound statement; the closing brace should begin a line and be
indented to the beginning of the compound statement.
In 6.005, we have no official style guide of this sort. We’re not going
to tell you where to put your curly braces. That’s a personal decision
that each programmer should make. It’s important to be self-
consistent, however, and it’s very important to follow the conventions
of the project you’re working on. If you’re the programmer who
reformats every module you touch to match your personal style, your
teammates will hate you, and rightly so. Be a team player.
But there are some rules that are quite sensible and target our big
three properties, in a stronger way than placing curly braces. The
rest of this reading talks about some of these rules, at least the ones
that are relevant at this point in the course, where we’re mostly
talking about writing basic Java. These are some things you should
start to look for when you’re code reviewing other students, and when
you’re looking at your own code for improvement. Don’t consider it an
exhaustive list of code style guidelines, however. Over the course of
the semester, we’ll talk about a lot more things — specifications,
abstract data types with representation invariants, concurrency and
thread safety — which will then become fodder for code review.
Smelly Example #1
Programmers often describe bad code as having a “bad smell” that
needs to be removed. “Code hygiene” is another word for this. Let’s
start with some smelly code.
The next few sections and exercises will pick out the particular smells
in this code example.
Avoid duplication like you’d avoid crossing the street without looking.
Copy-and-paste is an enormously tempting programming tool, and
you should feel a frisson of danger run down your spine every time
you use it. The longer the block you’re copying, the riskier it is.
The dayOfYear example is full of identical code. How would you DRY it
out?
reading exercises
(missing explanation)
check
check
Which of the following code skeletons could be used to DRY the code
out enough so that dayOfMonth+= appears only once?
(missing explanation)
check
/**
* Compute the hailstone sequence.
* See http://en.wikipedia.org/wiki/Collatz_conjecture#Statement_of_the_pro
* @param n starting number of sequence; requires n > 0.
* @return the hailstone sequence starting at n and ending with 1.
* For example, hailstone(3)=[3,10,5,16,8,4,2,1].
*/
public static List<Integer> hailstoneSequence(int n) {
...
}
The dayOfYear code needs some comments — where would you put
them? For example, where would you document whether month runs
from 0 to 11 or from 1 to 12?
if (month == 2) { // we're in February [C2]
dayOfMonth += 31; // add in the days of January that already passe
} else if (month == 3) {
dayOfMonth += 59; // month is 3 here [C4]
} else if (month == 4) {
dayOfMonth += 90;
}
...
} else if (month == 12) {
dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 31;
}
return dayOfMonth; // the answer [C5]
}
C1
C2
C3
C4
C5
(missing explanation)
check
Fail Fast
Failing fast means that code should reveal its bugs as early as
possible. The earlier a problem is observed (the closer to its cause),
the easier it is to find and fix. As we saw in the first reading, static
checking fails faster than dynamic checking, and dynamic checking
fails faster than producing a wrong answer that may corrupt
subsequent computation.
The dayOfYear function doesn’t fail fast — if you pass it the arguments
in the wrong order, it will quietly return the wrong answer. In fact, the
way dayOfYear is designed, it’s highly likely that a non-American will
pass the arguments in the wrong order! It needs more checking —
either static checking or dynamic checking.
reading exercises
Fail fast
dayOfYear(2, 9, 2019)
(missing explanation)
dayOfYear(1, 9, 2019)
(missing explanation)
dayOfYear(9, 2, 2019)
(missing explanation)
dayOfYear("February", 9, 2019)
(missing explanation)
dayOfYear(2019, 2, 9)
(missing explanation)
dayOfYear(2, 2019, 9)
(missing explanation)
check
Fail faster
(missing explanation)
(missing explanation)
(missing explanation)
public enum Month { JANUARY, FEBRUARY, MARCH, ..., DECEMBER };
public static int dayOfYear(Month month, int dayOfMonth, int year) {
...
}
(missing explanation)
(missing explanation)
check
All other constants are called magic because they appear as if out of
thin air with no explanation.
One way to explain a number is with a comment, but a far better way
is to declare the number as a named constant with a good, clear
name.
reading exercises
In the code:
if (month == 2) { ... }
(missing explanation)
check
Suppose you’re reading some code that uses a turtle graphics library
that you don’t know well, and you see the code:
turtle.rotate(3);
Which of the following are likely assumptions you might make about
the meaning of the magic number 3?
(missing explanation)
check
The magic numbers in this code cause it to fail all three of our
measures of code quality: it’s not safe from bugs (SFB), not easy to
understand (ETU) and not ready for change (RFC).
int x = 5;
for (int i = 0; i < x; ++i) {
turtle.forward(36);
turtle.turn(360.0 / x);
}
(missing explanation)
(missing explanation)
check
keyword says that the variable should never be reassigned, and the
Java compiler will check it statically. For example:
public static int dayOfYear(final int month, final int dayOfMonth, final in
...
}
Smelly Example #2
There was a latent bug in dayOfYear . It didn’t handle leap years at all.
As part of fixing that, suppose we write a leap-year method.
What are the bugs hidden in this code? And what style problems that
we’ve already talked about?
reading exercises
leap(2016)
(missing explanation)
check
leap(2050)
(missing explanation)
check
(missing explanation)
check
leap(916)
(missing explanation)
check
Magic numbers
How many magic numbers are in this code? Count every occurrence
if some appear more than once.
(missing explanation)
check
DRYing out
(missing explanation)
check
as:
In general, variable names like tmp , temp , and data are awful,
symptoms of extreme programmer laziness. Every local variable is
temporary, and every variable is data, so those names are generally
meaningless. Better to use a longer, more descriptive name, so that
your code reads clearly all by itself.
methodsAreNamedWithCamelCaseLikeThis
variablesAreAlsoCamelCase
CONSTANTS_ARE_IN_ALL_CAPS_WITH_UNDERSCORES
ClassesAreCapitalized
packages.are.lowercase.and.separated.by.dots
while variable and class names are usually noun phrases. Choose
short words, and be concise, but avoid abbreviations. For example,
message is clearer than msg , and word is so much better than wd . Keep in
mind that many of your teammates in class and in the real world will
not be native English speakers, and abbreviations can be even harder
for non-native speakers.
ALL_CAPS_WITH_UNDERSCORES is used for static final
The leap method has bad names: the method name itself, and the
local variable name. What would you call them instead?
reading exercises
Which of the following are good names for the leap() method?
leap
isLeapYear
IsLeapYear
is_divisible_by_4
(missing explanation)
check
leapYearString
yearString
temp
secondsPerDay
s
(missing explanation)
check
Put spaces within code lines to make them easy to read. The leap
example has some lines that are packed together — put in some
spaces.
Never use tab characters for indentation, only space characters. Note
that we say characters, not keys. We’re not saying you should never
press the Tab key, only that your editor should never put a tab
character into your source file in response to your pressing the Tab
key. The reason for this rule is that different tools treat tab characters
differently — sometimes expanding them to 4 spaces, sometimes to
2 spaces, sometimes to 8. If you run “git diff” on the command line, or
if you view your source code in a different editor, then the indentation
may be completely screwed up. Just use spaces. Always set your
programming editor to insert space characters when you press the
Tab key.
Smelly Example #3
Here’s a third example of smelly code that will illustrate the remaining
points of this reading.
reading exercises
countLongWords
n
LONG_WORD_LENGTH
longestWord
word
words
(missing explanation)
check
Effect of final
n
(missing explanation)
LONG_WORD_LENGTH
(missing explanation)
longestWord
(missing explanation)
word
(missing explanation)
words
(missing explanation)
check
Summary
Code review is a widely-used technique for improving software quality
by human inspection. Code review can detect many kinds of problems
in code, but as a starter, this reading talked about these general
principles of good code:
Ready for change. Code review helps here when it’s done by
experienced software developers who can anticipate what might
change and suggest ways to guard against it. DRY code is more
ready for change, because a change only needs to be made in
one place. Returning results instead of printing them makes it
easier to adapt the code to a new purpose.
Reading 5: Version Control
Introduction
Version control systems are essential tools of the software engineering world. More or less
every project — serious or hobby, open source or proprietary — uses version control. Without
version control, coordinating a team of programmers all editing the same project’s code will
reach pull-out-your-hair levels of aggravation.
Dropbox
Undo/redo buffer
Keeping multiple copies of files with version numbers
Version 1
Alice Hello.java
She starts with one file Hello.java in her pset, which she works on for several days.
At the last minute before she needs to hand in her pset to be graded, she realizes she has
made a change that breaks everything. If only she could go back in time and retrieve a past
version!
A simple discipline of saving backup files would get the job done.
Alice uses her judgment to decide when she has reached some milestone that justifies saving
the code. She saves the versions of Hello.java as Hello.1.java , Hello.2.java , and Hello.java . She
follows the convention that the most recent version is just Hello.java to avoid confusing Eclipse.
We will call the most recent version the head.
Now when Alice realizes that version 3 is fatally flawed, she can just copy version 2 back into
the location for her current code. Disaster averted! But what if version 3 included some changes
that were good and some that were bad? Alice can compare the files manually to find the
changes, and sort them into good and bad changes. Then she can copy the good changes into
version 2.
This is a lot of work, and it’s easy for the human eye to miss changes. Luckily, there are
standard software tools for comparing text; in the UNIX world, one such tool is diff . A better
version control system will make diffs easy to generate.
Alice also wants to be prepared in case her laptop gets run over by a bus, so she saves a
backup of her work in the cloud, uploading the contents of her working directory whenever she’s
satisfied with its contents.
If her laptop is kicked into the Charles, Alice can retrieve the backup and resume work on the
pset on a fresh machine, retaining the ability to time-travel back to old versions at will.
Furthermore, she can develop her pset on multiple machines, using the cloud provider as a
common interchange point. Alice makes some changes on her laptop and uploads them to the
cloud. Then she downloads onto her desktop machine at home, does some more work, and
uploads the improved code (complete with old file versions) back to the cloud.
Cloud
Version 5L Version 5D
Alice on Alice on
Hello.java laptop desktop Hello.java
If Alice isn’t careful, though, she can run into trouble with this approach. Imagine that she starts
editing Hello.java to create “version 5” on her laptop. Then she gets distracted and forgets about
her changes. Later, she starts working on a new “version 5” on her desktop machine, including
different improvements. We’ll call these versions “5L” and “5D,” for “laptop” and “desktop.”
When it comes time to upload changes to the cloud, there is an opportunity for a mishap! Alice
might copy all her local files into the cloud, causing it to contain version 5D only. Later Alice
syncs from the cloud to her laptop, potentially overwriting version 5L, losing the worthwhile
changes. What Alice really wants here is a merge, to create a new version based on the two
version 5’s.
At this point, considering just the scenario of one programmer working alone, we already have a
list of operations that should be supported by a version control scheme:
Multiple developers
Now let’s add into the picture Bob, another developer. The picture isn’t too different from what
we were just thinking about.
Cloud
Version 5A Version 5A Version 5B Version 5B
Alice and Bob here are like the two Alices working on different computers. They no longer share
a brain, which makes it even more important to follow a strict discipline in pushing to and pulling
from the shared cloud server. The two programmers must coordinate on a scheme for coming
up with version numbers. Ideally, the scheme allows us to assign clear names to whole sets of
files, not just individual files. (Files depend on other files, so thinking about them in isolation
allows inconsistencies.)
Merely uploading new source files is not a very good way to communicate to others the high-
level idea of a set of changes. So let’s add a log that records for each version who wrote it,
when it was finalized, and what the changes were, in the form of a short human-authored
message.
Cloud
Log: Log:
1: 1:
Alice, Alice,
7pm, 7pm,
... ...
... ...
Ver. 5A Ver. 5A Ver. 5B Ver. 5B
4: 4:
Bob, Bob,
Hello.java Greet.java Alice Bob Hello.java Greet.java
8pm, 8pm,
... ...
5A: 5B:
Alice, Bob,
9pm, 9pm,
... ...
Pushing another version now gets a bit more complicated, as we need to merge the logs. This
is easier to do than for Java files, since logs have a simpler structure – but without tool support,
Alice and Bob will need to do it manually! We also want to enforce consistency between the
logs and the actual sets of available files: for each log entry, it should be easy to extract the
complete set of files that were current at the time the entry was made.
But with logs, all sorts of useful operations are enabled. We can look at the log for just a
particular file: a view of the log restricted to those changes that involved modifying some file.
We can also use the log to figure out which change contributed each line of code, or, even
better, which person contributed each line, so we know who to complain to when the code
doesn’t work. This sort of operation would be tedious to do manually; the automated operation
in version control systems is called annotate (or, unfortunately, blame).
Multiple branches
It sometimes makes sense for a subset of the developers to go off and work on a branch, a
parallel code universe for, say, experimenting with a new feature. The other developers don’t
want to pull in the new feature until it is done, even if several coordinated versions are created
in the meantime. Even a single developer can find it useful to create a branch, for the same
reasons that Alice was originally using the cloud server despite working alone.
In general, it will be useful to have many shared places for exchanging project state. There may
be multiple branch locations at once, each shared by several programmers. With the right set-
up, any programmer can pull from or push to any location, creating serious flexibility in
cooperation patterns.
Of course, it turns out we haven’t invented anything here: Git does all these things for you, and
so do many other version control systems.
Dan Carol
Cloud
Alice Bob
Traditional centralized version control systems like CVS and Subversion do a subset of the
things we’ve imagined above. They support a collaboration graph – who’s sharing what changes
with whom – with one master server and copies that only communicate with the master.
In a centralized system, everyone must share their work to and from the master repository.
Changes are safely stored in version control if they are in the master repository, because that’s
the only repository.
Dan Carol
Cloud
Alice Bob
In contrast, distributed version control systems like Git and Mercurial allow all sorts of different
collaboration graphs, where teams and subsets of teams can experiment easily with alternate
versions of code and history, merging versions together as they are determined to be good
ideas.
In a distributed system, all repositories are created equal, and it’s up to users to assign them
different roles. Different users might share their work to and from different repos, and the team
must decide what it means for a change to be in version control. If the change is stored in just
a single programmer’s repo, do they still need to share it with a designated collaborator or
specific server before the rest of the team considers it official?
reading exercises
More equal
In 6.005, which of these problem set repos has a special role?
The repository on 6.005’s Athena locker
The repository on Didit
The repository on your laptop
The repository on your desktop
(missing explanation)
check
Reliable: keep versions around for as long as we need them; allow backups
Multiple files: track versions of a project, not single files
Meaningful versions: what were the changes, why were they made?
Revert: restore old versions, in whole or in part
Compare versions
Review history: for the whole project or individual files
Not just for code: prose, images, …
Git
The version control system we’ll use in 6.005 is Git. It’s powerful and worth learning. But Git’s
user interface can be terribly frustrating. What is Git’s user interface?
In 6.005, we will use Git on the command line. The command line is a fact of life,
ubiquitous because it is so powerful.
The command line can make it very difficult to see what is going on in your repositories. You
may find SourceTree (shown on the right) for Mac & Windows useful. On any platform, gitk
can give you a basic Git GUI. Ask Google for other suggestions.
Eclipse has built-in support for Git. If you follow the problem set instructions, Eclipse will
know your project is in Git and will show you helpful icons. We do not recommend using the
Eclipse Git UI to make changes, commit, etc., and course staff may not be able to help you
with problems.
GitHub makes desktop apps for Mac and Windows. Because the GitHub app changes how
some Git operations work, if you use the GitHub app, course staff will not be able to help
you.
On the Git website, you can find two particularly useful resources:
Pro Git documents everything you might need to know about Git.
The Git command reference can help with the syntax of Git commands.
You’ve already completed PS0 and the Getting Started intro to Git.
That reading introduces the three pieces of a Git repo: .git directory, working directory, and
staging area.
All of the operations we do with Git — clone, add, commit, push, log, merge, … — are
operations on a graph data structure that stores all of the versions of files in our project, and all
the log entries describing those changes. The Git object graph is stored in the .git directory of
your local repository. Another copy of the graph, e.g. for PS0, is on Athena in:
/mit/6.005/git/fa16/psets/ps0/[your username].git
The object graph is stored on disk in a convenient and efficient structure for performing Git
operations, but not in a format we can easily use. In Alice’s invented version control scheme, the
current version of Hello.java was just called Hello.java because she needed to be able to edit it
normally. In Git, we obtain normal copies of our files by checking them out from the object
graph. These are the files we see and edit in Eclipse.
We also decided above that it might be useful to support multiple branches in the version
history. Multiple branches are essential for large teams working on long-term projects. To keep
things simple in 6.005, we will not use branches and we don’t recommend that you create any.
Every Git repo comes with a default branch called master , and all of our work will be on the
master branch.
So step 2 of git clone gets us an object graph, and step 3 gets us a working directory full of
files we can edit, starting from the current version of the project.
Using commands from Getting Started or Pro Git 2.3: Viewing the Commit History, or by using
a tool like SourceTree, explain the history of this little project to yourself.
Each node in the history graph is a commit a.k.a. version a.k.a. revision of the project: a
complete snapshot of all the files in the project at that point in time. You may recall from our
earlier reading that each commit is identified by a unique ID, displayed as a hexadecimal
number.
Except for the initial commit, each commit has a pointer to its parent commit. For example,
commit 1255f4e has parent 41c4b8f : this means 41c4b8f happened first, then 1255f4e .
Some commits have the same parent: they are versions that diverged from a common previous
version. And some commits have two parents: they are versions that tie divergent histories back
together.
A branch — remember master will be our only branch for now — is just a name that points to a
commit.
Finally, HEAD points to our current commit — almost. We also need to remember which branch
we’re working on. So HEAD points to the current branch, which points to the current commit.
reading exercises
HEAD count
How many commits are in this project?
(missing explanation)
(missing explanation)
How many times has a new file been added to the project?
(missing explanation)
check
First impression
What was the original contents of hello.txt ?
(missing explanation)
check
Graph-ical
Which of these are a correct representation of the history of this repository?
check
(missing explanation)
check
Each commit is a snapshot of our entire project, which Git represents with a tree node. For a
project of any reasonable size, most of the files won’t change in any given revision. Storing
redundant copies of the files would be wasteful, so Git doesn’t do that.
Instead, the Git object graph stores each version of an individual file once, and allows multiple
commits to share that one copy. To the left is a more complete rendering of the Git object graph
for our example.
Keep this picture in the back of your mind, because it’s a wonderful example of the sharing
enabled by immutable data types, which we’re going to discuss a few classes from now.
Each commit also has log data — who, when, short log message, etc. — not shown in the
diagram.
In some alternate universe, git commit might create a new commit based on the current contents
of your working directory. So if you edited Hello.java and then did git commit , the snapshot would
include your changes.
We’re not in that universe; in our universe, Git uses that third and final piece of the repository:
the staging area (a.k.a. the index, which is only a useful name to know because sometimes it
shows up in documentation).
The staging area is like a proto-commit, a commit-in-progress. Here’s how we use the staging
area and git add to build up a new snapshot, which we then cast in stone using git commit :
Hover or tap on each step to update the diagram, and to see the output of git status at each
step:
1. If we haven’t made any changes yet, then the working directory, staging area, and HEAD
commit are all identical.
2. Make a change to a file. For example, let’s edit hello.txt .
4. Create a new commit out of all the staged changes using git commit .
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Use git status frequently to keep track of whether you have no changes, unstaged changes, or
staged changes; and whether you have new commits in your local repository that haven’t been
pushed.
reading exercises
Classy
(missing explanation)
check
(missing explanation)
check
Upstaged
Suppose we have a repo and there are changes staged for commit.
After the commit, can there still be changes that are staged?
yes
no
(missing explanation)
(missing explanation)
check
Downplayed
(missing explanation)
check
When you’re working independently, on a single machine, the DAG of your version history will
usually look like a sequence: commit 1 is the parent of commit 2 is the parent of commit 3…
There are three programmers involved in the history of our example repository. Two of them –
Alyssa and Ben – made changes “at the same time.” In this case, “at the same time” doesn’t
mean precisely contemporaneous. Instead, it means they made two different new versions
based on the same previous version, just as Alice made version 5L and 5D on her laptop and
desktop.
When multiple commits share the same parent commit, our history DAG changes from a
sequence to a tree: it branches apart. Notice that a branch in the history of the project doesn’t
require anyone to create a new Git branch, merely that we start from the same commit and
work in parallel on different copies of the repository:
⋮
* commit 82e049e248c63289b8a935ce71b130a74dc04152
| Author: Ben Bitdiddle <[email protected]>
| Greeting in Ruby
|
| * commit 64009369c5ab93492931ad07962ee81bda921ded
|/ Author: Alyssa P. Hacker <[email protected]>
| Greeting in Scheme
|
* commit 1255f4e4a5836501c022deb337fda3f8800b02e4
| Author: Max Goldman <[email protected]>
| Change the greeting
⋮
Finally, the history DAG changes from tree- to graph-shaped when the branching changes are
merged together:
⋮
* commit 3e62e60a7b4a0c262cd8eb4308ac3e5a1e94d839
|\ Author: Max Goldman <[email protected]>
| | Merge
| |
* | commit 82e049e248c63289b8a935ce71b130a74dc04152
| | Author: Ben Bitdiddle <[email protected]>
| | Greeting in Ruby
| |
| * commit 64009369c5ab93492931ad07962ee81bda921ded
|/ Author: Alyssa P. Hacker <[email protected]>
| Greeting in Scheme
|
* commit 1255f4e4a5836501c022deb337fda3f8800b02e4
| Author: Max Goldman <[email protected]>
| Change the greeting
⋮
How is it that changes are merged together? First we’ll need to understand how history is
shared between different users and repositories.
Send & receive object graphs with git push & git pull
We can send new commits to a remote repository using git push :
2. Using git commit , we add new commits to the local history on the master branch.
3. To send those changes back to the origin remote, use git push origin master .
And we receive new commits using git pull . Note that git pull , in addition to fetching new parts
of the object graph, also updates the working copy by checking out the latest version (just like
git clone checked out a working copy to start with).
Merging
Now, let’s examine what happens when changes occur in parallel:
1. Both Alyssa and Ben clone the repository with two commits ( 41c4b8f and 1255f4e ).
3. At the same time, Ben creates hello.rb and commits his change as 82e049e .
At this point, both of their changes only exist in their local repositories. In each repo, master
In this example, Git was able to merge Alyssa’s and Ben’s changes automatically, because they
each modified different files. If both of them had edited the same parts of the same files, Git
would report a merge conflict. Ben would have to manually weave their changes together
before committing the merge. All of this is discussed in the Getting Started section on merges,
merging, and merge conflicts.
reading exercises
Merge
Alice and Bob both start with the same Java file:
If Git merges the changes of Alice and Bob, what is the result of Hello.greet("Eve") ?
Hello, Eve
Hello, Eve!
Ciao, Eve
Ciao, Eve!
we can automatically merge, but the resulting code is broken (static error)
we can automatically merge, but the resulting code is broken (dynamic error)
we can automatically merge, but the resulting code is broken (no error, wrong answer)
we cannot automatically merge the changes
(missing explanation)
check
If Git merges the changes of Alice and Bob, what is the result of Hello.greet("Eve") ?
Hello, Eve
HelloEve
Ciao, Eve
CiaoEve
we can automatically merge, but the resulting code is broken (static error)
we can automatically merge, but the resulting code is broken (dynamic error)
we can automatically merge, but the resulting code is broken (no error, wrong answer)
we cannot automatically merge the changes
(missing explanation)
check
Continue Merging
If Git merges the changes of Alice and Bob, what is the result of running main ?
Hello, Eve
Hello, Eve!
Ciao, Eve
Ciao, Eve!
we can automatically merge, but the resulting code is broken (static error)
we can automatically merge, but the resulting code is broken (dynamic error)
we can automatically merge, but the resulting code is broken (no error, wrong answer)
we cannot automatically merge the changes
(missing explanation)
check
Git is assuming that most of our project does not change in any given commit, so showing only
the differences will be more useful. Almost all the time, that’s true.
But we can ask Git to show us what was in the repo at a particular commit:
hello.rb
hello.scm
hello.txt
you the contents of a now-broken file at some earlier version when the file was OK.
Easy to understand
why was a change made?
what else was changed at the same time?
who can I ask about this code?
Assertions
It is common practice to define a procedure for these kinds of
defensive checks, usually called assert :
This approach abstracts away from what exactly happens when the
assertion fails. The failed assert might exit; it might record an event in
a log file; it might email a report to a maintainer.
assert x >= 0;
x is -1
along with a stack trace that tells you where the assert statement
was found in your code and the sequence of calls that brought the
program to that point. This information is often enough to get started
in finding the bug.
If you just run your program as usual, none of your assertions will be
checked! Java’s designers did this because checking assertions can
sometimes be costly to performance. For example, a procedure that
searches an array using binary search has a requirement that the
array be sorted. Asserting this requirement requires scanning through
the entire array, however, turning an operation that should run in
logarithmic time into one that takes linear time. You should be willing
(eager!) to pay this cost during testing, since it makes debugging
much easier, but not after the program is released to users. For most
applications, however, assertions are not expensive compared to the
rest of the code, and the benefit they provide in bug-checking is worth
that small cost in performance.
@Test(expected=AssertionError.class)
public void testAssertionsEnabled() {
assert false;
}
Note that the Java assert statement is a different mechanism from the
JUnit methods assertTrue() , assertEquals() , etc. They all assert a
predicate about your code, but are designed for use in different
contexts. The assert statement should be used in implementation
code, for defensive checks inside the implementation. JUnit
assert...() methods should be used in JUnit tests, to check the result
of a test. The assert statements don’t run without -ea , but the JUnit
assert...() methods always run.
What to Assert
Here are some things you should assert:
switch (vowel) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u': return "A";
default: assert false;
}
The assertion in the default clause has the effect of asserting that
vowel must be one of the five vowel letters.
When should you write runtime assertions? As you write the code, not
after the fact. When you’re writing the code, you have the invariants in
mind. If you postpone writing assertions, you’re less likely to do it,
and you’re liable to omit some important invariants.
// don't do this:
x = y + 1;
assert x == y+1;
This assertion doesn’t find bugs in your code. It finds bugs in the
compiler or Java virtual machine, which are components that you
should trust until you have good reason to doubt them. If an assertion
is obvious from its local context, leave it out.
Never use assertions to test conditions that are external to your
program, such as the existence of files, the availability of the network,
or the correctness of input typed by a human user. Assertions test the
internal state of your program to ensure that it is within the bounds of
its specification. When an assertion fails, it indicates that the program
has run off the rails in some sense, into a state in which it was not
designed to function properly. Assertion failures therefore indicate
bugs. External failures are not bugs, and there is no change you can
make to your program in advance that will prevent them from
happening. External failures should be handled using exceptions
instead.
// don't do this:
assert list.remove(x);
Assertions
/**
* Solves quadratic equation ax^2 + bx + c = 0.
*
* @param a quadratic coefficient, requires a != 0
* @param b linear coefficient
* @param c constant term
* @return a list of the real roots of the equation
*/
public static List<Double> quadraticRoots(final int a, final int b, final i
List<Double> roots = new ArrayList<Double>();
// A
... // compute roots
// B
return roots;
}
(missing explanation)
check
Incremental Development
Our class on testing talked about two techniques that help with this:
int i;
for (i = 0; i < 100; ++i) {
which makes the scope of the variable the entire rest of the outer
curly-brace block containing this code, you should do this:
Declare a variable only when you first need it, and in the
innermost curly-brace block that you can. Variable scopes in
Java are curly-brace blocks, so put your variable declaration in
the innermost one that contains all the expressions that need to
use the variable. Don’t declare all your variables at the start of the
function – it makes their scopes unnecessarily large. But note that
in languages without static type declarations, like Python and
Javascript, the scope of a variable is normally the entire function
anyway, so you can’t restrict the scope of a variable with curly
braces, alas.
Avoid global variables. Very bad idea, especially as programs
get large. Global variables are often used as a shortcut to provide
a parameter to several parts of your program. It’s better to just
pass the parameter into the code that needs it, rather than putting
it in global space where it can inadvertently reassigned.
reading exercises
Variable scope
1 class Apartment {
2 Apartment(String newAddress) {
3 this.address = newAddress;
4 this.roommates = new HashSet<Person>();
5 }
6
7 String getAddress() {
8 return address;
9 }
10
11 void addRoommate(Person newRoommate) {
12 roommates.add(newRoommate);
13 if (roommates.size() > MAXIMUM_OCCUPANCY) {
14 roommates.remove(newRoommate);
15 throw new TooManyPeopleException();
16 }
17 }
18
19 int getMaximumOccupancy() {
20 return MAXIMUM_OCCUPANCY;
21 }
22 }
Which of these lines are within the scope of the newRoommate variable?
line 3
line 8
line 12
line 15
line 20
(missing explanation)
variable?
lines 2-21
lines 3-4
line 8
lines 12-16
(missing explanation)
Out of the choices below, what is the best declaration for the
roommates variable?
List<Person> roommates;
Set<Person> roommates;
final Set<Person> roommates;
HashSet<Person> roommates;
(missing explanation)
Out of the choices below, what is the best declaration for the
MAXIMUM_OCCUPANCY variable?
int MAXIMUM_OCCUPANCY = 8;
final int MAXIMUM_OCCUPANCY = 8;
static int MAXIMUM_OCCUPANCY = 8;
static final int MAXIMUM_OCCUPANCY = 8;
(missing explanation)
check
Summary
Avoid debugging
make bugs impossible with techniques like static typing,
automatic dynamic checking, and immutable types and
references
Keep bugs confined
failing fast with assertions keeps a bug’s effects from
spreading
incremental development and unit testing confine bugs to your
recent code
scope minimization reduces the amount of the program you
have to search
Safe from bugs. We’re trying to prevent them and get rid of
them.
Easy to understand. Techniques like static typing, final
declarations, and assertions are additional documentation of the
assumptions in your code. Variable scope minimization makes it
easier for a reader to understand how the variable is used,
because there’s less code to look at.
Ready for change. Assertions and static typing document the
assumptions in an automatically-checkable way, so that when a
future programmer changes the code, accidental violations of
those assumptions are detected.
Reading 9: Mutability & Immutability
Objectives
Mutability
Recall from Basic Java when we discussed snapshot diagrams that some
objects are immutable: once created, they always represent the same
value. Other objects are mutable: they have methods that change the value
of the object.
String s = "a";
s = s.concat("b"); // s+="b" and s=s+"b" also mean the same thing
By contrast, StringBuilder objects are mutable. This class has methods that
change the value of the object, rather than just returning new values:
StringBuilder has other methods as well, for deleting parts of the string,
inserting in the middle, or changing individual characters.
So what? In both cases, you end up with s and sb referring to the string of
characters "ab" . The difference between mutability and immutability doesn’t
matter much when there’s only one reference to the object. But there are
big differences in how they behave when there are other references to the
object. For example, when another variable t points to the same String
object as s , and another variable tb points to the same StringBuilder as sb ,
then the differences between the immutable and mutable objects become
more evident:
String t = s;
t = t + "c";
StringBuilder tb = sb;
tb.append("c");
too — possibly to the surprise of the programmer. That’s the essence of the
problem we’re going to look at in this reading.
Since we have the immutable String class already, why do we even need
the mutable StringBuilder in programming? A common use for it is to
concatenate a large number of strings together. Consider this code:
String s = "";
for (int i = 0; i < n; ++i) {
s = s + n;
}
Using immutable strings, this makes a lot of temporary copies — the first
number of the string ( "0" ) is actually copied n times in the course of building
up the final string, the second number is copied n-1 times, and so on. It
actually costs O(n2) time just to do all that copying, even though we only
concatenated n elements.
reading exercises
Follow me
Can a client with the variable terrarium modify the Turtle in red?
No, because the terrarium reference is immutable
No, because the Turtle object is immutable
Yes, because the reference from list index 0 to the Turtle is mutable
Yes, because the Turtle object is mutable
(missing explanation)
Can a client with the variable george modify the Gecko in blue?
No, because the george reference is immutable
No, because the Gecko object is immutable
Yes, because the reference from list index 1 to the Gecko is mutable
Yes, because the Gecko object is mutable
(missing explanation)
Can a client with just the variable petStore make it impossible for a client with
just the variable terrarium to reach the Gecko in blue?
(missing explanation)
check
Risks of mutation
Mutable types seem much more powerful than immutable types. If you were
shopping in the Datatype Supermarket, and you had to choose between a
boring immutable String and a super-powerful-do-anything mutable
StringBuilder , why on earth would you choose the immutable one?
StringBuilder should be able to do everything that String can do, plus set()
Let’s start with a simple method that sums the integers in a list:
Suppose we also need a method that sums the absolute values. Following
good DRY practice (Don’t Repeat Yourself), the implementer writes a
method that uses sum() :
/** @return the sum of the absolute values of the numbers in the list */
public static int sumAbsolute(List<Integer> list) {
// let's reuse sum(), because DRY, so first we take absolute values
for (int i = 0; i < list.size(); ++i)
list.set(i, Math.abs(list.get(i)));
return sum(list);
}
Notice that this method does its job by mutating the list directly. It
seemed sensible to the implementer, because it’s more efficient to reuse the
existing list. If the list is millions of items long, then you’re saving the time
and memory of generating a new million-item list of absolute values. So the
implementer has two very good reasons for this design: DRY, and
performance.
But the resulting behavior will be very surprising to anybody who uses it! For
example:
// meanwhile, somewhere else in the code...
public static void main(String[] args) {
// ...
List<Integer> myData = Arrays.asList(-5, -3, -2);
System.out.println(sumAbsolute(myData));
System.out.println(sum(myData));
}
What will this code print? Will it be 10 followed by -10? Or something else?
reading exercises
Risky #1
What will the code print?
(missing explanation)
check
Safe from bugs? In this example, it’s easy to blame the implementer of
sumAbsolute() for going beyond what its spec allowed. But really, passing
mutable objects around is a latent bug. It’s just waiting for some
programmer to inadvertently mutate that list, often with very good
intentions like reuse or performance, but resulting in a bug that may be
very hard to track down.
private is used for the object’s internal state and internal helper methods,
while public indicates methods and constructors that are intended for clients
of the class (access control).
Note that we draw the arrow from list with a double line, to indicate that it’s
final. That means the arrow can’t change once it’s drawn. But the ArrayList
The primitive types and primitive wrappers are all immutable. If you need
to compute with large numbers, BigInteger and BigDecimal are immutable.
Don’t use mutable Date s, use the appropriate immutable type from
java.time based on the granularity of timekeeping you need.
Collections.unmodifiableList
Collections.unmodifiableSet
Collections.unmodifiableMap
reading exercises
Immutability
Which of the following are correct?
1. A class is immutable if all of its fields are final
2. A class is immutable if instances of it always represent the same value
3. Instances of an immutable class can be safely shared
4. Objects can be made immutable using defensive copying
5. Immutability allows us to reason about global properties instead of
local ones
(missing explanation)
check
Summary
In this reading, we saw that mutability is useful for performance and
convenience, but it also creates risks of bugs by requiring the code that
uses the objects to be well-behaved on a global level, greatly complicating
the reasoning and testing we have to do to be confident in its correctness.
Make sure you understand the difference between an immutable object (like
a String ) and an immutable reference (like a final variable). Snapshot
diagrams can help with this understanding. Objects are values, represented
by circles in a snapshot diagram, and an immutable one has a double
border indicating that it never changes its value. A reference is a pointer to
an object, represented by an arrow in the snapshot diagram, and an
immutable reference is an arrow with a double line, indicating that the arrow
can’t be moved to point to a different object.
The key design principle here is immutability: using immutable objects and
immutable references as much as possible. Let’s review how immutability
helps with the main goals of this course:
Objectives
In this reading, we look at a powerful idea, abstract data types, which enable us to separate how we
use a data structure in a program from the particular form of the data structure itself.
Abstract data types address a particularly dangerous problem: clients making assumptions about the
type’s internal representation. We’ll see why this is dangerous and how it can be avoided. We’ll also
discuss the classification of operations, and some principles of good design for abstract data types.
You should already have read: Controlling Access to Members of a Class in the Java Tutorials.
reading exercises
The following questions use the code below. Study it first, then answer the questions.
class Wallet {
private int amount;
class Person {
private Wallet w;
Access control A
Which of the following statements are true about the line marked /*A*/ ?
that.amount += this.amount;
(missing explanation)
check
Access control B
Which of the following statements are true about the line marked /*B*/ ?
amount = 0;
(missing explanation)
check
Access control C
Which of the following statements are true about the line marked /*C*/ ?
constructor declared.
The illegal access is caught statically.
The illegal access is caught dynamically.
(missing explanation)
check
Access control D
Which of the following statements are true about the line marked /*D*/ ?
w.amount = 100;
(missing explanation)
check
Access control E
Which of the following statements are true about the line marked /*E*/
w.loanTo(w);
(missing explanation)
check
Access control F
Which of the following statements are true about the line marked /*F*/ ?
return w.amount;
The reference to w.amount is allowed by Java because both w and amount are private variables.
The reference to w.amount is allowed by Java because amount is a primitive type, even though it’s
private.
The reference to w.amount is not allowed by Java because amount is a private field in a different class.
The illegal access is caught statically.
The illegal access is caught dynamically.
(missing explanation)
check
Access control G
Which of the following statements are true about the line marked /*G*/ ?
return Wallet.amount == 0;
The reference to Wallet.amount is allowed by Java because Wallet has permission to access its own
private field amount .
(missing explanation)
check
Abstract data types are an instance of a general principle in software engineering, which goes by many
names with slightly different shades of meaning. Here are some of the names that are used for this
idea:
As a software engineer, you should know these terms, because you will run into them frequently. The
fundamental purpose of all of these ideas is to help achieve the three important properties that we care
about in 6.005: safety from bugs, ease of understanding, and readiness for change.
User-Defined Types
In the early days of computing, a programming language came with built-in types (such as integers,
booleans, strings, etc.) and built-in procedures, e.g., for input and output. Users could define their own
procedures: that’s how large programs were built.
A major advance in software development was the idea of abstract types: that one could design a
programming language to allow user-defined types, too. This idea came out of the work of many
researchers, notably Dahl (the inventor of the Simula language), Hoare (who developed many of the
techniques we now use to reason about abstract types), Parnas (who coined the term information
hiding and first articulated the idea of organizing program modules around the secrets they
encapsulated), and here at MIT, Barbara Liskov and John Guttag, who did seminal work in the
specification of abstract types, and in programming language support for them – and developed the
original 6.170, the predecessor to 6.005. Barbara Liskov earned the Turing Award, computer science’s
equivalent of the Nobel Prize, for her work on abstract types.
The key idea of data abstraction is that a type is characterized by the operations you can perform on
it. A number is something you can add and multiply; a string is something you can concatenate and
take substrings of; a boolean is something you can negate, and so on. In a sense, users could already
define their own types in early programming languages: you could create a record type date, for
example, with integer fields for day, month, and year. But what made abstract types new and different
was the focus on operations: the user of the type would not need to worry about how its values were
actually stored, in the same way that a programmer can ignore how the compiler actually stores
integers. All that matters is the operations.
In Java, as in many modern programming languages, the separation between built-in types and user-
defined types is a bit blurry. The classes in java.lang, such as Integer and Boolean are built-in; whether
you regard all the collections of java.util as built-in is less clear (and not very important anyway). Java
complicates the issue by having primitive types that are not objects. The set of these types, such as int
and boolean, cannot be extended by the user.
reading exercises
Consider an abstract data type Bool . The type has the following operations:
true : Bool
false : Bool
… where the first two operations construct the two values of the type, and the last three operations
have the usual meanings of logical and, logical or, and logical not on those values.
Which of the following are possible ways that Bool might be implemented, and still be able to satisfy the
specs of the operations? Choose all that apply.
As a single bit, where 1 means true and 0 means false.
As an int value where 5 means true and 8 means false.
As a reference to a String object where "false" means true and "true" means false.
As a long value where all possible values mean true.
(missing explanation)
check
Types, whether built-in or user-defined, can be classified as mutable or immutable. The objects of a
mutable type can be changed: that is, they provide operations which when executed cause the results
of other operations on the same object to give different results. So Date is mutable, because you can
call setMonth and observe the change with the getMonth operation. But String is immutable, because its
operations create new String objects rather than changing existing ones. Sometimes a type will be
provided in two forms, a mutable and an immutable form. StringBuilder , for example, is a mutable
version of String (although the two are certainly not the same Java type, and are not interchangeable).
Creators create new objects of the type. A creator may take an object as an argument, but not an
object of the type being constructed.
Producers create new objects from old objects of the type. The concat method of String , for
example, is a producer: it takes two strings and produces a new one representing their
concatenation.
Observers take objects of the abstract type and return objects of a different type. The size method
of List , for example, returns an int .
Mutators change objects. The add method of List , for example, mutates a list by adding an element
to the end.
creator : t* → T
producer : T+, t* → T
observer : T+, t* → t
mutator : T+, t* → void | t | T
These show informally the shape of the signatures of operations in the various classes. Each T is the
abstract type itself; each t is some other type. The + marker indicates that the type may occur one or
more times in that part of the signature, and the * marker indicates that it occurs zero or more times. |
indicates or. For example, a producer may take two values of the abstract type T, like String.concat()
does:
concat : String × String → String
A creator operation is often implemented as a constructor, like new ArrayList() . But a creator can simply
be a static method instead, like Arrays.asList() . A creator implemented as a static method is often
called a factory method. The various String.valueOf methods in Java are other examples of creators
implemented as factory methods.
Mutators are often signaled by a void return type. A method that returns void must be called for some
kind of side-effect, since it doesn’t otherwise return anything. But not all mutators return void. For
example, Set.add() returns a boolean that indicates whether the set was actually changed. In Java’s
graphical user interface toolkit, Component.add() returns the object itself, so that multiple add() calls can
be chained together.
Here are some examples of abstract data types, along with some of their operations, grouped by kind.
List is Java’s list type. List is mutable. List is also an interface, which means that other classes
provide the actual implementation of the data type. These classes include ArrayList and LinkedList .
producers: Collections.unmodifiableList
Designing an abstract type involves choosing good operations and determining how they should
behave. Here are a few rules of thumb.
It’s better to have a few, simple operations that can be combined in powerful ways, rather than lots
of complex operations.
Each operation should have a well-defined purpose, and should have a coherent behavior rather than
a panoply of special cases. We probably shouldn’t add a sum operation to List , for example. It might
help clients who work with lists of integers, but what about lists of strings? Or nested lists? All these
special cases would make sum a hard operation to understand and use.
The set of operations should be adequate in the sense that there must be enough to do the kinds of
computations clients are likely to want to do. A good test is to check that every property of an object of
the type can be extracted. For example, if there were no get operation, we would not be able to find
out what the elements of a list are. Basic information should not be inordinately difficult to obtain. For
example, the size method is not strictly necessary for List, because we could apply get on increasing
indices until we get a failure, but this is inefficient and inconvenient.
The type may be generic: a list or a set, or a graph, for example. Or it may be domain-specific: a
street map, an employee database, a phone book, etc. But it should not mix generic and domain-
specific features. A Deck type intended to represent a sequence of playing cards shouldn’t have a
generic add method that accepts arbitrary objects like integers or strings. Conversely, it wouldn’t make
sense to put a domain-specific method like dealCards into the generic type List .
Representation Independence
Critically, a good abstract data type should be representation independent. This means that the use
of an abstract type is independent of its representation (the actual data structure or data fields used to
implement it), so that changes in representation have no effect on code outside the abstract type itself.
For example, the operations offered by List are independent of whether the list is represented as a
linked list or as an array.
You won’t be able to change the representation of an ADT at all unless its operations are fully specified
with preconditions and postconditions, so that clients know what to depend on, and you know what you
can safely change.
Let’s look at a simple abstract data type to see what representation independence means and why it’s
useful. The MyString type below has far fewer operations than the real Java String , and their specs are
a little different, but it’s still illustrative. Here are the specs for the ADT:
These public operations and their specifications are the only information that a client of this data type is
allowed to know. Following the test-first programming paradigm, in fact, the first client we should
create is a test suite that exercises these operations according to their specs. At the moment,
however, writing test cases that use assertEquals directly on MyString objects wouldn’t work, because we
don’t have an equality operation defined on MyString . We’ll talk about how to implement equality
carefully in a later reading. For now, the only operations we can perform with MyStrings are the ones
we’ve defined above: valueOf , length , charAt , and substring . Our tests have to limit themselves to those
operations. For example, here’s one test for the valueOf operation:
MyString s = MyString.valueOf(true);
assertEquals(4, s.length());
assertEquals('t', s.charAt(0));
assertEquals('r', s.charAt(1));
assertEquals('u', s.charAt(2));
assertEquals('e', s.charAt(3));
We’ll come back to the question of testing ADTs at the end of this reading.
For now, let’s look at a simple representation for MyString : just an array of characters, exactly the
length of the string, with no extra room at the end. Here’s how that internal representation would be
declared, as an instance variable within the class:
private char[] a;
With that choice of representation, the operations would be implemented in a straightforward way:
(The ?: syntax in valueOf is called the ternary conditional operator and it’s a shorthand if-else statement.
See The Conditional Operators on this page of the Java Tutorials.)
Question to ponder: Why don’t charAt and substring have to check whether their parameters are within
the valid range? What do you think will happen if the client calls these implementations with illegal
inputs?
One problem with this implementation is that it’s passing up an opportunity for performance
improvement. Because this data type is immutable, the substring operation doesn’t really have to copy
characters out into a fresh array. It could just point to the original MyString object’s character array and
keep track of the start and end that the new substring object represents. The String implementation in
some versions of Java do this.
To implement this optimization, we could change the internal representation of this class to:
private char[] a;
private int start;
private int end;
With this new representation, the operations are now implemented like this:
Objectives
The topic of today’s class is interfaces: separating the interface of an abstract data type from its
implementation, and using Java interface types to enforce that separation.
After today’s class, you should be able to define ADTs with interfaces, and write classes that
implement interfaces.
Interfaces
Java’s interface is a useful language mechanism for expressing an abstract data type. An interface
in Java is a list of method signatures, but no method bodies. A class implements an interface if it
declares the interface in its implements clause, and provides method bodies for all of the interface’s
methods. So one way to define an abstract data type in Java is as an interface, with its
implementation as a class implementing that interface.
One advantage of this approach is that the interface specifies the contract for the client and nothing
more. The interface is all a client programmer needs to read to understand the ADT. The client can’t
create inadvertent dependencies on the ADT’s rep, because instance variables can’t be put in an
interface at all. The implementation is kept well and truly separated, in a different class altogether.
Another advantage is that multiple different representations of the abstract data type can co-exist in
the same program, as different classes implementing the interface. When an abstract data type is
represented just as a single class, without an interface, it’s harder to have multiple representations.
In the MyString example from Abstract Data Types, MyString was a single class. We explored two
different representations for MyString , but we couldn’t have both representations for the ADT in the
same program.
Java’s static type checking allows the compiler to catch many mistakes in implementing an ADT’s
contract. For instance, it is a compile-time error to omit one of the required methods, or to give a
method the wrong return type. Unfortunately, the compiler doesn’t check for us that the code
adheres to the specs of those methods that are written in documentation comments.
reading exercises
Java interfaces
Consider this Java interface and Java class, which are intended to implement an immutable set data
type:
Which of the following statements are true about Set<E> and ArraySet<E> ?
The line labeled A is a problem because Java interfaces can’t have constructors.
True
False
(missing explanation)
The line labeled B is a problem because Set mentions ArraySet , but ArraySet also mentions Set , which
is circular.
True
False
(missing explanation)
True
False
(missing explanation)
ArraySet doesn’t correctly implement Set because it’s missing the contains() method.
True
False
(missing explanation)
ArraySet doesn’t correctly implement Set because it includes a method that Set doesn’t have.
True
False
(missing explanation)
ArraySet doesn’t correctly implement Set because ArraySet is mutable while Set is immutable.
True
False
(missing explanation)
check
Subtypes
Recall that a type is a set of values. The Java List type is defined by an interface. If we think about
all possible List values, none of them are List objects: we cannot create instances of an interface.
Instead, those values are all ArrayList objects, or LinkedList objects, or objects of another class that
implements List . A subtype is simply a subset of the supertype: ArrayList and LinkedList are
subtypes of List .
That means B is only a subtype of A if B’s specification is at least as strong as A’s specification.
When we declare a class that implements an interface, the Java compiler enforces part of this
requirement automatically: for example, it ensures that every method in A appears in B, with a
compatible type signature. Class B cannot implement interface A without implementing all of the
methods declared in A.
But the compiler cannot check that we haven’t weakened the specification in other ways:
strengthening the precondition on some inputs to a method, weakening a postcondition, weakening
a guarantee that the interface abstract type advertises to clients. If you declare a subtype in Java
— implementing an interface is our current focus — then you must ensure that the subtype’s spec is
at least as strong as the supertype’s.
reading exercises
Immutable shapes
Yes
No
Yes
No
Yes
No
(missing explanation)
check
Mutable shapes
(missing explanation)
(missing explanation)
(missing explanation)
(missing explanation)
check
Example: MyString
Let’s revisit MyString . Using an interface instead of a class for the ADT, we can support multiple
implementations:
/** Get the substring between start (inclusive) and end (exclusive).
* @param start starting index
* @param end ending index. Requires 0 <= start <= end <= string length.
* @return string consisting of charAt(start)...charAt(end-1) */
public MyString substring(int start, int end);
}
We’ll skip the static valueOf method and come back to it in a minute. Instead, let’s go ahead using a
different technique from our toolbox of ADT concepts in Java: constructors.
private char[] a;
private char[] a;
private int start;
private int end;
Compare these classes to the implementations of MyString in Abstract Data Types. Notice how
the code that previously appeared in static valueOf methods now appears in the constructors,
slightly changed to refer to the rep of this .
Also notice the use of @Override . This annotation informs the compiler that the method must have
the same signature as one of the methods in the interface we’re implementing. But since the
compiler already checks that we’ve implemented all of the interface methods, the primary value
of @Override here is for readers of the code: it tells us to look for the spec of that method in the
interface. Repeating the spec wouldn’t be DRY, but saying nothing at all makes the code harder
to understand.
And notice the private empty constructors we use to make new instances in substring(..) before
we fill in their reps with data. We didn’t have to write these empty constructors before because
Java provides them by default when we don’t declare any others. Adding the constructors that
take boolean b means we have to declare the empty constructors explicitly.
Now that we know good ADTs scrupulously preserve their own invariants, these do-nothing
constructors are a bad pattern: they don’t assign any values to the rep, and they certainly don’t
establish any invariants. We should strongly consider revising the implementation. Since MyString
This code looks very similar to the code we write to use the Java collections classes:
Unfortunately, this pattern breaks the abstraction barrier we’ve worked so hard to build between
the abstract type and its concrete representations. Clients must know the name of the concrete
representation class. Because interfaces in Java cannot contain constructors, they must directly call
one of the concrete class’ constructors. The spec of that constructor won’t appear anywhere in the
interface, so there’s no static guarantee that different implementations will even provide the same
constructors.
Fortunately, (as of Java 8) interfaces are allowed to contain static methods, so we can implement
the creator operation valueOf as a static factory method in the interface MyString :
// ...
Now a client can use the ADT without breaking the abstraction barrier:
MyString s = MyString.valueOf(true);
System.out.println("The first character is: " + s.charAt(0));
reading exercises
Code review
Let’s review the code for FastMyString . Which of these are useful criticisms:
True
False
(missing explanation)
True
False
(missing explanation)
I wish the rep fields were final so they could not be reassigned
True
False
(missing explanation)
I wish the private constructor was public so clients could use it to construct empty strings
True
False
(missing explanation)
I wish the charAt specification did not expose that the rep contains individual characters
True
False
(missing explanation)
I wish the charAt implementation behaved more helpfully when i is greater than the length of the
string
True
False
(missing explanation)
check
Let’s consider as an example one of the ADTs from the Java collections library, Set . Set is the ADT
of finite sets of elements of some other type E . Here is a simplified version of the Set interface:
Set is an example of a generic type: a type whose specification is in terms of a placeholder type to
be filled in later. Instead of writing separate specifications and implementations for Set<String> ,
We can match Java interfaces with our classification of ADT operations, starting with a creator:
The make operation is implemented as a static factory method. Clients will write code like:
Set<String> strings = Set.make();
and the compiler will understand that the new Set is a set of String objects. (We write <E> at the
front of this signature because make is a static method. It needs its own generic type parameter,
separate from the E we’re using in instance method specs.)
Next we have two observer methods. Notice how the specs are in terms of our abstract notion of a
set; it would be malformed to mention the details of any particular implementation of sets with
particular private fields. These specs should apply to any valid implementation of the set ADT.
The representations used by CharSet1 / 2 / 3 are not suited for representing sets of arbitrary-type
elements. The String reps, for example, cannot represent a Set<Integer> without careful work to
define a new rep invariant and abstraction function that handles multi-digit numbers.
Generic interface, generic implementation. We can also implement the generic Set<E> interface
without picking a type for E . In that case, we write our code blind to the actual type that clients will
choose for E . Java’s HashSet does that for Set . Its declaration looks like:
// ... // ...
A generic implementation can only rely on details of the placeholder types that are included in the
interface’s specification. We’ll see in a future reading how HashSet relies on methods that every type
in Java is required to implement — and only on those methods, because it can’t rely on methods
declared in any specific type.
Why Interfaces?
Interfaces are used pervasively in real Java code. Not every class is associated with an interface,
but there are a few good reasons to bring an interface into the picture.
Documentation for both the compiler and for humans. Not only does an interface help the
compiler catch ADT implementation bugs, but it is also much more useful for a human to read
than the code for a concrete implementation. Such an implementation intersperses ADT-level
types and specs with implementation details.
Allowing performance trade-offs. Different implementations of the ADT can provide methods
with very different performance characteristics. Different applications may work better with
different choices, but we would like to code these applications in a way that is representation-
independent. From a correctness standpoint, it should be possible to drop in any new
implementation of a key ADT with simple, localized code changes.
Optional methods. List from the Java standard library marks all mutator methods as optional.
By building an implementation that does not support these methods, we can provide immutable
lists. Some operations are hard to implement with good enough performance on immutable lists,
so we want mutable implementations, too. Code that doesn’t call mutators can be written to
work automatically with either kind of list.
Methods with intentionally underdetermined specifications. An ADT for finite sets could
leave unspecified the element order one gets when converting to a list. Some implementations
might use slower method implementations that manage to keep the set representation in some
sorted order, allowing quick conversion to a sorted list. Other implementations might make many
methods faster by not bothering to support conversion to sorted lists.
Multiple views of one class. A Java class may implement multiple interfaces. For instance, a
user interface widget displaying a drop-down list is natural to view as both a widget and a list.
The class for this widget could implement both interfaces. In other words, we don’t implement an
ADT multiple times just because we are choosing different data structures; we may make
multiple implementations because many different sorts of objects may also be seen as special
cases of the ADT, among other useful perspectives.
More and less trustworthy implementations. Another reason to implement an interface
multiple times might be that it is easy to build a simple implementation that you believe is correct,
while you can work harder to build a fancier version that is more likely to contain bugs. You can
choose implementations for applications based on how bad it would be to get bitten by a bug.
Enum DayOfWeek
Constructor ArrayList()
Collections.singletonList() ,
Creator operation Static (factory) method
Arrays.asList()
Constant BigInteger.ZERO
Producer operation
Static method Collections.unmodifiableList()
Mutator operation
Static method Collections.copy()
reading exercises
Suppose you have an abstract data type for rational numbers, similar to the one we discussed in
Abstraction Functions & Rep Invariants, which is currently represented as a Java class:
You decide to change RatNum to a Java interface instead, along with an implementation class called
IntFraction :
For each piece of code below from the old RatNum class, identify it and decide where it should go in
the new interface—plus—implementation-class design.
Interface + implementation 1
This piece of code is: (check all that apply) It should be put in:
abstraction function the interface
creator the implementation class
mutator both
Reading 14: Recursion
Objectives
be able to decompose a recursive problem into recursive steps and base cases
know when and how to use helper methods in recursion
understand the advantages and disadvantages of recursion vs. iteration
Recursion
In today’s class, we’re going to talk about how to implement a method, once you already have a
specification. We’ll focus on one particular technique, recursion. Recursion is not appropriate for every
problem, but it’s an important tool in your software development toolbox, and one that many people
scratch their heads over. We want you to be comfortable and competent with recursion, because you will
encounter it over and over. (That’s a joke, but it’s also true.)
Since you’ve taken 6.01, recursion is not completely new to you, and you have seen and written recursive
functions like factorial and fibonacci before. Today’s class will delve more deeply into recursion than you
may have gone before. Comfort with recursive implementations will be necessary for upcoming classes.
In a base case, we compute the result immediately given the inputs to the function call.
In a recursive step, we compute the result with the help of one or more recursive calls to this same
function, but with the inputs somehow reduced in size or complexity, closer to a base case.
Consider writing a function to compute factorial. We can define factorial in two different ways:
Iterative Recursive
Iterative Recursive
In the recursive implementation on the right, the base case is n = 0, where we compute and return the
result immediately: 0! is defined to be 1. The recursive step is n > 0, where we compute the result with
the help of a recursive call to obtain (n-1)!, then complete the computation by multiplying by n.
To visualize the execution of a recursive function, it is helpful to diagram the call stack of currently-
executing functions as the computation proceeds.
starts
calls calls calls calls returns to returns to returns
in factorial(3) factorial(2) factorial(1) factorial(0) factorial(1) factorial(2) factoria
main
factorial
n=0
factorial factorial
returns 1
n=1 n=1
factorial factorial factorial
returns 1
n=2 n=1 n=2
factorial factorial factorial factoria
returns 2
n=3 n=2 n=2 n=3
main factorial factorial factorial
returns
n=3 n=2 n=3
main factorial factorial main
x n=3 n=3 x
main factorial main
x n=3 x
main main
x x
main
x
In the diagram, we can see how the stack grows as main calls factorial and factorial then calls itself, until
factorial(0) does not make a recursive call. Then the call stack unwinds, each call to factorial returning
its answer to the caller, until factorial(3) returns to main .
Here’s an interactive visualization of factorial . You can step through the computation to see the
recursion in action. New stack frames grow down instead of up in this visualization.
You’ve probably seen factorial before, because it’s a common example for recursive functions. Another
common example is the Fibonacci series:
/**
* @param n >= 0
* @return the nth Fibonacci number
*/
public static int fibonacci(int n) {
if (n == 0 || n == 1) {
return 1; // base cases
} else {
return fibonacci(n-1) + fibonacci(n-2); // recursive step
}
}
Fibonacci is interesting because it has multiple base cases: n=0 and n=1. You can look at an interactive
visualization of Fibonacci. Notice that where factorial’s stack steadily grows to a maximum depth and
then shrinks back to the answer, Fibonacci’s stack grows and shrinks repeatedly over the course of the
computation.
check
Recursive Fibonacci
(missing explanation)
check
subsequences("gc")
What does subsequences("gc") return?
"g,c"
",g,c,gc"
",gc,g,c"
"g,c,gc"
(missing explanation)
check
base case, which is the simplest, smallest instance of the problem, that can’t be decomposed any
further. Base cases often correspond to emptiness – the empty string, the empty list, the empty set,
the empty tree, zero, etc.
recursive step, which decomposes a larger instance of the problem into one or more simpler or
smaller instances that can be solved by recursive calls, and then recombines the results of those
subproblems to produce the solution to the original problem.
It’s important for the recursive step to transform the problem instance into something smaller, otherwise
the recursion may never end. If every recursive step shrinks the problem, and the base case lies at the
bottom, then the recursion is guaranteed to be finite.
A recursive implementation may have more than one base case, or more than one recursive step. For
example, the Fibonacci function has two base cases, n=0 and n=1.
reading exercises
Recursive structure
Recursive methods have a base case and a recursive step. What other concepts from computer science
also have (the equivalent of) a base case and a recursive step?
proof by induction
regression testing
recessive functions
binary trees
(missing explanation)
check
Helper Methods
The recursive implementation we just saw for subsequences() is one possible recursive decomposition of
the problem. We took a solution to a subproblem – the subsequences of the remainder of the string after
removing the first character – and used it to construct solutions to the original problem, by taking each
subsequence and adding the first character or omitting it. This is in a sense a direct recursive
implementation, where we are using the existing specification of the recursive method to solve the
subproblems.
In some cases, it’s useful to require a stronger (or different) specification for the recursive steps, to make
the recursive decomposition simpler or more elegant. In this case, what if we built up a partial
subsequence using the initial letters of the word, and used the recursive calls to complete that partial
subsequence using the remaining letters of the word? For example, suppose the original word is
“orange”. We’ll both select “o” to be in the partial subsequence, and recursively extend it with all
subsequences of “range”; and we’ll skip “o”, use “” as the partial subsequence, and again recursively
extend it with all subsequences of “range”.
/**
* Return all subsequences of word (as defined above) separated by commas,
* with partialSubsequence prepended to each one.
*/
private static String subsequencesAfter(String partialSubsequence, String word) {
if (word.isEmpty()) {
// base case
return partialSubsequence;
} else {
// recursive step
return subsequencesAfter(partialSubsequence, word.substring(1))
+ ","
+ subsequencesAfter(partialSubsequence + word.charAt(0), word.substring(1));
}
}
This subsequencesAfter method is called a helper method. It satisfies a different spec from the original
subsequences , because it has a new parameter partialSubsequence . This parameter fills a similar role that a
local variable would in an iterative implementation. It holds temporary state during the evolution of the
computation. The recursive calls steadily extend this partial subsequence, selecting or ignoring each letter
in the word, until finally reaching the end of the word (the base case), at which point the partial
subsequence is returned as the only result. Then the recursion backtracks and fills in other possible
subsequences.
To finish the implementation, we need to implement the original subsequences spec, which gets the ball
rolling by calling the helper method with an initial value for the partial subsequence parameter:
Don’t expose the helper method to your clients. Your decision to decompose the recursion this way
instead of another way is entirely implementation-specific. In particular, if you discover that you need
temporary variables like partialSubsequence in your recursion, don’t change the original spec of your
method, and don’t force your clients to correctly initialize those parameters. That exposes your
implementation to the client and reduces your ability to change it in the future. Use a private helper
function for the recursion, and have your public method call it with the correct initializations, as shown
above.
reading exercises
Unhelpful 1
Louis Reasoner doesn’t want to use a helper method, so he tries to implement subsequences() by storing
partialSubsequence as a static variable instead of a parameter. Here is his implementation:
(missing explanation)
check
Unhelpful 2
Louis fixes that problem by making partialSubsequence public:
/**
* Requires: caller must set partialSubsequence to "" before calling subsequencesLouis().
*/
public static String partialSubsequence;
Alyssa P. Hacker throws up her hands when she sees what Louis did. Which of these statements are true
about his code?
(missing explanation)
check
Unhelpful 3
Louis gives in to Alyssa’s strenuous arguments, hides his static variable again, and takes care of
initializing it properly before starting the recursion:
Unfortunately a static variable is simply a bad idea in recursion. Louis’s solution is still broken. To
illustrate, let’s trace through the call subsequences("xy") . You can step through an interactive visualization
of this version to see what happens. It will produce these recursive calls to subsequencesLouis() :
1. subsequencesLouis("xy")
2. subsequencesLouis("y")
3. subsequencesLouis("")
4. subsequencesLouis("")
5. subsequencesLouis("y")
6. subsequencesLouis("")
7. subsequencesLouis("")
When each of these calls starts, what is the value of the static variable partialSubsequence?
1. subsequencesLouis("xy")
2. subsequencesLouis("y")
3. subsequencesLouis("")
4. subsequencesLouis("")
5. subsequencesLouis("y")
6. subsequencesLouis("")
7. subsequencesLouis("")
(missing explanation)
check
/**
* @param n integer to convert to string
* @param base base for the representation. Requires 2<=base<=10.
* @return n represented as a string of digits in the specified base, with
* a minus sign if n<0.
*/
public static String stringValue(int n, int base)
For example, stringValue(16, 10) should return "16" , and stringValue(16, 2) should return "10000" .
Let’s develop a recursive implementation of this method. One recursive step here is straightforward: we
can handle negative integers simply by recursively calling for the representation of the corresponding
positive integer:
This shows that the recursive subproblem can be smaller or simpler in more subtle ways than just the
value of a numeric parameter or the size of a string or list parameter. We have still effectively reduced
the problem by reducing it to positive integers.
The next question is, given that we have a positive n, say n=829 in base 10, how should we decompose it
into a recursive subproblem? Thinking about the number as we would write it down on paper, we could
either start with 8 (the leftmost or highest-order digit), or 9 (the rightmost, lower-order digit). Starting at
the left end seems natural, because that’s the direction we write, but it’s harder in this case, because we
would need to first find the number of digits in the number to figure out how to extract the leftmost digit.
Instead, a better way to decompose n is to take its remainder modulo base (which gives the rightmost
digit) and also divide by base (which gives the subproblem, the remaining higher-order digits):
Think about several ways to break down the problem, and try to write the recursive steps. You
want to find the one that produces the simplest, most natural recursive step.
It remains to figure out what the base case is, and include an if statement that distinguishes the base
case from this recursive step.
reading exercises
Implementing stringValue
Here is the recursive implementation of stringValue() with the recursive steps brought together but with
the base case still missing:
/**
* @param n integer to convert to string
* @param base base for the representation. Requires 2<=base<=10.
* @return n represented as a string of digits in the specified base, with
* a minus sign if n<0. No unnecessary leading zeros are included.
*/
public static String stringValue(int n, int base) {
if (n < 0) {
return "-" + stringValue(-n, base);
} else if (BASE CONDITION) {
BASE CASE
} else {
return stringValue(n/base, base) + "0123456789".charAt(n%base);
}
}
Which of the following can be substituted for the BASE CONDITION and BASE CASE to make the code correct?
(missing explanation)
check
Calling stringValue
Assuming the code is completed with one of the base cases identified in the previous problem, what does
stringValue(170, 16) do?
returns "AA"
returns "170"
returns "1010"
throws StringIndexOutOfBoundsException
infinite loop
(missing explanation)
check
Another cue is when the data you are operating on is inherently recursive in structure. We’ll see many
examples of recursive data a few classes from now, but for now let’s look at the recursive data found in
every laptop computer: its filesystem. A filesystem consists of named files. Some files are folders, which
can contain other files. So a filesystem is recursive: folders contain other folders which contain other
folders, until finally at the bottom of the recursion are plain (non-folder) files.
The Java library represents the file system using java.io.File . This is a recursive data type, in the sense
that f.getParentFile() returns the parent folder of a file f , which is a File object as well, and f.listFiles()
/**
* @param f a file in the filesystem
* @return the full pathname of f from the root of the filesystem
*/
public static String fullPathname(File f) {
if (f.getParentFile() == null) {
// base case: f is at the root of the filesystem
return f.getName();
} else {
// recursive step
return fullPathname(f.getParentFile()) + "/" + f.getName();
}
}
Recent versions of Java have added a new API, java.nio.Files and java.nio.Path , which offer a cleaner
separation between the filesystem and the pathnames used to name files in it. But the data structure is
still fundamentally recursive.
Reentrant Code
Recursion – a method calling itself – is a special case of a general phenomenon in programming called
reentrancy. Reentrant code can be safely re-entered, meaning that it can be called again even while a
call to it is underway. Reentrant code keeps its state entirely in parameters and local variables, and
doesn’t use static variables or global variables, and doesn’t share aliases to mutable objects with other
parts of the program, or other calls to itself.
Direct recursion is one way that reentrancy can happen. We’ve seen many examples of that during this
reading. The factorial() method is designed so that factorial(n-1) can be called even though factorial(n)
hasn’t yet finished working.
Mutual recursion between two or more functions is another way this can happen – A calls B, which calls
A again. Direct mutual recursion is virtually always intentional and designed by the programmer. But
unexpected mutual recursion can lead to bugs.
When we talk about concurrency later in the course, reentrancy will come up again, since in a concurrent
program, a method may be called at the same time by different parts of the program that are running
concurrently.
It’s good to design your code to be reentrant as much as possible. Reentrant code is safer from bugs
and can be used in more situations, like concurrency, callbacks, or mutual recursion.
Another reason to use recursion is to take more advantage of immutability. In an ideal recursive
implementation, all variables are final, all data is immutable, and the recursive methods are all pure
functions in the sense that they do not mutate anything. The behavior of a method can be understood
simply as a relationship between its parameters and its return value, with no side effects on any other
part of the program. This kind of paradigm is called functional programming, and it is far easier to
reason about than imperative programming with loops and variables.
In iterative implementations, by contrast, you inevitably have non-final variables or mutable objects that
are modified during the course of the iteration. Reasoning about the program then requires thinking about
snapshots of the program state at various points in time, rather than thinking about pure input/output
behavior.
One downside of recursion is that it may take more space than an iterative solution. Building up a stack
of recursive calls consumes memory temporarily, and the stack is limited in size, which may become a
limit on the size of the problem that your recursive implementation can solve.
The base case is missing entirely, or the problem needs more than one base case but not all the base
cases are covered.
The recursive step doesn’t reduce to a smaller subproblem, so the recursion doesn’t converge.
Which of the following could be put in place of the line marked TODO to
make hashCode() consistent with equals() ?
return 42;
return firstName.toUpperCase();
return lastName.toUpperCase().hashCode();
return firstName.hashCode() + lastName.hashCode();
(missing explanation)
check
Recall our definition: two objects are equal when they cannot be
distinguished by observation. With mutable objects, there are two
ways to interpret this:
when they cannot be distinguished by observation that doesn’t
change the state of the objects, i.e., by calling only observer,
producer, and creator methods. This is often strictly called
observational equality, since it tests whether the two objects
“look” the same, in the current state of the program.
when they cannot be distinguished by any observation, even state
changes. This interpretation allows calling any methods on the two
objects, including mutators. This is often called behavioral
equality, since it tests whether the two objects will “behave” the
same, in this and all future states.
We can check that the set contains the list we put in it, and it does:
set.contains(list) → true
But now we mutate the list:
list.add("goodbye");
set.contains(list) → false!
It’s worse than that, in fact: when we iterate over the members of the
set, we still find the list in there, but contains() says it’s not there!
If the set’s own iterator and its own contains() method disagree about
whether an element is in the set, then the set clearly is broken. You
can see this code in action on Online Python Tutor.
The lesson we should draw from this example is that equals() should
implement behavioral equality. In general, that means that two
references should be equals() if and only if they are aliases for the
same object. So mutable objects should just inherit equals() and
hashCode() from Object . For clients that need a notion of observational
equality (whether two mutable objects “look” the same in the current
state), it’s better to define a new method, e.g., similar() .
Java doesn’t follow this rule for its collections, unfortunately, leading
to the pitfalls that we saw above.
reading exercises
Bag
/** modify this bag by adding an occurrence of e, and return this bag */
public Bag<E> add(E e)
/** modify this bag by removing an occurrence of e (if any), and return thi
public Bag<E> remove(E e)
Which of the following expressions are true after all the the code has
been run?
b1.count("a") == 1
b1.count("b") == 1
b2.count("a") == 1
b2.count("b") == 1
b3.count("a") == 1
b3.count("b") == 1
b4.count("a") == 1
b4.count("b") == 1
(missing explanation)
check
Bag behavior
b1.equals(b2)
b1.equals(b3)
b1.equals(b4)
b2.equals(b3)
b2.equals(b4)
b3.equals(b1)
(missing explanation)
check
Bean bag
(missing explanation)
check
The object type implements equals() in the correct way, so that if you
create two Integer objects with the same value, they’ll be equals() to
each other:
x == y // returns false
reading exercises
Boxes
(missing explanation)
After executing a.put("c", 130) , what is the runtime type that is used to
represent the value 130 in the map?
(missing explanation)
check
Circles
Draw a snapshot diagram after the code above has executed. How
many HashMap objects are in your snapshot diagram?
(missing explanation)
(missing explanation)
check
Equals
return?
(missing explanation)
check
Unboxes
int i = a.get("c");
int j = b.get("c");
boolean isEqual = (i == j);
(missing explanation)
check
Summary
Equality should be an equivalence relation (reflexive, symmetric,
transitive).
Equality and hash code must be consistent with each other, so
that data structures that use hash tables (like HashSet and HashMap )
work properly.
The abstraction function is the basis for equality in immutable data
types.
Reference equality is the basis for equality in mutable data types;
this is the only way to ensure consistency over time and avoid
breaking rep invariants of hash tables.
Equality is one part of implementing an abstract data type, and we’ve
already seen how important ADTs are to achieving our three primary
objectives. Let’s look at equality in particular:
Ready for
Safe from bugs Easy to understand
change
Objectives
Introduction
In this reading we’ll look at recursively-defined types, how to specify
operations on such types, and how to implement them. Our main
example will be immutable lists.
Summary
Let’s review how recursive datatypes fit in with the main goals of this
course:
Ready for
Safe from bugs Easy to understand
change
Objectives
Introduction
Today’s reading introduces several ideas:
Grammars
To describe a sequence of symbols, whether they are bytes,
characters, or some other kind of symbol drawn from a fixed set, we
use a compact representation called a grammar.
or y or url .
Grammar Operators
concatenation
x ::= y z an x is a y followed by a z
repetition
x ::= y | z an x is a y or a z
You can also use additional operators which are just syntactic sugar
(i.e., they’re equivalent to combinations of the big three operators):
option (0 or 1 occurrence)
x ::= y? an x is a y or is the empty sentence
character classes
reading exercises
Reading a Grammar 1
S ::= (B C)* T
B ::= M+ | P B P
C ::= B | E+
(missing explanation)
(missing explanation)
(missing explanation)
check
Reading a Grammar 2
Which strings match the root nonterminal of this grammar?
aabcc
bbbc
aaaaaaaa
abc
abab
aac
(missing explanation)
check
Reading a Grammar 3
Which strings match the root nonterminal of this grammar?
(missing explanation)
check
Reading a Grammar 4
Which strings match the root nonterminal of this grammar?
aaaBBB
abababab
aBAbabAB
AbAbAbA
(missing explanation)
check
Example: URL
http://stanford.edu/
http://google.com/
This grammar represents the set of all URLs that consist of just a
two-part hostname, where each part of the hostname consists of 1 or
more letters. So http://mit.edu/ and http://yahoo.com/ would match, but
not http://ou812.com/ . Since it has only one nonterminal, a parse tree
for this URL grammar would look like the picture on the right.
The parse tree for this grammar is now shown at right. The tree has
more structure now. The leaves of the tree are the parts of the string
that have been parsed. If we concatenated the leaves together, we
would recover the original string. The hostname and word nonterminals
are labeling nodes of the tree whose subtrees match those rules in
the grammar. Notice that the immediate children of a nonterminal
node like hostname follow the pattern of the hostname rule, word '.' word .
http://didit.csail.mit.edu:4949/
Using the repetition operator, we could also write hostname like this:
reading exercises
Writing a Grammar
Suppose we want the url grammar to also match strings of the form:
https://websis.mit.edu/
ftp://ftp.athena.mit.edu/
ptth://web.mit.edu/
mailto:[email protected]
What could you put in place of TODO to match the desirable URLs
but not the undesirable ones?
word
'ftp' | 'http' | 'https'
('http' 's'?) | 'ftp'
('f' | 'ht') 'tp' 's'?
(missing explanation)
check
Now let’s look at grammars for some file formats. We’ll be using two
different markup languages that represent typographic style in text.
Here they are:
Markdown
This is _italic_.
HTML
For simplicity, our example HTML and Markdown grammars will only
specify italics, but other text styles are of course possible.
reading exercises
Recursive Grammars
Look at the markdown and html grammars above, and compare their
italic productions. Notice that not only do they differ in delimiters ( _
in one case, < > tags in the other), but also in the nonterminal that is
matched between those delimiters. One grammar is recursive; the
other grammar is not.
For each string below, if you match the specified grammar against it,
which letters are inside matches to the italic nonterminal? Your
answer should be some subset of the letters abcde .
markdown: a_b_c_d_e
(missing explanation)
html: a<i>b<i>c</i>d</i>e
(missing explanation)
check
Regular Expressions
A regular grammar has a special property: by substituting every
nonterminal (except the root one) with its righthand side, you can
reduce it down to a single production for the root, with only terminals
and operators on the right-hand side.
format is just
([^_]*|_[^_]*_)*
Regular expressions are also called regexes for short. A regex is far
less readable than the original grammar, because it lacks the
nonterminal names that documented the meaning of each
subexpression. But a regex is fast to implement, and there are
libraries in many programming languages that support regular
expressions.
http://([a-z]+\.)+[a-z]+(:[0-9]+)/
reading exercises
Regular Expressions
Consider the following regular expression:
[A-G]+(♭|♯)?
(missing explanation)
check
Match a URL:
Pattern regex = Pattern.compile("http://([a-z]+\\.)+[a-z]+(:[0-9]+)?/");
Matcher m = regex.matcher(string);
if (m.matches()) {
// then string is a url
}
Notice the backslashes in the URL and HTML tag examples. In the
URL example, we want to match a literal period . , so we have to first
escape it as \. to protect it from being interpreted as the regex
match-any-character operator, and then we have to further escape it
as \\. to protect the backslash from being interpreted as a Java
string escape character. In the HTML example, we have to escape
the quote mark " as \" to keep it from ending the string. The
frequency of backslash escapes makes regexes still less readable.
reading exercises
(missing explanation)
check
Context-Free Grammars
statement ::=
'{' statement* '}'
| 'if' '(' expression ')' statement ('else' statement)?
| 'for' '(' forinit? ';' expression? ';' forupdate? ')' statement
| 'while' '(' expression ')' statement
| 'do' statement 'while' '(' expression ')' ';'
| 'try' '{' statement* '}' ( catches | catches? 'finally' '{' statement* '}
| 'switch' '(' expression ')' '{' switchgroups '}'
| 'synchronized' '(' expression ')' '{' statement* '}'
| 'return' expression? ';'
| 'throw' expression ';'
| 'break' identifier? ';'
| 'continue' identifier? ';'
| expression ';'
| identifier ':' statement
| ';'
Summary
Machine-processed textual languages are ubiquitous in computer
science. Grammars are the most popular formalism for describing
such languages, and regular expressions are an important subclass
of grammars that can be expressed without recursion.
Ready for
Safe from bugs Easy to understand
change
Objectives
Parser Generators
A parser generator is a good tool that you should make part of your
toolbox. A parser generator takes a grammar as input and
automatically generates source code that can parse streams of
characters using the grammar.
A ParserLib Grammar
The code for the examples that follow can be found on GitHub as
fa16-ex18-parser-generators.
root is the entry point of the grammar. This is the nonterminal that the
whole input needs to match. We don’t have to call it root . When
loading the grammar into our program, we will tell the library which
nonterminal to use as the entry point.
This rule shows that ParserLib rules can have the alternation operator
|, the repetition operators * and + , and parentheses for grouping, in
the same way we’ve been using in the grammars reading. Optional
parts can be marked with ? , just like we did earlier, but this particular
grammar doesn’t use ? .
Note that the terminal text uses the notation [^<>] from before to
represent all characters except < and > .
In general, terminal symbols do not have to be a fixed string; they can
be a regular expression as in the example. For example, here are
some other terminal patterns we used in the URL grammar earlier in
the reading, now written in ParserLib syntax:
Whitespace
Consider the grammar shown below.
This grammar will accept an expression like 42+2+5 , but will reject a
similar expression that has any spaces between the numbers and the
+ signs. We could modify the grammar to allow white space around
the plus sign by modifying the production rule for sum like this:
The @skip whitespace notation indicates that any text matching the
whitespace nonterminal should be skipped in between the parts that
make up the definitions of sum root and primitive . Two things are
important to note. First, there is nothing special about whitespace . The
@skip directive works with any nonterminal or terminal defined in the
grammar. Second, note how the definition of number was intentionally
left outside the @skip block. This is because we want to accept
expressions like 42 + 2 + 5 , but we want to reject expressions like
4 2 + 2 + 5. In the rest of the text, we refer to this grammar as the
IntegerExpression grammar.
import lib6005.parser;
The second step is to define an Enum type that contains all the
terminals and non-terminals used by your grammar. This will tell the
compiler which definitions to expect in the grammar and will allow it to
check for any missing ones.
From within your code, you can create a parser by calling the compile
...
Parser<IntegerGrammar> parser = GrammarCompiler.compile(new File("IntegerEx
The code opens the file IntegerExpression.g and compiles it using the
GrammarCompiler into a Parser object. The compile method takes as a
second argument the name of the nonterminal to use as the entry
point of the grammar; root in the case of this example.
Assuming you don’t have any syntax errors in your grammar file, the
result will be a Parser object that can be used to parse text in either a
string or a file. Notice that the Parser is a generic type that is
parameterized by the enum you defined earlier.
System.out.println(tree.toString());
You can also try calling the method display() which will attempt to
open a browser window that will show you a visualization of your
parse tree. If for any reason it is not able to open the browser
window, the method will print a URL to the terminal which you can
copy and paste to your browser to view the visualization.
In the example code: Main.java lines 34-35, which use the enum in
lines 13-17.
reading exercises
Parse trees
Which of the following statements are true of a parse tree?
the root node of the tree corresponds to the starting symbol of the
grammar
the leaves of the tree correspond to terminals
the internal nodes of the tree correspond to nonterminals
only a grammar with recursive productions can generate a parse
tree
(missing explanation)
check
The first step is to learn how to traverse the parse tree. The ParseTree
object has four methods that you need to be most familiar with.
/**
* Returns the substring of the original string that corresponds to this pa
* @return String containing the contents of this parse tree.
*/
public String getContents()
/**
* Ordered list of all the children nodes of this ParseTree node.
* @return a List of all children of this ParseTree node, ordered by positi
*/
public List<ParseTree<Symbols>> children()
/**
* Tells you whether a node corresponds to a terminal or a non-terminal.
* If it is terminal, it won't have any children.
* @return true if it is a terminal value.
*/
public boolean isTerminal()
/**
* Get the symbol for the terminal or non-terminal corresponding to this pa
* @return T will generally be an Enum representing the different symbols
* in the grammar, so the return value will be one of those.
*/
public Symbols getName()
Additionally, you can query the ParseTree for all children that match a
particular production rule:
/**
* Get all the children of this PareseTree node corresponding to a particul
* @param name
* Name of the non-terminal corresponding to the desired production rule.
* @return
* List of children ParseTree objects that match that name.
*/
public List <ParseTree<Symbols>> childrenByName(Symbols name);
Note that like the Parser itself, the ParseTree is also parameterized by
the type of the Symbols , which is expected to be an enum type that lists
all the symbols in the grammar.
/**
* Traverse a parse tree, indenting to make it easier to read.
* @param node
* Parse tree to print.
* @param indent
* Indentation to use.
*/
void visitAll(ParseTree<IntegerGrammar> node, String indent){
if(node.isTerminal()){
System.out.println(indent + node.getName() + ":" + node.getContents
}else{
System.out.println(indent + node.getName());
for(ParseTree<IntegerGrammar> child: node){
visitAll(child, indent + " ");
}
}
}
}
IntegerExpression = Number(n:int)
+ Plus(left:IntegerExpression, right:IntegerExpression)
/**
* Function converts a ParseTree to an IntegerExpression.
* @param p
* ParseTree<IntegerGrammar> that is assumed to have been constructed
* @return
*/
IntegerExpression buildAST(ParseTree<IntegerGrammar> p){
switch(p.getName()){
/*
* Since p is a ParseTree parameterized by the type IntegerGrammar,
* returns an instance of the IntegerGrammar enum. This allows the
* that we have covered all the cases.
*/
case NUMBER:
/*
* A number will be a terminal containing a number.
*/
return new Number(Integer.parseInt(p.getContents()));
case PRIMITIVE:
/*
* A primitive will have either a number or a sum as child (in
* By checking which one, we can determine which case we are in
*/
if(p.childrenByName(IntegerGrammar.number).isEmpty()){
return buildAST(p.childrenByName(IntegerGrammar.sum).get(0)
}else{
return buildAST(p.childrenByName(IntegerGrammar.number).get
}
case SUM:
/*
* A sum will have one or more children that need to be summed
* Note that we only care about the children that are primitive
* some whitespace children which we want to ignore.
*/
boolean first = true;
IntegerExpression result = null;
for(ParseTree<IntegerGrammar> child : p.childrenByName(IntegerG
if(first){
result = buildAST(child);
first = false;
}else{
result = new Plus(result buildAST(child));
result = new Plus(result, buildAST(child));
}
}
if(first){ throw new RuntimeException("sum must have a non whit
return result;
case ROOT:
/*
* The root has a single sum child, in addition to having poten
*/
return buildAST(p.childrenByName(IntegerGrammar.sum).get(0));
case WHITESPACE:
/*
* Since we are always avoiding calling buildAST with whitespac
* the code should never make it here.
*/
throw new RuntimeException("You should never reach here:" + p);
}
/*
* The compiler should be smart enough to tell that this code is un
*/
throw new RuntimeException("You should never reach here:" + p);
}
The function is quite simple, and very much follows the structure of
the grammar. An important thing to note is that there is a very strong
assumption that the code will process a ParseTree that corresponds to
the grammar in IntegerExpression.g . If you feed it a different kind of
ParseTree, the code will likely fail with a RuntimeException , but it will
always terminate and will never return a null reference.
reading exercises
String to AST 1
Plus(Number(19))
Plus(19, 23, 18)
Plus(Plus(19, 23), 18)
Plus(Plus(Number(19), Number(23)), Number(18))
Plus(Number(19), Plus(Number(23), Number(18)))
(missing explanation)
check
String to AST 2
Plus(Plus(Number(1), Number(2)),
Plus(Number(3), Number(4)))
"(1+2)+(3+4)"
"1+2+3+4"
"(1+2)+3+4"
"(((1+2)))+(3+4)"
(missing explanation)
check
Handling errors
Several things can go wrong when parsing a file.
If you give any of these grammars to ParserLib and then try to use
them to parse a symbol, ParserLib will fail with an UnableToParse
exception listing the offending non-terminal.
There are some general techniques to eliminate left recursion; for our
purposes, the simplest approach will be to replace left recursion with
repetition ( * ), so the grammar above becomes:
Greediness. This is not an issue that you will run into in this class,
but it is a limitation of ParserLib you should be aware of. The
ParserLib parsers are greedy in that at every point they try to match
a maximal string for any rule they are currently considering. For
example, consider the following grammar.
root ::= ab threeb;
ab ::= 'a'*'b'*
threeb ::= 'bbb';
Summary
The topics of today’s reading connect to our three properties of good
software as follows:
Ready for
Safe from bugs Easy to understand
change
Objectives
Concurrency
Concurrency means multiple computations are happening at the
same time. Concurrency is everywhere in modern programming,
whether we like it or not:
shared memory
Shared memory. In the shared memory model of concurrency,
concurrent modules interact by reading and writing shared objects in
memory.
message passing
How can I have many concurrent threads with only one or two
processors in my computer? When there are more threads than
processors, concurrency is simulated by time slicing, which means
that the processor switches between threads. The figure on the right
shows how three threads T1, T2, and T3 might be time-sliced on a
machine that has only two actual processors. In the figure, time
proceeds downward, so at first one processor is running thread T1
and the other is running thread T2, and then the second processor
switches to run thread T3. Thread T2 simply pauses, until its next
time slice on the same processor or another processor.
Always implement the Runnable interface and use the new Thread(..)
constructor.
reading exercises
When you run a Java program (for example, using the Run button in
Eclipse), how many processors, processes, and threads are created
at first?
Processors:
Processes:
Threads:
(missing explanation)
check
(missing explanation)
How many new threads are run?
(missing explanation)
(missing explanation)
check
(missing explanation)
How many new threads are run?
(missing explanation)
check
But if we run this code, we discover frequently that the balance at the
end of the day is not 0. If more than one cashMachine() call is running at
the same time – say, on separate processors in the same computer –
then balance may not be zero at the end of the day. Why not?
Interleaving
Here’s one thing that can happen. Suppose two cash machines, A
and B, are both working on a deposit at the same time. Here’s how
the deposit() step typically breaks down into low-level processor
instructions:
add 1
A B
A add 1
B add 1
B write back the result
(balance=2)
A B
A add 1
B add 1
A write back the result
(balance=1)
The balance is now 1 – A’s dollar was lost! A and B both read the
balance at the same time, computed separate final balances, and
then raced to store back the new balance – which failed to take the
other’s deposit into account.
Race Condition
This is an example of a race condition. A race condition means that
the correctness of the program (the satisfaction of postconditions and
invariants) depends on the relative timing of events in concurrent
computations A and B. When this happens, we say “A is in a race
with B.”
Some interleavings of events may be OK, in the sense that they are
consistent with what a single, nonconcurrent process would produce,
but other interleavings produce wrong answers – violating
postconditions or invariants.
// version 1
private static void deposit() { balance = balance + 1; }
private static void withdraw() { balance = balance - 1; }
// version 2
private static void deposit() { balance += 1; }
private static void withdraw() { balance -= 1; }
// version 3
private static void deposit() { ++balance; }
private static void withdraw() { --balance; }
You can’t tell just from looking at Java code how the processor is
going to execute it. You can’t tell what the indivisible operations – the
atomic operations – will be. It isn’t atomic just because it’s one line of
Java. It doesn’t touch balance only once just because the balance
identifier occurs only once in the line. The Java compiler, and in fact
the processor itself, makes no commitments about what low-level
operations it will generate from your code. In fact, a typical modern
Java compiler produces exactly the same code for all three of these
versions!
Reordering
It’s even worse than that, in fact. The race condition on the bank
account balance can be explained in terms of different interleavings of
sequential operations on different processors. But in fact, when
you’re using multiple variables and multiple processors, you can’t
even count on changes to those variables appearing in the same
order.
ready = tmpr;
// <-- what happens if useAnswer() interleaves here?
// ready is set, but answer isn't.
answer = tmpa;
}
reading exercises
Interleaving 1
Here’s the buggy code from our earlier exercise where two new
threads are started:
spinning
measuring
cutting
spinning
measuring
measuring
spinning
cutting
measuring
spinning
spinning
measuring
(missing explanation)
check
Interleaving 2
Here’s the buggy code from our earlier exercise where no new
threads are started:
spinning
measuring
measuring
spinning
spinning
measuring
(missing explanation)
check
Race conditions 1
Suppose methodA and methodB run sequentially, i.e. first one and then
the other. What is the final value of x ?
(missing explanation)
check
Race conditions 2
1
2
5
6
10
30
150
(missing explanation)
check
get-balance
if balance >= 1 then withdraw 1
One lesson here is that you need to carefully choose the operations
of a message-passing model. withdraw-if-sufficient-funds would be a
better operation than just withdraw .
reading exercises
Testing concurrency
You’re running a JUnit test suite (for code written by somebody else),
and some of the tests are failing. You add System.out.println
statements to the one method called by all the failing test cases, in
order to display some of its local variables, and the test cases
suddenly start passing. Which of the following are likely reasons for
this?
(missing explanation)
check
Summary
Concurrency: multiple computations running simultaneously
Shared-memory & message-passing paradigms
Processes & threads
Process is like a virtual computer; thread is like a virtual
processor
Race conditions
When correctness of result (postconditions and invariants)
depends on the relative timing of events
Safe from bugs. Concurrency bugs are some of the hardest bugs
to find and fix, and require careful design to avoid.
Ready for
Safe from bugs Easy to understand
change
Objectives
We’ll talk about the first three ways in this reading, along with how to
make an argument that your code is threadsafe using those three
ideas. We’ll talk about the fourth approach, synchronization, in a later
reading.
Strategy 1: Confinement
Our first way of achieving thread safety is confinement. Thread
confinement is a simple idea: you avoid races on mutable data by
keeping that data confined to a single thread. Don’t give any other
threads the ability to read or write the data directly.
Let’s look at snapshot diagrams for this code. Hover or tap on each
step to update the diagram:
If you have static variables in your program, then you have to make
an argument that only one thread will ever use them, and you have to
document that fact clearly. Better, you should eliminate the static
variables entirely.
Here’s an example:
private PinballSimulator() {
System.out.println("created a PinballSimulator object");
}
This class has a race in the getInstance() method – two threads could
call it at the same time and end up creating two copies of the
PinballSimulator object, which we don’t want.
To fix this race using the thread confinement approach, you would
specify that only a certain thread (maybe the “pinball simulation
thread”) is allowed to call PinballSimulator.getInstance() . The risk here
is that Java won’t help you guarantee this.
In general, static variables are very risky for concurrency. They might
be hiding behind an innocuous function that seems to have no side-
effects or mutations. Consider this example:
This function stores the answers from previous calls in case they’re
requested again. This technique is called memoization, and it’s a
sensible optimization for slow functions like exact primality testing.
But now the isPrime method is not safe to call from multiple threads,
and its clients may not even realize it. The reason is that the HashMap
reading exercises
Factorial
starts
The call to computeFact(99) starts before the call to computeFact(100)
starts
The call to computeFact(100) finishes before the call to computeFact(99)
starts
The call to computeFact(99) finishes before the call to computeFact(100)
starts
(missing explanation)
check
PinballSimulator
// ...
The code has a race condition that invalidates the invariant that only
one simulator object is created.
(missing explanation)
(missing explanation)
(missing explanation)
check
Confinement
public class C {
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
threadA();
}
}).start();
amountA
amountB
amountToSpend
cashLeft
name
(missing explanation)
check
Strategy 2: Immutability
Our second way of achieving thread safety is by using immutable
references and data types. Immutability tackles the shared-mutable-
data cause of a race condition and solves it simply by making the
shared data not mutable.
no mutator methods
all fields are private and final
no representation exposure
no mutation whatsoever of mutable objects in the rep – not even
beneficent mutation
If you follow these rules, then you can be confident that your
immutable type will also be threadsafe.
reading exercises
Immutability
fields
creator implementations
client calls to creators
producer implementations
client calls to producers
observer implementations
client calls to observers
mutator implementations
client calls to mutators
(missing explanation)
check
String buffers are safe for use by multiple threads. The methods
are synchronized where necessary so that all the operations on
any particular instance behave as if they occur in some serial
order that is consistent with the order of the method calls made
by each of the individual threads involved.
Threadsafe Collections
Now we see a way to fix the isPrime() method we had earlier in the
reading:
Iterators are still not threadsafe. Even though method calls on the
collection itself ( get() , put() , add() , etc.) are now threadsafe, iterators
created from the collection are still not threadsafe. So you can’t use
iterator() , or the for loop syntax:
Even if you make lst into a synchronized list, this code still may have
a race condition, because another thread may remove the element
between the isEmpty() call and the get() call.
The synchronized map ensures that containsKey() , get() , and put() are
now atomic, so using them from multiple threads won’t damage the
rep invariant of the map. But those three operations can now
interleave in arbitrary ways with each other, which might break the
invariant that isPrime needs from the cache: if the cache maps an
integer x to a value f, then x is prime if and only if f is true. If the
cache ever fails this invariant, then we might return the wrong result.
reading exercises
(missing explanation)
(missing explanation)
(missing explanation)
check
A safety argument needs to catalog all the threads that exist in your
module or program, and the data that that they use, and argue which
of the four techniques you are using to protect against races for each
data object or variable: confinement, immutability, threadsafe data
types, or synchronization. When you use the last two, you also need
to argue that all accesses to the data are appropriately atomic – that
is, that the invariants you depend on are not threatened by
interleaving. We gave one of those arguments for isPrime above.
Here’s another rep for MyString that requires a little more care in the
argument:
Note that since this MyString rep was designed for sharing the array
between multiple MyString objects, we have to ensure that the sharing
doesn’t threaten its thread safety. As long as it doesn’t threaten the
MyString ’s immutability, however, we can be confident that it won’t
threaten the thread safety.
We also have to avoid rep exposure. Rep exposure is bad for any
data type, since it threatens the data type’s rep invariant. It’s also
fatal to thread safety.
Bad Safety Arguments
This code has a race condition in it. There is a crucial moment when
the rep invariant is violated, right after the edges map is mutated, but
just before the nodes set is mutated. Another operation on the graph
might interleave at that moment, discover the rep invariant broken,
and return wrong results. Even though the threadsafe set and map
data types guarantee that their own add() and put() methods are
atomic and noninterfering, they can’t extend that guarantee to
interactions between the two data structures. So the rep invariant of
Graph is not safe from race conditions. Just using immutable and
threadsafe-mutable data types is not sufficient when the rep invariant
depends on relationships between objects in the rep.
We’ll have to fix this with synchronization, and we’ll see how in a
future reading.
reading exercises
Safety arguments
/** @return the first character of this buffer, or "" if this buffer is
public String first() {
if (text.length() > 0) {
return String.valueOf(text.charAt(0));
} else {
} else {
return "";
}
}
}
toUpperCase
insert
toString
clear
first
(missing explanation)
check
Serializability
Look again at the code for the exercise above. We might also be
concerned that clear and insert could interleave such that a client
sees clear violate its postcondition.
A B
call sb.clear()
— clear returns
— insert returns
assert sb.toString()
.equals("")
Thread A’s assertion will fail, but not because clear violated its
postcondition. Indeed, when all the code in clear has finished running,
the postcondition is satisfied.
A B
call sb.clear()
— clear returns
A B
— insert returns
assert sb.toString()
.equals("")
reading exercises
Serializability
For each pair of concurrent calls and their result, does that outcome
violate serializability (and therefore demonstrate that MyStringBuffer is
not threadsafe)?
Violates serializability
Consistent with serializability
(missing explanation)
Violates serializability
Consistent with serializability
(missing explanation)
Violates serializability
Consistent with serializability
(missing explanation)
Violates serializability
Consistent with serializability
(missing explanation)
Violates serializability
Consistent with serializability
(missing explanation)
check
Summary
This reading talked about three major ways to achieve safety from
race conditions on shared mutable data:
Ready for
Safe from bugs Easy to understand
change
Objectives
Some of the operations with sockets are blocking: they block the
progress of a thread until they can return a result. Blocking makes
writing some code easier, but it also foreshadows a new class of
concurrency bugs we’ll soon contend with in depth: deadlocks.
In this pattern there are two kinds of processes: clients and servers.
A client initiates the communication by connecting to a server. The
client sends requests to the server, and the server sends replies
back. Finally, the client disconnects. A server might handle
connections from many clients concurrently, and clients might also
connect to multiple servers.
Many Internet applications work this way: web browsers are clients
for web servers, an email program like Outlook is a client for a mail
server, etc.
Network sockets
IP addresses
You can ask Google for your current IP address. In general, as you
carry around your laptop, every time you connect your machine to the
network it can be assigned a new IP address.
Hostnames
web.mit.edu is the name for MIT’s web server. You can translate
this name to an IP address yourself using dig , host , or nslookup on
the command line, e.g.:
google.com is exactly what you think it is. Try using one of the
commands above to find google.com ’s IP address. What do you
see?
localhost is a name for 127.0.0.1 . When you want to talk to a
server running on your own machine, talk to localhost .
Port numbers
Network sockets
I/O
Buffers
The data that clients and servers exchange over the network is sent
in chunks. These are rarely just byte-sized chunks, although they
might be. The sending side (the client sending a request or the server
sending a response) typically writes a large chunk (maybe a whole
string like “HELLO, WORLD!” or maybe 20 megabytes of video
data). The network chops that chunk up into packets, and each
packet is routed separately over the network. At the other end, the
receiver reassembles the packets together into a stream of bytes.
reading exercises
Fill in the blanks for the URL you should visit in your web browser to
talk to your server:
__A__://__B__:__C__
__A__
__B__
__C__
(missing explanation)
check
(missing explanation)
check
* see What if Dr. Seuss Did Technical Writing?, although the issue
described in the first stanza is no longer relevant with the
obsolescence of floppy disk drives
Streams
With sockets, remember that the output of one process is the input of
another process. If Alice and Bob have a socket connection, Alice
has an output stream that flows to Bob’s input stream, and vice
versa.
Blocking
Blocking means that a thread waits (without doing further work) until
an event occurs. We can use this term to describe methods and
method calls: if a method is a blocking method, then a call to that
method can block, waiting until some event occurs before it returns
to the caller.
We’ll see in the next reading that this waiting gives rise to the second
major kind of bug (the first was race conditions) in concurrent
programming: deadlock, where modules are waiting for each other
to do something, so none of them can make any progress. But that’s
for next time.
try (
// create new objects here that require cleanup after being used,
// and assign them to variables
) {
// code here runs with those variables
// cleanup happens automatically after the code completes
} catch(...) {
// you can include catch clauses if the code might throw exceptions
}
reading exercises
Network sockets 1
Alice has a connected socket with Bob. How does she send a
message to Bob?
(missing explanation)
check
Network sockets 2
Which of these is it necessary for a client to know in order to connect
to and communicate with a server?
server IP address
server hostname
server port number
server process name
wire protocol
(missing explanation)
check
echoSocket.getInputStream()
new BufferedReader(new InputStreamReader(...))
userInput = stdIn.readLine()
in.readLine()
(missing explanation)
new ServerSocket(...)
Socket clientSocket = serverSocket.accept()
inputLine = in.readLine()
e.getMessage()
(missing explanation)
check
When a thread calls readLine , all other threads block until readLine
returns
When a thread calls readLine , that thread blocks until readLine
returns
When a thread calls readLine , the call can be blocked and an
exception is thrown
BufferedReader has its own thread for readLine , which runs a block of
code passed in by the client
(missing explanation)
check
Wire protocols
Now that we have our client and server connected up with sockets,
what do they pass back and forth over those sockets?
Telnet client
telnet is a utility that allows you to make a direct network connection
to a listening server and communicate with it via a terminal interface.
Linux and Mac OS X should have telnet installed by default.
If you do not have telnet , you can install it via Control Panel →
Programs and Features → Turn Windows features on/off →
Telnet client. However, this version of telnet may be very hard to
use. If it does not show you what you’re typing, you will need to
turn on the localecho option.
HTTP
You'll be using Telnet on the problem set, so try these out now. User
input is shown in green, and for input to the telnet connection,
newlines (pressing enter) are shown with ↵ :
$ telnet www.eecs.mit.edu 80
Trying 18.62.0.96...
Connected to eecsweb.mit.edu.
Escape character is '^]'.
GET /↵
<!DOCTYPE html>
... lots of output ...
<title>Homepage | MIT EECS</title>
... lots more output ...
The GET command gets a web page. The / is the path of the page
you want on the site. So this command fetches the page at
http://www.eecs.mit.edu:80/ . Since 80 is the default port for HTTP, this is
equivalent to visiting http://www.eecs.mit.edu/ in your web browser.
The result is HTML code that your browser renders to display the
EECS homepage.
$ telnet web.mit.edu 80
Trying 18.9.22.69...
Connected to web.mit.edu.
Escape character is '^]'.
GET /aboutmit/ HTTP/1.1↵
Host: web.mit.edu↵
↵
HTTP/1.1 200 OK
Date: Tue, 31 Mar 2015 15:14:22 GMT
... more headers ...
This time, your request must end with a blank line. HTTP version 1.1
requires the client to specify some extra information (called headers)
with the request, and the blank line signals the end of the headers.
You will also more than likely find that telnet does not exit after
making this request — this time, the server keeps the connection
open so you can make another request right away. To quit Telnet
manually, type the escape character (probably Ctrl - ] ) to bring up the
telnet> prompt, and type quit :
SMTP
$ telnet dmz-mailsec-scanner-4.mit.edu 25
Trying 18.9.25.15...
Connected to dmz-mailsec-scanner-4.mit.edu.
Escape character is '^]'.
220 dmz-mailsec-scanner-4.mit.edu ESMTP Symantec
Messaging Gateway
HELO your-IP-address-here↵
250 2.0.0 dmz-mailsec-scanner-4.mit.edu says HELO to
your-ip-address:port
MAIL FROM: <[email protected]>↵
250 2.0.0 MAIL FROM accepted
RCPT TO: <[email protected]>↵
250 2.0.0 RCPT TO accepted
DATA↵
354 3.0.0 continue. finished with "\r\n.\r\n"
From: <[email protected]>↵
To: <[email protected]>↵
Subject: testing↵
This is a hand-crafted artisanal email.↵
.↵
250 2.0.0 OK 99/00-11111-22222222
QUIT↵
221 2.3.0 dmz-mailsec-scanner-4.mit.edu closing
connection
Connection closed by foreign host.
When designing a wire protocol, apply the same rules of thumb you
use for designing the operations of an abstract data type:
In order to precisely define for clients & servers what messages are
allowed by a protocol, use a grammar.
For example, here is a very small part of the HTTP 1.1 request
grammar from RFC 2616 section 5:
Using the grammar, we can see that in this example request from
earlier:
GET /aboutmit/ HTTP/1.1
Host: web.mit.edu
GET is the method : we’re asking the server to get a page for us.
/aboutmit/ is the request-uri : the description of what we want to
get.
HTTP/1.1 is the http-version .
We don’t have any message-body — and since the server didn’t wait
to see if we would send one, presumably that only applies for
other kinds of requests.
What are the postconditions? What action will the server take
based on a message? What server-side data will be mutated?
What reply will the server send back to the client?
reading exercises
Wire protocols 1
Which of the following tools could you use to speak HTTP with a web
server?
(missing explanation)
check
Wire protocols 2
The client can turn lights, identified by numerical IDs, on and off. The
client can also request help.
The server can report the status of the lights and provides arbitrary
help messages.
(missing explanation)
(missing explanation)
check
Testing client/server code
Remember that concurrency is hard to test and debug. We can’t
reliably reproduce race conditions, and the network adds a source of
latency that is entirely beyond our control. You need to design for
concurrency and argue carefully for the correctness of your code.
upperCaseLine(in, out);
upperCaseLine(in, out);
Summary
In the client/server design pattern, concurrency is inevitable: multiple
clients and multiple servers are connected on the network, sending
and receiving messages simultaneously, and expecting timely replies.
A server that blocks waiting for one slow client when there are other
clients waiting to connect to it or to receive replies will not make
those clients happy. At the same time, a server that performs
incorrect computations or returns bogus results because of
concurrent modification to shared mutable data by different clients will
not make anyone happy.
Ready for
Safe from bugs Easy to understand
change
Objectives
After reading the notes and examining the code for this class, you
should be able to use message passing (with synchronous queues)
instead of shared memory for communication between threads.
The message passing model has several advantages over the shared
memory model, which boil down to greater safety from bugs. In
message-passing, concurrent modules interact explicitly, by passing
messages through the communication channel, rather than implicitly
through mutation of shared data. The implicit interaction of shared
memory can too easily lead to inadvertent interaction, sharing and
manipulating data in parts of the program that don’t know they’re
concurrent and aren’t cooperating properly in the thread safety
strategy. Message passing also shares only immutable objects (the
messages) between modules, whereas shared memory requires
sharing mutable objects, which we have already seen can be a
source of bugs.
In an ordinary Queue :
put(e) blocks until it can add element e to the end of the queue (if
the queue does not have a size bound, put will not block).
take() blocks until it can remove and return the element at the
head of the queue, waiting until the queue is non-empty.
and remove() .
Each cash machine and each account is its own module, and modules
interact by sending messages to one another. Incoming messages
arrive on a queue.
get-balance
if balance >= 1 then withdraw 1
SquareQueue.java line 6
SquareQueue.java line 48
SquareQueue.java line 77
try {
// make a request
requests.put(42);
// ... maybe do something concurrently ...
// read the reply
System.out.println(replies.take());
} catch (InterruptedException ie) {
ie.printStackTrace();
}
}
It should not surprise us that this code has a very similar flavor to the
code for implementing message passing with sockets.
reading exercises
Rep invariant
REP_INVARIANT
check
Code review
The code above undergoes a code review and produces the following
comments. Evaluate the comments.
(missing explanation)
“ Squarer.start() has an infinite loop in it, so the thread will never stop
until the whole process is stopped.”
True
False
(missing explanation)
“ Squarer ” can have only one client using it, because if multiple clients
put requests in its input queue, their results will get mixed up in the
result queue.
True
False
(missing explanation)
check
Stopping
What if we want to shut down the Squarer so it is no longer waiting for
new inputs? In the client/server model, if we want the client or server
to stop listening for our messages, we close the socket. And if we
want the client or server to stop altogether, we can quit that process.
But here, the squarer is just another thread in the same process, and
we can’t “close” a queue.
with operations:
For each option below: is the snippet of code a correct outline for
how you would implement this in Java that takes maximum advantage
of static checking?
Yes
No
(missing explanation)
Yes
No
(missing explanation)
class SquareRequest {
private final String requestType;
public static final String INTEGER_REQUEST = "integer";
public static final String STOP_REQUEST = "stop";
...
}
Yes
No
(missing explanation)
check
reading exercises
Message passing
Leif Noad just started a new job working for a stock trading company:
(missing explanation)
check
(missing explanation)
check
Summary
Rather than synchronize with locks, message passing systems
synchronize on a shared communication channel, e.g. a stream or
a queue.
Correct today and correct in Communicating clearly with future Designed to accommodate
the unknown future. programmers, including future you. change without rewriting.
Objectives
Introduction
Earlier, we defined thread safety for a data type or a function as behaving correctly when used from
multiple threads, regardless of how those threads are executed, without additional coordination.
Here’s the general principle: the correctness of a concurrent program should not depend on
accidents of timing.
To achieve that correctness, we enumerated four strategies for making code safe for concurrency:
We talked about strategies 1-3 earlier. In this reading, we’ll finish talking about strategy 4, using
synchronization to implement your own data type that is safe for shared-memory concurrency.
Synchronization
The correctness of a concurrent program should not depend on accidents of timing.
Since race conditions caused by concurrent manipulation of shared mutable data are disastrous bugs —
hard to discover, hard to reproduce, hard to debug — we need a way for concurrent modules that share
memory to synchronize with each other.
Locks are one synchronization technique. A lock is an abstraction that allows at most one thread to own
it at a time. Holding a lock is how one thread tells other threads: “I’m working with this thing, don’t touch
it right now.”
release relinquishes ownership of the lock, allowing another thread to take ownership of it.
Using a lock also tells the compiler and processor that you’re using shared memory concurrently, so that
registers and caches will be flushed out to shared storage. This avoids the problem of reordering,
ensuring that the owner of a lock is always looking at up-to-date data.
Our first example of shared memory concurrency was a bank with cash machines. The diagram from that
example is on the right.
The bank has several cash machines, all of which can read and write the same account objects in
memory.
Of course, without any coordination between concurrent reads and writes to the account balances, things
went horribly wrong.
To solve this problem with locks, we can add a lock that protects each bank account. Now, before they
can access or update an account balance, cash machines must first acquire the lock on that account.
In the diagram to the right, both A and B are trying to access account 1. Suppose B acquires the lock
first. Then A must wait to read and write the balance until B finishes and releases the lock. This ensures
that A and B are synchronized, but another cash machine C is able to run independently on a different
account (because that account is protected by a different lock).
Deadlock
When used properly and carefully, locks can prevent race conditions. But then another problem rears its
ugly head. Because the use of locks requires threads to wait ( acquire blocks when another thread is
holding the lock), it’s possible to get into a a situation where two threads are waiting for each other —
and hence neither can make progress.
In the figure to the right, suppose A and B are making simultaneous transfers between two accounts in
our bank.
A transfer between accounts needs to lock both accounts, so that money can’t disappear from the
system. A and B each acquire the lock on their respective “from” account: A acquires the lock on account
1, and B acquires the lock on account 2. Now, each must acquire the lock on their “to” account: so A is
waiting for B to release the account 2 lock, and B is waiting for A to release the account 1 lock.
Stalemate! A and B are frozen in a “deadly embrace,” and accounts are locked up.
Deadlock occurs when concurrent modules are stuck waiting for each other to do something. A deadlock
may involve more than two modules: the signal feature of deadlock is a cycle of dependencies, e.g. A is
waiting for B which is waiting for C which is waiting for A. None of them can make progress.
You can also have deadlock without using any locks. For example, a message-passing system can
experience deadlock when message buffers fill up. If a client fills up the server’s buffer with requests, and
then blocks waiting to add another request, the server may then fill up the client’s buffer with results and
then block itself. So the client is waiting for the server, and the server waiting for the client, and neither
can make progress until the other one does. Again, deadlock ensues.
Deadlock (1 page)
You can see all the code for this example on GitHub: edit buffer example. You are not expected to read
and understand all the code. All the relevant parts are excerpted below.
Suppose we’re building a multi-user editor, like Google Docs, that allows multiple people to connect to it
and edit it at the same time. We’ll need a mutable datatype to represent the text in the document. Here’s
the interface; basically it represents a string with insert and delete operations:
EditBuffer.java
/**
* Modifies this by deleting a substring
* @param pos starting position of substring to delete
* (requires 0 <= pos <= current buffer length)
* @param len length of substring to delete
* (requires 0 <= len <= current buffer length - pos)
*/
public void delete(int pos, int len);
/**
* @return length of text sequence in this edit buffer
*/
public int length();
/**
* @return content of this edit buffer
*/
public String toString();
}
SimpleBuffer.java
The downside of this rep is that every time we do an insert or delete, we have to copy the entire string
into a new string. That gets expensive. Another rep we could use would be a character array, with space
at the end. That’s fine if the user is just typing new text at the end of the document (we don’t have to
copy anything), but if the user is typing at the beginning of the document, then we’re copying the entire
document with every keystroke.
A more interesting rep, which is used by many text editors in practice, is called a gap buffer. It’s basically
a character array with extra space in it, but instead of having all the extra space at the end, the extra
space is a gap that can appear anywhere in the buffer. Whenever an insert or delete operation needs to
be done, the datatype first moves the gap to the location of the operation, and then does the insert or
delete. If the gap is already there, then nothing needs to be copied — an insert just consumes part of the
gap, and a delete just enlarges the gap! Gap buffers are particularly well-suited to representing a string
that is being edited by a user with a cursor, since inserts and deletes tend to be focused around the
cursor, so the gap rarely moves.
GapBuffer.java
In a multiuser scenario, we’d want multiple gaps, one for each user’s cursor, but we’ll use a single gap for
now.
1. Specify. Define the operations (method signatures and specs). We did that in the EditBuffer interface.
2. Test. Develop test cases for the operations. See EditBufferTest in the provided code. The test suite
includes a testing strategy based on partitioning the parameter space of the operations.
3. Rep. Choose a rep. We chose two of them for EditBuffer , and this is often a good idea:
1. Implement a simple, brute-force rep first. It’s easier to write, you’re more likely to get it right,
and it will validate your test cases and your specification so you can fix problems in them before
you move on to the harder implementation. This is why we implemented SimpleBuffer before moving
on to GapBuffer . Don’t throw away your simple version, either — keep it around so that you have
something to test and compare against in case things go wrong with the more complex one.
2. Write down the rep invariant and abstraction function, and implement checkRep() . checkRep()
asserts the rep invariant at the end of every constructor, producer, and mutator method. (It’s
typically not necessary to call it at the end of an observer, since the rep hasn’t changed.) In fact,
assertions can be very useful for testing complex implementations, so it’s not a bad idea to also
assert the postcondition at the end of a complex method. You’ll see an example of this in
GapBuffer.moveGap() in the code with this reading.
In all these steps, we’re working entirely single-threaded at first. Multithreaded clients should be in the
back of our minds at all times while we’re writing specs and choosing reps (we’ll see later that careful
choice of operations may be necessary to avoid race conditions in the clients of your datatype). But get it
working, and thoroughly tested, in a sequential, single-threaded environment first.
This part of the reading is about how to do step 4. We already saw how to make a thread safety
argument, but this time, we’ll rely on synchronization in that argument.
5. Iterate. You may find that your choice of operations makes it hard to write a threadsafe type with the
guarantees clients require. You might discover this in step 1, or in step 2 when you write tests, or in
steps 3 or 4 when you implement. If that’s the case, go back and refine the set of operations your
ADT provides.
Locking
Locks are so commonly-used that Java provides them as a built-in language feature.
In Java, every object has a lock implicitly associated with it — a String , an array, an ArrayList , and every
class you create, all of their object instances have a lock. Even a humble Object has a lock, so bare
Object s are often used for explicit locking:
You can’t call acquire and release on Java’s intrinsic locks, however. Instead you use the synchronized
Synchronized regions like this provide mutual exclusion: only one thread at a time can be in a
synchronized region guarded by a given object’s lock. In other words, you are back in sequential
programming world, with only one thread running at a time, at least with respect to other synchronized
regions that refer to the same object.
Locks are used to guard a shared data variable, like the account balance shown here. If all accesses to
a data variable are guarded (surrounded by a synchronized block) by the same lock object, then those
accesses will be guaranteed to be atomic — uninterrupted by other threads.
Because every object in Java has a lock implicitly associated with it, you might think that simply owning
an object’s lock would prevent other threads from accessing that object. That is not the case. Acquiring
the lock associated with object obj using
Locks only provide mutual exclusion with other threads that acquire the same lock. All accesses to a data
variable must be guarded by the same lock. You might guard an entire collection of variables behind a
single lock, but all modules must agree on which lock they will all acquire and release.
Monitor pattern
When you are writing methods of a class, the most convenient lock is the object instance itself, i.e. this .
As a simple approach, we can guard the entire rep of a class by wrapping all accesses to the rep inside
synchronized (this) .
Note the very careful discipline here. Every method that touches the rep must be guarded with the lock —
even apparently small and trivial ones like length() and toString() . This is because reads must be guarded
as well as writes — if reads are left unguarded, then they may be able to see the rep in a partially-
modified state.
This approach is called the monitor pattern. A monitor is a class whose methods are mutually exclusive,
so that only one thread can be inside an instance of the class at a time.
Java provides some syntactic sugar for the monitor pattern. If you add the keyword synchronized to a
method signature, then Java will act as if you wrote synchronized (this) around the method body. So the
code below is an equivalent way to implement the synchronized SimpleBuffer :
/** SimpleBuffer is a threadsafe EditBuffer with a simple rep. */
public class SimpleBuffer implements EditBuffer {
private String text;
...
public SimpleBuffer() {
text = "";
checkRep();
}
public synchronized void insert(int pos, String ins) {
text = text.substring(0, pos) + ins + text.substring(pos);
checkRep();
}
public synchronized void delete(int pos, int len) {
text = text.substring(0, pos) + text.substring(pos+len);
checkRep();
}
public synchronized int length() {
return text.length();
}
public synchronized String toString() {
return text;
}
}
Notice that the SimpleBuffer constructor doesn’t have a synchronized keyword. Java actually forbids it,
syntactically, because an object under construction is expected to be confined to a single thread until it
has returned from its constructor. So synchronizing constructors should be unnecessary.
reading exercises
(missing explanation)
check
(missing explanation)
check
It is now safe to use sharedList from multiple threads without acquiring any locks… except! Which of the
following would require a synchronized(sharedList) { ... } block?
call isEmpty
call add
(missing explanation)
check
I heard you like locks so I acquired your lock so you can lock while you acquire
synchronized (obj) {
// ...
synchronized (obj) { // <-- uh oh, deadlock?
// ...
}
// <-- do we own the lock on obj?
}
yes
no
If we don’t deadlock, on the line “do we own the lock on obj”, does the thread own the lock on obj?
yes
no
we deadlocked
(missing explanation)
check
The same argument works for GapBuffer , if we use the monitor pattern to synchronize all its methods.
Note that the encapsulation of the class, the absence of rep exposure, is very important for making this
argument. If text were public:
then clients outside SimpleBuffer would be able to read and write it without knowing that they should first
acquire the lock, and SimpleBuffer would no longer be threadsafe.
Locking discipline
A locking discipline is a strategy for ensuring that synchronized code is threadsafe. We must satisfy two
conditions:
1. Every shared mutable variable must be guarded by some lock. The data may not be read or written
except inside a synchronized block that acquires that lock.
2. If an invariant involves multiple shared mutable variables (which might even be in different objects),
then all the variables involved must be guarded by the same lock. Once a thread acquires the lock,
the invariant must be reestablished before releasing the lock.
The monitor pattern as used here satisfies both rules. All the shared mutable data in the rep — which the
rep invariant depends on — are guarded by the same lock.
Atomic operations
Consider a find-and-replace operation on the EditBuffer datatype:
This method makes three different calls to buf — to convert it to a string in order to search for s , to
delete the old text, and then to insert t in its place. Even though each of these calls individually is atomic,
the findReplace method as a whole is not threadsafe, because other threads might mutate the buffer while
findReplace is working, causing it to delete the wrong region or put the replacement back in the wrong
place.
To prevent this, findReplace needs to synchronize with all other clients of buf .
It’s sometimes useful to make your datatype’s lock available to clients, so that they can use it to
implement higher-level atomic operations using your datatype.
So one approach to the problem with findReplace is to document that clients can use the EditBuffer ’s lock
to synchronize with each other:
The effect of this is to enlarge the synchronization region that the monitor pattern already put around the
individual toString , delete , and insert methods, into a single atomic region that ensures that all three
methods are executed without interference from other threads.
So is thread safety simply a matter of putting the synchronized keyword on every method in your program?
Unfortunately not.
First, you actually don’t want to synchronize methods willy-nilly. Synchronization imposes a large cost on
your program. Making a synchronized method call may take significantly longer, because of the need to
acquire a lock (and flush caches and communicate with other processors). Java leaves many of its
mutable datatypes unsynchronized by default exactly for these performance reasons. When you don’t
need synchronization, don’t use it.
Another argument for using synchronized in a more deliberate way is that it minimizes the scope of access
to your lock. Adding synchronized to every method means that your lock is the object itself, and every client
with a reference to your object automatically has a reference to your lock, that it can acquire and release
at will. Your thread safety mechanism is therefore public and can be interfered with by clients. Contrast
that with using a lock that is an object internal to your rep, and acquired appropriately and sparingly using
synchronized() blocks.
Finally, it’s not actually sufficient to sprinkle synchronized everywhere. Dropping synchronized onto a method
without thinking means that you’re acquiring a lock without thinking about which lock it is, or about
whether it’s the right lock for guarding the shared data access you’re about to do. Suppose we had tried
to solve findReplace ’s synchronization problem simply by dropping synchronized onto its declaration:
This wouldn’t do what we want. It would indeed acquire a lock — because findReplace is a static method,
it would acquire a static lock for the whole class that findReplace happens to be in, rather than an instance
object lock. As a result, only one thread could call findReplace at a time — even if other threads want to
operate on different buffers, which should be safe, they’d still be blocked until the single lock was free.
So we’d suffer a significant loss in performance, because only one user of our massive multiuser editor
would be allowed to do a find-and-replace at a time, even if they’re all editing different documents.
Worse, however, it wouldn’t provide useful protection, because other code that touches the document
probably wouldn’t be acquiring the same lock. It wouldn’t actually eliminate our race conditions.
The synchronized keyword is not a panacea. Thread safety requires a discipline — using confinement,
immutability, or locks to protect shared data. And that discipline needs to be written down, or maintainers
won’t know what it is.
So if we’re designing a datatype specifically for use in a concurrent system, we need to think about
providing operations that have better-defined semantics when they are interleaved. For example, it might
be better to pair EditBuffer with a Position datatype representing a cursor position in the buffer, or even a
Selection datatype representing a selected range. Once obtained, a Position could hold its location in the
text against the wash of insertions and deletions around it, until the client was ready to use that Position .
If some other thread deleted all the text around the Position , then the Position would be able to inform a
subsequent client about what had happened (perhaps with an exception), and allow the client to decide
what to do. These kinds of considerations come into play when designing a datatype for concurrency.
As another example, consider the ConcurrentMap interface in Java. This interface extends the existing Map
interface, adding a few key methods that are commonly needed as atomic operations on a shared
mutable map, e.g.:
With locking, deadlock happens when threads acquire multiple locks at the same time, and two threads
end up blocked while holding locks that they are each waiting for the other to release. The monitor
pattern unfortunately makes this fairly easy to do. Here’s an example.
And then think about what happens when two independent threads are repeatedly running:
// thread A // thread B
harry.friend(snape); snape.friend(harry);
harry.defriend(snape); snape.defriend(harry);
We will deadlock very rapidly. Here’s why. Suppose thread A is about to execute harry.friend(snape) , and
thread B is about to execute snape.friend(harry) .
Thread A acquires the lock on harry (because the friend method is synchronized).
Then thread B acquires the lock on snape (for the same reason).
They both update their individual reps independently, and then try to call friend() on the other object
— which requires them to acquire the lock on the other object.
So A is holding Harry and waiting for Snape, and B is holding Snape and waiting for Harry. Both threads
are stuck in friend() , so neither one will ever manage to exit the synchronized region and release the lock
to the other. This is a classic deadly embrace. The program simply stops.
The essence of the problem is acquiring multiple locks, and holding some of the locks while waiting for
another lock to become free.
Notice that it is possible for thread A and thread B to interleave such that deadlock does not occur:
perhaps thread A acquires and releases both locks before thread B has enough time to acquire the first
one. If the locks involved in a deadlock are also involved in a race condition — and very often they are —
then the deadlock will be just as difficult to reproduce or debug.
One way to prevent deadlock is to put an ordering on the locks that need to be acquired simultaneously,
and ensuring that all code acquires the locks in that order.
In our social network example, we might always acquire the locks on the Wizard objects in alphabetical
order by the wizard’s name. Since thread A and thread B are both going to need the locks for Harry and
Snape, they would both acquire them in that order: Harry’s lock first, then Snape’s. If thread A gets
Harry’s lock before B does, it will also get Snape’s lock before B does, because B can’t proceed until A
releases Harry’s lock again. The ordering on the locks forces an ordering on the threads acquiring them,
so there’s no way to produce a cycle in the waiting-for graph.
Here’s what the code might look like:
(Note that the decision to order the locks alphabetically by the person’s name would work fine for this
book, but it wouldn’t work in a real life social network. Why not? What would be better to use for lock
ordering than the name?)
Although lock ordering is useful (particularly in code like operating system kernels), it has a number of
drawbacks in practice.
First, it’s not modular — the code has to know about all the locks in the system, or at least in its
subsystem.
Second, it may be difficult or impossible for the code to know exactly which of those locks it will need
before it even acquires the first one. It may need to do some computation to figure it out. Think about
doing a depth-first search on the social network graph, for example — how would you know which
nodes need to be locked, before you’ve even started looking for them?
A more common approach than lock ordering, particularly for application programming (as opposed to
operating system or device driver programming), is to use coarser locking — use a single lock to guard
many object instances, or even a whole subsystem of a program.
For example, we might have a single lock for an entire social network, and have all the operations on any
of its constituent parts synchronize on that lock. In the code below, all Wizard s belong to a Castle , and we
just use that Castle object’s lock to synchronize:
reading exercises
Deadlock
In the code below three threads 1, 2, and 3 are trying to acquire locks on objects alpha , beta , and gamma .
For each of the scenarios below, determine whether the system is in deadlock if the threads are currently
on the indicated lines of code.
Scenario A
Thread 3 finished
deadlock
not deadlock
(missing explanation)
Scenario B
Thread 1 finished
Thread 2 blocked on synchronized (beta)
deadlock
not deadlock
(missing explanation)
Scenario C
deadlock
not deadlock
(missing explanation)
Scenario D
Thread 2 finished
Thread 3 blocked on 2nd synchronized (gamma)
deadlock
not deadlock
(missing explanation)
check
Locked out
(missing explanation)
check
Safety. Does the concurrent program satisfy its invariants and its specifications? Races in accessing
mutable data threaten safety. Safety asks the question: can you prove that some bad thing never
happens?
Liveness. Does the program keep running and eventually do what you want, or does it get stuck
somewhere waiting forever for events that will never happen? Can you prove that some good thing
eventually happens?
Deadlocks threaten liveness. Liveness may also require fairness, which means that concurrent modules
are given processing capacity to make progress on their computations. Fairness is mostly a matter for
the operating system’s thread scheduler, but you can influence it (for good or for ill) by setting thread
priorities.
Concurrency in practice
What strategies are typically followed in real programs?
Library data structures either use no synchronization (to offer high performance to single-threaded
clients, while leaving it to multithreaded clients to add locking on top) or the monitor pattern.
Mutable data structures with many parts typically use either coarse-grained locking or thread
confinement. Most graphical user interface toolkits follow one of these approaches, because a
graphical user interface is basically a big mutable tree of mutable objects. Java Swing, the graphical
user interface toolkit, uses thread confinement. Only a single dedicated thread is allowed to access
Swing’s tree. Other threads have to pass messages to that dedicated thread in order to access the
tree.
Search often uses immutable datatypes. Our Boolean formula satisfiability search would be easy to
make multithreaded, because all the datatypes involved were immutable. There would be no risk of
either races or deadlocks.
Operating systems often use fine-grained locks in order to get high performance, and use lock
ordering to deal with deadlock problems.
We’ve omitted one important approach to mutable shared data because it’s outside the scope of this
course, but it’s worth mentioning: a database. Database systems are widely used for distributed
client/server systems like web applications. Databases avoid race conditions using transactions, which
are similar to synchronized regions in that their effects are atomic, but they don’t have to acquire locks,
though a transaction may fail and be rolled back if it turns out that a race occurred. Databases can also
manage locks, and handle locking order automatically. For more about how to use databases in system
design, 6.170 Software Studio is strongly recommended; for more about how databases work on the
inside, take 6.814 Database Systems.
And if you’re interested in the performance of concurrent programs — since performance is often one of
the reasons we add concurrency to a system in the first place — then 6.172 Performance Engineering is
the course for you.
Summary
Producing a concurrent program that is safe from bugs, easy to understand, and ready for change
requires careful thinking. Heisenbugs will skitter away as soon as you try to pin them down, so debugging
simply isn’t an effective way to achieve correct threadsafe code. And threads can interleave their
operations in so many different ways that you will never be able to test even a small fraction of all
possible executions.
Make thread safety arguments about your datatypes, and document them in the code.
Acquiring a lock allows a thread to have exclusive access to the data guarded by that lock, forcing
other threads to block — as long as those threads are also trying to acquire that same lock.
The monitor pattern guards the rep of a datatype with a single lock that is acquired by every method.
Ready for
Safe from bugs Easy to understand
change
Objectives
In this reading you’ll learn a design pattern for implementing functions that
operate on sequences of elements, and you’ll see how treating functions
themselves as first-class values that we can pass around and manipulate in
our programs is an especially powerful idea.
Map/filter/reduce
Lambda expressions
Functional objects
Higher-order functions
Introduction: an example
Suppose we’re given the following problem: write a method that finds the
words in the Java files in your project.
Following good practice, we break it down into several simpler steps and
write a method for each one:
find all the files in the project, by scanning recursively from the project’s
root folder
restrict them to files with a particular suffix, in this case .java
Writing the individual methods for these substeps, we’ll find ourselves
writing a lot of low-level iteration code. For example, here’s what the
recursive traversal of the project folder might look like:
/**
* Find all the files in the filesystem subtree rooted at folder.
* @param folder root of subtree, requires folder.isDirectory() == true
* @return list of all ordinary files (not folders) that have folder as
* their ancestor
*/
public static List<File> allFilesIn(File folder) {
List<File> files = new ArrayList<>();
for (File f : folder.listFiles()) {
if (f.isDirectory()) {
files.addAll(allFilesIn(f));
} else if (f.isFile()) {
files.add(f);
}
}
return files;
}
And here’s what the filtering method might look like, which restricts that file
list down to just the Java files (imagine calling this like
onlyFilesWithSuffix(files, ".java") ):
/**
* Filter a list of files to those that end with suffix.
* @param files list of files (all non-null)
* @param suffix string to test
* @return a new list consisting of only those files whose names end with
* suffix
*/
public static List<File> onlyFilesWithSuffix(List<File> files, String suffix) {
List<File> result = new ArrayList<>();
for (File f : files) {
if (f.getName().endsWith(suffix)) {
result.add(f);
}
}
return result;
return result;
}
Along the way, we’ll also see an important Big Idea: functions as “first-
class” data values, meaning that they can be stored in variables, passed as
arguments to functions, and created dynamically like other values.
Iterator abstraction
But this code depends on the size and get methods of List , which might be
different in another data structure. Using an iterator abstracts away the
details:
Now the loop will be identical for any type that provides an Iterator . There
is, in fact, an interface for such types: Iterable . Any Iterable can be used
with Java’s enhanced for statement — for (File f : files) — and under the
hood, it uses an iterator.
Map/filter/reduce abstraction
Sequences
We’ll have three operations for sequences: map, filter, and reduce. Let’s
look at each one in turn, and then look at how they work together.
Map
Map applies a unary function to each element in the sequence and returns a
new sequence containing the results, in the same order:
Functions as values
Let’s pause here for a second, because we’re doing something unusual with
functions. The map function takes a reference to a function as its first
argument — not to the result of that function. When we wrote
we didn’t call sqrt (like sqrt(25) is a call), instead we just used its name. In
Python, the name of a function is a reference to an object representing that
function. You can assign that object to another variable if you like, and it still
behaves like sqrt :
We’ve seen how to use built-in library functions as first-class values; how do
we make our own? One way is using a familiar function definition, which
gives the function a name:
>>> def powerOfTwo(k):
... return 2**k
...
>>> powerOfTwo(5)
32
>>> map(powerOfTwo, [1, 2, 3, 4])
[2, 4, 8, 16]
When you only need the function in one place, however — which often
comes up in programming with functions — it’s more convenient to use a
lambda expression:
lambda k: 2**k
Guido Von Rossum, the creator of Python, wrote a blog post about the
design principle that led not only to first-class functions in Python, but first-
class methods as well: First-class Everything.
Map is useful even if you don’t care about the return value of the function.
When you have a sequence of mutable objects, for example, you can map a
mutator operation over them:
map(IOBase.close, streams) # closes each stream on the list
map(Thread.join, threads) # waits for each thread to finish
Some versions of map (including Python’s built-in map ) also support mapping
functions with multiple arguments. For example, you can add two lists of
numbers element-wise:
reading exercises
map 1
1
[1, 1, 1]
[1, 2, 3]
[ [1], [1], [1] ]
[ [1], [2], [3] ]
error
(missing explanation)
check
map 2
1
[1, 1, 1]
[1, 2, 3]
[ [1], [1], [1] ]
[ [1], [2], [3] ]
error
(missing explanation)
check
map 3
1
[1, 1, 1]
[1, 2, 3]
[ [1], [1], [1] ]
[ [1], [2], [3] ]
error
(missing explanation)
check
map 4
'a b c'
['a b c']
['a', 'b', 'c']
[ ['a', 'b', 'c'] ]
something else
error
(missing explanation)
check
map 5
'a b c'
['a b c']
['a', 'b', 'c']
[ ['a', 'b', 'c'] ]
something else
error
(missing explanation)
check
Filter
Our next important sequence operation is filter, which tests each element
with a unary predicate. Elements that satisfy the predicate are kept; those
that don’t are removed. A new list is returned; filter doesn’t modify its input
list.
Python examples:
filter 1
Given:
x1 = {'x': 1}
y2 = {'y': 2}
x3_y4 = {'x': 3, 'y': 4}
(missing explanation)
check
filter 2
Again given:
x1 = {'x': 1}
y2 = {'y': 2}
x3_y4 = {'x': 3, 'y': 4}
0
False
None
[]
[ {}, {}, {} ]
[ None, None, None ]
(missing explanation)
check
filter 3
''
'ab'
['ab']
['a', 'b']
['a', '', 'b', '']
['a', '1', 'b', '2']
(missing explanation)
check
filter 4
'A1B2'
'a1b2'
['A1B2']
['a1b2']
['A', '1', 'B', '2']
['a', '1', 'b', '2']
(missing explanation)
check
Reduce
Our final operator, reduce, combines the elements of the sequence
together, using a binary function. In addition to the function and the list, it
also takes an initial value that initializes the reduction, and that ends up
being the return value if the list is empty.
reduce : (F × E → F) × Seq<E> × F → F
reduce(f, list, init) combines the elements of the list from left to right, as
follows:
result0 = init
result1 = f(result0, list[0])
result2 = f(result1, list[1])
...
resultn = f(resultn-1, list[n-1])
There are two design choices in the reduce operation. First is whether to
require an initial value. In Python’s reduce function, the initial value is
optional, and if you omit it, reduce uses the first element of the list as its
initial value. So you get behavior like this instead:
This makes it easier to use reducers like max , which have no well-defined
initial value:
>>> reduce(max, [5, 8, 3, 1])
8
The second design choice is the order in which the elements are
accumulated. For associative operators like add and max it makes no
difference, but for other operators it can. Python’s reduce is also called
fold-left in other programming languages, because it combines the
sequence starting from the left (the first element). Fold-right goes in the
other direction:
fold-right : (E × F → F) × Seq<E> × F → F
result0 = init
result1 = f(list[n-1], result0)
result2 = f(list[n-2], result1)
...
resultn = f(list[0], resultn-1)
Here’s a diagram of two ways to reduce: from the left or from the right:
fold-left : (F × E → F) × Seq<E> × F → F
fold-left(-, [1, 2, 3], 0) = -6
fold-right : (E × F → F) × Seq<E> × F → F
fold-right(-, [1, 2, 3], 0) = 2
The return type of the reduce operation doesn’t have to match the type of
the list elements. For example, we can use reduce to glue together a
sequence into a string:
>>> reduce(lambda s,x: s+str(x), [1, 2, 3, 4], '')
'1234'
def flatten(list):
return reduce(operator.concat, list, [])
More examples
This code uses the convenient Python generator method range(a,b) , which
generates a list of integers from a to b-1. In map/filter/reduce programming,
this kind of method replaces a for loop that indexes from a to b.
cameras is a sequence (a list of rows, where each row has the data for
one camera)
pixels is a map (extracting just the pixels field from the row)
max is a reduce
reading exercises
reduce 1
returns False
returns True iff all the values in the list are strings
returns True iff all the values in the list are the string 'True'
returns True iff some value in the list is the string 'True'
(missing explanation)
check
reduce 2
Try these in the Python interpreter if you’re not sure!
(missing explanation)
(missing explanation)
check
def fileEndsWith(suffix):
return lambda file: file.getName().endsWith(suffix)
filter(fileEndsWith(".java"), files)
Now let’s use map, filter, and flatten (which we defined above using reduce)
to recursively traverse the folder tree:
def allFilesIn(folder):
children = folder.listFiles()
subfolders = filter(File.isDirectory, children)
descendants = flatten(map(allFilesIn, subfolders))
return descendants + filter(File.isFile, children)
The first line gets all the children of the folder, which might look like this:
The second line is the key bit: it filters the children for just the subfolders,
and then recursively maps allFilesIn against this list of subfolders! The
result might look like this:
This actually looks like a single map operation where we want to apply
three functions to the elements, so let’s pause to create another useful
higher-order function: composing functions together.
def compose(f, g):
"""Requires that f and g are functions, f:A->B and g:B->C.
Returns a function A->C by composing f with g."""
return lambda x: g(f(x))
Better, since we already have three functions to apply, let’s design a way to
compose an arbitrary chain of functions:
def chain(funcs):
"""Requires funcs is a list of functions [A->B, B->C, ..., Y->Z].
Returns a fn A->Z that is the left-to-right composition of funcs."""
return reduce(compose, funcs)
Since this map will produce a list of lists of lines (one list of lines for each
file), let’s flatten it to get a single line list, ignoring file boundaries:
And we’re done, we have our list of all words in the project’s Java files! As
promised, the control statements have disappeared.
reading exercises
map/filter/reduce
This Python function accepts a list of numbers and computes the product of
all the odd numbers:
def productOfOdds(list):
result = 1
for x in list:
if x % 2 == 1:
result *= x
return result
def productOfOdds(list):
return reduce(r_func, filter(f_func, map(m_func, list)))
Where m_func , f_func , and r_func are each one of the following:
A. list H. def is_odd(x):
return x % 2 == 1
B. x I. x_is_odd = x % 2 == 1
C. y J. def odd_or_identity(x):
return x if is_odd(x) else 1
G. def modulus_tester(i): N. x * y
return lambda x: x % 2 == i
def productOfOdds(list):
return reduce(r_func, filter(f_func, map(m_func, list)))
Yes
No
(missing explanation)
E + F + L: reduce(product, filter(always_true, map(identity, list)))
Yes
No
(missing explanation)
Yes
No
(missing explanation)
Yes
No
(missing explanation)
Yes
No
(missing explanation)
Yes
No
(missing explanation)
check
First-class functions in Java
We’ve seen what first-class functions look like in Python; how does this all
work in Java?
In Java, the only first-class values are primitive values (ints, booleans,
characters, etc.) and object references. But objects can carry functions with
them, in the form of methods. So it turns out that the way to implement a
first-class function, in an object-oriented programming language like Java
that doesn’t support first-class functions directly, is to use an object with a
method representing the function.
In a future class, we’ll see KeyListener objects that you register with the
graphical user interface toolkit to get keyboard events. They act as a
bundle of several functions, keyPressed(KeyEvent) , keyReleased(KeyEvent) , etc.
There’s no magic here: Java still doesn’t have first-class functions. So you
can only use a lambda when the Java compiler can verify two things:
1. It must be able to determine the type of the functional object the lambda
will create. In this example, the compiler sees that the Thread constructor
takes a Runnable , so it will infer that the type must be Runnable .
object.
/**
* Apply a function to every element of a list.
* @param f function to apply
* @param list list to iterate over
* @return [f(list[0]), f(list[1]), ..., f(list[n-1])]
*/
public static <T,R> List<R> map(Function<T,R> f, List<T> list) {
List<R> result = new ArrayList<>();
for (T t : list) {
result.add(f.apply(t));
}
return result;
}
And here’s an example of using map; first we’ll write it using the familiar
syntax:
In the Java Tutorials, you can read more about method references if you
want the details.
Using a method reference (vs. calling it) in Java serves the same purpose
as referring to a function by name (vs. calling it) in Python.
Map/filter/reduce in Java
The abstract sequence type we defined above exists in Java as Stream ,
Collection types like List and Set provide a stream() operation that returns a
Stream for the collection, and there’s an Arrays.stream function for creating a
Stream from an array.
Here’s endsWith :
You can compare all three versions: the familiar Java implementation,
Python with map/filter/reduce, and Java with map/filter/reduce.
/**
* Compose two functions.
* @param f function A->B
* @param g function B->C
* @return new function A->C formed by composing f with g
*/
public static <A,B,C> Function<A,C> compose(Function<A,B> f,
Function<B,C> g) {
return t -> g.apply(f.apply(t));
// --or--
// return new Function<A,C>() {
// public C apply(A t) { return g.apply(f.apply(t)); }
// };
}
It turns out that we can’t write chain in strongly-typed Java, because List s
/**
* Compose a chain of functions.
* @param funcs list of functions A->A to compose
* @return function A->A made by composing list[0] ... list[n-1]
*/
public static <A> Function<A,A> chain(List<Function<A,A>> funcs) {
return funcs.stream().reduce(Function.identity(), Function::compose);
}
Our Python version didn’t use an initial value in the reduce , it required a non-
empty list of functions. In Java, we’ve provided the identity function (that is,
f(t) = t) as the identity value for the reduction.
reading exercises
Comparator<Dog>
We have several Dog objects, and we’d like to keep a collection of them,
sorted by how loud they bark.
functional object
lambda expression
method reference
something we can’t do in Java
something we can’t do in Python
(missing explanation)
check
Which of these would create a TreeSet to sort our dogs from quietest bark
to loudest?
Correct
Incorrect
(missing explanation)
Correct
Incorrect
(missing explanation)
(missing explanation)
Correct
Incorrect
(missing explanation)
check
Summary
This reading is about modeling problems and implementing systems with
immutable data and operations that implement pure functions, as opposed
to mutable data and operations with side effects. Functional programming
is the name for this style of programming.
Ready for
Safe from bugs Easy to understand
change
Objectives
View Tree
a graphical user interface with views
labeled
This leads to the first important pattern we’ll talk about today: the
view tree. Views are arranged into a hierarchy of containment, in
which some views contain other views. Typical containers are
windows, panels, and toolbars. The view tree is not just an arbitrary
hierarchy, but is in fact a spatial one: child views are nested inside
their parent’s bounding box.
Input. Views can have input handlers, and the view tree controls how
mouse and keyboard input is processed. More on this in a moment.
Layout. The view tree controls how the views are laid out on the
screen, i.e. how their bounding boxes are assigned. An automatic
layout algorithm automatically calculates positions and sizes of views.
Specialized containers (like JSplitPane , JScrollPane ) do layout
themselves. More generic containers ( JPanel , JFrame ) delegate layout
decisions to a layout manager (e.g. GroupLayout , BorderLayout , BoxLayout ,
…).
reading exercises
View Tree
JComponent = JLabel(label:String)
+ JPanel(children:JComponent[])
+ ...
Let’s fill in some more of the “…” on the righthand side of this
definition. To answer the questions below, you may need to look at
the documentation for the particular classes.
Which is the best description of JButton for the righthand side of the
definition?
JButton()
JButton(label:String)
JButton(children:JComponent[])
(missing explanation)
(missing explanation)
(missing explanation)
check
Input Handling
Input is handled somewhat differently in GUIs than we’ve been
handling it in parsers and servers. In those systems, we’ve seen a
single parser that peels apart the input and decides how to direct it to
different modules of the program. If a GUI were written that way, it
might look like this (in pseudocode):
while (true) {
read mouse click
if (clicked on Thrash button) doThrash();
else if (clicked on textbox) doPlaceCursor();
else if (clicked on a name in the listbox) doSelectItem();
...
}
In a GUI, we don’t directly write this kind of method, because it’s not
modular – it mixes up responsibilities for button, listbox, and textbox
all in one place. Instead, GUIs exploit the spatial separation provided
by the view tree to provide functional separation as well. Mouse
clicks and keyboard events are distributed around the view tree,
depending on where they occur.
In this case, the mouse is the event source, and the events are
changes in the state of the mouse: its x,y position or the state of its
buttons (whether they are pressed or released). Events often include
additional information about the transition (such as the x,y position of
mouse), which might be bundled into an event object or passed as
parameters.
When an event occurs, the event source distributes it to all
subscribed listeners, by calling their callback methods.
The control flow through a graphical user interface proceeds like this:
The last part – listeners return to the event loop as fast as possible –
is very important, because it preserves the responsiveness of the
user interface. We’ll come back to this later in the reading.
The Listener pattern isn’t just used for low-level input events like
mouse clicks and keyboard keypresses. Many GUI objects generate
their own higher-level events, often as a result of some combination
of low-level input events. For example:
reading exercises
Listeners
Put the following items in order according to when they would happen
during the execution of a Swing graphical user interface.
launchButton.addActionListener(launchMissiles);
Mouse click event on the launch button is handled by the Swing event
loop
(missing explanation)
check
But we’re still missing the application itself – the backend that
represents the data and logic that the user interface is showing and
editing. (Why do we want to separate this from the user interface?)
reading exercises
Model-View-Controller
“All the data is kept in JTextField objects in the window, and other
classes can look it up just by getting a reference to the JTextField and
calling getText() .”
True
False
(missing explanation)
“If the view listens for ball-moved events from the pinball board, then
we can have multiple views showing the same board.”
True
False
(missing explanation)
True
False
(missing explanation)
“Looks like the model is the best place to store the name of the
pinball board.”
True
False
(missing explanation)
check
The view tree is a big meatball of shared state, and the Swing
specification doesn’t guarantee that there’s any lock protecting it.
Instead the view tree is confined to the event-dispatch thread, by
specification. So it’s ok to access view objects from the event-
dispatch thread (i.e., in response to input events), but the Swing
specification forbids touching – reading or writing – any JComponent
objects from a different thread. See Swing threading and the event-
dispatch thread.
The safe way to access the view tree is to do it from the event-
dispatch thread. So Swing takes a clever approach: it uses the event
queue itself as a message-passing queue. In other words, you can
put your own custom messages on the event queue, the same queue
used for mouse clicks, keypresses, button action events, and so
forth. Your custom message is actually a piece of executable code,
an object that implements Runnable , and you put it on the queue using
SwingUtilities.invokeLater . For example:
SwingUtilities.invokeLater(new Runnable() {
public void run() {
content.add(thumbnail);
...
}
});
The invokeLater() drops this Runnable object at the end of the queue,
and when Swing’s event loop reaches it, it simply calls run() . Thus the
body of run() ends up run by the event-dispatch thread, where it can
safely call observers and mutators on the view tree.
reading exercises
Background Processing
(missing explanation)
Event queue blocking – the event loop is waiting for an input event
on the event queue, but the queue is empty, so nothing is happening
in the program.
True
False
(missing explanation)
True
False
(missing explanation)
True
False
(missing explanation)
check
Summary
The view tree organizes the screen into a tree of nested
rectangles, and it is used in dispatching input events as well as
displaying output.
Ready for
Safe from bugs Easy to understand
change
Objectives
Formula = Variable(name:String)
+ Not(formula:Formula)
+ And(left:Formula, right:Formula)
+ Or(left:Formula, right:Formula)
We used instances of Formula to take propositional logic formulas, e.g.
(p ∨ q) ∧ (¬p ∨ r), and represent them in a data structure, e.g.:
And(Or(Variable("p"), Variable("q")),
Or(Not(Variable("p")), Variable("r")))
But why did we define a Formula type? Java already has a way to
represent expressions of Boolean variables with logical and, or, and
not. For example, given boolean variables p , q , and r :
(p || q) && ((!p) || r)
Done!
Music language
In class, we will design and implement a language for generating and
playing music. To prepare, let’s first understand the Java APIs for
playing music with the MIDI synthesizer. We’ll see how to write a
program to play MIDI music. Then we’ll begin to develop our music
language by writing a recursive abstract data type for simple musical
tunes. We’ll choose a notation for writing music in strings, and we’ll
implement a parser to create instances of our Music type.
The full source code for the basic music language is on GitHub.
Clone the fa16-ex26-music-starting repo so you can run the code and
follow the discussion below.
Pitch is an abstract data type for musical pitches (think keys on the
piano keyboard).
Our music data type will rely on Pitch in its rep, so be sure to
understand the Pitch spec as well as its rep and abstraction function.
Using the MIDI sequence player and Pitch , we’re ready to write code
for our first bit of music!
reading exercises
Pitch
transpose(int)
difference(Pitch)
value()
equals(Object)
toString()
(missing explanation)
check
transpose
Pitch.transpose(int) is a:
creator
producer
observer
mutator
(missing explanation)
check
addNote
SequencePlayer.addNote(..) is a:
creator
producer
observer
mutator
(missing explanation)
check
notes will be a static factory method; rather than put it in Music (which
we could do), we’ll put it in a separate class: MusicLanguage will be our
place for all the static methods we write to operate on Music .
Now that we’ve chosen some operations in the spec of Music , let’s
choose a representation.
Composite
The GUI view tree relies heavily on the composite pattern: there
are primitive views like JLabel and JTextField that don’t have
children, and composite views like JPanel and JScollPage that do
contain other views as children. Both implement the common
JComponent interface.
Emptiness
reading exercises
Music rep
Assume we have
import music.*;
import static music.Instrument.*;
import static music.MusicLanguage.*;
(missing explanation)
check
Music notation
For example:
C D E F G A B C' B A G F E D C represents the one-octave ascending and
descending C major scale we played in ScaleSequence . C is middle C,
and C' is C one octave above middle C. Each note is a quarter note.
C/2 D/2 _E/2 F/2 G/2 _A/2 _B/2 C' is the ascending scale in C minor,
played twice as fast. The E, A, and B are flat. Each note is an eighth
note.
You don’t need to understand the parser implementation yet, but you
should understand the simplified abc notation enough to make sense
of the examples.
reading exercises
E2/4
E1/2
E/2
B'/2
B''/2
C,/2
C,,/2
_D/2
^E/2
(missing explanation)
check
The notes method parses strings of simplified abc notation into Music .
reading exercises
parsePitch
C
_C
C'
C/2
.
a single space
a single vertical bar
(missing explanation)
check
Why does this operation take atBeat ? Why not simply play the music
now?
methods.
You should be able to follow their recursive implementations.
Just one more piece of utility code before we’re ready to jam:
music.midi.MusicPlayer plays a Music using the MidiSequencePlayer . Music
Run the main method in ScaleMusic . You should hear the same one-
octave scale again.
Can you follow the flow of the code from calling notes(..) to having an
instance of Music to the recursive play(..) call to individual addNote(..)
calls?
reading exercises
notes
27
28
29
54
55
56
more than 56
(missing explanation)
check
duration
(missing explanation)
check
Music
Assume we have
import music.*;
import static music.Instrument.*;
import static music.MusicLanguage.*;
And
Music r = rest(1);
Pitch p = new Pitch('A').transpose(6);
Music n = note(1, p, GLOCKENSPIEL);
List<Music> s = Arrays.asList(r, n);
r
concat(r, r)
concat(r, r, r)
p
n
s
(missing explanation)
check
To be continued
Playing Row, row, row your boat is pretty exciting, but so far the most
powerful thing we’ve done is not so much the music language as it is
the very basic music parser. Writing music using the simplified abc
notation is clearly much more easy to understand, safe from bugs,
and ready for change than writing page after page of addNote addNote
addNote …
In class, we’ll expand our music language and turn it into a powerful
tool for constructing and manipulating complex musical structures.
Reading 27: Team Version Control
Software in 6.005
Designed to
Correct today and Communicating clearly
accommodate
correct in the unknown with future programmers,
change without
future. including future you.
rewriting.
Objectives
Git workflow
You’ve been using Git for problem sets and in-class exercises for a while now.
Most of the time, you haven’t had to coordinate with other people pushing and
pulling to and from the same repository as you at the same time. For the group
projects, that will change.
In this reading, prepare for some in-class Git exercises by reviewing what you
know and brushing up on some commands. Now that you’re more comfortable
with Git basics, it’s a good time to go back and review some of the resources
from the beginning of the semester.
If you need to, review Learn the Git workflow from the Getting Started page.
Use log commands to make sure you understand the history of the repo.
Graph of commits
Recall that the history recorded in a Git repository is a directed acyclic graph.
The history of any particular branch in the repo (such as the default master
branch) starts at some initial commit, and then its history may split apart and
come back together, if multiple developers made changes in parallel (or if a
single developer worked on two different machines without committing-pushing-
pulling before the switch).
Here’s the output of git lol for the example repository, which shows an ASCII-
art graph:
In the ex05-hello-git example repo, make sure you can explain where the history
of master splits apart, and where it comes back together.
Review Merging from the Version Control reading.
You should understand every step of the process, and how it relates to the
result in the example repo.
Review the Getting Started section on merges, including merging and merge
conflicts.
reading exercises
Merge
Alice and Bob both start with the same Java file:
If Git merges the changes of Alice and Bob, what is the result of
Hello.greet("Eve") ?
Hello, Eve
Hello, Eve!
Ciao, Eve
Ciao, Eve!
we can automatically merge, but the resulting code is broken (static error)
we can automatically merge, but the resulting code is broken (dynamic error)
we can automatically merge, but the resulting code is broken (no error,
wrong answer)
we cannot automatically merge the changes
(missing explanation)
check
If Git merges the changes of Alice and Bob, what is the result of
Hello.greet("Eve") ?
Hello, Eve
Hello, Eve!
Ciao, Eve
Ciao, Eve!
we can automatically merge, but the resulting code is broken (static error)
we can automatically merge, but the resulting code is broken (dynamic error)
we can automatically merge, but the resulting code is broken (no error,
wrong answer)
we cannot automatically merge the changes
(missing explanation)
check
Continue Merging
If Git merges the changes of Alice and Bob, what is the result of running main ?
Hello, Eve
Hello, Eve!
Ciao, Eve
Ciao, Eve!
we can automatically merge, but the resulting code is broken (static error)
we can automatically merge, but the resulting code is broken (dynamic error)
we can automatically merge, but the resulting code is broken (no error,
wrong answer)
we cannot automatically merge the changes
(missing explanation)
check
Communicate. Tell your teammates what you’re going to work on. Tell them
that you’re working on it. And tell them that you worked on it. Communication
is the best way to avoid wasted time and effort cleaning up broken code.
Write specs. Necessary for the things we care about in 6.005, and part of
good communication.
Write tests. Don’t wait for a giant pile of code to accumulate before you try
to test it. Avoid having one person write tests while another person writes
implementation (unless the implementation is a prototype you plan to throw
away). Write tests first to make sure you agree on the specs. Everyone
should take responsibility for the correctness of their code.
Run the tests. Tests can’t help you if you don’t run them. Run them before
you start working, run them again before you commit.
Automate. You’ve already automated your tests with a tool like JUnit, but
now you want to automate running those tests whenever the project
changes. For 6.005 group projects, we provide Didit as a way to
automatically run your tests every time a team member pushes to Athena.
This also removes “it worked on my machine” from the equation: either it
works in the automated build, or it needs to be fixed.
Review what you commit. Use git diff --staged or a GUI program to see
what you’re about to commit. Run the tests. Don’t use commit -a , that’s a
great way to fill your repo with println s and other stuff you didn’t mean to
commit. Don’t annoy your teammates by committing code that doesn’t
compile, spews debug output, isn’t actually used, etc.
Pull before you start working. Otherwise, you probably don’t have the
latest version as your starting point — you’re editing an old version of the
code! You’re guaranteed to have to merge your changes later, and you’re in
danger of having to waste time resolving a merge conflict.
Sync up. At the end of a day or at the end of a work session, make sure
everyone has pushed and pulled all the changes, you’re all at the same
commit, and everyone is satisfied with the state of the project.
reading exercises
Pushing small commits, one for each file changed during some work
Pushing small commits, one for each different change to the project
Pushing small commits, including intermediate work that doesn’t compile
Always committing all changes (for example, git commit -a )
(missing explanation)
check
Team project
Most class times during the project phase will be devoted to group work.
These classes are required, just as normal classes are, and you must check in
with your project mentor TA during class.