Skip to main content
Object Oriented Design, Analysis and Programming

Packages, Input-Output Streams and File Handling in Java

Published: 2026-08-19
Level: postgraduate
Audience: Postgraduate students in Object Oriented Design, Analysis and Programming

Prerequisite Knowledge

This lecture builds on the following concepts from earlier lectures. If any feel unfamiliar, review the linked notes before proceeding.

Previously Covered in This Subject

  • Packages -- Organizing Code With Folders and Import - covered in Lecture 16: Introduction to Java and Object-Oriented Programming Foundations
  • Classes -- The Blueprint Idea - covered in Lecture 16: Introduction to Java and Object-Oriented Programming Foundations
  • Objects and Instances -- Concrete Values From a Blueprint - covered in Lecture 16: Introduction to Java and Object-Oriented Programming Foundations

# Packages, Input-Output Streams and File Handling in Java

This lecture builds the bridge from isolated classes to organized, reusable Java systems. You first learn how packages tame the chaos of hundreds of class names, then how the same stream idea lets you read from a keyboard, a file, or a network socket through one uniform pipe, and finally how the classic File, BufferedReader, PrintWriter and Scanner classes put that idea to work with real files — including the exception handling and closing discipline that keeps programs robust.

19.1 Packages — Grouping, Directory Correspondence and Namespace Control

19.1.1 What a Package Is and How It Maps to Directories

Hook: Why can ten different teams each write a class called List, Date, or A and still ship one program without a name fight? The trick is not clever naming — it is putting every name inside an address.

Intuition — the folder you already know: Think of a package as a labelled folder for related classes, exactly like a project folder on your laptop that groups only invoices, or only photos from one trip.

Mapping: folder name → package name; file inside folder → class inside package; subfolder inside folder → subpackage. Just as Photos/2024/Beach.jpg tells you the disk path to the picture, dir1.dir2.c tells you the package path to the class.

Where the analogy breaks: A normal folder is just storage you create with the mouse. A package is also a visibility boundary — classes outside the folder cannot see package-private members, and the compiler enforces the folder-to-package correspondence. You cannot simply drag a *.class file elsewhere and expect it to run.

Formalize — package as naming and visibility control

A package is a named container for a set of functionally related classes and subpackages. It serves two linked purposes, as emphasized in the standard references:

  1. Name-space partitioning. Every package forms its own name space, so a short name need be unique only inside that package. Without packages every class lives in one global space and you would eventually run out of convenient names — the classic "who gets to use Foobar?" problem.
  1. Access control. Packages add a dimension beyond private/public. A class or member with default (no modifier) access is visible to every class in the same package but invisible outside it; protected adds visibility to subclasses outside the package; public opens it everywhere; private keeps it inside one class. Packages are therefore both a naming and an encapsulation mechanism.

Core language rules:

  • If you omit a package statement, the class goes into the default package (the unnamed package). This is fine for tiny demos but inadequate for real applications.
  • A package hierarchy is written with dots, each dot meaning "one level deeper in the directory": package pkg1.pkg2.pkg3; must be stored as pkg1/pkg2/pkg3 on Linux/macOS or pkg1\pkg2\pkg3 on Windows. Case must match exactly — MyPackage and mypackage are different.
  • The Java runtime finds packages from the current working directory, from the CLASSPATH environment variable, or from -classpath on the java/javac command line. Beginning with JDK 9 a package may also be found on the module path, but for this course the class path is the relevant mechanism.
  • More than one source file may declare package mypackage; — they all contribute classes to that single logical package, which is usually spread across many files.
  • Renaming a package always means renaming the directory tree, because the two are mirrors of each other.

How you use a packaged class:

Either import it once at the top and use the short name, or write the fully qualified name everywhere:

// with import
import dir1.dir2.c;
c obj = new c();

// without import — fully qualified at point of use
dir1.dir2.c obj = new dir1.dir2.c();

Both compile to the same bytecode; import is pure convenience.

The correspondence works concretely. Start with an outer folder dir1 that holds a.java and b.java. Inside dir1 create two subfolders dir2 and dir3. Put c.java in dir2 and d.java in dir3. If you also add e.java to dir2, then dir2 now holds c.java and e.java. To use a class from that package you follow the hierarchy with dots: dir1.dir2.c or dir1.dir2.* when you want all classes of that subpackage. The star after the last dot means all classes of that deepest package become available — not its subpackages. The dots mirror moving down one directory level each time.

Visual intuition: Picture the file explorer as a tree.

 project-root/                 ← current directory on CLASSPATH
 └─ dir1/                      ← package dir1
    ├─ a.java                 ← class dir1.a
    ├─ b.java                 ← class dir1.b
    ├─ dir2/                  ← package dir1.dir2
    │  ├─ c.java              ← class dir1.dir2.c
    │  └─ e.java              ← class dir1.dir2.e
    └─ dir3/                  ← package dir1.dir3
       └─ d.java              ← class dir1.dir3.d

Horizontal axis is sibling packages at the same depth; vertical axis is depth (dots). Follow a path from the root, adding one dot per level down — that string is the import path. The landmark to notice is that a and c sit at different depths even though both are inside dir1; their fully qualified names immediately reveal that difference.

19.1.2 Namespace Collision Avoided and Fully Qualified Names

Namespace collision defined: A namespace collision occurs when two classes need the same short name and the compiler cannot tell which one you mean. Inside one package that is forbidden — you cannot have two files named a.java in the same folder. Across packages it is allowed, because the true identity of a class is not its short name but its fully qualified name: package path plus class name.

Two packages can each contain a.java and coexist because dir1.a and dir2.a are different types, stored in different directories, just as two houses can both be "12, Park Street" if they are in different cities. The package prefix is the city.

Predefined Java libraries use exactly the same addressing so that hundreds of classes can coexist without clashes:

  • java.lang.Stringjava is the top package, lang is the subpackage for core language classes, String is the class.
  • java.util.Arraysutil is the utilities package, Arrays is the helper for array operations.
  • java.io.BufferedReaderio is the input-output package, BufferedReader reads buffered text.
  • java.util.Dateutil again, Date handles date/time (now supplemented by java.time).

In each case dots separate package levels and end with the class. Because java.lang is implicitly imported (import java.lang.*; is inserted by the compiler), you can write String s instead of java.lang.String s, but for every other package you must either import or fully qualify.

Scope and assumptions: Package-to-directory mapping assumes the file system is case-sensitive where the JVM runs and that you compile from a directory above the package root (or set CLASSPATH correctly). If you move a .class file without its package folder, java pkg.ClassName will fail with ClassNotFoundException even though the file exists — the runtime expects the folder hierarchy. Also, import pkg.* imports only the classes directly in pkg, not those in pkg.sub. java.util.* does not give you java.util.concurrent classes.

Pitfalls:

  • Star does not mean recursive. Beginners assume import myPackage.* also imports myPackage.myPackageA.ABC. It does not — you must import each package level you actually use.
  • Default package is a dead end. Classes in the default (no-name) package cannot be imported by classes in a named package. Keep every production class in a named package.
  • Duplicate short name in one package still fails. dir1 cannot hold two a.java files even if their contents differ — the file system itself blocks it and the compiler would have no way to distinguish them.

19.1.3 Worked Examples

Example 1 — directory to import translation

Setup: dir1 holds a.java and b.java. dir2 inside dir1 holds c.java. Directory tree:

dir1/a.java
dir1/b.java
dir1/dir2/c.java

Task A — use only c: Write import dir1.dir2.c; at the top (after any package line, before class definitions). Now c can be used by its short name: c obj = new c();.

Task B — use every class in dir2: Write import dir1.dir2.*; All classes directly in dir2 (c and e if present) become visible as short names; classes in dir1 itself (a, b) are not imported.

Sense-check: The dots trace the explorer path dir1 → dir2 → c. Adding one more subfolder would add one more dot segment.

Example 2 — same short name, different packages, no clash

Setup: dir1/a.java contains class a { ... } and dir2/a.java also contains class a { ... } with different fields/methods. Both folders are packages.

  • Inside a single package the duplicate is blocked: you cannot create a second a.java in dir1 — the OS refuses and javac would report "duplicate class".
  • Across packages the pair is legal. The compiler distinguishes them as dir1.a versus dir2.a. To use both in one file you must fully qualify at least one:
import dir1.a;
// dir2.a must be qualified where used
dir1.a x = new dir1.a();
dir2.a y = new dir2.a();

If you instead wrote import dir1.a; import dir2.a; and then a z = new a(); the compiler errors with "reference to a is ambiguous". The fix is always to revert one or both uses to the fully qualified form.

Sense-check: The short name is a nickname useful only inside one neighbourhood; the fully qualified name is the postal address that is globally unique.

19.1.4 Student Questions and Answers

Q: The earlier example moved from dir1 to dir2 with a dot. Is that dot required for every level down?

A: Yes. Each time you go one level deeper in the directory hierarchy you add one more dot and the name of the next folder or package. The dots trace the path from the outermost folder to the class, so dir1.dir2.c means "start at dir1, go into dir2, find c". The same rule applies when you want all classes of the deepest package with a star: import dir1.dir2.*; — the .* replaces the class name but still sits after the final dot that got you to dir2. If you go three levels deep, you write two dots before the class: myPackage.myPackageA.ABC.A. There is no shortcut that skips a dot.

Why students asked: The dot looks like punctuation, so it is tempting to think it is needed only once. Seeing it as the visual echo of the folder separator / or \ makes the rule memorable.

19.1.5 Industry Applications

Production Java libraries — from java.util to Spring and Android SDKs — organize hundreds of classes exactly this way so that short names stay ergonomic inside a feature area while globally unique fully qualified names keep builds and deployments safe. Project teams group related classes into their own packages (for example com.company.billing, com.company.shipping) so that build tools, IDEs, and dependency managers can follow the same folder-to-package mapping when compiling, testing, and packaging JARs. When two libraries coincidentally define Config or List, the package prefix prevents the clash at compile and run time.

Recap: A package is a folder-backed name space that groups related classes, maps one-to-one onto directories, and controls visibility. Dots mirror directory depth, fully qualified names are the globally unique address, and imports are a typing convenience — * imports one package level only.

Bridge: Knowing what a package is raises the next question: how do you create one and place compiled classes exactly where the hierarchy demands? That is the job of the package keyword and the compilation discipline in 19.2.

Exam note: Be ready to (a) translate a drawn directory tree into an import statement with and without *, (b) write the fully qualified name of any class from its folder picture, and (c) explain in one sentence why two classes with the same short name do not clash when they live in different packages.

19.2 Creating Packages — The package Keyword and Directory Building

19.2.1 The package Keyword and the Single-Package Rule

Hook: If a class could belong to two packages at once, which folder should java look in? Java resolves the paradox by allowing exactly one home address per source file.

Intuition — one passport, one citizenship: Think of the package declaration as the citizenship stamped on the first page of a passport. A person has one citizenship at a time and is filed under that country's records. Similarly, a *.java file belongs to exactly one package, and its compiled *.class will be filed in exactly one folder tree.

Where it breaks: Unlike a person who can hold dual citizenship, a Java class cannot be compiled into two packages simultaneously. If you want the same logic in two packages, you duplicate the source file with two different package lines or — better — put the shared logic in its own package and import it where needed.

Formalize — declaring and placing a package

To make a class belong to a package you write a package statement as the very first non-comment line of the source file, before any import and before class:

package myPackage.myPackageA;
public class S1 {
    public S1() {
        System.out.println("This is class S1");
    }
}

Saved as S1.java, compiling with javac -d . S1.java (or simply javac S1.java from the correct root) produces S1.class inside folder myPackage/myPackageA. The -d option is the clean way to have the compiler create missing package folders automatically; without it you must already be in a directory where the relative path myPackage/myPackageA exists.

General form: package pkg1[.pkg2[.pkg3]]; — each pkg segment must be a legal identifier ([A-Za-z_][A-Za-z0-9_]*, conventionally all lowercase). The hierarchy must be reflected in the file system: package a.b.c; must be stored as a/b/c (Unix) or a\b\c (Windows). The directory name must match the package name exactly, including case.

Single-package rule: A source file written as *.java can contain at most one package statement. The reason is that one *.class file can live in only one directory at a time — a file cannot sit in dir1 and dir2 simultaneously. You cannot write:

package dir1;
package dir2;  // compile error
class A { }

Public scope: Write public class S1 when you intend the class to be reusable outside its own package — for example to be extended or instantiated from Test code in another package. A class with default (package-private) access is invisible outside its package. The file name must match the public class name: public class S1 must live in S1.java.

A helpful surprise for beginners is that you do not have to click through the file explorer creating myPackage then myPackageA beforehand. By writing package myPackage.myPackageA; at the top and compiling with javac -d ., the compiler creates the required directory structure itself. The word package plus the dotted path drives the placement; manual folder creation also works but is redundant when -d is used.

Scope and assumptions: The automatic folder creation assumes you compile from the intended package root and that the compiler has write permission there. Building inside an IDE hides this, but on the command line you must run java myPackage.myPackageA.S1 from the directory above myPackage, or set CLASSPATH to include that root. Also, each public class needs its own file — S1.java cannot also contain public class S2.

Pitfalls:

  • Putting package anywhere but the first line. import must follow package; reversing them is a compile error. Comments may precede it, nothing else.
  • Mismatched folder and package. Writing package myPackage.MyPackageA; but storing the file in myPackage/mypackagea fails on case-sensitive file systems. Conventional style is lower-case, dot-separated package names precisely to avoid this.
  • Expecting two homes for one file. Trying to make one A.java serve both ABC and IJK by adding two package lines fails — split it into two files in two folders or refactor the shared code.

19.2.2 Building the MyPackage Hierarchy and Compiling Classes

The hierarchy built in the session looks like a small product family:

myPackage/                         ← package myPackage
├─ myPackageA/                     ← package myPackage.myPackageA
│  ├─ S1.java  S2.java  S3.java    ← classes myPackage.myPackageA.S1 etc.
│  ├─ ABC/                         ← package myPackage.myPackageA.ABC
│  │  ├─ A.java  B.java  C.java
│  └─ DEG/                         ← package myPackage.myPackageA.DEG
│     ├─ D.java  E.java  F.java
└─ myPackageB/                     ← package myPackage.myPackageB
   ├─ S4.java  S5.java  S6.java
   ├─ IJK/                         ← package myPackage.myPackageB.IJK
   │  ├─ A.java  B.java  C.java
   └─ XYZ/                         ← package myPackage.myPackageB.XYZ
      ├─ X.java  Y.java  Z.java

Notice ABC and IJK each hold classes named A, B, C. The short names collide, but the full identities do not:

  • myPackage.myPackageA.ABC.A — trace myPackagemyPackageAABCA.
  • myPackage.myPackageB.IJK.A — trace myPackagemyPackageBIJKA.

The number of dots tells you the depth: one name alone (myPackage), two names (myPackage.myPackageA), three names (myPackage.myPackageA.ABC).

Visual intuition: Imagine a city map where myPackage is the city, myPackageA/myPackageB are two districts, and ABC/IJK are streets in those districts. Two houses can both be "No. A" because the street and district disambiguate — the postal address (fully qualified name) is unique even though the house number repeats.

Creating a class at any level — the repeatable recipe

Steps to create S1 in myPackageA:

  1. Ensure the folder path myPackage/myPackageA exists or will be created by javac -d ..
  2. Start S1.java with package myPackage.myPackageA; as line 1.
  3. Define public class S1 with its constructor.
  4. Save as S1.java (matching the public class name) and compile: javac -d . myPackage/myPackageA/S1.java or equivalently javac -d . S1.java if the file already lives inside its package folder and imports are resolved.

The compiler creates S1.class in the same package folder myPackage/myPackageA. The same recipe with a different class name yields S2.class side by side:

// S2.java — same package line, different class
package myPackage.myPackageA;
public class S2 {
    public S2() { System.out.println("This is class S2"); }
}

Going one level deeper, for A in IJK you add one more dot segment because you go one more folder down:

package myPackage.myPackageB.IJK;
public class A {
    public A() { System.out.println("This is class A"); }
}

Saved as A.java inside myPackageB/IJK (or compiled with -d), this yields A.class there. Repeating the pattern fills every row:

  • S3 keeps myPackage.myPackageA
  • S4S6 use myPackage.myPackageB
  • D E F use myPackage.myPackageA.DEG
  • X Y Z use myPackage.myPackageB.XYZ

Worked trace — compiling the tree end-to-end

Assume the project root is E:\demo. You have just written myPackage/myPackageA/ABC/A.java with header package myPackage.myPackageA.ABC;.

  1. From E:\demo, compile verbatim: javac -d . myPackage\myPackageA\ABC\A.java — the -d . tells the compiler "create the package tree under .". No manual mkdir needed.
  2. Verify: dir myPackage\myPackageA\ABC now lists A.java and the newly generated A.class.
  3. Run any class: java myPackage.myPackageA.ABC.A — you must pass the fully qualified name and run from the root above myPackage. Running java A from inside ABC fails.
  4. If you deliberately misplace A.java (for example store it under myPackageA/B but declare package myPackage.myPackageA.ABC) compilation still succeeds in some toolchains but java myPackage.myPackageA.ABC.A will not find the class at runtime — a reminder that the declaration and the storage must agree.

Sense-check: Count dots after myPackage — each dot equals one subfolder level on disk.

19.2.3 Student Questions and Answers

Q: Do we have to create each folder by hand before writing the class?

A: Not necessarily. You can write the package line at the top of the file with the dotted path you want, place the file anywhere (or in its correct folder), and then compile with javac -d .. The required folder path that matches the dotted path is created or followed by the compiler and the bytecode lands in the correct place. Manual folder creation also works, but the package declaration together with -d drives the placement. On command line the complementary step is running from the directory above the top package (or setting CLASSPATH) so the runtime can climb down the same folder chain.

Why this confuses: IDEs hide the folder work, so students assume the package line alone teleports the file. On the command line the extra steps — correct root, -d, fully qualified java launch — make the mechanism visible.

Recap: The package keyword, written once as the first line, assigns a single home to a class and dictates the folder where its .class must live. More dots mean deeper nesting; the compiler can create the tree for you, but declaration and storage must agree exactly, including case.

Bridge: With classes now correctly filed, the next problem is how other code reaches them without typing a full address every time — and what happens when two addresses share a house number. That is importing and ambiguity resolution in 19.3.

Exam note: Be able to (a) write the first two lines of a file belonging to myPackage.myPackageA.ABC, (b) state why a file cannot have two package lines, and (c) describe the javac -d . and java fully.qualified.Name workflow.

19.3 Importing Packages and Resolving Name Ambiguity

19.3.1 Import Statements and Wildcards

Hook: Would you rather write the full postal address on every envelope, or add a contact once to your address book and then use just the name? import is that address book -- and the star * is the shortcut that imports every class in a package at once.

Intuition — the address book: An import statement copies a class's fully qualified name into a short-name lookup table for this one source file. After import myPackage.myPackageA.ABC.B; the compiler knows that bare B in this file means myPackage.myPackageA.ABC.B, just as after saving "Alice — 12 Park Street, Delhi" you can write just "Alice" on the note.

Where it breaks: The address book is per file, not global. Every *.java file that uses B needs its own import. And import never copies code or loads classes from disk at compile time — it only tells the compiler how to expand short names.

Formalize — import placement and forms

An import statement brings a class from another package into visibility so you can refer to it by its short name. General form:

import pkg1[.pkg2].(classname | *);
  • import java.util.Date; — imports one class Date from java.util.
  • import java.io.*; — imports every public class directly in java.io (but not those in subpackages like java.io.nio).

Rules:

  • import lines appear immediately after the package line (if present) and before any class declaration. Multiple imports are allowed and order does not matter.
  • Depth is unbounded except by the file system: a.b.c.d.E is legal if the directory chain exists.
  • java.lang.* is implicitly imported by the compiler — equivalent to writing import java.lang.*; at the top of every file. That is why String, Math, System work without an import.
  • import is optional. Wherever you could use a short name plus an import, you may instead write the fully qualified name inline: java.util.Date d = new java.util.Date(); compiles without any import.

To use classes A and B from ABC where ABC lives under myPackage.myPackageA, you have two equivalent styles:

import myPackage.myPackageA.ABC.*;   // all classes in ABC
// or
import myPackage.myPackageA.ABC.A;
import myPackage.myPackageA.ABC.B;   // only A and B

Follow the hierarchy with dots: outermost myPackage dot myPackageA dot ABC dot *. The star is a shorthand for "all public types in that exact package".

A small test program using the star form:

import myPackage.myPackageA.ABC.*;
public class Test {
    public static void main(String[] args) {
        B b1 = new B();
        C c1 = new C();
    }
}

Once the import is in place you create objects with short names B and C directly. Without the import you would write myPackage.myPackageA.ABC.B b1 = new myPackage.myPackageA.ABC.B(); each time — correct but verbose.

Choice in practice: Large code bases use the star when many classes from one feature package are needed (for example import mypack.*; in the textbook's TestBalance demo), and single-class imports when only one type is needed and you want the dependency to be explicit and readable in code review.

19.3.2 The Ambiguity Error and the Fully Qualified Fix

The clash scenario: Suppose ABC and IJK each contain a class called AA in ABC might be a bank account model while A in IJK might be an audit logger, but the short name coincides. If you write:

import myPackage.myPackageA.ABC.*;
import myPackage.myPackageB.IJK.*;
public class Test {
    public static void main(String[] args) {
        A a1 = new A();   // which A?
    }
}

the compiler reports a compile-time error: reference to A is ambiguous. Both star imports silently coexist until you actually try to use the colliding short name — only then must you disambiguate.

This mirrors the textbook note: "If a class with the same name exists in two different packages that you import using the star form, the compiler will remain silent, unless you try to use one of the classes. In that case, you will get a compile-time error and have to explicitly name the class specifying its package."

Ways to fix it — pick one based on how often you need each A:

  • Fully qualify at point of use (always safe):
// A from ABC
myPackage.myPackageA.ABC.A a1 = new myPackage.myPackageA.ABC.A();
// A from IJK
myPackage.myPackageB.IJK.A a2 = new myPackage.myPackageB.IJK.A();
  • Single-class import for the common case, fully qualify the rare one:
import myPackage.myPackageA.ABC.A;          // A now means ABC.A by default
import myPackage.myPackageB.IJK.*;          // brings IJK's A but clash avoided for bare A
A a1 = new A();                             // this is ABC.A
myPackage.myPackageB.IJK.A a2 = new myPackage.myPackageB.IJK.A(); // explicit for the other
  • Fully qualify both and import neither when the file uses each only once — most explicit for a reviewer.

In every case the full dotted path from the top package down to the class is the exact name the system uses to locate the .class file on disk, so the clash disappears once the short name is replaced.

Scope and pitfalls:

  • * is not recursive. import myPackage.* does not import myPackage.myPackageA or myPackage.myPackageA.ABC. You must list each package level you actually reference.
  • Clash is lazy. The two star imports alone compile. The error appears only at new A(). This surprises beginners who expect the import lines themselves to fail.
  • Avoid import java.util.*; import java.sql.*; both defining Date. Date d = new Date(); is then ambiguous — a classic real-world version of the A example. Fix by java.util.Date or java.sql.Date fully qualified.

19.3.3 Worked Example

Example — star imports and the ambiguous A

Setup: Packages myPackage.myPackageA.ABC and myPackage.myPackageB.IJK as in 19.2; each contains A.java, B.java, C.java. You create Test.java.

Step 1 — successful star import:

import myPackage.myPackageA.ABC.*;
public class Test {
    public static void main(String[] args) {
        B b1 = new B();   // resolves to ABC.B
        C c1 = new C();   // resolves to ABC.C
    }
}

Compiles and runs from the project root with javac -d . Test.java and java Test. Both B and C expand to myPackage.myPackageA.ABC.B / C.

Step 2 — introduce the clash:

import myPackage.myPackageA.ABC.*;
import myPackage.myPackageB.IJK.*;
public class Test {
    public static void main(String[] args) {
        A a1 = new A();               // FAILS: ambiguous
    }
}

Compiler error (paraphrased): reference to A is ambiguous, both class myPackage.myPackageA.ABC.A and class myPackage.myPackageB.IJK.A match.

Step 3 — resolve with fully qualified names:

import myPackage.myPackageA.ABC.*;
import myPackage.myPackageB.IJK.*;
public class Test {
    public static void main(String[] args) {
        // decide which A you need — here ABC
        myPackage.myPackageA.ABC.A a1 = new myPackage.myPackageA.ABC.A();
        a1.toString();

        // the other A in the same file needs its own fully qualified form
        myPackage.myPackageB.IJK.A a2 = new myPackage.myPackageB.IJK.A();
        a2.toString();
    }
}

Compiles. The import * lines remain valid — they still give you B and C from both packages without qualification, but A is now explicit.

Sense-check: If only ABC.A were needed, dropping import myPackage.myPackageB.IJK.*; would also remove the ambiguity, but at the cost of qualifying every B/C from IJK you might need later — pick the style by frequency of use.

Recap: import is a per-file short-name convenience, not a runtime load; * means "all classes in this exact package". Two star imports clash only when you use the shared short name — fixed by falling back to the fully qualified name at the point of use.

Bridge: Imports solve naming, but the next lecture block leaves names behind and asks how any data at all gets into a program — starting with the quickest keyboard helper, Scanner, in 19.4.

Exam note: Expect a snippet with two import ...*; lines introducing the same class name and a line A a1 = new A();. You must spot the ambiguity and rewrite the declaration and new with the fully qualified form for the intended package.

19.4 Scanner Class — Quick Keyboard Input

19.4.1 Import and Association with the Keyboard

Hook: Reading from the keyboard sounds trivial until you realize keystrokes are just bytes — someone must turn them into int, float, or a String with spaces. Who is that someone?

Intuition — a translator at the keyboard: System.in is the raw byte pipe from the keyboard, like a telephone line carrying uninterpreted tones. Scanner is the translator who listens on that line and hands you typed values: "that was an int 20, this is a String hello". Without the translator you have noise; with it you have typed data.

Where it breaks: Wrapping with Scanner is convenient but line-buffered and relatively slow. For large file parsing or precise control over buffering, BufferedReader plus manual parsing may be preferred — the translator is easy, not always the most efficient.

Formalize — Scanner's role and wiring

The Scanner class is a predefined service in java.util, designed for quick, formatted input. Its job is to tokenize an input source and convert tokens to Java types. Sources can be many: keyboard, a file stream, a String, a network channel.

To use it you must import it — unlike java.lang, java.util is not implicitly imported:

import java.util.Scanner;

For keyboard input you create a Scanner object and connect it to the keyboard by passing System.in:

Scanner a = new Scanner(System.in);

Breakdown:

  • Scanner — the type of the helper.
  • a — the object name (often called sc or scanner by convention).
  • new Scanner(...) — calls the constructor that stores the source.
  • System.in — an InputStream object (a byte stream) that the JVM ties to the keyboard by default. In the stream hierarchy, System.in is the screen linked to the keyboard, System.out the screen linked to the monitor.

After this one association, every read through a comes from the keyboard until the scanner is closed.

Architecture note: Under the hood Scanner wraps the byte stream with decoding (bytes → characters via the platform default charset) and tokenization (splitting on whitespace by default). You do not see these layers, but they explain why Scanner can offer nextInt() directly while raw System.in.read() only yields bytes.

19.4.2 Methods for Different Data Types

Once the object a is linked, you call its predefined methods with the dot operator. Let a be the scanner object name:

  • String with spaces: a.nextLine() reads the entire line up to the line break, including spaces, and consumes the line break. Example: user types Hello WorldString str = a.nextLine(); stores "Hello World" with the space kept.
  • String without spaces (one token): a.next() reads characters until the first whitespace delimiter. If the user types Hello World and you call a.next(), only "Hello" is read; World remains for the next call. Delimiters are whitespace by default and can be customized with useDelimiter.
  • Single character: There is no direct nextChar(). The idiom is a.next().charAt(0). The chain means: read one token without spaces, then take its first character. If the token is Hello, positions are 0:H 1:e 2:l 3:l 4:o, so char c = a.next().charAt(0); yields 'H'.
  • Integer: a.nextInt() parses the next token as a decimal integer using Integer.parseInt internally. Example: int num = a.nextInt();. Throws InputMismatchException if the token is not an integer.
  • Floating point: a.nextFloat() parses the next token as a float (nextDouble() for double). Example: float f = a.nextFloat();.

In each case assign to a matching variable type. Other typed helpers exist in the same family: nextLong(), nextShort(), nextByte(), nextBoolean(), hasNextInt() (a probe that returns boolean without consuming).

Scope and assumptions: Scanner's default delimiter is whitespace. That is why next() stops at a space while nextLine() does not. After a nextInt() call the numeric characters are consumed but the trailing newline that the user typed with Enter remains in the buffer. A subsequent nextLine() will see that newline and return an empty string — the classic "skipped input" trap. Fix by inserting an extra nextLine() to consume the dangling newline before the real nextLine(). Also, mixing next()/nextInt() with nextLine() in one program requires careful sequencing; pick one style per input section.

Visualize the keyboard pipe as:

keyboard → [System.in byte stream] → [Scanner: decode + tokenize] → a.nextInt()/a.nextLine()

Keyboard bytes flow left to right through primary memory (RAM) into the scanner's buffer, then typed values emerge to your variables.

Pitfalls:

  • Forgetting import java.util.Scanner;. System.in alone compiles, but new Scanner(...) does not without the import.
  • Using next() when spaces are needed. A full name "Riya Sharma" requires nextLine(); next() would return only "Riya".
  • Not handling InputMismatchException. If you call nextInt() and the user types "twenty", the program throws and aborts. Robust code probes with hasNextInt() or wraps in try-catch.
  • Leaving the scanner open. For System.in you typically do not close Scanner until program end, because closing it also closes System.in and no further keyboard reads are possible in that JVM run.

19.4.3 Worked Example — Reading Name, Age and Gender

Program — Test with typed reads

import java.util.Scanner;
public class Test {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("Enter name, age and gender");
        String name = sc.next();
        int age = sc.nextInt();
        String gender = sc.next();
        System.out.println(name);
        System.out.println(age);
        System.out.println(gender);
    }
}

Trace with user input:

Suppose the program prints Enter name, age and gender and the user types three tokens separated by whitespace or newlines:

Riya
20
Female

or equivalently on one line: Riya 20 Female followed by Enter.

Step Code executed Scanner action Variable result
1 import java.util.Scanner; Compiler makes Scanner type known
2 Scanner sc = new Scanner(System.in); sc linked to keyboard byte pipe System.in sc ready
3 String name = sc.next(); Reads next whitespace-delimited token "Riya" name = "Riya"
4 int age = sc.nextInt(); Parses next token "20" as int age = 20
5 String gender = sc.next(); Reads next token "Female" gender = "Female"
6 three println calls Writes each value to System.out (monitor) Screen shows Riya, 20, Female each on its own line

Variation with spaces: If you wanted a full name like "Riya Sharma" you would replace step 3 with String name = sc.nextLine();. But if that line follows a nextInt() earlier without consuming the newline, insert sc.nextLine(); as a dummy read first — otherwise you get an empty name.

Sense-check: sc is used for every read after the one-time association; you never recreate Scanner per value. The typed method must match what the user actually typed.

19.4.4 Student Questions and Answers

Q: When we already have System.in why must we still import Scanner?

A: System.in is the channel — the raw byte stream linked to the keyboard, defined in java.lang.System. It can deliver bytes but does not know how to turn the character sequence "20" into the integer 20.

Why it seemed plausible: Students see System.in in new Scanner(System.in) and think the presence of the channel is enough.

Correction: Scanner is the helper class in java.util that knows the tokenization and parsing rules — nextInt(), nextLine(), next(), nextFloat(), charAt(0). The classes for stream screens live in java.io (for example InputStreamReader, FileInputStream) and java.util (for Scanner). Without import java.util.Scanner; the compiler cannot find the type Scanner at new Scanner(...), so compilation fails even though System.in itself is always known. The separation is intentional: the channel (where bytes come from) and the parser (how bytes become typed values) are orthogonal concerns, and swapping the parser or the channel is how file input reuse works in 19.9.

Q (extension related): If System.in already connects to the keyboard, why not read raw bytes directly?

A: You can — System.in.read() returns an int byte — but you would hand-roll decoding, whitespace splitting, and NumberFormat parsing for every program. Scanner packages exactly that repetitive work and adds exception semantics (InputMismatchException) instead of silent garbage.

Recap: Scanner is the typed translator sitting on top of raw System.in. Import java.util.Scanner, associate once with new Scanner(System.in), then call the method that matches the next token's type — nextLine() to keep spaces, next()/charAt(0) for characters, nextInt()/nextFloat() for numbers.

Bridge: Keyboard input touches only typed tokens. Real applications must also ask about files — do they exist, how large are they, where do they sit — without reading their contents. That is the attribute world of the File class in 19.5.

Exam note: Be able to write import java.util.Scanner; plus Scanner sc = new Scanner(System.in); and pick the correct read method per type, including the two-step character idiom and the next() versus nextLine() distinction with spaces.

19.5 File Class — Attributes, Paths and Constructors

19.5.1 Working on Attributes Not Data and Platform Independence

Hook: How can code ask "is this file hidden? how big is it? does it even exist?" without opening a single byte of its content?

Intuition — the file card in a library: Think of the File object as the catalogue card for a book, not the book itself. The card lists title, shelf location, page count, last borrowed date, and whether the book is reference-only. You interrogate the card to decide whether to fetch the book; you need a different tool (a stream) to actually read the pages.

Where it breaks: Unlike a library card that is created when a book arrives, a File object is just a handle you create anywhere. new File("ghost.txt") creates a card for a book that may not exist — you must call exists() to check.

Formalize — what java.io.File does and does not do

The File class lives in java.io (so import java.io.File; is required). Its purpose is to operate on the attributes or characteristics of a file or directory, not on the data inside. Attributes include name, parent folder, full path, length in bytes, last-modified timestamp, and permission flags such as readable, writable, hidden, absolute.

  • Reading or writing the actual bytes/characters needs other stream classes (FileInputStream, FileReader, BufferedReader, Scanner, PrintWriter, covered in 19.6–19.9).
  • A directory in Java is simply treated as a File with one extra property: the list of file names it contains, accessible via list() / listFiles().

Platform independence nuance: Java is platform independent because of the Java Virtual Machine — you write once and run on Linux, Windows, or macOS. However, the file naming convention itself follows the underlying operating system beneath the JVM. How a path is written (slashes versus backslashes, allowed characters, case sensitivity), what constitutes a root (/ versus C:\), and what names are legal are handled by the OS. Your Java code supplies a path string, and the JVM together with the OS resolves the association. The lean design choice is deliberate: Java does not virtualize the file system, it adapts to it.

A file named F1.txt on disk can be opened, closed, read, or written, but the File object f1 itself only gives you metadata. Once you associate a File object with a path, every query is a question to that catalogue card.

19.5.2 Path Names, Constructors and Virtual Association

Path names — absolute versus relative

A path name tells both the file name and the folder chain required to reach it.

  • Absolute (full) path starts from the root. Example: C:/Data/first.java means "start at root drive C:, go into folder Data, find first.java". Absolute paths are unambiguous but tie your code to one machine layout.
  • Relative path starts from the directory where the program is running (the current working directory). Example: if your program lives in myProject and the file is at myProject/data/first.java, passing "data/first.java" is sufficient without going all the way to C:. Relative paths make a project portable across installations.

Default rule: File f1 = new File("a.java"); supplies only the name. The rule is that the file is assumed to be in the same directory as the running program. f1 is then linked to that a.java in the current folder; from f1 you can query attributes or later connect streams to read or write.

Virtual association: Creating a File object does not create a file on disk. You are creating a virtual association — a handle — that lets you manipulate an existing file or later create a new one through a stream. The file on disk stays where it is; f1 is the pointer.

The three constructors: Pick the one that matches how your path information is split:

  1. File(String pathname) — one string that includes path and name together.
  2. File(String parent, String child) — two strings: parent path and file name.
  3. File(File parent, String child) — a File object for the parent folder plus the file name string. A fourth overload File(URI uri) exists in the library but is outside this lecture's scope.

Decision guide: If your code builds a path by concatenating strings you likely want form 1 or 2; if you already hold a File for a directory (for example after listFiles()), form 3 avoids re-parsing the parent string.

Worked handling of slashes and parent/child splits:

  • File f = new File("C:/D/first.java"); — one string with drive C:, folder D, file first.java. Uses constructor 1. Forward slash is used, which Java accepts even on Windows and correctly resolves.
  • File f1 = new File("C:\\D\\first.java"); — same logical path but with Windows backslashes escaped as \\ inside the Java string. Also one string, also constructor 1. Either style reaches the same file because the OS and JVM normalize separators.
  • File f2 = new File("C:/D", "first.java"); — two arguments: parent "C:/D" and child "first.java" separated by a comma. Uses constructor 2.
  • File f3 = new File("C:/D"); — only a parent directory, no file name. You cannot use f3 alone to query first.java, but you can use it as the parent handle for constructor 3.
  • File f4 = new File(f3, "first.java"); — uses f3 the File parent object plus name "first.java". Uses constructor 3. This form is handy when f3 came from directory traversal rather than a literal.

When an entire path plus name is supplied as one string you are in constructor 1. When parent and name are split into two strings you are in constructor 2. When you hold a File for the parent and add a name you are in constructor 3.

Visual intuition: Draw a line for the path string and a box for the File handle. new File(...) does not draw a new file rectangle on disk; it only draws an arrow from the handle to the existing rectangle. Streams later use that arrow to pump bytes.

Scope and assumptions: The separator normalization (/ works on Windows, \ needs \\ in a Java literal) is handled by File's constructor, but hard-coded absolute paths still break portability — prefer relative paths for coursework and use File.separator or Paths.get for library code. Constructor 2/3 do not validate that parent exists; existence is checked later with exists().

19.5.3 Attribute Methods and Boolean Permission Methods

Once the handle exists, the file is considered "open for metadata" — associated and ready to query. Each call uses the dot operator on the File object f.

Attribute methods (information-bearing):

  • getName() returns String — only the final name component, for example "first.java".
  • getParent() returns String — the parent path component as supplied (may be null, see Example B).
  • getPath() returns String — the original pathname string you passed to the constructor, normalized.
  • getAbsolutePath() returns the absolute form regardless of whether you gave a relative input (useful for debugging).
  • length() returns long — length in bytes, how much storage the file occupies.
  • lastModified() returns long — timestamp as milliseconds since the epoch (January 1, 1970 00:00:00 GMT), suitable for new Date(long).
  • getParentFile() (companion to getParent() but returning a File handle) is available when you prefer to stay in the File domain.

Boolean methods (true/false probes):

  • canRead() — is the file readable by this process.
  • canWrite() — is the file writable.
  • isHidden() — is the file marked hidden by the OS.
  • exists() — does a file or directory at that path actually exist on disk.
  • isFile() — is the path a normal file (returns false for directories, device files, named pipes).
  • isDirectory() — is the path a directory.
  • isAbsolute() — was the pathname supplied as absolute (has root/drive prefix).

You typically probe before acting: if (f.exists() && f.canRead()) { /* open stream */ } else { /* report */ }. Several methods throw SecurityException when a security manager denies access — relevant in sandboxed or applet contexts but not needed when running via plain java.

Pitfalls:

  • Thinking new File(...) creates the file. It creates only the handle. The file on disk is created when you open a FileOutputStream/PrintWriter for writing, or explicitly call createNewFile().
  • Expecting getParent() to always return a string. When no parent was supplied, it returns null, not empty string — guard against NullPointerException.
  • Confusing getPath() with getAbsolutePath(). getPath() echoes what you typed; getAbsolutePath() always expands to a full path from the file system root.

19.5.4 Worked Examples

Example A — attribute reads with a full path

File f = new File("C:/data/first.java");
String a = f.getName();       // "first.java" — only last segment
String b = f.getParent();     // "C:/data"    — everything before name
String c = f.getPath();       // "C:/data/first.java" — what you passed, normalized
long len = f.length();        // bytes used, e.g. 2140
long mod = f.lastModified();  // e.g. 1716200000000L → new Date(mod) for readable form

Trace: f points at first.java inside data on drive C:. getName() strips the directory, getParent() strips the file leaving the directory chain, getPath() echoes the full argument, length() queries the file system for size, lastModified() queries the timestamp. None of these calls reads file content.

Example B — default path with only a name (null parent case)

File f1 = new File("first.java");
f1.getParent(); // returns null — no explicit parent component
f1.getName();   // returns "first.java"
f1.getPath();   // returns "first.java"
f1.getAbsolutePath(); // e.g. "E:\project\first.java" on Windows

Why null? The constructor was given only "first.java" with no folder chain. The rule "default is the program's own directory" is a runtime resolution, not a string stored in the parent field, so getParent() has nothing to return and yields null. getName() and getPath() both understandably give "first.java". This null-parent pattern is common in student code and must be guarded: if (f1.getParent() != null) { ... }.

Example C — Boolean checks and isAbsolute distinction

File f = new File("first.java");            // only name, no path
File f1 = new File("C:/data/first.java");   // full path
boolean b1 = f.isAbsolute();    // false — no drive/root was supplied
boolean b2 = f1.isAbsolute();   // true  — a complete absolute path was supplied
boolean readable = f1.canRead(); // true/false depending on OS permissions
boolean exists = f1.exists();    // true if file at that location is present

Also useful in the same family: f.isFile() vs f.isDirectory() to dispatch directory listing logic (see DirList demo in reference docs), f.isHidden() to filter OS-hidden files, f.exists() before f.length() to avoid operating on a ghost path.

Sense-check: isAbsolute() answers "did you give me an absolute-looking string?" not "does this file exist?". A path can be absolute yet non-existent, or relative yet existent.

19.5.5 Student Questions and Answers

Q: You wrote the file path one time with forward slash / and one time with backward slash \. Must we always follow one style to be safe?

A: No. You may provide just the name if the file sits in the same folder as the program — that is the default and needs no path at all. If the file is elsewhere you must provide a path, but Java is platform independent in handling separators: you can give the full path as one string using either Unix-style forward slashes C:/data/first.java or Windows-style backslashes C:\data\first.java (which inside a Java string literal appears as "C:\\data\\first.java" because \ must be escaped as \\). The JVM together with the underlying OS normalizes either form to the correct native representation, so both reach the same file. Splitting parent and file name into two constructor arguments (new File("C:/D", "first.java")) or using a File parent also works and sidesteps separator worries altogether. Pick the constructor that matches how your path data is stored, but all forms are portable at the File level — hard-coded absolute paths still reduce overall program portability, so prefer relative paths plus File.separator in production code.

Why students asked: The session alternated between C:/D/... and C:\D\... on different slides, suggesting a hidden correctness rule. The real rule is normalization, not uniformity.

Recap: File is the catalogue-card handle — new File(...) creates no file, only a virtual association. Absolute paths start from the root, relative paths from the program's directory, and three constructor shapes cover single-string versus split parent/child forms. getName/getParent/getPath/length/lastModified reveal metadata; canRead/canWrite/exists/isFile/isDirectory/isHidden/isAbsolute probe state — with null parent when no path was given.

Bridge: Knowing where a file is and what it is does not move its bytes. Moving bytes needs a connected one-way pipe — the stream — introduced generically in 19.6 before concrete BufferedReader/PrintWriter/Scanner wrappers use it.

Exam note: State that File works on attributes not content, distinguish absolute versus relative paths, name the three constructors with signatures and when each applies, list attribute methods getName/getParent/getPath/length/lastModified and Boolean methods canRead/canWrite/isHidden/exists/isFile/isDirectory/isAbsolute, and explain the null parent when only the name is supplied.

19.6 Streams — Virtual Pipelines for Reading and Writing

19.6.1 What a Stream Is

Hook: Keyboard, file, network socket — three wildly different devices, yet Java reads from them with the same read() and writes with the same write(). What single idea hides the hardware?

Intuition — the garden hose: Think of a stream as a one-way garden hose (also called a screen in this lecture) connecting a water source to a sprinkler. The source can be a tank, a river, or a municipal main; the sprinkler stays the same because the hose standardizes the flow. A Java stream is the analogous virtual hose connecting a program (in RAM) to a source or sink. Data flows one direction through the hose, and on the way it is converted from the device's native form to bytes characters your program understands.

Where it breaks: Real hoses can flow either way if you reverse them; a Java stream is strictly one-way — an input stream reads only, an output stream writes only. To read and write the same file you need two streams (or a RandomAccessFile).

Formalize — stream as abstraction and placement in the hierarchy

A stream is a virtual pipeline that connects a program to a place where data comes from or goes to. It is an abstraction layered on top of a physical device or file by the Java I/O system. All data passing between source/sink and program travels through primary memory (RAM) via the stream, and the stream hides device specifics behind a uniform interface.

Physical picture for a file:

disk: first.txt (bytes on storage)
        ↕
   stream (pipe, hosted in RAM, managed by FileReader/FileInputStream/etc.)
        ↕
   program variables in heap

For a file, the file on disk holds the bytes, the pipe links it to the program, and through the pipe you read bytes into primary memory or write bytes from primary memory back to disk. Whether reading or writing, the data lands in RAM buffers first — you never manipulate disk bytes in place.

At the top of the class hierarchy are four abstract bases:

  • InputStream / OutputStream — byte streams (binary data, any file, images, sockets). Byte I/O is the lowest common denominator; even character streams ultimately use bytes.
  • Reader / Writer — character streams (Unicode text, internationalized). Added in Java 1.1; preferred when dealing with characters or strings because they handle charset decoding correctly.

Every concrete class — FileInputStream, FileOutputStream, FileReader, BufferedReader, InputStreamReader, PrintWriter, Scanner's underlying channel — is a subclass of one of these four, inheriting the single-method read()/write() contract and adding device or buffering specifics.

19.6.2 Classification of Streams

Two orthogonal classifications — and the same idea beyond files

By direction (the only runtime classification):

  • Reader or input stream — data flows into primary memory. Used to read from keyboard, file, or network socket. Concrete examples you have met: reading from keyboard with Scanner over System.in (System.in itself is an InputStream), reading from a file with BufferedReader wrapping FileReader, reading from a network socket with socket.getInputStream() wrapped in InputStreamReader + BufferedReader.
  • Writer or output stream — data flows out of primary memory to a device. Used to write to monitor, file, or network socket. Examples: printing to the screen with System.out.println (System.out is a PrintStream — a byte output stream), writing to a file with PrintWriter over FileOutputStream.

A helpful complementarity to memorize:

Console Stream wrapped inside Direction Typical wrapper you add
System.in (keyboard) hides a keyboard input stream input new Scanner(System.in) or new BufferedReader(new InputStreamReader(System.in))
System.out / System.err (monitor) hides a monitor output stream output PrintWriter pw = new PrintWriter(System.out, true) for character-mode console

In file and network code you build the analogous file streams (FileReader, FileInputStream, FileOutputStream) and wrap them with higher helpers (BufferedReader, Scanner, PrintWriter) to gain buffering, line methods, or formatted printing.

By data unit (design choice for new code):

  • Byte stream (InputStream/OutputStream family) — handles raw bytes. Use when working with binary data, images, serialized objects, or when you need the exact bytes.
  • Character stream (Reader/Writer family) — handles Unicode characters, translating bytes ↔ chars via a Charset. Use when working with text, strings, and internationalized output; often more efficient and correct for textual data.

Visual intuition: Draw two pipes side by side. The byte pipe is narrow and carries raw balls (bytes); the character pipe is wider and carries labelled envelopes (chars) — but under the glass the envelope pipeline still moves balls and repackages them. That repackaging is InputStreamReader / OutputStreamWriter.

Why the abstraction matters: Because the same Reader/Writer methods apply irrespective of source, you can write business logic once against the Reader interface and reuse it for console input, file reading, and socket reading — swapping only the construction line. Frameworks for web servers, ETL ingestion, and file transfer exploit exactly this substitution.

Scope and assumptions: "All streams behave the same way" is true at the API level, not at performance: a socket stream may block, a file stream may be seekable, System.in is line-buffered by default so read() often appears to wait until Enter. Character streams require a charset; if you do not specify one, the platform default is used, which can break when moving files between machines with different defaults.

Pitfalls:

  • Using a byte stream for text with non-ASCII characters ignores charset. Reading a UTF-8 file with FileInputStream and casting bytes to char corrupts any character beyond ASCII. Use Reader/Writer for text.
  • Forgetting that streams are one-way. Opening a FileInputStream to read and then calling write on it fails — you need the complementary FileOutputStream.
  • Not closing streams. Every open stream holds a file handle and native buffer. Rely on try-with-resources or explicit close()/flush() as in 19.7–19.9.

Recap: A stream is Java's uniform one-way virtual hose through RAM; byte streams (InputStream/OutputStream) carry raw bytes, character streams (Reader/Writer) carry Unicode chars with charset translation; input streams pull data in, output streams push it out, and the same interfaces hide keyboard, file, and socket differences — System.in and System.out are just the pre-wired console streams.

Bridge: The generic pipe is now concrete: for traditional line-based reading with navigation you wrap it as BufferedReader (19.7); for writing text lines you wrap it as PrintWriter (19.8); and for quick token parsing you keep using Scanner (19.4 → 19.9). Each adds buffering, methods, and resource discipline on top of the same pipe.

19.7 BufferedReader Class — Reading with a Buffer

19.7.1 Purpose and the Buffer Idea

Hook: Why read one character at a time directly from disk when you can pull a whole bucket at once and sip from it?

Intuition — the warehouse staging area: Imagine a warehouse where goods arrive by truck (slow, expensive per trip) but are shipped out to customers in small parcels. The staging buffer is the middle shelf: a truck unloads a full pallet at once, then workers fulfill orders from the shelf without waiting for the next truck.

Similarly, a buffer is a temporary storage area in RAM. Whatever data you read is first collected in the buffer as a chunk, and then your program takes pieces from that buffer at its own pace. The system pulls data from the file or keyboard into the buffer in one efficient chunk, then BufferedReader serves read() / readLine() calls from memory, reducing direct device hits and smoothing reading. For keyboard input the same chip-away from a buffered line is what makes Enter-based line buffering natural.

Where it breaks: Buffering helps for sequential reads. For random access by byte position, buffering adds little and RandomAccessFile or SeekableByteChannel is more appropriate.

Formalize — what BufferedReader is and where it lives

BufferedReader is a classic character-input class in java.io for efficient, buffered reading. It belongs to the Reader hierarchy (character stream) and wraps another Reader to add two things: (1) an in-memory buffer, and (2) convenient line and navigation methods. Although newer APIs (Scanner, java.nio.file.Files.readAllLines, BufferedReader over java.nio channels) now exist, BufferedReader remains widely used because of its simplicity and precise control over mark/reset/skip.

  • Location: java.io.BufferedReader — so import java.io.BufferedReader; (and import java.io.FileReader; / import java.io.InputStreamReader; for the inner source) is required.
  • Construction pattern: outer buffered wrapper + inner source reader that already knows the device.

19.7.2 Syntax for Keyboard, Socket and File

To use BufferedReader you create an outer BufferedReader object and tell it which underlying source to wrap. InputStreamReader is the helper that bridges a byte stream (for example System.in bytes) to a character Reader.

  • From keyboard (byte source → chars):
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

Here BufferedReader is the outer buffered helper, new InputStreamReader(System.in) builds a character screen linked to System.in (the keyboard byte pipe). The object name br now reads buffered characters from the keyboard through that two-layer pipe: System.in (bytes) → InputStreamReader (bytes→chars) → BufferedReader (buffer + lines).

  • From network socket (same wrapper, different source bytes):
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));

The rest stays identical. socket.getInputStream() supplies the byte input stream from the socket; wrapping with InputStreamReader and BufferedReader is unchanged. The network setup and socket lifecycle belong to networking material, but the wrapping pattern matches the keyboard case exactly — another demonstration of stream uniformity from 19.6.

  • From a specific text file (character source directly):
BufferedReader br2 = new BufferedReader(new FileReader("ABC.txt"));

FileReader already is a Reader that directly opens the named file and associates it, converting bytes to chars using the default charset. No separate InputStreamReader is needed for the simple file case in the form taught here (though new InputStreamReader(new FileInputStream("ABC.txt"), StandardCharsets.UTF_8) is the more explicit, charset-safe variant). br2 wraps FileReader so that read() is buffered and readLine() becomes available.

Common textbook alternative for keyboard (also byte→char→buffered but more explicit about charset control) is new BufferedReader(new InputStreamReader(System.in)) — the session's recommended form.

Wrapping as a design mantra: Outer BufferedReader adds buffering and line methods; inner object (InputStreamReader over System.in or socket.getInputStream(), or FileReader for files) supplies the source screen. You can swap the inner source and keep every outer call identical.

19.7.3 Methods Available

Useful methods exposed by BufferedReader (all potentially throwing IOException):

  • Constructor: BufferedReader(Reader in) — takes the inner reader you chose. An overload BufferedReader(Reader in, int sz) lets you set buffer size in characters (default 8192 is usually fine).
  • int read() — reads a single character as an int in the range 0–65535, or -1 at end of stream. Assign to char after cast: char ch = (char) br.read(); but always test for -1 before casting at EOF.
  • String readLine() — reads a whole line, returning String without the terminating line break (\n, \r, or \r\n). Returns null at EOF. Ideal for strings with spaces — no token splitting.
  • int read(char[] cbuf, int off, int len) — reads characters into an array segment, returns count or -1. Useful for bulk transfers.
  • void close() — closes the file and releases the connection (also closes the wrapped inner reader). Must be called when done, preferably via try-with-resources.
  • void mark(int readAheadLimit) — marks the present position in the stream so you can return to it later with reset(). The limit readAheadLimit says how many characters you may read past the mark while keeping the mark valid; exceeding it may invalidate the mark (depends on buffer size).
  • void reset() — returns to the most recent mark. Throws IOException if no mark was set or the mark was invalidated.
  • long skip(long n) — skips forward n characters (not bytes — this is a character stream), returning actual skipped count. Also respects the read-ahead limit when a mark is active.
  • boolean markSupported() — returns true for BufferedReader (the feature is supported).
  • boolean ready() — returns true if the stream is ready to be read without blocking (useful for non-blocking probes, not needed for file reading).

Scope and pitfalls:

  • Forgetting close() leaks a file handle. Prefer try (BufferedReader br = new BufferedReader(new FileReader("f.txt"))) { ... }.
  • mark limit is not window size. Mark with mark(10) then reading 20 characters before reset() may fail — the buffer may discard the marked data after 10 extra characters. For the WELCOME example the session used mark(100) to safely stay within limit for an 7-character word.
  • read() returns int not char for a reason. The -1 EOF sentinel does not fit in a char. Always check int v = br.read(); if (v == -1) ... else char c = (char)v;.

19.7.4 Worked Example — hello.txt with WELCOME

Setup: File hello.txt on disk holds the single word (7 letters, no newline):

WELCOME

Indices for reference: 0:W 1:E 2:L 3:C 4:O 5:M 6:E.

Program steps with buffer-aware cursor:

BufferedReader br1 = new BufferedReader(new FileReader("hello.txt"));

Now br1 is associated with the file; an internal buffer (default 8192 chars) is allocated but the logical cursor sits before index 0.

Step Code Cursor before → after Value read / effect
1 char a = (char) br1.read(); before W → after W (at E) a = 'W'
2 br1.read() (implicit in trace) would read E, then L — after three single reads the cursor sits at C (index 3). The session consolidates this as "further read() calls would read E then L" and notes the cursor now at C. at E → at L → at C
3 br1.mark(100); at C (index 3), mark placed at current position C is now the marked spot; read-ahead limit 100 covers the remaining 4 characters easily, so mark will stay valid
4 char c = (char) br1.read(); at C → at O (index 4) c = 'C'
5 br1.skip(2); at O (index 4) → skip O (4) and M (5) → lands at E (index 6, last letter) Skips exactly 2 characters; return value 2 confirms
6 char e1 = (char) br1.read(); at E (index 6) → at EOF (past 6) e1 = 'E' (the final letter; second E of WELCOME)
7 br1.reset(); at EOF → back to marked C (index 3) Cursor returns to the marked C
8 char c2 = (char) br1.read(); at C → at O c2 = 'C' again from the mark — proves reset rewound
9 br1.close(); Releases the handle and underlying FileReader; subsequent reads throw IOException

Complete minimal runnable trace for steps 4–8:

br1.mark(100);
char c  = (char) br1.read(); // C
br1.skip(2);                 // over O, M
char e1 = (char) br1.read(); // E (last)
br1.reset();                 // back to C
char c2 = (char) br1.read(); // C again
br1.close();

Sense-check: The output sequence of characters read is W (step 1), C, E, C — with O and M intentionally skipped and the E after the skip being the last character, not the second one. mark/reset lets you peek ahead and re-read without reopening.

What readLine() would do here: If hello.txt contained WELCOME\n, a single br1.readLine() would return "WELCOME" (no newline) in one call, discarding the buffering detail — the manual read/mark/skip walk is only to teach navigation.

19.7.5 Student Questions and Answers

Q: Does mark work only for files?

A: No. mark, reset, and skip are facilities offered by BufferedReader itself, regardless of the underlying source. You can mark a keyboard-decoded stream (new InputStreamReader(System.in)), a file stream (FileReader), or a socket stream (InputStreamReader(socket.getInputStream())) and later return to the mark within the read-ahead limit. For file streams the effect is easy to demonstrate with a stable word like WELCOME, but the mechanism is general — it operates on the buffer, not on the disk.

In practice: Mark/reset is rarely used on keyboard streams because line-buffered console input is usually consumed immediately; it shines in parsing tasks where you want to look ahead a few tokens and backtrack (for example detecting whether a number is followed by a unit).

Related subtlety: markSupported() returns true for BufferedReader but false for a raw FileInputStream; that is why you mark the BufferedReader, not the underlying byte stream.

Recap: BufferedReader buffers chunk reads and wraps any Reader. Wire keyboard as new BufferedReader(new InputStreamReader(System.in)), sockets by swapping the byte source, and files as new BufferedReader(new FileReader("...")). The method set — read()/readLine()/read(cbuf)/mark(limit)/reset()/skip(n)/close() — covers single-char, line, bulk, navigational, and lifecycle needs.

Bridge: Reading has its buffered, navigable tool. The mirror for writing formatted text lines — with the complementary construction twist that it needs a byte bridge — is PrintWriter in 19.8, where exception handling and closing discipline become explicit.

Exam note: Write both the keyboard wrapper new BufferedReader(new InputStreamReader(System.in)) and the file wrapper new BufferedReader(new FileReader("...")), and walk read/mark at C/skip(2) to E/reset to C/close on hello.txtWELCOME.

19.8 PrintWriter Class — Writing to Text Files with Exception Handling

19.8.1 Purpose and Relation to System.out

Hook: You already know System.out.println("hi") prints to the monitor. What if the same line could print to a file with one swapped name?

Intuition — two printers, one language: Think of System.out as a printer hard-wired to the screen, and a PrintWriter as the same printer model but with its cable replugged to a text file. The buttons are identical — print, println, printf — only the destination changes. Learning one set of buttons teaches the other.

Where it breaks: System.out auto-flushes and lives for the whole program. A file-bound PrintWriter buffers, must be closed, can throw file-creation errors, and overwrites existing content by default — consequences the screen never has.

Formalize — PrintWriter as the text-file writing screen

PrintWriter is the character-based screen class in java.io for writing human-readable text to a file (or to any OutputStream). While BufferedReader is the preferred class for traditional reading, PrintWriter is the preferred class for writing lines to files because it mirrors the familiar System.out API: print(...), println(...), printf(...)/format(...).

Relationship to System.out:

  • System.out is a PrintStream (a byte stream) connected to the monitor, available globally as System.out.
  • PrintWriter wrapping a FileOutputStream is the analogous screen connected to a text file: PrintWriter p1 = new PrintWriter(new FileOutputStream("a.txt")); makes p1.println("hi") write "hi" plus newline to a.txt just as System.out.println("hi") writes to the screen.

A PrintWriter gives you formatting for free: p1.println(42), p1.printf("score %d", 95), p1.print(true) all work because overloads call String.valueOf / toString internally.

All file I-O classes including BufferedReader, PrintWriter, FileInputStream, FileOutputStream live in java.io. They are not implicitly imported; you must import them:

import java.io.PrintWriter;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;

The first brings the writer, the second the file-connecting bridge (explained in 19.8.2), the third the exception type thrown when the file cannot be created or found.

19.8.2 Why FileOutputStream Is Needed

The two-piece bridge — because PrintWriter has no direct file-name constructor in the form taught here

Unlike some readers where you can pass a file name directly to FileReader("name.txt"), the PrintWriter construction taught in this lecture has no single-argument file-name constructor. It needs a stream object that already points to the file. FileOutputStream does exactly that job: it converts a file-name string into a byte stream object that points to that file, handling OS-level opening/creation. Then PrintWriter wraps that byte stream and adds character-mode print/println.

Canonical syntax in this course:

PrintWriter outputStream = new PrintWriter(new FileOutputStream("stuff.txt"));

Breakdown:

  • new FileOutputStream("stuff.txt") — takes the String file name, asks the OS to open or create stuff.txt, returns an anonymous FileOutputStream (a byte OutputStream) aimed at that file. Can throw FileNotFoundException if the path is invalid (for example a non-existent directory with no permission to create).
  • new PrintWriter(...) — takes that anonymous OutputStream and wraps it. Inside it builds an OutputStreamWriter (bytes→chars) automatically. The name outputStream (of type PrintWriter) is now connected to stuff.txt through the two-layer pipe:
"stuff.txt" (disk) ← FileOutputStream (bytes) ← PrintWriter (chars + formatting) ← your code: outputStream.println(...)

Modern convenience note: The standard library now also offers new PrintWriter(String fileName) and new PrintWriter(File file) that internally create the FileOutputStream for you. The two-step form is kept in this course because it makes the bridge explicit and matches the exam's expected answer. Either form is correct in production when you control the JDK.

19.8.3 Opening Behaviour and Writing Methods

The process of connecting a stream to a file is called opening the file. With the two-step line above the file is opened in the background and linked to outputStream eagerly — no separate open() call exists.

  • If stuff.txt already exists, opening for writing in this mode truncates it: the old contents are lost because you opened with the default non-append constructor. This is desirable for a fresh report but dangerous for logs — to preserve existing content you would add the append flag: new FileOutputStream("stuff.txt", true) then wrap.
  • If stuff.txt does not exist, a new empty file named stuff.txt is created in the target directory (or in the current directory if no path was given), provided the parent directory exists and the OS permits creation. If the parent directory is missing or the name contains illegal characters, FileNotFoundException is raised.

Once opened, the file is ready to receive data:

outputStream.print("value=");   // no newline
outputStream.println(42);       // value plus platform line break
outputStream.printf("pi=%.2f%n", 3.14159); // formatted
outputStream.flush();           // push buffered chars to disk eagerly

print versus println matters for verification: the demo "The quick brown fox" followed by println then "jump over the lazy dog" yields two lines; replacing println with print would concatenate them on one line, which would be marked wrong on a file-content check.

19.8.4 Exception Handling with try and catch

What an exception is in the file context

An exception is a runtime error that causes abnormal termination if not handled. The program compiles without error, but at runtime a problem arises that interrupts normal control flow. File-specific triggers include:

  • Arithmetic: int x = 5/0;ArithmeticException.
  • User data: parsing "abc" as intInputMismatchException/NumberFormatException.
  • File: new FileOutputStream("::illegal") or a missing parent directory → FileNotFoundException; trying to read a file that was deleted between exists() and open()FileNotFoundException; I/O failure mid-stream → broader IOException.

FileNotFoundException is a subclass of IOException. Because a file is a separate entity outside the program, it may be missing or unreachable precisely when you try to open it, so the compiler requires you to either handle or declare the exception.

The required handling pattern — declare outside, open inside try:

PrintWriter outputStream = null; // ① declared outside so visible in try, catch, and after
try {
    outputStream = new PrintWriter(new FileOutputStream("stuff.txt")); // ② open attempt
} catch (FileNotFoundException e) {
    System.err.println("Error opening the file stuff.txt"); // ③ handler
    // optionally: System.exit(0); or return; or rethrow
}
// ④ only after success do you write
if (outputStream != null) {
    outputStream.println("The quick brown fox");
}

Why each piece matters:

  • Declare null outside try. If you write try { PrintWriter outputStream = new PrintWriter(...); } then outputStream is scoped only inside try and cannot be used to write after it. Declaring outside keeps it visible to the write and close statements.
  • Inside try initialize. This is the faulting point. On success the link is made. On failure control jumps immediately to catch without executing remaining try lines.
  • catch (FileNotFoundException e). Catches the specific open-failure. A common real variant catches IOException e to also handle generic I/O problems; both are acceptable as long as FileNotFoundException is covered. Report to System.err (conventional for errors) or System.out; the lecture accepts either but System.err signals intent.
  • Program continues instead of crashing. Without the try/catch, the JVM would print a stack trace and terminate. With it you choose recovery: print a message, create an alternate file, or exit gracefully with System.exit(0) as the demo does.

Modern alternative: try (PrintWriter outputStream = new PrintWriter(new FileOutputStream("stuff.txt"))) { ... } — try-with-resources (Java 7+) declares, opens, and auto-closes in one line; it implicitly requires AutoCloseable (which PrintWriter implements). The demo keeps the classic form because the single-point-of-closure discipline is an exam focus.

Assumptions and scope: Exception handling shown covers the open failure. Failures while writing (disk full, file deleted mid-write) surface as I/O errors that the try block can extend to cover. FileNotFoundException for writing is somewhat misleadingly named — it really means "file could not be opened for writing", which includes creation failures, not just missing files.

19.8.5 Worked Example — TextFileOutputDemo

Complete class as taught, annotated:

import java.io.PrintWriter;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;

public class TextFileOutputDemo {
    public static void main(String[] args) {
        PrintWriter outputStream = null;
        try {
            outputStream = new PrintWriter(new FileOutputStream("stuff.txt"));
        } catch (FileNotFoundException e) {
            System.err.println("Error opening the file stuff.txt");
            System.exit(0); // graceful termination after reporting
        }
        outputStream.println("The quick brown fox");
        outputStream.println("jump over the lazy dog");
        outputStream.close();
    }
}

Line-by-line trace — success path:

  1. Three imports: PrintWriter (writer), FileOutputStream (byte bridge), FileNotFoundException (open failure signal).
  2. public class TextFileOutputDemo with public static void main(String[] args) entry point.
  3. PrintWriter outputStream = null; — declare outside try so every later block sees it.
  4. Enter try { and execute outputStream = new PrintWriter(new FileOutputStream("stuff.txt")); — JVM asks OS to open/create stuff.txt via FileOutputStream, then PrintWriter wraps it. If stuff.txt did not exist, the OS creates it empty at this instant.
  5. Open path forks: if the open throws FileNotFoundException (illegal name, missing parent, permission denied), control jumps to catch which prints Error opening the file stuff.txt and calls System.exit(0) — program ends without touching the filesystem further.
  6. Success: stuff.txt is now open in the background through the two-layer pipe outputStream → FileOutputStream → stuff.txt.
  7. outputStream.println("The quick brown fox"); — writes 19 chars plus the platform line separator into the buffer.
  8. outputStream.println("jump over the lazy dog"); — writes 19 chars plus newline. Because println was used, the two strings sit on separate lines. Replacing with print would yield The quick brown foxjump over the lazy dog on one line — a common grading trip.
  9. Resulting file stuff.txt contains exactly:
The quick brown fox
jump over the lazy dog

Two lines, each terminated. flush() happens implicitly on close().

  1. outputStream.close(); — flushes buffered data to disk, releases the file handle and OS resources. If close() happened inside try-with-resources, it would be automatic.

Failure variant — missing parent directory: Change the name to "noSuchDir/stuff.txt" where directory noSuchDir does not exist. Step 4 now throws FileNotFoundException, catch prints the error, and no file is created nor overwritten.

Sense-check: The two println strings are deliberately lower-case with a single space between words; verify no extra trailing spaces when grading file content.

19.8.6 Closing the File

Closing is not optional style — it is resource management. When a program has finished writing it should close the stream connected to that file.

  • What close() does: flushes any buffered characters, writes them to the OS, closes the underlying FileOutputStream and its file descriptor, and marks the PrintWriter as closed so further writes raise errors.
  • What forgetting does: If the program ends without an explicit close(), the JVM will eventually close the stream on process termination, but buffered data that has not been flushed may be lost, and on some OSes the file remains locked until exit, blocking deletion or rename. Explicit close() makes intent clear and frees file handles promptly, enabling the OS to reclaim memory and allow other processes to access the file.
  • Idiom: outputStream.close(); after the last write. Guard with if (outputStream != null) outputStream.close(); when not using try-with-resources, to avoid NullPointerException when the open failed. With try-with-resources: try (PrintWriter outputStream = new PrintWriter(new FileOutputStream("stuff.txt"))) { outputStream.println(...); } — no explicit close() needed.
  • Exception-aware variant: close() itself can throw IOException on the underlying stream (disk full while flushing). Production code often uses try-with-resources precisely because it suppresses a close-time exception correctly instead of hiding the first exception.

Pitfalls:

  • Closing only in the success path. If catch calls System.exit(0) without close(), that is fine because nothing was opened; but if you recover and continue writing, ensure the successfully opened stream is eventually closed in a finally or via try-with-resources.
  • Forgetting to flush() before reading the same file you just wrote. Without close()/flush(), a companion reader may see stale content.
  • Assuming overwrite is always desired. The taught constructor overwrites. For append semantics use new FileOutputStream("stuff.txt", true) inside the PrintWriter.

Recap: PrintWriter is System.out replugged to a text file via the FileOutputStream byte bridge: new PrintWriter(new FileOutputStream("stuff.txt")). Opening overwrites an existing file or creates a new one; print/println/printf write formatted text. File opens must be protected by PrintWriter outputStream = null; try { outputStream = new PrintWriter(new FileOutputStream(...)); } catch (FileNotFoundException e) { ... } — declare outside so writes can see it — and close() (or try-with-resources) after use.

Bridge: You can write lines with PrintWriter; the symmetric read-back with quick token parsing is the same Scanner you used for the keyboard, now pointed at a file — simply swapping System.in for new FileInputStream("..."), covered in 19.9.

Exam note: Expect to write the three imports, the null declaration, the try with the two-step new PrintWriter(new FileOutputStream("stuff.txt")), the catch (FileNotFoundException e) with error print, and an explicit close() — plus the print versus println line-break distinction.

19.9 Scanner for File Input — Reading Files with FileInputStream

19.9.1 From Keyboard to File by Swapping the Stream

Hook: If Scanner already turns keyboard keystrokes into int and String, can the same few lines turn file characters into the same types without learning a new API?

Intuition — one translator, two phone lines: Think of Scanner as a human translator who is fluent in typed tokens. Plug the translator's headset into the keyboard line (System.in) and they translate keystrokes. Unplug and replug into a file line (FileInputStream("morestuff.txt")) and the same translator, with the same methods nextInt()/nextLine(), now translates file characters. The translator did not change — only the cable did.

Where it breaks: Files have a fixed length and an EOF, while the keyboard is potentially infinite and line-buffered. That is why file reads throw FileNotFoundException at open and return sentinel values or NoSuchElementException at exhaustion, while keyboard reads block waiting for the next line.

Formalize — swapping the source screen

The Scanner pattern for the keyboard wraps System.in:

Scanner sc = new Scanner(System.in); // keyboard line

The same Scanner can read from a text file by swapping that screen for a file byte stream built from the file name via FileInputStream:

Scanner inputStream = new Scanner(new FileInputStream("morestuff.txt"));

Breakdown:

  • new FileInputStream("morestuff.txt") — byte InputStream that opens morestuff.txt for reading (throws FileNotFoundException if the file is absent or unreadable). This is the file analogue of System.in — a byte source.
  • new Scanner(...) — wraps whatever InputStream you give it with decoding + tokenization. The Scanner constructor accepts any InputStream, any File, any Path, or a String source. Here the source happens to be a file stream.
  • inputStream — the Scanner object name, now linked through that screen to the file rather than the keyboard. After this association, any Scanner method (next(), nextInt(), nextLine(), next().charAt(0), nextFloat()) pulls from morestuff.txt.

You still need imports, and note the packages:

import java.util.Scanner;              // java.util — the translator
import java.io.FileInputStream;        // java.io — the file byte pipe
import java.io.FileNotFoundException;  // java.io — checked exception on open

Alternative convenience form: new Scanner(new File("morestuff.txt")) or new Scanner(Paths.get("morestuff.txt")) internally opens the same file input stream. The new FileInputStream form is taught in this course because it makes the byte-stream bridge explicit, matching the PrintWriter/FileOutputStream pair in 19.8. For character-set control you might instead write new Scanner(new FileInputStream("morestuff.txt"), "UTF-8").

Unified stream picture:

keyboard:   Scanner(InputStream = System.in)               → nextInt/nextLine
file:       Scanner(InputStream = new FileInputStream(f))   → same nextInt/nextLine
socket:     Scanner(InputStream = socket.getInputStream())  → same nextInt/nextLine

One API, three cables.

Scope and assumptions: FileInputStream is a byte stream; Scanner decodes bytes to characters using either the platform default charset or an explicit charset you supply. If the file is not plain text (for example a binary image), Scanner tokenization will produce garbage — use FileInputStream raw bytes instead. The file path follows the same File rules from 19.5: "morestuff.txt" alone means "in the current directory", "/data/morestuff.txt" is absolute.

19.9.2 Methods and Worked Example — morestuff.txt

All the familiar Scanner methods now work against the file byte-for-byte as they did for the keyboard: next() (token to next whitespace), nextLine() (rest of line without newline), nextInt() (next token parsed as int), nextFloat(), nextDouble(), next().charAt(0) for a character, hasNextInt() for probing. No method name changes — only the data source changes at construction.

Full example — TextFileScannerDemo (as taught, with token-by-token trace)

import java.util.Scanner;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class TextFileScannerDemo {
    public static void main(String[] args) {
        System.out.println("I will read three numbers and a line of text from the file morestuff.txt");
        Scanner inputStream = null;
        try {
            inputStream = new Scanner(new FileInputStream("morestuff.txt"));
        } catch (FileNotFoundException e) {
            System.err.println("Error opening the file morestuff.txt");
            System.exit(0);
        }
        int n1 = inputStream.nextInt();
        int n2 = inputStream.nextInt();
        int n3 = inputStream.nextInt();
        String line = inputStream.nextLine(); // consumes leftover newline after 3
        line = inputStream.nextLine();        // reads the real text line
        System.out.println("The three numbers read from the file are " + n1 + ", " + n2 + ", " + n3);
        System.out.println("The line read from the file is: " + line);
        inputStream.close();
    }
}

A simpler textbook version reads exactly n1 n2 n3 then line with the same two-nextLine dance:

int n1 = inputStream.nextInt();
int n2 = inputStream.nextInt();
int n3 = inputStream.nextInt();
String line = inputStream.nextLine(); // eat remainder of numbers line (often empty)
line = inputStream.nextLine();        // harvest the sentence on the next line

File content used in the session — morestuff.txt:

1 2 3
He is a jolly good fellow

In some walk-throughs a variant with 1 2 3 4 on the first line appears; the taught pattern of three nextInt calls simply ignores any fourth token.

Trace for the file above (numbers and sentence):

  1. System.out.println(...) prints the preamble I will read three numbers... to the monitor via the System.out output screen.
  2. Scanner inputStream = null; declared outside try so it is visible to catch and to later reads.
  3. Inside try, inputStream = new Scanner(new FileInputStream("morestuff.txt")); attempts to open morestuff.txt. On success the OS links the file through a FileInputStream byte pipe into the Scanner's buffer; on failure FileNotFoundException is caught and Error opening the file morestuff.txt is printed.
  4. inputStream.nextInt() parses characters 1n1 = 1. The scanner's cursor moves past 1 but the space and newline handling is internal.
  5. inputStream.nextInt()n2 = 2.
  6. inputStream.nextInt()n3 = 3. Cursor now sits just after 3, immediately before the line break that ends the numbers line.
  7. inputStream.nextLine() — the classic newline-consumer. After the last nextInt(), the line break after 3 is still pending (because nextInt() stops at the delimiter but does not consume the line separator). This call consumes that remainder and returns "" (empty string), which is discarded. Without this extra call, the next nextLine() would harvest only that empty remainder and you would lose the sentence.
  8. inputStream.nextLine() reads "He is a jolly good fellow" (without the line break) into line.
  9. System.out.println("The three numbers... 1, 2, 3") and System.out.println("The line read... He is a jolly good fellow") display the results.
  10. inputStream.close(); closes both the Scanner and the underlying FileInputStream, releasing the file handle. In modern code this would be try (Scanner inputStream = new Scanner(new FileInputStream("morestuff.txt"))) { ... }.

Output produced (exact console lines):

I will read three numbers and a line of text from the file morestuff.txt
The three numbers read from the file are 1, 2, 3
The line read from the file is: He is a jolly good fellow

Sense-check on tokenization: If morestuff.txt instead held 1 2 3 4 on the first line, steps 4–6 would still give 1, 2, 3; the 4 would be left as the first token of the not-yet-consumed remainder, and the first nextLine() would return " 4" (the rest of that line). That is why you must know the file's exact layout when mixing nextInt() and nextLine().

Close rule mirrored from PrintWriter: Closing the Scanner also closes the underlying FileInputStream (one close handles both layers), mirroring that PrintWriter.close() flushes and closes its FileOutputStream. Forgetting close() may lock the file on Windows until the JVM exits.

Pitfalls:

  • Forgetting the dummy nextLine() after nextInt() and wondering why the sentence comes back empty. The dangling newline is the cause every time.
  • Declaring Scanner inputStream = new Scanner(...) inside try without a null outer declaration. Then inputStream.nextInt() after the block is out of scope and fails to compile — the classic exam trap shared with PrintWriter.
  • Using next() when the sentence contains spaces. next() would yield only He; nextLine() is needed for "He is a jolly good fellow" with spaces kept.
  • Not catching FileNotFoundException. new FileInputStream("morestuff.txt") is a checked exception site; the compiler requires either a try/catch or a throws declaration. The exam requires the try/catch form with null initialization.

19.9.3 Student Questions and Answers

Q: System.out and System.in — do they belong to the same package and how do they relate to screens?

A: Both are static members of java.lang.System. java.lang is the default package implicitly imported in every file, so System.out and System.in are always known without an import. Their roles as screens are opposite:

  • System.out (type PrintStream, byte output stream) is the output screen connected by default to the console/monitor. Calling System.out.println("text") sends characters through that output screen to the display.
  • System.in (type InputStream, byte input stream) is the input screen connected by default to the keyboard. Calling new Scanner(System.in) wraps that keyboard byte screen with a Scanner so that next(), nextInt(), and nextLine() can turn keystrokes into typed values.

Import rule for Scanner work: The channel System.in itself needs no import, but the translator Scanner lives in java.util, so import java.util.Scanner; is required before new Scanner(...) compiles. The input method new BufferedReader(new InputStreamReader(System.in)) similarly needs java.io imports. For file work, System.in is not involved at all — the file pipe new FileInputStream("morestuff.txt") takes its place under the same Scanner wrapper.

Q: If I can use Scanner for both keyboard and file, why learn BufferedReader and PrintWriter at all?

A: Three complementary sweet spots, not three replacements:

  • Scanner is the quick token parser — ideal when your input is a sequence of typed tokens (int, float, words, lines) and you want nextInt()/nextLine() convenience for both keyboard and file by swapping one constructor argument. It hides tokenization but pays a modest parsing cost.
  • BufferedReader is the efficient line and navigation reader — buffered block reads, readLine() for large lines, plus mark/reset/skip for look-ahead and re-read within a readAheadLimit, and it works directly with any character Reader. It does not parse integers for you, but it is faster for bulk text and gives precise cursor control that Scanner lacks.
  • PrintWriter is the text writer mirror — print/println/printf to text files with automatic OutputStreamWriter bridging, automatic file creation or overwrite handling through FileOutputStream, and flush/close lifecycle. Scanner cannot write.

Together they cover the common pipeline stages: BufferedReader for traditional fast text reading with navigation, PrintWriter for reliable text writing, and Scanner for flexible token reading from any source by swapping the underlying stream. Exam questions, production code reviews, and library selection expect you to reach for the tool that matches the stage, not force one tool everywhere.

Recap: File input with Scanner reuses the keyboard pattern by swapping one constructor argument: new Scanner(new FileInputStream("morestuff.txt")) replaces new Scanner(System.in). Imports java.util.Scanner + java.io.FileInputStream + java.io.FileNotFoundException, the null-outside / open-inside try/catch guard, and nextInt()×3 plus the double nextLine() newline dance for morestuff.txt1,2,3 and "He is a jolly good fellow" are the canonical workflow.

Bridge: You have now moved bytes with equal ease from keyboard and from file — and seen why buffered line readers and formatted writers earn their separate places.

Exam note: Be able to swap System.in for new FileInputStream("morestuff.txt") inside new Scanner(...), reproduce the try/catch (FileNotFoundException e) with null initialization, explain the extra nextLine() needed after nextInt(), and predict the console output for the given morestuff.txt content. Always close() the Scanner after use.

Exam Guidance Summary

The professor did not publish a mark distribution, did not state which of the nine blocks carries the most marks, and did not list omitted topics or a makeup-exam exclusion list. The guidance below is distilled from repeated verbal emphasis and from the worked demos that were flagged as "expect this" in class.

Exam note — Packages (19.1–19.3): Draw the directory tree from a folder picture, write package myPackage.myPackageA; as the very first line (explain the single-package rule: one package per file because one class lives in one folder), show compilation to S1.class in the matching folder with javac -d ., and contrast import pkg.Class versus import pkg.*; (star = one package level only). Any question showing import ABC.*; import IJK.*; plus A a1 = new A(); where both packages define A is an ambiguity trap — fix it by fully qualifying one or both uses: myPackage.myPackageA.ABC.A a1 = new myPackage.myPackageA.ABC.A();.

Exam note — Scanner keyboard (19.4): Reproduce import java.util.Scanner; then Scanner sc = new Scanner(System.in);. Match method to type: nextLine() keeps spaces, next() stops at whitespace, next().charAt(0) for a single character (no direct nextChar), nextInt() for int, nextFloat()/nextDouble() for decimals. Mention the nextInt()→ dangling newline before nextLine() and the hasNextInt() probe.

Exam note — File attributes (19.5): Open with "File works on attributes, not content". Distinguish absolute (C:/data/first.java from root) versus relative (data/first.java from program's directory) paths and the default "same directory as program" when only a name is given. Name the three constructors — File(String pathname), File(String parent, String child), File(File parent, String child) — and when each applies. List attribute methods getName()/getParent()/getPath()/length()/lastModified() and Boolean probes canRead()/canWrite()/isHidden()/exists()/isFile()/isDirectory()/isAbsolute(), and explain null parent when only the file name was supplied and isAbsolute() false vs true.

Exam note — Streams (19.6): Define stream as a one-way virtual pipeline/screen between program and device/file through RAM. Classify into reader/input (keyboard → Scanner(System.in), file → BufferedReader/FileReader, socket → socket.getInputStream()) and writer/output (monitor → System.out.println, file → PrintWriter/FileOutputStream). Emphasize uniform read()/write() API hiding device detail.

Exam note — BufferedReader (19.7): Write the two wrappers from memory: keyboard new BufferedReader(new InputStreamReader(System.in)) and file new BufferedReader(new FileReader("ABC.txt")) (or the FileInputStream+InputStreamReader charset-safe form). Demonstrate the WELCOME walk: read() at W, mark(100) at C, read() C, skip(2) over O M to E, read() E, reset() to C, read() C again, then close().

Exam note — PrintWriter (19.8): List the three imports PrintWriter/FileOutputStream/FileNotFoundException, give the two-step open new PrintWriter(new FileOutputStream("stuff.txt")), state overwrite-if-exists versus create-if-missing and the append variant FileOutputStream(name, true), contrast println (adds line break) versus print (same line), declare PrintWriter outputStream = null; outside try, initialize inside try, catch FileNotFoundException with System.err.println("Error opening ..."), and close explicitly with outputStream.close(); (or try-with-resources). Forgetting outer null declaration or close is a common mark loss.

Exam note — Scanner for file (19.9): Reproduce the cable swap: new Scanner(new FileInputStream("morestuff.txt")) replacing System.in, keep the same imports plus FileNotFoundException, protect with the outer-null / inner-try guard, read 1 2 3 with nextInt()×3 then handle the dangling newline with nextLine() dummy before the real nextLine() for "He is a jolly good fellow", and print both:

The three numbers read from the file are 1, 2, 3
The line read from the file is: He is a jolly good fellow

Plus the discipline reminder.

In every file or stream answer: include the try { open } catch (FileNotFoundException e) { System.err.println(...); } block, declare the stream variable as null outside try so it is visible for later writes and for close(), and close explicitly after use — mention that explicit close() is good practice even though the JVM will close on process exit, because it flushes buffers, releases file handles, and signals intent.

Key Industry Applications

Modular Java projects and libraries: Packages are the foundation of every industrial Java codebase — from the JDK itself to Spring, Hibernate, and Android SDKs. Teams organize functionally related classes into their own packages (for example com.company.billing, com.company.shipping) so short names stay ergonomic inside a feature area while fully qualified names keep builds, JARs, and deployments globally unique. Build tools and IDEs follow the identical folder-to-package mapping during compilation and packaging; CI pipelines fail the build if the declared package does not match the directory.

Standard library design: The fully qualified names java.lang.String, java.util.Arrays, java.io.BufferedReader, and java.util.Date (now supplemented by java.time) exemplify how the JDK exposes hundreds of classes without clashes. java.lang.* is implicitly imported, every other package is explicit — the same trade-off you make in your own projects.

Console tools and quick input: Scanner with System.in remains the standard quick-input helper for CLI utilities, competitive-programming harnesses, student projects, debugging stubs, and scripting glue where typed console reads (nextInt(), nextLine()) are needed and performance is not the bottleneck.

File attribute inspection: File attribute checks — exists()/canRead()/canWrite()/isAbsolute()/isHidden()/length()/lastModified() — are used by installers, backup and sync tools, log rotators, file browsers, and security scanners to inspect files before opening content. A build script probes exists() and isDirectory() before listing, an uploader checks canRead() and length() before streaming.

Stream architecture: Streams as virtual pipelines underpin every Java I-O operation — keyboard input, monitor output, file access, and network sockets. Web servers, message queues, ETL ingestion, and file transfer frameworks all program against the abstract InputStream/OutputStream/Reader/Writer interfaces and swap a FileInputStream for a Socket input stream without changing business logic. InputStreamReader/OutputStreamWriter provide the charset bridge that makes internationalized text pipelines reliable.

Buffered line reading and navigation: BufferedReader wrapping InputStreamReader over System.in or over socket.getInputStream() supports interactive console applications, telnet-style clients, and socket-based network readers where buffered readLine() plus mark()/reset()/skip() let a parser peek ahead and backtrack. File readers benefit from chunk buffering that reduces disk I/O.

Text file writing and reporting: PrintWriter wrapped over FileOutputStream is the conventional path for producing text reports, application logs, CSV and JSON dumps. Its print/println/printf mirror System.out but target files, which simplifies formatted output and keeps internationalization through Writer. Logback, CSV printers, and report generators all sit on this pattern, adding append mode FileOutputStream(name, true) for rolling logs.

Structured file ingestion: Scanner over FileInputStream (or directly over File/Path) is the go-to for ingesting structured text files — configuration files, test data dumps holding numbers and sentences, grading datasets — using the same token methods nextInt()/nextLine() mastered for keyboard input after swapping the underlying file stream. The pattern generalizes to any delimited text without rewriting parsing code.

OODAP Lecture 19 notes · Packages, Input-Output Streams and File Handling in Java

Object Oriented Design, Analysis and Programming Architecture· postgraduate· 2026-08-19

Sections Breakdown

119.1 Packages — Grouping, Directory Correspondence and Namespace Control

Package as folder-backed namespace that groups related classes, mirrors directories with dots, and prevents name clashes via fully qualified names.

219.2 Creating Packages — The package Keyword and Directory Building

package declaration as single-home rule; folder hierarchy built or verified via compiler and reproduced in the MyPackage tree.

319.3 Importing Packages and Resolving Name Ambiguity

import as per-file address book; star vs single-class import and fully qualified fix for ambiguous A in ABC vs IJK.

419.4 Scanner Class — Quick Keyboard Input

Scanner over System.in as typed translator; typed read methods and the next vs nextLine vs charAt distinction.

519.5 File Class — Attributes, Paths and Constructors

File as metadata handle with absolute/relative paths, three constructor forms, attribute and Boolean probes with null-parent case.

619.6 Streams — Virtual Pipelines for Reading and Writing

Stream as one-way virtual hose through RAM; byte vs character hierarchies and input vs output direction unifying keyboard, file, socket.

719.7 BufferedReader Class — Reading with a Buffer

BufferedReader buffering and wrapping pattern for keyboard, socket, and file with read/mark/skip/reset navigation on hello.txt WELCOME.

819.8 PrintWriter Class — Writing to Text Files with Exception Handling

PrintWriter as screen replugged to file via FileOutputStream bridge with overwrite/create semantics and classic try-catch close discipline.

919.9 Scanner for File Input — Reading Files with FileInputStream

Swapping System.in for FileInputStream to reuse Scanner token parsing; try-catch open and nextInt/nextLine dance for morestuff.txt.

10Exam Guidance Summary

Distilled exam focus for each block without mark distribution, emphasizing form and common traps.

11Key Industry Applications

Industrial mapping of each concept to real tooling: modular packages, Scanner tools, File probes, stream frameworks.

Postgraduate students in Object Oriented Design, Analysis and Programming

Exam Revision Notes

Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.

Packages -- Grouping, Directory Correspondence and Namespace Control

Must-know: Dots mirror directory depth; fully qualified name is globally unique; star imports only one package level.

Top pitfall: Star import does not import subpackages; default package cannot be imported.

Self-check: Translate dir1/dir2/c.java to an import statement.

Connects to: 19.2, 19.3

Creating Packages -- The package Keyword and Directory Building

Must-know: package must be first line, one per file; dots = depth; compiler can create folders with -d.

Top pitfall: Mismatched package name and folder case breaks runtime.

Self-check: Write the first line for a class that belongs to myPackage.myPackageA.ABC.

Connects to: 19.1, 19.3

Importing Packages and Resolving Name Ambiguity

Must-know: Import is per-file convenience; ambiguous bare name fixed by fully qualified use.

Top pitfall: Thinking import loads code; it only expands short names at compile time.

Self-check: Two star imports both contain A: what error and fix?

Connects to: 19.2, 19.4

Scanner Class -- Quick Keyboard Input

Must-know: import java.util.Scanner; sc = new Scanner(System.in); choose nextLine vs next vs nextInt correctly.

Top pitfall: Forgetting dummy nextLine after nextInt leaves empty string.

Self-check: How to read a single character with Scanner?

Connects to: 19.6, 19.9

File Class -- Attributes, Paths and Constructors

Must-know: File handles attributes not content; three constructors, absolute vs relative, null parent, Boolean checks.

Top pitfall: Thinking new File creates a file on disk.

Self-check: What does getParent() return for new File("first.java")?

Connects to: 19.6, 19.8

Streams -- Virtual Pipelines for Reading and Writing

Must-know: Stream is one-way pipe via RAM; byte vs char and input vs output classification; System.in/out are prewired streams.

Top pitfall: Using byte stream for non-ASCII text corrupts characters.

Self-check: Classify Scanner(System.in) and System.out.println directionally.

Connects to: 19.7, 19.8, 19.9

BufferedReader Class -- Reading with a Buffer

Must-know: Keyboard wrapper vs file wrapper; WELCOME trace mark at C skip 2 to E reset to C.

Top pitfall: Mark invalidated after readAheadLimit exceeded; read returns int -1 at EOF.

Self-check: Write both BufferedReader constructors for keyboard and file.

Connects to: 19.6, 19.8

PrintWriter Class -- Writing to Text Files with Exception Handling

Must-know: Two-step open, overwrite vs create, null outside try, catch FileNotFoundException, close explicitly, println vs print.

Top pitfall: Declaring PrintWriter inside try makes it invisible for close.

Self-check: Why FileOutputStream is needed for PrintWriter in this course?

Connects to: 19.5, 19.9

Scanner for File Input -- Reading Files with FileInputStream

Must-know: Replace System.in with FileInputStream; dummy nextLine after nextInt; close Scanner.

Top pitfall: Missing extra nextLine after nextInt yields empty sentence.

Self-check: Trace morestuff.txt 1 2 3 + He is... through Scanner reads.

Connects to: 19.4, 19.6

Exam Guidance Summary

Must-know: Every file/stream answer needs try-catch with null outside and explicit close.

Top pitfall: Forgetting close or outer null declaration.

Self-check: What must accompany every FileInputStream/PrintWriter open?

Connects to: 19.1, 19.9

Key Industry Applications

Must-know: Packages modularize projects; File checks gate I/O; streams unify devices; PrintWriter/Scanner are complementary stages.

Top pitfall: Forcing one I/O class for all tasks instead of matching tool to stage.

Self-check: Name one industry use per I/O class.

Connects to: 19.1, 19.6

Was this lecture useful?

Loading comments…
🤖

BitsNotes AI Assistant

Subject Notes Assistant

Configure AI Chat

Choose how to access the chatbot
Have your own API key?

Switch to "Bring Your Own Key" tab above for unlimited access with any OpenAI-compatible provider.

🔑 Enter API key above to fetch live models from provider, or enter model name manually.
OpenAI-Compatible API Support

Choose any provider preset (Gemini, DeepSeek, Kimi, GLM, MiniMax, Qwen, OpenAI, Groq, Ollama, etc.) or enter a custom endpoint URL.

Security & Privacy First

Your API key is sent directly from your browser to your specified provider. BitsNotes servers never store or see your key.