Giải thích nhập chuẩn trong Java: Scanner vs BufferedReader và các kỹ thuật nhập nhanh

目次

1. Những gì bạn sẽ học trong bài viết này (Kết luận nhanh)

Có một vài cách để xử lý đầu vào chuẩn trong Java, nhưng ý tưởng chính rất đơn giản:

Chọn phương pháp nhập dựa trên mục đích của bạn.

Bạn không cần giải pháp nhanh nhất hay phức tạp nhất ngay từ đầu.
Bài viết này giải thích đầu vào chuẩn của Java từng bước, để bạn có thể hiểu rõ khi nào và tại sao sử dụng mỗi cách tiếp cận.

Chúng ta sẽ đề cập đến nhập liệu trong Java ở ba mức độ thực tiễn:

  • Dành cho người mới bắt đầu và các chương trình nhỏ : Scanner
  • Dành cho đầu vào lớn và hiệu năng ổn định : BufferedReader
  • Dành cho lập trình thi đấu và đầu vào rất lớn : FastScanner

1.1 Bạn nên dùng cái nào? (Scanner vs BufferedReader vs Fast Input)

Nếu bạn đang vội, phần này một mình đã đủ để quyết định.

1) Học Java hoặc xử lý đầu vào nhỏ → Scanner

  • Trường hợp sử dụng điển hình: wp:list /wp:list

    • Hướng dẫn Java
    • Bài tập trường học
    • Công cụ dòng lệnh nhỏ
  • Ưu điểm: wp:list /wp:list

    • Rất dễ viết
    • Dễ đọc và hiểu
  • Nhược điểm: wp:list /wp:list

    • Chậm khi kích thước đầu vào trở nên lớn

Sử dụng Scanner khi:

  • Kích thước đầu vào nhỏ
  • Độ dễ đọc quan trọng hơn hiệu năng

2) Đầu vào lớn hoặc hiệu năng ổn định → BufferedReader

  • Trường hợp sử dụng điển hình: wp:list /wp:list

    • Ứng dụng thực tế
    • Xử lý hàng loạt
    • Bài tập lập trình với nhiều dòng đầu vào
  • Ưu điểm: wp:list /wp:list

    • Nhanh hơn nhiều so với Scanner
    • Hành vi dự đoán được và ổn định
  • Nhược điểm: wp:list /wp:list

    • Bạn phải tự phân tích số và token

BufferedReader là lựa chọn tốt khi:

  • Kích thước đầu vào lớn
  • Hiệu năng quan trọng
  • Bạn muốn kiểm soát toàn bộ quá trình xử lý đầu vào

3) Lập trình thi đấu hoặc đầu vào khổng lồ → FastScanner

  • Trường hợp sử dụng điển hình: wp:list /wp:list

    • Lập trình thi đấu
    • Các vấn đề có thời gian giới hạn rất chặt chẽ
  • Ưu điểm: wp:list /wp:list

    • Rất nhanh
  • Nhược điểm: wp:list /wp:list

    • Khó đọc
    • Khó gỡ lỗi
    • Không phù hợp cho phát triển ứng dụng thông thường

FastScanner phù hợp khi:

  • Kích thước đầu vào khổng lồ
  • Scanner hoặc BufferedReader gây lỗi thời gian

1.2 Ai là đối tượng của bài viết này

Bài viết này được thiết kế cho một dải rộng các nhà phát triển Java.

Người mới bắt đầu

  • Muốn hiểu đầu vào chuẩn là gì
  • Cần giải thích rõ ràng về Scanner
  • Thường bị nhầm lẫn bởi các lỗi liên quan đến đầu vào

Người học trung cấp

  • Đã biết Scanner
  • Muốn hiểu tại sao nó chậm
  • Cần cách đáng tin cậy để xử lý đầu vào lớn

Lập trình viên thi đấu

  • Cần kỹ thuật nhập nhanh
  • Muốn mẫu thực tế cho các cuộc thi

1.3 Bạn sẽ có thể làm gì sau khi đọc

Khi kết thúc bài viết, bạn sẽ có thể:

  • Hiểu thực sự gì là đầu vào chuẩn của Java (System.in)
  • Chọn phương pháp nhập đúng cho mỗi tình huống
  • Sử dụng Scanner mà không gặp các bẫy thường gặp
  • Đọc đầu vào lớn hiệu quả với BufferedReader
  • Xử lý đầu vào lập trình thi đấu bằng các kỹ thuật nhanh
  • Gỡ lỗi các vấn đề thường gặp liên quan đến đầu vào một cách tự tin

1.4 Điều gì sẽ đến tiếp theo

Trong phần tiếp theo, chúng ta sẽ giải thích đầu vào chuẩn thực sự là gì trong Java, bắt đầu từ System.in.

Hiểu nền tảng này sẽ làm cho sự khác biệt giữa Scanner, BufferedReader và các phương pháp nhập nhanh trở nên rõ ràng hơn.

2. “Đầu vào chuẩn” trong Java là gì?

Trước khi học cách sử dụng Scanner hoặc BufferedReader, quan trọng là phải hiểu đầu vào chuẩn thực sự là gì trong Java.
Nhiều sự nhầm lẫn liên quan đến nhập liệu xuất phát từ việc bỏ qua khái niệm cơ bản này.

2.1 Vai trò của đầu vào chuẩn (System.in)

Trong Java, đầu vào chuẩn là nguồn dữ liệu mặc định mà một chương trình đọc khi nó khởi chạy.
Nguồn này được biểu diễn bằng đối tượng sau:

System.in

Các điểm chính về System.in:

  • Nó đại diện cho một luồng đầu vào được hệ điều hành cung cấp
  • Kiểu của nó là InputStream
  • Java tự nó không quyết định nguồn đầu vào đến từ đâu

Nói cách khác, System.in chỉ đơn giản là một luồng dữ liệu, không phải một “API bàn phím”.

2.2 Đầu vào chuẩn không luôn luôn là bàn phím

Một hiểu lầm rất phổ biến là:

Đầu vào chuẩn = đầu vào bàn phím

Điều này chỉ đúng một phần.

Khi bạn chạy một chương trình như sau:

java Main

đầu vào chuẩn được kết nối với bàn phím.

Tuy nhiên, bạn cũng có thể chạy nó như sau:

java Main < input.txt

Trong trường hợp này:

  • Đầu vào đến từ một tệp
  • Không phải từ bàn phím
  • Nhưng Java vẫn đọc nó qua System.in

Từ quan điểm của chương trình, không có sự khác biệt nào.

Đây là lý do tại sao đầu vào chuẩn thường được mô tả là:

“Bất kỳ dữ liệu nào được đưa vào chương trình khi chạy”

2.3 Tại sao System.in khó sử dụng trực tiếp

Mặc dù System.in mạnh mẽ, nhưng nó không thuận tiện để sử dụng trực tiếp.

Lý do rất đơn giản:

  • System.in đọc byte thô
  • Nó không hiểu: wp:list /wp:list

    • Dòng
    • Số
    • Khoảng trắng
    • Mã hoá văn bản

Ví dụ:

InputStream in = System.in;

Ở mức này, bạn chỉ làm việc với các byte, không phải các giá trị có ý nghĩa.

Đó là lý do Java cung cấp các lớp bao bọc để chuyển đổi đầu vào thô thành dữ liệu có thể sử dụng.

2.4 Các lớp xử lý đầu vào trong Java

Quá trình xử lý đầu vào của Java có thể hiểu như một cấu trúc lớp.

[ Input source (keyboard, file, pipe) ]
                ↓
           System.in (InputStream)
                ↓
      Input helper classes
        ├ Scanner
        ├ BufferedReader
        └ Fast input implementations

Mỗi lớp có một trách nhiệm rõ ràng:

  • System.in wp:list /wp:list

    • Luồng byte cấp thấp
    • Scanner wp:list /wp:list

    • Nhập dựa trên token dễ dàng (chậm nhưng đơn giản)

    • BufferedReader wp:list /wp:list

    • Nhập dựa trên dòng nhanh

    • FastScanner wp:list /wp:list

    • Nhập số tập trung vào hiệu năng

Hiểu cấu trúc này giải thích tại sao có nhiều phương pháp nhập liệu.

2.5 Tại sao Java có nhiều phương pháp nhập liệu

Java được sử dụng trong nhiều bối cảnh khác nhau:

  • Giáo dục
  • Hệ thống doanh nghiệp
  • Công cụ dòng lệnh
  • Lập trình thi đấu

Mỗi bối cảnh có các ưu tiên khác nhau:

  • Dễ sử dụng
  • Độ dễ đọc
  • Hiệu năng
  • Ổn định

Vì vậy, Java không ép buộc một “phương pháp nhập tốt nhất” duy nhất.
Thay vào đó, nó cung cấp nhiều công cụ cho các nhu cầu khác nhau.

2.6 Những gì sẽ tới tiếp theo

Trong phần tiếp theo, chúng ta sẽ bắt đầu sử dụng Scanner, cách thân thiện nhất cho người mới bắt đầu để đọc đầu vào chuẩn.

Bạn sẽ học:

  • Cách đọc chuỗi
  • Cách đọc số
  • Những bẫy thường gặp khiến người mới bối rối

Điều này sẽ chuẩn bị cho bạn việc hiểu các phương pháp nhập nhanh hơn sau này.

3. Các cách thường dùng để đọc đầu vào chuẩn trong Java (Tổng quan)

Bây giờ bạn đã hiểu đầu vào chuẩn là gì, hãy xem ba cách chính để đọc nó trong Java.
Trước khi đi sâu vào chi tiết mã, quan trọng là nhìn thấy bức tranh tổng thể.

Mỗi phương pháp tồn tại vì một lý do, và chọn đúng sẽ tiết kiệm thời gian và giảm bớt phiền toái.

3.1 Scanner: Lựa chọn dễ nhất và thân thiện nhất cho người mới bắt đầu

Scanner thường là lớp nhập liệu đầu tiên mà người mới học Java tiếp cận.

Scanner sc = new Scanner(System.in);

Với một dòng duy nhất này, bạn có thể dễ dàng đọc:

  • Chuỗi
  • Số nguyên
  • Số thực

Các tính năng chính của Scanner

  • Pros wp:list /wp:list

    • Rất dễ viết và hiểu
    • Đọc giá trị trực tiếp dưới dạng kiểu Java
    • Rộng rãi trong các hướng dẫn và sách giáo trình
    • Cons wp:list /wp:list

    • Chậm đối với đầu vào lớn

    • Có một số hành vi khó hiểu mà người mới thường gặp

Khi nào Scanner là lựa chọn tốt

  • Học Java
  • Chương trình nhỏ
  • Yêu cầu nhập liệu đơn giản

Nếu mục tiêu của bạn là hiểu cú pháp và logic Java, Scanner là một điểm khởi đầu tuyệt vời.

3.2 BufferedReader: Nhanh và Đáng Tin Cậy cho Các Ứng Dụng Thực Tế

BufferedReader là lựa chọn tiêu chuẩn khi kích thước đầu vào hoặc hiệu năng trở nên quan trọng.

BufferedReader br = new BufferedReader(
    new InputStreamReader(System.in)
);

Không giống như Scanner, BufferedReader tập trung vào đọc toàn bộ dòng một cách hiệu quả.

Các tính năng chính của BufferedReader

  • Ưu điểm wp:list /wp:list

    • Nhanh hơn nhiều so với Scanner
    • Hành vi dự đoán được và ổn định
    • Thích hợp cho lượng dữ liệu lớn
    • Nhược điểm wp:list /wp:list

    • Cần phải phân tích thủ công

    • Mã hơi phức tạp hơn

Khi nào BufferedReader là lựa chọn tốt

  • Ứng dụng thực tế
  • Xử lý hàng loạt
  • Các bài toán lập trình có nhiều dòng đầu vào

BufferedReader thường được coi là lựa chọn chuyên nghiệp mặc định.

3.3 Đầu vào Nhanh (FastScanner): Dành cho Lập Trình Thi Đấu

Trong lập trình thi đấu, ngay cả BufferedReader cũng có thể quá chậm.

Để giải quyết vấn đề này, nhiều nhà phát triển sử dụng các lớp đầu vào nhanh tùy chỉnh, thường được gọi là FastScanner.

Đặc điểm của đầu vào nhanh

  • Đọc byte thô bằng bộ đệm
  • Tránh tạo đối tượng không cần thiết
  • Chuyển đổi số một cách thủ công

Ưu và nhược điểm

  • Ưu điểm wp:list /wp:list

    • Siêu nhanh
    • Lý tưởng cho kích thước đầu vào khổng lồ
    • Nhược điểm wp:list /wp:list

    • Khó đọc

    • Khó gỡ lỗi
    • Không phù hợp cho các ứng dụng thông thường

Khi nào nên dùng đầu vào nhanh

  • Lập trình thi đấu
  • Giới hạn thời gian rất chặt chẽ
  • Kích thước đầu vào khổng lồ

Trong phát triển phần mềm bình thường, đầu vào nhanh hiếm khi cần thiết.

3.4 Bảng So Sánh Nhanh

MethodEase of UseSpeedTypical Use
ScannerVery highLowLearning, small programs
BufferedReaderMediumHighReal applications
FastScannerLowVery highCompetitive programming

Bảng này một mình đã có thể giúp bạn quyết định công cụ nào nên sử dụng.

3.5 Cách Quyết Định Khi Bạn Không Chắc

Nếu bạn không chắc phương pháp nào nên chọn, hãy theo quy tắc này:

  1. Đây có phải là để học tập hoặc một chương trình nhỏ không? wp:list /wp:list

    • Có → Scanner 2. Đầu vào có lớn hoặc hiệu năng quan trọng không? wp:list /wp:list

    • Có → BufferedReader 3. Đây có phải là một bài toán lập trình thi đấu không? wp:list /wp:list

    • Có → FastScanner

Hầu hết các trường hợp thực tế đều được giải quyết hoàn hảo bằng BufferedReader.

3.6 Những Gì Sắp Đến

Trong phần tiếp theo, chúng ta sẽ tập trung vào cách sử dụng Scanner một cách đúng đắn.

Bạn sẽ học:

  • Cách đọc chuỗi
  • Cách đọc số
  • Những lỗi thường gặp khi dùng Scanner và cách tránh chúng

Điều này sẽ giúp bạn viết mã xử lý đầu vào chính xác ngay từ đầu.

4. Đọc Dữ Liệu Đầu Vào Tiêu Chuẩn với Scanner (Từ Cơ Bản Đến Mẹo Thực Tế)

Trong phần này, chúng ta sẽ tập trung vào Scanner, cách thân thiện nhất cho người mới bắt đầu để đọc dữ liệu đầu vào tiêu chuẩn trong Java.
Chúng tôi không chỉ sẽ chỉ cách sử dụng mà còn giải thích tại sao một số vấn đề xảy ra, để bạn có thể tránh các lỗi phổ biến.

4.1 Đọc Một Dòng Văn Bản (nextLine)

Trường hợp sử dụng đơn giản nhất là đọc một dòng văn bản đầy đủ.

Scanner sc = new Scanner(System.in);
String line = sc.nextLine();
System.out.println(line);
  • nextLine() đọc mọi thứ cho tới ký tự xuống dòng
  • Các khoảng trắng trong dòng được giữ nguyên

Ví dụ đầu vào:

Hello Java World

Kết quả:

Hello Java World

Phương pháp này lý tưởng khi bạn muốn đọc câu hoặc văn bản tự do.

4.2 Đọc Số (nextInt, nextLong, v.v.)

Một trong những ưu điểm lớn nhất của Scanner là nó có thể đọc số trực tiếp.

int n = sc.nextInt();
long x = sc.nextLong();
double d = sc.nextDouble();

Nếu đầu vào là:

42

Giá trị 42 sẽ được lưu dưới dạng int mà không cần chuyển đổi thủ công.

Tại sao điều này tiện lợi

  • Không cần Integer.parseInt
  • Giảm mã lặp lại
  • Dễ hiểu cho người mới bắt đầu

4.3 Cạm Bẫy Nổi Tiếng: nextInt() Theo Sau Bởi nextLine()

Đây là một trong những lỗi phổ biến nhất liên quan đến Scanner.

int n = sc.nextInt();
String s = sc.nextLine(); // often becomes empty

Tại sao lại xảy ra điều này?

  • nextInt() chỉ đọc số
  • Ký tự xuống dòng (\n) còn lại trong bộ đệm đầu vào
  • nextLine() đọc ký tự xuống dòng còn lại đó

Kết quả, s trở thành một chuỗi rỗng.

4.4 Cách khắc phục vấn đề nextLine()

Giải pháp tiêu chuẩn là tiêu thụ ký tự xuống dòng còn lại.

int n = sc.nextInt();
sc.nextLine(); // consume newline
String s = sc.nextLine();

Mẫu này rất phổ biến và đáng ghi nhớ.

Ngoài ra, bạn có thể tránh hoàn toàn việc trộn nextInt()nextLine() bằng cách đọc mọi thứ dưới dạng chuỗi.

4.5 Đọc các giá trị cách nhau bằng dấu cách

Scanner tự động coi dấu cách và ký tự xuống dòng là dấu phân tách.

Đầu vào:

10 20 30

Mã:

int a = sc.nextInt();
int b = sc.nextInt();
int c = sc.nextInt();

Cách này hoạt động mà không cần bất kỳ logic bổ sung nào.

Chi phí ẩn

  • Scanner nội bộ sử dụng biểu thức chính quy
  • Điều này làm cho nó linh hoạt, nhưng chậm hơn đối với đầu vào lớn

4.6 Đọc cho đến khi hết đầu vào (EOF)

Đôi khi bạn cần đọc đầu vào cho đến khi không còn dữ liệu.

while (sc.hasNext()) {
    int value = sc.nextInt();
    System.out.println(value);
}

Hoặc đối với đầu vào dựa trên dòng:

while (sc.hasNextLine()) {
    String line = sc.nextLine();
    System.out.println(line);
}

Mẫu này hữu ích cho việc đọc file và các hệ thống chấm bài trực tuyến.

4.7 Tại sao Scanner chậm với đầu vào lớn

Scanner chậm không phải vì nó “xấu”, mà vì nó được thiết kế cho sự an toàn và linh hoạt.

Các nguyên nhân bao gồm:

  • Phân tích biểu thức chính quy
  • Chuyển đổi kiểu tự động
  • Kiểm tra đầu vào kỹ lưỡng

Đối với đầu vào nhỏ, chi phí này không đáng kể.
Đối với đầu vào lớn, nó trở thành vấn đề hiệu năng nghiêm trọng.

4.8 Khi nào nên dùng Scanner

Scanner là lựa chọn tốt khi:

  • Bạn đang học Java
  • Kích thước đầu vào nhỏ
  • Độ dễ đọc của mã quan trọng hơn tốc độ

Nếu hiệu năng trở thành mối quan ngại, đã đến lúc chuyển sang BufferedReader.

4.9 Những gì sẽ tới tiếp theo

Trong phần tiếp theo, chúng ta sẽ giới thiệu BufferedReader, giải pháp tiêu chuẩn cho việc nhập nhanh và đáng tin cậy.

Bạn sẽ học:

  • Nhập dựa trên dòng
  • Phân tích số thủ công
  • Xử lý hiệu quả đầu vào lớn

5. Đọc đầu vào chuẩn với BufferedReader (Nhanh và Đáng tin cậy)

Trong phần này, chúng ta chuyển sang BufferedReader, được sử dụng rộng rãi trong các chương trình Java thực tế và các thử thách lập trình.
So với Scanner, nó yêu cầu một chút mã nhiều hơn, nhưng mang lại hiệu năng và kiểm soát tốt hơn rất nhiều.

5.1 Ví dụ tối thiểu: Đọc một dòng duy nhất

Cấu hình cơ bản cho BufferedReader như sau:

BufferedReader br = new BufferedReader(
    new InputStreamReader(System.in)
);

String line = br.readLine();
System.out.println(line);
  • readLine() đọc đầu vào dòng theo dòng
  • Nó trả về null khi không còn đầu vào (EOF)

Hành vi rõ ràng này giúp việc gỡ lỗi dễ dàng hơn nhiều so với Scanner.

5.2 Đọc nhiều dòng cho đến EOF

Khi số lượng dòng đầu vào không biết trước, mẫu này là tiêu chuẩn:

String line;
while ((line = br.readLine()) != null) {
    System.out.println(line);
}

Tại sao mẫu này hoạt động tốt

  • null chỉ rõ ràng kết thúc đầu vào
  • Không cần kiểm tra thêm
  • Rất phổ biến trong việc đọc file và các hệ thống chấm bài trực tuyến

5.3 Chuyển đổi chuỗi thành số

Khác với Scanner, BufferedReader luôn đọc chuỗi, vì vậy việc chuyển đổi số phải thực hiện thủ công.

int n = Integer.parseInt(br.readLine());
long x = Long.parseLong(br.readLine());
double d = Double.parseDouble(br.readLine());

Lưu ý quan trọng

  • Nếu đầu vào không phải là số hợp lệ, sẽ xảy ra NumberFormatException
  • Điều này buộc bạn phải chính xác về định dạng đầu vào

Mặc dù ban đầu có thể bất tiện, nhưng nó dẫn đến các chương trình dự đoán được hơn.

5.4 Xử lý đầu vào cách nhau bằng dấu cách

Các giá trị cách nhau bằng dấu cách rất phổ biến.

Đầu vào:

10 20 30

Phương pháp 1: Sử dụng split (Ưu tiên Độ dễ đọc)

String[] parts = br.readLine().split(" ");
int a = Integer.parseInt(parts[0]);
int b = Integer.parseInt(parts[1]);
int c = Integer.parseInt(parts[2]);
  • Dễ hiểu
  • Phù hợp với đầu vào nhỏ đến trung bình
  • Chậm hơn một chút do sử dụng biểu thức chính quy

Phương pháp 2: Sử dụng StringTokenizer (Ưu tiên Hiệu năng)

StringTokenizer st = new StringTokenizer(br.readLine());
int a = Integer.parseInt(st.nextToken());
int b = Integer.parseInt(st.nextToken());
int c = Integer.parseInt(st.nextToken());
  • Nhanh hơn split
  • Rất phổ biến trong lập trình thi đấu
  • Vẫn hoàn toàn hợp lệ trong Java hiện đại

5.5 Đọc các Dòng Có Các Giá Trị Cách Nhau Bằng Khoảng Trắng Lặp Lại

Ví dụ đầu vào:

3
10 20
30 40
50 60

Mã:

int n = Integer.parseInt(br.readLine());

for (int i = 0; i < n; i++) {
    StringTokenizer st = new StringTokenizer(br.readLine());
    int x = Integer.parseInt(st.nextToken());
    int y = Integer.parseInt(st.nextToken());
    System.out.println(x + y);
}

Mẫu này xuất hiện thường xuyên trong:

  • Các bài kiểm tra lập trình
  • Các bài toán thuật toán
  • Các công cụ xử lý dữ liệu

5.6 Xử lý IOException

Các phương thức của BufferedReader ném các ngoại lệ đã kiểm tra, vì vậy bạn phải xử lý chúng.

Cách tiếp cận đơn giản (học / thi đấu):

public static void main(String[] args) throws Exception {
    BufferedReader br = new BufferedReader(
        new InputStreamReader(System.in)
    );
}

Cách tiếp cận kiểu sản xuất:

try {
    BufferedReader br = new BufferedReader(
        new InputStreamReader(System.in)
    );
} catch (IOException e) {
    e.printStackTrace();
}

Đối với người mới bắt đầu, cách tiếp cận đầu tiên thường là đủ.

5.7 Tại sao BufferedReader nhanh

BufferedReader nhanh vì:

  1. Nó đọc dữ liệu theo các khối lớn (đệm)
  2. Tránh việc phân tích biểu thức chính quy
  3. Cho phép bạn kiểm soát cách dữ liệu được diễn giải

Tóm lại, nó tách biệt:

  • Đọc
  • Phân tích

Thiết kế này là điểm khác biệt chính so với Scanner.

5.8 Scanner vs BufferedReader Tóm tắt

FeatureScannerBufferedReader
Ease of useVery highMedium
SpeedLowHigh
Line-based inputWeakExcellent
Numeric parsingAutomaticManual
Real-world usageLimitedVery common

5.9 Những gì sẽ đến tiếp

Trong phần tiếp theo, chúng ta sẽ khám phá các kỹ thuật nhập nhanh được sử dụng trong lập trình thi đấu.

Bạn sẽ học:

  • Tại sao ngay cả BufferedReader cũng có thể quá chậm
  • Cách hoạt động của nhập nhanh về mặt khái niệm
  • Một triển khai FastScanner thực tế

6. Nhập nhanh cho lập trình thi đấu (FastScanner)

Trong lập trình thi đấu, kích thước đầu vào có thể lớn đến mức ngay cả BufferedReader cũng trở thành nút thắt.
Phần này giải thích tại sao cần nhập nhanh, cách nó hoạt động, và khi nào bạn nên (và không nên) sử dụng nó.

6.1 Tại sao Scanner gây lỗi thời gian giới hạn

Các đề bài lập trình thi đấu thường có:

  • Hàng trăm ngàn hoặc hàng triệu số
  • Giới hạn thời gian nghiêm ngặt (1–2 giây)
  • Môi trường thực thi hạn chế

Scanner thực hiện nhiều kiểm tra nội bộ:

  • Phân tích biểu thức chính quy
  • Chuyển đổi kiểu tự động
  • Kiểm tra toàn diện

Các tính năng này hữu ích cho độ an toàn—nhưng tốn kém về hiệu năng.
Do đó, Scanner thường gây ra TLE (Time Limit Exceeded) trong các cuộc thi.

6.2 Khi BufferedReader vẫn chưa đủ

BufferedReader nhanh hơn nhiều so với Scanner, nhưng nó vẫn:

  • Tạo các đối tượng String
  • Tách chuỗi thành các token
  • Phân tích số từ văn bản

Khi đầu vào cực kỳ lớn, những bước này một mình có thể quá chậm.

Điều này dẫn đến một cách tiếp cận khác:

Đọc byte thô và chuyển đổi số một cách thủ công

6.3 Cách hoạt động của nhập nhanh (về mặt khái niệm)

Các kỹ thuật nhập nhanh thường tuân theo quy trình này:

  1. Đọc một khối byte lớn một lần
  2. Lưu chúng vào bộ đệm byte
  3. Bỏ qua các ký tự không cần thiết (khoảng trắng, xuống dòng)
  4. Chuyển đổi các chữ số trực tiếp thành số
  5. Tránh tạo các đối tượng không cần thiết

Điều này giảm thiểu:

  • Phân bổ bộ nhớ
  • Thu gom rác
  • Chi phí CPU

6.4 Triển khai FastScanner thực tế

Dưới đây là một FastScanner tối thiểu và thực tế phù hợp cho các cuộc thi.

static class FastScanner {
    private final InputStream in = System.in;
    private final byte[] buffer = new byte[1 << 16];
    private int ptr = 0, len = 0;

    private int readByte() throws IOException {
        if (ptr >= len) {
            len = in.read(buffer);
            ptr = 0;
            if (len <= 0) return -1;
        }
        return buffer[ptr++];
    }

    int nextInt() throws IOException {
        int c;
        do {
            c = readByte();
        } while (c <= ' ');

        boolean negative = false;
        if (c == '-') {
            negative = true;
            c = readByte();
        }

        int value = 0;
        while (c > ' ') {
            value = value * 10 + (c - '0');
            c = readByte();
        }
        return negative ? -value : value;
    }
}

Ví dụ sử dụng:

FastScanner fs = new FastScanner();
int n = fs.nextInt();

Cách tiếp cận này cực kỳ nhanh cho đầu vào số lớn.

6.5 Những Hạn Chế Quan Trọng Của FastScanner

Đầu vào nhanh không phải là giải pháp phổ quát.

Nhược điểm:

  • Khó đọc và bảo trì
  • Khó gỡ lỗi
  • Không phù hợp cho đầu vào chứa nhiều văn bản
  • Quá mức cần thiết cho hầu hết các ứng dụng

Chỉ sử dụng FastScanner khi cần thiết, thường là trong các cuộc thi.

6.6 Tóm Tắt Phương Pháp Đầu Vào

  • Scanner → Học tập, đầu vào nhỏ
  • BufferedReader → Ứng dụng thực tế, đầu vào lớn
  • FastScanner → Chỉ dành cho lập trình thi đấu

Việc chọn công cụ đơn giản nhất đáp ứng nhu cầu hiệu suất của bạn luôn là tốt nhất.

6.7 Những Gì Sẽ Đến Tiếp Theo

Trong phần tiếp theo, chúng ta sẽ tóm tắt cách chọn phương pháp đầu vào phù hợp bằng một hướng dẫn quyết định đơn giản.

7. Cách Chọn Phương Pháp Đầu Vào Phù Hợp (Hướng Dẫn Quyết Định Nhanh)

Đến nay, bạn đã thấy nhiều cách xử lý đầu vào chuẩn trong Java.
Phần này giúp bạn quyết định nhanh chóng và tự tin phương pháp nào nên sử dụng trong các tình huống thực tế.

7.1 Dành Cho Học Tập Và Các Chương Trình Nhỏ

Khuyến nghị: Scanner

Tại sao Scanner hoạt động tốt ở đây

  • Dễ đọc và viết
  • Ít mã lặp lại
  • Phù hợp với hầu hết các hướng dẫn dành cho người mới bắt đầu

Các tình huống điển hình:

  • Học các kiến thức cơ bản về Java
  • Công cụ dòng lệnh nhỏ
  • Bài tập với kích thước đầu vào hạn chế
    Scanner sc = new Scanner(System.in);
    int n = sc.nextInt();
    

Nếu chương trình của bạn chỉ đọc một lượng đầu vào nhỏ, Scanner hoàn toàn ổn.

7.2 Dành Cho Đầu Vào Lớn Và Hiệu Suất Ổn Định

Khuyến nghị: BufferedReader

Tại sao BufferedReader là lựa chọn chuyên nghiệp mặc định

  • Nhanh và có thể dự đoán
  • Hoạt động tốt với đầu vào lớn
  • Dễ kiểm soát logic phân tích đầu vào

Các tình huống điển hình:

  • Ứng dụng thực tế
  • Công việc hàng loạt
  • Các bài toán lập trình với nhiều dòng đầu vào
    BufferedReader br = new BufferedReader(
        new InputStreamReader(System.in)
    );
    

Nếu bạn không chắc chắn và hiệu suất quan trọng, BufferedReader là lựa chọn an toàn nhất.

7.3 Dành Cho Lập Trình Thi Đấu Và Kích Thước Đầu Vào Cực Lớn

Khuyến nghị: FastScanner

Tại sao FastScanner tồn tại

  • Được thiết kế để tránh TLE
  • Xử lý đầu vào số lớn hiệu quả

Các tình huống điển hình:

  • Các cuộc thi lập trình thi đấu
  • Các bài toán với giới hạn thời gian rất nghiêm ngặt
  • Bộ dữ liệu cực lớn
    FastScanner fs = new FastScanner();
    int n = fs.nextInt();
    

Ngoài lập trình thi đấu, cách tiếp cận này thường không cần thiết.

7.4 Luồng Quyết Định Đơn Giản

Khi còn nghi ngờ, hãy theo logic này:

  1. Đây có phải là học tập hoặc kịch bản nhỏ không? → Sử dụng Scanner
  2. Đầu vào có lớn hoặc hiệu suất có quan trọng không? → Sử dụng BufferedReader
  3. Đây có phải là bài toán thi đấu với giới hạn thời gian nghiêm ngặt không? → Sử dụng FastScanner

Trong thực tế, hầu hết các chương trình Java rơi vào bước 2.

7.5 Những Hiểu Lầm Phổ Biến

“Phương pháp nhanh nhất luôn là tốt nhất”

Điều này là sai.

  • Đầu vào nhanh hơn làm giảm khả năng đọc
  • Mã phức tạp tăng rủi ro lỗi
  • Bảo trì trở nên khó khăn hơn

Always prefer the phương pháp đơn giản nhất đáp ứng yêu cầu của bạn.

“Scanner không bao giờ nên được sử dụng”

Cũng sai.

Scanner là một công cụ học tập tuyệt vời và hoàn toàn hợp lệ cho các nhiệm vụ nhỏ.

7.6 Phần Tiếp Theo

Trong phần tiếp theo, chúng ta sẽ xem xét các lỗi phổ biến và mẹo khắc phục sự cố liên quan đến đầu vào chuẩn của Java.

Bạn sẽ học:

  • Tại sao đầu vào hoạt động không mong đợi
  • Cách sửa các lỗi phân tích phổ biến
  • Cách chẩn đoán các vấn đề hiệu suất

8. Các Lỗi Phổ Biến và Khắc Phục Sự Cố

Ngay cả khi bạn hiểu các nguyên tắc cơ bản của đầu vào chuẩn Java, các sai lầm nhỏ có thể gây ra lỗi khó hiểu hoặc vấn đề hiệu suất.
Phần này thu thập các vấn đề phổ biến nhất, giải thích tại sao chúng xảy ra, và cho thấy cách sửa chúng.

8.1 Đầu Vào Có Vẻ Bị Bỏ Qua Hoặc Thiếu

Triệu Chứng

  • Một biến chuỗi trở nên rỗng
  • Đầu vào bị bỏ qua mà không chờ đầu vào người dùng

Nguyên Nhân Thường Gặp

Điều này thường xảy ra khi trộn nextInt() (hoặc các phương thức tương tự) với nextLine() trong Scanner.

int n = sc.nextInt();
String s = sc.nextLine(); // becomes empty

Giải Pháp

Tiêu thụ dòng mới còn lại trước khi gọi nextLine().

int n = sc.nextInt();
sc.nextLine(); // consume newline
String s = sc.nextLine();

Hoặc, sử dụng BufferedReader và xử lý phân tích thủ công.

8.2 Chương Trình Chờ Vô Tận Để Nhập

Triệu Chứng

  • Chương trình không kết thúc
  • Nộp bài cho thẩm phán trực tuyến không bao giờ hoàn thành

Nguyên Nhân Thường Gặp

  • Chương trình mong đợi nhiều đầu vào hơn được cung cấp
  • EOF (kết thúc đầu vào) không được xử lý đúng cách

Giải Pháp

Sử dụng các vòng lặp đầu vào nhận biết EOF.

String line;
while ((line = br.readLine()) != null) {
    // process line
}

Luôn kiểm tra kép thông số định dạng đầu vào.

8.3 NumberFormatException Khi Phân Tích Số

Triệu Chứng

  • Chương trình sập khi chuyển đổi chuỗi thành số
    int n = Integer.parseInt(line);
    

Nguyên Nhân Thường Gặp

  • Khoảng trắng đầu hoặc cuối
  • Dòng trống
  • Ký tự không mong đợi trong đầu vào

Giải Pháp

Làm sạch đầu vào trước khi phân tích.

line = line.trim();
int n = Integer.parseInt(line);

Cũng xác minh rằng định dạng đầu vào khớp với kỳ vọng của bạn.

8.4 split() Tạo Ra Kết Quả Không Mong Đợi

Triệu Chứng

  • Số lượng token sai
  • Chuỗi rỗng trong mảng kết quả

Nguyên Nhân Thường Gặp

Nhiều khoảng trắng giữa các giá trị.

String[] parts = line.split(" ");

Giải Pháp

Sử dụng biểu thức chính quy xử lý nhiều khoảng trắng.

String[] parts = line.trim().split("\\s+");

Điều này hoạt động cho một hoặc nhiều khoảng trắng, tab, hoặc ngắt dòng.

8.5 Chương Trình Quá Chậm (Vượt Giới Hạn Thời Gian)

Triệu Chứng

  • Chương trình hoạt động cục bộ nhưng thất bại trong cuộc thi
  • Thời gian thực thi vượt quá giới hạn

Nguyên Nhân Thường Gặp

  • Sử dụng Scanner cho đầu vào lớn
  • Sử dụng quá mức split()
  • Các cuộc gọi System.out.println() thường xuyên

Giải Pháp

  • Chuyển sang BufferedReader
  • Sử dụng StringTokenizer thay vì split
  • Xuất hàng loạt sử dụng StringBuilderPrintWriter

Thường thì, tốc độ đầu vào là nút thắt cổ chai thực sự, không phải thuật toán.

8.6 Nhầm Lẫn Về Các Ngoại Lệ Được Kiểm Tra (IOException)

Triệu Chứng

  • Lỗi biên dịch liên quan đến IOException
  • Không chắc chắn nơi thêm try-catch

Giải Pháp Đơn Giản (Học Tập / Cuộc Thi)

public static void main(String[] args) throws Exception {
    BufferedReader br = new BufferedReader(
        new InputStreamReader(System.in)
    );
}

Giải Pháp Sản Xuất

Sử dụng các khối try-catch đúng cách và xử lý lỗi một cách khéo léo.

8.7 Vấn Đề Mã Hóa Ký Tự

Triệu Chứng

  • Văn bản không phải tiếng Anh xuất hiện bị hỏng
  • Ký tự không mong đợi trong đầu vào

Nguyên Nhân

Không khớp giữa mã hóa đầu vào và mã hóa mặc định của Java.

Giải Pháp

Chỉ định mã hóa một cách rõ ràng.

BufferedReader br = new BufferedReader(
    new InputStreamReader(System.in, "UTF-8")
);

Điều này đặc biệt quan trọng khi đọc tệp hoặc đầu vào đa ngôn ngữ.

8.8 Danh Sách Kiểm Tra Xử Lý Nhanh

Khi đầu vào hoạt động không như mong đợi, hãy kiểm tra các mục sau:

  1. Mã của bạn có khớp chính xác với định dạng đầu vào không?
  2. Bạn có xử lý đúng các ký tự xuống dòng và khoảng trắng không?
  3. Bạn có kết hợp các phương thức của Scanner một cách an toàn không?
  4. Kích thước đầu vào có quá lớn đối với Scanner không?
  5. Bạn có xử lý EOF một cách đúng đắn không?

Việc kiểm tra có hệ thống các điểm này sẽ giải quyết hầu hết các vấn đề.

8.9 Những Gì Sắp Đến

Trong phần tiếp theo, chúng tôi sẽ trả lời các câu hỏi thường gặp (FAQ) về đầu vào chuẩn của Java.

Điều này sẽ giúp làm rõ những nghi ngờ còn lại và củng cố các thực hành tốt nhất.

9. Các Câu Hỏi Thường Gặp (FAQ)

Phần này trả lời những câu hỏi phổ biến nhất liên quan đến đầu vào chuẩn của Java, đặc biệt là những câu hỏi mà người mới bắt đầu và các nhà phát triển trung cấp thường đặt ra.

9.1 Cái Nào Tốt Hơn: Scanner hay BufferedReader?

Tùy vào mục đích của bạn.

  • Sử dụng Scanner nếu: wp:list /wp:list

    • Bạn đang học Java
    • Kích thước đầu vào nhỏ
    • Độ dễ đọc quan trọng hơn hiệu năng
    • Sử dụng BufferedReader nếu: wp:list /wp:list

    • Kích thước đầu vào lớn

    • Hiệu năng quan trọng
    • Bạn muốn hành vi dự đoán được

Nếu bạn không chắc, BufferedReader thường là lựa chọn an toàn hơn về lâu dài.

9.2 Liệu Scanner Có Thật Sự Chậm Không?

Có, đối với đầu vào lớn.

Scanner được thiết kế cho:

  • An toàn
  • Linh hoạt
  • Dễ sử dụng

Những tính năng này khiến nó chậm hơn khi xử lý một lượng lớn dữ liệu.
Đối với đầu vào nhỏ, sự khác biệt là không đáng kể.

9.3 Tại Sao nextLine() Trả Về Chuỗi Rỗng?

Điều này xảy ra khi nextLine() được gọi sau nextInt() hoặc các phương thức tương tự.

Lý do:

  • nextInt() không tiêu thụ ký tự xuống dòng
  • nextLine() đọc ký tự xuống dòng còn lại

Giải pháp:

sc.nextLine(); // consume newline

Hoặc tránh kết hợp các phương thức nhập dựa trên token và dựa trên dòng.

9.4 Nên Dùng split() Hay StringTokenizer?

  • Sử dụng split() khi: wp:list /wp:list

    • Đầu vào nhỏ
    • Độ dễ đọc quan trọng
    • Sử dụng StringTokenizer khi: wp:list /wp:list

    • Đầu vào lớn

    • Hiệu năng quan trọng

Trong lập trình thi đấu, StringTokenizer vẫn được sử dụng rộng rãi.

9.5 Làm Thế Nào Để Đọc Đầu Vào Cho Đến EOF?

Với BufferedReader:

String line;
while ((line = br.readLine()) != null) {
    // process input
}

Với Scanner:

while (sc.hasNext()) {
    // process input
}

Xử lý EOF là phổ biến trong nhập tệp và các hệ thống chấm điểm trực tuyến.

9.6 Tôi Có Thể Dùng FastScanner Trong Ứng Dụng Thực Tế Không?

Không được khuyến nghị.

FastScanner:

  • Khó đọc
  • Khó bảo trì
  • Chỉ được tối ưu cho các cuộc thi

Đối với các ứng dụng thực tế, BufferedReader là sự cân bằng tốt nhất giữa tốc độ và tính rõ ràng.

9.7 Tôi Luôn Phải Xử Lý Ngoại Lệ Không?

  • Đối với việc học và các cuộc thi: public static void main(String[] args) throws Exception là chấp nhận được.
  • Đối với mã sản xuất: wp:list /wp:list

    • Sử dụng try-catch thích hợp
    • Xử lý lỗi một cách rõ ràng

9.8 Đầu Vào Nhanh, Nhưng Đầu Ra Chậm. Tôi Nên Làm Gì?

Tối ưu hoá đầu ra cũng như đầu vào.

  • Tránh gọi System.out.println() quá thường xuyên
  • Sử dụng StringBuilder
  • Dùng PrintWriter cho đầu ra có bộ đệm

Hiệu năng nhập và xuất nên được tối ưu cùng nhau.

9.9 Những Gì Sắp Đến

Trong phần cuối, chúng tôi sẽ tóm tắt mọi thứ và nhắc lại các kết luận chính.

10. Tổng Kết Cuối Cùng

Trong bài viết này, chúng tôi đã khám phá đầu vào chuẩn của Java từ các khái niệm cơ bản đến các kỹ thuật nâng cao được sử dụng trong lập trình thi đấu.
Hãy kết thúc bằng cách xem lại những điểm quan trọng nhất.

10.1 Ba Bài Học Chính

  • Đầu vào nhỏ hoặc mục đích họcScanner
  • Đầu vào lớn hoặc ứng dụng thực tếBufferedReader
  • Lập trình thi đấu và kích thước đầu vào cực lớnFastScanner

Việc chọn công cụ phù hợp cho từng tình huống quan trọng hơn rất nhiều so với việc luôn dùng phương pháp nhanh nhất ở mọi nơi.

10.2 Cách Ngừng Bối Rối Khi Xử Lý Input

Hầu hết sự bối rối về input trong Java xuất phát từ việc không hiểu:

  • Tại sao có nhiều phương pháp input khác nhau
  • Những đánh đổi mà mỗi phương pháp mang lại
  • Khi nào hiệu năng thực sự quan trọng

Khi bạn xem việc xử lý input như một lựa chọn thiết kế, sự bối rối sẽ biến mất.

10.3 Lộ Trình Học Tập Được Đề Xuất Cho Người Mới Bắt Đầu

Nếu bạn mới học Java, hãy theo trình tự sau:

  1. Học cách nhập dữ liệu bằng Scanner
  2. Chuyển sang BufferedReader để có hiệu năng và kiểm soát tốt hơn
  3. Học các kỹ thuật nhập nhanh chỉ khi bạn tham gia lập trình thi đấu

Lộ trình này giúp bạn vừa tự tin vừa hình thành thói quen đúng đắn.

10.4 Mẹo Thực Tế Khi Áp Dụng Trong Dự Án Thực Tế

  • Luôn xác nhận định dạng của dữ liệu đầu vào
  • Cẩn thận với các ký tự xuống dòng và khoảng trắng
  • Tối ưu cả input và output đồng thời
  • Nghi ngờ tốc độ input nếu hiệu năng kém

Những thói quen đơn giản này ngăn ngừa phần lớn lỗi liên quan đến input.

10.5 Bước Tiếp Theo

Để nâng cao kỹ năng Java sau khi đã thành thạo input tiêu chuẩn, bạn có thể học thêm:

  • Tối ưu hoá output tiêu chuẩn (PrintWriter, StringBuilder)
  • Các nguyên tắc cơ bản của xử lý ngoại lệ
  • Các cấu trúc dữ liệu Collections (List, Map) kết hợp với input
  • Thiết kế input/output cho các thuật toán

Input tiêu chuẩn của Java có vẻ đơn giản, nhưng đó là một kỹ năng cốt lõi hỗ trợ mọi chương trình Java mà bạn viết.
Việc thành thạo nó sẽ giúp mã của bạn chạy nhanh hơn, sạch hơn và đáng tin cậy hơn.