عرض عملي: بناء الآلة الحاسبة دالة بعد دالة
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
This is a demo lesson. Instead of a new idea, you will watch (and then repeat) a program grow from nothing into a working menu-driven calculator, one small method at a time, using everything from Lessons 1 to 6: methods with parameters and return values, separate computing from printing, and a handler per task. Your lab, BEG-014-LAB1, is the same program with two additions, so treat this lesson as the warm-up: type it in, run it, break it, and improve it.
The recipe-card picture fits well. A restaurant does not have one giant recipe called "dinner". It has cards for sauce, for dough, for the garnish, and a head chef who calls them in the right order. Our calculator will have cards (add, divide, power, readNumber, printResult) and a short main that plays the head chef.
Follow along in your own editor or on the command line (javac Calculator.java, then java Calculator). Menus for creating files vary between editors, so use whichever way you normally create a Java file. This lesson builds on all earlier lessons in the module, and on Module 11 (Scanner) and Module 13 (loops).
Learning Objectives
By the end of this lesson you will be able to:
- Build a program in layers: pure maths methods first, then input and output methods, then the loop that connects them.
- Keep computing methods free of
printlnandScanner. - Pass a
Scannerto a helper method instead of creating a new one each time. - Handle division by zero in the caller, before the maths method is used.
- Spot and repair the "method that prints instead of returning" mistake.
- Keep
mainshort by delegating to handler methods.
Written Explanation
The plan: three layers
We build in this order, running the program after each step:
- Math layer:
add,subtract,multiply,divide,power. Each takes numbers, returns a number, prints nothing. - Input/output layer:
printMenu,readNumber,printResult. These talk to the user and do no maths. - Control layer:
runBasic,runPowerandmain, which read the menu choice and connect the other layers.
The rule behind the layering is the one from Lesson 2: methods that compute return values; methods that show or read values do the printing and reading. Because of it, the maths layer works whether the result goes to the screen, a file, or (later) a web page, and each layer can be fixed without touching the others.
Step 1: the maths methods
static double add(double a, double b) { return a + b; }
static double subtract(double a, double b) { return a - b; }
static double multiply(double a, double b) { return a * b; }Test them from a temporary main (System.out.println(add(2, 3)); gives 5.0). We use double so the calculator accepts decimals.
Step 2: the planned mistake
Suppose that, in a hurry, you write add like this:
static double add(double a, double b) {
System.out.println(a + b); // prints instead of just returning
return a + b;
}It seems to work when you test it. The problem appears at the next feature. If you later want a "memory" that adds a result to a running total, printResult(add(a, b)) would show the number twice, once from inside add and once from printResult. A method that prints can never be reused quietly. The repair is to delete the println in add and let the caller decide how to show the value. Every maths method stays silent; only the printing layer prints.
Step 3: division and the zero problem
divide has one special case. For double values, Java does not throw an error on a zero divisor; it produces special values. A quick test on JDK 21 printed Infinity for 1.0 / 0 and NaN ("not a number") for 0.0 / 0. Integer division by zero, on the other hand, throws an ArithmeticException (1 / 0 crashed with / by zero in my run). Neither result is something a user wants to see as an answer.
So who should handle it? Two reasonable designs: the method signals the problem, or the caller checks first. Because divide has no good number to return for a zero divisor, we choose the second: the caller checks b == 0 and prints a friendly message, and divide stays a simple one-line method. Later modules introduce exceptions, which are the usual professional way for a method to report a problem; for now the check-first approach is clear and testable. The acceptance rule for the lab is the same: division by zero must be handled, not crash and not print Infinity.
Step 4: power with a loop
static double power(double base, int exponent) {
double result = 1;
for (int i = 0; i < exponent; i++) {
result *= base;
}
return result;
}The loop multiplies result by base, exponent times, starting from 1. That also handles exponent 0 correctly (the loop body never runs, so the answer is 1). The method assumes the exponent is zero or more; negative exponents are an extension you can add in the lab.
Step 5: the menu, input helpers, and the loop
printMenu prints the options. readNumber(Scanner in, String prompt) prints a prompt and reads a double. Notice that it receives the Scanner as a parameter. Creating a second Scanner on System.in in every helper is a source of bugs, so we create one in main and pass it down. Passing it copies the reference (Lesson 4), so every helper reads from the same input.
To keep main short we add two handler methods: runBasic for choices 1 to 4 and runPower for choice 5. Next, the whole program in its final form (factorial, the missing piece, is yours to add in the lab).
Examples
Example 1: the finished program (simple to read, realistic to run)
import java.util.Scanner;
public class Calculator {
// --- math: compute and return, never print or read ---
static double add(double a, double b) {
return a + b;
}
static double subtract(double a, double b) {
return a - b;
}
static double multiply(double a, double b) {
return a * b;
}
static double divide(double a, double b) {
return a / b;
}
static double power(double base, int exponent) {
double result = 1;
for (int i = 0; i < exponent; i++) {
result *= base;
}
return result;
}
// --- input and output ---
static void printMenu() {
System.out.println("1) add 2) subtract 3) multiply 4) divide 5) power 0) quit");
}
static double readNumber(Scanner in, String prompt) {
System.out.print(prompt);
return in.nextDouble();
}
static void printResult(double value) {
System.out.println("Result: " + value);
}
// --- one handler per kind of menu choice ---
static void runBasic(Scanner in, int choice) {
double a = readNumber(in, "First number: ");
double b = readNumber(in, "Second number: ");
if (choice == 1) {
printResult(add(a, b));
} else if (choice == 2) {
printResult(subtract(a, b));
} else if (choice == 3) {
printResult(multiply(a, b));
} else if (b == 0) {
System.out.println("Cannot divide by zero.");
} else {
printResult(divide(a, b));
}
}
static void runPower(Scanner in) {
double base = readNumber(in, "Base: ");
int exponent = (int) readNumber(in, "Exponent (whole, 0 or more): ");
printResult(power(base, exponent));
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int choice = -1;
while (choice != 0) {
printMenu();
System.out.print("Choice: ");
choice = in.nextInt();
if (choice >= 1 && choice <= 4) {
runBasic(in, choice);
} else if (choice == 5) {
runPower(in);
} else if (choice != 0) {
System.out.println("Unknown choice.");
}
}
System.out.println("Goodbye");
}
}I compiled this with JDK 21 and fed it a scripted session: add 2 and 3; divide 10 by 0; divide 10 by 4; power 2 to the 10; the unknown choice 9; quit. The program's responses (menu lines omitted, and the typed answers do not appear because the input was piped) were:
Choice: First number: Second number: Result: 5.0
Choice: First number: Second number: Cannot divide by zero.
Choice: First number: Second number: Result: 2.5
Choice: Base: Exponent (whole, 0 or more): Result: 1024.0
Choice: Unknown choice.
Choice: GoodbyeIn an interactive session each prompt appears on screen, followed by your typing on the same line. Look at the shape: the maths layer has no println, and main is short.
Example 2: trace one menu choice through the layers (practical)
Follow "add 2 and 3" on the call stack from Lesson 5:
mainreadschoice = 1and callsrunBasic(in, 1).runBasiccallsreadNumbertwice (each frame returns adouble), soa = 2.0,b = 3.0.runBasicevaluatesprintResult(add(a, b)). The inner call runs first:add(2.0, 3.0)returns5.0; thenprintResult(5.0)printsResult: 5.0.runBasicreturns tomain; the loop prints the menu again.
At the deepest point the stack is add over runBasic over main. This is a good moment to run your debugger or add a temporary Thread.dumpStack() to see it for yourself.
Example 3: improving it without breaking it (realistic)
Real programs change. Say a user asks for a modulus operation, or you want to refuse negative exponents. Because of the layers, each change touches one place:
- New operation (
remainder): addstatic double remainder(double a, double b)to the maths layer, add a number to the menu text, add a branch inmainandrunBasic. Nothing else moves. - Negative exponent: in
runPower, before callingpower, print "Exponent must be 0 or more." and return ifexponent < 0. Againpoweritself is unchanged. - Two decimals: change only
printResult, usingSystem.out.printf("Result: %.2f%n", value);from Module 11. Every operation then prints two decimals at once.
Backend Insight
The three layers are a tiny version of how backend services are organised: a layer that receives requests (here: main and readNumber), a layer with the business rules (the maths methods), and a layer that formats the response (printResult). In a real service the middle layer knows nothing about the web or the console, which is why it can be tested alone and reused. Splitting by responsibility is the point of this module. "Validate before you compute" is also the first line of defence in backend code.
Common Mistakes
Mistake 1: Computing and printing in the same method
The planned mistake above. If a maths method contains println, it cannot be reused for any other output.
Mistake 2: Creating a new Scanner in every helper
Each new Scanner(System.in) can buffer input independently, leading to lost or skipped input. Create one in main and pass it to the helpers.
Mistake 3: Letting Infinity or NaN reach the user
With double arithmetic, dividing by zero does not crash; it quietly prints Infinity or NaN. Check the divisor and show a message.
Mistake 4: Duplicated logic
Copy-pasting the "read two numbers" lines into every operation is what the lab's acceptance criteria forbid. Put them in readNumber and one handler.
Mistake 5: Skipping validation of the menu input
This demo uses nextInt and nextDouble directly, so typing a letter would crash it. Module 11 showed how to validate input; add that to your lab version once the methods work.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Which methods in the finished program are "maths layer", and what do they have in common?
أظهر الحل A1
A1. add, subtract, multiply, divide and power. They take values, return a value, and neither print nor read input.
A2. What does power(3, 0) return, and why does the loop produce it?
أظهر الحل A2
A2. It returns 1.0. The result starts as 1 and the loop body runs zero times because i < 0 is false immediately.
A3. Why is the Scanner passed to readNumber as a parameter?
أظهر الحل A3
A3. So all helpers read from the one Scanner that main created. A parameter is a copy of the reference, so every helper uses the same input source and no input is lost.
Level B: Application
B1. Add a remainder(double a, double b) method (use the % operator) and menu option 6 for it. Which existing methods must you change, and which stay untouched?
أظهر الحل B1
B1.
static double remainder(double a, double b) {
return a % b;
}Change printMenu (new text), main (send choice 6 to runBasic) and runBasic (a branch that prints remainder(a, b), plus the zero-divisor check, since % 0 is also a problem). The other maths and input/output methods stay untouched.
B2. Make runPower refuse a negative exponent with a message instead of calling power. Show the new runPower.
أظهر الحل B2
B2.
static void runPower(Scanner in) {
double base = readNumber(in, "Base: ");
int exponent = (int) readNumber(in, "Exponent (whole, 0 or more): ");
if (exponent < 0) {
System.out.println("Exponent must be 0 or more.");
return;
}
printResult(power(base, exponent));
}B3. Change the output so every result prints with two decimals. Which single method do you edit?
أظهر الحل B3
B3. Edit only printResult: System.out.printf("Result: %.2f%n", value);.
Level C: Challenge
C1. Refactor the if/else if chain in runBasic into a method static double calculate(int choice, double a, double b) that returns the result, so that runBasic becomes: read two numbers, check the zero case, print calculate(...). Discuss one advantage and one cost of this change.
أظهر الحل C1
C1.
static double calculate(int choice, double a, double b) {
if (choice == 1) return add(a, b);
if (choice == 2) return subtract(a, b);
if (choice == 3) return multiply(a, b);
return divide(a, b);
}
static void runBasic(Scanner in, int choice) {
double a = readNumber(in, "First number: ");
double b = readNumber(in, "Second number: ");
if (choice == 4 && b == 0) {
System.out.println("Cannot divide by zero.");
} else {
printResult(calculate(choice, a, b));
}
}Advantage: calculate is a pure function, so it can be checked on its own and runBasic is simpler. Cost: one more method to read and one more place that must know the menu numbers, so a change to the menu has to be made in two places.
C2. Write static boolean isDivisionSafe(double divisor) returning true when the divisor is not 0. Use it in runBasic and say why that name reads better than the raw b == 0 test.
أظهر الحل C2
C2.
static boolean isDivisionSafe(double divisor) {
return divisor != 0;
}The name states the intention, and the rule can later change in one place. It still passes a tiny divisor such as 0.0000000001, where the quotient is huge. For a beginner calculator an exact zero test is the expected rule.
Mini Challenge
Without looking at the lesson code, rebuild the calculator from scratch in a new file in this order, running after each step: (1) the five maths methods, tested from a temporary main; (2) printMenu, readNumber, printResult; (3) the loop in main with runBasic and runPower; (4) the zero-divisor check. Then add your own sixth operation.
Check your work: no maths method contains println or Scanner; dividing by 0 prints a message; there is exactly one Scanner; and main is under 20 lines.
Summary & Next Lesson
Key takeaways
- Build in layers: maths methods that return, input/output methods that read and print, and a short control layer.
- A maths method that prints cannot be reused; return the value and print in the caller.
- Handle the divide-by-zero case before dividing, so the user never sees a crash or
Infinity. - Create one
Scannerand pass it to helpers; the reference is copied, but it points to the same input. - Handler methods keep
mainshort and each change local to one method.
Next lesson
The final lesson, "Demo: tracing recursion on the call stack", uses a debugger to watch factorial(4) pile up and unwind, so you can finish the recursive factorial the lab asks for with confidence.
Self-check
- Which layer of the calculator should contain
System.out.println? - Why check for a zero divisor before calling
divide? - Why is the
Scannerpassed as a parameter? - What does
power(2, 3)do in the loop, step by step? - What is the main advantage of keeping
addsilent?
أظهر الإجابات
- The input/output layer (
printMenu,printResultand the handlers), not the maths methods. - With
double, dividing by zero givesInfinityorNaNrather than a clear message, and integer division would crash; checking first lets us show a friendly message. - So that every helper reads from the same
Scannercreated inmain. resultstarts at 1, then becomes 2, 4 and 8 after three multiplications by 2, and 8.0 is returned.- The caller can reuse the value (print it, store it, format it, test it) without unwanted output.
References
- Oracle, Defining Methods. Verified: method declaration parts and naming conventions used in the demo.
- Oracle, Passing Information to a Method or a Constructor. Verified: references are passed by value, which is why the shared
Scannerworks. - Behaviours claimed about
doubleandintdivision by zero (Infinity,NaN,/ by zero), the calculator's output and the compiler results come from running code with JDK 21 (javac 21.0.10). I did not fetch the JLS paragraph on floating-point division, so the general rule is stated from that observed behaviour. - The advice that several
Scannerobjects on one input can lose data is a common-practice statement that I did not verify against documentation.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.