Skip to main content
Object Oriented Design, Analysis and Programming

Review and Revision of Object Oriented Programming Fundamentals

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

  • Bytecode and the Way a Java Program Executes — covered in Lecture 16
  • Components of the Java Execution Environment — JDK, JRE, JVM and JIT — covered in Lecture 16
  • Classes — The Blueprint Idea — covered in Lecture 16
  • Objects and Instances — covered in Lecture 16
  • Constructors — Purpose, Rules and Automatic Invocation — covered in Lecture 17
  • Static Variables — Class Variables — covered in Lecture 17
  • Static Blocks — covered in Lecture 17
  • The this Keyword — Reference to the Current Object — covered in Lecture 17
  • The final Keyword and Immutable Classes — covered in Lecture 17
  • Arrays — Fundamentals, Declaration, Initialization and Access — covered in Lecture 18
  • The Arrays Utility Class — covered in Lecture 18
  • Strings in Java — Immutability, the String Constant Pool, and Heap-Stack Memory — covered in Lecture 18
  • StringTokenizer — Breaking a String into Tokens — covered in Lecture 18
  • BufferedReader and PrintWriter for File I/O — covered in Lecture 19
  • Scanner for File Input — covered in Lecture 19


This lecture consolidates two hours of review covering Java execution foundations, class and object design, construction and initialization, variable kinds, static members, the this and final keywords, arrays, strings, and file streams, preparing essential concepts for examination.

25.1 Bytecode — The Foundation and Magic of Java

25.1.1 Definition and Nature of Bytecode

A bytecode — the highly optimized set of instructions that the Java runtime system executes — is the direct output of the Java compiler. It is not an executable file in the ordinary sense. When a program is written in a typical language, the source is compiled straight to a native executable tied to one machine. In Java the path is split into two stages. The source file, saved with a .java extension, is compiled by javac into a .class file. That .class file holds bytecode. Bytecode is then taken up by an interpreter, the Java Virtual Machine or JVM, which turns it into actions the underlying hardware and operating system can carry out.

Real-world: This two-stage design is the reason bytecode is often called Java's magic. It is the foundation that lets Java programs move between platforms without recompilation.

Bytecode is described as a highly optimized set of instructions to be executed by the JVM. The class file — for example First.class produced from First.java — is the physical container for bytecode. The JVM reads that container and performs interpretation. The distinction matters: compilation checks syntax and creates bytecode, interpretation creates the final program outcome.

25.1.2 Compilation to Bytecode and Interpretation by the JVM

The workflow from editing to running has a fixed order that is worth memorizing.

  1. Interact with a text editor and write the program.
  2. Save the source as a .java file, for example First.java. Saving with the class name is the recommended practice.
  3. Compile with the Java compiler: javac First.java. During compilation the compiler checks whether the writing respects the language, checks types, checks syntax and checks for typos.
  4. If no error is found, compilation succeeds and creates First.class. That file is bytecode.
  5. Interpret the bytecode with java First. Note that the interpreter is invoked with the class name only, without an extension. The interpreter converts bytecode to hardware and operating system compatibility and the program prints its result, for example hello world.

Exam note: The two files created — .java as source and .class as bytecode — appear in checks of how a Java program is written, compiled, and interpreted. The names javac for the compiler and java for the interpreter are the expected answers.

Think of bytecode as a compact, portable intermediate language. It is not tied to Windows, Linux, or Mac. It is tied only to the JVM specification.

25.1.3 Why Bytecode Matters — Key Intuition

A helpful way to picture bytecode is as a parcel that any post office can deliver as long as the local branch knows the parcel format. Here the parcel is the .class file, and the local branch is the JVM installed on the target machine. Only the JVM needs to be implemented for each platform. Once that is in place, the same bytecode runs everywhere.

Real-world: Portability is the direct payoff. A program created on one machine can be taken as bytecode and executed on another machine with a different operating system, provided a JVM is present on the second machine.

A common way to restate the idea is: translating a Java program into bytecode makes it much easier to run on any platform because only the JVM needs a platform-specific implementation, not every application.

Hook — why does Java need an extra step? Imagine you write a letter in English and want friends in France, Japan, and Brazil to read it without rewriting it three times. You could invent a compact middle language everyone agrees to translate locally. That middle language is bytecode. Java chose an extra stage so one compilation can reach many platforms rather than compiling separately for each CPU and operating system.

Intuition and analogy — the parcel and local post office. The professor's parcel analogy is the right picture to keep. Think of First.class as a parcel packed in a standard box size agreed by all post offices. The parcel itself does not change. Each city's post office — the JVM for Windows, for Linux, for macOS — knows how to open that parcel and deliver it on local streets. The analogy maps: parcel = .class file with bytecode, packing standard = JVM specification, local delivery truck = native machine code and system calls, post office building = installed JVM. Where it breaks: a real post office can hold a parcel for days; the JVM translates and runs bytecode immediately and adds safety checks a post office does not.

Formalized view — two stages, two tools, two artifacts. Stage 1 compilation (javac) reads UTF-8 source text, checks syntax, types, and names, and emits a binary class file that follows the Java class-file format. Stage 2 interpretation and execution (java) loads that class file, verifies bytecode, and either interprets it or hands it to a Just-In-Time compiler that turns hot bytecode into native code on demand.

Artifacts and tools to memorize:

  • Source artifact: First.java — human readable, must match public class name, lives on disk as text.
  • Bytecode artifact: First.class — binary, contains magic header 0xCAFEBABE, version, constant pool, access flags, methods as bytecode instructions such as aload, invokevirtual.
  • Compiler command: javac First.java — produces First.class or prints errors; no .class appears on error.
  • Launcher command: java First — loads First.class from classpath, starts at public static void main(String[]); no extension in the argument.

The JVM itself is the interpreter and runtime: it combines a class loader, a verifier, an execution engine, and a standard library. Different vendors and platforms provide their own JVM builds, but all accept the same bytecode.

Bytecode sits between source and native code. Visualize a flow diagram: left box is First.java on your editor, arrow labeled javac points to middle box First.class (bytecode) with a small badge portable, then fan-out arrows from that middle box to three right boxes labeled Windows JVM, Linux JVM, macOS JVM, each arrow labeled java. The takeaway from the picture is one compile, many runs, with only the right-hand JVMs being platform specific.

Assumptions and scope — when the model holds. Bytecode portability assumes a correct, compatible JVM for the target platform and Java version. A class file built for Java 17 with a class-file version 61 may not run on a Java 8 JVM. Bytecode also assumes the program uses only standard library classes available on that JVM; using a platform-specific native library breaks portability. Bytecode is not native executable: double-clicking First.class does not run it without java. Security and verification also assume the bytecode verifier runs; disabling it would break safety guarantees.

Pitfalls to avoid. A common trap is to say First.class is bytecode and then treat it like an .exe. It is not directly executable by the OS loader. Another is to write java First.class at the command line. The launcher wants the class name, not the file name. A third is to expect recompilation per OS. If you recompile per target, you lose the benefit the design gives. Keep the mental split: compiler errors come from javac, runtime errors come from java.

Recap and bridge. Bytecode is the highly optimized, platform-neutral instruction set in .class files produced by javac and executed by a platform-specific JVM. This split is the foundation for everything in 25.2 portability: once only the JVM is ported, every .class file becomes portable without change. Remember the two commands and the two files, and that compilation checks language while interpretation delivers the result.

Excellent portability is why bytecode is often called Java's magic: it decouples what you ship from where it runs, at the cost of requiring a JVM on every target.

25.2 Portability and Platform Independence

25.2.1 Source to Bytecode to Execution

Portability — the ability to carry a program from one system to another without change — follows directly from the bytecode design. The sequence is:

  • Write Java code and keep it as a source file, for example Program.java.
  • Compile with javac to generate bytecode as a .class file.
  • Carry the .class file to any platform.
  • Execute on Windows, Linux, Mac, or any other platform that hosts a JVM.

That ability to generate code on one machine and execute it on another machine is what makes a Java program portable. The figure used to illustrate this shows a single source branching after compilation into one .class artifact that fans out to multiple operating system targets.

Exam note: Questions about portability often ask what is required to run bytecode on a new platform. The answer is a correctly installed JVM for that platform.

25.2.2 Role of the Java Virtual Machine

The Java Virtual Machine is the interpreter and runtime system that bridges bytecode and the real machine. Whatever platform is involved — Windows, Mac, or Linux — the JVM must be installed there. Once installed, any .class file, which is bytecode, executes successfully because the JVM handles the conversion to hardware-specific instructions and operating system calls.

Real-world: This is why Java is called a platform-independent language. The independence does not mean bytecode runs without help; it means the help is standardized as the JVM, not as a recompilation of the application for each operating system.

A useful mental check is: bytecode itself is platform independent, the JVM is platform dependent, and the combination gives platform independent execution for the application.

Hook — can you build once and run everywhere else? In many languages the answer is no: you rebuild for each CPU. Java answers yes for bytecode, provided the local JVM exists. The question to test yourself is: what exactly must be moved and what must already be present on the destination?

Formalizing portability. A program is portable when its distributable artifact can be moved between systems without source changes or recompilation. In Java the distributable is the .class file. The mechanism is: compile once Program.java -> Program.class, copy the .class bytes unchanged to any machine, run with that machine's java launcher. Platform independence therefore means application independence, not that no platform-specific code exists at all.

Three statements that pass exams:

  • Bytecode is platform independent.
  • The JVM is platform dependent — each OS and CPU gets its own build.
  • Their combination gives platform-independent execution for the Java application.

Imagine a diagram: one source box at the top, one Program.class box in the middle, then three parallel host icons below (Windows, Linux, Mac) each containing a small JVM layer on top of Hardware + OS. Arrows from Program.class point straight down into each JVM. The shape says one artifact fans out to many hosts, and the only per-host work is installing the JVM once.

Scope — where portability stops. Portability holds for pure Java bytecode and standard APIs. It does not extend to native code loaded via JNI, to file paths hard-coded as C:\data, or to GUI assumptions that differ across hosts. Version scope also matters: newer bytecode may need a newer JVM. Networking and file access are portable in API but still subject to host permissions.

Pitfalls. The classic exam trap is to answer that bytecode runs with no help. The correct answer names the JVM as the required help. Another trap is to say you must recompile on each destination. You copy the .class only. A third is to confuse javac and java roles: javac creates portability, java realizes it.

Recap and bridge. Portability in Java is a direct consequence of bytecode: compile to .class once, run wherever a matching JVM is installed. The figure that shows one .class fanning out to multiple operating systems is the picture to remember. This idea prepares the ground for 25.3 and 25.4, where portability must coexist with object-oriented organization — the same portable program is still built from classes and objects.

Cross-check: generate Program.java on machine A and run it on machine B with no javac on B — only java — and hello world still prints if the JVM is present.

25.3 Core Features of Object Oriented Programming

Four features form the core of object oriented programming. They appear together whenever organizing programs around classes.

25.3.1 Classes and Encapsulation

A class — a collection of member data and member functions grouped into a single unit — expresses the idea of encapsulation. Encapsulation groups the data members and the functions that operate on that data into one unit. That unit is the class. Operations defined in the class work over the data members of that class.

A class is described as a set of attributes and the operations performed on those attributes. Data members represent attributes; functions represent operations. Grouping them together is the single-unit idea.

Real-world: In Java a program cannot be written without creating a class. The class is the base for carrying out computation and for organizing code.

25.3.2 Objects as Instances

An object — an instance of a class — is a concrete realization of the blueprint. A class named Student may define attributes such as identifier, name, stream, and marks. A specific student such as Mike or George is an object of that class. Both Mike and George have their own identifier, their own name, their own stream, and their own marks. The set of attributes is shared in definition; the values are local to each object.

The same pattern holds for other domains. An Account class or a Circle class can have many objects. Each object carries its own state while sharing the operations defined by the class.

25.3.3 Polymorphism — One Name, Many Forms

Polymorphism translates to one name and many forms. The typical realization is overloading. Overloading keeps the function name the same while changing the argument list.

For example, a function named getData may appear in a parent class. A second definition with the same name getData is added in the same class but with a different set of arguments — two arguments, then three arguments, and so on. The name remains getData. Which definition is followed depends on the number of arguments provided at the call site. One name therefore supports multiple forms, and that is polymorphism under the overloading category.

25.3.4 Inheritance with Extends

Inheritance is about accessing the properties of one class in another. One class is the parent class, another is the child class. The child extends the parent and gains access to data members and functions of the parent that are declared public or protected. Private data members and private functions are not accessible through inheritance.

In Java the keyword is extends. A sketch of the form is:

class ABC {
    // body of parent class
}
class XYZ extends ABC {
    // can access public and protected members of ABC
}

This is the fourth main feature. It lets a child reuse and extend behavior without rewriting the parent.

Hook — what makes a language object oriented? If you had to explain to a newcomer in one minute why Java looks different from C, you would point to four ideas that appear together every time you organize code around classes. What are they and how do they connect?

The four core ideas, formalized.

  • Class — a user-defined type that groups data members and the methods that operate on them into one unit. This grouping is encapsulation. Syntax: class Name { /* members */ }. In Java every line of executable code lives inside a class.
  • Object — an instance of a class, created with new. A class is the blueprint; objects are the houses built from it. They share the same shape but hold independent values.
  • Polymorphism — one name, many forms. The simplest form in this lecture is compile-time overloading: getData(int,int) and getData(int,int,int) share the name getData; the call's argument list selects the version.
  • Inheritance — a child class reuses and extends a parent class with extends, gaining access to public and protected members but not private members.

Picture a Venn-style stack: bottom layer is class as the unit of grouping, next layer adds objects as live copies, third layer adds polymorphism as multiple interfaces on the same name, top layer adds inheritance as vertical reuse. The takeaway is that the first two build state and behavior, the last two add flexibility and reuse without rewriting.

Assumptions and scope. These features apply to class-based, single-inheritance Java as taught here. Encapsulation is by convention and access modifiers; reflection can bypass it. Polymorphism via overloading is resolved at compile time from argument types and counts; it is different from overriding which is resolved at runtime. Inheritance with extends is single inheritance for classes; interfaces provide multiple-inheritance of type.

Pitfalls and professor flags. Do not describe encapsulation as merely putting data and methods in the same file; the point is controlled access through methods, often with private fields and public methods. Do not say private members are inherited — they exist in the parent but are not accessible in the child. Do not confuse overloading (same name, different parameters in the same class) with overriding (same signature in parent and child) — the former is the example here with getData.

Recap and bridge. The four features are classes and encapsulation, objects as instances, polymorphism via overloading in this lecture, and inheritance via extends with public/protected visibility. They form the vocabulary for every blueprint in 25.4 and for every program structure in 25.5 onward. Carry these definitions forward: blueprint, instance, one name many forms, and parent-child reuse.

25.4 Classes as Blueprints — Structure and Examples

25.4.1 Attributes and Operations Together

A class is a blueprint from which individual objects can be created. It holds a set of attributes and a set of operations that work on those attributes. Multiple objects of a single class can be created, each with its own local values for the attributes but using the same operations. Saying a class is a blueprint captures three ideas: it defines what data exists, it defines what can be done with that data, and it allows many distinct instances that conform to that definition.

In programming methodology terms, data members are the attributes and functions or methods are the operations. A class bundles them so that any object of the class has the same shape of state and the same set of behaviors, differing only in the concrete values stored.

25.4.2 Illustrative Classes — Account, Student, Circle

Three concrete blueprints illustrate the idea.

Account class. Data members are account name and account balance. Functions are withdraw, deposit, and determineBalance. Each of those functions can access the two data members. Any number of objects can be created — for example two or three accounts — each with its own name and balance.

Student class. The concept has been introduced with attributes identifier, name, stream, and marks. The same shape applies here.

Circle class. Attributes are center and radius. Operations are area and circumference. The geometry depends on those attributes.

These examples show the same pattern across domains: name the class, list its data members, list its operations. That triple is the class definition.

25.4.3 Instance Creation and Property Access

Consider two classes side by side.

The first class is named Student with attributes name, identifier, stream, and marks. Objects John and Jill are instances of Student. Each has its own identifier, name, stream, and marks.

The second class is named Circle with attributes center and radius and operations area and circumference. Objects Circle A and Circle B are instances of Circle. Each has its own center and radius and can access area and circumference as defined in the class.

Stated directly: objects are created from a class and then use the properties of that class. Property here means both data members and the ability to invoke operations. This is the instance relationship that underlies every object oriented program.

Hook — if a class is a blueprint, what exactly is drawn on the paper and what is built on site? The blueprint lists what each house has and what you can do with it; the built house holds the specific values for one family. That split explains why many objects can share one class.

Formal detail — what a class definition contains. A class lists (a) the name, (b) the data members that define state, also called attributes or instance variables, and (c) the operations that define behavior, also called member functions or methods. Each object created from the class gets its own copy of the data members; the method code is shared but executed in the context of one object's state. Saying class is a blueprint captures three ideas mentioned in the dense section: it defines what data exists, it defines what can be done, and it allows many instances that differ only in stored values.

Example shapes retained from the dense section: Account with account name, account balance and withdraw, deposit, determineBalance; Student with identifier, name, stream, marks; Circle with center, radius and area, circumference. That triple — name, members, operations — is the class definition to reproduce on exams.

Worked illustration — Account, Student, Circle as blueprints with data members and operations.

Account class — concrete instantiation. Define: Account { String accountName; double balance; void withdraw(double amt){ balance-=amt; } void deposit(double amt){ balance+=amt; } double determineBalance(){ return balance; } }. Create two objects: Account a1 = new Account(); a1.accountName="Asha"; a1.balance=5000; and Account a2 = new Account(); a2.accountName="Ravi"; a2.balance=1200;. Call a1.deposit(500) and a2.withdraw(200). Then a1.determineBalance() returns 5100 and a2.determineBalance() returns 1000. Sense-check: same class, same methods, independent balances because data members are per object.

Student class — same pattern. Student { int id; String name; String stream; int marks; } Objects Student Mike = new Student(); Mike.id=101; Mike.name="Mike"; Mike.stream="CS"; Mike.marks=88; and Student George = new Student(); George.id=102; George.name="George"; George.stream="EE"; George.marks=76;. Both share the attribute set; values are local.

Circle class — geometry. Circle { Point center; double radius; double area(){ return 3.14159*radius*radius; } double circumference(){ return 2*3.14159*radius; } } Objects Circle A {center (0,0), radius 5} -> area = 78.54, circumference = 31.42 and Circle B {center (2,3), radius 10} -> area = 314.16, circumference = 62.83. Each object carries its own center and radius, so computed results differ. This demonstrates instance creation and property access across three domains.

Picture two Student cards side by side labeled John and Jill. Both cards have four printed lines name, identifier, stream, marks — same shape. Handwritten values differ. Below them two Circle discs labeled Circle A and Circle B with different radii and the same two buttons area, circumference. The takeaway is one blueprint, many distinct instances.

Scope — when the blueprint view is exact. The blueprint metaphor assumes well-encapsulated classes where state is per object. With static members the picture changes: one value is shared across all instances, which will be detailed in 25.7. The blueprint idea also assumes constructor or method initialization has run; freshly allocated objects before initialization hold default values.

Pitfalls. Do not say objects share values because they share a class. They share a definition and method code, not stored values. Do not confuse data members with local variables — the former define persistent state per object, the latter live only during a method call.

Recap and bridge. A class bundles attributes and operations; objects are instances that hold their own values for those attributes while sharing the operations. Account, Student, and Circle illustrate the same triple across business, academic, and geometric domains. This instance relationship is the basis for constructors in 25.6 that initialize that per-object state.

25.5 Writing, Saving, Compiling and Running a Simple Program

25.5.1 Required Structure — Class and Main Method

A Java program always starts with a class. Provide the name of the class, for example First, preceded by the keyword class. The class body is enclosed in braces. Inside the class, include the main method. The signature is:

public static void main(String args[])

where args is a string array used for receiving command line arguments during interpretation.

A key rule is: whether there is a single class or a multi-class scenario where classes are connected through objects, at least one main method must exist in at least one of the classes. For a single-class program, main must be present in that class. Execution starts from main. Printing a simple string uses System.out.println("hello world").

Exam note: Expect to write the exact main signature and to explain that execution begins at main. Missing String args[] or writing white instead of void will not compile.

25.5.2 Saving, Compiling with javac and Interpreting with java

Saving, compiling, and interpreting are three distinct steps that are easy to confuse.

  • Save the source with the class name: First.java. The .java extension is required.
  • Compile: javac First.java. This invokes the Java compiler. It checks language conformance and syntax. On success it creates First.class. That file is bytecode.
  • Interpret: java First. This invokes the interpreter on the class name alone. The bytecode is interpreted and the output hello world appears on the screen.

The two-stage outcome — compiler produces .class, interpreter runs the class — is the bytecode workflow in miniature. The process illustrates why saving with the correct class name, using the correct compiler name javac, and using the correct interpreter form java First all matter.

25.5.3 From Class Diagram to Code — Account Example

A common teaching figure shows a class on the left and its code on the right, using the Account class as the subject.

On the left, the name of the class is Account. Data members are account number, name, and amount. Functions are insert, withdraw, deposit, checkBalance, and display.

On the right, the translation to Java is:

class Account {
    int accountNumber;
    String name;
    float amount;

    void insert(int acc, String n, float amt) {
        accountNumber = acc;
        name = n;
        amount = amt;
    }
    // withdraw, deposit, checkBalance, display defined similarly
}

Only one method, insert, is shown fully. It receives three parameters and assigns them inside the class. The same pattern creates the remaining methods.

If this class is not intended to be used from another class through inheritance, the recommended practice is to include public static void main inside this class, create objects of the class there, and call methods one by one — for example calling insert to add an amount, or withdraw and deposit as needed. That keeps creation, initialization, and use in one executable unit.

Hook — what is the minimum a Java file must contain to run? The answer is surprisingly small: one class and one method with a very exact signature. Remembering that pattern saves syntax errors that block every compile.

Required structure formalized. Every Java program lives in at least one class. The entry point for a runnable class is:

public static void main(String args[])

where public makes it reachable by the JVM launcher, static lets it run without an existing object, void means it returns no value to the OS directly, main is the fixed name the launcher searches for, and String args[] is the array for command-line arguments. Execution starts at main and proceeds top to bottom. For printing, System.out.println("hello world") writes a line to the console.

Rules carried from the dense section: if a program uses a single class, that class must contain main; if it uses several classes linked by objects, at least one class must contain main and you launch that class with java.

Worked example — First.java hello world compilation with javac and interpretation with java.

Setup. Open a text editor, type the full class:

class First {
    public static void main(String args[]) {
        System.out.println("hello world");
    }
}

Save as First.java in the current folder. The name matches the class First — case matters.

Compile. Run javac First.java at the terminal. The compiler parses the text, checks syntax, types, and the main signature. If it prints no output, it succeeded and the directory now lists First.java and the new First.class (bytecode). If you wrote white instead of void or omitted String args[], javac would report an error and no .class would appear.

Interpret and run. Run java First — note no .java and no .class suffix, just the class name. The JVM loads First.class, finds main, executes System.out.println, and the console shows hello world on one line. Sense-check: the two commands use different names javac for the compiler and java for the interpreter, and produce two distinct files .java source versus .class bytecode.

Variation — Account diagram to code. The figure's left side Account lists accountNumber, name, amount and insert, withdraw, deposit, checkBalance, display. The translation is the class Account block shown in section 25.5.3 where insert assigns its three parameters to fields. Adding public static void main inside the same Account class, creating Account a = new Account(); a.insert(101,"Amit",5000.0f); a.display(); keeps creation and use in one executable unit when inheritance is not intended.

Think of the folder as two layered stacks: top stack is source text First.java, bottom stack is bytecode First.class. An arrow labeled javac points down from top to bottom. A second arrow labeled java First points from the bottom stack to the screen image hello world. Visual landmarks: the source stack is readable text; the bytecode stack is smaller binary text not readable; the screen is the final destination.

Assumptions and scope. This structure assumes command-line JDK tools on the classpath. In an IDE the same steps happen behind a Run button, but the artifacts .java and .class are still created in the build folder. The args array may be empty if you launch with no arguments; it is never null for a normal launch.

Pitfalls — exam traps. Writing public static white main fails because the return type must be exactly void. Forgetting String args[] or writing string lowercase fails because String is a class name. Saving as first.java lowercase while the class is First fails on case-sensitive file systems. Running java First.java or java First.class fails — launch with class name only.

Recap and bridge. A runnable Java program is a class containing public static void main(String args[]). You save it as ClassName.java, compile with javac to get ClassName.class bytecode, and run with java ClassName to see output. The Account diagram shows how a design sketch becomes fields and an insert method — the same pattern that constructors in 25.6 will make automatic at creation time.

Exam note repeated for retention: two files .java and .class, two tools javac and java, one entry point main.

25.6 Constructors — Default and Parameterized

25.6.1 What a Constructor Is and Its Rules

A constructor is a special entity related to methods. It follows two strict rules:

  • Its name must be the same as its class name.
  • It must have no explicit return type. It is not written with void or any other return type.

For a class named ABC, a constructor is written as public ABC() with no return type marker. That form identifies it as a constructor, not an ordinary method. Its purpose is initialization of objects.

25.6.2 Default Constructor and Automatic Initialization

A default constructor is also called a no-argument constructor. It takes no parameters. For class ABC, it is public ABC() with an empty parameter list.

A default constructor provides default values to objects. In Java, an int is initialized to 0, a String is initialized to null, and a float is initialized to 0.0 when no explicit value is given.

A minimal illustration uses a class named Account with data members account number of type int, name of type String, and amount of type float, and a method display that prints those three values. No initialization is performed in the class and no values are passed in main. The program creates an object:

Account even = new Account();
even.display();

The call new Account() uses the default constructor. Even though no constructor was explicitly written in the source as public Account(), the program executes successfully. The reason is that a default constructor is provided automatically if none is written. The display prints the defaults:

0   null   0.0

where 0 corresponds to int, null to String, and 0.0 to float. The important takeaway is that failing to write a default constructor explicitly is not an error when no other constructor exists; the language supplies one.

25.6.3 Parameterized Constructor and the No-Auto-Creation Rule

A parameterized constructor receives values and uses them to initialize the data members of the class. In the same Account class, a parameterized constructor is:

Account(int acc, String n, float amt) {
    accountNumber = acc;
    name = n;
    amount = amt;
}

In main, creation supplies concrete values:

Account even = new Account(101, "Amit", 5000.0f);
even.display();

The call passes three values. Inside the constructor those values initialize the data members. The subsequent display prints the supplied values rather than the defaults.

A sharp edge case needs attention. Consider adding a commented line after the successful creation:

// Account a1 = new Account();

If the class contains only the parameterized constructor shown above and no explicit no-argument constructor, then new Account() with no arguments will not compile. When a parameterized constructor is implemented, a copy of the default constructor is not created automatically. In the earlier example with no constructor at all, the automatic default existed. Once a parameterized constructor is present, that automatic creation stops. To allow both forms, an explicit no-argument constructor must be added:

Account() { }

Only then can Account a1 = new Account(); coexist with Account even = new Account(101, "Amit", 5000.0f);. This rule — parametric presence suppresses the auto default — is a frequent source of compilation complaints and is worth checking first when a no-argument creation fails after adding a parameterized constructor.

Hook — how do you guarantee every object starts with sensible values? You could set fields after construction with an insert method, but that relies on remembering to call it. A constructor makes initialization happen automatically the moment new runs.

What a constructor is — rules and types. A constructor is a special member of a class used to initialize a new object. Two strict rules: its name must be exactly the class name including case, and it must have no explicit return type — not even void. Example for ABC: public ABC() with empty parentheses and no return marker.

Two kinds in this lecture:

  • Default or no-argument constructorpublic ABC() with no parameters. If you write no constructor at all, Java supplies one automatically. That auto version initializes fields to defaults: int to 0, String to null, float to 0.0, boolean to false, references to null.
  • Parameterized constructor — e.g., Account(int acc, String n, float amt) that receives values and assigns them to fields. After you write any parameterized constructor, the automatic no-argument constructor is no longer supplied.

This distinction is the source of the lecture's warning: parameterized presence suppresses the auto default.

Worked example 1 — default constructor Account even display prints 0 null 0.0.

Define:

class Account {
    int accountNumber;
    String name;
    float amount;
    void display(){ System.out.println(accountNumber+" "+name+" "+amount); }
}

In main:

Account even = new Account();
even.display();

No constructor appears in source. At new Account() the compiler-provided default Account(){} runs, leaving fields at defaults. The printed line is 0 null 0.0 where 0 is the int, null the String, 0.0 the float. Sense-check: the program runs without any explicit constructor, confirming auto-provision when no constructor exists.

Worked example 2 — parameterized constructor Account initialization and suppressed auto default.

Add:

Account(int acc, String n, float amt){
    accountNumber = acc; name = n; amount = amt;
}

Now in main:

Account even = new Account(101, "Amit", 5000.0f);
even.display();

The call supplies three arguments; inside the constructor parameters acc, n, amt receive 101, "Amit", 5000.0f and are stored into fields. Display prints 101 Amit 5000.0 rather than defaults. If the next line is uncommented:

// Account a1 = new Account();

compilation now fails with constructor Account() not defined or no suitable constructor found. The reason is the rule above: once a parameterized constructor exists, no automatic default is created. Fix by adding an explicit no-argument constructor:

Account(){ }

After that both new Account() and new Account(101,"Amit",5000.0f) compile. This edge case is the frequent trap to check first when a no-argument creation breaks after adding parameters.

Visualize a timeline for object creation: left marker new Account(101,"Amit",5000.0f) triggers a box constructor runs with arrows acc->accountNumber, n->name, amt->amount, then a green check object ready and call to display. For default construction the same timeline shows new Account() -> auto constructor -> fields filled with 0, null, 0.0 -> ready.

Scope — when defaults apply. The 0, null, 0.0 defaults apply only to instance fields before any assignment. Local variables inside methods have no defaults and must be assigned before use. Static fields also get defaults if not initialized, but their initialization timing differs (see static block).

Pitfalls. Do not write void Account() expecting a constructor — that becomes an ordinary method. Do not assume an auto default still exists alongside a parameterized one. When you need both creation styles, write both constructors explicitly. Mind case: account() is not Account().

Recap and bridge. A constructor shares the class name and has no return type. A default constructor with no arguments is auto-supplied only when no other constructor is written and yields 0, null, 0.0. A parameterized constructor initializes fields from arguments; its presence suppresses the auto default, so a no-argument call then requires an explicit Account(){}. This initialization discipline leads to variables in 25.7 — where per-object fields meet a shared class field.

Parameterized constructor suppresses automatic default constructor — the warning to memorize and the first thing to check when new Account() stops compiling.

25.7 Variables — Instance and Static

25.7.1 What Is a Variable

A variable — a name given to a memory location — is the basic unit of storage in a program. The name refers to a place in memory where a value is held. The value can be varied over time.

For example, int A = 10; means A is a variable holding 10 at the memory address assigned to A, say address 1000. When the program later executes A = 20;, the value at address 1000 is overwritten with 20. Later A = 30; overwrites it again with 30. Only one value is retrievable at a time, and each assignment replaces the previous content. All operations done on the variable affect that memory location. A variable must be declared before it is used.

25.7.2 Instance Variables

An instance variable belongs to an object. Each object gets its own copy. A data member such as int A and int B inside class ABC becomes an instance variable when objects are created.

Create two objects:

ABC a1 = new ABC();
ABC a2 = new ABC();

Then a1.A and a1.B are distinct from a2.A and a2.B. Giving a1.A = 10 and a1.B = 20 while giving a2.A = 30 and a2.B = 40 shows complete independence. Changing the value in one object never affects the other. This per-object copy is the defining property of instance variables.

25.7.3 Static or Class Variables — One Copy Shared

A static variable — also called a class variable — is any field declared with the static modifier. It tells the compiler that exactly one copy of this variable exists regardless of how many times the class has been instantiated. There will be only one copy per class, shared among all instances.

A concrete layout helps. Inside class ABC:

class ABC {
    int a;
    int b;
    static int c;
}

Create two objects as before, a1 and a2. Instance fields behave as in the previous subsection: a1.a may be 10, a1.b may be 20, a2.a may be 30, a2.b may be 40. The static field c is different. Suppose c holds 50. That 50 is the single shared value. Whether accessed conceptually through a1 or a2, the value seen is 50. If at some instant the value is modified to 60, then any later access, even through a1, sees 60. It is shared.

Because only one copy exists, the intended access form uses the class name directly, not an object name:

ABC.c

for class ABC and field c. Using the class name signals that the variable is a class variable, not an instance variable. In an instance, only one copy of a static variable exists regardless of how many objects are created, and that is the essential contrast with instance variables.

An applied example uses a static field of type String:

class ABC {
    static String classware = "Java programming";
}
class XYZ {
    public static void main(String args[]) {
        System.out.println(ABC.classware);
    }
}

The class is ABC, the static variable is classware initialized to the string Java programming. Inside another class XYZ, inside main, the value is accessed as ABC.classware. That prints Java programming on the screen. Accessing through the class name is the correct way to use a class variable, and it emphasizes that the storage belongs to the class as a whole.

Real-world: Shared configuration such as a program title, a default version string, or a counter of how many objects have been created is often kept as a static variable so that all instances see the same value.

Exam note: Questions may ask how many copies of a static variable exist after creating three objects. The answer remains one copy shared by all three.

Hook — where does a value live and who can see it? The same assignment c = 50 can mean a private desk drawer visible to one person or a whiteboard visible to an entire team. That difference separates an instance variable from a static variable.

What is a variable and the two kinds here. A variable is a named memory location that holds a value you can change. Declaration such as int A = 10; asks the system to reserve a slot, say at address 1000, store 10 there, and let later assignments A=20; A=30; overwrite that slot so only the last value is retrievable. A variable must be declared before use.

  • Instance variable — also called a field without static. Each object gets its own copy. Modifying a1.A never affects a2.A because they are distinct slots at different addresses.
  • Static or class variable — a field declared static. The compiler ensures exactly one copy per class, shared by all instances. Access the single slot as ClassName.variable, e.g., ABC.c. Changing it through any object or through the class name changes the same slot.

Key sentence from the lecture: regardless of how many times the class has been instantiated there will be only one copy of a static variable. The ABC.c intuition and the classware = Java programming illustration are the exam anchors.

Worked example — static variable ABC.c single shared copy versus instance variables a and b.

Define:

class ABC {
    int a; int b; static int c;
}

In main:

ABC a1 = new ABC(); ABC a2 = new ABC();
a1.a = 10; a1.b = 20; a2.a = 30; a2.b = 40;
ABC.c = 50; // or a1.c = 50 — same single slot

Visual memory map: a1 owns slots a=10 at addr 1000 and b=20 at 1004; a2 owns a=30 at 2000 and b=40 at 2004; there is one slot c=50 at 3000 shared. Print a1.a, a1.b, a2.a, a2.b, ABC.c shows 10 20 30 40 50. Now execute ABC.c = 60; and print ABC.c through either a1 or a2 — both see 60 because the slot is shared. Sense-check: two objects, four instance slots, one static slot.

Second illustration — String classware.

class ABC { static String classware = "Java programming"; }
class XYZ { public static void main(String args[]){ System.out.println(ABC.classware); } }

classware is initialized once when class ABC is loaded. Access via ABC.classware prints Java programming. Changing it once to "Advanced Java" makes every later access see the new string because storage belongs to the class.

Imagine two house icons a1 and a2. Inside each house are two labeled drawers a and b with different numbers. Above both houses is a shared whiteboard labeled c with one number 50 visible from either house. Turn the whiteboard to 60 and anyone looking from either house sees 60. The drawers stay independent.

Assumptions and scope. Static storage is per class loader in the JVM, not per object. Initialization at declaration static int c = 50; or in a static block happens once at class-load time, not per new. Instance creation does not reset static values.

Pitfalls. Accessing a static field through an object a1.c compiles but misleads readers into thinking it is per object — prefer ABC.c. Creating three objects does not create three copies of c; a question that reports three copies expects the answer one.

Recap and bridge. A variable is a named memory slot. Instance variables give each object its own independent a, b slots; a static variable gives the whole class one shared c slot reachable as ABC.c. Shared configuration and counters favor static. That shared slot often needs one-time setup, which the static block in 25.8 provides.

Static variable single shared copy ABC.c versus per-object a b — the intuition to keep when counting copies.

25.8 Static Block for Initializing Static Variables

25.8.1 Syntax and Automatic Execution on Class Loading

A static block is used for initializing static variables. Normal data members can be initialized at declaration or inside a constructor, and their values vary per object. A static variable is a class variable, so its initialization is tied to the class itself, not to any particular object. The static block addresses that need.

The syntax is a block preceded by the keyword static:

class ABC {
    static int I;
    static {
        I = 20;
        System.out.println("inside static block");
    }
}

Whatever is written inside the static block is executed automatically. No explicit call is required. The block executes when the class is loaded into memory. As soon as class ABC is loaded, the block gets its chance to run, and whatever it contains runs before ordinary code that uses the class. In this example the static variable I is initialized to 20 inside the block, and the message inside static block is printed.

Because I is static, its initialized value is accessed through the class name:

System.out.println(ABC.I);

That prints 20. The sequence is: class loads, static block runs, static variables obtain their block-assigned values, and later code can read those values through ClassName.variable. Adding a System.out.println inside the block is a simple way to observe that the block ran at class-load time, not at object-creation time.

A helpful intuition is that the static block is the class-level constructor. Ordinary constructors initialize per-object state; the static block initializes per-class state exactly once.

Hook — how do you initialize a value that belongs to no single object? Constructors run per object, so they cannot be the place for class-wide setup. A static block runs once when the class itself is loaded.

Syntax and execution guarantee. A static block is a static { /* code */ } segment inside a class, often used to initialize static fields. Any assignment inside is the block's job. Example:

class ABC {
    static int I;
    static { I = 20; System.out.println("inside static block"); }
}

General rules: the block has no name, no parameters, and no explicit call. It executes automatically when the class is loaded into memory by the class loader, before any constructor runs and before any static member is first accessed. If a class has multiple static blocks, they run in textual order. The static field I is then read as ABC.I, producing 20, and the block's message prints once.

Worked example — static block initializing ABC.I to 20 on class loading.

Steps and trace:

  1. Source contains static int I; with no initializer and the static block assigning I = 20 and printing inside static block.
  2. Program starts in main with System.out.println(ABC.I); as the first use of ABC. No object has been created.
  3. Class loader loads ABC. Immediately the static block fires: I becomes 20, console shows inside static block as the first line.
  4. The read ABC.I now returns 20, so the second line printed is 20.
  5. Creating new ABC() afterward does not re-run the block — a second object sees the same 20 without a second print. If another class creates new ABC() later, the block still does not re-run.

Sense-check: the print from inside the block appears before the print of ABC.I, proving class-load order, not object-creation order.

Visualize time on a horizontal axis: mark class load with a tall bar labeled static { I=20 }, then marks new a1, new a2, and reads ABC.I as small ticks after. The block's bar occurs once at the very left; object creations sit to its right. Takeaway: class-level constructor versus object-level constructor.

Scope — what belongs where. Use the static block for static fields that need computation or exception handling beyond a one-line initializer, such as reading a configuration or setting a map size conditionally. Do not use it for instance fields — those belong in constructors or initializers.

Pitfalls. Expecting the block to run per new is a frequent error; it runs once total. Placing a static field initialization after a block that reads it can show stale defaults due to textual order — initialize before use.

Recap and bridge. The static block static { ... } is the class-level initializer that runs automatically on class loading, perfect for setting static int I = 20 and for any one-time shared setup. Think of it as the class constructor. With shared state initialized, the next keyword this in 25.9 addresses the complementary problem: identifying the current object's state among many similar objects.

25.9 The this Keyword — Reference to the Current Object

25.9.1 Core Idea and Six Distinct Usages

The keyword this is a reference variable that refers to the current object. Current object means the object that is presently executing a method or whose context is active. When a method is called through a particular object, this inside that method denotes that calling object.

Six different usages have been discussed:

  1. To refer to the current class instance variable.
  2. To invoke the current class method.
  3. To invoke the current class constructor.
  4. To be passed as an argument in a method call.
  5. To be passed as an argument in a constructor call.
  6. To return the current class instance from a method.

Whenever the need is to deal with the current object — whether a field, a method, or a constructor — this is the handle for the calling object or the object associated with the current execution of the class.

A tiny illustration of why it matters starts with a class ABC that has a field int x = 10; and a method showData that prints x. In main, objects are created:

ABC a1 = new ABC();
ABC a2 = new ABC();
ABC a3 = new ABC();

Each has its own x, potentially initialized differently through a parameterized constructor. Calling a1.showData(), a2.showData(), or a3.showData() reaches the same method body, but this inside the body can identify which of a1, a2, or a3 made the call. That identification is the first usage and the root of the other five.

25.9.2 Usage 1 — Disambiguating Instance Variables

The most frequent use is to resolve name collision between a parameter and a field.

Consider a class Account with data members:

int accountNumber;
String name;
float amount;

It has a parameterized constructor that receives three values and a display method that prints them. Suppose the constructor is written as:

Account(int acc, String n, float amt) {
    accountNumber = acc;
    name = n;
    amount = amt;
}

That works when parameter names differ from field names. If the names are kept the same for clarity, for example the first parameter is also called accountNumber, a direct assignment accountNumber = accountNumber; does not achieve the intended initialization. The right-hand name is taken as the field itself, not the parameter, so the assignment has no effect.

The fix uses this:

Account(int accountNumber, String name, float amount) {
    this.accountNumber = accountNumber;
    this.name = name;
    this.amount = amount;
}

Here the left side this.accountNumber is explicitly the field of the calling object, while the right side accountNumber is the parameter received by the constructor. The keyword connects the left side to the calling object — the object created with Account even = new Account(101, "Amit", 5000.0f); where those values were supplied. The association this.accountNumber with the field and accountNumber with the parameter avoids confusion and guarantees that the field gets the parameter value. Passing values during creation and then calling even.display() prints the supplied numbers.

25.9.3 Usage 2 — Invoking Current Class Method

this can also be used to invoke the current class method without qualifying with an object name.

Take the same Account class, but instead of a constructor, provide an insert method that receives values and initializes fields, and a display method that prints them.

class Account {
    int accountNumber;
    String name;
    float amount;
    void insert(int acc, String n, float amt) {
        this.accountNumber = acc;
        this.name = n;
        this.amount = amt;
        this.display();
    }
    void display() {
        System.out.println(accountNumber + " " + name + " " + amount);
    }
}

In main, an object is created with the default constructor and used as:

Account even = new Account();
even.insert(101, "Amit", 5000.0f);

Inside insert, this.accountNumber, this.name, and this.amount make clear that the fields of the current calling object — even — are being set. The line this.display(); invokes the display method for that same current object. It is equivalent to writing even.display(); from inside the method, but it uses this to mean the current instance that called insert. Even if this is omitted and the code simply says display();, the program behaves the same because the implicit receiver is the current instance that called insert. Writing this.display() makes the receiver explicit and is the form that highlights this keyword usage.

25.9.4 Remaining Usages in Brief

The other four usages follow the same principle — this stands for the current object — applied in different call sites:

  • To invoke the current class constructor, this() or this(arguments) is used as the first line inside another constructor to delegate to a sibling constructor of the same class.
  • To be passed as an argument in a method call, this is handed to another method so that the receiving method can work with the calling object itself.
  • To be passed as an argument in a constructor call, this is handed to a constructor of another class, linking the newly created object with the current one.
  • To return the current class instance from a method, the method returns this so that callers can chain calls on the same object.

In each case the phrase current object keeps its meaning: the object that is the receiver of the currently executing method or the object whose constructor is running.

Hook — when one method body serves many objects, how does it know which object's data to touch? Three houses share the same repair manual, yet the electrician must know which house they are in. this is the name tag the method reads.

Core idea and the six usages — restated with exam focus. this is a reference variable that always denotes the current object — the receiver of the currently executing method or the object whose constructor is running. When a1.showData() is called, inside showData the expression this refers to a1; for a2.showData() it refers to a2.

The six distinct usages taught in the lecture:

  1. To refer to the current class instance variable, typically this.field.
  2. To invoke the current class method, this.method().
  3. To invoke another constructor of the same class, this(...) as the first line of a constructor.
  4. To be passed as an argument in a method call, someMethod(this).
  5. To be passed as an argument in a constructor call, new OtherClass(this).
  6. To return the current instance from a method, return this; which enables chaining.

Worked example 1 — disambiguating instance variables with accountNumber — reinforced.

Define:

class Account {
    int accountNumber; String name; float amount;
    Account(int accountNumber, String name, float amount){
        this.accountNumber = accountNumber; this.name = name; this.amount = amount;
    }
    void display(){ System.out.println(accountNumber+" "+name+" "+amount); }
}

Create Account even = new Account(101,"Amit",5000.0f); The constructor's parameters have the same names as fields. Without this, the assignment accountNumber = accountNumber; would assign the parameter to itself, leaving the field unchanged as 0 null 0.0. With this, the left side this.accountNumber is the field slot of the current object even at its address, the right side accountNumber is the parameter value 101 on the stack. After this.accountNumber = accountNumber the field becomes 101, similarly name becomes Amit and amount becomes 5000.0. Calling even.display() prints 101 Amit 5000.0. Sense-check: swapping to distinct names acc, n, amt works without this; matching names requires it.

Worked example 2 — invoking current class method with this.display — reinforced.

An alternative teaching version uses insert rather than a constructor:

class Account {
    int accountNumber; String name; float amount;
    void insert(int acc, String n, float amt){
        this.accountNumber = acc; this.name = n; this.amount = amt;
        this.display();
    }
    void display(){ System.out.println(accountNumber+" "+name+" "+amount); }
}

Execute:

Account even = new Account();
even.insert(101,"Amit",5000.0f);

Inside insert, the receiver is even, so this again means even. The three field stores target even's fields, and this.display() invokes display on that same even, equivalent to even.display() written from outside. Even a bare display(); would work because the implicit receiver is the current object, but this.display(); makes it explicit and is the pattern the lecture highlights.

Quick picture: three object blobs a1, a2, a3 each with an x slot, all pointing with a single arrow to one method box showData. Inside the box a small badge reads this -> current caller. The call arrow from a1 lights up badge this==a1. Takeaway: one code body, many contexts, this selects.

Assumptions and scope. this is valid only inside instance methods and constructors. It is not available in static methods or static blocks because there is no current instance. Constructor delegation this() must be the very first statement in a constructor.

Pitfalls. Writing accountNumber = accountNumber with matching names does nothing visible but compiles — a silent bug that this. fixes. In a static context writing this.display() does not compile.

Recap and bridge. this is the current object's reference, letting a shared method identify its caller. Its most used forms are this.field = field for disambiguation and this.display() for current-object method calls, with four additional forms for constructor delegation, passing the instance, and returning it. The same disciplined naming that makes this useful also helps final in 25.10 lock down that state once set.

Six usages in brief for later listing: refer to instance variable, invoke method, invoke constructor, pass as method argument, pass as constructor argument, return the instance.

25.10 The final Keyword and Immutable Classes

25.10.1 Three Levels of Restriction

The keyword final is about putting restrictions. It has three distinct applications, all with the same theme that the marked element cannot be changed in a specific way.

  • Put final before a variable and the variable becomes a constant variable. Its value cannot be changed after initialization.
  • Put final before a method and the method cannot be overridden. In an inheritance hierarchy, a child will not be able to provide a new definition for that method.
  • Put final before a class and the class cannot be inherited. No child class can be created that extends it.

Each placement adds one restriction at its own level: variable level is value restriction, method level is override restriction, class level is inheritance restriction.

25.10.2 Mutable versus Immutable Classes

Classes discussed earlier such as Account and Student are mutable classes. They are mutable because there is no final restriction. Inheritance is free, methods can be overridden, and variable values can be changed. That is the default character of ordinary classes.

An immutable class — a class with restrictions that cannot be modified — is the opposite. Once an object is created, its state cannot be changed. Immutability is built by applying final at multiple places in a disciplined way.

25.10.3 Building an Immutable Class

An immutable class satisfies a checklist:

  1. The class must be declared final so that its child class cannot be created. If an attempt is made to write class B extends ABC where ABC is final, that attempt will not compile.
  2. Data members must be declared final. Their values are set once and then become constant. For example, final int x; in class ABC. If x is initialized to 10 in a parameterized constructor, that 10 is fixed. Trying to change x to 20 later causes a compilation error because a final field cannot be reassigned.
  3. Provide a parameterized constructor and getter methods for all variables so that values can be supplied once at creation and read later. Provide no setter methods. Without setters, there is no API to modify a field after the object leaves the constructor. Values can be initialized once when the object is created and retrieved through getters, but they cannot be altered.

A sketch of the pattern inside a class ABC is:

final class ABC {
    final int x;
    ABC(int x) {
        this.x = x;
    }
    int getX() {
        return x;
    }
}

Here final at the class level blocks inheritance, final on x makes the field constant after construction, the parameterized constructor sets x once, and only a getter is present. There is no setX. Attempting x = 20 after construction is a compilation error. That trio — final class, final fields, getters only with no setters — captures what is meant by an immutable class: it cannot be extended, its fields cannot be reassigned, and its API offers no mutation.

Real-world: Immutable representations are favored for values that should stay stable, such as currency amounts, dates, or strings themselves, because sharing them across code is safe when no one can change them.

Hook — how do you promise that something will not change after creation? In banking, once a transaction record is written it must not be edited; in geometry, a circle that represents a fixed measurement should not allow its radius to drift. final is the language tool for that promise.

Three restriction levels, one theme — reinforced. The keyword final adds a cannot-change guarantee at the point where it appears:

  • final before a variable makes it a constant. After the single initialization, assignment is forbidden. final int x = 10; x = 20; // compile error
  • final before a method prevents overriding in a child. The method's behavior is sealed in the hierarchy.
  • final before a class prevents inheritance altogether. final class ABC cannot be extended; class B extends ABC does not compile.

Each is a barrier at a different level: value restriction, override restriction, inheritance restriction. Classes such as Account and Student without final are mutable by default.

Immutable classes — the checklist restated for exams. An immutable class is one whose objects cannot be modified after creation. Immutability is valuable for values shared widely, like currency or dates, because sharing is safe.

The lecture's checklist:

  1. Declare the class final so no child can be created to sneak in mutability.
  2. Declare data members final so each field is constant after construction. Example final int x; set once in a parameterized constructor stays at that 10; later assignment fails.
  3. Use a parameterized constructor to set those final fields exactly once, and provide getter methods for all variables so callers can read values. Provide no setter methods — without setX there is no API to change state after construction.

Sketch:

final class ABC {
    final int x;
    ABC(int x){ this.x = x; }
    int getX(){ return x; }
}

Here final on the class blocks class B extends ABC, final on x blocks reassignment, the constructor initializes once, and getX exposes reading. Attempting x = 20 after construction is a compile error. That triple — final class, final fields, getters only — is what defines an immutable class.

Concrete fixed-value demonstration. Build final class FixedAmount { final int amount; FixedAmount(int amount){ this.amount = amount; } int getAmount(){ return amount; } } and create FixedAmount a = new FixedAmount(5000); The printed amount is 5000. Calls a.getAmount() repeatedly return 5000. Attempting a.amount = 6000; or adding void setAmount(int v){ amount=v; } would not compile because the field is final and the class intention is no setters. The same pattern models Account balances that must not change or dates that must remain stable.

Scope — when immutability is complete. Declaring the field final prevents reassignment of the reference, but if the field is a reference to a mutable object, the object's internal state could still change unless that object is also immutable or defensively copied. True immutability for reference fields needs that extra care.

Pitfalls. Marking only the class final without making fields final and hiding setters still leaves values mutable. Marking fields final but leaving a setter compiles only if the setter is removed — keep getters only.

Recap and bridge. final introduces three guarantees: constant variable, non-overridable method, non-inheritable class. An immutable class combines final class, final fields, a parameterized constructor, getters only, and no setters, so once an object is created its state is fixed. Immutable values will reappear in 25.12 where String itself is the most famous immutable class in Java.

25.11 Arrays — Declaration, Memory and the Arrays Utility Class

25.11.1 Declaration, Creation, Initialization and Indexing

An array — a collection of similar data types — groups many values of the same type together. An array of integers, an array of strings, or an array of nodes are all examples of the same idea: one name for many consecutive slots that hold the same kind of value.

Three syntactic variants are used to declare an array, differing only in bracket placement:

int[] arr;
int arr[];
int []arr;

Any of these tells the compiler that the variable will hold an array of type integer. At this point only the declaration exists. The actual storage is linked when the size is provided, for example:

arr = new int[6];

That reserves six consecutive units of memory for integers. The compiler now knows the physical extent. Initialization can be done on the right-hand side as well, for example:

int[] a = {2, 3, 5, 1, 4, 7};

Here the name of the array is a and it holds six members with those six values. Indexing always starts from zero. So a[0] is 2, a[1] is 3, a[2] is 5, a[3] is 1, a[4] is 4, a[5] is 7.

25.11.2 Loops for Access — Simple, For-Each and Labeled

To access elements, a loop is used.

A simple for loop that prints every element using the length property is:

for (int i = 0; i < a.length; i++) {
    System.out.println(a[i]);
}

The loop continues up to a.length. At each iteration one value is printed. When the loop exhausts, all values have appeared on the screen. The name length is a predefined property available with arrays.

A for-each loop offers a shorter form for the same traversal, and it has been used in earlier sessions. A labeled for loop allows a name to be attached to a loop for control flow, for example:

AA: for (int i = 0; i < a.length; i++) {
    // body; the label AA can be referenced to return to this loop
}

Here AA is the label for the loop. Labeling is useful when multiple nested loops exist and the logic needs to refer to a specific loop.

25.11.3 Working with Arrays — Sort, Binary Search, Copy, Fill

Arrays is a class in Java that brings several ready-made methods for working with arrays directly. These methods save manual coding.

Sort. Arrays.sort sorts elements. It can sort a bounded range or the whole array.

int[] a = {2, 3, 5, 1, 4, 7};
Arrays.sort(a, 0, 4);

Here 0 is the starting index and 4 is the number of elements to sort from that start. The first four elements 2, 3, 5, 1 become sorted to 1, 2, 3, 5. The remaining two elements 4, 7 stay unsorted because the sort was bounded to four elements. The array after the call is {1, 2, 3, 5, 4, 7}. If no bounds are provided as Arrays.sort(a), the entire array is sorted to {1, 2, 3, 4, 5, 7}.

Binary search. To find the position of a value in a sorted structure, Arrays.binarySearch is used.

int pos = Arrays.binarySearch(a, 5);

If the array is viewed as {1, 2, 3, 5, 4, 7} after the bounded sort, the call for 5 returns index 3 in zero-based counting. In a fully sorted view, the index for 5 would be 4 as 0:1, 1:2, 2:3, 3:4, 4:5. The method returns the index number where the element is found. The example reports four as the index in that fully sorted framing, counting from zero.

Copy. A copy of an array can be created up to a particular length.

int[] b = Arrays.copyOf(a, 4);
int[] c = Arrays.copyOf(a, a.length);

copyOf takes the source array and a length. With length 4, the first four elements are copied. With a.length, which is 6, all six elements are copied. A related method copyOfRange copies a specific window:

int[] d = Arrays.copyOfRange(a, 1, 4);

Here 1 is the starting index and the logical end is bounded so that four elements starting from index 1 are considered in the description used in the session, producing a copy containing 2, 3, 4 style slices depending on the precise bounds. The session text presents the call as starting at index 1 and taking four elements forward, yielding a copy that reflects that window.

Fill. An array can be filled with a single value.

Arrays.fill(a, 1);

That fills the entire array with 1s, so the array becomes all ones. A bounded fill is also available:

Arrays.fill(a, 0, 4, 1);

Here 0 is the start index, 4 is the end bound, and 1 is the value. The described effect is that the array gets filled with ones in the selected range, and with the one-argument form the whole array becomes ones. The key point is that Arrays as a class brings sort, binarySearch, copyOf, copyOfRange, and fill as predefined operations.

Real-world: These utilities are part of daily Java practice for ordering data, searching efficiently, duplicating buffers, and initializing tables without manual loops.

25.11.4 The Equality Trap — Reference versus Content Comparison

Comparing arrays needs care. Consider two separate arrays:

int[] arr1 = {1, 2, 3};
int[] arr2 = {1, 2, 3};

A natural test is:

if (arr1 == arr2) System.out.println("same");
else System.out.println("not same");

The output is not same even though the elements look identical. The reason is that == compares reference variables, not contents. Suppose arr1 starts at memory location 1000 and arr2 starts at 2000. The comparison is effectively 1000 == 2000, which is false. Each array is a distinct object at a distinct address, so reference comparison fails.

The solution is the content-based method:

if (Arrays.equals(arr1, arr2)) System.out.println("same");
else System.out.println("not same");

Arrays.equals does a one-to-one comparison of the elements in the two arrays. Since 1, 2, 3 matches 1, 2, 3 element by element, it returns true and same is printed. This contrast — == as reference check versus equals as element-wise check — is the essential trap when testing array equality.

Exam note: Expect a question that shows two arrays with identical literals and asks what arr1 == arr2 prints. The intended answer is not same and the fix is Arrays.equals.

Hook — how do you hold 100 grades without writing 100 separate variables? One name with indexed slots keeps the data together, contiguous, and loopable.

Declaration, creation, and indexing formalized — recap of dense with confidence. An array is a collection of similar-type values stored in consecutive memory under one name. Three declaration spellings are interchangeable:

int[] arr; int arr[]; int []arr;

At this stage only the reference exists. Creation with size allocates consecutive units:

arr = new int[6];

which reserves six integer slots. Local initialization combines both:

int[] a = {2,3,5,1,4,7};

where a has length 6. Indexing is zero-based: a[0]=2, a[1]=3, a[2]=5, a[3]=1, a[4]=4, a[5]=7. The property a.length gives 6.

Worked example 1 — array six elements 2 3 5 1 4 7 loop with length and labeled loop — expanded trace.

Define int[] a = {2,3,5,1,4,7}; Simple for loop:

for(int i=0;i<a.length;i++) System.out.println(a[i]);

Trace: i=0 prints 2, i=1 prints 3, i=2 prints 5, i=3 prints 1, i=4 prints 4, i=5 prints 7, loop stops as i==6==a.length.

For-each alternative:

for(int v: a) System.out.println(v);

same output in shorter form.

Labeled loop illustration:

AA: for(int i=0;i<a.length;i++){
    if(a[i]==5) continue AA;
    System.out.println(a[i]);
}

Here AA names the loop so continue AA and break AA can target it from nested contexts — useful when two loops are nested and you need to control the outer one.

Worked example 2 — Arrays utility: sort bounded 0 4, sort all, binarySearch, copyOf, copyOfRange, fill — expanded.

Start int[] a = {2,3,5,1,4,7}; Bounded sort:

Arrays.sort(a,0,4);

Indices 0,1,2,3 cover values 2,3,5,1 which become sorted 1,2,3,5. Tail 4,7 stays. Result {1,2,3,5,4,7}. Unbounded Arrays.sort(a) would give {1,2,3,4,5,7}.

Binary search:

int pos = Arrays.binarySearch(a,5);

On {1,2,3,5,4,7} the result is 3 (zero-based). On a fully sorted {1,2,3,4,5,7} it would be 4 as 0:1,1:2,2:3,3:4,4:5. Returning the index where the element is found is the key behavior; the method expects a sorted array.

Copy:

int[] b = Arrays.copyOf(a,4);
int[] c = Arrays.copyOf(a,a.length);
int[] d = Arrays.copyOfRange(a,1,4);

b is first 4 elements {1,2,3,5}, c is all 6 {1,2,3,5,4,7}, d is window [1,4) {2,3,5} with start inclusive end exclusive per API. The lecture phrase starting at index 1 and taking four elements forward reflects the window idea.

Fill:

Arrays.fill(a,1);
Arrays.fill(a,0,4,1);

First fills all with 1 to {1,1,1,1,1,1}, second fills range [0,4) with 1. Sense-check: sort changes order, binarySearch needs order, copyOf duplicates without sharing, fill overwrites.

Worked example 3 — array equality reference comparison versus Arrays.equals — not same warning — reinforced.

Define:

int[] arr1 = {1,2,3}; int[] arr2 = {1,2,3};
if(arr1==arr2) System.out.println("same"); else System.out.println("not same");

Output is not same. Suppose arr1 at 1000 and arr2 at 2000; == compares 1000==2000, false. Correct:

if(Arrays.equals(arr1,arr2)) System.out.println("same"); else System.out.println("not same");

Arrays.equals compares element-by-element and returns true, printing same.

Visualize array a as six consecutive boxes labeled 0..5 on a line. sort reorders values inside boxes. binarySearch hops to middle then halves. copyOf draws a second line of boxes copying values. == compares box-line addresses, Arrays.equals compares box contents.

Assumptions and scope. Arrays have fixed length once created; ArrayList is the growable alternative. Arrays.binarySearch requires sorted input; on unsorted input the index is meaningless. a.length is a field, not a method.

Pitfalls. Saying arr1==arr2 checks contents is the core bug this section warns about — use Arrays.equals. ArrayIndexOutOfBounds happens at a[6] when length is 6. Forgetting import java.util.Arrays; blocks sort and binarySearch.

Recap and bridge. Declare with int[] arr, create with new int[6], initialize with {...}, index from 0, loop with for and a.length, sort and search with Arrays.sort and Arrays.binarySearch, copy with copyOf/copyOfRange, fill with fill, and compare contents with Arrays.equals not ==. The same content-versus-reference theme prepares the string constant pool puzzle in 25.12.

Arrays reference comparison shows not same — use Arrays.equals for content. That warning is the exam answer to memorize.

25.12 Strings — Constant Pool, Immutability, StringBuffer and StringTokenizer

25.12.1 Nature of Strings and Ways to Create Them

A string — a sequence of characters — is related to arrays but handled in a special way in Java. A string is bounded text, and handling strings involves the ideas of mutable and immutable, which are treated in the next subsection.

Creating a string can be done in several ways:

String str = "ABC";
char[] data = {'A', 'B', 'C'};
String s2 = new String(data);
String s3 = s2;

The first form puts text in double quotes after the type String. The second builds a string from a character array. The third passes one string to another for initialization. All produce a sequence of characters that can be printed, compared, and passed to methods. The crucial difference from arrays lies in how identity and content are treated in memory.

25.12.2 String Constant Pool and the Equality Puzzle

Strings carry a memory optimization called the string constant pool. When a string literal is created, the literal is stored in the pool and given an address, say 1000.

Example with two literals:

String s1 = "Java";
String s2 = "Java";
System.out.println(s1 == s2);

At first this is puzzling because with arrays == returned false for two separate objects with the same content. Here s1 == s2 prints true. The pool explains the difference. When s1 is created, Java is stored in the pool at address 1000 and s1 points there. When a request is made to initialize s2 with the same literal Java, no separate space is provided. Instead s2 is linked directly to the same pool entry at 1000 to save memory. Both references point to the same address, so reference comparison is true.

A contrast with new makes the pool behavior clear:

String s3 = new String("Java");
System.out.println(s2 == s3);

Because new forces creation, s3 is placed at a different address, not the pool entry reused by s1 and s2. Now s2 points to 1000 while s3 points elsewhere, so s2 == s3 is false even though the characters are the same.

The content check uses equals:

System.out.println(s2.equals(s3));

Content-wise the two strings are both Java, so a one-to-one character comparison returns true. This string behavior is the mirror of the array case: with arrays new int[]{1,2,3} always creates distinct addresses, while with string literals the pool reuses an existing entry.

25.12.3 Immutability and StringBuffer for Mutable Sequences

Strings are immutable. Once a string points to Java at address 1000, its content cannot be changed in place. Assigning a modified text creates a separate string and repoints the variable to the new address; the original Java entry keeps existing in the pool.

When mutable, growable, writable character sequences are needed, the class StringBuffer is used. Its size grows automatically to accommodate added characters, and it supports substring, insert, and append operations.

Constructors for StringBuffer include:

StringBuffer sb1 = new StringBuffer();
StringBuffer sb2 = new StringBuffer(20);
StringBuffer sb3 = new StringBuffer("hello");

The first is a default constructor, the second specifies a capacity, and the third initializes with a string. The effect is a buffer that can change without creating a fresh string object on each edit, which strings themselves cannot do.

Real-world: Mutable buffers are chosen for building text incrementally in loops or for editing operations where many inserts and appends occur.

25.12.4 StringTokenizer — Breaking a String into Tokens

The class StringTokenizer breaks a string into tokens. A token is a piece of the string separated by delimiters. By default a space is considered the delimiter, though other delimiters can be supplied.

Take the string java programming with objects. With space as delimiter, four tokens are produced: java, then a space, then programming, then a space, then with, then a space, then objects. Logically the word tokens are java, programming, with, objects — four tokens.

Code that extracts them is:

import java.util.StringTokenizer;

class Test {
    public static void main(String args[]) {
        StringTokenizer st = new StringTokenizer("java programming with objects");
        int i = 0;
        int j = st.countTokens();
        while (st.hasMoreTokens()) {
            System.out.println(st.nextToken());
            i++;
        }
        System.out.println("loop iterated " + i + " times");
        System.out.println("countTokens now " + st.countTokens());
    }
}

Steps in full detail:

  • Import java.util.StringTokenizer.
  • Inside main, create the tokenizer st with the subject string java programming with objects.
  • Initialize int i = 0 as a loop counter.
  • Initialize int j = st.countTokens() which first counts four tokens and stores 4 in j.
  • Enter a while loop with condition st.hasMoreTokens(). While tokens remain, print st.nextToken(). The first call before any token is consumed returns java, the second returns programming, the third returns with, the fourth returns objects. At each iteration increment i.
  • After the loop, i is 4 because the loop iterated four times, once per token. Printing the tokens shows each word on its own line in order.
  • Calling countTokens after the string is exhausted returns 0 because all tokens have been consumed. The output therefore includes 4 at the start as the initial count and 0 at the end as the exhausted count.

This walkthrough shows how to identify tokens from a string, how to print them one by one, how to track the iteration count, and how countTokens changes as tokens are consumed. The exhaustive versus remaining count distinction is a frequent point of confusion and is made visible by printing before and after the loop.

Real-world: Tokenizing is a step in many text processing pipelines — splitting a sentence into words before counting, indexing, or analyzing.

Hook — why does == sometimes look like it checks contents for strings when it did not for arrays? The answer is a behind-the-scenes memory sharing optimization that reuses literal text.

Nature of strings and ways to create them — reinforced. A string is a sequence of characters, related to an array but handled specially in Java. Three creation forms in the lecture:

String str = "ABC";
char[] data = {'A','B','C'}; String s2 = new String(data);
String s3 = s2;

The first uses a literal in double quotes, the second builds from a character array, the third assigns one reference to another. All produce sequences that can be printed and passed to methods, but identity versus content differs due to the pool.

Strings are immutable — once str points to "ABC" at address 1000, no method changes that ABC in place; any edit creates a new string and moves the reference, while the pooled literal stays.

When mutable, growable sequences are needed, StringBuffer is used. Its constructors:

StringBuffer sb1 = new StringBuffer();
StringBuffer sb2 = new StringBuffer(20);
StringBuffer sb3 = new StringBuffer("hello");

They provide automatic capacity growth plus append, insert, substring.

The class StringTokenizer breaks a string into tokens — pieces separated by delimiters. By default space is the delimiter, though others can be supplied, and the subject java programming with objects yields four word tokens java, programming, with, objects.

Worked example 1 — string constant pool s1 s2 true, s2 s3 new false, equals true — reinforced.

Setup:

String s1 = "Java";
String s2 = "Java";
System.out.println(s1==s2);

Memory: literal "Java" is placed in the string constant pool at address 1000 on the first creation; s1 points to 1000. On s2 = "Java" the pool is checked, the same "Java" entry at 1000 is reused, s2 also points to 1000 to save memory, so s1==s2 compares 1000==1000 and prints true. This differs from int[] where new always allocated distinct addresses.

Contrast:

String s3 = new String("Java");
System.out.println(s2==s3);

new forces a fresh allocation outside the shared pool entry, so s3 is at, say, 2000. Then s2==s3 compares 1000==2000 and prints false even though characters match.

Content check:

System.out.println(s2.equals(s3));

equals walks characters J a v a one-to-one, finds identical content, returns true and prints true. This trio — literal reuse true, new separation false, content true — is the standard exam pattern.

Worked example 2 — StringTokenizer java programming with objects four tokens countTokens — reinforced.

import java.util.StringTokenizer;
class Test {
    public static void main(String args[]){
        StringTokenizer st = new StringTokenizer("java programming with objects");
        int i=0; int j = st.countTokens();
        while(st.hasMoreTokens()){ System.out.println(st.nextToken()); i++; }
        System.out.println("loop iterated "+i+" times");
        System.out.println("countTokens now "+st.countTokens());
    }
}

Step trace: import connects the class. Construct st with the sentence; default delimiter space splits into word tokens java, programming, with, objects. j = st.countTokens() counts remaining unused tokens and stores 4. Loop hasMoreTokens is true four times: first nextToken() returns java, second programming, third with, fourth objects, each printed on its own line while i increments 0->1->2->3->4. After loop i is 4, so loop iterated 4 times prints. Now st.countTokens() is 0 because all tokens were consumed, so countTokens now 0 prints. Initial count versus exhausted count is the frequent confusion made visible.

Visualize strings: pool box at top holds literal "Java" at 1000. Two arrows s1 and s2 point to that same box. A separate heap box holds s3's distinct "Java". The == comparison checks arrow targets; equals checks characters inside boxes. For tokenizer, picture four word boxes in order, st as a cursor moving box by box; countTokens shrinks as cursor advances.

Assumptions and scope — immutability versus buffer choice. String immutability assumes you accept new allocation for edits; for loops doing many appends, StringBuffer or StringBuilder is the scope-appropriate tool because it grows in place and avoids many temporary string objects.

Pitfalls. Testing string content with == works only accidentally for interned literals; always use equals for content. Forgetting that countTokens decreases as you consume tokens leads to expecting 4 after the loop instead of 0.

Recap and bridge. Strings are immutable character sequences with a constant pool that reuses literals: s1==s2 true for same literal, s2==s3 false when new is used, equals true for content. StringBuffer provides mutable growth, StringTokenizer splits java programming with objects into four tokens whose remaining count changes during iteration. This immutability theme connects to streams in 25.13 where text handling moves from in-memory strings to external files.

String constant pool memory sharing intuition explains s1==s2 true — the picture of one pool entry with two references is the mental model to keep.

25.13 Streams and File Input-Output — BufferedReader, PrintWriter and Scanner

25.13.1 Streams as Virtual Connections

A stream — a virtual connection between a program and a medium — is the abstraction that handles input and output. To take input, a virtual connection to the source is needed. To give output, a virtual connection to the destination is needed. The connection itself is called a stream.

Two everyday instances make the idea concrete:

  • System.in is an input stream that connects to the keyboard. Associating a scanner or reader with System.in lets a program collect data typed by a user.
  • System.out is an output stream that connects to the screen, also called the monitor or console. Calling System.out.println writes through that stream to the display.

The same abstraction scales beyond keyboard and screen. By associating the stream object with a File object, the program can read from or write to a file. By associating with a Socket, it can exchange data over a network. The program code looks similar; only the stream partner changes.

25.13.2 BufferedReader for Input and PrintWriter for Output

For taking input, the class BufferedReader is commonly used. Its creation depends on the source:

  • From keyboard: BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); Here an InputStreamReader bridges System.in to the reader that collects characters.
  • From a network socket: BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); Here socket.getInputStream() is the predefined method that supplies bytes from the network, wrapped by InputStreamReader and BufferedReader.
  • From a specific file: BufferedReader br = new BufferedReader(new FileReader("abc.txt")); Here new FileReader("abc.txt") opens the named file and the reader collects its content.

For giving output to a text file, the class PrintWriter is used. It is a stream class that writes to a text file and offers methods print and println that look just like the System.out methods of the same names, except the destination is a file, not the screen.

A complete example that writes to a file demonstrates the life cycle:

import java.io.*;

class TextFileOutputDemo {
    public static void main(String args[]) {
        PrintWriter outStream = null;
        try {
            outStream = new PrintWriter(new FileOutputStream("stuff.txt"));
        } catch (FileNotFoundException e) {
            System.out.println("Error opening file");
            System.exit(0);
        }
        outStream.println("Hello world");
        outStream.println("Second line");
        outStream.close();
    }
}

Walkthrough step by step:

  1. Declare an object PrintWriter outStream without initializing it.
  2. Inside a try block, initialize it with new PrintWriter(new FileOutputStream("stuff.txt")). The name of the file is stuff.txt. The file is the stream partner.
  3. The initialization is placed inside try and catch because the file being opened may not exist or may not be creatable, which would otherwise cause abnormal termination at runtime with an exception. Handling that inside catch keeps the program controlled, often by printing a message and exiting.
  4. Write content with outStream.println and string arguments. Each call sends a line to the file.
  5. Once writing is complete, close the connection with outStream.close(). Closing clears pointers and background structures that were required to associate the file with the program. The connection between program and file — logically two boxes joined by the stream — is now broken cleanly.

Real-world: PrintWriter plus FileOutputStream is the textbook way to create log files, reports, or exported text from a running application.

25.13.3 Scanner for Reading a Text File — Complete Walkthrough

Input from a text file can be taken with BufferedReader, but the Scanner class can also be used. Earlier, Scanner was associated with System.in to read from the keyboard. The same class can be re-associated with a file to read text from that file.

The needed imports are:

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

A class that reads from a file and echoes to the screen is TextFileScannerDemo:

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

class TextFileScannerDemo {
    public static void main(String args[]) {
        System.out.println("I will read three numbers and a line of text from file morestuff.txt");
        Scanner inputStream = null;
        try {
            inputStream = new Scanner(new FileInputStream("morestuff.txt"));
        } catch (FileNotFoundException e) {
            System.out.println("File morestuff.txt not found");
            System.exit(0);
        }
        int n1 = inputStream.nextInt();
        int n2 = inputStream.nextInt();
        int n3 = inputStream.nextInt();
        inputStream.nextLine();
        String line = inputStream.nextLine();
        System.out.println("The three numbers read from file are " + n1 + " " + n2 + " " + n3);
        System.out.println("The line read from file is " + line);
        inputStream.close();
    }
}

Complete execution story:

  • Print a prompt: I will read three numbers and a line of text from file morestuff.txt. This goes to the console through System.out.
  • Declare Scanner inputStream as null.
  • Inside try, initialize it with new Scanner(new FileInputStream("morestuff.txt")). That connects the program to the file morestuff.txt using a file stream wrapped by a scanner. Inside catch, handle the case where the file is not present by printing a message and exiting. This avoids abnormal termination without a message.
  • If the try succeeds, the file is available and no exit occurs. The program has an active association — logically the file on one side, the program on the other, linked by the scanner.
  • Read three integers: n1 = inputStream.nextInt(), n2 = inputStream.nextInt(), n3 = inputStream.nextInt(). Each nextInt consumes the next number in the file.
  • Move to the next line with inputStream.nextLine() so that the subsequent string read starts at the beginning of the text line rather than at the trailing newline after the numbers.
  • Read the line: String line = inputStream.nextLine(); That captures the remaining text as a string.
  • Print the results to the screen: the three numbers as n1 n2 n3 and the line as stored.
  • Close the scanner with inputStream.close().

Consider a concrete file morestuff.txt containing:

1 2 3 4
He is a jolly good fellow

The four numbers 1, 2, 3, 4 and the sentence He is a jolly good fellow are present. The program reads 1, 2, 3 into n1, n2, n3. It then moves to the next line and reads He is a jolly good fellow into line. The remaining 4 on the first line illustrates that only the first three numbers were consumed as directed.

Expected output is:

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

Three distinct stream associations have now been seen: System.in to keyboard, System.out to monitor, and file streams through BufferedReader, PrintWriter, or Scanner. For input, BufferedReader and Scanner are alternatives; for output to file, PrintWriter is the direct choice. Choosing among them depends on whether formatted token reading, buffered character reading, or simple line printing is the main need.

Real-world: File-based scanner reading is a quick way to ingest configuration, test data, or small datasets during development without setting up a database.

Exam note: Try-catch handling around file opening is not optional decoration. Programs that open files should place the open inside try and provide a catch for FileNotFoundException, then close the stream at the end.

Hook — how do you swap keyboard input for file input without rewriting most of your code? Java's answer is a small abstraction called a stream — a virtual connection between your program and whatever is on the other end.

Streams as virtual connections, formalized. A stream is a virtual connection between a program and a medium, such as keyboard, screen, file, or network socket. To take input you attach a reader to the source stream; to give output you attach a writer to the destination stream. Code shape stays similar; only the partner object changes.

Core examples from the lecture:

  • System.in is the standard input stream tied to the keyboard. Wrapping it as new BufferedReader(new InputStreamReader(System.in)) or new Scanner(System.in) lets readLine() or nextInt() collect typed data.
  • System.out is the standard output stream tied to the screen. System.out.println(...) writes through that connection to the console.
  • By swapping the partner to a File object, new BufferedReader(new FileReader("abc.txt")) reads from that file, new PrintWriter(new FileOutputStream("stuff.txt")) writes to it, and new Scanner(new FileInputStream("morestuff.txt")) reads tokens from it.
  • By swapping to socket.getInputStream(), the same BufferedReader reads from a network.

For input, BufferedReader offers buffered character reading; Scanner offers token parsing. For file output, PrintWriter offers print and println that mirror System.out but send to a file. The three file patterns to practice are BufferedReader with InputStreamReader or FileReader, PrintWriter with FileOutputStream, and Scanner with FileInputStream.

Worked example 1 — PrintWriter stuff.txt try catch and close writing hello world — reinforced.

import java.io.*;
class TextFileOutputDemo {
    public static void main(String args[]){
        PrintWriter outStream = null;
        try{ outStream = new PrintWriter(new FileOutputStream("stuff.txt")); }
        catch(FileNotFoundException e){ System.out.println("Error opening file"); System.exit(0); }
        outStream.println("Hello world");
        outStream.println("Second line");
        outStream.close();
    }
}

Trace: declare outStream as null. Inside try, new FileOutputStream("stuff.txt") asks the OS to create or truncate stuff.txt; wrapping with PrintWriter adds println. If the file cannot be opened, FileNotFoundException is thrown, caught, a message prints, and the program exits instead of crashing with an unchecked exception. On success, two println calls write Hello world and Second line each followed by a newline into the file. close() flushes buffers, releases the file handle, and logically breaks the connection represented as two boxes — program and file — joined by the stream. This try plus catch plus close pattern is the template to copy.

Worked example 2 — Scanner morestuff.txt nextInt nextLine He is a jolly good fellow — reinforced.

import java.util.Scanner; import java.io.FileInputStream; import java.io.FileNotFoundException;
class TextFileScannerDemo {
    public static void main(String args[]){
        System.out.println("I will read three numbers and a line of text from file morestuff.txt");
        Scanner inputStream = null;
        try{ inputStream = new Scanner(new FileInputStream("morestuff.txt")); }
        catch(FileNotFoundException e){ System.out.println("File morestuff.txt not found"); System.exit(0); }
        int n1 = inputStream.nextInt(); int n2 = inputStream.nextInt(); int n3 = inputStream.nextInt();
        inputStream.nextLine(); String line = inputStream.nextLine();
        System.out.println("The three numbers read from file are "+n1+" "+n2+" "+n3);
        System.out.println("The line read from file is "+line);
        inputStream.close();
    }
}

Assume morestuff.txt holds:

1 2 3 4
He is a jolly good fellow

Execution story: prompt prints to console via System.out. try connects Scanner to the file stream; if missing, catch prints and exits. Three nextInt() calls consume 1, 2, 3 into n1,n2,n3 leaving 4 and the newline unread. nextLine() consumes the remainder of the first line including 4's tail, moving the cursor to the start of the second line. nextLine() then reads the full text He is a jolly good fellow into line. Echo prints The three numbers read from file are 1 2 3 and The line read from file is He is a jolly good fellow. close() terminates. The same reading approach works if System.in replaces the file stream.

Picture three diagrams side by side: left shows keyboard -> System.in -> Scanner -> program, middle shows program -> PrintWriter -> stuff.txt, right shows morestuff.txt -> FileInputStream -> Scanner -> program. All three share the stream line metaphor with the medium on one side and the program on the other.

Scope — try catch required. File open may fail because the path is wrong or permissions are lacking; placing new FileInputStream or new FileOutputStream inside try with catch(FileNotFoundException) is not decorative but required for safe code. Modern Java prefers try-with-resources that closes automatically, but the lecture pattern with explicit close() is the exam form to reproduce.

Pitfalls. Forgetting close() leaks handles and may leave the last buffer unwritten. Mixing nextInt() and nextLine() without the intermediate nextLine() leaves a dangling newline and makes the next read skip.

Recap and bridge. A stream is the virtual connection abstraction: System.in for keyboard, System.out for screen, BufferedReader/Scanner for reading and PrintWriter for file writing. The demonstrated life cycle is declare null, open in try, handle FileNotFoundException in catch, read or write, then close. Mastering PrintWriter stuff.txt and Scanner morestuff.txt with nextInt and nextLine closes the revision from in-memory bytecode and objects to external persistence.

Stream try catch required for file open and close to avoid abnormal termination — the warning that turns a runtime crash into a controlled message and exit.

Exam Guidance Summary

The session is positioned as a review and revision covering the material needed to understand object oriented programming, its basics, and related ideas, spanning modules one to seven in the current meeting. Time for the review is about two hours for this meeting, with a second meeting to cover the later modules.

  • Bytes and execution carry weight. Be ready to show the two files created — .java and .class — to name javac as compiler and java as interpreter, and to explain the line first.class is bytecode.
  • Portability questions focus on definition and on the single requirement of a correctly installed JVM per platform.
  • Classes demand precise language: a set of attributes and operations, a collection of member data and member functions in a single unit, a blueprint that allows many objects with local values. The Account and Circle illustrations are likely short-answer sources.
  • Constructors are a frequent trap. Know the two rules — name same as class, no return type — the default values 0, null, 0.0, that an unwritten default is auto-provided only when no parameterized constructor exists, and that once a parameterized constructor is present a no-argument new Account() fails without an explicit no-argument constructor.
  • Variables are split into instance versus static. The static statement to memorize is exactly one copy per class regardless of instantiations, accessed as ClassName.variable.
  • Static block execution is automatic on class loading, before any explicit call, and is the initializer for static fields.
  • this has six usages. At minimum be able to write the disambiguation pattern this.field = field and the method invocation this.display(). The remaining four — constructor delegation, passing as method argument, passing as constructor argument, returning the instance — should be listed.
  • final has three restriction levels — variable as constant, method as non-overridable, class as non-inheritable — and ties directly to the immutable class checklist: final class, final fields, parameterized constructor, getters only, no setters.
  • Arrays require the three declaration forms, the new type[size] linking to consecutive memory, zero-based indexing, the three loop forms including labeled loops, and the Arrays utility methods sort with and without bounds, binarySearch, copyOf, copyOfRange, and fill. The == versus Arrays.equals distinction for arrays is a predicted question.
  • Strings contrast with arrays through the string constant pool. s1 == s2 as true for reused literals, new String creating a separate address as false, and equals for content as true form a standard trio. Immutability versus StringBuffer mutability and the StringTokenizer extraction of four tokens from java programming with objects are the string-to-token steps to rehearse.
  • Streams are the virtual connection abstraction. System.in to keyboard and System.out to screen are the base examples. BufferedReader with InputStreamReader or FileReader for input, PrintWriter with FileOutputStream for output inside try and catch, and Scanner with FileInputStream for file reading with nextInt and nextLine are the three file stream patterns to practice with close at the end.
  • Overall advice for anticipated question types is to present work in tables or listed steps where possible, to write assumptions in full, and to show code in the exact forms given — especially public static void main(String args[]) and System.out.println.

This appendix is retained with the same guidance as the initial lecture notes and expanded for focused study. The review spans modules one to seven and is allocated about two hours in this meeting, with later modules following. Study advice from the lecture: present answers in tables or listed steps, write assumptions in full sentences, and keep code in the exact forms taught — public static void main(String args[]), System.out.println, javac First.java, java First, ABC.c, this.field = field, final class with final fields and getters.

Rehearsal checks: explain why First.class is bytecode not executable, why a correctly installed JVM per platform is the portability requirement, what the three parts of a class definition are with Account and Circle screenshots, the two constructor rules and suppressed-default case, the single-copy static statement with ClassName.variable access, that a static block runs on class loading, the six this usages with emphasis on disambiguation and this.display(), the three final levels plus immutable checklist, array declarations with new type[size] and Arrays.equals versus ==, the string pool s1==s2 / new / equals trio and StringTokenizer four tokens, and the stream try/catch/close with BufferedReader, PrintWriter, and Scanner.

Key Industry Applications

Real-world: Bytecode as a highly optimized portable instruction set with the JVM as platform-specific interpreter enables write-once-run-anywhere delivery, with portability realized by shipping .class files and requiring only a JVM per target operating system.

Real-world: Encapsulation through classes that bundle member data and member functions underlies account management systems where operations such as withdraw, deposit, and check balance operate on account attributes, student record systems where each student object holds its own identifier and marks, and geometric libraries where circle objects expose area and circumference from center and radius.

Real-world: Polymorphism via overloading, illustrated by multiple getData definitions differentiated by argument count, supports flexible APIs where one name handles varied input shapes.

Real-world: Inheritance via extends with reuse of public and protected members underpins framework extension and reuse without rewriting parent logic.

Real-world: Static or class variables accessed as ClassName.variable support shared configuration such as a program-wide title like Java programming, counters of live objects, or cached defaults that must remain single-copy across many instances.

Real-world: Static blocks that run on class loading provide one-time initialization of those shared values before any object is created.

Real-world: The this reference for the current object enables correct construction when parameter names match field names and supports fluent APIs that return the current instance.

Real-world: Immutable classes built with final classes, final fields, and getter-only access model stable values such as amounts, dates, or keys that are safe to share widely.

Real-world: The Arrays utility class offers production-grade operations — bounded and unbounded sort, binarySearch for position finding, copyOf and copyOfRange for buffer duplication, and fill for initialization — used in data preparation, searching, and table setup.

Real-world: The string constant pool optimization reduces memory for repeated literals in large codebases, while StringBuffer provides growable writable sequences for incremental text construction, and StringTokenizer supports word splitting for analysis tasks such as breaking java programming with objects into tokens.

Real-world: Streams as virtual connections — System.in to keyboard and System.out to screen — scale to file and network programming, where BufferedReader with InputStreamReader or FileReader, PrintWriter with FileOutputStream, and Scanner with FileInputStream cover reading and writing for files and sockets, including logging to stuff.txt and ingest from morestuff.txt.

This appendix is retained to keep industry placement with each technical idea, now with a stronger placement sentence for each. Bytecode and JVM enable write-once-run-anywhere deployment pipelines that ship .class files to cloud nodes with per-OS JVMs. Encapsulation in Account and Student models translates to banking and university information systems; Circle shapes translate to CAD and graphics libraries. Overloading supports flexible API layers; extends with public/protected reuse supports framework evolution. static shared state models configuration singletons and live-object counts; static blocks seed that state at load time. this disambiguation keeps constructors correct and underpins fluent builders that return this. Immutable final types underpin safe keys and money values in concurrent services. Arrays utilities support ETL and search steps; string pooling and buffers support high-throughput text services; tokenizers feed indexing; streams with BufferedReader, PrintWriter, and Scanner cover file and socket I/O for reports and ingest.

OODAP Lecture 25 notes · Review and Revision of Object Oriented Programming Fundamentals

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

Sections Breakdown

1Bytecode — The Foundation and Magic of Java

Bytecode is optimized instructions in First.class produced by javac and run by JVM

2Portability and Platform Independence

Portability via one Program.class running on any JVM

3Core Features of Object Oriented Programming

Four features: class/encapsulation, object, polymorphism overloading, inheritance extends

4Classes as Blueprints — Structure and Examples

Class blueprint with Account Student Circle attributes and operations

5Writing, Saving, Compiling and Running a Simple Program

Save First.java, compile javac First.java, run java First from main

6Constructors — Default and Parameterized

Constructor same name no return; default 0 null 0.0 auto only when no param constructor

7Variables — Instance and Static

One copy static ABC.c shared vs per-object a b

8Static Block for Initializing Static Variables

Static block static{I=20} runs once on class loading

9The this Keyword — Reference to the Current Object

this is current object for field disambiguation and this.display

10The final Keyword and Immutable Classes

final variable/method/class and immutable final class final fields getters

11Arrays — Declaration, Memory and the Arrays Utility Class

Array declaration new int[6] length loops Arrays utilities and == vs equals

12Strings — Constant Pool, Immutability, StringBuffer and StringTokenizer

String pool s1==s2 true s2==s3 false equals true and StringTokenizer 4 tokens

13Streams and File Input-Output — BufferedReader, PrintWriter and Scanner

Streams virtual connection System.in System.out BufferedReader PrintWriter Scanner file

14Exam Guidance Summary

Review modules 1-7 two hours exam focus per section

15Key Industry Applications

Bytecode portability to banking and file streams

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.

Bytecode — The Foundation and Magic of Java

Must-know: Bytecode is optimized instructions in First.class produced by javac and run by JVM

⚠️ Top pitfall: Treating First.class as native exe or calling java First.class

Self-check: What does javac produce and what does java do?

Connects to: 25.2

Portability and Platform Independence

Must-know: Portability via one Program.class running on any JVM

⚠️ Top pitfall: Thinking bytecode runs without JVM

Self-check: What must be installed to run Program.class on new OS?

Connects to: 25.1

Core Features of Object Oriented Programming

Must-know: Four features: class/encapsulation, object, polymorphism overloading, inheritance extends

⚠️ Top pitfall: Saying private members inherited

Self-check: Name the four OOP features with keywords

Connects to: 25.4

Classes as Blueprints — Structure and Examples

Must-know: Class blueprint with Account Student Circle attributes and operations

⚠️ Top pitfall: Confusing shared definition with shared values

Self-check: How are Account and Circle similar as blueprints?

Connects to: 25.6

Writing, Saving, Compiling and Running a Simple Program

Must-know: Save First.java, compile javac First.java, run java First from main

⚠️ Top pitfall: Writing white instead of void or using file extension with java

Self-check: Write the exact main signature

Connects to: 25.6

Constructors — Default and Parameterized

Must-know: Constructor same name no return; default 0 null 0.0 auto only when no param constructor

⚠️ Top pitfall: Assuming auto default exists alongside parameterized

Self-check: When does new Account() fail after adding parameterized constructor?

Connects to: 25.7

Variables — Instance and Static

Must-know: One copy static ABC.c shared vs per-object a b

⚠️ Top pitfall: Expecting three copies of static c

Self-check: How many copies of static c after three objects?

Connects to: 25.8

Static Block for Initializing Static Variables

Must-know: Static block static{I=20} runs once on class loading

⚠️ Top pitfall: Expecting static block per new

Self-check: When does static{I=20} run?

Connects to: 25.9

The this Keyword — Reference to the Current Object

Must-know: this is current object for field disambiguation and this.display

⚠️ Top pitfall: Writing accountNumber=accountNumber without this

Self-check: How to fix accountNumber=accountNumber bug?

Connects to: 25.10

The final Keyword and Immutable Classes

Must-know: final variable/method/class and immutable final class final fields getters

⚠️ Top pitfall: Leaving setters in immutable class

Self-check: List immutable checklist

Connects to: 25.12

Arrays — Declaration, Memory and the Arrays Utility Class

Must-know: Array declaration new int[6] length loops Arrays utilities and == vs equals

⚠️ Top pitfall: Using == for array content

Self-check: What does arr1==arr2 print for {1,2,3}?

Connects to: 25.12

Strings — Constant Pool, Immutability, StringBuffer and StringTokenizer

Must-know: String pool s1==s2 true s2==s3 false equals true and StringTokenizer 4 tokens

⚠️ Top pitfall: Using == for string content

Self-check: Why does s1==s2 true but s2==s3 false?

Connects to: 25.13

Streams and File Input-Output — BufferedReader, PrintWriter and Scanner

Must-know: Streams virtual connection System.in System.out BufferedReader PrintWriter Scanner file

⚠️ Top pitfall: Missing try catch or close

Self-check: Why is FileNotFoundException try catch required?

Connects to: 25.11

Exam Guidance Summary

Must-know: Review modules 1-7 two hours exam focus per section

⚠️ Top pitfall: Check details carefully

Self-check: Explain the concept in one sentence

Connects to: None

Key Industry Applications

Must-know: Bytecode portability to banking and file streams

⚠️ Top pitfall: Check details carefully

Self-check: Explain the concept in one sentence

Connects to: None

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.