Lec 07 - Immutability and Nested Classes

So far in this course, we have been focusing on three ways of dealing with software complexity:

  1. by encapsulating and hiding the complexity behind abstraction barriers,

  2. using a language with a strong type system and adhering to the subtyping substitution principle,

  3. and applying the abstraction principles and reusing code written as functions, classes, and generics types.

This summarizes the Lec 01 - Lec 06 so perfectly!

Immutability

The fourth way of dealing with software complexity is to make our classes immutable.

An immutable class means that: the internal fields of the same instance from this immutable class cannot be changed outside its abstraction. (a.k.a If you do make a change, then likely you will get a new instance and it is not the same instance as before anymore).

Write Immutable Class

Basically, there are two steps we should follow to make a class immutable

1

Add final to class declaration to prevent inheritance

This is to prevent subclass of immutable class to override some methods to make the internal field of the immutable class mutable.

In Java, if a class is final, it cannot be inherited!

2

Make the class's internal fields immutable

This can be done by many ways:

  1. For fields of primitive types, simply declare them as final. For other fields, make sure they are of immutable type! (a.k.a we can then create other larger immutable classes by only using immutable classes as fields.)

  2. Have a class that prevents any and all kinds of sharing by copying all the parameters before assigning them to the fields and copying all return values. (See Another way to make internal fields immutable)

For example, the following are two good examples of the immutable Point and Circle class.

Point.java
final class Point {
  private final double x;
  private final double y;

  public Point(double x, double y) {
    this.x = x;
    this.y = y;
  }

  public Point moveTo(double x, double y) {
    return new Point(x, y);
  }
    :

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

We start by making the fields final to signal our intention that we do not intend to assign another value to them. Now that the x and y cannot be re-assigned (a new value or even the same value), to move a point, we shouldn't re-assign to the fields x and y anymore. Instead, we return a new Point instance to prevent mutating the current instance.

Returning a new instance is a recommended and useful practice in writing immutable class!

Similarly, we can write our immutable class Circle.

Another way to make internal fields immutable

In fact, there is another way to make the internal fields of an immutable class immutable. That is,

to have a class that prevents any and all kinds of sharing by copying all the parameters before assigning them to the fields and copying all return values.

For example, we can rewrite our immutable Circle class as follows:

Here, the .clone() is used to make a copy of the value of internal fields.

Takeaway

  1. That is not to say that the final keyword is not important. It helps accidental re-assignment and in some cases, that is sufficient especially if the fields are of primitive type.

  2. Once we have created one immutable class, we can then create other larger immutable classes by only using immutable classes as fields.

Advantages of Immutable Class

Ease of Understanding

Code written with immutable objects is easier to reason with and easier to understand because once we create an instance from an immutable class, its internal fields will never change unless we have explicitly re-assigned it.

Enabling Safe Sharing of Objects

This means we can now safely create a class field to represent an immutable property. For example, if our Point class is immutable, we can create a class field called ORIGIN to always represent the point (0, 0).

Enabling Safe Sharing of Internals

This advantage also utilizes the fact that the internal fields of an instance from an immutable class are immutable. See more from the lecture notes.

Enabling Safe Concurrent Execution

This won't be discussed for now. But imagine having code where we have to ensure its correctness regardless of how the execution interleaves! That's interesting right?

Nested Class

Definition: A nested class is a class defined within another containing class. For example,

class A is a containing class and class B is a nested class.

Use: It usage is to encapsulate information within a containing class.

Property: A nested class is a field of the containing class and can access fields and methods of the containing class, including those declared as private.

A nested class can be classified into the following four types

  1. Inner class

  2. Static nested class

  3. Local class

  4. Anonymous class

Inner class

It is a non-static nested class, thus it can access all fields and methods of the containing class.

Qualified this

This is to resolve the issue above. A qualified this reference is prefixed with the containing class name, to differentiate between the this of the inner class and the this of the containing class. In the example above, we can access x from A through the A.this reference.

To create an instance of the inner class, we can see the following code

Static nested class

It is a static nested class, thus it is associated with the containing class, NOT an instance.

So, by default, it can only access static fields and static methods of the containing class. If you want to access the fields of the containing class, you can do so by using object reference.

  1. When you instantiate a static nested class, you can ignore the containing class's instance. static only restricts that the nested class is associated with the containing class itself, NOT with any instance.

  2. But inside the static nested class, if you want to access non-static (instance) members of the containing class, you need an explicit reference to an instance of the containing class, because this in the nested class does not carry a reference to an enclosing B instance.

Hiding nested class

The notes is very detailed. Please go back and refer to it if needed.

To put it simply, the nested class should be defined as private for the sake of not breaking the abstract barrier. This makes calling the nested class explicitly not allowed. However, calling a public method from the containing class that returns an instance of the private nested class is still allowed. For example,

Since the type A.B is private to within A, we cannot call methods of B outside of A as well.

Local class

It is a nested class declared within a function, just like a local variable. The local class has access to all the local variables from within the method it is declared, as well as the fields of its containing class.

The declaration and the instantiation of a local class is usually seperated.

Variable Capture

It is a behavior that the local class will capture the following variables

  1. The local variables of the method where the local class comes from (including only the arguments that the lambda uses, see more in Diagnostic Quiz Q13)

  2. The instance that invokes the method where the local class comes from. (See more in Rec 05)

Effectively final

Effectively final means that an implicitly final variable cannot be re-assigned after they are captured.

The use of this rule is because variable capture can sometimes be confusing, thus Java enforces a rule that only final or effectively final local variables can be captured. If the variables captured are neither final nor effectively final, then a compile error will be generated!

  1. Note, this "re-assignment" is usually associated with primitive type. If we use the reference type, we can mutate the value of the instance instead of re-assigning. Thus, the second one with mutation is allowed in Java.

  2. The "effectively final" rule applies to reading the variable’s value, not to enabling assignments. See more from Diagnostic Quiz Q12.

Anonymous Class

An anonymous class is one where you declare a local class and instantiate it in a single statement.

It has the following format: new X (arguments) { body }, where:

  1. X is a class that the anonymous class extends or an interface that the anonymous class implements.

  2. arguments are the arguments that you want to pass into the constructor of the anonymous class.

  3. body is the body of the class as per normal, except that we cannot have a constructor for an anonymous class.

Last updated