前言
使用 Scanner 處理使用者輸入時,hasNextInt() 搭配 nextInt() 是常見模式。但若使用者輸入非整數內容,若不正確處理緩衝區,**極易陷入無限迴圈**。本文深入剖析根因,提供三種解決方案並比較優劣。
一、問題重現:為什麼會無限迴圈?
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print("請輸入整數: ");
if (sc.hasNextInt()) {
int num = sc.nextInt();
System.out.println("您輸入: " + num);
} else {
System.out.println("輸入錯誤,請重新輸入");
// ❌ 缺少消耗錯誤 token,導致無限迴圈
}
}
根因分析
hasNextInt() 檢查緩衝區**下一個 token** 是否為整數。
- 若使用者輸入
abc,hasNextInt() 回傳 false。
- 進入
else 區塊,**但緩衝區內的 abc 並未被消耗**!
- 下一次迴圈
hasNextInt() 仍檢查同一個 abc,再次回傳 false。
- **無限迴圈形成**。
二、三種解決方案完整比較
方案一:每次迴圈建立新 Scanner (不推薦)
while (true) {
Scanner sc = new Scanner(System.in); // 每次新建
if (sc.hasNextInt()) { ... }
else { System.out.println("錯誤"); }
// sc 結束時自動關閉 System.in,後續無法再讀取!
}
| 優點 | 缺點 |
| 邏輯簡單,無需手動清緩衝區 | 嚴重缺陷:Scanner 關閉時會關閉 System.in,導致後續無法讀取任何輸入 |
| 效能差,頻繁建立物件 |
| 無法處理同行多輸入 (如 10 20 abc) |
方案二:使用 next() 消耗單一錯誤 token (推薦單字輸入)
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print("請輸入整數: ");
if (sc.hasNextInt()) {
int num = sc.nextInt();
System.out.println("您輸入: " + num);
} else {
String badToken = sc.next(); // ✅ 消耗掉錯誤 token
System.out.println("無效輸入: '" + badToken + "',請輸入整數");
}
}
| 優點 | 缺點 |
| 精確消耗單一 token | 若使用者輸入 abc def 123,需迴圈 3 次才能清完錯誤輸入 |
| 保留同行後續合法輸入 | 不適合「整行輸入驗證」場景 |
方案三:使用 nextLine() 消耗整行 (推薦整行驗證)
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print("請輸入整數: ");
if (sc.hasNextInt()) {
int num = sc.nextInt();
sc.nextLine(); // ✅ 關鍵:消耗換行符,避免殘留影響下次 nextLine()
System.out.println("您輸入: " + num);
} else {
String badLine = sc.nextLine(); // ✅ 一次消耗整行錯誤輸入
System.out.println("無效輸入: '" + badLine + "',請輸入整數");
}
}
| 優點 | 缺點 |
| 一次清空整行,用戶體驗佳 | 若同行有混合輸入 10 abc,10 會被 nextInt() 讀走,abc 連同換行被 nextLine() 讀走 |
| 符合「逐行輸入」直覺 | |
避免換行符殘留導致後續 nextLine() 讀到空字串 | |
三、進階最佳實踐:封裝通用輸入工具類
import java.util.Scanner;
import java.util.function.Predicate;
public class InputUtil {
private static final Scanner SC = new Scanner(System.in);
/** 通用整數輸入驗證 */
public static int readInt(String prompt, Predicate validator, String errorMsg) {
while (true) {
System.out.print(prompt);
if (SC.hasNextInt()) {
int value = SC.nextInt();
SC.nextLine(); // 消耗換行
if (validator == null || validator.test(value)) {
return value;
}
System.out.println(errorMsg);
} else {
SC.nextLine(); // 清除整行無效輸入
System.out.println("格式錯誤,請輸入整數");
}
}
}
/** 通用字串輸入 (非空) */
public static String readNonEmptyString(String prompt) {
while (true) {
System.out.print(prompt);
String line = SC.nextLine().trim();
if (!line.isEmpty()) return line;
System.out.println("不可為空,請重新輸入");
}
}
// 使用範例
public static void main(String[] args) {
int age = readInt("請輸入年齡 (1-120): ",
v -> v >= 1 && v <= 120,
"年齡必須在 1-120 之間");
String name = readNonEmptyString("請輸入姓名: ");
System.out.printf("歡迎 %s,您 %d 歲%n", name, age);
}
}
四、常見陷阱總結表
| 現象 | 原因 | 解決 |
| 無限迴圈印出錯誤訊息 | 未消耗緩衝區錯誤 token | 在 else 區塊呼叫 next() 或 nextLine() |
nextLine() 讀到空字串 | 前一次 nextInt() 留下換行符 | nextInt() 後立即呼叫 nextLine() 清除換行 |
| 關閉 Scanner 後無法再讀取 | Scanner.close() 會關閉 System.in | 全程使用單一 Scanner 實例,程式結束前勿關閉 |
混合輸入 10 abc 行為怪異 | nextInt() 只讀 10,abc 留在緩衝區 | 統一用 nextLine() 讀整行,再用 Integer.parseInt() 解析 |
五、替代方案:完全避開 Scanner 緩衝區問題
若專案允許,建議使用 BufferedReader 讀整行再解析,邏輯最清晰:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class SafeInput {
private static final BufferedReader BR = new BufferedReader(new InputStreamReader(System.in));
public static int readInt(String prompt) throws IOException {
while (true) {
System.out.print(prompt);
String line = BR.readLine();
try {
return Integer.parseInt(line.trim());
} catch (NumberFormatException e) {
System.out.println("請輸入有效整數");
}
}
}
}
結語
Scanner 的緩衝區機制是導致無限迴圈的核心原因。**黃金法則:每次 hasNextXxx() 失敗時,必須在 else 區塊呼叫 next() 或 nextLine() 消耗輸入。** 方案三 (nextLine()) 最符合人類逐行輸入直覺,推薦作為預設處理模式;封裝通用工具類可大幅減少重複錯誤。