Lecture from: 01.11.2024 | Video: Videos ETHZ

Working with null

In Java, reference type variables (variables that hold objects, arrays, or strings) can have a special value called null. Understanding null is crucial for avoiding common errors and writing robust code.

What is null?

null represents the absence of a value. It indicates that a reference variable does not currently point to any valid object, array, or string in memory.

  • Not an Object: null itself is not an object. It’s a special keyword.
  • Default Value: Reference type variables are automatically initialized to null if you don’t explicitly assign them a value.

null vs. null

The expression null == null evaluates to true. You can compare a reference variable to null to check if it currently refers to an object.

The NullPointerException

The dreaded NullPointerException occurs when you try to access a member (attribute or method) of a variable that is currently null. This is because null doesn’t point to an object, so there are no members to access.

Example:

String name = null;
int length = name.length(); // NullPointerException: name is null
Cat[] cats = new Cat[3]; // Elements are initially null
String upperName = cats[0].name.toUpperCase(); // NullPointerException: cats[0] is null.

Preventing NullPointerExceptions

If you try to access an attribute or method of a null reference, you get a NullPointerException.

1. Check for null before dereferencing: This is the most common and effective way to prevent NullPointerExceptions.

if (name != null) {
    int length = name.length(); // Safe: name is not null
}
 
// Or, using a temporary variable for clarity, especially with nested access:
 
Cat cat = cats[0];
if (cat != null && cat.name != null) {  // Check both for null!
  String upperName = cat.name.toUpperCase();
}

2. Use the Optional class (Advanced): (Beyond EProg scope currently.) The Optional class provides a more structured way to handle situations where a value might be absent.

3. Careful Initialization: Initialize variables to non-null values whenever possible. This can sometimes eliminate the need for null checks. However, it’s not always feasible or appropriate.

4. Assertions (For Debugging): In development, use assertions to check for unexpected null values. Assertions are checked at runtime and will throw an error if the assertion fails, helping you identify potential problems early.

assert name != null : "Name should not be null here!";

5. Handle Potential null Returns from Methods: Be aware that methods can return null. Always check for null on return values if the method documentation indicates it’s a possibility.

Null Checks and Conditional Logic

Be mindful of how null checks interact with conditional logic, especially with && (AND) and || (OR).

  • Short-Circuiting: Java uses short-circuit evaluation. With &&, if the left side is false, the right side is not evaluated. With ||, if the left side is true, the right side is not evaluated.
// Safe because of short-circuiting:
if (obj != null && obj.someMethod()) { 
    // ... only called if obj is not null
}
 
if (obj == null || !obj.isValid()) {
  // ...
}

Methods

Methods define the behavior of a class. They are functions that operate on the object’s state (its attributes).

Defining Methods

Methods are declared inside the class body, similar to functions in procedural programming.

Syntax:

public class ClassName {
  // ... attributes
 
  returnType methodName(type1 param1, type2 param2, ...) {
    // Method body (statements)
    // ...
    return value; // If returnType is not void
  }
}

Object Methods vs. Static Methods (Preview – More Detail Later)

  • Object Methods (Instance Methods): Operate on a specific object’s state. They have access to the object’s attributes via the this reference.
  • Static Methods (Class Methods): Not associated with a specific object. They cannot access instance attributes or use the this keyword. We’ll discuss static methods in more detail later.

The toString() Method

Every Java class inherits a toString() method from the Object class. This method is called implicitly when you try to print an object or concatenate it with a string. The default implementation of toString() returns a string representation of the object’s memory address, which is often not very helpful.

Example:

Point p = new Point(2, -2);
System.out.println(p); // Output: Something like "Point@21213b92"

Overriding toString()

To provide a more meaningful string representation, you can override the toString() method in your class.

Example:

public class Point {
    // ... attributes
 
    @Override // <- this is a best practice and is recommended
    public String toString() {
        return "(" + x + ", " + y + ")";
    }
}
 
Point p = new Point(2, -2);
System.out.println(p); // Output: "(2, -2)"
System.out.println("p = " + p);  // Output: "p = (2, -2)"

Important: The overriding toString() method must have the exact signature public String toString().

Nested Structures and toString()

If you have nested objects (e.g., an array of Point objects), calling toString() on the outer object will implicitly call toString() on each of the inner objects.

Example:

Point[] trace = new Point[2];
trace[0] = new Point(0, 1);
trace[1] = new Point(3, 0);
 
System.out.println(Arrays.toString(trace)); // Output: "[(0, 1), (3, 0)]"

Constructors

Constructors are special methods used to initialize the state of an object when it’s created. They have the same name as the class and no return type (not even void).

Defining Constructors

public class ClassName {
    // ... attributes
 
    public ClassName(type1 param1, type2 param2, ...) {
        // Initialization statements
        this.attribute1 = param1;
        // ...
    }
}

Constructor Execution

A constructor is automatically called when an object is created using the new operator.

Example: Point Constructors

public class Point {
    int x;
    int y;
 
    public Point(int x, int y) {  // Constructor with parameters
        this.x = x;
        this.y = y;
    }
 
    public Point(Point other) { // Constructor taking another Point object
        assert other != null; //Prevents copying from null references
        this.x = other.x;
        this.y = other.y;       
    }
 
    public Point() {          // Constructor without parameters (no-arg constructor)
        this.x = 0;          // Initialize x and y to 0
        this.y = 0;
    }
}
 
Point p1 = new Point(2, 9);    // Calls the first constructor
Point p2 = new Point(p1);     // Calls the second constructor (copy constructor)
Point p3 = new Point();       // Calls the third constructor (no arguments)

Default Constructor

If you don’t define any constructors for a class, Java automatically provides a default constructor with no parameters. This default constructor initializes all instance variables to their default values (0, false, or null). However, if you define any constructor yourself, the default constructor is not automatically provided.

Delegating Constructors

Constructors can call other constructors within the same class using this(parameters). This is called constructor delegation and can help reduce redundant code. The delegating call must be the first statement in the constructor.

Example:

public class User {
    String name;
    int age;
    Point location;
 
    public User(String name, int age, Point location) {
        this.name = name;
        this.age = age;
        this.location = location;
    }
 
    public User(String name) {  // Delegating constructor
        this(name, -1, null);    // Calls the other constructor with default values for age and location
    }
}
 
User user1 = new User("Jana", 75, new Point(10, 20));
User user2 = new User("Peter"); // Uses the delegating constructor, age and location will be -1 and null respectively

Common Constructor Errors

  1. Shadowing: Accidentally declaring local variables with the same name as attributes inside the constructor, effectively hiding the attributes. Use this.attributeName to refer to attributes unambiguously.

  2. Return Type: Declaring a return type (even void) for a constructor. Constructors don’t have return types.

  3. Missing Default Constructor: Trying to use a no-argument constructor after defining a constructor with parameters, without also explicitly defining a no-argument constructor.

Continue here: 13 Visibility Modifiers, Object Invariants, Static Methods, Final and Attributes