bufferedreader: read a file line by line module (BufferedReader vs Scanner vs Files.lines vs readAllLines on a 1 GB file; heap at -Xmx64m, timings, read-call counts, one-huge-line OOM, charset/BOM/line-terminator pitfalls, Scanner swallowing IOException), 10 contract tests
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b3pDoGLiWk6LLBeHjW
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
package com.ankurm.bufferedreader;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Scanner;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
/**
|
||||
* The same file read five ways when its bytes are NOT valid UTF-8, and a UTF-8 BOM. Run it twice:
|
||||
* once normally and once with -Dfile.encoding=COMPAT to see the pre-JDK-18 default come back.
|
||||
*/
|
||||
public final class CharsetDemo {
|
||||
|
||||
public static void main(String[] a) throws Exception {
|
||||
Path dir = Files.createTempDirectory("charset");
|
||||
System.out.println("Charset.defaultCharset() = " + Charset.defaultCharset());
|
||||
System.out.println("native.encoding = " + System.getProperty("native.encoding"));
|
||||
System.out.println("file.encoding (property) = " + System.getProperty("file.encoding"));
|
||||
System.out.println();
|
||||
|
||||
Path latin1 = dir.resolve("latin1.txt");
|
||||
Files.write(latin1, "caf\u00e9\n".getBytes(StandardCharsets.ISO_8859_1));
|
||||
System.out.println("latin1.txt bytes: " + HexFormat.ofDelimiter(" ").formatHex(Files.readAllBytes(latin1)) + " (the text is \"caf\u00e9\" saved as ISO-8859-1)");
|
||||
run("new FileReader(path) ", () -> firstLine(new BufferedReader(new FileReader(latin1.toFile()))));
|
||||
run("new FileReader(path, UTF_8) ", () -> firstLine(new BufferedReader(new FileReader(latin1.toFile(), StandardCharsets.UTF_8))));
|
||||
run("Files.newBufferedReader(path) ", () -> firstLine(Files.newBufferedReader(latin1)));
|
||||
run("Files.readAllLines(path) ", () -> Files.readAllLines(latin1).toString());
|
||||
run("Files.lines(path).toList() ", () -> { try (var s = Files.lines(latin1)) { return s.toList().toString(); } });
|
||||
run("new Scanner(path).nextLine() ", () -> { try (Scanner sc = new Scanner(latin1)) { return Esc.of(sc.nextLine()); } });
|
||||
run("readAllLines(path, ISO_8859_1) <- fix ", () -> Files.readAllLines(latin1, StandardCharsets.ISO_8859_1).toString());
|
||||
System.out.println();
|
||||
|
||||
Path utf8 = dir.resolve("utf8.txt");
|
||||
Files.writeString(utf8, "caf\u00e9 \u20b9100\n", StandardCharsets.UTF_8);
|
||||
System.out.println("utf8.txt bytes: " + HexFormat.ofDelimiter(" ").formatHex(Files.readAllBytes(utf8)) + " (\"caf\u00e9 \u20b9100\" saved as UTF-8)");
|
||||
run("new FileReader(path) ", () -> firstLine(new BufferedReader(new FileReader(utf8.toFile()))));
|
||||
run("new FileReader(path, UTF_8) <- fix ", () -> firstLine(new BufferedReader(new FileReader(utf8.toFile(), StandardCharsets.UTF_8))));
|
||||
System.out.println();
|
||||
|
||||
Path bom = dir.resolve("bom.csv");
|
||||
Files.write(bom, concat(new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}, "id,name\n1,Asha\n".getBytes(StandardCharsets.UTF_8)));
|
||||
System.out.println("bom.csv bytes: " + HexFormat.ofDelimiter(" ").formatHex(Files.readAllBytes(bom)).substring(0, 41) + " ...");
|
||||
try (BufferedReader br = Files.newBufferedReader(bom)) {
|
||||
String header = br.readLine();
|
||||
System.out.println("header line = " + Esc.of(header) + " length=" + header.length() + " (the text is 7 characters)");
|
||||
System.out.println("header.equals(\"id,name\") = " + header.equals("id,name"));
|
||||
String cleaned = header.startsWith("\uFEFF") ? header.substring(1) : header;
|
||||
System.out.println("after stripping U+FEFF = " + Esc.of(cleaned) + " equals=" + cleaned.equals("id,name"));
|
||||
}
|
||||
}
|
||||
|
||||
static byte[] concat(byte[] x, byte[] y) {
|
||||
byte[] r = new byte[x.length + y.length];
|
||||
System.arraycopy(x, 0, r, 0, x.length);
|
||||
System.arraycopy(y, 0, r, x.length, y.length);
|
||||
return r;
|
||||
}
|
||||
|
||||
static String firstLine(BufferedReader br) throws IOException {
|
||||
try (br) { return Esc.of(br.readLine()); }
|
||||
}
|
||||
|
||||
static void run(String label, Callable<String> c) {
|
||||
String result;
|
||||
try { result = c.call(); }
|
||||
catch (Exception e) {
|
||||
result = e.getClass().getSimpleName() + ": " + e.getMessage();
|
||||
if (e.getCause() != null) result += " [cause: " + e.getCause().getClass().getSimpleName() + ": " + e.getCause().getMessage() + "]";
|
||||
}
|
||||
System.out.println(label + " -> " + result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.ankurm.bufferedreader;
|
||||
|
||||
/** Makes non-ASCII and control characters visible so transcripts show what is really in the String. */
|
||||
final class Esc {
|
||||
private Esc() {}
|
||||
static String of(String s) {
|
||||
if (s == null) return "null";
|
||||
StringBuilder sb = new StringBuilder("\"");
|
||||
for (char c : s.toCharArray()) {
|
||||
if (c == '\n') sb.append("\\n");
|
||||
else if (c == '\r') sb.append("\\r");
|
||||
else if (c >= 32 && c < 127) sb.append(c);
|
||||
else sb.append(String.format("\\u%04X", (int) c));
|
||||
}
|
||||
return sb.append('"').toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.ankurm.bufferedreader;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileReader;
|
||||
import java.lang.management.GarbageCollectorMXBean;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Reads one file line by line with one of four approaches and prints lines, characters, wall time and GC cost.
|
||||
* Every approach does identical work (count lines, sum their lengths) so the totals must match.
|
||||
* Run each approach in its own fresh JVM: {@code java -Xmx64m LineCount reader big.log}.
|
||||
*/
|
||||
public final class LineCount {
|
||||
|
||||
public static void main(String[] a) throws Exception {
|
||||
String approach = a[0];
|
||||
Path p = Path.of(a[1]);
|
||||
long t0 = System.nanoTime();
|
||||
long[] r;
|
||||
try {
|
||||
r = switch (approach) {
|
||||
case "reader" -> reader(p, 8192);
|
||||
case "reader64k" -> reader(p, 65536);
|
||||
case "scanner" -> scanner(p);
|
||||
case "lines" -> lines(p);
|
||||
case "readAllLines" -> readAllLines(p);
|
||||
default -> throw new IllegalArgumentException("unknown approach " + approach);
|
||||
};
|
||||
} catch (OutOfMemoryError e) {
|
||||
System.out.printf("%-13s OutOfMemoryError: %s (-Xmx%d MB)%n", approach, e.getMessage(), Runtime.getRuntime().maxMemory() >> 20);
|
||||
return;
|
||||
}
|
||||
long ms = (System.nanoTime() - t0) / 1_000_000;
|
||||
long gcCount = 0, gcMs = 0;
|
||||
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
|
||||
gcCount += gc.getCollectionCount();
|
||||
gcMs += gc.getCollectionTime();
|
||||
}
|
||||
System.out.printf("%-13s lines=%,d chars=%,d time=%,d ms gc=%d collections / %d ms (-Xmx%d MB)%n",
|
||||
approach, r[0], r[1], ms, gcCount, gcMs, Runtime.getRuntime().maxMemory() >> 20);
|
||||
}
|
||||
|
||||
/** BufferedReader.readLine() in try-with-resources, charset stated explicitly. */
|
||||
static long[] reader(Path p, int bufferChars) throws Exception {
|
||||
long n = 0, chars = 0;
|
||||
try (BufferedReader br = new BufferedReader(new FileReader(p.toFile(), StandardCharsets.UTF_8), bufferChars)) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) { n++; chars += line.length(); }
|
||||
}
|
||||
return new long[]{n, chars};
|
||||
}
|
||||
|
||||
/** Scanner.nextLine(). */
|
||||
static long[] scanner(Path p) throws Exception {
|
||||
long n = 0, chars = 0;
|
||||
try (Scanner sc = new Scanner(p, StandardCharsets.UTF_8)) {
|
||||
while (sc.hasNextLine()) { chars += sc.nextLine().length(); n++; }
|
||||
}
|
||||
return new long[]{n, chars};
|
||||
}
|
||||
|
||||
/** Files.lines(): lazy Stream, must be closed. */
|
||||
static long[] lines(Path p) throws Exception {
|
||||
try (Stream<String> s = Files.lines(p, StandardCharsets.UTF_8)) {
|
||||
long[] acc = new long[2];
|
||||
s.forEach(l -> { acc[0]++; acc[1] += l.length(); });
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
|
||||
/** Files.readAllLines(): the whole file as a List before you see line one. */
|
||||
static long[] readAllLines(Path p) throws Exception {
|
||||
List<String> all = Files.readAllLines(p, StandardCharsets.UTF_8);
|
||||
long chars = 0;
|
||||
for (String l : all) chars += l.length();
|
||||
return new long[]{all.size(), chars};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.ankurm.bufferedreader;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
/** What counts as "a line"? Terminators, a final newline, empty files, and what readLine() cannot tell you. */
|
||||
public final class LineEndingsDemo {
|
||||
|
||||
public static void main(String[] a) throws IOException {
|
||||
System.out.printf("%-16s | %-34s | %-34s | %s%n", "input", "BufferedReader.readLine()", "Scanner.nextLine()", "split(\"\\n\").length");
|
||||
System.out.println("-".repeat(104));
|
||||
String[] inputs = {"a\nb", "a\nb\n", "a\nb\n\n", "a\r\nb", "a\rb", "", "\n", "a\n\rb"};
|
||||
for (String s : inputs) {
|
||||
System.out.printf("%-16s | %-34s | %-34s | %d%n", Esc.of(s), brLines(s), scLines(s), s.split("\n").length);
|
||||
}
|
||||
}
|
||||
|
||||
static List<String> brLines(String s) throws IOException {
|
||||
List<String> out = new ArrayList<>();
|
||||
try (BufferedReader br = new BufferedReader(new StringReader(s))) {
|
||||
String l;
|
||||
while ((l = br.readLine()) != null) out.add(Esc.of(l));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static List<String> scLines(String s) {
|
||||
List<String> out = new ArrayList<>();
|
||||
try (Scanner sc = new Scanner(new StringReader(s))) {
|
||||
while (sc.hasNextLine()) out.add(Esc.of(sc.nextLine()));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.ankurm.bufferedreader;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* readLine() has no maximum length. One giant line (minified JSON, a log with a stack trace flattened onto one line)
|
||||
* is read into a single String. Two modes: {@code line} (the naive loop) and {@code chunks} (read(char[]) in 8 KB pieces).
|
||||
*/
|
||||
public final class LongLineDemo {
|
||||
|
||||
public static void main(String[] a) throws Exception {
|
||||
String mode = a[0];
|
||||
Path p = Path.of(a[1]);
|
||||
System.out.printf("mode=%s -Xmx%d MB%n", mode, Runtime.getRuntime().maxMemory() >> 20);
|
||||
try (BufferedReader br = new BufferedReader(new FileReader(p.toFile(), StandardCharsets.UTF_8))) {
|
||||
if (mode.equals("line")) {
|
||||
String line = br.readLine();
|
||||
System.out.printf("readLine() returned %,d chars%n", line == null ? -1 : line.length());
|
||||
} else {
|
||||
char[] buf = new char[8192];
|
||||
long total = 0, commas = 0;
|
||||
int n;
|
||||
while ((n = br.read(buf, 0, buf.length)) != -1) {
|
||||
total += n;
|
||||
for (int i = 0; i < n; i++) if (buf[i] == ',') commas++;
|
||||
}
|
||||
System.out.printf("read(char[]) processed %,d chars, %,d commas, never holding more than %d chars%n", total, commas, buf.length);
|
||||
}
|
||||
} catch (OutOfMemoryError e) {
|
||||
System.out.println("OutOfMemoryError: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.ankurm.bufferedreader;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* What does BufferedReader actually save? Counts the read calls that reach the underlying InputStream, then times
|
||||
* three ways of consuming the same file. Corrects a common claim: an unbuffered InputStreamReader/FileReader does NOT
|
||||
* make one operating-system read per character, because its decoder already reads 8 KB of bytes at a time.
|
||||
*/
|
||||
public final class ReadCalls {
|
||||
|
||||
/** Counts calls and bytes that reach the stream under the reader. */
|
||||
static final class CountingStream extends FilterInputStream {
|
||||
long calls, bytes;
|
||||
CountingStream(InputStream in) { super(in); }
|
||||
@Override public int read() throws IOException { int b = super.read(); calls++; if (b >= 0) bytes++; return b; }
|
||||
@Override public int read(byte[] b, int off, int len) throws IOException {
|
||||
int n = super.read(b, off, len); calls++; if (n > 0) bytes += n; return n;
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] a) throws Exception {
|
||||
Path p = Path.of(a[0]);
|
||||
System.out.println("== 1. read() calls that reach the underlying InputStream (first 50 MB of the file) ==");
|
||||
count("InputStreamReader.read() per char", p, false, 0, 'c');
|
||||
count("BufferedReader(8192).read() per char", p, true, 8192, 'c');
|
||||
count("BufferedReader(8192).readLine()", p, true, 8192, 'l');
|
||||
count("BufferedReader(65536).readLine()", p, true, 65536, 'l');
|
||||
count("BufferedReader(1).readLine()", p, true, 1, 'l');
|
||||
System.out.println();
|
||||
System.out.println("== 2. time to consume the whole file (3 passes each; indicative, same JVM) ==");
|
||||
for (int pass = 1; pass <= 3; pass++) {
|
||||
System.out.printf("pass %d InputStreamReader.read() per char %,6d ms%n", pass, time(p, false, 0, 'c'));
|
||||
System.out.printf("pass %d BufferedReader.read() per char %,6d ms%n", pass, time(p, true, 8192, 'c'));
|
||||
System.out.printf("pass %d BufferedReader.readLine() %,6d ms%n", pass, time(p, true, 8192, 'l'));
|
||||
}
|
||||
}
|
||||
|
||||
static final long LIMIT = 50_000_000L;
|
||||
|
||||
static void count(String label, Path p, boolean buffered, int size, char mode) throws Exception {
|
||||
CountingStream cs = new CountingStream(new FileInputStream(p.toFile()));
|
||||
Reader base = new InputStreamReader(cs, StandardCharsets.UTF_8);
|
||||
Reader r = buffered ? new BufferedReader(base, size) : base;
|
||||
long chars = 0;
|
||||
try (r) {
|
||||
if (mode == 'c') { int c; while (chars < LIMIT && (c = r.read()) != -1) chars++; }
|
||||
else { BufferedReader br = (BufferedReader) r; String l; while (chars < LIMIT && (l = br.readLine()) != null) chars += l.length() + 1; }
|
||||
}
|
||||
System.out.printf("%-38s underlying calls=%,9d avg bytes/call=%,8.1f%n", label, cs.calls, (double) cs.bytes / cs.calls);
|
||||
}
|
||||
|
||||
static long time(Path p, boolean buffered, int size, char mode) throws Exception {
|
||||
long t0 = System.nanoTime(), x = 0;
|
||||
Reader base = new InputStreamReader(new FileInputStream(p.toFile()), StandardCharsets.UTF_8);
|
||||
Reader r = buffered ? new BufferedReader(base, size) : base;
|
||||
try (r) {
|
||||
if (mode == 'c') { int c; while ((c = r.read()) != -1) x += c; }
|
||||
else { BufferedReader br = (BufferedReader) r; String l; while ((l = br.readLine()) != null) x += l.length(); }
|
||||
}
|
||||
if (x == 42) System.out.print(""); // keep the loop alive
|
||||
return (System.nanoTime() - t0) / 1_000_000;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.ankurm.bufferedreader;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
* A reader that serves two lines and then fails, as a dying disk or a dropped network share would.
|
||||
* BufferedReader tells you; Scanner looks exactly like a clean end of file.
|
||||
*/
|
||||
public final class ScannerSwallowsDemo {
|
||||
|
||||
/** Delivers "line one\nline two\n" in one read, then throws on the next. */
|
||||
static final class FailingReader extends Reader {
|
||||
private boolean first = true;
|
||||
@Override public int read(char[] buf, int off, int len) throws IOException {
|
||||
if (!first) throw new IOException("simulated read failure (disk gone)");
|
||||
first = false;
|
||||
String s = "line one\nline two\n";
|
||||
s.getChars(0, s.length(), buf, off);
|
||||
return s.length();
|
||||
}
|
||||
@Override public void close() {}
|
||||
}
|
||||
|
||||
public static void main(String[] a) {
|
||||
System.out.println("-- BufferedReader --");
|
||||
int n = 0;
|
||||
try (BufferedReader br = new BufferedReader(new FailingReader())) {
|
||||
String l;
|
||||
while ((l = br.readLine()) != null) { n++; System.out.println("got: " + l); }
|
||||
System.out.println("loop ended normally after " + n + " lines");
|
||||
} catch (IOException e) {
|
||||
System.out.println("IOException after " + n + " lines: " + e.getMessage());
|
||||
}
|
||||
|
||||
System.out.println("-- Scanner --");
|
||||
n = 0;
|
||||
try (Scanner sc = new Scanner(new FailingReader())) {
|
||||
while (sc.hasNextLine()) { n++; System.out.println("got: " + sc.nextLine()); }
|
||||
System.out.println("loop ended normally after " + n + " lines");
|
||||
System.out.println("sc.ioException() = " + sc.ioException());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.ankurm.bufferedreader;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Deterministic generator for the synthetic files used by the benchmarks.
|
||||
*
|
||||
* <pre>
|
||||
* java TestData lines big.log 1000000000 # ~1 GB of ~100-byte ASCII log lines
|
||||
* java TestData oneline long.txt 200000000 # 200 MB with NO newline at all
|
||||
* </pre>
|
||||
*/
|
||||
public final class TestData {
|
||||
public static void main(String[] a) throws IOException {
|
||||
String kind = a[0];
|
||||
Path p = Path.of(a[1]);
|
||||
long bytes = Long.parseLong(a[2]);
|
||||
long written = 0;
|
||||
long t0 = System.nanoTime();
|
||||
try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(p), 1 << 16)) {
|
||||
if (kind.equals("lines")) {
|
||||
long i = 0;
|
||||
while (written < bytes) {
|
||||
String line = "2026-01-01T00:00:%02d.%03dZ INFO [worker-%02d] request id=%09d path=/api/v1/items/%d status=%d ms=%d\n"
|
||||
.formatted(i % 60, i % 1000, i % 32, i, i % 977, i % 7 == 0 ? 500 : 200, i % 113);
|
||||
byte[] b = line.getBytes(StandardCharsets.US_ASCII);
|
||||
out.write(b);
|
||||
written += b.length;
|
||||
i++;
|
||||
}
|
||||
} else if (kind.equals("oneline")) {
|
||||
byte[] chunk = "{\"k\":\"vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\"},".getBytes(StandardCharsets.US_ASCII);
|
||||
while (written < bytes) { out.write(chunk); written += chunk.length; }
|
||||
} else throw new IllegalArgumentException(kind);
|
||||
}
|
||||
System.out.printf("generated %s: %,d bytes in %d ms%n", p.getFileName(), Files.size(p), (System.nanoTime() - t0) / 1_000_000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.ankurm.bufferedreader;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.MalformedInputException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
/** Every behaviour the post states about line reading, pinned as an assertion. */
|
||||
class ReadLineContractTest {
|
||||
|
||||
@TempDir Path dir;
|
||||
|
||||
static List<String> br(String s) throws IOException {
|
||||
List<String> out = new ArrayList<>();
|
||||
try (BufferedReader r = new BufferedReader(new StringReader(s))) { String l; while ((l = r.readLine()) != null) out.add(l); }
|
||||
return out;
|
||||
}
|
||||
|
||||
@Test void readLineAcceptsAllThreeTerminators() throws IOException {
|
||||
assertEquals(List.of("a", "b"), br("a\nb"));
|
||||
assertEquals(List.of("a", "b"), br("a\r\nb"));
|
||||
assertEquals(List.of("a", "b"), br("a\rb"));
|
||||
}
|
||||
|
||||
@Test void finalNewlineDoesNotCreateAnEmptyLastLine() throws IOException {
|
||||
assertEquals(List.of("a", "b"), br("a\nb\n"));
|
||||
assertEquals(List.of("a", "b", ""), br("a\nb\n\n"));
|
||||
}
|
||||
|
||||
@Test void emptyFileHasZeroLinesAndLoneNewlineHasOne() throws IOException {
|
||||
assertEquals(List.of(), br(""));
|
||||
assertEquals(List.of(""), br("\n"));
|
||||
}
|
||||
|
||||
@Test void readLineCannotTellWhetherTheLastLineHadATerminator() throws IOException {
|
||||
assertEquals(br("a\nb"), br("a\nb\n"));
|
||||
}
|
||||
|
||||
@Test void allFourApproachesAgreeOnAWellFormedFile() throws Exception {
|
||||
Path p = dir.resolve("f.txt");
|
||||
Files.writeString(p, "one\ntwo\r\nthree\n\nfive");
|
||||
List<String> viaReader = new ArrayList<>();
|
||||
try (BufferedReader r = Files.newBufferedReader(p)) { String l; while ((l = r.readLine()) != null) viaReader.add(l); }
|
||||
List<String> viaScanner = new ArrayList<>();
|
||||
try (Scanner sc = new Scanner(p)) { while (sc.hasNextLine()) viaScanner.add(sc.nextLine()); }
|
||||
List<String> viaLines;
|
||||
try (Stream<String> s = Files.lines(p)) { viaLines = s.toList(); }
|
||||
List<String> viaAll = Files.readAllLines(p);
|
||||
assertEquals(List.of("one", "two", "three", "", "five"), viaReader);
|
||||
assertEquals(viaReader, viaScanner);
|
||||
assertEquals(viaReader, viaLines);
|
||||
assertEquals(viaReader, viaAll);
|
||||
}
|
||||
|
||||
@Test void invalidUtf8IsStrictInFilesApiAndSilentlyReplacedInFileReader() throws Exception {
|
||||
Path p = dir.resolve("latin1.txt");
|
||||
Files.write(p, "caf\u00e9\n".getBytes(StandardCharsets.ISO_8859_1));
|
||||
assertThrows(MalformedInputException.class, () -> Files.readAllLines(p));
|
||||
UncheckedIOException u = assertThrows(UncheckedIOException.class, () -> { try (Stream<String> s = Files.lines(p)) { s.toList(); } });
|
||||
assertInstanceOf(MalformedInputException.class, u.getCause());
|
||||
try (BufferedReader r = new BufferedReader(new FileReader(p.toFile(), StandardCharsets.UTF_8))) {
|
||||
assertEquals("caf\uFFFD", r.readLine());
|
||||
}
|
||||
assertEquals(List.of("caf\u00e9"), Files.readAllLines(p, StandardCharsets.ISO_8859_1));
|
||||
}
|
||||
|
||||
@Test void utf8BomSurvivesAsTheFirstCharacter() throws Exception {
|
||||
Path p = dir.resolve("bom.csv");
|
||||
Files.write(p, new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF, 'i', 'd', '\n'});
|
||||
try (BufferedReader r = Files.newBufferedReader(p)) {
|
||||
String first = r.readLine();
|
||||
assertEquals("\uFEFFid", first);
|
||||
assertNotEquals("id", first);
|
||||
}
|
||||
}
|
||||
|
||||
@Test void scannerHidesIoExceptionButBufferedReaderThrows() {
|
||||
assertThrows(IOException.class, () -> {
|
||||
try (BufferedReader r = new BufferedReader(new ScannerSwallowsDemo.FailingReader())) { while (r.readLine() != null) { } }
|
||||
});
|
||||
Scanner sc = new Scanner(new ScannerSwallowsDemo.FailingReader());
|
||||
int n = 0;
|
||||
while (sc.hasNextLine()) { sc.nextLine(); n++; }
|
||||
assertEquals(2, n);
|
||||
assertNotNull(sc.ioException());
|
||||
}
|
||||
|
||||
@Test void readLineReturnsAStringAsLongAsTheLine() throws Exception {
|
||||
Path p = dir.resolve("long.txt");
|
||||
Files.writeString(p, "x".repeat(1_000_000));
|
||||
try (BufferedReader r = Files.newBufferedReader(p)) { assertEquals(1_000_000, r.readLine().length()); }
|
||||
}
|
||||
|
||||
@Test void unclosedFilesLinesStillReadsButClosingIsYourJob() throws Exception {
|
||||
Path p = dir.resolve("f.txt");
|
||||
Files.writeString(p, "a\nb\n");
|
||||
try (Stream<String> s = Files.lines(p)) { assertEquals(2, s.count()); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user