Java foundations · Lesson 39 About 85 min with practice
Java Read and Write Text Files
Save a small document as UTF-8, read back the exact text, and choose deliberately whether a write replaces or appends. Learn what happens to line endings, missing files, and existing contents before building a verified report exercise.
Explain default writeString behavior and deliberate open options.
Append without assuming Java inserts a separator.
Reject accidental replacement with CREATE_NEW.
Choose whole-text versus line-oriented reading deliberately.
Verify saved content and distinguish missing files from valid empty text.
Text needs an agreed encoding on both sides
A file contains bytes, while a Java String represents text using UTF-16 code units. Encoding turns that text into bytes; decoding turns bytes back into text. Both sides need the same agreement if café, Arabic text, or other non-ASCII characters are to survive the round trip.
The examples pass StandardCharsets.UTF_8 to writeString and readString explicitly. Their convenience overloads also use UTF-8, but stating the charset makes the contract visible and avoids incorrectly assuming that every unrelated Java text API uses the same default.
These are real file operations on a fresh disposable report in the isolated Java container. They do not read your project files, save platform progress, or persist between runs. If you copy the examples to a local JDK, retain the temporary target rather than substituting valuable documents.
JAVA 21 COMPILER
Main.java · Save and reload exactly the same Unicode text
Predict the result, edit Main.java, then run your program.
Select Run code to compile and execute this program.
Expected output · predict encoding, write policy, and exact saved text
Topic: Java
Note: café
Exact match: true
Read and write a small document without managing a stream yourself
Files.writeString writes the supplied characters using the chosen encoding, and Files.readString returns the complete decoded contents. These convenience methods manage their opened resource internally, so the call does not leave a reader or writer for the caller to close.
OwnedReport is a different lifetime helper: it creates one temporary file and removes that exact owned path when its scope closes. It does not make every Path a closeable stream. The try-with-resources here is about deleting the disposable workspace file, not closing a Files.readString result.
Whole-file operations are suitable for the small reports in this lesson. They load or encode the document in memory and should not be used blindly for huge or untrusted-size inputs. Buffered streaming and explicit readers and writers follow in the next lesson.
A default write can discard the previous document
With no open options supplied, writeString behaves as a create-if-needed write that truncates an existing file. The second write in the example replaces the longer report with Short and a newline; old trailing bytes do not remain.
That behavior is useful only when replacement is the intended policy. It is not an append, backup, version history, or atomic safe-save transaction. If writing fails after opening or truncating, the previous contents may already be lost or the result may be partial.
Do not write a valuable target just to test the default. Our demonstration replaces only a newly owned disposable file. A production editor may need a temporary sibling, a deliberate move/replace strategy, and a documented atomicity and durability policy rather than assuming writeString supplies them.
JAVA 21 COMPILER
Main.java · A shorter default write replaces the old contents
Predict the result, edit Main.java, then run your program.
Select Run code to compile and execute this program.
Expected output · predict encoding, write policy, and exact saved text
Short
Bytes: 6
Why does the longer report disappear?
The first write stores the longer text in an owned temporary file.
The second write uses default options and truncates that target.
The shorter text becomes the complete new contents.
Reloading returns only Short and its supplied newline, not the previous suffix.
Truncation is intentional replacement, not a backup or safe-merge operation.
Append preserves existing bytes, not your intended record boundaries
APPEND directs the write to the end of the existing file. It does not insert a newline or turn each call into a separate record. Appending Second to First produces FirstSecond until the program explicitly supplies a separator.
Our ReportStore.append uses WRITE and APPEND without CREATE, so a missing report is an error under this contract. CREATE plus APPEND would be a different policy that permits starting a new file. Supplying explicit options means choosing the needed options rather than adding them to an imagined complete set of defaults.
Append is not a universal concurrency guarantee. Whether end-of-file positioning and writing are atomic with other writers depends on the filesystem and operation, and a logical record can still need coordination. This lesson uses one process and sequential calls, not a production concurrent log service.
JAVA 21 COMPILER
Main.java · Append deliberately and supply the newline yourself
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;importjava.nio.charset.StandardCharsets;importjava.nio.file.*;publicclassMain{publicstaticvoidmain(String[] args)throwsIOException{try(OwnedReport report =newOwnedReport()){ReportStore.replace(report.file(),"First");ReportStore.append(report.file(),"Second");System.out.println(ReportStore.read(report.file()));ReportStore.append(report.file(),"\nThird\n");System.out.print(ReportStore.read(report.file()));}}}classOwnedReportimplementsAutoCloseable{privatefinalPath file =Files.createTempFile("sovrancode-text-",".txt");publicOwnedReport()throwsIOException{}publicPathfile(){return file;}@Overridepublicvoidclose()throwsIOException{Files.deleteIfExists(file);}}classReportStore{privatestaticvoidrequirePath(Path file){if(file ==null)thrownewIllegalArgumentException("File path must not be null.");}privatestaticvoidrequireText(String text){if(text ==null)thrownewIllegalArgumentException("Text must not be null.");}publicstaticvoidreplace(Path file,String text)throwsIOException{requirePath(file);requireText(text);Files.writeString(file, text,StandardCharsets.UTF_8,StandardOpenOption.WRITE,StandardOpenOption.TRUNCATE_EXISTING);}publicstaticvoidappend(Path file,String text)throwsIOException{requirePath(file);requireText(text);Files.writeString(file, text,StandardCharsets.UTF_8,StandardOpenOption.WRITE,StandardOpenOption.APPEND);}publicstaticStringread(Path file)throwsIOException{requirePath(file);returnFiles.readString(file,StandardCharsets.UTF_8);}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · predict encoding, write policy, and exact saved text
FirstSecond
FirstSecond
Third
Use CREATE_NEW when existing content must not be replaced
CREATE_NEW requests a new entry and fails when the target already exists. The worked example attempts it against the already-owned temporary report, catches FileAlreadyExistsException, and reads the original content back to demonstrate that the rejected creation did not truncate it.
Do not implement this policy by first checking exists and then doing a default replacing write. Another actor can change the entry between those calls. The create-new operation makes the existence-and-creation decision together, although later content writing can still fail and leave a partial new file.
The temporary file returned by createTempFile already exists. Therefore using CREATE_NEW directly on that same path should reject, exactly as the example shows. To create a genuinely new named document, choose an absent name inside a directory you control and manage ownership and cleanup deliberately.
JAVA 21 COMPILER
Main.java · Refuse to replace an existing file
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;importjava.nio.charset.StandardCharsets;importjava.nio.file.*;publicclassMain{publicstaticvoidmain(String[] args)throwsIOException{try(OwnedReport report =newOwnedReport()){ReportStore.replace(report.file(),"Original\n");try{Files.writeString(report.file(),"Replacement\n",StandardCharsets.UTF_8,StandardOpenOption.CREATE_NEW,StandardOpenOption.WRITE);}catch(FileAlreadyExistsException error){System.out.println("Existing file preserved.");}System.out.print(ReportStore.read(report.file()));}}}classOwnedReportimplementsAutoCloseable{privatefinalPath file =Files.createTempFile("sovrancode-text-",".txt");publicOwnedReport()throwsIOException{}publicPathfile(){return file;}@Overridepublicvoidclose()throwsIOException{Files.deleteIfExists(file);}}classReportStore{privatestaticvoidrequirePath(Path file){if(file ==null)thrownewIllegalArgumentException("File path must not be null.");}privatestaticvoidrequireText(String text){if(text ==null)thrownewIllegalArgumentException("Text must not be null.");}publicstaticvoidreplace(Path file,String text)throwsIOException{requirePath(file);requireText(text);Files.writeString(file, text,StandardCharsets.UTF_8,StandardOpenOption.WRITE,StandardOpenOption.TRUNCATE_EXISTING);}publicstaticvoidappend(Path file,String text)throwsIOException{requirePath(file);requireText(text);Files.writeString(file, text,StandardCharsets.UTF_8,StandardOpenOption.WRITE,StandardOpenOption.APPEND);}publicstaticStringread(Path file)throwsIOException{requirePath(file);returnFiles.readString(file,StandardCharsets.UTF_8);}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · predict encoding, write policy, and exact saved text
Existing file preserved.
Original
Treat missing, empty, and unreadable as different outcomes
An existing zero-byte report can validly decode to the empty String. A missing target instead causes the read operation to fail. Catching every IOException and returning an empty String would erase that distinction and make failed storage look like a genuine empty document.
NoSuchFileException is the specific missing-target response demonstrated by the isolated filesystem. Other access or decoding problems need their own meaningful handling. The examples declare IOException to keep unexpected failures visible rather than printing a misleading success result.
An exists check before read does not guarantee that read will succeed or that the file will remain unchanged. Perform the intended operation and handle its outcome. Never recreate an absent document as a side effect of a read unless that behavior is explicitly part of the application's contract.
JAVA 21 COMPILER
Main.java · Missing text is not a valid empty document
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;importjava.nio.charset.StandardCharsets;importjava.nio.file.*;publicclassMain{publicstaticvoidmain(String[] args)throwsIOException{try(OwnedReport report =newOwnedReport()){Files.delete(report.file());try{ReportStore.read(report.file());}catch(NoSuchFileException error){System.out.println("Report is missing, not empty.");}System.out.println("Still absent: "+Files.notExists(report.file()));}}}classOwnedReportimplementsAutoCloseable{privatefinalPath file =Files.createTempFile("sovrancode-text-",".txt");publicOwnedReport()throwsIOException{}publicPathfile(){return file;}@Overridepublicvoidclose()throwsIOException{Files.deleteIfExists(file);}}classReportStore{privatestaticvoidrequirePath(Path file){if(file ==null)thrownewIllegalArgumentException("File path must not be null.");}privatestaticvoidrequireText(String text){if(text ==null)thrownewIllegalArgumentException("Text must not be null.");}publicstaticvoidreplace(Path file,String text)throwsIOException{requirePath(file);requireText(text);Files.writeString(file, text,StandardCharsets.UTF_8,StandardOpenOption.WRITE,StandardOpenOption.TRUNCATE_EXISTING);}publicstaticvoidappend(Path file,String text)throwsIOException{requirePath(file);requireText(text);Files.writeString(file, text,StandardCharsets.UTF_8,StandardOpenOption.WRITE,StandardOpenOption.APPEND);}publicstaticStringread(Path file)throwsIOException{requirePath(file);returnFiles.readString(file,StandardCharsets.UTF_8);}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · predict encoding, write policy, and exact saved text
Report is missing, not empty.
Still absent: true
Choose whole-text preservation or line-oriented interpretation
readString preserves the decoded characters, including carriage returns and line feeds. readAllLines returns line text without those separators. The example's blank middle line remains an empty String entry, while the trailing terminator does not become an extra empty final line.
The enhanced-for loop can visit the line values without requiring a full collections lesson yet. readAllLines still loads the content into memory; it is not a streaming alternative for arbitrarily large files. Line-oriented processing is a choice about meaning, not just a different spelling of readString.
Our report format uses explicit newline characters for reproducible saved text. System.lineSeparator is another deliberate choice when a local platform's convention is required. Do not strip or normalize loaded text before an exact round-trip check, because that could hide a separator or whitespace defect.
JAVA 21 COMPILER
Main.java · Line-oriented reading does not preserve separators
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;importjava.nio.charset.StandardCharsets;importjava.nio.file.*;publicclassMain{publicstaticvoidmain(String[] args)throwsIOException{try(OwnedReport report =newOwnedReport()){String original ="Alpha\r\n\r\nBeta\n";ReportStore.replace(report.file(), original);int count =0;for(String line :Files.readAllLines(report.file(),StandardCharsets.UTF_8)){
count++;System.out.println("Line "+ count +": ["+ line +"]");}System.out.println("Whole text exact: "+ original.equals(ReportStore.read(report.file())));}}}classOwnedReportimplementsAutoCloseable{privatefinalPath file =Files.createTempFile("sovrancode-text-",".txt");publicOwnedReport()throwsIOException{}publicPathfile(){return file;}@Overridepublicvoidclose()throwsIOException{Files.deleteIfExists(file);}}classReportStore{privatestaticvoidrequirePath(Path file){if(file ==null)thrownewIllegalArgumentException("File path must not be null.");}privatestaticvoidrequireText(String text){if(text ==null)thrownewIllegalArgumentException("Text must not be null.");}publicstaticvoidreplace(Path file,String text)throwsIOException{requirePath(file);requireText(text);Files.writeString(file, text,StandardCharsets.UTF_8,StandardOpenOption.WRITE,StandardOpenOption.TRUNCATE_EXISTING);}publicstaticvoidappend(Path file,String text)throwsIOException{requirePath(file);requireText(text);Files.writeString(file, text,StandardCharsets.UTF_8,StandardOpenOption.WRITE,StandardOpenOption.APPEND);}publicstaticStringread(Path file)throwsIOException{requirePath(file);returnFiles.readString(file,StandardCharsets.UTF_8);}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · predict encoding, write policy, and exact saved text
Line 1: [Alpha]
Line 2: []
Line 3: [Beta]
Whole text exact: true
Choose the kind of read you need
Use readString when exact CR/LF characters matter. Use line values when processing logical lines and their separators are intentionally irrelevant. Neither whole-file method is a promise of bounded-memory processing for huge inputs.
Measure encoded bytes separately from text length
The String café has four UTF-16 code units, while UTF-8 encodes it into five bytes. Files.size reports the file's byte length, not the number of user-visible characters. Other text, especially supplementary Unicode characters, can make String length differ from visible-character counts too.
Using an incompatible charset can decode wrong text or fail rather than producing a valid round trip. readString with UTF-8 reports malformed input through an IOException when bytes are not valid for that decoding. Changing the charset randomly until a message disappears is not a reliable repair.
The lesson's helper rejects null paths and null text as argument errors before writing. Empty text remains valid and replacing with it intentionally truncates the owned file to zero bytes. Validation of arguments does not prove filesystem access, encoding success, atomicity, or the ability to recover previous contents after an I/O failure.
JAVA 21 COMPILER
Main.java · File bytes and String length answer different questions
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;importjava.nio.charset.StandardCharsets;importjava.nio.file.*;publicclassMain{publicstaticvoidmain(String[] args)throwsIOException{try(OwnedReport report =newOwnedReport()){String text ="café";ReportStore.replace(report.file(), text);System.out.println("UTF-16 units: "+ text.length());System.out.println("UTF-8 bytes: "+Files.size(report.file()));System.out.println("Decoded correctly: "+ text.equals(ReportStore.read(report.file())));}}}classOwnedReportimplementsAutoCloseable{privatefinalPath file =Files.createTempFile("sovrancode-text-",".txt");publicOwnedReport()throwsIOException{}publicPathfile(){return file;}@Overridepublicvoidclose()throwsIOException{Files.deleteIfExists(file);}}classReportStore{privatestaticvoidrequirePath(Path file){if(file ==null)thrownewIllegalArgumentException("File path must not be null.");}privatestaticvoidrequireText(String text){if(text ==null)thrownewIllegalArgumentException("Text must not be null.");}publicstaticvoidreplace(Path file,String text)throwsIOException{requirePath(file);requireText(text);Files.writeString(file, text,StandardCharsets.UTF_8,StandardOpenOption.WRITE,StandardOpenOption.TRUNCATE_EXISTING);}publicstaticvoidappend(Path file,String text)throwsIOException{requirePath(file);requireText(text);Files.writeString(file, text,StandardCharsets.UTF_8,StandardOpenOption.WRITE,StandardOpenOption.APPEND);}publicstaticStringread(Path file)throwsIOException{requirePath(file);returnFiles.readString(file,StandardCharsets.UTF_8);}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · predict encoding, write policy, and exact saved text
Make replacement and append contracts visible in a small API
ReportStore.replace uses WRITE and TRUNCATE_EXISTING without CREATE: it updates a report that must already exist. append similarly requires an existing target. read decodes the current contents as UTF-8. All three methods operate on a supplied path but do not claim ownership or delete it.
OwnedReport owns the temporary file and removes it after the store operations finish. This separation makes the borrowed-path boundary explicit: a storage method should not close or delete an unrelated caller's document merely because it received a Path value.
The store is intentionally not a complete safe-save or authorization service. It does not constrain arbitrary caller paths, prevent symbolic-link races, maintain backups, or synchronize concurrent writers. The supplied programs use only fixed operations on the fresh owned temporary file, and the next layer must define stronger production guarantees if needed.
JAVA 21 COMPILER
Main.java · Require an existing target instead of silently creating one
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;importjava.nio.charset.StandardCharsets;importjava.nio.file.*;publicclassMain{publicstaticvoidmain(String[] args)throwsIOException{try(OwnedReport report =newOwnedReport()){ReportStore.replace(report.file(),"Saved\n");ReportStore.replace(report.file(),"Updated\n");System.out.print(ReportStore.read(report.file()));Files.delete(report.file());try{ReportStore.append(report.file(),"Not recreated");}catch(NoSuchFileException error){System.out.println("Append requires an existing report.");}}}}classOwnedReportimplementsAutoCloseable{privatefinalPath file =Files.createTempFile("sovrancode-text-",".txt");publicOwnedReport()throwsIOException{}publicPathfile(){return file;}@Overridepublicvoidclose()throwsIOException{Files.deleteIfExists(file);}}classReportStore{privatestaticvoidrequirePath(Path file){if(file ==null)thrownewIllegalArgumentException("File path must not be null.");}privatestaticvoidrequireText(String text){if(text ==null)thrownewIllegalArgumentException("Text must not be null.");}publicstaticvoidreplace(Path file,String text)throwsIOException{requirePath(file);requireText(text);Files.writeString(file, text,StandardCharsets.UTF_8,StandardOpenOption.WRITE,StandardOpenOption.TRUNCATE_EXISTING);}publicstaticvoidappend(Path file,String text)throwsIOException{requirePath(file);requireText(text);Files.writeString(file, text,StandardCharsets.UTF_8,StandardOpenOption.WRITE,StandardOpenOption.APPEND);}publicstaticStringread(Path file)throwsIOException{requirePath(file);returnFiles.readString(file,StandardCharsets.UTF_8);}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · predict encoding, write policy, and exact saved text
Updated
Append requires an existing report.
Verify exact content before claiming the save worked
The lab writes a short UTF-8 report, appends a note, reloads the result, and compares it with the expected String. It then replaces the report with shorter text and checks that no old tail remains. Each check targets a different storage mistake.
Also test empty text, blank lines, non-ASCII content, missing targets, duplicate create-new requests, and invalid arguments. Null text should fail before replacement can truncate the current valid file. A rejected operation should not be confused with a successful empty result.
Read-back equality is useful evidence for this local sequence, but it is not proof that data is durably flushed to physical storage or safe against another process editing the file afterward. Keep claims proportional: this exercise verifies API behavior on a small temporary report, not an end-to-end production persistence guarantee.
Repair exercise: the second write discarded the first line
The program is meant to keep First and add Second, but its second default write replaces the first line. Repair only the second write with explicit append behavior. Keep UTF-8 and the supplied newline characters; do not rebuild both lines by hand.
JAVA 21 COMPILER
Main.java · Diagnose accidental truncation
Predict the result, edit Main.java, then run your program.
Select Run code to compile and execute this program.
The starter compiles but currently prints only:
Second
Reveal the explained repair
The default second write truncates the file, so the original First line is discarded. WRITE plus APPEND writes at the end of the existing report. The newline is part of the supplied text, not an automatic side effect of appending.
JAVA 21 COMPILER
Main.java · Append without replacing the prefix
Predict the result, edit Main.java, then run your program.
Select Run code to compile and execute this program.
First
Second
Independent lab: save, append, reload, and verify a study report
Save and reload a small UTF-8 study report on an owned temporary file. Make append and replacement separate operations, verify exact text, and handle a missing report without silently recreating it. No course progress is persisted between website runs.
Implement OwnedReport implements AutoCloseable with one private final Path from Files.createTempFile. Expose file(), declare IOException for construction and cleanup, and remove only this owned file with deleteIfExists in close().
Implement ReportStore.replace(Path, String), append(Path, String), and read(Path). Use explicit UTF-8. replace uses WRITE plus TRUNCATE_EXISTING; append uses WRITE plus APPEND. Neither method should use CREATE: a missing target must remain an error.
Reject a null path with IllegalArgumentException and File path must not be null. Reject null write text with Text must not be null. Validate before opening or truncating. Empty text is valid and read-only methods must not create missing files.
In main write Topic: Java and Minutes: 25 as newline-terminated lines, append Note: café and a newline, then reload. Print the report and compare it with the exact expected text, without strip or newline normalization.
Replace the owned report with Reviewed and a newline. Read it again and verify the shorter text has no old tail. Delete this disposable file deliberately, then catch the missing read and verify the read did not recreate it.
Test exact Unicode and whitespace round trips, append without an automatic separator, empty replacement, missing replace/append/read, CREATE_NEW rejection, invalid null arguments, and cleanup after failure. Do not apply the examples to valuable files or claim atomic durable saving.
The completed program should preserve the note, replace deliberately, and leave a missing read free of creation side effects:
Topic: Java
Minutes: 25
Note: café
Round trip: true
Reviewed
Old tail removed: true
Missing report handled.
Read did not recreate: true
JAVA 21 COMPILER
Main.java · Build the UTF-8 report store
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;importjava.nio.charset.StandardCharsets;importjava.nio.file.*;publicclassMain{publicstaticvoidmain(String[] args)throwsIOException{// TODO: own a temporary report.// TODO: write, append, reload, and verify exact text.// TODO: replace with shorter content, then demonstrate a missing read.}}classOwnedReportimplementsAutoCloseable{// TODO: own one freshly created temporary file.@Overridepublicvoidclose()throwsIOException{}}classReportStore{// TODO: explicit UTF-8 replace, append, and read contracts.}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Stage 1 hints: own one disposable target
createTempFile returns an existing empty file. Keep its Path in OwnedReport and delete only that path at close. Validate null arguments in ReportStore before any write can open or truncate the target.
Stage 2 hints: choose the exact open options
Use WRITE with TRUNCATE_EXISTING for replacement, WRITE with APPEND for extension, and UTF-8 on both reads and writes. Leave CREATE out because the lab requires an existing report. Do not add implicit line separators.
Stage 3 hints: compare exact content and failure outcomes
Compare the reloaded String with initial plus note. Replace with shorter text and compare again without trimming. Delete only the disposable file, catch the missing read, and verify that read did not recreate the path.
Reveal the complete explained solution
JAVA 21 COMPILER
Main.java · Complete save-and-reload report lab
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;importjava.nio.charset.StandardCharsets;importjava.nio.file.*;publicclassMain{publicstaticvoidmain(String[] args)throwsIOException{try(OwnedReport report =newOwnedReport()){String initial ="Topic: Java\nMinutes: 25\n";String note ="Note: café\n";ReportStore.replace(report.file(), initial);ReportStore.append(report.file(), note);String loaded =ReportStore.read(report.file());System.out.print(loaded);System.out.println("Round trip: "+ loaded.equals(initial + note));ReportStore.replace(report.file(),"Reviewed\n");String shorter =ReportStore.read(report.file());System.out.print(shorter);System.out.println("Old tail removed: "+ shorter.equals("Reviewed\n"));Files.delete(report.file());try{ReportStore.read(report.file());}catch(NoSuchFileException error){System.out.println("Missing report handled.");}System.out.println("Read did not recreate: "+Files.notExists(report.file()));}}}classOwnedReportimplementsAutoCloseable{privatefinalPath file =Files.createTempFile("sovrancode-text-",".txt");publicOwnedReport()throwsIOException{}publicPathfile(){return file;}@Overridepublicvoidclose()throwsIOException{Files.deleteIfExists(file);}}classReportStore{privatestaticvoidrequirePath(Path file){if(file ==null)thrownewIllegalArgumentException("File path must not be null.");}privatestaticvoidrequireText(String text){if(text ==null)thrownewIllegalArgumentException("Text must not be null.");}publicstaticvoidreplace(Path file,String text)throwsIOException{requirePath(file);requireText(text);Files.writeString(file, text,StandardCharsets.UTF_8,StandardOpenOption.WRITE,StandardOpenOption.TRUNCATE_EXISTING);}publicstaticvoidappend(Path file,String text)throwsIOException{requirePath(file);requireText(text);Files.writeString(file, text,StandardCharsets.UTF_8,StandardOpenOption.WRITE,StandardOpenOption.APPEND);}publicstaticStringread(Path file)throwsIOException{requirePath(file);returnFiles.readString(file,StandardCharsets.UTF_8);}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
The explicit encoding preserves café, append keeps the initial report, and reloading checks exact contents. Replacement deliberately truncates the old file rather than retaining a tail. Once the owned file is removed, read reports absence and does not invent an empty report. Cleanup tolerates the already-absent owned target.
Test changes and boundaries
Round-trip empty, whitespace-only, multiline, non-ASCII, and supplementary Unicode text using exact String equality and expected UTF-8 bytes.
Append two pieces with and without supplied separators. Replace a long value with a short value and then an empty value; verify no previous suffix remains.
Delete only the disposable owned file and test replace, append, and read. Each must fail under the existing-target policy without recreating it.
Reject null paths and null write text with the documented messages. Establish valid contents first and verify null text cannot truncate them before rejection.
Attempt CREATE_NEW against an existing report and verify its contents remain unchanged. Test malformed UTF-8 and distinguish decoding failure from a valid empty file.
Common mistakes and how to diagnose them
Assuming a default write appends
Without options, writeString can create and truncate. Choose append explicitly when preserving the existing prefix is the contract.
Expecting APPEND to add a newline
Append adds only the characters supplied. Include an explicit separator when your document format requires one.
Confusing missing text with empty text
An empty existing file is valid data; a failed read is not. Do not catch all IOException and silently return an empty String.
Using String.length as an encoded file size
String length counts UTF-16 code units, while Files.size counts bytes. The charset determines the encoded representation.
Assuming write success means atomic durable storage
These APIs do not automatically provide backups, rollback, coordination, or physical-storage durability. A failed write may already have changed the target.
Using whole-file APIs for unbounded input
readString and readAllLines retain the content in memory. Use a suitable bounded or streaming design for large files rather than treating convenience methods as limitless.
Frequently asked questions
Do readString and writeString need my own try-with-resources?
These convenience methods manage their opened resource internally. OwnedReport is closeable here to delete its disposable file, not because returned text is an open stream.
What charset do the convenience overloads use?
The no-charset readString and writeString overloads use UTF-8. The lesson passes UTF-8 explicitly so the agreement remains visible instead of generalizing about unrelated text APIs.
Will append create a missing file?
Not under ReportStore's WRITE plus APPEND policy. Add CREATE only if creating an absent document is deliberately part of the intended contract.
Does readAllLines preserve exact newline characters?
No. It returns line contents without their separators. Use readString for the exact decoded character sequence, including CR and LF characters.
Can a failed replacement damage the previous document?
Yes. Validation happens first in this helper, but an I/O or encoding failure during the write can still leave changed or partial content. Atomic safe-save and backup behavior require a separate design.
Will the website remember the saved report next time?
No. Each run uses isolated temporary storage and removes its owned report. This is real temporary I/O, not persistent platform course-progress storage.
Make encoding and replacement policy visible
UTF-8 is the text agreement, not a storage transaction. Replacement, append, and create-new are different policies. Separators belong to the format, missing files are not empty documents, and exact read-back comparisons catch defects that a successful write call alone cannot reveal.
Explain why the repaired append keeps First, why café occupies more bytes than its String length, and why the store refuses to recreate a missing report. Next: I/O streams and buffered readers and writers, where incremental processing and resource lifetime become explicit.
KNOWLEDGE CHECK
Can you preserve the intended text without accidental replacement?
Answer six questions and read the explanations. Quiz answers are temporary and do not save course progress.
References and related learning
These examples, explanations, and exercises are original SovranCode material. The official Java 21 references document text I/O and open-option behavior.