08: Quiz Review and Supporting Lists

Announcements

A student wrote and asked about tests not completing in Eclipse. No test we write will take more than a fraction of a second to run if your code is correct. If you have a test not complete, that’s the reason, not that your computer is slow.

A reminder of something I mentioned the first week, which may have since slipped your mind. Generally speaking, the autograder’s grading of your code is the grade you are going to receive. We are not going to grant partial credit for code that “almost works” (whatever that means). And in particular, the code you submit to the autograder must be functional. It might not be correct, but at bare minimum it must compile, and it must not trigger infinite loops on valid inputs. Both of these conditions are detectable within Eclipse when you run tests locally. Submitting code with either problem will result in a zero.

If you start assignments early, upload to the autograder, and discover a problem, we (the TAs and I) will be happy to help you fix it. But if you wait until late Friday afternoon, we likely won’t be available — and it will be your problem to fix. You have a week for programming assignments, and waiting until a few hours before they’re due to begin means giving up access to help with them.

Quiz review

So the quiz was on 121 material, which is appropriate, since we’d been reviewing up until the week before.

Most people did OK on the quiz: mean 15/20, median around 17/20. If you got below about a 15, you might want to think about some things. Be glad you’re not in 187, I guess. Did you just have a bad day? Or was it something more fundamental? Come to office hours to talk with the TAs or me about additional review you might be able to do on your own.

By far the place where most people seemed to have the most trouble on the programming portion of the quiz (the third question). So we’re going to spend part of today’s lecture going over the four programming problems. I’ll point out some of the odd or incorrect things people did along the way and try to give you some advice about how to avoid making these mistakes in the future. I know it sucks having to write code on paper, but that’s the forseeable future until we have access to enough computers to test students on. I don’t love it, but it helps prep you for 187, where historically your course grade is ultimately determined by your ability to code on paper.

A. A sequence is said to be a trincremental if each value is exactly three greater than the previous value. Write a method isTrincrementalSequence that takes a single array of ints as a parameter and returns true if and only if the array represents such a sequence. Assume the array contains at least two values.

So let’s run through some options.

Bad:

(Sample w/ bounds error.)

(Sample error: return true too early.)

(Sample error: set boolean each time through the loop.)

Ugly:

(Sample w/ counter.)

(Sample w/ boolean.)

Good:

boolean isTrincremental(int[] a) {
    for (int i = 1; i < a.length; i++) {
        if (a[i] != a[i-1] + 3) {
            return false;
        }
    }
    return true;
}

D. A palindrome is a string that reads the same backward and forward, for example, “abba” or “amanaplanacanalpanama”. Write a method that takes only a String as a parameter that returns true if and only if the value of the parameter is a palindrome. The charAt method of String will be helpful. Assume the value of the parameter is a lowercase string containing only alphabetic characters.

boolean isPalindrome(String s) {
    for (int i = 0; i < s.length(); i++) {
        if (s.charAt(i) != s.charAt(s.length() - i - 1)) {
            return false;
        }
    }
    return true;
}

Putting it together

So we’ve been talking about Lists and generics and reviewing 121 stuff like boolean conditions and flow control. I thought it would be interesting to put it all together into working example.

We’re going to design and write a class to represent simplified postal addresses (street numbers and names). Then we’re going to dump several instances of it into a list. Finally, we’re going to define a custom Comparator on a postal address that will let us sort the list in a special way.

Let’s get going.

Given the problem statement, we know we’re going to want to define a class that defines objects containing a street number and name:

public class PostalAddress {
    public final int number;
    public final String streetName;
}

What is up with Marc and his use of public final? Here’s what’s up: when you know a data type is going to remain fixed, and that any particular object isn’t going to have its data change, there’s no reason to deal with the pain of writing private instance variables and then turning around to write public accessors (get methods). “But Marc, what if you change how the value is stored, or how it is determined?” Then you’ll be giving a different name (or writing an accessor, etc.) and your IDE will flag all occurrences for you to fix. Or better yet, will allow you to refactor them all yourself.

(See “Effective Java, 2nd edition” which though eight years old is one of the best bits of reading you can do once you’re an intermediate Java programmer.)

Anyway, now let’s add a few constructors. First the obvious one:

public PostalAddress(int number, String streetName) {
    this.number = number;
    this.streetName = streetName;
}

Now one that does some simple parsing:

public PostalAddress(String textAddress) {
    String[] matches = textAddress.split("\\s+", 2); // oops! 2, not 1!
    this.number = Integer.parseInt(matches[0]);
    this.streetName = matches[1];
}

OK! Now we’ve got a simple class. Let’s try making a few of them and adding them to a list:

List<PostalAddress> addresses = new ArrayList<PostalAddress>();

for (int i = 1; i <= 10; i++) {
    addresses.add(new PostalAddress(i, "Maple St"));
}

and maybe printing them out:

System.out.println(addresses);

Ugh, what’s this PostalAddress@677327b6 nonsense? Well, it turns out when you print an object, Java tries to coerce it to a String using its toString method. We haven’t written one, so we get the Object default, which is what you see. It is derived from the class name and the hashCode() method. Since we haven’t defined the latter, we get its default, which is usually but not always the object’s memory address. Yuck. Let’s make it better:

public String toString() {
    return streetName + ", " + number;
}

Now it’s a little better. Hey, let’s see if 6 Maple Street is in our list:

System.out.println(addresses.contains(new PostalAddress("6 Maple St")));

false? What? Maybe our constructor is broken, let’s try the other one:

System.out.println(addresses.contains(new PostalAddress(6, "Maple St")));

Nope, still false. Why? Let’s look at the List.contains method javadoc. Effing trinary. What does (o==null ? e==null : o.equals(e)) mean? This is a very terse way to express an if/else statement that returns a value.

Break it on the ? and the :. If the thing before the ? evaluates to true, return the thing between the ? and :; else return the thing after the :. Here, it means return true if both things are null; if not, only if equals returns true. Ahh, but we haven’t written an equals, which means we’re using the default one from Object. …which checks hashCode, which will be different for different instances of the object. (Look it up in the java doc) Let’s fix this problem?

public boolean equals(PostalAddress o) {
    return number == o.number && streetName.equals(o.streetName);
}

But it still doesn’t work? Oh man, you’re gonna love this. Note that contains (and equals) work on Objects, not PostalAddresses. So we need to change the signature. And then check the type:

public boolean equals(Object o) {
    if (!(o instanceof PostalAddress)) return false;
    PostalAddress p = (PostalAddress)o;
    return number == p.number && streetName.equals(p.streetName);
}

OK, better. But I will note we are omitting some important-in-practice details, as we’re ignoring null and we’re violating the contract for hashCode:

If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.

We’ll come back to hashCode in more detail when we get to Sets later, but for now we’ll ignore this problem. (Sneak peak though: Use Eclise’s code generator to do it. Demo.)

OK, our last task. Let’s say we want to be able to sort our address list. We use the sort function on the List, which if passed null uses the natural order, or if passed a Comparator uses it to sort the list. But what’s the natural order of a PostalAddress? It’s only defined if the PostalAddress implements Comparable. So looks like we’re doing one or the other.

Generally, you want to define Comparable if you expect values of the data type to be compared and you want there to be a canonical way to compare them. You usually define custom Comparators for things like the custom sort. Let’s do both. First, let’s impose a natural order on PostalAddress so that they sort first by street name, then by number. Add implements Comparable<PostalAddress> to the class signature, and Eclipse can helpfully add the missing method:

@Override
public int compareTo(PostalAddress o) {
    // TODO Auto-generated method stub
    return 0;
}

Well that won’t do.

What to do

Implement the method. (JavaDoc for compareTo on board.) See: http://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html

@Override
public int compareTo(PostalAddress o) {
    if (streetName.compareTo(o.streetName) < 0) return -1;
    if (streetName.compareTo(o.streetName) > 0) return 1;
    if (number < o.number) return -1;
    if (number > o.number) return 1;
    return 0;       
}

or slightly more concisely:

@Override
public int compareTo(PostalAddress o) {
    if (streetName.compareTo(o.streetName) < 0) return -1;
    if (streetName.compareTo(o.streetName) > 0) return 1;
    return Integer.compare(number, o.number);
}

Remember, you can look up Integer.compare in the Java API (or just Google it).

Let’s create the list out of order, print it, sort it, then print it:

for (int i = 10; i >=1; i -= 2) {
    addresses.add(new PostalAddress(i, "Maple St"));
}       
for (int i = 1; i < 10; i += 2) {
    addresses.add(new PostalAddress(i, "Birch St"));
}
System.out.println(addresses);
addresses.sort(null);
System.out.println(addresses);      

Hey, it works!

Now let’s define a custom comparator for use in doing a “postal sort”. That is, we still want to sort such that street names are alphabetical, but we want the numbers sorted as all odd first (in ascending order), then all even (in descending order). This is how the truck might go up and down the street (on board).

What does that look like? Let’s declare a new Comparator:

public class PostalOrderComparator implements Comparator<PostalAddress> { ... }

Again, Eclipse helpfully fills it out with the method we need to implement, so let’s do it.

It will be similar to but more complicated than the compareTo method we just wrote. A tip: x % 2 == 0 if and only if x is even. x % 2 == 1 iff it’s false.

public int compare(PostalAddress o1, PostalAddress o2) {
    if (o1.streetName.compareTo(o2.streetName) < 0) return -1;
    if (o1.streetName.compareTo(o2.streetName) > 0) return 1;
    if (o1.number % 2 == 1 && o2.number % 2 == 0) return -1;
    if (o1.number % 2 == 0 && o2.number % 2 == 1) return 1;
    if (o1.number % 2 == 1) return Integer.compare(o1.number, o2.number);
    if (o1.number % 2 == 0) return Integer.compare(o2.number, o1.number);
    return 0;
}

And let’s check it out:

for (int i = 6; i >=1; i -= 2) {
    addresses.add(new PostalAddress(i, "Maple St"));
}       
for (int i = 1; i < 6; i += 2) {
    addresses.add(new PostalAddress(i, "Birch St"));
}
for (int i = 6; i >=1; i -= 2) {
    addresses.add(new PostalAddress(i, "Birch St"));
}       
for (int i = 1; i < 6; i += 2) {
    addresses.add(new PostalAddress(i, "Maple St"));
}
System.out.println(addresses);
addresses.sort(null);
System.out.println(addresses);      
addresses.sort(new PostalOrderComparator());
System.out.println(addresses);      

Things we might do to improve this? Add an isOdd and/or isEven method for readability, perhaps.