1. Introduction to Java
2. Reference Types and Semantics
3. Method Specifications and Testing
4. Loop Invariants
5. Analyzing Complexity
6. Recursion
7. Sorting Algorithms
8. Classes and Encapsulation
9. Interfaces and Polymorphism
10. Inheritance
11. Additional Java Features
12. Collections and Generics
11. Additional Java Features

11. Additional Java Features

Over the past three lectures, we have surveyed three central features of object-oriented programming: encapsulated class design, subtype polymorphism, and inheritance. As a wrap-up for this module of the course, this lecture will highlight some miscellaneous Java features. First, we’ll talk about how Java handles exceptional behavior, with a particular focus on checked exceptions. Next, we’ll talk a bit more about the final keyword and immutability in Java. Finally, we’ll take a look at two methods of the Object class, with a focus on notions of equality in Java.

Exceptions

Sometimes when a method is executing, it may be unable to reach its post-condition due to exceptional circumstances.

In many cases, these issues are discovered at runtime, while a method is executing. Returning normally from the method would send a signal to the client that the post-conditions have been met, which is likely untrue in these cases. As an alternative, we’d like a way to signal to the client that something has gone awry and leave them with the responsibility for deciding how to proceed (for example, by trying an operation again, alerting the user to the issue and requesting a modified input, or perhaps crashing the program if there is no sensible way to recover). Java uses exceptions for this alternate signaling mechanism.

Definition: Exception

An exception is an object that signals that an atypical circumstance has arisen at runtime.

Suppose we execute the following buggy code (try to identify the issue).

ExceptionDemo1.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class ExceptionDemo1 {

    public static void main(String[] args) {
        System.out.print(mean(new int[0]));
    }

    /**
     * Returns the mean of all entries in `nums`.
     */
    public static double mean(int[] nums) {
        int sum = nums[0];
        for (int i = 1; i < nums.length; i++) {
            sum += nums[i];
        }
        return (double) sum / nums.length;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class ExceptionDemo1 {

    public static void main(String[] args) {
        System.out.print(mean(new int[0]));
    }

    /**
     * Returns the mean of all entries in `nums`.
     */
    public static double mean(int[] nums) {
        int sum = nums[0];
        for (int i = 1; i < nums.length; i++) {
            sum += nums[i];
        }
        return (double) sum / nums.length;
    }
}

We receive the output,


Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds 
for length 0
	at ExceptionDemo1.mean(ExceptionDemo1.java:11)
	at ExceptionDemo1.main(ExceptionDemo1.java:4)

This message informs us that a java.lang.ArrayIndexOutOfBoundsException, an object that models a type of runtime exception, was produced while executing the line 11 of the method ExceptionDemo1.mean() (the initialization of sum), which we got to from line 4 of the main() method (the print() statement that calls mean()). The first line of this output tells us the type of exception with an explanatory message (“Index 0 out of bounds for length 0”), and the subsequent lines contain a stack trace, listing the line number and file of the line of code that was in the process of executing for each call frame on the runtime stack.

Definition: Stack Trace

When an exception propagates out of the main() method of our program, a stack trace is printed in an error message that summarizes the state of the program's execution at the time when the exception was generated.

Stack traces are a useful debugging tool. They identify the exact location in our code where exceptional behavior was first encountered (the top line of the stack trace), and they provide the context for this exceptional behavior in the lower call frames. To understand how the stack trace is produced, we will need to explore different facets of Java’s exception handling framework. Broadly, this includes three components:

  1. A throw statement is written by a method’s implementer to produce an exception and alert the client to exceptional behavior.
  2. A throws clause is included by the implementer in their method’s signature to document the possibility of exceptional behavior.
  3. A try/catch block is included in the client’s code to prepare for the possibility of an exception.

Next, we’ll describe each of these components in detail.

throw statements

A throw statement is used to signal that exceptional behavior has occurred. This “signal” is packaged up in an object, specifically an instance of some subclass of Java’s Throwable class.

For an example of a throw statement, suppose we are writing a method that locates a local maximum in an int[] array.

1
2
3
4
5
/**
 * Returns an index `i` with `0 < i < a.length-1` that corresponds to a 
 * local maximum of array `a`, meaning `a[i] > a[i-1]` and `a[i] > a[i+1]`. 
 */
public static int findLocalMax(int[] a) { ... }
1
2
3
4
5
/**
 * Returns an index `i` with `0 < i < a.length-1` that corresponds to a 
 * local maximum of array `a`, meaning `a[i] > a[i-1]` and `a[i] > a[i+1]`. 
 */
public static int findLocalMax(int[] a) { ... }

In Lecture 3, we had this method return the sentinel value 0 when a local maximum was not found. An alternate (arguably more principled) approach is to treat this case with an exception. We’ll define a bespoke exception type called NoLocalMaxException that will alert this case.

NoLocalMaxException.java

1
2
3
public class NoLocalMaxException extends Exception {

}
1
2
3
public class NoLocalMaxException extends Exception {

}
Remark:

While it may appear that we have omitted the body of this class, this is not the case. In this case, the state and behaviors that are provided by Java's Exception class (a subclass of Throwable) are sufficient for our purposes; inheritance takes care of the class definition for us! The only thing that we need from this class definition is a new type name, NoLocalMaxException, that helps us signal the exact exception situation that has arisen. We will see why this new type becomes useful when we discuss try/catch blocks soon.

Now, let’s develop the body of findLocalMax(). We can meet its specifications by writing a simple loop over the interior values of a (a(..)) and checking if each is a local max. If we find a local max, we can immediately return its index. In this way, exiting the loop signals the absence of a local max, at which point we can throw our NoLocalMaxException.

1
2
3
4
5
6
7
8
 /* loop invariant: `a[1..i)` are not local maxima. */
for (int i = 1; i < a.length - 1; i++) {
  if (a[i] > a[i - 1] && a[i] > a[i + 1]) {
    return i;
  }
}
// loop exited, no local max found
throw new NoLocalMaxException();
1
2
3
4
5
6
7
8
 /* loop invariant: `a[1..i)` are not local maxima. */
for (int i = 1; i < a.length - 1; i++) {
  if (a[i] > a[i - 1] && a[i] > a[i + 1]) {
    return i;
  }
}
// loop exited, no local max found
throw new NoLocalMaxException();

Within our throw statement, we construct a new Throwable object (in this case, a NoLocalMaxException), which has state that models the cause and location of the exception. As soon as a throw statement executes within a method, we immediately exit that method (just as we do when we encounter a return statement). Intuitively, a method throws an exception when it identifies that it is no longer able to perform its primary function; our findLocalMax() method definitely won’t find a local maximum once it knows that there isn’t one. At that point, its best course of action is to stop what it’s doing and pass control back to its client, who may have a better sense of how to proceed.

throws clauses

A throws clause provides a method a way to alert other classes to the possibility that it will throw an exception. To discuss the semantics of throws clauses, we need to take a brief detour to discuss a subtle distinction between two types of exceptional behavior, unchecked and checked. To do this, we’ll need to take a look at some classes in the Throwable type hierarchy:

Broadly, there are two categories of Throwables, Errors and Exceptions.

Errors model serious issues with little chance for recovery (such as AssertionErrors that model logical inconsistencies within the program and VirtualMachineErrors that model situations like running out of memory). We almost always want the program to crash when we encounter an Error.

Exceptions typically model less serious situations where recovery is possible. RuntimeExceptions model exceptional behavior that results because of particular values of variables at runtime, such as NullPointerExceptions (a reference turned out to be null), ClassCastExceptions (a variable referenced an object with an unexpected dynamic type), and ArithmeticException (our calculation included a value we don’t know how to handle).

We also typically don’t try to handle RuntimeExceptions, as doing so would be pretty burdensome (imagine having to write code to patch a faulty division by 0 each time you wanted to divide, or addressing the possibility of null each time that you called an instance method). Together, Errors and RuntimeExceptions are deemed unchecked, since the compiler allows them to arise without any warning. All other Exceptions that are not subtypes of RuntimeExceptions are checked exceptions, and must be addressed explicitly in our code.

Definition: Checked Exception

Methods must explicitly alert their clients of the possibility that they will propagate a checked exception. This name comes from the fact that checked exception declarations are a code soundness property that is statically checked at compile time.

We do this “alerting” by adding a throws clause to the signature of any method that may propagate a checked exception, listing the type(s) of checked exceptions that are possible. In our findLocalMax(int[] a) method signature, we will need a throws clause to alert to the possibility of a NoLocalMaxException. We should also update the documentation with a “Throws” clause to specify when this exception will occur.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/**
 * Returns an index `i` with `0 < i < a.length-1` that corresponds to a local 
 * maximum of array `a`, meaning `a[i] > a[i-1]` and `a[i] > a[i+1]`. Throws a 
 * `NoLocalMaxException` if `a` does not contain a local max.
 */
static int findLocalMax(int[] a) throws NoLocalMaxException {
  /* loop invariant: `a[1..i)` are not local maxima. */
  for (int i = 1; i < a.length - 1; i++) {
    if (a[i] > a[i - 1] && a[i] > a[i + 1]) {
      return i;
    }
  }
  // loop exited, no local max found
  throw new NoLocalMaxException();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/**
 * Returns an index `i` with `0 < i < a.length-1` that corresponds to a local 
 * maximum of array `a`, meaning `a[i] > a[i-1]` and `a[i] > a[i+1]`. Throws a 
 * `NoLocalMaxException` if `a` does not contain a local max.
 */
static int findLocalMax(int[] a) throws NoLocalMaxException {
  /* loop invariant: `a[1..i)` are not local maxima. */
  for (int i = 1; i < a.length - 1; i++) {
    if (a[i] > a[i - 1] && a[i] > a[i + 1]) {
      return i;
    }
  }
  // loop exited, no local max found
  throw new NoLocalMaxException();
}
Remark:

Here, we have been very careful in our choice of words when describing the semantics of a throws clause. It must be included when a checked exception can be "propagated" by a method, not just when it can be "thrown". Here, the word "propagate" refers to any time that an exception causes an "abnormal" return from a method. This includes the possibility of throwing a "fresh" exception, but also the possibility of not handling an exception that propagated out to this method, which will lead to its further propagation.

try/catch blocks

As a final component of the exception handling framework, we’ll need a way to respond to exceptions that have been thrown. We do this with a try/catch block:

1
2
3
4
5
try {
  // code that may propagate an exception
} catch (<Exception Type> <Exception Variable>) {
  // code to handle exception
} 
1
2
3
4
5
try {
  // code that may propagate an exception
} catch (<Exception Type> <Exception Variable>) {
  // code to handle exception
} 

Within the try block, we place the code (e.g., method calls) that may result in an exception. By surrounding code in a try block, we are telling Java to be ready for an incoming exception (I think about it as telling Java to put on its “baseball glove” to be ready for something to be thrown). When an exception propagates out to the try block, its dynamic type is matched against the catch blocks. The first (and only the first) catch block whose static type matches (i.e., is a supertype of) the caught exception gets executed. For example, if the call to scaryCalculation() in the following code snippet propagates an ArithmeticException, then the second catch block would be entered;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
try {
  scaryCalculation();
} catch (IOException e) {
  // not entered, ArithmeticException is not a subtype of IOException
} catch (RuntimeException e) {
  // entered, ArithmeticException is a subtype of RuntimeException
} catch (Exception e) {
  // not entered, although the caught expression matches Exception,
  // we only enter the first suitable `catch` block.
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
try {
  scaryCalculation();
} catch (IOException e) {
  // not entered, ArithmeticException is not a subtype of IOException
} catch (RuntimeException e) {
  // entered, ArithmeticException is a subtype of RuntimeException
} catch (Exception e) {
  // not entered, although the caught expression matches Exception,
  // we only enter the first suitable `catch` block.
}

Within the scope of the catch block, we place the code to process the exception. This may involve logic to handle the exception (for example, by retrying an I/O routine or asking the user to revise their input), print() statements that offer more detailed debugging information that can help diagnose issues in the code, or another throw statement. It can even be empty, if we want to ignore the exception and proceed with the client code.

Let’s write some client code to see this exception handling in action. Suppose we define the following method to decrease a local maximum in an array by 1.

1
2
3
4
5
6
7
/**
 * Decreases the value of a local maximum of array `a` by 1. 
 */
public static void reduceMax(int[] a) {
  int maxLoc = findLocalMax(a);
  a[maxLoc]--;
}
1
2
3
4
5
6
7
/**
 * Decreases the value of a local maximum of array `a` by 1. 
 */
public static void reduceMax(int[] a) {
  int maxLoc = findLocalMax(a);
  a[maxLoc]--;
}

If we try to compile this method, we receive the error message


unreported exception NoLocalMaxException; must be caught or declared to be thrown". 

Since NoLocalMaxException is a checked exception that may propagate to reduceMax(), we must take one of two actions.

  1. If we don’t want to handle this exception, we can do nothing and allow it to continue to propagate. Information from the reduceMax() invocation is added to the exception’s stack trace, and we immediately exit from reduceMax(). When such an exception propagates all the way out of the main() method, this is when the stack trace is printed and the program crashes. The compiler requires that we account for this possible behavior by adding a “throws NoLocalMaxException” clause to reduceMax().

If we select this option, we should add a “Throws” clause to the specifications to further alert to this possibility.

1
2
3
4
5
6
7
8
/**
 * Decreases the value of a local maximum of array `a` by 1. Throws a 
 * `NoLocalMaxException` if no local max is present in `a`. 
 */
public static void reduceMax(int[] a) throws NoLocalMaxException {
  int maxLoc = findLocalMax(a);
  a[maxLoc]--;
}
1
2
3
4
5
6
7
8
/**
 * Decreases the value of a local maximum of array `a` by 1. Throws a 
 * `NoLocalMaxException` if no local max is present in `a`. 
 */
public static void reduceMax(int[] a) throws NoLocalMaxException {
  int maxLoc = findLocalMax(a);
  a[maxLoc]--;
}
  1. If we feel equipped to handle this exception, we can surround the assignment statement calling findLocalMax() in a try-block and include a catch block with an exception matching NoLocalMaxException.

We’ll take the latter route, and choose to have an empty catch block. In this case, our method will have no effect when a does not have a local max; all entries will remain unchanged. The catch block ensures that a NoLocalMaxException will not propagate out of reduceMax(), so a throws clause is unneeded. We must also include the decrement statement “a[maxLoc]--;” within the try block since this is the scope of maxLoc. Finally, we should update the specifications to explain the method’s behavior when no local max is present.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
 * Decreases the value of a local maximum of array `a` by 1, or does nothing 
 * if no local max is present in `a`.
 */
public static void reduceMax(int[] a) {
  try {
    int maxLoc = findLocalMax(a);
    a[maxLoc]--;
  } catch (NoLocalMaxException ignored) {
    // do nothing
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
 * Decreases the value of a local maximum of array `a` by 1, or does nothing 
 * if no local max is present in `a`.
 */
public static void reduceMax(int[] a) {
  try {
    int maxLoc = findLocalMax(a);
    a[maxLoc]--;
  } catch (NoLocalMaxException ignored) {
    // do nothing
  }
}

Exceptions and Specifications

Exceptions add a new layer of possibilities to our previous discussion of specifications, so let’s take this opportunity to reopen this discussion. Here, we emphasize two points that are common misconceptions for students:

Undefined behavior may, but does not need to, result in an exception.

A lot of times, violating preconditions (e.g., providing input values that are out of range, supplying a null argument in violation of an implicit non-null pre-condition) will result in runtime exceptions as the method executes. However, don’t feel responsible for generating these runtime exceptions yourself; throwing your own runtime exceptions can be seen a bad coding practice (if you are aware of an issue at runtime, why not correct it?). If you want to do something, we prefer using defensive programming assert statements to check pre-conditions.

Exceptions are not undefined behavior.

While we use exceptions as a tool to handle atypical situations, the presence of an exception does not automatically alert that something has gone wrong. Instead, exceptions make up part of a method’s specifications. Checked exceptions that may propagate are documented both with a throws clause and in the specifications. If you throw an exception to exit a method, you are handing back control to the client, so you must ensure that the class invariant is met.

Since exceptions are often documented in a method’s specifications, we may wish to write unit tests that verify their correct propagation. We can do this using JUnit’s assertThrows() methods. The arguments to assertThrows() require a special syntax. For example, the following method will check that findLocalMax() (our first implementation that does not include a try/catch block) throws a NoLocalMaxException:

1
2
3
4
5
6
@DisplayName("WHEN `findLocalMax()` is called on an array with no local max, THEN a `NoLocalMaxException` is thrown.")
@Test
void testException() {
    int[] a = {1, 2, 3, 4}; // no local max
    assertThrows(NoLocalMaxException.class, () -> findLocalMax(a));
}
1
2
3
4
5
6
@DisplayName("WHEN `findLocalMax()` is called on an array with no local max, THEN a `NoLocalMaxException` is thrown.")
@Test
void testException() {
    int[] a = {1, 2, 3, 4}; // no local max
    assertThrows(NoLocalMaxException.class, () -> findLocalMax(a));
}

This syntax allows us to “package up” the call to findLocalMax() so that JUnit can execute it once it is ready (i.e., from within its own try/catch block that is set up to check for the correct type of Throwable). We’ll explore the mechanics of this special lambda expression syntax in a few lectures.

Immutability

So far, most classes we have written have been mutable, they expose to the client behaviors that can modify their internal state. For example, our account classes from the past two lectures allowed users to deposit and transfer funds, which modified both the balance and transactions fields. Mutability adds complexity to a class. Whenever the state of an object can be modified, there is the possibility that the class invariant becomes violated. The implementer must account for these possibilities and ensuring that the class invariant is maintained. Additionally, it can be more challenging to reason formally about mutable objects within our code; properties that we can assert on one line of code about the state of an object may not hold on later lines. For these reasons, mutability increases the possibility for bugs in large programs.

While in many cases mutable objects are a pragmatic choice, sometimes classes do not need mutable state. Their objects serve as containers to organize some data, but this data may not need to change during the object’s lifetime. We have already seen an example of this. Strings are immutable objects that represent a sequence of characters. Once they are initialized, Strings cannot be modified. Instead, they provide methods that can query certain properties of the string (charAt(), length(), etc.) and methods that can construct and return new String objects with different properties (substring(), toUpperCase(), etc.).

Definition: Mutable, Immutable Class

A class is mutable if it includes methods that modify its objects' states after construction. When it is not possible to modify an object's state, we say that its class is immutable.

The Java language does not include mechanisms to natively support or enforce immutability. Instead, it is the responsibility of an implementor to document the guarantee of immutability in a class’ specifications and to enforce this guarantee through its method definitions.

As an example, let’s develop an immutable Point class that represents a point in the 2D coordinate plane. The state of a Point consists of an x-coordinate (a double x) and a y-coordinate (a double y) which are fixed at construction. We’ll add accessor methods x() and y() for these fields.

Point.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/** An immutable class representing a point in the 2D coordinate plane with `double` coordinates. */
public class Point {
  /** The x-coordinate of this point. */
  private double x;

  /** The y-coordinate of this point. */
  private double y;

  /** Constructs a `Point` object with the given `x`- and `y`-coordinates. */
  public Point(double x, double y) {
    this.x = x;
    this.y = y;
  }

  /** Returns the x-coordinate of this point. */
  public double x() {
    return x;
  }

  /** Returns the y-coordinate of this point. */
  public double y() {
    return y;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/** An immutable class representing a point in the 2D coordinate plane with `double` coordinates. */
public class Point {
  /** The x-coordinate of this point. */
  private double x;

  /** The y-coordinate of this point. */
  private double y;

  /** Constructs a `Point` object with the given `x`- and `y`-coordinates. */
  public Point(double x, double y) {
    this.x = x;
    this.y = y;
  }

  /** Returns the x-coordinate of this point. */
  public double x() {
    return x;
  }

  /** Returns the y-coordinate of this point. */
  public double y() {
    return y;
  }
}

We can add additional methods to support other behaviors of points. For example, we can add a method that returns the distance to another point.

Point.java

1
2
3
4
/** Returns the distance between this point and the given `other` point. */
public double distanceTo(Point other) {
  return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2));
}
1
2
3
4
/** Returns the distance between this point and the given `other` point. */
public double distanceTo(Point other) {
  return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2));
}
Remark:

Notice that in this code, we can access the fields other.x and other.y even though they are private. Remember, the private visibility modifier restricts access to the class where the member is defined, not to the instance of that class. An object can access the private members of other objects of its type.

We can also add methods that perform transformations on a point, for example a method to reflect a point over a given line (useful for graphics applications). Since Point is an immutable class, we won’t modify its coordinates to carry out this reflection. Instead, we’ll return a new Point object with the new coordinates.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/** 
 * Returns a new `Point` object that is obtained by reflecting this point about
 * the line y = `m`x + `b` for the given slope `m` and y-intercept `b`.
 */
public Point reflectOver(double m, double b) {
  double xp = this.x - 2 * b * m + 2 * m * this.y - this.x * m * m;
  double yp = 2 * this.x * m + 2 * b + m * m * this.y - this.y;
  double d = 1 + m * m;
  return new Point(xp / d, yp / d);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/** 
 * Returns a new `Point` object that is obtained by reflecting this point about
 * the line y = `m`x + `b` for the given slope `m` and y-intercept `b`.
 */
public Point reflectOver(double m, double b) {
  double xp = this.x - 2 * b * m + 2 * m * this.y - this.x * m * m;
  double yp = 2 * this.x * m + 2 * b + m * m * this.y - this.y;
  double d = 1 + m * m;
  return new Point(xp / d, yp / d);
}

The final Keyword

A related, but separate, notion to immutability in Java is its final keyword. We have already seen final used in two ways in the previous lecture.

Today, we’ll talk about a third use of final, as a modifier for an instance variable. When an instance variable is declared as final, it cannot be reassigned after its initialization. This is another static property that is enforced by the compiler. Since our Point class is immutable, we know that its x and y fields will not be modified after they initialized in the Point() constructor. Thus, we can mark them as final.

1
2
3
4
5
/** The x-coordinate of this point. */
private final double x;

/** The y-coordinate of this point. */
private final double y;
1
2
3
4
5
/** The x-coordinate of this point. */
private final double x;

/** The y-coordinate of this point. */
private final double y;

By doing this, we allow the compiler to help us enforce immutability by warning us about any reassignment of these fields. For example, if we wrote reflectOver() to modify the coordinates rather than constructing a new Point,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/** 
 * Reflects this point over the line y = `m`x + `b` for the given slope `m` 
 * and y-intercept `b`.
 */
public Point reflectOver(double m, double b) {
  x = this.x - 2 * b * m + 2 * m * this.y - this.x * m * m;
  y = 2 * this.x * m + 2 * b + m * m * this.y - this.y;
  double d = 1 + m * m;
  x /= d;
  y /= d;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/** 
 * Reflects this point over the line y = `m`x + `b` for the given slope `m` 
 * and y-intercept `b`.
 */
public Point reflectOver(double m, double b) {
  x = this.x - 2 * b * m + 2 * m * this.y - this.x * m * m;
  y = 2 * this.x * m + 2 * b + m * m * this.y - this.y;
  double d = 1 + m * m;
  x /= d;
  y /= d;
}

the compiler gives the error message


cannot assign a value to final variable x

Marking variables as final also allows the compiler to perform optimizations to your code to improve its performance that are only possible once it can guarantee that a variable will not be reassigned. Because of this optimization possibility, IDEs like IntelliJ will often detect variables that can be made final and suggest this. Accepting these suggestions is a good programming practice.

Here, we caution that the semantics of final are not the same as being immutable when dealing with variables with reference types. Just because a variable cannot be reassigned doesn’t mean that its contents will not change. For example, suppose that we instead modeled our Point class with a final double[] array of length 2.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Point {
  /** 
   * The coordinates of this point, with `coord[0]` representing its x-coordinate
   * and `coord[1]` representing its y-coordinate.
   */
  private final double[] coord;

  /** Constructs a `Point` object with the given `x`- and `y`-coordinates. */
  public Point(double x, double y) {
    this.coord = new double[2];
    this.coord[0] = x;
    this.coord[1] = y;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Point {
  /** 
   * The coordinates of this point, with `coord[0]` representing its x-coordinate
   * and `coord[1]` representing its y-coordinate.
   */
  private final double[] coord;

  /** Constructs a `Point` object with the given `x`- and `y`-coordinates. */
  public Point(double x, double y) {
    this.coord = new double[2];
    this.coord[0] = x;
    this.coord[1] = y;
  }
}

If we make the same mistake and modify the coordinates in reflectOver(), the compiler will not catch this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/** 
 * Reflects this point over the line y = `m`x + `b` for the given slope `m` 
 * and y-intercept `b`.
 */
public Point reflectOver(double m, double b) {
  coord[0] = coord[0] - 2 * b * m + 2 * m * coord[1] - coord[0] * m * m;
  coord[1] = 2 * coord[0] * m + 2 * b + m * m * coord[1] - coord[1];
  double d = 1 + m * m;
  coord[0] /= d;
  coord[1] /= d;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/** 
 * Reflects this point over the line y = `m`x + `b` for the given slope `m` 
 * and y-intercept `b`.
 */
public Point reflectOver(double m, double b) {
  coord[0] = coord[0] - 2 * b * m + 2 * m * coord[1] - coord[0] * m * m;
  coord[1] = 2 * coord[0] * m + 2 * b + m * m * coord[1] - coord[1];
  double d = 1 + m * m;
  coord[0] /= d;
  coord[1] /= d;
}

In this case, we are not reassigning the field coord to reference a new array object. Rather, we are modifying the entries of the original array object to store new values. Said succinctly, the final keyword does not enforce that every aspect of an object remains unmodified (i.e., it does not enforce immutability); it only guards the top-level reference stored in the field. It is incumbent on you, as the implementer to document and guarantee immutability.

Object Methods

In the previous lecture, we introduced the Object class as the “superest of all classes”, the top class in Java’s type hierarchy. Today, we’ll look at two instance methods of Object (i.e., two behaviors that every single object in Java supports).

toString()

The toString() method of the Object class has the signature “public String toString()”. It “[r]eturns a string representation of the object”. This is a very useful method for debugging; it allows us to print out a representation of an object at some point in our program to better understand what our code is doing. Since our Point class does not declare an explicit superclass (with an extends clause), it is a direct subclass of Object. Therefore, it inherits Object’s toString() definition. Let’s see what this does. If we run the client code,

1
2
Point p = new Point(1.0,3.5);
System.out.print(p);
1
2
Point p = new Point(1.0,3.5);
System.out.print(p);

we obtain output


Point@6f496d9f

This default toString() definition in Object includes the name of the class and a reference to the memory address where it is stored on the heap. This is not a particularly useful String representation, so let’s override toString() in the Point class.

1
2
3
4
@Override
public String toString() {
  return "Point (" + this.x + "," + this.y + ")";
}
1
2
3
4
@Override
public String toString() {
  return "Point (" + this.x + "," + this.y + ")";
}

Now, our client code from above prints


Point (1.0,3.5)

equals()

Suppose we execute the following client code:

1
2
3
4
5
6
Point p1 = new Point(0.0, 3.0);
Point p2 = new Point(4.0, 1.0);
Point p3 = p1.reflectOver(2.0, -2.0);
System.out.println("p2: " + p2);
System.out.println("p3: " + p3);
System.out.println(p2 == p3);
1
2
3
4
5
6
Point p1 = new Point(0.0, 3.0);
Point p2 = new Point(4.0, 1.0);
Point p3 = p1.reflectOver(2.0, -2.0);
System.out.println("p2: " + p2);
System.out.println("p3: " + p3);
System.out.println(p2 == p3);

Note that the String concatenation operator + on line 4 implicitly calls p2.toString() to obtain the String representation of p2 (and similar for p3 on line 5) Thus, the first two lines of output are:


p2: Point (4.0,1.0)
p3: Point (4.0,1.0)

What do you expect the third line of the output to be?

View the program output.


p2: Point (4.0,1.0)
p3: Point (4.0,1.0)
false

How can we explain this?

The == operator compares the values of the expressions on both sides of it. These expressions are Point variables, whose values are references (i.e., memory addresses) of Point objects. Although they have the same state, the Point objects referenced by p2 and p3 are different; one (p2) was constructed in the client code and the other (p3) was constructed in the reflectOver() method. Thus, the values stored in variables p2 and p3 are different.

Sometimes, comparing memory addresses (i.e., checking reference equality) is desired, so using the == operator on reference types is warranted. Other times, we want to instead compare the states of the objects. For this, Java defines an equals() method in the Object class with signature “public boolean equals(Object obj)” that “Indicates whether some other object is “equal to” this one.” The default behavior of equals() is the same as == (reference equality), but we can override equals() to encode a more natural notion of equality for a class we are defining.

Remark:

Pay careful attention to the type of the parameter of the equals method. It is Object, so it must be Object in the signature of our equals() definition. A common mistake is to replace Object with the class we are writing, but this defines a new method that does not override Object.equals(). This can lead to some undesired behavior in code that leverages Object's equals() method. In short, your equals() methods must be able to compare to any other Object, regardless of its type.

In light of the above remark, we typically start our equals methods with a standard “template” that checks to make sure that the parameter Object has the same type as the target. If this is not the case, we return false (an object that is not a Point should not be equal to a Point).

1
2
3
if ((obj == null) || (this.getClass() != obj.getClass())) {
  return false;
}
1
2
3
if ((obj == null) || (this.getClass() != obj.getClass())) {
  return false;
}

getClass() is another Object method that returns a representation of the dynamic type of an object. Once we’ve completed this check, we can safely cast the obj parameter to the class’ type and proceed with our equality check. In our example, two Points are equal when they have the same coordinates.

1
2
3
4
5
6
7
8
9
@Override
public boolean equals(Object obj) {
  if ((obj == null) || (this.getClass() != obj.getClass())) {
    return false;
  }

  Point other = (Point) obj;
  return (this.x == other.x) && (this.y == other.y);
}
1
2
3
4
5
6
7
8
9
@Override
public boolean equals(Object obj) {
  if ((obj == null) || (this.getClass() != obj.getClass())) {
    return false;
  }

  Point other = (Point) obj;
  return (this.x == other.x) && (this.y == other.y);
}

Note that we can compare this.x and other.x with == because they are primitive types; these fields directly store their values. Sometimes, you might need to make an inner call to equals() to compare fields that are reference types.

The Object.equals() specifications require that three properties are guaranteed by any overriding equals() definition.

These properties establish an equivalence relation over the set of all objects of a given type (you’ll discuss equivalence relations more in CS 2800), and ensures that equality functions the way that you expect in your code. You can verify that our definition above meets these three properties. We explore this idea more in Exercise 11.10.

If we replace the last line of our client code with

1
System.out.println(p2.equals(p3));
1
System.out.println(p2.equals(p3));

the code now reports that p2 and p3 are equal.

Record Classes

Altogether, our immutable Point class contains a lot of code that will be standard for many immutable classes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/** An immutable class representing a point in the 2D coordinate plane with `double` coordinates. */
public class Point {
  /** The x-coordinate of this point. */
  private final double x;

  /** The y-coordinate of this point. */
  private final double y;

  /** Constructs a `Point` object with the given `x`- and `y`-coordinates. */
  public Point(double x, double y) {
    this.x = x;
    this.y = y;
  }

  /** Returns the x-coordinate of this point. */
  public double x() {
    return x;
  }

  /** Returns the y-coordinate of this point. */
  public double y() {
    return y;
  }

  /** Returns the distance between this point and the given `other` point. */
  public double distanceTo(Point other) {
    return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2));
  }

  /**
   * Reflects this point over the line y = `m`x + `b` for the given slope 
   * `m` and y-intercept `b`.
   */
  public Point reflectOver(double m, double b) {
    double newx = this.x - 2 * b * m + 2 * m * this.y - this.x * m * m;
    double newy = 2 * this.x * m + 2 * b + m * m * this.y - this.y;
    double d = 1 + m * m;
    return new Point(newx / d, newy / d);
  }

  @Override
  public String toString() {
      return "Point (" + this.x + "," + this.y + ")";
  }

  @Override
  public boolean equals(Object obj) {
    if ((obj == null) || (this.getClass() != obj.getClass())) {
      return false;
    }

    Point other = (Point) obj;
    return (this.x == other.x) && (this.y == other.y);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/** An immutable class representing a point in the 2D coordinate plane with `double` coordinates. */
public class Point {
  /** The x-coordinate of this point. */
  private final double x;

  /** The y-coordinate of this point. */
  private final double y;

  /** Constructs a `Point` object with the given `x`- and `y`-coordinates. */
  public Point(double x, double y) {
    this.x = x;
    this.y = y;
  }

  /** Returns the x-coordinate of this point. */
  public double x() {
    return x;
  }

  /** Returns the y-coordinate of this point. */
  public double y() {
    return y;
  }

  /** Returns the distance between this point and the given `other` point. */
  public double distanceTo(Point other) {
    return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2));
  }

  /**
   * Reflects this point over the line y = `m`x + `b` for the given slope 
   * `m` and y-intercept `b`.
   */
  public Point reflectOver(double m, double b) {
    double newx = this.x - 2 * b * m + 2 * m * this.y - this.x * m * m;
    double newy = 2 * this.x * m + 2 * b + m * m * this.y - this.y;
    double d = 1 + m * m;
    return new Point(newx / d, newy / d);
  }

  @Override
  public String toString() {
      return "Point (" + this.x + "," + this.y + ")";
  }

  @Override
  public boolean equals(Object obj) {
    if ((obj == null) || (this.getClass() != obj.getClass())) {
      return false;
    }

    Point other = (Point) obj;
    return (this.x == other.x) && (this.y == other.y);
  }
}

To cut down on the need to rewrite this fairly boilerplate code, recent versions of Java have added a feature called a record class to model objects with all final fields. Record classes automatically generate many common methods such as the constructor that initializes the fields, accessors for each of the fields, and definitions of toString(), equals(), and hashCode() (another Object method that we will discuss soon). You can read more about record classes on Java’s documentation page.

If we refactor our Point implementation as a record class, we can obtain the same behavior with much less code. Note that we must separately override toString() to obtain our desired String representation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** An immutable class representing a point in the 2D coordinate plane with `double` coordinates. */
public record Point(double x, double y) {
  @Override
  public String toString() {
      return "Point (" + this.x + "," + this.y + ")";
  }

  /** Returns the distance between this point and the given `other` point. */
  public double distanceTo(Point other) {
    return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2));
  }

  /**
   * Reflects this point over the line y = `m`x + `b` for the given slope 
   * `m` and y-intercept `b`.
   */
  public Point reflectOver(double m, double b) {
      double newx = this.x - 2 * b * m + 2 * m * this.y - this.x * m * m;
      double newy = 2 * this.x * m + 2 * b + m * m * this.y - this.y;
      double d = 1 + m * m;
      return new Point(newx / d, newy / d);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** An immutable class representing a point in the 2D coordinate plane with `double` coordinates. */
public record Point(double x, double y) {
  @Override
  public String toString() {
      return "Point (" + this.x + "," + this.y + ")";
  }

  /** Returns the distance between this point and the given `other` point. */
  public double distanceTo(Point other) {
    return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2));
  }

  /**
   * Reflects this point over the line y = `m`x + `b` for the given slope 
   * `m` and y-intercept `b`.
   */
  public Point reflectOver(double m, double b) {
      double newx = this.x - 2 * b * m + 2 * m * this.y - this.x * m * m;
      double newy = 2 * this.x * m + 2 * b + m * m * this.y - this.y;
      double d = 1 + m * m;
      return new Point(newx / d, newy / d);
  }
}

Main Takeaways:

  • Exceptions provide a mechanism for a method to signal an abnormal situation to its client. A throw statement is used to generate an exception, which immediately propagates outward, building up a stack trace until it is caught in a try/catch block.
  • Checked exceptions are tracked by the compiler. If a method has the possibility of propagating a checked exception, it must declare this with a throws clause in its signature.
  • An immutable object does not expose behaviors that can modify its state after initialization. Immutability is enforced through implementer discipline and specifications.
  • When a field is marked as final, the compiler will not allow this field to be reassigned after it is initialized. This differs from immutability in reference types, whose state may be modified even when a variable is marked final.
  • The == operator checks reference equality between objects. To compare the states of the objects, override and use the Object.equals() method.

Exercises

Exercise 11.1: Check Your Understanding
(a)

Consider a Java program consisting of the following static methods:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
  int x = Integer.parseInt(args[0]);
  try {
    f2(x);
  } catch (RuntimeException e) {
    // Do nothing
  }
  int z = 2 * x; // Statement A
}

static void f2(int a) {
  f3(2, a);
  int c = 3 - a; // Statement B
}

static void f3(int x, int y) {
  int z = x / y; // Could throw an ArithmeticException
  int a = x + y; // Statement C
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
  int x = Integer.parseInt(args[0]);
  try {
    f2(x);
  } catch (RuntimeException e) {
    // Do nothing
  }
  int z = 2 * x; // Statement A
}

static void f2(int a) {
  f3(2, a);
  int c = 3 - a; // Statement B
}

static void f3(int x, int y) {
  int z = x / y; // Could throw an ArithmeticException
  int a = x + y; // Statement C
}
If the program is passed an argument of 0, an ArithmeticException will be thrown. Which statement will be executed immediately after the exception is thrown?
Check Answer
(b)

The constructor for FileOutputStream <: OutputStream declares that it throws FileNotFoundException (observe that FileNotFoundException <: IOException <: Exception). You are implementing a method currently defined as follows:

1
2
3
4
public void writeScores() {
  OutputStream out = new FileOutputStream("scores.txt");
  // Code that writes scores to `out`, no `throw` statements are present.
}
1
2
3
4
public void writeScores() {
  OutputStream out = new FileOutputStream("scores.txt");
  // Code that writes scores to `out`, no `throw` statements are present.
}
Which of the following changes to writeScores() would individually be sufficient to allow it to compile?
Check Answer
(c)

Consider the following partial class definition.

1
2
3
4
5
6
public class T {
  public final int x;
  private final double[] y;
  protected String z;
  // ...
}
1
2
3
4
5
6
public class T {
  public final int x;
  private final double[] y;
  protected String z;
  // ...
}
Which of the following are true?
Check Answer
(d)
Which of the following properties must an override equals() method uphold?
Check Answer
Exercise 11.2: Tracing Exceptions
For each of the following, state whether an exception is thrown. If so, state the most specific type of the exception that caused the program to crash. If not, state what is printed out when the program is executed.
(a)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public static void main(String[] args) {
  int b = 6;
  try {
    b = 1;
    int a = 3 / 0;
    b = 4;
    System.out.println("Done");
  } catch (RuntimeException e) {
    b = 3;
  }
  System.out.println(b);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public static void main(String[] args) {
  int b = 6;
  try {
    b = 1;
    int a = 3 / 0;
    b = 4;
    System.out.println("Done");
  } catch (RuntimeException e) {
    b = 3;
  }
  System.out.println(b);
}
(b)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static void f() throws Exception {
  throw new IOException();
}

public static void main(String[] args) {
  try {
      f();
  } catch (IOException e) {
      System.out.println("Caught IOException");
  } catch (Exception e) {
      System.out.println("Caught Exception");
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static void f() throws Exception {
  throw new IOException();
}

public static void main(String[] args) {
  try {
      f();
  } catch (IOException e) {
      System.out.println("Caught IOException");
  } catch (Exception e) {
      System.out.println("Caught Exception");
  }
}
(c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void printLength(String str) {
  System.out.println(str.length());
} 

static void printStringArray(String[] arr) {
  for (int i = 0; i <= arr.length; i++) {
    System.out.println(arr[i]);
  }
}

public static void main(String args[]) {
  String[] a = new String[4];
  try {
    printLength(a[0]);
  } catch (NullPointerException e) {
    a = new String[]{"A", "B", "C", "D"};
    printStringArray(a);
  } catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("All caught!");
  }
  System.out.println(a);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void printLength(String str) {
  System.out.println(str.length());
} 

static void printStringArray(String[] arr) {
  for (int i = 0; i <= arr.length; i++) {
    System.out.println(arr[i]);
  }
}

public static void main(String args[]) {
  String[] a = new String[4];
  try {
    printLength(a[0]);
  } catch (NullPointerException e) {
    a = new String[]{"A", "B", "C", "D"};
    printStringArray(a);
  } catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("All caught!");
  }
  System.out.println(a);
}
Exercise 11.3: Defining and Handling Custom Exceptions
Recall the Account.java class in last lecture. In both depositFunds() and transferFunds(), we require that amount > 0 as part of the precondition. Let's refactor these methods to throw exceptions instead.
(a)
Define a new exception called NegativeValueException that extends Exception.
(b)
Rewrite depositFunds() and transferFunds() to throw NegativeValueException when a client passes in a negative value for amount. Modify the specifications accordingly. Note that NegativeValueException is checked.
(c)
Imagine you are the client of the SavingsAccount class, which doesn’t override any of the above methods. Write client code to deposit a negative amount in a try block. Handle this exception in a catch block that prints a helpful statement.
Exercise 11.4: Adding More Behaviors to Point
We can support more behaviors to the Point class. For each of the following, implement the method according to its specifications and write specifications if not given.
(a)

Add a method to return the midpoint between two points. The midpoint \((x_m, y_m) \in \mathbb{R}^2 \) between two points \((x_1, y_1), (x_2, y_2) \in\mathbb{R}^2\) has coordinates given by the equations,

\[ x_m=\frac{x_1+x_2}{2} \hspace{50pt} y_m=\frac{y_1+y_2}{2} \]
1
public Point midpoint(Point other) { ... }
1
public Point midpoint(Point other) { ... }
(b)

Given some point \( (x_0, y_0) \in \mathbb{R}^2 \) , its rotation about \( (x_c, y_c) \in \mathbb{R}^2 \) for \( \theta \in \mathbb{R} \) radians counterclockwise is \( (x_1, y_1) \in \mathbb{R}^2 \), where

\[ \begin{align*} x_1 &= (x_0-x_c)\cos(\theta) - (y_0-y_c)\sin(\theta) + x_c\\ y_1 &= (x_0-x_c)\sin(\theta) - (y_0-y_c)\cos(\theta) + y_c \end{align*} \]
1
public Point rotate(double x, double y, double theta) { ... }
1
public Point rotate(double x, double y, double theta) { ... }
(c)

Implement these methods using the rotate() and reflectOver() methods defined above.

1
2
3
4
5
6
7
8
/** Rotates `this` about the origin `theta` radians counterclockwise. */
public Point rotateAboutOrigin(double theta) { ... }

/** Reflects `this` across the x-axis. */
public Point reflectAcrossX() { ... }

/** Reflects `this` across the y-axis. */
public Point reflectAcrossY() { ... }
1
2
3
4
5
6
7
8
/** Rotates `this` about the origin `theta` radians counterclockwise. */
public Point rotateAboutOrigin(double theta) { ... }

/** Reflects `this` across the x-axis. */
public Point reflectAcrossX() { ... }

/** Reflects `this` across the y-axis. */
public Point reflectAcrossY() { ... }
(d)

These transformations can be generally written in the form of a matrix times a vector. Given a matrix \( A \in \mathbb{R}^{2\times2} \), we apply the transformation to a point \( (x, y) \in \mathbb{R}^2\) by computing:

\[ A \vec{x} = \begin{bmatrix} a & b \\ c & d \end{bmatrix} \begin{bmatrix} x\\ y \end{bmatrix} = \begin{bmatrix} ax+by\\ cx+dy \end{bmatrix} \]

Note that InvalidArgumentException <: RuntimeException.

1
2
3
4
5
6
/**
 * Transforms `this` by applying the linear transformation described by 
 * the given `matrix`. Throws an `InvalidArgumentException` when either
 * `matrix.length != 2` or `matrix[i].length != 2` for `0 <= i < 2`.
 */
public Point transform(double[][] matrix) { ... }
1
2
3
4
5
6
/**
 * Transforms `this` by applying the linear transformation described by 
 * the given `matrix`. Throws an `InvalidArgumentException` when either
 * `matrix.length != 2` or `matrix[i].length != 2` for `0 <= i < 2`.
 */
public Point transform(double[][] matrix) { ... }
Exercise 11.5: Testing Exceptions
(a)
Write two test cases that asserts NegativeValueException is thrown in the modified depositFunds() and transferFunds() methods of Account.java that you wrote in Exercise 11.3.b.
(b)
Write test cases that assert that IllegalArgumentExceptions are thrown according to the specs for the method in Exercise 11.4.d.
Exercise 11.6: Lines and Points
Let's move up a dimension! We want to define an immutable Line class that works closely with Point.
1
2
3
4
5
/** 
 * An immutable class representing the line through two points in the 2D coordinate 
 * plane. 
 */
public record Line(Point p1, Point p2) { ... }
1
2
3
4
5
/** 
 * An immutable class representing the line through two points in the 2D coordinate 
 * plane. 
 */
public record Line(Point p1, Point p2) { ... }
Write a method (documented with appropriate specifications) for each of the following described behaviors. Consider which methods make sense as static methods and which make sense as instance methods.
(a)
Write a method that returns the slope() of a Line.
(b)
Write a method that returns the yIntercept() of a Line. How will you handle lines that do not have a y-intercept?
(c)
Override the equals() method in the Line record class so that two Lines are equal when they have the same slope and intersect at least one point.
(d)
Given a Point and a slope, return the Line given by the point-slope formula.
(e)
Given two Lines, return their Point of intersection. How will you handle two lines with infinite or zero points of intersection?
(f)
Given two Points, return the Line between them.
Exercise 11.7: Immutable Fractions
We want to develop an immutable fraction class to represent a simplified (and possibly improper) fraction. Study the partial class definition:
1
2
3
4
5
6
7
8
/** A simplified improper fraction. */
public class Fraction {
  /** The numerator. Requires `gcd(numerator, denominator) = 1`. */
  private final int numerator;

  /** The denominator. Requires `denominator > 0`. */
  private final int denominator;
}
1
2
3
4
5
6
7
8
/** A simplified improper fraction. */
public class Fraction {
  /** The numerator. Requires `gcd(numerator, denominator) = 1`. */
  private final int numerator;

  /** The denominator. Requires `denominator > 0`. */
  private final int denominator;
}
(a)
Define a private helper method assertInv() to assert the class invariants. For more information about gcd(), view Discussion 1.
(b)
Add methods to add and multiply Fractions. Keep in mind the class invariant!
(c)
Add methods to subtract and divide Fractions. Can you use the methods defined in the last subproblem to simplify the implementation?
(d)
Override the toString() and equals() methods from Object. The implementation details are up to you, but remember to refine the specifications and properties of equals()!
(e)
Refactor Fraction to use a record class.
Exercise 11.8: Accounts as Strings
Override the toString() method in the Account class and its subclasses to produce a sensible String representation.
Exercise 11.9: Equality Beyond Exact Match
Suppose we want to represent a URL address. We say that two URLs equal each other if their origins match, that is if they have the same scheme, hostname, and port.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/** Represents a URL address. */
public class URL {
  /** The scheme of the URL. One of `http` or `https`. */
  private String scheme;

  /** The host name of the URL, e.g., courses.cis.cornell.edu. */
  private String hostname;
  
  /** The port of the URL. Requires 0 <= `port` < 2^16. */
  private int port;

  /** The path of the URL, with each element being a segment of the path. */
  private String[] path;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/** Represents a URL address. */
public class URL {
  /** The scheme of the URL. One of `http` or `https`. */
  private String scheme;

  /** The host name of the URL, e.g., courses.cis.cornell.edu. */
  private String hostname;
  
  /** The port of the URL. Requires 0 <= `port` < 2^16. */
  private int port;

  /** The path of the URL, with each element being a segment of the path. */
  private String[] path;
}
(a)

Override the equals() method according to its specifications.

1
2
3
4
5
6
/**
 * Returns whether `this` URL has the same origin as `obj`. Two URLs have the same
 * origin if (and only if) their scheme, hostname, and port all match.
 */
@Override
public boolean equals(Object obj) { ... }
1
2
3
4
5
6
/**
 * Returns whether `this` URL has the same origin as `obj`. Two URLs have the same
 * origin if (and only if) their scheme, hostname, and port all match.
 */
@Override
public boolean equals(Object obj) { ... }
(b)
By the specifications, it can be the case that two URL objects have different elements in its path field but evaluate to true. Is this acceptable?
(c)

Override the toString() method according to its specifications. The formatted URL is given by the following strings concatenated together: scheme, “://”, hostname, “:”, port, “/”, path. The formatted path is the elements of path joined together with "/".

1
2
3
4
5
/**
 * Returns the formatted URL of `this`.
 */
@Override
public String toString() { ... }
1
2
3
4
5
/**
 * Returns the formatted URL of `this`.
 */
@Override
public String toString() { ... }
Exercise 11.10: Validating equals()
For each of the following implementations of equals() determine if it is a valid override. If it is not, state which property or properties are violated by the implementation.
(a)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Widget {
  private final int x;
  private final int y;
  private final boolean usesX;

  @Override
  public boolean equals(Object obj) {
    if ((obj == null) || (this.getClass() != obj.getClass())) {
      return false;
    }
    Widget w = (Widget) obj;
    if (usesX) {
      return this.x == w.x;
    } else {
      return this.y == w.y;
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Widget {
  private final int x;
  private final int y;
  private final boolean usesX;

  @Override
  public boolean equals(Object obj) {
    if ((obj == null) || (this.getClass() != obj.getClass())) {
      return false;
    }
    Widget w = (Widget) obj;
    if (usesX) {
      return this.x == w.x;
    } else {
      return this.y == w.y;
    }
  }
}
(b)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Player {
  private String playerName;
  private int jerseyNo;
  private String team;

  @Override
  public boolean equals(Object obj) {
    if ((obj == null) || (this.getClass() != obj.getClass())) {
      return false;
    }
    Player pl = (Player) obj;
    if (this.jerseyNo < pl.jerseyNo) {
      return playerName.equals(pl.playerName) && team.equals(pl.team);
    }
    return playerName.equals(pl.playerName);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Player {
  private String playerName;
  private int jerseyNo;
  private String team;

  @Override
  public boolean equals(Object obj) {
    if ((obj == null) || (this.getClass() != obj.getClass())) {
      return false;
    }
    Player pl = (Player) obj;
    if (this.jerseyNo < pl.jerseyNo) {
      return playerName.equals(pl.playerName) && team.equals(pl.team);
    }
    return playerName.equals(pl.playerName);
  }
}