Learn Java

Lesson 80Elective: Desktop apps with JavaFX

Event handling and GUI programs

What an event is, the ways to write a handler, how events travel, and four complete programs: areas, perfect numbers, interest and EMI, and Armstrong numbers.

By Updated 75 min

  • event
  • event source
  • EventHandler
  • ActionEvent
  • setOnAction
  • anonymous class

The registration form in the last lesson came alive through one call, submitButton.setOnAction(...), and the block of code inside it that ran whenever Submit was pressed. This lesson is about that call. You will see what JavaFX hands your code when the user clicks, the ways to write and attach a handler, and the route an event takes through the window before your code runs. Then you will build four event-driven programs, from an area calculator to an Armstrong number checker, each of which turns text typed into a field into a number, checks it, and shows a result.

By the end of this lesson
  • Explain event handling in JavaFX by naming the event, the event source and the event handler, and say which event class a click, a key press or a closing window produces.
  • Write a handler as an anonymous inner class, as a lambda and as a method reference, and register it with setOnAction or addEventHandler.
  • Explain why a local variable used inside a handler must be effectively final.
  • Trace an event through target selection, the capturing phase and the bubbling phase, and stop it with consume().
  • Guard a handler against bad input with NumberFormatException and IllegalArgumentException, caught in the order the compiler requires.
  • Build and test four programs: an area calculator, perfect number checker, interest calculator, Armstrong number or factorial.
From before
A handler is an object that implements an interface with a single method. This lesson writes that object in two ways you already know: as an anonymous inner class and as a lambda. The four programs protect their arithmetic with try and catch, the arithmetic itself is the digit and divisor loops from loops, and their windows are GridPanes like the one in form creation.

What an event is#

A console program decides its own order. It runs main from top to bottom, and when it needs input it stops at scanner.nextInt() and waits. A GUI program hands that control to the user. start builds the window and returns, and from then on the program waits for the user to act: press a button, type a key, close the window. Each action is an event, and which of your code runs next depends on which event arrives. This style is called event-driven programming.

You do not write the waiting. Glass, the windowing layer from the JavaFX overview, receives each mouse and keyboard action from the operating system. JavaFX turns it into an object, works out which node it is meant for, and calls the code you attached to that node for that kind of event. All of this happens on one thread, the JavaFX Application Thread. Your part is to attach the code.

Event handling has three parts. For a click on the area calculator's Calculate Circle Area button they are:

  • The event. An object describing what happened, of class javafx.event.Event or a subclass, here ActionEvent. It records its type (getEventType(), here ActionEvent.ACTION), its source (getSource(), the object whose handler is running) and its target (getTarget(), the node the event is aimed at). Mouse and key events also carry details: the pointer's coordinates, which mouse button, which key.
  • The event source. The node the user acted on, here circleButton. Handlers are registered on it, and it passes each event to them.
  • The event handler. The code that runs in response, registered on the source before any event happens: circleButton.setOnAction(e -> calculateCircleArea(circleInput.getText())). It is an object implementing EventHandler, and JavaFX calls its handle method once for every event.
Event classPackageFired whenRegistered with
ActionEventjavafx.eventA Button is pressed (by mouse, by Space, or by Enter on a default button), Enter is pressed in a TextField, a MenuItem is chosen, a CheckBox is ticked or untickedsetOnAction
MouseEventjavafx.scene.inputA mouse button is pressed, released or clicked over a node, or the pointer moves, drags, enters or leaves itsetOnMouseClicked, setOnMousePressed, setOnMouseEntered and so on
KeyEventjavafx.scene.inputA key is pressed, released or typed while the node has keyboard focussetOnKeyPressed, setOnKeyReleased, setOnKeyTyped
WindowEventjavafx.stageA window is shown or hidden, or the user asks to close itsetOnShown, setOnHidden, setOnCloseRequest on the stage
The event classes you meet first. All four extend javafx.event.Event.

One click produces more than one event. Pressing the mouse button over circleButton fires a MouseEvent of type MOUSE_PRESSED; letting go fires MOUSE_RELEASED and then MOUSE_CLICKED. The button's built-in behaviour watches the press and the release and, when both happen over it, fires one more event of its own: an ActionEvent aimed at itself. That is the event setOnAction listens for. An ActionEventmeans “the button was used”, by whatever means, so the same handler also runs when the user presses Space while the button has focus, or Enter when it is the default button, as Submit is in the registration form.

Key idea

You never ask whether the button was clicked. You give JavaFX a handler in advance, and JavaFX calls it, with an event object describing the click, at the moment the click happens.

EventHandler: one interface, one method#

Every handler, however it is written, ends up as an object of a class that implements this interface from javafx.event:

EventHandler.java (in javafx.event)
read along
1@FunctionalInterface
2public interface EventHandler<T extends Event> extends EventListener {
3 void handle(T event);
4}

<T extends Event> is a type parameter. A handler declares which kind of event it accepts, and T must be Event or one of its subclasses: an EventHandler<ActionEvent> takes action events, an EventHandler<MouseEvent> takes mouse events, and the compiler will not let you register one where the other is expected. handle is the method JavaFX calls, passing the event in. Because it is the only abstract method, EventHandler is a functional interface, so wherever one is expected you can write a lambda instead of a whole class. (EventListener is an empty marker interface from java.util; it adds no methods.)

Registering a handler#

A handler does nothing until it is registered on a node. For each common event type, nodes have a convenience method named setOnfollowed by the event. These four lines could go into the area calculator's start method, one for each event class in the table:

SimpleCalculator.java (extra handlers)
read along
1// ActionEvent: Enter pressed inside the radius field
2circleInput.setOnAction(e -> calculateCircleArea(circleInput.getText()));
3
4// MouseEvent: a click on the "Radius:" label
5circleLabel.setOnMouseClicked(e -> circleInput.requestFocus());
6
7// KeyEvent: a key pressed while the height field has focus
8triangleHeight.setOnKeyPressed(e -> System.out.println("Key pressed: " + e.getCode()));
9
10// WindowEvent: the user clicks the window's close button
11primaryStage.setOnCloseRequest(e -> System.out.println("Calculator closed"));
Output
Enter in the Radius field now calculates the area, like the button.
A click on the Radius: label puts the cursor in the Radius field.
Typing 7 into the Height field prints Key pressed: DIGIT7 in the console.
Closing the window prints Calculator closed in the console.

The parameter e has a different type in each line, taken from the method it is passed to. In setOnKeyPressed it is a KeyEvent, which is why e.getCode() compiles there and would not compile in the setOnAction line.

setOnAction replaces, addEventHandler adds#

A setOn method stores exactly one handler. Call it a second time and the first handler is gone. When two separate pieces of code must both react to the same event, use addEventHandler, the general method underneath, which keeps a list.

setOnAction(handler)
addEventHandler(ActionEvent.ACTION, handler)
Holds one handler for that event type on that node.
Keeps a list, and every handler in it runs.
A second call replaces the first handler.
A second call adds a second handler.
The method name says the event: setOnAction, setOnMouseClicked.
An EventType constant says the event: ActionEvent.ACTION, MouseEvent.MOUSE_CLICKED.
Removed with setOnAction(null).
Removed with removeEventHandler(type, handler), which needs the same handler object.
Replace or add
read along
1circleButton.setOnAction(e -> System.out.println("first"));
2circleButton.setOnAction(e -> System.out.println("second")); // replaces "first"
3
4rectangleButton.addEventHandler(ActionEvent.ACTION, e -> System.out.println("one"));
5rectangleButton.addEventHandler(ActionEvent.ACTION, e -> System.out.println("two"));
Output
Clicking Calculate Circle Area prints:
second

Clicking Calculate Rectangle Area prints:
one
two

Writing a handler: three forms#

Handlers are usually written in one of two ways. The registration form from the last lesson uses an anonymous inner class; every program in this lesson uses lambdas, sometimes one line long and sometimes a whole block. A third form, the method reference, turns up in other code. Each one produces the same thing: an object with a handle method that JavaFX can call.

An anonymous inner class (the registration form)#

RegistrationFormApplication.java (inside addUIControls)
read along
1submitButton.setOnAction(new EventHandler<ActionEvent>() {
2 @Override
3 public void handle(ActionEvent event) {
4 if(nameField.getText().isEmpty()) {
5 showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(),
6 "Form Error!", "Please enter your name");
7 return;
8 }
9 if(emailField.getText().isEmpty()) {
10 showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(),
11 "Form Error!", "Please enter your email id");
12 return;
13 }
14 if(passwordField.getText().isEmpty()) {
15 showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(),
16 "Form Error!", "Please enter a password");
17 return;
18 }
19
20 showAlert(Alert.AlertType.CONFIRMATION, gridPane.getScene().getWindow(),
21 "Registration Successful!", "Welcome " + nameField.getText());
22 }
23});
Output
Submit pressed (or Enter, since Submit is the default button) with the name empty:
an error alert titled "Form Error!" saying "Please enter your name".
With all three fields filled: a confirmation alert, "Registration Successful!",
saying "Welcome " followed by the name.

Read new EventHandler<ActionEvent>() { ... } as two things at once. It declares a class with no name that implements EventHandler<ActionEvent>, and it creates one object of that class. The class body overrides handle, and setOnAction stores the object on the button: that is what registering a handler means. None of the code inside handle runs at this point. It runs later, each time the button fires: whenever the user clicks Submit, or presses Enter. Enter works because the same method called submitButton.setDefaultButton(true) a few lines earlier.

Each return ends this one call of handle straight after its error alert, so the confirmation alert at the bottom is reached only when all three fields have text.

A one-line lambda (the area calculator)#

From the area calculator on, every handler in this lesson is a lambda. These are the calculator's three registrations:

SimpleCalculator.java (inside start)
read along
1circleButton.setOnAction(e -> calculateCircleArea(circleInput.getText()));
2rectangleButton.setOnAction(e -> calculateRectangleArea(rectangleLength.getText(), rectangleWidth.getText()));
3triangleButton.setOnAction(e -> calculateTriangleArea(triangleBase.getText(), triangleHeight.getText()));

Take the first line apart. e is the parameter of handle, the ActionEvent. Its type is not written because setOnAction expects an EventHandler<ActionEvent>, so the compiler already knows it. After the arrow comes the body, a single expression with no braces: a call to a private method, passing whatever text is in the radius field. The handler never looks at e. That is normal for buttons: the event only says the button was pressed, and each handler already belongs to one button.

circleInput.getText() sits inside the lambda, so it runs at click time and reads what the user has typed by then. Had the code copied the text into a variable before calling setOnAction and used that variable instead, every click would see the empty string the field held while start was running.

Forwarding to a named method keeps start short and gives each calculation a name. The perfect number checker does the same with checkButton.setOnAction(e -> checkPerfectNumber()).

A block lambda (the interest calculator)#

When a handler needs several statements, the body goes in braces. The interest calculator's Simple Interest handler parses three fields, checks them, computes and displays, all inside the lambda:

InterestCalculator.java (inside start)
read along
1siButton.setOnAction(e -> {
2 try {
3 double p = Double.parseDouble(principalField.getText());
4 double r = Double.parseDouble(rateField.getText());
5 double t = Double.parseDouble(timeField.getText());
6
7 if (p < 0 || r < 0 || t < 0) {
8 throw new IllegalArgumentException(
9 "Negative values not allowed");
10 }
11
12 double si = (p * r * t) / 100;
13
14 resultField.setText(String.format("%.2f", si));
15
16 } catch (NumberFormatException ex) {
17 resultField.setText("Invalid input!");
18 } catch (IllegalArgumentException ex) {
19 resultField.setText(ex.getMessage());
20 }
21});

Inside the braces you write an ordinary method body: local variables, if, try and catch. Nothing is returned, because handle is void. A few lines read well inline; a long body is easier to follow as a named method called from a one-line lambda, as in the area calculator.

A method reference#

When a method already has the shape of handle, taking one ActionEvent and returning nothing, you can pass the method itself with ::. The programs here never do this, but you will see it in other code:

PerfectNumberChecker.java (a variation)
read along
1checkButton.setOnAction(this::onCheck);
2
3private void onCheck(ActionEvent event) {
4 checkPerfectNumber();
5}

this::onCheck means “when handle(event) is called, call onCheck(event) on this object”. The shapes must match. this::checkPerfectNumber does not compile, because checkPerfectNumber takes no parameter and handle passes one; javac reports an invalid method reference.

Anonymous class or lambda#

Both forms build an object that implements EventHandler. They differ in four ways.

Anonymous inner class
Lambda
Spells out new EventHandler<ActionEvent>(), @Override and the full handle header before any real work.
Writes only the parameter and the body: e -> ....
Inside handle, this is the handler object itself.
this is the enclosing object, such as the SimpleCalculator, just as in the surrounding method.
Compiles to its own class file, such as RegistrationFormApplication$1.class.
No extra class file; the JVM builds the handler object while the program runs.
Can implement any interface or extend a class, and can declare fields and extra methods.
Works only where a functional interface is expected.

For your own code, write the lambda unless you need something only a class can have, such as a field of its own that keeps a count from one click to the next.

Why captured variables must be effectively final#

Look at what the registration form's handler uses: nameField, emailField, passwordField and gridPane. All four are local variables or parameters of addUIControls, and addUIControls has long finished by the time anyone presses Submit. Its local variables are gone. The handler can still use them because Java copied their values into the handler object when it was created. A copy is only safe if the original never changes afterwards, so Java has a rule: a local variable used inside a lambda or an anonymous class must be final or effectively final, meaning it is assigned once and never again.

Counter.java (does not compile)
read along
1int clicks = 0;
2countButton.setOnAction(e -> {
3 clicks++; // compile error
4 countLabel.setText("Clicks: " + clicks);
5});
Output
error: local variables referenced from a lambda expression must be final or effectively final

Two things remain allowed. Calling a method on a captured object is fine, because the variable still points at the same object: resultField.setText(...) changes the text field, not the variable, and the interest and Armstrong handlers do exactly that with a local resultField. And a field of the class is not captured at all; the lambda reaches it through the object, so it may change a field freely. That is the fix for the counter: declare private int clicks = 0; as a field instead, and clicks++ compiles.

How an event reaches its handler#

Between the click and your handler, JavaFX takes four steps. They complete the picture, and they explain behaviour that otherwise looks odd, such as a pane reacting to a click on a button inside it.

  1. 1Target selection. JavaFX decides which node the event is for. A mouse event goes to the topmost node under the pointer, a key event to the node with keyboard focus, and an ActionEvent to the control that fired it.
  2. 2Route construction. It builds the event dispatch chain: the path from the stage, through the scene and each parent, down to the target. For circleButton that is Stage, Scene, GridPane, Button.
  3. 3Capturing phase. The event travels down the route from the stage to the target. Event filters, registered with addEventFilter, run on the way down.
  4. 4Bubbling phase. The event travels back up from the target to the stage, and event handlersrun on the way: the target's first, then its parent's, and so on. Every handler registered with a setOn method or addEventHandler runs in this phase.

A filter or handler anywhere on the route can call event.consume(). The event stops where it is, and nodes further along the route never see it.

Stage1. filters, 8. handlers
Scene2. filters, 7. handlers
GridPane3. filters, 6. handlers
Button (circleButton)the target: 4. filters, 5. handlers, where setOnAction runs
The route for an ActionEvent fired by circleButton. The numbers give the order: filters on the way down, handlers on the way back up.
Key idea

Filters run on the way down, handlers on the way back up, and consume() ends the trip wherever it is called.

The route in code#

This fragment gives the calculator's GridPane a filter and a handler for ActionEvent, beside the button's own handler, and prints from each:

SimpleCalculator.java (inside start)
read along
1grid.addEventFilter(ActionEvent.ACTION,
2 e -> System.out.println("1. filter on the grid"));
3
4circleButton.setOnAction(
5 e -> System.out.println("2. handler on the button"));
6
7grid.addEventHandler(ActionEvent.ACTION, e -> {
8 System.out.println("3. handler on the grid");
9 System.out.println(" source: " + e.getSource().getClass().getSimpleName());
10 System.out.println(" target: " + ((Button) e.getTarget()).getText());
11});
Output
Clicking Calculate Circle Area prints:
1. filter on the grid
2. handler on the button
3. handler on the grid
   source: GridPane
   target: Calculate Circle Area

With e.consume() added to the filter, only the first line prints:
the event never reaches the button.

The last two lines show the difference between source and target. In the button's own handler both are the button. In the grid's handler the source is the grid, because getSource() always names the node whose handler is running, while the target is still the button that fired. One handler on a parent can therefore serve many buttons, using getTarget() to tell which one was pressed.

Reading numbers from text fields#

Every handler in the four programs follows the same five steps. Once you see the shape, each program is a variation on it.

  1. 1Read. getText() returns the field's contents as a String, always. An empty field gives the empty string "".
  2. 2Parse. Integer.parseInt or Double.parseDouble turns the text into a number. This is the step that fails when the user types abc.
  3. 3Validate. Check what parsing cannot, such as a negative principal. If the value is unusable, throw new IllegalArgumentException(...) with a message for the user.
  4. 4Compute. The formula or the loop.
  5. 5Show. setText on a result field, usually one made read-only with setEditable(false).
Key idea

Everything in a TextField is a String until you parse it, and parsing is the step that fails.

NumberFormatException: text that is not a number#

Integer.parseInt("42") returns 42. Given text that is not a whole number, it has nothing sensible to return, so it throws NumberFormatException. The two parse methods differ on a few inputs worth knowing:

Text in the fieldInteger.parseInt(text)Double.parseDouble(text)
"42"4242.0
"-7"-7-7.0
"12.5"throws12.5
" 5 " (spaces around it)throws5.0 (spaces are ignored)
"" (an empty field)throwsthrows
"abc"throwsthrows
Every throws in the table is a NumberFormatException.

NumberFormatException is unchecked, so the compiler does not make you catch it. Leave it uncaught and it escapes the handler: JavaFX prints the stack trace in the console, the window carries on, and the user sees nothing change. Catching it is how the user learns what went wrong.

IllegalArgumentException: a number you refuse#

-5 parses perfectly well, but a negative principal makes no sense, and neither does the factorial of a negative number. The interest and Armstrong programs reject such values by throwing an exception themselves:

InterestCalculator.java (inside each handler)
read along
1if (p < 0 || r < 0 || t < 0) {
2 throw new IllegalArgumentException(
3 "Negative values not allowed");
4}

throw jumps straight to the matching catch and skips the calculation. That catch shows ex.getMessage(), the text given to the constructor, so the user reads Negative values not allowed in the result field. The checks stay a flat list at the top of the handler, and every failure ends up in one place.

Catch order: the subclass first#

NumberFormatException is a subclass of IllegalArgumentException:

RuntimeExceptionunchecked: the compiler does not force a catch
IllegalArgumentExceptionthrown by the programs' own checks
NumberFormatExceptionthrown by parseInt and parseDouble
ArithmeticExceptionint or long division by zero, Math.multiplyExact overflow
The exceptions the four programs deal with. All extend RuntimeException, so all are unchecked.

Java tries catch blocks from top to bottom and runs the first one whose type matches. A catch (IllegalArgumentException ex) matches a NumberFormatException too, because an object of a subclass is also an instance of its superclass. Put that block first and the NumberFormatException block below it could never run. Java treats an unreachable catch as an error:

Wrong order (does not compile)
read along
1} catch (IllegalArgumentException ex) {
2 resultField.setText(ex.getMessage());
3} catch (NumberFormatException ex) { // compile error
4 resultField.setText("Invalid input!");
5}
Output
error: exception NumberFormatException has already been caught

The programs use the order that compiles: NumberFormatException first, so text that is not a number gets its own message, then IllegalArgumentException for the values the code rejected itself.

Division by zero: doubles do not throw#

Divide by zero cannot happen here
Exercises like the area calculator often ask you to handle exceptions such as divide by zero. None of its three formulas divides by anything the user types; they only multiply. And a division would not throw anyway, because the values are doubles: only integer division (int or long, with / or %) by zero throws ArithmeticException. The failure the calculator really has to handle is text that is not a number, and that is the NumberFormatException its code catches.
DoubleDivision.java
1public class Main {
2 public static void main(String[] args) {
3 double length = 5.0;
4 double zero = 0.0;
5
6 System.out.println(length / zero);
7 System.out.println(-length / zero);
8 System.out.println(zero / zero);
9 }
10}
Prints
›Infinity
›-Infinity
›NaN

Infinity and NaN (“not a number”) are genuine doublevalues, defined by the IEEE 754 standard that Java's double follows. Nothing stops them: they flow into later arithmetic and reach a result field as text. Make the two variables ints holding 5 and 0, and the first division stops the program with java.lang.ArithmeticException: / by zero.

Example: an area calculator#

The task: a JavaFX calculator that works out the area of a circle, a rectangle and a triangle, shows the answer in a text field, and handles bad input with exceptions.

Start from the inputs. A circle needs a radius, a rectangle a length and a width, a triangle a base and a height: five text fields. Each shape gets its own button, and all three buttons write to one shared result field that the user cannot edit. A two-column GridPane holds it all, with labels in column 0 and everything else in column 1, one shape after another down the rows.

The layout#

Every control gets its cell from one GridPane.setConstraints(node, column, row) line. The figure draws each control where its line puts it, filled in with sample values.

column 0labelscolumn 1inputs, buttons, resultrow 0row 1row 2row 3row 4row 5row 6row 7row 8Radius:5Calculate Circle AreaLength:3Width:4Calculate Rectangle AreaBase:4Height:7Calculate Triangle AreaTriangle Area: 14.0
Fourteen controls, fourteen setConstraints lines. Column 0 stays empty in the button rows and the result row.

Placing a node takes two steps in this program. GridPane.setConstraints(circleLabel, 0, 0) is a static method: it writes “column 0, row 0” onto the label itself and changes nothing on screen. The label appears only when grid.getChildren().addAll(...) puts it in the grid, which then reads the position stored on it. The interest and Armstrong programs use grid.add(node, column, row), which does both steps in one call. The two styles fail in different ways: a node that gets constraints but is left out of addAll never appears, and a node added without constraints lands in column 0, row 0, on top of whatever is already there.

The complete program#

SimpleCalculator.java
read along
1import javafx.application.Application;
2import javafx.geometry.Insets;
3import javafx.scene.Scene;
4import javafx.scene.control.Button;
5import javafx.scene.control.Label;
6import javafx.scene.control.TextField;
7import javafx.scene.layout.GridPane;
8import javafx.stage.Stage;
9
10public class SimpleCalculator extends Application {
11 private TextField resultField;
12
13 public static void main(String[] args) {
14 launch(args);
15 }
16
17 @Override
18 public void start(Stage primaryStage) {
19 primaryStage.setTitle("Simple Calculator");
20
21 GridPane grid = new GridPane();
22 grid.setPadding(new Insets(10, 10, 10, 10));
23 grid.setVgap(8);
24 grid.setHgap(10);
25
26 Label circleLabel = new Label("Radius:");
27 TextField circleInput = new TextField();
28 Button circleButton = new Button("Calculate Circle Area");
29
30 Label rectangleLabel = new Label("Length:");
31 TextField rectangleLength = new TextField();
32 Label rectangleWidthLabel = new Label("Width:");
33 TextField rectangleWidth = new TextField();
34 Button rectangleButton = new Button("Calculate Rectangle Area");
35
36 Label triangleLabel = new Label("Base:");
37 TextField triangleBase = new TextField();
38 Label triangleHeightLabel = new Label("Height:");
39 TextField triangleHeight = new TextField();
40 Button triangleButton = new Button("Calculate Triangle Area");
41
42 resultField = new TextField();
43 resultField.setEditable(false);
44 resultField.setPromptText("Result");
45
46 GridPane.setConstraints(circleLabel, 0, 0);
47 GridPane.setConstraints(circleInput, 1, 0);
48 GridPane.setConstraints(circleButton, 1, 1);
49
50 GridPane.setConstraints(rectangleLabel, 0, 2);
51 GridPane.setConstraints(rectangleLength, 1, 2);
52 GridPane.setConstraints(rectangleWidthLabel, 0, 3);
53 GridPane.setConstraints(rectangleWidth, 1, 3);
54 GridPane.setConstraints(rectangleButton, 1, 4);
55
56 GridPane.setConstraints(triangleLabel, 0, 5);
57 GridPane.setConstraints(triangleBase, 1, 5);
58 GridPane.setConstraints(triangleHeightLabel, 0, 6);
59 GridPane.setConstraints(triangleHeight, 1, 6);
60 GridPane.setConstraints(triangleButton, 1, 7);
61
62 GridPane.setConstraints(resultField, 1, 8);
63
64 circleButton.setOnAction(e -> calculateCircleArea(circleInput.getText()));
65 rectangleButton.setOnAction(e -> calculateRectangleArea(rectangleLength.getText(), rectangleWidth.getText()));
66 triangleButton.setOnAction(e -> calculateTriangleArea(triangleBase.getText(), triangleHeight.getText()));
67
68 grid.getChildren().addAll(circleLabel, circleInput, circleButton,
69 rectangleLabel, rectangleLength, rectangleWidthLabel, rectangleWidth, rectangleButton,
70 triangleLabel, triangleBase, triangleHeightLabel, triangleHeight, triangleButton,
71 resultField);
72
73 Scene scene = new Scene(grid, 400, 300);
74 primaryStage.setScene(scene);
75 primaryStage.show();
76 }
77
78 private void calculateCircleArea(String radiusStr) {
79 try {
80 double radius = Double.parseDouble(radiusStr);
81 double area = Math.PI * Math.pow(radius, 2);
82 resultField.setText("Circle Area: " + area);
83 } catch (NumberFormatException e) {
84 resultField.setText("Invalid input");
85 }
86 }
87
88 private void calculateRectangleArea(String lengthStr, String widthStr) {
89 try {
90 double length = Double.parseDouble(lengthStr);
91 double width = Double.parseDouble(widthStr);
92 double area = length * width;
93 resultField.setText("Rectangle Area: " + area);
94 } catch (NumberFormatException e) {
95 resultField.setText("Invalid input");
96 }
97 }
98
99 private void calculateTriangleArea(String baseStr, String heightStr) {
100 try {
101 double base = Double.parseDouble(baseStr);
102 double height = Double.parseDouble(heightStr);
103 double area = 0.5 * base * height;
104 resultField.setText("Triangle Area: " + area);
105 } catch (NumberFormatException e) {
106 resultField.setText("Invalid input");
107 }
108 }
109}
Output
A 400 by 300 window titled "Simple Calculator".
With 5, 3, 4, 4 and 7 in the five fields, the three buttons show:
Circle Area: 78.53981633974483
Rectangle Area: 12.0
Triangle Area: 14.0
With abc in a field, its button shows: Invalid input

resultField is declared as a field, above main, while every other control is a local variable inside start. The reason is reach. The three calculate methods are separate methods of the class and must write to the result field, so it has to belong to the object. The input fields never need to leave start: each lambda reads its fields at click time and passes the text in as an argument. setEditable(false) stops the user typing into the answer (it can still be selected and copied), and setPromptText("Result") shows Result in grey until there is an answer.

The three calculate methods share one shape: parse inside try, compute, display, and on NumberFormatException display Invalid input. Parsing comes first, so a bad value stops the method before any arithmetic happens. The circle uses Math.PI * Math.pow(radius, 2); radius * radius gives the same number. Joining a double onto a String prints it in full, so a radius of 5 shows Circle Area: 78.53981633974483. The interest calculator, later, rounds with String.format.

Two things to watch for. The program accepts negative lengths without complaint: a radius of -5 gives the same area as 5, because squaring removes the sign, and a rectangle of -3 by 4 reports Rectangle Area: -12.0. And the scene is 300 pixels tall, a little less than nine rows of controls with 8-pixel gaps need, so the result field is cut off at the bottom edge; new Scene(grid, 400, 340) gives it room. (Divide by zero cannot happen here, as the section on reading numbers explained.)

Simple Calculator
Radius:
5
Calculate Circle Area
Length:
3
Width:
4
Calculate Rectangle Area
Base:
4
Height:
7
Calculate Triangle Area
Triangle Area: 14.0
Calculate Triangle Area was pressed last, so the field shows 0.5 * 4 * 7. The other two buttons would show Circle Area: 78.53981633974483 and Rectangle Area: 12.0.

The formulas as a console program#

The formulas themselves are plain Java, so they can run here. Each shape becomes a static method, and main passes in sample values instead of reading text fields.

Areas.java
1public class Main {
2 public static void main(String[] args) {
3 System.out.println("Circle Area: " + circleArea(5.0));
4 System.out.println("Rectangle Area: " + rectangleArea(3.0, 4.0));
5 System.out.println("Triangle Area: " + triangleArea(4.0, 7.0));
6 }
7
8 static double circleArea(double radius) {
9 // The visualizer has no Math.PI, so the value is written out here.
10 // In real Java, write Math.PI * Math.pow(radius, 2).
11 double pi = 3.141592653589793;
12 return pi * Math.pow(radius, 2);
13 }
14
15 static double rectangleArea(double length, double width) {
16 return length * width;
17 }
18
19 static double triangleArea(double base, double height) {
20 return 0.5 * base * height;
21 }
22}
Prints
›Circle Area: 78.53981633974483
›Rectangle Area: 12.0
›Triangle Area: 14.0

The output matches the window. Rectangle Area prints as 12.0, not 12, because the parameters are doubles, and a double always prints with a decimal point.

Example: a perfect number checker#

The task: a window with two text fields and a Check button. The user types a whole number into the first field. When they click Check, the second field says whether it is a perfect number, and if it is not, shows the sum of its digits instead.

A perfect number equals the sum of its proper divisors, the divisors smaller than the number itself. 6 is perfect: its proper divisors are 1, 2 and 3, and 1 + 2 + 3 = 6. The next is 28 (1 + 2 + 4 + 7 + 14), then 496 and 8128. Those four are the only ones below ten thousand.

isPerfectNumber adds up the divisors. It starts sum at 1, because 1 divides every number, and tries each i from 2 up to number / 2. Half is far enough: the smallest divisor above 1 is at least 2, so the largest proper divisor is at most half the number. For 28 the loop tries 2 to 14 and finds 2, 4, 7 and 14. The guard if (number <= 1) return false; deals with 1, 0 and every negative number, none of which is perfect. Without it, 1 would pass, since sum starts at 1 and the loop never runs.

sumOfDigits is the digit-peeling loop from the loops lesson: number % 10 is the last digit, and number /= 10 drops it. For 567:

number at the top of the loopnumber % 10sum afterwardsnumber after /= 10
5677756
566135
55180
Once number is 0, the condition number != 0 fails and the method returns 18.

The complete program#

PerfectNumberChecker.java
read along
1import javafx.application.Application;
2import javafx.geometry.Insets;
3import javafx.scene.Scene;
4import javafx.scene.control.Button;
5import javafx.scene.control.Label;
6import javafx.scene.control.TextField;
7import javafx.scene.layout.GridPane;
8import javafx.stage.Stage;
9
10public class PerfectNumberChecker extends Application {
11 private TextField inputField;
12 private TextField resultField;
13
14 public static void main(String[] args) {
15 launch(args);
16 }
17
18 @Override
19 public void start(Stage primaryStage) {
20 primaryStage.setTitle("Perfect Number Checker");
21
22 GridPane grid = new GridPane();
23 grid.setPadding(new Insets(10, 10, 10, 10));
24 grid.setVgap(8);
25 grid.setHgap(10);
26
27 Label inputLabel = new Label("Enter an integer:");
28 inputField = new TextField();
29 Button checkButton = new Button("Check");
30 resultField = new TextField();
31 resultField.setEditable(false);
32 resultField.setPromptText("Result");
33
34 GridPane.setConstraints(inputLabel, 0, 0);
35 GridPane.setConstraints(inputField, 1, 0);
36 GridPane.setConstraints(checkButton, 1, 1);
37 GridPane.setConstraints(resultField, 1, 2);
38
39 checkButton.setOnAction(e -> checkPerfectNumber());
40
41 grid.getChildren().addAll(inputLabel, inputField, checkButton, resultField);
42
43 Scene scene = new Scene(grid, 300, 200);
44 primaryStage.setScene(scene);
45 primaryStage.show();
46 }
47
48 private void checkPerfectNumber() {
49 try {
50 int number = Integer.parseInt(inputField.getText());
51 if (isPerfectNumber(number)) {
52 resultField.setText(number + " is a perfect number.");
53 } else {
54 resultField.setText("Sum of digits: " + sumOfDigits(number));
55 }
56 } catch (NumberFormatException e) {
57 resultField.setText("Invalid input. Please enter an integer.");
58 }
59 }
60
61 private boolean isPerfectNumber(int number) {
62 if (number <= 1) return false;
63 int sum = 1;
64 for (int i = 2; i <= number / 2; i++) {
65 if (number % i == 0) {
66 sum += i;
67 }
68 }
69 return sum == number;
70 }
71
72 private int sumOfDigits(int number) {
73 int sum = 0;
74 while (number != 0) {
75 sum += number % 10;
76 number /= 10;
77 }
78 return sum;
79 }
80}
Output
A 300 by 200 window titled "Perfect Number Checker".
28 and Check:  28 is a perfect number.
567 and Check: Sum of digits: 18
abc and Check: Invalid input. Please enter an integer.

Both inputField and resultField are fields this time, because checkPerfectNumber reads one and writes the other and takes no arguments, which shrinks the handler to e -> checkPerfectNumber(). The area calculator chose the other design, passing the text in as an argument and keeping only the result as a field. Both work. Notice too that isPerfectNumber and sumOfDigits contain no JavaFX at all, which is what lets the snippet below run them in the visualizer.

Watch for negative input. -567 is not perfect, so the program shows its digit sum, and Java's % keeps the sign of the number being divided: -567 % 10 is -7, so the field shows Sum of digits: -18.

Perfect Number Checker
Enter an integer:
567
Check
Sum of digits: 18
567 is not perfect (its proper divisors add up to 401), so the program shows the sum of its digits.

Stepping through the two methods#

Here are the two methods with a main that checks 28 and 567, the two cases the window has to tell apart. Step through it with the trace open.

PerfectNumber.java
1public class Main {
2 public static void main(String[] args) {
3 check(28);
4 check(567);
5 }
6
7 static void check(int number) {
8 if (isPerfectNumber(number)) {
9 System.out.println(number + " is a perfect number.");
10 } else {
11 System.out.println("Sum of digits: " + sumOfDigits(number));
12 }
13 }
14
15 static boolean isPerfectNumber(int number) {
16 if (number <= 1) return false;
17 int sum = 1;
18 for (int i = 2; i <= number / 2; i++) {
19 if (number % i == 0) {
20 sum += i;
21 }
22 }
23 return sum == number;
24 }
25
26 static int sumOfDigits(int number) {
27 int sum = 0;
28 while (number != 0) {
29 sum += number % 10;
30 number /= 10;
31 }
32 return sum;
33 }
34}
Prints
›28 is a perfect number.
›Sum of digits: 18

For 28, watch sum pick up 2, 4, 7 and 14 on top of its starting 1 and land on exactly 28. For 567, isPerfectNumber tries every i from 2 to 283, ends with 401, and returns false; then sumOfDigits strips 567 down to 18. That second trace is long, and it is a fair picture of the work one click on Check does.

Example: an interest calculator#

The task: a JavaFX application that calculates simple interest, compound interest and the monthly payment on a loan (EMI), shows the result in a text field, and handles invalid input with exceptions.

The three formulas#

With P the principal (the amount borrowed or invested), R the yearly interest rate in percent and T the time in years:

  • Simple interest, paid on the principal only: SI = P * R * T / 100.
  • Compound interest, where each year's interest joins the balance and earns interest itself: CI = P * (1 + R / 100)^T - P. Subtracting P leaves only the interest.
  • EMI, the equated monthly instalment, the fixed monthly payment that repays a loan with its interest: EMI = P * r * (1 + r)^n / ((1 + r)^n - 1), where r = R / (12 * 100) is the monthly rate and n = T * 12 is the number of months.

Here ^ means “to the power of”. In Java that is Math.pow; Java's own ^ operator is bitwise exclusive or, so 2 ^ 3 is 1, not 8.

With P = 10000, R = 10 and T = 2: simple interest is 10000 * 10 * 2 / 100 = 2000.00. Compound interest is 10000 * 1.1 * 1.1 - 10000 = 12100 - 10000 = 2100.00. For the EMI, r is 10 / 1200 = 0.008333... and n is 24, so (1 + r)^24 is about 1.22039, and the EMI is 10000 * 0.008333 * 1.22039 / 0.22039, about 461.45 a month.

The complete program#

InterestCalculator.java
read along
1import javafx.application.Application;
2import javafx.geometry.Insets;
3import javafx.scene.Scene;
4import javafx.scene.control.*;
5import javafx.scene.layout.GridPane;
6import javafx.stage.Stage;
7
8public class InterestCalculator extends Application {
9
10 public void start(Stage stage) {
11
12 Label principalLabel = new Label("Principal Amount:");
13 Label rateLabel = new Label("Interest Rate (%):");
14 Label timeLabel = new Label("Time (Years):");
15
16 TextField principalField = new TextField();
17 TextField rateField = new TextField();
18 TextField timeField = new TextField();
19
20 Button siButton = new Button("Simple Interest");
21 Button ciButton = new Button("Compound Interest");
22 Button emiButton = new Button("EMI");
23
24 Label resultLabel = new Label("Result:");
25 TextField resultField = new TextField();
26 resultField.setEditable(false);
27
28 siButton.setOnAction(e -> {
29 try {
30 double p = Double.parseDouble(principalField.getText());
31 double r = Double.parseDouble(rateField.getText());
32 double t = Double.parseDouble(timeField.getText());
33
34 if (p < 0 || r < 0 || t < 0) {
35 throw new IllegalArgumentException(
36 "Negative values not allowed");
37 }
38
39 double si = (p * r * t) / 100;
40
41 resultField.setText(String.format("%.2f", si));
42
43 } catch (NumberFormatException ex) {
44 resultField.setText("Invalid input!");
45 } catch (IllegalArgumentException ex) {
46 resultField.setText(ex.getMessage());
47 }
48 });
49
50 ciButton.setOnAction(e -> {
51 try {
52 double p = Double.parseDouble(principalField.getText());
53 double r = Double.parseDouble(rateField.getText());
54 double t = Double.parseDouble(timeField.getText());
55
56 if (p < 0 || r < 0 || t < 0) {
57 throw new IllegalArgumentException(
58 "Negative values not allowed");
59 }
60
61 double ci = p * Math.pow((1 + r / 100), t) - p;
62
63 resultField.setText(String.format("%.2f", ci));
64
65 } catch (NumberFormatException ex) {
66 resultField.setText("Invalid input!");
67 } catch (IllegalArgumentException ex) {
68 resultField.setText(ex.getMessage());
69 }
70 });
71
72 emiButton.setOnAction(e -> {
73 try {
74 double p = Double.parseDouble(principalField.getText());
75 double annualRate = Double.parseDouble(rateField.getText());
76 double years = Double.parseDouble(timeField.getText());
77
78 if (p <= 0 || annualRate <= 0 || years <= 0) {
79 throw new IllegalArgumentException(
80 "Values must be greater than zero");
81 }
82
83 double monthlyRate = annualRate / (12 * 100);
84 int months = (int) (years * 12);
85
86 double emi = (p * monthlyRate *
87 Math.pow(1 + monthlyRate, months))
88 / (Math.pow(1 + monthlyRate, months) - 1);
89
90 resultField.setText(String.format("%.2f", emi));
91
92 } catch (NumberFormatException ex) {
93 resultField.setText("Invalid input!");
94 } catch (IllegalArgumentException ex) {
95 resultField.setText(ex.getMessage());
96 }
97 });
98
99 GridPane grid = new GridPane();
100 grid.setPadding(new Insets(20));
101 grid.setHgap(10);
102 grid.setVgap(10);
103 grid.add(principalLabel, 0, 0);
104 grid.add(principalField, 1, 0);
105 grid.add(rateLabel, 0, 1);
106 grid.add(rateField, 1, 1);
107 grid.add(timeLabel, 0, 2);
108 grid.add(timeField, 1, 2);
109 grid.add(siButton, 0, 3);
110 grid.add(ciButton, 1, 3);
111 grid.add(emiButton, 2, 3);
112 grid.add(resultLabel, 0, 4);
113 grid.add(resultField, 1, 4, 2, 1);
114 Scene scene = new Scene(grid, 550, 250);
115 stage.setTitle("Interest Calculator");
116 stage.setScene(scene);
117 stage.show();
118 }
119
120 public static void main(String[] args) {
121 launch(args);
122 }
123}
Output
A 550 by 250 window titled "Interest Calculator".
With 10000, 10 and 2 in the three fields:
Simple Interest shows    2000.00
Compound Interest shows  2100.00
EMI shows                461.45
A letter in any field shows Invalid input!
A negative value shows Negative values not allowed
EMI with a zero shows Values must be greater than zero

All three handlers are block lambdas with the same skeleton: parse the three fields, validate, compute, format. Every control is a local variable in start; none needs to be a field, because all the work happens inside lambdas, which can use the effectively final locals around them. start has no @Override here. It still overrides Application.start; the annotation is optional and only asks the compiler to check.

String.format("%.2f", si) turns the result into text with exactly two digits after the decimal point, rounding the rest: % starts a format, .2 is the number of decimal places, and f means a decimal number. The rounding matters, because the raw results are not tidy. The compound interest really comes out as 2100.000000000002, since 1.1 has no exact binary form, and the EMI as 461.44926337516654. Formatted, they show as 2100.00 and 461.45.

Simple and compound interest reject only negative values (p < 0 || r < 0 || t < 0), so zero is allowed, and a 0% rate correctly gives 0.00. The EMI check is stricter, p <= 0 || annualRate <= 0 || years <= 0, and its rate test is there for a reason. With a rate of 0, monthlyRate is 0, the top of the formula is P * 0 * 1 = 0 and the bottom is 1 - 1 = 0. 0.0 / 0.0 is NaN, so without the check the field would show NaN. A 0% loan is not impossible, though: its EMI is P / n, 10000 / 24 = 416.67 for the example. One more edge: (int) (years * 12) truncates, so 0.05 years becomes 0 months, the bottom of the formula is 0 again, and the field shows Infinity.

This program places nodes with grid.add. The five-argument form, grid.add(resultField, 1, 4, 2, 1), adds a column span and a row span: start at column 1, row 4, and cover 2 columns and 1 row. That is why the result field is wider than the inputs.

column 0labels, SI buttoncolumn 1fields, CI buttoncolumn 2EMI buttonrow 0row 1row 2row 3row 42 colsPrincipal Amount:10000Interest Rate (%):10Time (Years):2Simple InterestCompound InterestEMIResult:461.45
Three columns, five rows. Only the EMI button lives in column 2, and the result field spans columns 1 and 2.

Each column is as wide as its widest node. Column 1 is sized by its text fields, which are wider than the Compound Interest button, so the EMI button sits after a visible gap.

Interest Calculator
Principal Amount:
10000
Interest Rate (%):
10
Time (Years):
2
Simple Interest
Compound InterestEMI
Result:
461.45
Filled with 10000, 10 and 2, the EMI button shows 461.45; Simple Interest would show 2000.00 and Compound Interest 2100.00.

The formulas as a console program#

The visualizer has no String.format, so this version prints the raw doubles. That turns out to be useful: you see exactly what the window rounds.

Interest.java
1public class Main {
2 public static void main(String[] args) {
3 double p = 10000.0;
4 double r = 10.0;
5 double t = 2.0;
6
7 double si = (p * r * t) / 100;
8 System.out.println("Simple Interest: " + si);
9
10 double ci = p * Math.pow((1 + r / 100), t) - p;
11 System.out.println("Compound Interest: " + ci);
12
13 double monthlyRate = r / (12 * 100);
14 int months = (int) (t * 12);
15 double emi = (p * monthlyRate * Math.pow(1 + monthlyRate, months))
16 / (Math.pow(1 + monthlyRate, months) - 1);
17 System.out.println("EMI: " + emi);
18 }
19}
Prints
›Simple Interest: 2000.0
›Compound Interest: 2100.000000000002
›EMI: 461.44926337516654

Match each line to the window: 2000.0 formats as 2000.00, 2100.000000000002 as 2100.00, and 461.44926337516654 as 461.45. The stray 2 at the end of the compound interest comes from 1.1, which a double can only approximate; squaring it carries the error into the twelfth decimal place. Real Java prints these same three lines.

Example: Armstrong number or factorial#

The task: a window with an input field, an output field and a button. When the button is clicked, check whether the number typed in is an Armstrong number. If it is, say so; if not, show its factorial. Invalid input must be handled with exceptions.

An Armstrong number equals the sum of its own digits, each raised to the power of the number of digits. 153 has three digits, and 1³ + 5³ + 3³ = 1 + 125 + 27 = 153. The other three-digit ones are 370, 371 and 407, and the four-digit ones are 1634, 8208 and 9474. Every one-digit number qualifies, since a digit to the power 1 is itself, and there are no two-digit Armstrong numbers at all.

The handler makes two passes over the digits. The first only counts them, dividing by 10 until nothing is left: 153 becomes 15, then 1, then 0, so three digits. Zero needs its own case, digits = 1, because while (temp != 0) would not run even once for it. The second pass peels the digits off again with % 10 and adds each one raised to that power.

One line deserves a closer look: sum += Math.pow(digit, digits);. Math.pow returns a double, and sum is an int. The long form, sum = sum + Math.pow(digit, digits);, does not compile (“possible lossy conversion from double to int”). The compound form does, because Java defines sum += x as sum = (int) (sum + x): the cast is built in. Here it is also safe, because Math.pow is exact for small whole numbers; Math.pow(5, 3) is exactly 125.0.

The complete program#

ArmstrongFactorial.java
read along
1import javafx.application.Application;
2import javafx.geometry.Insets;
3import javafx.scene.Scene;
4import javafx.scene.control.*;
5import javafx.scene.layout.GridPane;
6import javafx.stage.Stage;
7
8public class ArmstrongFactorial extends Application {
9
10 @Override
11 public void start(Stage stage) {
12
13 Label inputLabel = new Label("Enter a Number:");
14 TextField inputField = new TextField();
15 Button checkButton = new Button("Check");
16 Label resultLabel = new Label("Result:");
17 TextField resultField = new TextField();
18 resultField.setEditable(false);
19 checkButton.setOnAction(e -> {
20 try {
21 int number = Integer.parseInt(inputField.getText());
22
23 if (number < 0) {
24 throw new IllegalArgumentException(
25 "Negative numbers are not allowed");
26 }
27
28 int original = number;
29 int temp = number;
30
31 // Count number of digits
32 int digits = 0;
33
34 if (number == 0) {
35 digits = 1;
36 } else {
37 while (temp != 0) {
38 digits++;
39 temp = temp / 10;
40 }
41 }
42
43 // Calculate Armstrong value
44 temp = number;
45 int sum = 0;
46
47 while (temp != 0) {
48 int digit = temp % 10;
49 sum += Math.pow(digit, digits);
50 temp = temp / 10;
51 }
52
53 if (number == 0 || sum == original) {
54 resultField.setText(
55 original + " is an Armstrong number.");
56 } else {
57 // Calculate factorial
58 long factorial = 1;
59
60 for (int i = 1; i <= number; i++) {
61 factorial *= i;
62 }
63
64 resultField.setText(
65 "Not Armstrong. Factorial = " + factorial);
66 }
67
68 } catch (NumberFormatException ex) {
69 resultField.setText("Invalid input! Enter an integer.");
70 } catch (IllegalArgumentException ex) {
71 resultField.setText(ex.getMessage());
72 } catch (ArithmeticException ex) {
73 resultField.setText("Arithmetic error occurred.");
74 }
75 });
76
77 GridPane grid = new GridPane();
78 grid.setPadding(new Insets(20));
79 grid.setHgap(10);
80 grid.setVgap(10);
81 grid.add(inputLabel, 0, 0);
82 grid.add(inputField, 1, 0);
83 grid.add(checkButton, 1, 1);
84 grid.add(resultLabel, 0, 2);
85 grid.add(resultField, 1, 2);
86 Scene scene = new Scene(grid, 500, 180);
87 stage.setTitle("Armstrong Number and Factorial");
88 stage.setScene(scene);
89 stage.show();
90 }
91 public static void main(String[] args) {
92 launch(args);
93 }
94}
Output
A 500 by 180 window titled "Armstrong Number and Factorial".
153 and Check: 153 is an Armstrong number.
12 and Check:  Not Armstrong. Factorial = 479001600
-5 and Check:  Negative numbers are not allowed
abc and Check: Invalid input! Enter an integer.

The catch blocks are in the order that compiles: NumberFormatException before its superclass IllegalArgumentException. ArithmeticException is related to neither, so its position does not matter. The test number == 0 || sum == original has a redundant half: for 0, sum is 0 anyway, so sum == original already holds. It does no harm.

This is what the window shows for 153 and for 12:

Armstrong Number and Factorial
Enter a Number:
153
Check
Result:
153 is an Armstrong number.
153: three digits, and 1 + 125 + 27 = 153.
Armstrong Number and Factorial
Enter a Number:
12
Check
Result:
Not Armstrong. Factorial = 479001600
12: two digits, and 1 + 4 = 5, not 12, so the program shows the factorial of 12 instead.

The overflow the catch never sees#

A long holds values up to 9223372036854775807. That is enough for 20! = 2432902008176640000, but 21! is about 5.1 × 1019 and does not fit. Java integer arithmetic never checks for overflow: the bits that do not fit are dropped, and the result wraps around, often to a negative number. 21 is not an Armstrong number (2² + 1² = 5), so typing it shows Not Armstrong. Factorial = -4249290049419214848. From 66 upward the factorial comes out as 0, because 66! contains the factor 2 sixty-four times, and multiplying by 2 sixty-four times pushes every bit out of a 64-bit long.

So the program's catch (ArithmeticException ex) never runs: nothing in the try block can throw it. Integer division by zero would, but every division here is by 10. The fix is Math.multiplyExact, which multiplies like * but throws ArithmeticException (message long overflow) instead of wrapping:

ArmstrongFactorial.java (the fixed loop)
read along
1long factorial = 1;
2
3for (int i = 1; i <= number; i++) {
4 factorial = Math.multiplyExact(factorial, i);
5}
Output
20 and Check: Not Armstrong. Factorial = 2432902008176640000
21 and Check: Arithmetic error occurred.

Now the existing catch block has a job, and 21 shows a message instead of a wrong number. The fix also ends the loop at i = 21 for every larger input, which matters more than it looks; the last section of this lesson shows the input that freezes the original program for good.

Stepping through the check and the factorial#

The same logic as a console program, split into a check and a factorial method, run for 153 and 12:

Armstrong.java
1public class Main {
2 public static void main(String[] args) {
3 check(153);
4 check(12);
5 }
6
7 static void check(int number) {
8 if (isArmstrong(number)) {
9 System.out.println(number + " is an Armstrong number.");
10 } else {
11 System.out.println("Not Armstrong. Factorial = " + factorial(number));
12 }
13 }
14
15 static boolean isArmstrong(int number) {
16 int temp = number;
17 int digits = 0;
18 if (number == 0) {
19 digits = 1;
20 } else {
21 while (temp != 0) {
22 digits++;
23 temp = temp / 10;
24 }
25 }
26
27 temp = number;
28 int sum = 0;
29 while (temp != 0) {
30 int digit = temp % 10;
31 sum += Math.pow(digit, digits);
32 temp = temp / 10;
33 }
34 return sum == number;
35 }
36
37 static long factorial(int number) {
38 long factorial = 1;
39 for (int i = 1; i <= number; i++) {
40 factorial *= i;
41 }
42 return factorial;
43 }
44}
Prints
›153 is an Armstrong number.
›Not Armstrong. Factorial = 479001600

Watch digits reach 3 in the first loop, then sum grow 27, 152, 153 in the second, since the digits come off from the right: 3, then 5, then 1. For 12 the check fails with a sum of 5, and factorial climbs through 1, 2, 6, 24 and on to 479001600. One limit of the visualizer: its long does not wrap around the way Java's does, so check(21) here prints the true value of 21! instead of Java's -4249290049419214848. Try that case in a real JDK.

Keep handlers short#

Every handler in this lesson runs on the JavaFX Application Thread, the thread that also redraws the window and takes the next event off the queue. While one handler runs, nothing else does: the window is not repainted, and no click or key press is handled until the handler returns. The four programs get away with this because, for sensible input, each handler finishes in a fraction of a millisecond.

Unusual input exposes it. Type 2147483647, the largest int, into the Armstrong program as first written. It is not an Armstrong number, so the factorial loop starts, and its condition i <= number can never be false when number is the largest int: once i reaches 2147483647, i++ wraps around to -2147483648 and the loop carries on. The handler never returns, and the window stays frozen until you kill the program. (The Math.multiplyExact version escapes this: it throws at i = 21, long before i can wrap.) Slow work freezes the window in the same way, only for a limited time: a loop over a billion numbers, a large file, a database query.

The freeze is easy to see with a deliberate delay:

A handler that blocks
read along
1checkButton.setOnAction(e -> {
2 resultField.setText("Working...");
3 try {
4 Thread.sleep(5000); // stands in for five seconds of slow work
5 } catch (InterruptedException ex) {
6 return;
7 }
8 resultField.setText("Done");
9});
Output
The window freezes for five seconds, then shows Done.
"Working..." never appears.

The text did change to Working..., but redrawing the window is the same thread's job, and that thread was asleep inside your handler. By the time it is free to redraw, the text already says Done.

Moving slow work off the thread
The cure is to run slow work on a background thread and hand only the final setText back to the JavaFX Application Thread. JavaFX provides javafx.concurrent.Task for exactly this. Database calls are the classic slow work, so the last lesson of this phase comes back to this rule.

Try it

Answer each one in your head or on paper first, then open the answer.

1
Explain event handling in JavaFX using the area calculator's Calculate Circle Area button. Name the event, the event source and the event handler, and describe what happens between the click and the area appearing.
2
Rewrite circleButton.setOnAction(e -> calculateCircleArea(circleInput.getText())); as an anonymous inner class. Inside your version, what does this refer to?
3
Inside start, a student writes int clicks = 0; and then countButton.setOnAction(e -> clicks++);. It does not compile. What does the compiler say, why, and how do you fix it?
4
Add a Clear button to the area calculator that empties all five input fields and the result field. Where does it go in the grid, and what does its handler look like?
5
In the area calculator, what does the result field show after each of these radius inputs, followed by a click on Calculate Circle Area: abc, an empty field, " 5 " with spaces around it, and -5? Which of them reach the catch block?
6
In the interest calculator, swap the two catch blocks so that catch (IllegalArgumentException ex) comes first. What happens, and why is the original order the right one?

Practice

Work through these on your own. Each one builds on this page.

1
Make the area calculator reject negative values. In each calculate method, after parsing, throw an IllegalArgumentException with the message Values must not be negative when any input is below zero, and add a second catch block that shows ex.getMessage(). Test with a radius of -5 and with a rectangle of -3 by 4, then explain in a sentence why the order of your two catch blocks matters.
2
Give the interest calculator a proper 0% EMI. When the rate is exactly 0, show P / n (10000 over 24 months is 416.67) instead of rejecting it, while still rejecting a negative rate and a principal or time of zero. Then deal with times shorter than one month, such as 0.05 years, which currently show Infinity.
3
Build a prime number checker in JavaFX: an input field, a Check button and a read-only result field in a GridPane. Show 17 is a prime number. or 18 is not prime: divisible by 2, reject numbers below 2 with an IllegalArgumentException, and show a message for text that is not an integer. Put the test in its own private boolean isPrime(int n) method and register the handler with a one-line lambda.
4
Write a temperature converter: one field for the value and two buttons, To Fahrenheit and To Celsius, using F = C * 9 / 5 + 32 and C = (F - 32) * 5 / 9. Show the answer to one decimal place with String.format. Reject anything below absolute zero (-273.15 °C, which is -459.67 °F) by throwing an IllegalArgumentException with a clear message, and catch NumberFormatException first.
5
In the Armstrong program, replace factorial *= i with factorial = Math.multiplyExact(factorial, i). Record what the window shows for 20, 21 and 100, before and after the change, and write two sentences explaining why its catch (ArithmeticException ex) could never run in the original.

Spotted a mistake, or a part that did not make sense? Report it by email