Complete Java Notes Detailed Edition
Complete Java Notes Detailed Edition
Detailed Edition
Table of Contents
1. Introduction to Java
2. Setting Up the Environment
3. Anatomy of a Java Program
4. Variables, Data Types, and Memory
5. Operators (Complete Reference)
6. Taking Input from the User
7. Control Flow Statements
8. Loops
9. Arrays
10. Strings
11. Methods (Functions) In Depth
12. Object-Oriented Programming — Classes and Objects
13. Constructors
14. Static vs Instance Members
15. Inheritance
16. Polymorphism
17. Encapsulation
18. Abstraction (Abstract Classes and Interfaces)
19. Packages and Access Modifiers
20. Exception Handling
21. Generics
22. Collections Framework
23. File Handling (I/O)
24. Multithreading
25. Enums
26. Wrapper Classes and Utility Classes
27. Common Beginner Mistakes (Explained)
28. Practice Programs with Explanations
29. Keywords Cheat Sheet
1. Introduction to Java
Java was originally created for embedded consumer electronics, but it found its real success on the web
and later in enterprise, mobile (Android), and server-side backend systems. Today Java powers
everything from Android apps to banking systems to large-scale distributed backends (Netflix, LinkedIn,
Amazon all use Java extensively).
1.2 Why Java Was Revolutionary: “Write Once, Run Anywhere” (WORA)
Before Java, most compiled languages (like C or C++) compiled source code directly into machine code
specific to one operating system and processor architecture. A program compiled for Windows would not
run on Linux or macOS without recompilation.
1. The Java compiler ( javac ) does not produce native machine code. Instead, it produces an
intermediate form called bytecode, stored in .class files.
2. This bytecode is not tied to any specific operating system or CPU. It is executed by a program
called the Java Virtual Machine (JVM).
3. Every operating system has its own version of the JVM, but the bytecode itself is identical
everywhere.
This means you compile your code once, and the resulting .class file can run on any machine that
has a JVM installed — Windows, Linux, macOS, or embedded devices — without changes. This is the
meaning of “Write Once, Run Anywhere.”
[ [Link] ]
|
| javac (compiler)
v
[ [Link] ] <-- bytecode (platform independent)
|
| distributed to any machine
v
[ JVM on Windows ] [ JVM on Linux ] [ JVM on macOS ]
| | |
runs runs runs
These three acronyms confuse almost every beginner, so let’s break them down layer by layer, from the
inside out.
JVM (Java Virtual Machine) This is the engine that actually executes the bytecode. It is an abstract
computing machine — it does not exist as a single physical thing, but as a specification implemented
differently on each OS (HotSpot JVM, OpenJ9, etc.). The JVM’s jobs include: - Loading .class files
(Class Loader subsystem) - Verifying bytecode for safety (Bytecode Verifier) - Executing bytecode
(Execution Engine — includes an interpreter and the JIT compiler) - Managing memory automatically
(Garbage Collector)
JRE (Java Runtime Environment) This is the JVM plus the standard class libraries (like [Link] ,
[Link] , [Link] ) and supporting files needed to actually run a compiled Java program. If you only
want to run Java applications (not develop them), the JRE is sufficient.
JDK (Java Development Kit) This is the JRE plus development tools: the compiler ( javac ), the
debugger ( jdb ), the archiver ( jar ), documentation generator ( javadoc ), and more. If you want to
write and compile Java code, you need the JDK. (In modern Java distributions, JRE is no longer
distributed separately — installing the JDK gives you everything.)
JDK
|-- Development Tools (javac, javadoc, jar, jdb...)
|-- JRE
|-- Class Libraries ([Link], [Link], [Link]...)
|-- JVM
|-- Class Loader
|-- Bytecode Verifier
|-- Execution Engine (Interpreter + JIT Compiler)
|-- Garbage Collector
1. Download a JDK distribution. Popular free options include Oracle OpenJDK, Eclipse Temurin
(AdoptOpenJDK), or Amazon Corretto.
2. Run the installer for your OS (Windows .msi , macOS .pkg , or Linux package manager, e.g. sudo apt
install openjdk-21-jdk ).
3. Configure environment variables (usually automatic on modern installers):
JAVA_HOME should point to the JDK installation directory.
The bin folder inside that directory should be added to your system PATH so that java and
javac commands are available from any terminal location.
java -version
javac -version
If both commands print a version number, your setup is complete. A mismatch (e.g., java shows one
version and javac shows a very different one) usually indicates multiple JDKs are installed and your
PATH is picking up the wrong one.
Plain text editor + terminal: good for absolute beginners to understand the compile/run cycle
without abstraction (Notepad++, VS Code with no plugins).
VS Code with the “Extension Pack for Java”: lightweight IDE-like experience, free.
IntelliJ IDEA (Community Edition): the most popular full-featured Java IDE, free and very
beginner-friendly with excellent error highlighting and refactoring tools.
Eclipse: another long-standing free, full-featured IDE, widely used in academic settings.
If your class belongs to a package (see Section 19), you must compile and run it while respecting the
folder structure that mirrors the package name, and run it using the fully qualified name, e.g. java
[Link] .
package [Link]; Packages are Java’s namespacing mechanism — a way to organize related
classes into folders/groups and avoid naming collisions between classes from different libraries. This
line, if present, must be the very first non-comment line in the file.
import [Link]; Tells the compiler where to find a class you’re referencing by its short name
( Scanner ) instead of writing its fully qualified name ( [Link] ) every time. You do not need to
import classes from [Link] (like String , System , Math ) — these are imported automatically into
every Java file.
public class HelloWorld { Declares a class named HelloWorld . Rules: - A .java source file can contain
at most one public top-level class. - If a public class exists, the file name must exactly match the
class name, including capitalization ( [Link] ). - A file can contain multiple non-public classes
in addition to one public one.
public static void main(String[] args) { This is the method the JVM looks for and calls automatically
when your program starts. It must have exactly this signature: - public — the JVM (which is external to
your class) must be able to call it, so it cannot be private or protected . - static — the JVM calls main
without first creating an object of your class, so the method must belong to the class itself, not to an
instance. - void — main does not return any value back to the JVM. - main — this exact name is what
the JVM looks for. - (String[] args) — an array of command-line arguments passed to the program is
delivered here. args can be any name, but the type must be String[] (or the equivalent varargs form
String... args ).
3.2 Comments
/* multi-line
comment — everything between /* and */ is ignored */
/**
* Documentation comment (Javadoc).
* Used to auto-generate HTML documentation with the `javadoc` tool.
* @param name the name to greet
*/
Comments are not executed; they exist purely to help human readers (including your future self)
understand the code’s intent.
A variable is a named piece of memory that holds a value of a particular type. In Java, every variable
must have a declared type, and that type never changes for the lifetime of the variable (Java is a
statically typed language — type checking happens at compile time, not at runtime like in Python or
JavaScript).
int age = 25; // 'age' is a container that can only ever hold an int
Understanding memory layout demystifies a lot of confusing Java behavior later on (especially around
object references).
The Stack: Stores local variables (method parameters and variables declared inside a method) and
the primitive values assigned directly to them. Each method call gets its own “stack frame,” which is
destroyed automatically when the method returns. This is very fast memory.
The Heap: Stores all objects (anything created with new , including arrays and instances of classes)
. Objects live on the heap until no references to them remain, at which point the Garbage Collector
reclaims that memory automatically — you never manually free memory in Java.
void example() {
int x = 10; // x (value 10) lives on the stack
Car myCar = new Car(); // 'myCar' (a reference/pointer) lives on the stack
// the actual Car OBJECT lives on the heap
}
When myCar is assigned to another variable, only the reference (memory address) is copied — both
variables end up pointing to the same object on the heap.
Java has exactly 8 primitive types. Unlike objects, primitives store their actual value directly (not a
reference), and they are not part of the object hierarchy (they have no methods of their own).
≈ -9.2 quintillion to
long 8 bytes (64 bits) 0L long l = 100000L;
9.2 quintillion
~7 decimal digits of
float 4 bytes 0.0f float f = 3.14f;
precision
single 16-bit
char 2 bytes '\u0000' Unicode character char c = 'A';
(0 to 65,535)
JVM-dependent true or false boolean flag =
boolean false
(conceptually 1 bit) only true;
Notes: - long literals need an L suffix ( 100000L ) so the compiler knows to treat the literal as 64-bit, not
32-bit. - float literals need an f suffix ( 3.14f ); without it, a decimal literal like 3.14 is treated as a
double by default. - char is actually a numeric type under the hood — it stores a Unicode code point, so
you can do arithmetic on characters: char nextLetter = (char)('a' + 1); // 'b' .
Anything that is not a primitive is a reference type: String , arrays, and every object created from a
class (including your own custom classes). A reference type variable holds the memory address of the
object, not the object’s data directly. The default value of an uninitialized reference variable is null
(meaning “points to nothing”).
int i = 100;
long l = i; // int -> long, automatic
double d = l; // long -> double, automatic
Widening order: byte -> short -> int -> long -> float -> double (char also widens to int and beyond).
Narrowing (explicit) conversion — you must manually cast, because data loss or unexpected results
are possible:
double d = 9.78;
int i = (int) d; // 9 - the fractional part is simply truncated (not rounded!)
void method() {
int localVar = 3; // local variable - exists only during this method call
}
}
Local variables must be initialized before use (the compiler enforces this) and only exist within the
block { } where they are declared.
Instance variables belong to a specific object; every object has its own copy. They get a default
value automatically (0, false, or null) even if not explicitly initialized.
Static variables belong to the class itself, not any one object — there is only ever one copy, shared
across every instance (see Section 14 for a deep dive).
Identifiers may contain letters, digits, $ , and _ , but cannot start with a digit.
Cannot be a reserved keyword ( class , if , int , new , etc.).
Java is case-sensitive: age and Age are different identifiers.
Convention (not enforced by the compiler, but expected by every Java developer):
camelCase for variables and methods: studentAge , calculateTotal()
PascalCase for classes and interfaces: StudentRecord , Runnable
UPPER_SNAKE_CASE for constants: MAX_SIZE
all-lowercase for packages: [Link]
int a = 10, b = 3;
[Link](a + b); // 13 addition
[Link](a - b); // 7 subtraction
[Link](a * b); // 30 multiplication
[Link](a / b); // 3 integer division - fractional part discarded!
[Link](a % b); // 1 modulus (remainder)
double x = 10, y = 3;
[Link](x / y); // 3.3333... - division is only integer division when BOTH operands are integer types
Important trap for beginners: int / int always produces an int result (truncated), even if you
assign it to a double variable:
double result = 10 / 3; // result is 3.0, NOT 3.333! The division happens as int/int first.
double correct = 10.0 / 3; // 3.333... - at least one operand must be a floating type
a == b // equal to
a != b // not equal to
a > b // greater than
a < b // less than
a >= b // greater than or equal to
a <= b // less than or equal to
Important trap: for reference types like String , == compares memory addresses (whether two
variables point to the same object), not content. Use .equals() to compare content — see Section 10.
// Safe: if arr is null, the length check on the right is never evaluated
if (arr != null && [Link] > 0) { ... }
Java also has non-short-circuiting logical operators & and | , which always evaluate both sides — rarely
used for boolean logic but common for bitwise operations (see below).
int x = 10;
x += 5; // same as x = x + 5; -> 15
x -= 3; // x = x - 3; -> 12
x *= 2; // x = x * 2; -> 24
x /= 4; // x = x / 4; -> 6
x %= 4; // x = x % 4; -> 2
int x = 5;
int y = x++; // POST-increment: y gets 5 (old value), THEN x becomes 6
int z = ++x; // PRE-increment: x becomes 7 FIRST, then z gets 7
This distinction commonly trips up beginners inside loop conditions or array indexing — always be clear
on whether the increment happens before or after the value is used.
int a = 5; // 0101
int b = 3; // 0011
When in doubt, use parentheses () to make evaluation order explicit — it costs nothing and prevents
subtle bugs.
import [Link];
[Link](name + " is " + age + " years old with GPA " + gpa);
nextInt() , nextDouble() , etc. only consume the token itself, leaving the trailing newline character
in the input buffer. If you call nextLine() right after, it immediately reads that leftover empty newline
instead of waiting for new input, which looks like your program “skipped” a line.
nextInt() an int
nextDouble() a double
nextLong() a long
hasNext() / hasNextInt() etc. check whether more input is available (useful in loops)
import [Link];
import [Link];
import [Link];
BufferedReader is generally faster for reading large amounts of input (common in competitive
programming) but requires you to manually parse strings into numbers using methods like
[Link]() .
The most fundamental decision-making structure. Conditions are evaluated top to bottom; the first true
branch executes and the rest are skipped.
Java requires the condition inside if (...) to be a boolean expression — unlike C, you cannot write if
(1) expecting it to mean “true.”
An alternative to long if-else if chains when comparing one variable against many discrete values.
int day = 3;
switch (day) {
case 1:
[Link]("Monday");
break;
case 2:
[Link]("Tuesday");
break;
case 3:
[Link]("Wednesday");
break;
default:
[Link]("Invalid day");
}
Fall-through behavior: without break , execution continues into the next case rather than exiting the
switch. This is sometimes used intentionally to group cases:
switch (day) {
case 6:
case 7:
[Link]("Weekend");
break;
default:
[Link]("Weekday");
}
The newer arrow syntax removes fall-through entirely and can directly produce a value.
8. Loops
Anatomy: for (initialization; condition; update) . 1. initialization runs once, before the loop starts.
2. condition is checked before every iteration; if false , the loop ends. 3. The loop body runs. 4. update
runs after every iteration. 5. Go back to step 2.
Best when the number of iterations is not known ahead of time, and depends on some condition
evaluated during the loop.
int i = 1;
while (i <= 5) {
[Link](i);
i++;
}
The condition is checked before each iteration — if it’s false at the very start, the loop body never runs
at all.
Same as while , except the condition is checked after the loop body — guaranteeing the body executes
at least once, which is useful for things like input-validation menus.
int i = 1;
do {
[Link](i);
i++;
} while (i <= 5);
Used to iterate over every element of an array or collection without manually managing an index
variable.
Limitation: you cannot get the current index, and you cannot modify the original array through n (it’s a
copy of each element for primitives).
By default, break / continue only affect the innermost loop. Labels let you target an outer loop.
outer:
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (j == 2) continue outer; // skips to next iteration of the OUTER loop
[Link](i + "," + j);
}
}
9. Arrays
An array is a fixed-size, ordered collection of elements of the same type, stored in a single contiguous
block of memory on the heap. Because array size is fixed at creation, you cannot add or remove elements
later — you’d need to create a new array (this is one of the main reasons the Collections framework’s
ArrayList , covered later, is often preferred).
Default values when using new type[size] : 0 for numeric types, false for boolean, '\u0000' for char,
null for reference types.
Arrays are zero-indexed: the first element is at index 0 , and the last element is at index length - 1 .
Accessing an index outside the valid range (e.g., numbers[5] when length is 5) throws an
ArrayIndexOutOfBoundsException at runtime — the compiler cannot catch this ahead of time.
import [Link];
10. Strings
String s = "hello";
[Link](); // this does NOTHING to s - the return value is discarded!
[Link](s); // still prints "hello"
To save memory, Java maintains a special memory region called the String pool (part of the heap).
String literals (written directly in code with quotes) are automatically placed in this pool, and identical
literals are reused rather than duplicated.
String a = "hello";
String b = "hello";
[Link](a == b); // true - both point to the SAME pooled object
String c = new String("hello"); // explicitly forces a NEW object outside the pool
[Link](a == c); // false - different objects in memory, even though content is equal!
[Link]([Link](c)); // true - .equals() compares CONTENT, not memory address
Rule of thumb: always use .equals() (or .equalsIgnoreCase() ) to compare String content —
never == .
// Concatenation
String s1 = "Name: " + name + ", Age: " + age;
// printf directly
[Link]("Name: %s, Age: %d%n", name, age);
Common format specifiers: %s (String), %d (integer), %f (floating point, e.g. %.2f for 2 decimal
places), %n (platform-independent newline).
Because every String “modification” creates a brand-new object, building up a string piece by piece
inside a loop using += is inefficient — it creates a new object on every single iteration, wasting memory
and CPU time. StringBuilder solves this by using an internally resizable, mutable character array.
Rule of thumb: use String for values that don’t change or change rarely; use StringBuilder when
doing many concatenations, especially inside loops. ( StringBuffer is an older, thread-safe but slower
cousin of StringBuilder — prefer StringBuilder unless multiple threads modify the same buffer
concurrently.)
This is one of the most misunderstood topics for beginners. Java always copies the value of an
argument into the method’s parameter. The behavior differs depending on whether that value is a
primitive or a reference:
For primitives: the actual value is copied. Changes to the parameter inside the method have zero effect
on the original variable.
int num = 5;
increment(num);
[Link](num); // still 5 - 'x' was a completely separate copy
For objects: the value copied is the reference (the memory address), not the object itself. This means
both the original variable and the parameter point to the same object — so changes made through the
reference (like modifying a field) are visible from outside. However, reassigning the parameter to point
somewhere else does not affect the original variable.
reassign(myCar);
[Link]([Link]); // still 200 - reassignment inside the method didn't propagate out
Multiple methods can share the same name as long as their parameter lists differ (in number, type, or
order of parameters). The compiler picks the correct version to call based on the arguments provided.
This is resolved at compile time, so it’s called “compile-time polymorphism.”
static int add(int a, int b) { return a + b; }
static double add(double a, double b) { return a + b; }
static int add(int a, int b, int c) { return a + b + c; }
Note: overloading based only on return type (keeping the same parameters) is not allowed — the
compiler wouldn’t be able to tell which one you meant just from the call.
Lets you pass any number of arguments of the same type without manually creating an array.
static int sum(int... numbers) { // 'numbers' behaves like an int[] inside the method
int total = 0;
for (int n : numbers) total += n;
return total;
}
sum(); // 0
sum(5); // 5
sum(1, 2, 3, 4); // 10
11.6 Recursion
A method that calls itself to solve a smaller version of the same problem. Every recursive method needs
a base case (a condition that stops the recursion) to avoid infinite recursion, which eventually crashes
with a StackOverflowError .
Trace of factorial(4) :
factorial(4) = 4 * factorial(3)
factorial(3) = 3 * factorial(2)
factorial(2) = 2 * factorial(1)
factorial(1) = 1 <- base case reached
// unwinding: 2*1=2, 3*2=6, 4*6=24
A class is a blueprint or template — it defines what data (fields) and behavior (methods) objects of that
type will have, but a class by itself holds no actual data. An object (also called an “instance”) is a
concrete thing created from that blueprint, with its own actual values stored in memory.
Analogy: Car the class is like an architectural blueprint for a car. myCar and yourCar are objects —
actual cars built from that blueprint, each possibly painted a different color or with a different amount of
fuel.
public class Car {
// fields (a.k.a. instance variables) - describe the STATE of each object
String brand;
String color;
int speed;
void displayInfo() {
[Link](brand + " (" + color + ") going " + speed + " km/h");
}
}
Car yourCar = new Car(); // a SEPARATE object with its own independent fields
[Link] = "Honda";
[Link] = "Blue";
[Link](40);
[Link](); // "Honda (Blue) going 40 km/h" - independent of myCar
}
}
Inside an instance method or constructor, this refers to the specific object the method was called on.
It’s most commonly needed to disambiguate between a field and a parameter/local variable that share
the same name.
1. Encapsulation — bundling data and the methods that operate on it together, hiding internal details
(Section 17)
2. Inheritance — a class acquiring fields and methods from a parent class (Section 15)
3. Polymorphism — the same method call behaving differently depending on the actual object (Section
16)
4. Abstraction — exposing only essential features while hiding implementation complexity (Section 18)
13. Constructors
// Constructor
Student(String name, int age) {
[Link] = name;
[Link] = age;
[Link]("A new Student object was created!");
}
}
Student s1 = new Student("Alice", 20); // prints the message, then [Link]="Alice", [Link]=20
If you don’t write any constructor at all, Java automatically provides an invisible no-argument “default
constructor” that does nothing but initialize fields to their default values (0, false, null). The moment you
write any constructor yourself, this automatic default constructor disappears — if you still want a no-
argument constructor, you must write it explicitly.
Just like methods, a class can have multiple constructors as long as their parameter lists differ, giving
callers flexible ways to create objects.
{ // instance initializer block - runs before the constructor body, every time an object is made
x = 10;
[Link]("Initializer block ran");
}
Example() {
[Link]("Constructor ran, x = " + x);
}
}
This distinction is fundamental and worth its own section, since it explains why main must be static
and clarifies a lot of confusing beginner errors.
Instance fields and methods belong to a specific object. Each object gets its own independent copy of
every instance field. You must create an object before you can access its instance members.
A static field or method belongs to the class, not to any individual object. There is only ever one copy
of a static field, shared by every object of that class (and even accessible without creating any object at
all).
public class Counter {
static int totalCounters = 0; // ONE shared copy across ALL Counter objects
int count = 0; // each object's own copy
Counter() {
totalCounters++; // every time a new Counter is made, the shared count increases
}
}
The JVM needs to call main before any object of your class exists. Since static methods belong to the
class itself and don’t require an object to be called, this is exactly what makes public static void
main(...) callable as the program’s entry point.
A static method has no associated object ( this doesn’t exist inside it), so it cannot directly reference
instance fields or call instance methods — it would need an explicit object reference to do so.
Because they don’t require an object, static methods are perfect for pure “utility” operations that don’t
depend on any particular object’s state — this is exactly why [Link]() , [Link]() , and
[Link]() are all static.
15. Inheritance
Inheritance lets a class (the subclass or child class) acquire the fields and methods of another class
(the superclass or parent class), using the extends keyword. This models “is-a” relationships (a Dog
is an Animal ) and enables code reuse — shared behavior lives in one place instead of being duplicated
across similar classes.
class Animal {
String name;
void eat() {
[Link](name + " is eating");
}
void sleep() {
[Link](name + " is sleeping");
}
}
Dog automatically has everything Animal has ( name , eat() , sleep() ), plus its own additional method
bark() . Animal itself has no idea Dog exists — the relationship only flows from child to parent, never
the other way.
super refers to the parent class, and is used for two main purposes:
class Animal {
String name;
Animal(String name) {
[Link] = name;
[Link]("Animal constructor ran");
}
}
If you don’t explicitly call super(...) , Java automatically inserts a call to the parent’s no-argument
constructor as the first line. If the parent class has no no-argument constructor available, this causes a
compile error, forcing you to call super(...) explicitly with matching arguments.
When a subclass defines an instance method with the exact same signature as one in its parent, it
overrides it — the subclass’s version is used whenever called on a subclass object (see Polymorphism,
Section 16). When a subclass defines a static method with the same signature as a static parent method,
this is called hiding, not overriding, and it behaves very differently (resolved at compile time based on
the reference type, not the actual object type) — a subtlety usually only relevant once you’re comfortable
with the basics.
Single inheritance: one subclass, one direct superclass ( class B extends A ) — this is what Java
supports for classes.
Multilevel inheritance: a chain of inheritance ( class C extends B extends A ) — C inherits from B ,
which inherits from A .
Hierarchical inheritance: multiple subclasses share the same single superclass ( class Dog extends
Animal , class Cat extends Animal ).
Java deliberately does NOT support multiple inheritance of classes (a class extending two or more
classes at once) — this is disallowed specifically to avoid the “Diamond Problem”: if two parent classes
both defined a conflicting method, the compiler couldn’t unambiguously decide which version the child
should inherit. Java sidesteps this entirely by allowing a class to extend only one class, while still
permitting it to implement multiple interfaces (Section 18), which don’t carry the same conflict risk.
If a class doesn’t explicitly extend anything, Java implicitly makes it extend [Link] . This
means every single class in Java automatically has methods like toString() , equals() , and hashCode()
available, which you can override for custom behavior.
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public String toString() {
return "(" + x + ", " + y + ")";
}
}
“Poly” (many) + “morph” (forms) — the ability for the same method call to behave differently depending
on context. Java has two kinds.
Already covered in Section 11.4 — multiple methods share a name but differ in parameters, and the
compiler decides which one to call based on the arguments at compile time.
When a subclass provides its own implementation of a method that’s already defined in its superclass
(same name, same parameters), calling that method on a subclass object uses the subclass’s version,
even if you’re referring to the object through a superclass-typed variable.
class Animal {
void sound() { [Link]("Animal makes a sound"); }
}
Even though the declared type of a1 is Animal , the JVM looks at the actual object it points to at runtime
(a Cat ) to decide which sound() implementation to run. This is called dynamic method dispatch, and
it’s what makes polymorphism genuinely powerful: you can write code that operates on the general
Animal type, and it will automatically behave correctly for whatever specific subtype is actually passed
in — without needing if/else chains checking types.
Placing @Override above a method that’s meant to override a parent method is optional, but strongly
recommended: it tells the compiler to verify that a matching method genuinely exists in the superclass.
Without it, a small typo in the method signature (wrong parameter type, misspelled name) silently
creates an unrelated new method instead of overriding, and the bug can be very hard to spot.
Cat c = (Cat) a; // downcasting - must be explicit, and only safe if 'a' REALLY points to a Cat
Downcasting to the wrong type throws a ClassCastException at runtime. Use instanceof to check first if
you’re unsure:
if (a instanceof Cat) {
Cat c = (Cat) a; // safe
}
17. Encapsulation
Encapsulation means bundling an object’s data (fields) together with the methods that operate on that
data, while restricting direct outside access to the internal fields. Instead, controlled access is
provided through public methods — typically getters (to read a value) and setters (to change a value,
often with validation).
Without encapsulation, any part of a program could set a field to an invalid state:
class BankAccount {
public double balance; // BAD: directly public, no protection
}
With encapsulation, the field is hidden ( private ), and all access is routed through methods that can
enforce rules:
class BankAccount {
private double balance; // hidden from outside code entirely
Standard naming convention used throughout the Java ecosystem (and required by many frameworks
and tools):
// for boolean fields, the getter conventionally uses "is" instead of "get"
private boolean active;
public boolean isActive() {
return active;
}
Abstraction means exposing only the essential, high-level features of something while hiding the
complex implementation details behind them. When you drive a car, you use the steering wheel and
pedals (the abstraction) without needing to know how the engine’s internal combustion actually works
(the implementation). Java provides two language mechanisms for abstraction: abstract classes and
interfaces.
An abstract class is declared with the abstract keyword. It cannot be instantiated directly (you can
never write new Shape() if Shape is abstract) — it exists only to be extended. It can mix: - Abstract
methods: declared but with no body — subclasses are forced to provide an implementation. - Concrete
methods: fully implemented, inherited as-is (or optionally overridden) by subclasses. - Regular fields,
constructors, and static methods.
abstract class Shape {
String color;
@Override
double area() { // MUST implement this - otherwise Circle would ALSO have to be abstract
return [Link] * radius * radius;
}
}
@Override
double area() {
return width * height;
}
}
18.3 Interfaces
An interface defines a pure contract: a set of method signatures that any implementing class promises
to provide, without dictating how. Before Java 8, every method in an interface was implicitly public
abstract (no body allowed at all). Since Java 8, interfaces can also include default methods (with a
body, providing a fallback implementation) and static methods.
interface Drawable {
void draw(); // implicitly public abstract
interface Resizable {
void resize(double factor);
}
@Override
public void draw() {
[Link]("Drawing a square with side " + side);
}
@Override
public void resize(double factor) {
side *= factor;
}
}
All fields declared in an interface are implicitly public static final (constants) — interfaces cannot
hold ordinary instance state.
A class can implement as many interfaces as it wants, because interfaces (traditionally) only specify
what must be done, not how — there’s no field-level state to create ambiguity, so no diamond-problem
conflict arises the way it would with multiple class inheritance.
Instantiable? No No
Constructors Yes No
A package is Java’s way of grouping related classes together, similar to how folders organize files.
Packages prevent naming collisions (two different libraries can each have a class named Utils as long
as they’re in different packages) and provide a natural access-control boundary.
package [Link];
The folder structure on disk must mirror the package name: a class in package [Link] must
live inside a folder path com/example/myapp/ .
Classes in [Link] (like String , Math , System , Object ) are automatically available everywhere
without any import.
Subclass Everywhere
Modifier Same Class Same Package (different (different
package) package)
private Yes No No No
(default / no
Yes Yes No No
modifier)
private : only code inside the exact same class can access it. The strictest option — used for internal
implementation details, typically fields (see Encapsulation, Section 17).
default (package-private): if you write no modifier at all, the member is accessible to any class
within the same package, but invisible outside it.
protected : accessible within the same package, AND accessible to subclasses even if they live in a
different package (commonly used for members meant to be extended/customized by subclasses).
public : accessible from absolutely anywhere.
package [Link];
A well-designed class typically keeps fields private and exposes controlled access through public
methods — this is the essence of encapsulation. Access modifiers on classes themselves are usually just
public (visible everywhere) or default (visible only within the package, useful for internal helper classes
not meant to be part of a library’s public API).
An exception is an event that disrupts the normal flow of a program’s instructions — typically an error
condition detected at runtime (dividing by zero, accessing an invalid array index, trying to open a file
that doesn’t exist). Without handling, an exception terminates the program (or at least the current
thread) and prints a stack trace. Java’s exception-handling mechanism lets you detect and gracefully
respond to these situations instead.
Throwable
/ \
Error Exception
(serious, usually / \
unrecoverable — RuntimeException (checked exceptions,
e.g. OutOfMemoryError) | e.g. IOException,
(unchecked exceptions — SQLException)
e.g. NullPointerException,
ArithmeticException,
ArrayIndexOutOfBoundsException)
Error : represents serious problems a normal application generally shouldn’t try to catch (e.g.,
OutOfMemoryError , StackOverflowError ) — usually indicates something is fundamentally wrong with
the JVM or environment.
Exception : represents conditions a program might reasonably want to catch and handle. Splits
further into:
Checked exceptions: subclasses of Exception (but not RuntimeException ). The compiler forces
you to either catch them or declare them with throws — examples include IOException (file
operations) and SQLException (database operations).
Unchecked exceptions: subclasses of RuntimeException . The compiler does not force you to
handle these — they usually represent programming bugs (like a null reference or bad array
index) rather than expected external failure conditions. Examples: NullPointerException ,
ArithmeticException , ArrayIndexOutOfBoundsException , ClassCastException ,
NumberFormatException .
20.3 try-catch-finally
public class Main {
public static void main(String[] args) {
try {
int result = 10 / 0; // this line throws ArithmeticException
[Link]("This never runs");
} catch (ArithmeticException e) {
[Link]("Error: " + [Link]());
} finally {
[Link]("This ALWAYS runs, whether an exception occurred or not");
}
[Link]("Program continues normally");
}
}
Execution flow: 1. Code inside try runs until either it finishes normally, or an exception is thrown. 2. If
an exception is thrown, execution immediately jumps to a matching catch block (skipping the rest of
try ). 3. finally runs no matter what — whether an exception occurred, was caught, or even if the
try / catch contained a return statement. It’s the ideal place for cleanup code (closing files, releasing
resources). 4. If no catch block matches the thrown exception’s type, the exception propagates upward
(out of the current method, to whatever called it), potentially crashing the program if never caught
anywhere.
You can catch different exception types differently. Catch blocks are checked top to bottom, and only the
first matching one runs — so more specific exception types must be listed before more general ones.
try {
int[] arr = new int[5];
arr[10] = 50; // throws ArrayIndexOutOfBoundsException
} catch (ArrayIndexOutOfBoundsException e) {
[Link]("Array error: " + [Link]());
} catch (ArithmeticException e) {
[Link]("Arithmetic error: " + [Link]());
} catch (Exception e) { // general fallback - catches anything not matched above
[Link]("Unexpected error: " + [Link]());
}
If two exception types should be handled identically, you can combine them:
try {
// risky code
} catch (IOException | SQLException e) {
[Link]("I/O or database error: " + [Link]());
}
throw : an actual statement used inside code to trigger an exception right now.
throws : appears in a method’s signature to declare that this method might propagate a checked
exception, requiring callers to handle it.
static void checkAge(int age) throws IllegalArgumentException {
if (age < 18) {
throw new IllegalArgumentException("Must be at least 18 years old");
}
[Link]("Age is valid");
}
You can create your own exception types by extending Exception (checked) or RuntimeException
(unchecked), which is useful for representing domain-specific error conditions clearly.
class BankAccount {
double balance;
20.8 try-with-resources
Any object implementing the AutoCloseable interface (like file streams, scanners, and database
connections) can be declared inside the parentheses of try (...) , and Java automatically closes it
when the block ends — whether normally or due to an exception — eliminating the need for a manual
finally { [Link](); } block.
Before generics (introduced in Java 5), collections like ArrayList stored plain Object references,
meaning you could accidentally put mismatched types into the same list, and you had to manually cast
every element back to its real type when retrieving it — an error-prone process discovered only at
runtime.
Generics let you parameterize a class or method with a specific type, so the compiler enforces
correctness before the program even runs.
ArrayList<String> list = new ArrayList<>(); // this list can ONLY ever hold Strings
[Link]("Hello");
// [Link](42); // COMPILE ERROR - caught immediately, not at runtime!
String s = [Link](0); // no cast needed - the compiler already knows it's a String
class Box<T> { // T is a "type parameter" - a placeholder for whatever type is used later
private T content;
You can restrict a type parameter to be a subtype of a particular class or interface using extends .
static <T extends Number> double sum(T[] numbers) { // T must be Number or a subclass (Integer, Double, ...)
double total = 0;
for (T n : numbers) {
total += [Link]();
}
return total;
}
Every class in the Collections Framework (covered next) is built around generics — this is why you’ll
almost always see angle-bracket syntax like List<String> or Map<String, Integer> when working with
collections.
Arrays have a fixed size decided at creation and offer very few built-in operations (no easy way to insert
in the middle, remove an item, or check for existence). The Collections Framework ( [Link] )
provides flexible, resizable, feature-rich data structures for storing groups of objects, built around a
small set of core interfaces.
Collection
/ | \
List Set Queue
A List maintains insertion order and allows duplicate elements, and lets you access elements by
numeric index (like an array, but resizable).
import [Link];
import [Link];
List<String> names = new ArrayList<>(); // ArrayList is the most common List implementation
[Link]("Alice");
[Link]("Bob");
[Link]("Alice"); // duplicates ARE allowed
[Link](1, "Charlie"); // insert "Charlie" at index 1, shifting others right
ArrayList (backed by a resizable array — fast random access, slower inserts/removes in the middle) vs
LinkedList (backed by a doubly linked list — fast inserts/removes at the ends, slower random access).
U s e ArrayList by default unless you specifically need frequent insertions/removals at arbitrary
positions.
A Set automatically rejects duplicate elements (based on .equals() ), and generally does not guarantee
any particular ordering (with HashSet ) unless you use TreeSet (sorted) or LinkedHashSet (insertion
order preserved).
import [Link];
import [Link];
A Map associates unique keys with values — think of it as a dictionary or lookup table. Keys must be
unique (adding a value with an existing key overwrites the old value); values can be duplicated.
import [Link];
import [Link];
[Link]([Link]("Alice")); // 26
[Link]([Link]("Charlie")); // null - key doesn't exist
[Link]([Link]("Charlie", 0)); // 0 - safe fallback instead of null
[Link]([Link]("Bob")); // true
[Link]("Bob");
for ([Link]<String, Integer> entry : [Link]()) { // iterate over key-value pairs together
[Link]([Link]() + " is " + [Link]() + " years old");
}
import [Link];
import [Link];
import [Link];
Comparable — implemented by the class itself, defining its “natural” default sort order:
@Override
public int compareTo(Student other) {
return [Link] - [Link]; // sorts ascending by age
}
}
Comparator — a separate object defining a custom sort order, useful when you want multiple different
sort orders without modifying the class itself:
import [Link];
Represents a path to a file or directory on disk (does not itself read/write content).
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link].*;
import [Link];
As covered in Section 20.8, wrapping file streams in try (...) guarantees they are closed
automatically, even if an exception occurs mid-read/write — this prevents resource leaks (files being left
“locked” open) which is a very common bug when file handling is done manually with explicit close()
calls that might get skipped due to an early exception.
24. Multithreading
A thread is an independent path of execution within a program. Every Java program has at least one
thread automatically (the “main thread,” which runs your main method). Multithreading means
running multiple threads concurrently, which allows a program to perform multiple tasks seemingly at
the same time (e.g., keeping a UI responsive while downloading a file in the background).
// Or, using a lambda expression (since Runnable has a single abstract method):
Thread t2 = new Thread(() -> [Link]("Lambda thread running"));
[Link]();
}
}
Why Runnable is usually preferred: since Java classes can only extend one class, if your class already
extends something else, it cannot also extend Thread . Implementing Runnable keeps your class free to
extend another class while still being runnable on a thread — it also cleanly separates “the task to run”
from “the mechanism that runs it.”
A thread moves through several states: New (created but not started) → Runnable (started, eligible to
run, waiting for CPU time) → Running (actively executing) → Blocked/Waiting (paused, e.g. waiting for
a lock or sleep() ) → Terminated (finished executing run() ).
[Link](1000); // pauses the CURRENT thread for ~1000 milliseconds (checked exception: InterruptedException)
[Link](); // makes the CALLING thread wait until t1 finishes before continuing
When multiple threads access and modify shared data at the same time without coordination,
unpredictable results can occur — this is called a race condition.
class Counter {
int count = 0;
void increment() {
count++; // NOT ATOMIC! This is actually: read count, add 1, write count back - 3 separate steps
}
}
If two threads call increment() at nearly the same instant, both might read the same starting value
before either writes back the incremented result, causing one increment to be silently lost.
Ensures that only one thread at a time can execute a particular method or block for a given object,
preventing race conditions on shared data.
class Counter {
int count = 0;
synchronized void increment() { // only one thread can be inside this method at a time, per object
count++;
}
}
Synchronization has a performance cost (threads must wait their turn), so it should be applied only to
the specific sections of code that actually touch shared, mutable state — not entire programs
indiscriminately.
Multithreading is a deep topic (thread pools, [Link] utilities like ExecutorService , locks,
atomic variables, and more lie beyond this introduction) — but understanding Thread / Runnable ,
start() vs run() , and the basic idea of race conditions and synchronized gives you a solid foundation
to build on.
25. Enums
An enum (enumeration) is a special data type that represents a fixed set of named constant values.
It’s ideal for situations where a variable should only ever hold one of a small, known set of options (days
of the week, directions, states of an order, card suits, etc.) — far safer than using arbitrary int codes or
raw String s, since the compiler enforces that only valid values are used.
enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
Unlike in some languages where enums are just glorified integers, Java enums are full classes under the
hood — they can have fields, constructors, and methods, and each constant is technically a singleton
instance of the enum type.
enum Planet {
MERCURY(3.3e23, 2.4e6),
VENUS(4.9e24, 6.1e6),
EARTH(5.9e24, 6.4e6);
Planet(double mass, double radius) { // enum constructors are always implicitly private
[Link] = mass;
[Link] = radius;
}
double surfaceGravity() {
final double G = 6.67300E-11;
return G * mass / (radius * radius);
}
}
[Link]([Link]());
Day d = [Link];
[Link]([Link]()); // "MONDAY" - the constant's exact name as a String
[Link]([Link]()); // 0 - position in the declaration order (zero-based)
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean
Java automatically converts between a primitive and its wrapper when needed, without you having to
write explicit conversion code.
int a = 10;
Integer obj = a; // AUTOBOXING - int automatically wrapped into an Integer object
int b = obj; // UNBOXING - Integer automatically unwrapped back into an int
Caution: autoboxing/unboxing has a small performance cost and a notable trap — comparing wrapper
objects with == compares references (like Strings), not values, for values outside a small internally
cached range:
import [Link];
Random rand = new Random();
int n = [Link](100); // random int from 0 to 99 (exclusive upper bound)
Why it happens: == works correctly for primitives (comparing actual values) but for objects it
compares references. This is one of the single most common bugs for people coming from other
languages.
switch (x) {
case 1:
[Link]("one"); // if x==1 and break is missing, execution FALLS THROUGH
case 2:
[Link]("two"); // this ALSO runs even though x wasn't 2!
break;
}
Fix: always include break at the end of each case unless fall-through is deliberate (and comment it
clearly if so).
double avg = 5 / 2; // WRONG - this is int/int = 2 (as an int), THEN converted to 2.0
double avg2 = 5.0 / 2; // CORRECT - 2.5, because at least one operand is already a double
Arrays are zero-indexed with valid indices from 0 to length - 1 . Using <= with .length causes an
ArrayIndexOutOfBoundsException on the final iteration.
27.5 NullPointerException
String s = null;
[Link]([Link]()); // throws NullPointerException - can't call a method on "nothing"
Fix: check for null before use, or ensure objects are always properly initialized:
if (s != null) {
[Link]([Link]());
}
this refers to the current object; super refers specifically to the immediate parent class. Mixing them
up (especially in constructor chaining) causes confusing compile errors, since this(...) and
super(...) are both only valid as the very first statement in a constructor, and you can never use both in
the same constructor.
Fix: always use try-with-resources (Section 20.8) so closing happens automatically and reliably.
Fix: use an explicit Iterator and its .remove() method (Section 22.7), or collect items to remove into a
separate list first and remove them afterward.
Applies to any object type, not just Strings — == should almost always be reserved for primitives (and,
occasionally, explicit reference-identity checks). When in doubt about objects, use .equals() .
class Example {
int value = 10;
void setValue(int value) { // parameter "value" SHADOWS the field "value"
value = value; // WRONG - this just assigns the parameter to itself, does nothing useful!
}
}
Fix: use [Link] = value; to clearly distinguish the field from the parameter (Section 12.2).
if (num <= 1) {
isPrime = false; // numbers 1 and below are never prime by definition
}
// Outer loop: controls how many passes through the array we make
for (int i = 0; i < [Link] - 1; i++) {
// Inner loop: compares each pair of ADJACENT elements,
// and swaps them if they're in the wrong order.
// After each full pass, the largest remaining unsorted element
// "bubbles up" to its correct final position at the end.
for (int j = 0; j < [Link] - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
if ([Link](reversed)) {
[Link](str + " is a palindrome");
} else {
[Link](str + " is not a palindrome");
}
}
}
for (int i = 1; i < [Link]; i++) { // start from index 1 - already have index 0
if (arr[i] > max) {
max = arr[i]; // found a bigger one - update our running maximum
}
}
Keyword Purpose
End of Detailed Notes. The best way to internalize these concepts is to actually type out and run every
code example yourself, then deliberately break them (introduce bugs) to see the error messages Java
produces — learning to read and understand compiler errors and stack traces is one of the most
valuable beginner skills of all.