انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-011-L05

التحقق من المدخلات: لا تثق بالمستخدم أبداً

  • الدرس 5 من 6
  • 12 دقيقة قراءة
  • مبتدئ
في هذا الدرس 11 موضوعات

هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.

Lesson Overview

Lesson 3 ended with a polite program that gave up after one bad answer. This lesson makes it ask again. You will build the standard pattern for reading input safely: keep asking until the answer is acceptable.

There is also a larger idea in the title. A program cannot trust what comes in from outside. The user may be friendly and still type abc, press Enter on an empty line, enter -5 for a quantity, or paste a number with a stray comma. Another user may deliberately type something designed to break your program. Both cases get the same answer: check the input before you use it. This habit is the seed of an entire area of backend engineering, because web services receive input from strangers all day.

At the shop counter: if a customer says "I'll take minus three pens", the cashier does not ring that up. They say "How many?" again. Your program must be that cashier.

A note on tools. Asking again needs a decision (if) and repetition (while), taught properly in BEG-012 and BEG-013. Here you get just enough to read and run the pattern. Treat it as a preview: you are not expected to master loops today.

This lesson builds on Lessons 1 to 4, in particular hasNextInt() from Lesson 3.

Learning Objectives

By the end of this lesson you will be able to:

  • Explain why input must be checked even when the user is friendly.
  • Distinguish format validation (is it a number?) from range validation (is it at least 1?).
  • Read and run the loop pattern that asks again until an int in range is entered.
  • Explain why the bad token must be discarded with next(), and what happens if it is not.
  • Reject empty text and out-of-range or special numbers such as NaN.
  • Give a clear error message that tells the user what to type.

Written Explanation

Why input cannot be trusted

Input is anything that comes from outside the code you wrote: the keyboard, a file, a web request, a command-line argument. You do not control it, so assume it can be:

Kind of problemExample
Wrong formatabc where a number is needed
Wrong range-3 or 0 as a quantity
Missingthe user just presses Enter
Surprising but valid-looking1e400, NaN, or Infinity in a decimal field
Deliberately hostiletext crafted to break something downstream

The OWASP Input Validation Cheat Sheet, a widely used guide for developers, makes two points we use here. First, validation covers both the form of a value and whether it makes sense for the operation, and its own example is a booking whose end date must come after its start date. Second, it recommends defining what is acceptable and rejecting everything else (an allowlist), instead of trying to list every bad value.

For our small programs the allowlist is simple: a whole number from 1 to 10 means exactly that and nothing else.

Two kinds of check

  1. Format: can the text be read as the type I need? hasNextInt() answers that for an int.
  2. Range or meaning: is the value sensible? A quantity must be at least 1; an age is probably below 150; a price must be above zero.

A value can pass the first check and fail the second: -5 is a perfectly good int. Both checks are needed.

The decision: if

You saw the shape in Lesson 3. A condition inside parentheses is either true or false, and the block runs only if it is true:

java
if (qty >= 1) {
    // runs only when qty is 1 or more
}

The conditions use the comparison and logical operators of BEG-010: >=, <=, && ("and"), ! ("not").

The repetition: while (true) and break

A while loop repeats its block as long as its condition is true. while (true) never stops by itself, so we use break to leave it from the inside once the input is good:

java
while (true) {
    // ask, read, check
    // if the answer is good: break;
}

Read it as "keep doing this forever, until a break is reached". It is a very common pattern for input, and BEG-013 explains loops and break in more detail.

The complete pattern

java
int qty;
while (true) {
    System.out.print("Quantity (1-10): ");
    if (in.hasNextInt()) {
        qty = in.nextInt();
        if (qty >= 1 && qty <= 10) {
            break;                  // good answer: leave the loop
        }
    } else {
        in.next();                  // discard the bad token
    }
    System.out.println("Enter a whole number from 1 to 10.");
}

Walk through it:

  1. Print the prompt.
  2. hasNextInt() asks "is a whole number waiting?" without reading it. If yes, read it with nextInt().
  3. Check the range. If it passes, break leaves the loop. If not, the code falls out of the if and reaches the error message, and the loop starts again.
  4. If no whole number is waiting, discard the bad token with in.next(), print the message, and loop again.

When the loop ends, qty is guaranteed to hold a whole number from 1 to 10. Everything after the loop can rely on that.

Why in.next() matters

hasNextInt() only looks. If it returns false, the bad token is still waiting. Without the discard, the loop asks hasNextInt() again, the same bad token is still first in line, the answer is false again, and the program prints the error message forever without waiting for the user. This is an infinite loop. A deliberately broken version, limited to three tries so it is safe to run:

java
int count = 0;
while (!in.hasNextInt() && count < 3) {
    System.out.println("Try again");
    count++;
}

With abc typed, the real output is:

text
Try again
Try again
Try again
stopped after 3

It never waited for new input, because abc was never removed. in.next() removes exactly one token: the bad one. (If the user typed several words, the loop handles them one at a time and prints the message once per bad token. That is acceptable for a first version.)

Validation with lines: text and decimals

For text fields, the check is often "not empty". The String method trim() removes spaces at both ends, and isEmpty() is true for text of length zero. (BEG-009 gave length(); isEmpty() is the same check as length() == 0.) A name made of spaces only is also empty:

java
String name = in.nextLine().trim();
if (name.isEmpty()) {
    // ask again
}

For a decimal such as a price, a tidy way to check a line is to hand the text to a small temporary scanner and ask hasNextDouble(). Here is a complete, tested loop. It uses continue, which means "skip the rest of this round and start the next round":

java
double price;
while (true) {
    System.out.print("Price: ");
    String line = in.nextLine().trim();
    if (line.isEmpty()) {
        System.out.println("Type a price.");
        continue;
    }
    if (!new Scanner(line).hasNextDouble()) {
        System.out.println("Not a number.");
        continue;
    }
    price = Double.parseDouble(line);
    if (price > 0) {
        break;
    }
    System.out.println("Price must be above zero.");
}

The ! means "not". new Scanner(line) makes a throw-away scanner that reads from a String instead of the keyboard. It uses the locale rules of Lesson 1, so on a comma-decimal machine test with the formats you intend to accept.

The sneaky decimals: NaN and Infinity

Decimal parsing accepts the words NaN ("not a number") and Infinity. In a compiled test, Scanner reported hasNextDouble() as true for both. Look at what comparisons do with them:

  • NaN > 0 is false, and NaN < 0 is also false. So the check price > 0 rejects NaN, but a check written as "reject if price < 0" would let it through.
  • Infinity > 0 is true. A check for "above zero" alone accepts it.

This is a good reason to write the check as "accept only if it is in the range I want", the allowlist way, and to set an upper bound where there is a sensible one. A realistic price check would be price > 0 && price < 100000 or whatever your business allows.

Good error messages

A message such as Invalid input tells the user nothing. A good message says what is expected: Enter a whole number from 1 to 10.

Validation never ends at the keyboard

The same rules apply to every program that takes input from anywhere. For a web service, validation in the browser is only a convenience: OWASP's cheat sheet says client-side checks can be bypassed, so the server must validate again. Injection attacks, in which a stranger's input is treated as part of a command, depend on a program that trusted its input.

Examples

Example 1: whole number from 1 to 10 (simple)

The pattern from above as a program. With the user typing abc, 0, 11, 3.5, and finally 7:

text
Quantity (1-10): Enter a whole number from 1 to 10.
Quantity (1-10): Enter a whole number from 1 to 10.
Quantity (1-10): Enter a whole number from 1 to 10.
Quantity (1-10): Enter a whole number from 1 to 10.
Quantity (1-10): OK, 7

(The typed answers are not echoed here, because the test read them from a file.) abc fails the format test, 0 and 11 fail the range test, 3.5 fails the format test (it is not a whole number), and 7 passes.

Example 2: a validated price from a line (practical)

The price loop from the Written Explanation, tested with the lines: (empty), free, -2, 0, 2.5:

text
Price: Type a price.
Price: Not a number.
Price: Price must be above zero.
Price: Price must be above zero.
Price: price=2.5

Each bad case gets its own message, and the program asks until it has a positive number. Note that the empty line and free are caught before Double.parseDouble runs, so the program never crashes.

Example 3: what NaN does (realistic)

A test program that reads one decimal and compares it:

java
double d = in.nextDouble();
System.out.println(d + " " + (d > 0) + " " + (d < 0));
User typesOutputMeaning
NaNNaN false falseneither positive nor negative
InfinityInfinity true falsepasses a "greater than zero" check

This is why a real validation uses a closed range, not only "not negative". A price of NaN would otherwise slip into your totals, and every sum it touches becomes NaN.

Backend Insight

Every layer of a backend that receives data asks the same questions you just coded: Is the format right? Is the value in range? Is it allowed at all? Java teams later use Bean Validation, which lets you write the rules as annotations (@Min(1), @NotBlank) instead of loops. The rules you write now are those rules in plain code. Learn them here and the annotations will be easy.

Two habits to keep:

  • Validate at the border. Check input where it enters the program, so that the rest of the code can rely on it.
  • Reject, do not repair, when unsure. Silently turning abc into 0 hides the problem and produces wrong results later.

Common Mistakes

Mistake 1: Not discarding the bad token

Forgetting in.next() causes an infinite loop that prints the error message endlessly.

Mistake 2: Checking format but not range

nextInt() accepts -5 and 0. For a quantity, add qty >= 1.

Mistake 3: Silent defaults

Using 0 or 1 when the input is bad hides errors. Ask again, with a message that says what is expected, not just "Error!".

Mistake 4: Checking only "not negative"

NaN is not negative and not positive. Accept only what you intend: a closed range.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. Give two reasons to validate input from a friendly user.

أظهر الحل A1

A1. Any two of: people mistype; people misunderstand the question (a word instead of a number); a value may be valid in format but not sensible (-5 seats); input from other programs or attackers may be wrong or hostile.

A2. Which check is a format check and which is a range check: (a) hasNextInt(); (b) age >= 0 && age <= 120?

أظهر الحل A2

A2. (a) format; (b) range.

A3. In the loop pattern, what does in.next() do inside the else branch, and what would happen without it?

أظهر الحل A3

A3. It reads and discards one token, the one that was not a whole number. Without it, hasNextInt() would keep returning false for the same token, and the loop would repeat endlessly without waiting for new input.

Level B: Application

B1. Write a loop that asks for a percentage and accepts only a whole number from 0 to 100.

أظهر الحل B1

B1.

java
int pct;
while (true) {
    System.out.print("Percent (0-100): ");
    if (in.hasNextInt()) {
        pct = in.nextInt();
        if (pct >= 0 && pct <= 100) {
            break;
        }
    } else {
        in.next();
    }
    System.out.println("Enter a whole number from 0 to 100.");
}

B2. Write a loop that asks for a name and keeps asking while the name is empty or only spaces.

أظهر الحل B2

B2.

java
String name;
while (true) {
    System.out.print("Name: ");
    name = in.nextLine().trim();
    if (!name.isEmpty()) {
        break;
    }
    System.out.println("The name cannot be empty.");
}

If you used nextInt() earlier in the program, remember the throw-away nextLine() from Lesson 2 before this loop.

B3. Rewrite the error message Wrong! so that it helps a user who typed -1 as the number of seats (valid seats: 1 to 4).

أظهر الحل B3

B3. For example: Seats must be a whole number from 1 to 4. It says what is valid.

Level C: Challenge

C1. Write a loop for a decimal temperature between -50 and 60 (inclusive). It must reject text, and also NaN and Infinity. Explain why your range check rejects the two special values.

أظهر الحل C1

C1.

java
double temp;
while (true) {
    System.out.print("Temperature (-50 to 60): ");
    if (in.hasNextDouble()) {
        temp = in.nextDouble();
        if (temp >= -50 && temp <= 60) {
            break;
        }
    } else {
        in.next();
    }
    System.out.println("Enter a number from -50 to 60.");
}

NaN fails every comparison, so temp >= -50 is false and it is rejected. Infinity is larger than 60 and fails temp <= 60. (Reasoned from the compiled NaN/Infinity test of Example 3; this exact loop was not run.)

C2. The user types 5 abc 7 on one line for a loop that wants one whole number in 1..10. Trace what the quantity loop does: which tokens are read, and what is qty at the end?

أظهر الحل C2

C2. The first pass: hasNextInt() is true, nextInt() reads 5, and the range check passes, so the loop ends with qty = 5. The remaining text abc 7 stays in the input for the next read. This is the same leftover issue as Lesson 2: a later nextLine() would return abc 7.

Mini Challenge

Write Seat booking. Ask for a name (line, must not be empty), a number of seats (whole number 1 to 4), and a ticket price (decimal, above zero and below 1000). Re-ask with a specific message for each failure. Then print one confirmation line with printf. Test it with: an empty name, abc seats, 0 seats, 5 seats, -3 price, NaN price, and finally good values.

Check your work: no input crashes the program; each loop re-asks with a message that says what is valid; the leftover line after the seats number does not skip the name or the price.

Summary & Next Lesson

Key takeaways

  • Treat all outside input as untrustworthy: friendly users make mistakes, and hostile users exploit them.
  • Check format (hasNextInt(), hasNextDouble()) and range (qty >= 1 && qty <= 10).
  • Pattern: loop, prompt, check, break on success; on a bad token discard it with in.next().
  • Accept only what you intend: a closed range also rejects NaN and Infinity.
  • Give messages that say what is expected. Validate at the border, then rely on the value.

Next lesson

The last lesson, "Demo: building the receipt printer with input validation", combines reading, validating, and formatting into one program, and it prepares you for the module lab.

Self-check

  1. Why is -5 a problem even though hasNextInt() returns true for it?
  2. What does in.next() do in the else-branch?
  3. What is an infinite loop, and how does forgetting in.next() cause one?
  4. Why is "reject if negative" not enough for decimals?
  5. What makes a good error message?
أظهر الإجابات
  1. It is a valid int (format), but not a valid quantity (range).
  2. It discards the bad token so the next check sees fresh input.
  3. A loop that never ends. Without next(), the same bad token is checked again and again.
  4. NaN is not negative, so it passes a "not negative" check. Use an allowed range.
  5. It says what is expected, ideally with an example, such as the valid range.

References

  • OWASP Cheat Sheet Series, Input Validation Cheat Sheet. Supports checking both type/format and meaning, preferring allowlists, and validating on the server even when the browser also checks.
  • Oracle, Java SE 21 API, Scanner. Supports hasNextInt() not advancing the scanner and locale-dependent number reading.
  • Oracle, Java SE 21 API, Double.parseDouble. Supports acceptance of NaN and Infinity as valid text.
  • The loops, the NaN and Infinity comparisons, and the printed transcripts were compiled and run with JDK 21. The description of Bean Validation annotations and of injection attacks is general background that was not checked against a page here.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.