Java Arena
Step 2 of 5

Handlers that remember

A pomodoro is 25 minutes of focused work, then a short break. A window counts the pomodoros finished today: a label with the count, and a button to add one. The first try keeps the count in a variable of start:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage stage) {
        Label done = new Label("Pomodoros today: 0");
        Button finish = new Button("Finished one");
        int count = 0;
        finish.setOnAction(event -> {
            count++;
            done.setText("Pomodoros today: " + count);
        });

        stage.setScene(new Scene(new VBox(done, finish)));
        stage.setTitle("Pomodoros");
        stage.show();
    }

    public static void main(String[] args) {
        launch(Main.class);
    }
}
What javac prints
Main.java:16: error: local variables referenced from a lambda expression must be final or effectively final
            count++;
            ^
Main.java:17: error: local variables referenced from a lambda expression must be final or effectively final
            done.setText("Pomodoros today: " + count);
                                               ^
2 errors

javac complains twice: once where the lambda changes count, and once where it reads it. Module 37 met this message: a lambda may use the local variables of its method (and its parameters) only when they are effectively final, which means never changed after they get their value. count++ changes count.

Here's why Java has the rule. start runs once and returns before anyone clicks, and its local variables are gone after that. So a lambda doesn't keep the variable itself: it keeps a copy of its value, made when the lambda was made. If the variable could change later, the copy and the variable would disagree. A variable that never changes has a copy that is always right.

The lambda uses done too, and javac doesn't mind: done never changes, since it always refers to the same Label. The label's text changes, but that's a change inside the Label object, not a change of the variable.

A count that changes needs a place that outlives start. There are three usual ones.

An instance variable

An instance variable belongs to the Main object, which JavaFX made before it called start, and which lives as long as the program does. The rule about effectively final variables is only for local variables and parameters, so a lambda can change an instance variable:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {
    private int pomodoros;

    @Override
    public void start(Stage stage) {
        Label done = new Label("Pomodoros today: 0");
        Button finish = new Button("Finished one");
        Button reset = new Button("Reset");
        finish.setOnAction(event -> {
            this.pomodoros++;
            done.setText("Pomodoros today: " + this.pomodoros);
        });
        reset.setOnAction(event -> {
            this.pomodoros = 0;
            done.setText("Pomodoros today: 0");
        });

        stage.setScene(new Scene(new VBox(done, new HBox(finish, reset))));
        stage.setTitle("Pomodoros");
        stage.show();
    }

    public static void main(String[] args) {
        launch(Main.class);
    }
}
The window

After: Click "Finished one", click "Finished one", then click "Finished one"

This window is wider than the screen: scroll it sideways, or read it as text below.

The window as text
Window "Pomodoros"
  VBox
    Label "Pomodoros today: 3"
    HBox
      Button "Finished one"
      Button "Reset"

The lambdas are written inside start, so this in them is the object whose start is running: the Main object. this.pomodoros is the same variable at every click, and it keeps its value between them.

An object that counts

An object can hold the count instead. PomodoroLog is a small class of its own, and start makes one:

Main.java
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage stage) {
        PomodoroLog log = new PomodoroLog();
        Label done = new Label("Pomodoros today: 0");
        Button finish = new Button("Finished one");
        Button reset = new Button("Reset");
        finish.setOnAction(event -> {
            log.finishOne();
            done.setText("Pomodoros today: " + log.count());
        });
        reset.setOnAction(event -> {
            log.reset();
            done.setText("Pomodoros today: " + log.count());
        });

        stage.setScene(new Scene(new VBox(done, new HBox(finish, reset))));
        stage.setTitle("Pomodoros");
        stage.show();
    }

    public static void main(String[] args) {
        launch(Main.class);
    }
}
PomodoroLog.java
public class PomodoroLog {
    private int count;

    public void finishOne() {
        this.count++;
    }

    public void reset() {
        this.count = 0;
    }

    public int count() {
        return this.count;
    }
}
The window

After: Click "Finished one", click "Finished one", click "Reset", then click "Finished one"

This window is wider than the screen: scroll it sideways, or read it as text below.

The window as text
Window "Pomodoros"
  VBox
    Label "Pomodoros today: 1"
    HBox
      Button "Finished one"
      Button "Reset"

log is a local variable, but an effectively final one: it refers to the same PomodoroLog object all the time. What changes is the object's instance variable, as with the label's text. Step 4 builds on this: a class of its own that does the work, and handlers that only call it.

The window itself

When a label shows nothing but the number, the handler can read the number back from the label:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage stage) {
        Label visitors = new Label("0");
        Button plus = new Button("+1");
        plus.setOnAction(event -> {
            int now = Integer.valueOf(visitors.getText());
            visitors.setText("" + (now + 1));
        });

        stage.setScene(new Scene(new HBox(10, plus, visitors)));
        stage.setTitle("Door counter");
        stage.show();
    }

    public static void main(String[] args) {
        launch(Main.class);
    }
}
The window

After: Click "+1", click "+1", click "+1", then click "+1"

This window is wider than the screen: scroll it sideways, or read it as text below.

The window as text
Window "Door counter"
  HBox spacing 10
    Button "+1"
    Label "4"

That's fine for a small window like this one. When the label says more than the number, or several handlers share the count, an instance variable or an object is clearer.

A value read too early

A lambda's copy of a variable has the value the variable had when the lambda was made. That matters when the value comes from the window. A hotel's wake-up call service:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage stage) {
        TextField room = new TextField();
        room.setPromptText("Room number");
        Button book = new Button("Wake me at 7");
        Label booked = new Label("");
        String number = room.getText();
        book.setOnAction(event -> booked.setText("Wake-up call at 7 for room " + number));

        stage.setScene(new Scene(new VBox(booked, room, book)));
        stage.setTitle("Wake-up calls");
        stage.show();
    }

    public static void main(String[] args) {
        launch(Main.class);
    }
}
The window

After: Type "214" in text field 1, then click "Wake me at 7"

This window is wider than the screen: scroll it sideways, or read it as text below.

The window as text
Window "Wake-up calls"
  VBox
    Label "Wake-up call at 7 for room "
    TextField "214", prompt "Room number"
    Button "Wake me at 7"

The room number is missing. number got its value once, when start ran, and the field was empty then. To use what the user typed, read the field inside the handler: booked.setText("Wake-up call at 7 for room " + room.getText()).

Your task

Challenge 1 of 3

A water tracker doesn't compile: it keeps the number of glasses in a local variable, which its lambdas change. Keep the count in a private instance variable of Main instead, so that Drink one adds a glass and New day starts again from 0. Don't change the window or the texts. The task card shows the window after two clicks on Drink one.

Challenge 2

A delivery note gets the name of whoever signs for a parcel. Watch which value each part of the handler uses, and when the label changes. What does the program print?

Challenge 3

A bike's handlebar display shows its gear. The window is built already: the label Gear 1, and under it the buttons Down and Up. Give the buttons their handlers. Up shifts one gear up and Down one gear down, and the label shows the gear, like Gear 4. The bike has gears 1 to 7: Up does nothing in gear 7, and Down does nothing in gear 1 (keep both buttons enabled). The task card shows the window after three clicks on Up.