Packages: Introduction
A package in Java is a structured way to group related classes so that large
applications become easier to organize, maintain, and navigate. The idea is similar to
how we store different types of files in separate folders on a computer. By placing
classes with similar purposes in the same package, Java ensures that programs
remain clear and manageable even as they grow in size.
Packages also help avoid naming conflicts. Two developers might create classes with
the same name, such as Student or Helper . Without packages, these names
would clash. With packages, each class resides in its own namespace, just like two
files with the same name can exist in two different folders. Beyond organization,
packages support controlled access to classes and methods, allowing developers to
hide internal logic while exposing only what is necessary.
Why We Need Packages
Packages offer several important advantages. They provide a clean structure, making
it easy to understand where each class belongs in a project. They improve code
readability, particularly in collaborative or large-scale applications. They allow us to
create reusable modules: once a package is written, it can be imported and used in
any other program. Packages also enhance encapsulation. A class can be made
accessible only inside its package if needed, preventing accidental misuse.
Finally, packages enable the distribution of software as units. Libraries, frameworks,
and APIs are nothing more than collections of packages bundled together to deliver
specific functionality.
Java API Packages
Java includes a vast collection of pre-built packages known as the Java API
(Application Programming Interface). This API is part of the Java Development Kit
(JDK) and provides ready-made classes that solve common programming problems.
Instead of writing every feature from scratch, developers rely on these well-tested
classes to perform input/output tasks, handle data structures, process strings,
manage dates and times, create graphical interfaces, and much more.
The Java API is organized into hundreds of packages, each containing classes and
interfaces focused on a specific task.
Important Java API Packages
1. [Link]
This is the most fundamental package and is imported automatically into every
program. It contains essential classes such as: String
Math
System
Object
Thread
These classes provide basic language features like string handling, mathematical
functions, system-level operations, and thread management.
2. [Link]
This package contains utility classes commonly used in real-world applications. It
includes: ArrayList, LinkedList, HashMap, HashSet
Collections and Arrays helper classes
Scanner for input
Date, Random, Timer
These classes support data structures, algorithms, iteration, and general-purpose
utilities.
3. [Link]
This package handles input and output operations. It includes classes like: File
FileInputStream, FileOutputStream
BufferedReader, InputStreamReader
PrintWriter
These classes help with reading and writing files, handling streams, and performing
byte or character-level processing.
4. [Link] and [Link]
These modern I/O packages provide highly efficient and scalable file-handling and
buffer-based operations. They include: Paths, Files, FileChannel, ByteBuffer
Widely used in enterprise applications for large-scale data processing.
5. [Link]
This package supports networking features. Its classes include: URL
Socket and ServerSocket
InetAddress
These classes allow communication across networks and the web.
6. [Link] and [Link]
These packages enable the creation of graphical user interfaces. They include:
Frame, Button, Label (AWT)
JFrame, JButton, JPanel (Swing)
They help developers build desktop applications.
7. [Link]
This package allows interaction with databases using JDBC (Java Database
Connectivity). Common classes include: Connection
Statement
ResultSet
PreparedStatement
They help applications store and retrieve data from relational databases.
Summary
Packages serve as the backbone of Java’s organization system. They help developers
structure code, avoid conflicts, and control visibility. The Java API offers hundreds of
pre-defined packages containing thousands of reliable, industrial-level classes. These
packages allow Java programmers to build powerful applications quickly by reusing
existing capabilities instead of reinventing the wheel.
Using System Packages
System packages are the built-in packages that come with the Java API. They contain
classes written by the Java developers to perform common tasks such as input
handling, data structures, file operations, networking, and mathematics.
To use a class from a system package, we must import it at the top of the program.
This tells Java where to find the class instead of writing its full path every time.
Example: using Scanner from [Link]
import [Link];
public class DemoInput {
public static void main(String[] args) {
Scanner sc = new Scanner([Link]);
[Link]("Enter a number:");
int x = [Link]();
[Link]("You entered: " + x);
[Link]();
}
}
Only one package does not need importing:
[Link] (contains String, Math, System, Object, etc.)
Using system packages reduces effort because we reuse ready-made classes that
are already tested, optimized, and production-ready.
Naming Conventions for Packages
Package names in Java follow strict conventions so that large projects stay organized
and avoid name conflicts.
Package names are always lowercase, even when they contain multiple words.
Simple examples include:
utilities , database , networking .
For professional or enterprise-level applications, package names use the reverse
domain name format.
Example using domain [Link] :
package [Link];
This approach ensures global uniqueness and builds a clear hierarchy.
Inside such a structure, you may create sub-packages like:
package [Link];
package [Link];
package [Link];
Package rules:
No spaces
No symbols
Only lowercase letters
Words separated by dots
This style helps developers easily identify the origin and purpose of each package in
a project.
Additional Naming Convention Rules for
Packages
Java enforces clear naming conventions for packages so that projects remain
readable, scalable, and consistent across teams.
Package names must be simple, lowercase, and structured in a hierarchical manner.
1. Use Only Lowercase Letters
Package names should never contain uppercase letters.
This avoids clashes with class or interface names.
Correct:
[Link]
Incorrect:
[Link]
2. No Spaces, Hyphens, or Special Characters
Package names cannot contain spaces or symbols such as - , @ , # , ! , or & .
Correct:
[Link]
Incorrect:
[Link]
Incorrect:
my [Link]
Only letters and dots are allowed.
3. Keep Names Short, Meaningful, and Descriptive
Names should clearly represent the purpose of the package.
Examples: database , network , analytics , ui , students , security
Avoid very long or vague names like: datahandlingpackageone or stuff
4. Follow Reverse-Domain Naming for Large Projects
For organizations, the package name starts with the reverse of the domain.
Example domain: [Link]
Package structure:
package [Link];
package [Link];
package [Link];
This ensures uniqueness across the world.
5. Sub-Packages Should Represent Logical Modules
Sub-packages allow deeper structure. Each level should narrow down the
functionality.
Example:
[Link]
[Link]
[Link]
Each represents a separate responsibility within the analytics module.
6. Avoid Using Java Reserved Keywords
Package names cannot be Java keywords like: class , int , static , switch ,
package
Correct: [Link]
Incorrect: [Link]
7. Use Singular Nouns for General Modules
Package names are usually singular unless the content is naturally plural.
Correct: student , account , network , util
Plural only when meaningful: collections , annotations , exceptions
8. Keep Package Levels Under Control
Too many nested sub-packages make navigation difficult.
Use sensible depth.
Correct: [Link]
Avoid:
[Link]
e
9. Match Folder Structure Exactly
Every package corresponds to a folder.
Example:
package [Link];
Must be stored in:
in/edu/gcu/analytics/reports/
Java will not compile if the folder structure does not match.
These additional rules help keep package structures clean, predictable, and easy to
navigate in both small and large Java applications.
Creating Packages in Java
A package is created using the package keyword placed at the very top of a Java
file.
When this file is compiled, Java automatically creates the corresponding folder
structure based on the package name.
Example of creating a package:
package [Link];
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
This file must be saved inside:
in/edu/gcu/utilities/
The folder structure and package name must match exactly, or Java will not
compile.
Creating packages helps organize code neatly, separating functionalities into
different modules.
Accessing a Package
To use classes from another package, we must tell Java where those classes are
located.
There are two ways to do this:
1. Using Fully Qualified Class Name
We specify the complete path of the class including its package.
Example:
[Link] calc = new [Link]
This method avoids imports but can make the code long and less readable.
2. Using the import Keyword
This is the preferred way to access external classes.
The import statement allows us to use the class directly without writing the full path
every time.
Example:
import [Link];
public class TestApp {
public static void main(String[] args) {
Calculator c = new Calculator();
[Link]([Link](10, 20));
}
}
Imports are written after the package statement (if present) and before the class
definition.
Using a Package
Using a package simply means accessing the classes inside it.
To use a package:
1. Create the package
2. Compile package classes
3. Import them into another file
4. Create objects or call methods from the package
Example workflow:
Step 1: Create the package
package [Link];
public class Printer {
public void show(String msg) {
[Link]("Message: " + msg);
}
}
Step 2: Compile
From terminal:
javac -d . [Link]
-d . tells Java to create folders based on the package name.
Step 3: Use the package
import [Link];
public class Demo {
public static void main(String[] args) {
Printer p = new Printer();
[Link]("Hello from package!");
}
}
Step 4: Run the program
java Demo
Imports make the program clean, readable, and modular.
Packages allow real-world application structuring where code can be split into
meaningful layers like utilities, models, controllers, services, and interfaces.
Adding a Class to a Package
Adding a class to a package is simply a matter of placing the class file inside the
correct directory and including the package statement at the top of the file. Java
treats every file as part of the package written in its first line, and the folder structure
must match this name. When compiled, Java automatically places the .class file
inside the proper package directory.
Example of adding a class to a package:
package [Link];
public class Book {
public void details() {
[Link]("Book information displayed.");
}
}
This file must be saved inside:
in/edu/gcu/library/
If you want to add another class to the same package, simply place it in the same
folder and use the same package statement:
package [Link];
public class Author {
public void showAuthor() {
[Link]("Author information displayed.");
}
}
Now both Book and Author belong to the same package [Link] ,
and other programs can import these classes individually or import the entire
package.
Hiding Classes
Java allows us to hide classes within a package so that they are not accessible
outside their package. This is done by not marking the class as public.
If a class has default access (no access modifier), it becomes visible only within its
own package and cannot be imported by external programs.
This technique is used to hide helper classes or internal implementation details that
are not meant to be exposed to the user.
Example of a hidden class:
package [Link];
// No "public" keyword → this class is hidden from outside packages
class LibraryHelper {
void log(String msg) {
[Link]("LOG: " + msg);
}
}
A public class in the same package can still use this hidden class:
package [Link];
public class LibraryManager {
public void manage() {
LibraryHelper helper = new LibraryHelper();
[Link]("Managing library...");
}
}
But outside this package, the hidden class cannot be accessed:
import [Link]; // ERROR: cannot access Librar
When do we hide classes?
We hide classes when:
They serve only internal purposes
They should not be exposed to programmers using the package
They contain sensitive logic that must remain encapsulated
We want to prevent unnecessary misuse or errors
Hiding classes keeps the package clean, secure, and easier to maintain.
Exception Handling — Types of Errors and
Exceptions (With Examples)
Exception handling is Java’s mechanism to respond to unexpected events that occur
during program execution.
Errors and exceptions both indicate problems, but they differ in origin, recoverability,
and how the language expects you to handle them. Below we explain the categories,
contrast checked vs unchecked, and show concise Java examples that demonstrate
how common exceptions arise.
High-level Categories
Compile-time errors (Syntax / Semantic).
These are caught by the compiler and must be fixed before the program runs.
Examples include missing semicolons, undeclared variables, and type-mismatch in
code. They are not handled by try/catch because the program will not compile until
corrected.
Runtime exceptions.
These occur while the program runs. They are often caused by invalid operations
such as dividing by zero or accessing a null reference. Many runtime exceptions are
recoverable with appropriate checks or try/catch blocks.
Errors ([Link] and subclasses).
Errors indicate serious problems that applications typically should not attempt to
handle, such as OutOfMemoryError or StackOverflowError . They are thrown
by the JVM and usually signal resource or environment failure rather than bugs you
can fix at runtime.
Checked vs Unchecked Exceptions
Checked exceptions must be declared or handled. The compiler forces you to catch
them or declare them with throws . They represent conditions outside the
program’s immediate control (e.g., I/O problems).
Unchecked exceptions (subclasses of RuntimeException ) do not require explicit
handling; they usually signal programmer errors (null access, bad casts, invalid
indices).
Common Runtime Exceptions — Explanations +
Small Examples
Each small code snippet below demonstrates how the exception can be produced.
The comment shows the expected outcome.
NullPointerException
Occurs when you dereference null (call method or access field on a null
reference).
// [Link]
String s = null;
int len = [Link](); // Throws NullPointerException at runtime
ArrayIndexOutOfBoundsException
Happens when an array index is outside 0 to length-1 .
// [Link]
int[] a = new int[3];
int x = a[3]; // Throws ArrayIndexOutOfBoundsException (valid indices 0..
ArithmeticException
Typically caused by illegal arithmetic, such as integer division by zero.
// [Link]
int a = 10;
int b = 0;
int c = a / b; // Throws ArithmeticException: / by zero
NumberFormatException
Raised when attempting to convert a string that does not contain a parsable number.
// [Link]
String t = "abc";
int n = [Link](t); // Throws NumberFormatException
ClassCastException
Thrown when an object is cast to a type it is not an instance of.
// [Link]
Object obj = "hello";
Integer i = (Integer) obj; // Throws ClassCastException at runtime
IllegalArgumentException
Raised to indicate that a method has been passed an inappropriate or illegal
argument.
// [Link]
[Link](-100); // hypothetical: some methods throw IllegalArgumentEx
// Many library methods explicitly throw IllegalArgumentException for inv
Common Checked Exceptions — Explanations +
Small Examples
Checked exceptions must be declared or handled. The compilation will fail unless you
catch or declare them.
FileNotFoundException (checked)
Thrown by I/O methods when attempting to open a file that does not exist.
// [Link]
import [Link].*;
public class FileReadExample {
public static void main(String[] args) throws FileNotFoundException {
FileInputStream fis = new FileInputStream("[Link]");
// Compiler requires handling or declaring FileNotFoundException
}
}
IOException (checked)
General I/O failure (read/write issues, closed streams, etc.). Typically handled with
try/catch.
// [Link]
import [Link].*;
public class IOExample {
public static void main(String[] args) {
try {
FileReader fr = new FileReader("[Link]");
[Link]();
} catch (IOException ex) {
[Link]("I/O problem: " + [Link]());
}
}
}
Errors — Examples (not typically caught)
Errors indicate serious JVM problems or resource exhaustion. You generally do not
catch them (but you can).
OutOfMemoryError
Occurs when the JVM cannot allocate more heap space.
// [Link]
import [Link].*;
public class OutOfMemoryDemo {
public static void main(String[] args) {
List<int[]> big = new ArrayList<>();
while (true) {
[Link](new int[10_000_000]); // Eventually throws OutOfMemor
}
}
}
StackOverflowError
Happens when deep or infinite recursion exhausts the call stack.
// [Link]
public class StackOverflowDemo {
static void recurse() { recurse(); }
public static void main(String[] args) { recurse(); } // Throws Stack
}
How Exceptions Manifest — Practical Notes
A checked exception forces you to either catch it or declare it with throws in
the method signature; this is a compile-time contract.
Unchecked exceptions signal likely programming mistakes. Use validation to
avoid them (null checks, index checks, correct parsing).
Errors are usually fatal or environmental and are not meant to be recovered from
inside typical application logic.
When to Catch vs When to Let It Propagate
Catch exceptions when you can meaningfully handle or recover from the condition
(e.g., retry, use fallback, show user-friendly message). Let exceptions propagate
when your method cannot resolve the problem — declare them with throws so
callers can handle them at an appropriate level.
Short Patterns to Demonstrate in Class
1. Show simple runtime exceptions by running tiny snippets that deliberately cause
NullPointerException and ArrayIndexOutOfBoundsException .
2. Demonstrate a checked exception by attempting to open a non-existent file,
and show compiler error if you neither catch nor declare it.
3. Illustrate OutOfMemoryError and StackOverflowError under controlled
settings to explain limits of JVM resources.
4. Contrast == vs equals() for strings when discussing exceptions emerging
from incorrect casts or assumptions.
Summary
Compile-time errors must be fixed before running.
Checked exceptions must be handled or declared; they model external
problems (I/O, network).
Unchecked exceptions are typically programming errors you should prevent
via validation.
Errors represent serious problems from which normal programs usually do not
recover.
When you are ready, tell me which subtopic you want next (for example: Multiple
catch, Nested try-catch, throw vs throws, finally block, built-in exceptions list, or
creating and throwing custom exceptions), and I will produce the corresponding
Colab-ready notes and demonstration code in the agreed format.
Multiple Catch and Nested Try–Catch in Java
Java allows handling different kinds of exceptions separately using multiple catch
blocks, and also allows placing one try–catch block inside another using nested
try–catch. Both techniques help create safer, more fault-tolerant programs that
respond precisely to different error situations.
Multiple Catch Blocks
A single try block can have multiple catch blocks attached to it.
This is useful when different exceptions may occur inside the same risky code
segment, and each type needs different handling.
The order matters:
More specific exceptions must come before more general ones, because Java
matches the first catch block whose type fits the thrown exception.
Example: Multiple Catch Blocks
public class MultipleCatchDemo {
public static void main(String[] args) {
try {
int[] arr = {10, 20, 30};
int a = 10 / 0; // ArithmeticException
int x = arr[5]; // ArrayIndexOutOfBoundsExcept
[Link](x);
} catch (ArithmeticException e) {
[Link]("Cannot divide by zero!");
} catch (ArrayIndexOutOfBoundsException e) {
[Link]("Array index is invalid!");
} catch (Exception e) {
[Link]("General exception occurred: " + [Link]
}
}
}
How This Works
If division by zero happens, the ArithmeticException block executes.
If illegal array access happens, the ArrayIndexOutOfBoundsException block
executes.
Any other unexpected exception flows to the general catch block.
This separation gives precise control and more meaningful error messages.
Nested Try–Catch
Nested try–catch means placing one try block inside another.
It is useful when some operations depend on others, or when deeper tasks require
separate handling while still being part of a larger error-prone section.
Outer try–catch protects a broader task, while inner try–catch handles specific
operations inside that task.
Example: Nested Try–Catch
public class NestedTryDemo {
public static void main(String[] args) {
try {
[Link]("Outer try block begins.");
int a = 10 / 2; // OK
try {
int[] arr = new int[3];
[Link](arr[5]); // Inner exception
}
catch (ArrayIndexOutOfBoundsException e) {
[Link]("Inner catch: Invalid array index.");
}
[Link]("Continuing outer try block...");
int x = [Link]("abc"); // Outer exception
}
catch (NumberFormatException e) {
[Link]("Outer catch: Cannot convert to number.");
}
[Link]("Program continues normally.");
}
}
How This Works
The inner try handles array issues.
The outer try handles number format issues.
Errors are handled where they make the most sense.
After inner catch executes, control returns to outer try block.
If the outer operation fails later, the outer catch handles it.
Summary
Multiple catch blocks allow handling different exception types using
customized logic.
Order matters: specific exceptions must come before general ones.
Nested try–catch enables more granular error handling inside complex
operations.
The inner block handles local failures; the outer block covers broader
operations.
When you are ready, we can continue to the next part: throw vs throws, finally, or
built-in exceptions list.
throw, throws, and finally in Java
Java provides three important components for advanced exception handling: throw,
throws, and finally.
They give developers control over how exceptions are generated, declared, and
cleaned up after execution.
The throw Keyword
throw is used to manually create and throw an exception.
It is useful when you want your program to signal an error condition deliberately,
rather than waiting for the JVM to detect one.
You can throw either:
Built-in exception objects (e.g., new ArithmeticException() )
User-defined exceptions
Example: Using throw
public class ThrowDemo {
static void checkAge(int age) {
if(age < 18) {
throw new ArithmeticException("Access denied: Must be 18 or o
}
[Link]("Access granted!");
}
public static void main(String[] args) {
checkAge(15); // Throws exception manually
}
}
How it works
If the condition is invalid, the method throws an exception immediately.
Program flow stops unless there is a matching catch block.
Useful for validating input, security checks, or business rules.
The throws Keyword
throws is used in a method signature to declare that the method may throw
certain exceptions.
It informs the caller that handling the exception is their responsibility.
This is mandatory for checked exceptions.
Example: Using throws
import [Link].*;
public class ThrowsDemo {
static void readFile() throws IOException {
FileReader fr = new FileReader("[Link]"); // May throw IOExcep
[Link]();
}
public static void main(String[] args) {
try {
readFile();
} catch (IOException e) {
[Link]("File handling error: " + [Link]());
}
}
}
How it works
readFile() declares that it might throw IOException .
The caller (main method) must handle it.
Useful for methods performing I/O, networking, and database operations.
The finally Block
finally is used to run code whether an exception occurs or not.
It is mainly used for cleanup operations such as closing files, releasing resources, or
disconnecting a database.
Example: Using finally
public class FinallyDemo {
public static void main(String[] args) {
try {
int a = 10 / 0; // Causes exception
}
catch (ArithmeticException e) {
[Link]("Error: " + [Link]());
}
finally {
[Link]("This will always run, no matter what.");
}
}
}
Key idea
finally always executes, even if an exception occurs.
It also executes when return statements are present.
It does not execute only when the JVM shuts down abruptly (power loss, forced
termination).
Combining throw, throws, and finally
Below is a program that uses all three:
import [Link].*;
public class CombinedDemo {
static void riskyMethod() throws IOException {
throw new IOException("Simulated I/O error.");
}
public static void main(String[] args) {
try {
riskyMethod(); // Must be in try because riskyMethod declar
}
catch (IOException e) {
[Link]("Caught in main: " + [Link]());
}
finally {
[Link]("Cleanup actions executed here.");
}
}
}
Summary
throw: Manually creates and throws an exception inside a method.
throws: Declares that a method may throw certain exceptions; caller must
handle it.
finally: Always executes for cleanup, regardless of exceptions.
These features together make exception handling robust, predictable, and reliable in
Java programs.
Built-in Exceptions and Throwing Our Own
Exceptions
Java provides a rich set of built-in exceptions to handle common error situations.
Along with these predefined exceptions, Java also allows programmers to create and
throw their own custom exceptions when specific application rules are violated.
Built-in Exceptions in Java
Built-in exceptions are part of the Java API and are organized under the packages
[Link] , [Link] , and others.
They are already defined and ready to use, and most of them come from two major
families:
Runtime exceptions (unchecked)
Checked exceptions
Below are common built-in exceptions along with examples to demonstrate how they
occur.
1. NullPointerException
Occurs when methods or fields are accessed on a null reference.
String name = null;
[Link](); // Throws NullPointerException
2. ArrayIndexOutOfBoundsException
Thrown when accessing array elements outside valid index range.
int[] arr = {1, 2, 3};
int x = arr[5]; // Throws ArrayIndexOutOfBoundsException
3. ArithmeticException
Occurs during illegal arithmetic operations like division by zero.
int a = 10 / 0; // Throws ArithmeticException
4. NumberFormatException
Occurs when converting a non-numeric string into a number.
int x = [Link]("hello"); // Throws NumberFormatException
5. ClassCastException
Thrown when casting an object to an incompatible type.
Object obj = "Java";
Integer num = (Integer) obj; // Throws ClassCastException
6. IllegalArgumentException
Thrown when a method receives an invalid or inappropriate argument.
[Link](-100); // Negative time → IllegalArgumentException
7. IOException and FileNotFoundException
Common checked exceptions when working with files or streams.
import [Link].*;
FileInputStream fis = new FileInputStream("[Link]"); // FileNotFoun
Throwing Our Own Exceptions (Custom
Exceptions)
In real-world applications, we often need custom rules.
For example:
Age must be above 18
Marks cannot be negative
Username should not be empty
Java allows us to create user-defined exceptions to enforce such rules clearly.
We create custom exceptions by extending Exception (checked) or
RuntimeException (unchecked).
Example: Creating and Throwing a Custom Exception
// Custom exception class
class InvalidAgeException extends Exception {
public InvalidAgeException(String message) {
super(message);
}
}
public class CustomExceptionDemo {
static void verifyAge(int age) throws InvalidAgeException {
if(age < 18) {
throw new InvalidAgeException("Age must be 18 or above.");
}
[Link]("Age is valid.");
}
public static void main(String[] args) {
try {
verifyAge(15); // Will trigger custom exception
}
catch (InvalidAgeException e) {
[Link]("Caught: " + [Link]());
}
}
}
How this works
A custom exception class ( InvalidAgeException ) is created.
The method verifyAge() checks the age.
If invalid, it throws the custom exception.
The main method catches it and displays a clear message.
Example: Custom Runtime Exception
Unchecked custom exception:
class NegativeMarksException extends RuntimeException {
public NegativeMarksException(String msg) {
super(msg);
}
}
public class MarksDemo {
public static void main(String[] args) {
int marks = -10;
if(marks < 0) {
throw new NegativeMarksException("Marks cannot be negative.")
}
}
}
When to use checked vs unchecked?
Use checked exceptions when callers must handle the situation.
Use unchecked exceptions for programmer errors or validation issues.
Summary
Java has many built-in exceptions that handle common runtime and
input/output issues.
Built-in exceptions help quickly identify common mistakes like null access,
wrong index, invalid input, or file problems.
Custom exceptions let developers enforce business rules or domain-specific
validations cleanly.
Both built-in and user-defined exceptions work with the same try–catch–finally
structure.
Practice Exercises on Exception Handling
These exercises help students understand how different exceptions occur and how to
handle them using try–catch, multiple catch, nested try, throw, throws, and custom
exceptions.
Exercise 1: Handle Multiple Exceptions
Write a program that:
1. Creates an integer array of size 3
2. Reads a number from the user
3. Divides 100 by that number
4. Prints the element at the user-given index
Handle:
ArithmeticException
ArrayIndexOutOfBoundsException
A general Exception
Exercise 2: Nested Try–Catch
Create a program that uses:
An outer try block for division
An inner try block for array access
Conditions:
Division might cause /0
Array access may exceed bounds
Catch each separately inside appropriate catch blocks.
Exercise 3: Use the finally Block
Write a program that:
Opens a resource (like a dummy operation or object)
Causes an exception intentionally (division or parsing)
Ensures the finally block prints “Cleanup completed.”
Observe how finally executes even when an exception occurs.
Exercise 4: Demonstrate throw
Write a method validateAge(int age) that:
Throws IllegalArgumentException if age < 18
Otherwise prints “Eligible”
Call the method with different inputs and observe the behavior.
Exercise 5: Demonstrate throws
Create a method readFile() that:
Declares throws IOException
Tries to open a file named "[Link]"
Call this method from main() using try–catch.
Exercise 6: Built-in Exception Experiment
Write a program to intentionally generate the following exceptions (one by one):
NullPointerException
NumberFormatException
ClassCastException
Handle each with a dedicated catch block and print custom messages.
Exercise 7: Create a Custom Exception
Define a custom exception called InvalidMarksException .
Write a method checkMarks(int marks) that:
Throws the custom exception if marks < 0 or > 100
Prints “Valid marks” otherwise
Catch the exception in main() and print the message.
Exercise 8: Multiple Custom Validations
Create a program that checks:
Name should not be empty
Age should be between 18–60
Salary should be > 0
Use:
throw for raising custom validation errors
At least 2 custom exception classes
A single try–catch section in main
Exercise 9: Combined Handling
Write a single program that contains:
A method that throws an arithmetic error
A method that throws an I/O error
A custom exception method
Use multiple catch blocks to handle all three.
Exercise 10: Mini Project — Student Registration
Validator
Create a small application that:
1. Accepts name, age, email, and marks
2. Checks the following:
Name must be letters only
Age must be ≥ 18
Email must contain “@”
Marks must be between 0–100
3. Throws appropriate exceptions (custom + built-in)
4. Uses try–catch–finally structure
5. Prints “Registration complete” in the finally block
These exercises cover real-world use cases of exception handling and help students
strengthen their conceptual and programming skills.
Demonstration 7: Inheritance and Polymorphism
in Java
// Parent Class
class Shape {
void draw() {
[Link]("Drawing a shape");
}
// Method Overloading (Compile-time Polymorphism)
void area(int side) {
[Link]("Area of square: " + (side * side));
}
void area(int length, int width) {
[Link]("Area of rectangle: " + (length * width));
}
}
// Child Class 1
class Circle extends Shape {
@Override
void draw() { // Method Overriding (Runtime Polymorphism)
[Link]("Drawing a circle");
}
void area(double radius) {
[Link]("Area of circle: " + (3.14 * radius * radius))
}
}
// Child Class 2
class Triangle extends Shape {
@Override
void draw() { // Method Overriding
[Link]("Drawing a triangle");
}
void area(double base, double height) {
[Link]("Area of triangle: " + (0.5 * base * height));
}
}
public class PolymorphismDemo {
public static void main(String[] args) {
// Runtime Polymorphism (Overriding)
Shape s;
s = new Circle();
[Link](); // Calls Circle's draw()
s = new Triangle();
[Link](); // Calls Triangle's draw()
// Compile-time Polymorphism (Overloading)
Shape shape = new Shape();
[Link](5); // Square
[Link](4, 6); // Rectangle
Circle c = new Circle();
[Link](3.5); // Circle area (double radius)
Triangle t = new Triangle();
[Link](4.0, 7.0); // Triangle area
}
}
Demonstration 8: Implementing Interfaces and
Exception Handling in Java
// Interface defining behavior
interface Payment {
void makePayment(double amount) throws Exception; // Method with ch
}
// Class implementing the interface
class CreditCardPayment implements Payment {
@Override
public void makePayment(double amount) throws Exception {
if (amount <= 0) {
throw new Exception("Invalid amount. Must be greater than zer
}
[Link]("Processing credit card payment of: " + amount
}
}
// Another implementation
class UpiPayment implements Payment {
@Override
public void makePayment(double amount) throws Exception {
if (amount > 50000) {
throw new Exception("UPI limit exceeded! Max allowed is 50,00
}
[Link]("Processing UPI payment of: " + amount);
}
}
public class InterfaceExceptionDemo {
public static void main(String[] args) {
Payment pay;
try {
pay = new CreditCardPayment();
[Link](2000); // Valid payment
pay = new UpiPayment();
[Link](80000); // Throws exception
} catch (Exception e) {
[Link]("Payment Failed: " + [Link]());
}
[Link]("Program continues safely...");
}
}
Multithreaded Programming in Java
Multithreading in Java allows a program to execute multiple tasks simultaneously.
Each task runs in an independent path of execution called a thread. This improves
application performance, responsiveness, and efficient use of CPU resources,
especially on multi-core systems.
What is a Thread
A thread is the smallest unit of execution within a process.
A single Java program always starts with one thread called the main thread, and
additional threads can be created to perform parallel tasks such as background
processing, animations, file downloading, or handling multiple users.
Real-life analogy:
A kitchen with multiple cooks preparing different dishes at the same time.
Creating Threads in Java
Java supports multithreading through built-in support in the [Link] package.
There are two main ways to create a thread:
Extending the Thread class
Implementing the Runnable interface
In this unit, we first focus on extending the Thread class, as it clearly demonstrates
thread behavior.
Extending the Thread Class
When a class extends the Thread class, it becomes capable of running as a
separate thread.
The key method here is run() , which contains the code that the thread executes.
To start a thread, we call the start() method, not run() directly.
Calling start() creates a new call stack and invokes run() internally.
class MyThread extends Thread {
public void run() {
[Link]("Thread is running");
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread t = new MyThread();
[Link](); // starts a new thread
}
}
Stopping a Thread
Early versions of Java provided a stop() method, but it is deprecated because it is
unsafe.
Stopping a thread abruptly can leave shared data in an inconsistent state.
Recommended approach:
Use a flag variable
Let the thread stop naturally by checking a condition
class Worker extends Thread {
boolean running = true;
public void run() {
while(running) {
[Link]("Thread working...");
}
}
public void stopThread() {
running = false;
}
}
This ensures safe and controlled thread termination.
Blocking a Thread
Blocking means temporarily suspending a thread’s execution so other threads can
run.
Common ways a thread gets blocked:
sleep(time) → pauses execution for a fixed time
wait() → waits until another thread notifies
join() → waits for another thread to finish
class SleepDemo extends Thread {
public void run() {
try {
[Link](1000);
[Link]("Thread resumed after sleep");
} catch (InterruptedException e) {
[Link]("Thread interrupted");
}
}
}
Blocking improves coordination and CPU utilization.
Life Cycle of a Thread
A thread passes through different states during its execution.
1. New
Thread object is created but start() is not called.
2. Runnable
Thread is ready to run and waiting for CPU time.
3. Running
Thread is currently executing its run() method.
4. Blocked / Waiting
Thread is temporarily inactive due to sleep, wait, or I/O operation.
5. Terminated (Dead)
Thread finishes execution or stops naturally.
Thread Life Cycle Diagram (Conceptual)
New → Runnable → Running → Blocked/Waiting → Runnable → Terminated
The thread may move between runnable and blocked states multiple times before
termination.
Why Multithreading is Important
Multithreading:
Improves application performance
Makes programs responsive
Efficiently uses CPU cores
Essential for servers, GUI applications, and real-time systems
Summary
A thread is an independent execution path in a program
Java supports multithreading natively
Threads can be created by extending Thread class
Threads should be stopped safely using control flags
Blocking allows better synchronization and coordination
Thread life cycle defines how threads execute and terminate
Multithreaded Programming in Java – Advanced
Concepts
This section covers important thread-related concepts that control execution,
coordination, and safety when multiple threads run concurrently. These concepts are
essential for building reliable and efficient multithreaded applications.
Using Thread Methods
Java provides several built-in methods in the Thread class to control thread
execution and behavior.
Commonly Used Thread Methods
start()
Starts a new thread and invokes the run() method internally. Calling run()
directly does not create a new thread.
sleep(time)
Pauses the execution of the current thread for a specified time (in milliseconds).
During sleep, the thread enters a blocked state.
join()
Makes one thread wait until another thread finishes execution. Useful when
tasks must complete in sequence.
isAlive()
Checks whether a thread is still running.
currentThread()
Returns a reference to the currently executing thread.
class ThreadMethodDemo extends Thread {
public void run() {
try {
[Link]("Thread running: " + [Link](
[Link](1000);
[Link]("Thread finished");
} catch (InterruptedException e) {
[Link]("Thread interrupted");
}
}
public static void main(String[] args) throws InterruptedException {
ThreadMethodDemo t1 = new ThreadMethodDemo();
[Link]();
[Link]();
[Link]("Main thread resumes after t1 finishes");
}
}
Thread Exceptions
Threads commonly deal with InterruptedException, which occurs when a sleeping
or waiting thread is interrupted by another thread.
Key idea:
Java forces handling of thread interruptions to avoid unpredictable thread
behavior.
This exception is a checked exception, so it must be caught or declared.
class ThreadExceptionDemo extends Thread {
public void run() {
try {
[Link](2000);
} catch (InterruptedException e) {
[Link]("Thread was interrupted");
}
}
}
Thread Priority
Thread priority determines which thread gets preference when multiple threads
compete for CPU time.
Java supports priority values from 1 (MIN_PRIORITY) to 10 (MAX_PRIORITY).
Priority constants:
Thread.MIN_PRIORITY → 1
Thread.NORM_PRIORITY → 5
Thread.MAX_PRIORITY → 10
Higher priority threads are more likely to be scheduled, but exact behavior depends
on the JVM and operating system.
class PriorityDemo extends Thread {
public void run() {
[Link]("Running thread: " + getName() +
" with priority " + getPriority());
}
public static void main(String[] args) {
PriorityDemo t1 = new PriorityDemo();
PriorityDemo t2 = new PriorityDemo();
[Link](Thread.MIN_PRIORITY);
[Link](Thread.MAX_PRIORITY);
[Link]();
[Link]();
}
}
Synchronization
Synchronization is used to prevent multiple threads from accessing shared
resources at the same time, which can lead to inconsistent or incorrect results. This
problem is known as a race condition.
Java uses the synchronized keyword to lock an object or method so that only one
thread can access it at a time.
class Counter {
int count = 0;
synchronized void increment() {
count++;
}
}
class SyncDemo extends Thread {
static Counter c = new Counter();
public void run() {
for(int i = 0; i < 1000; i++) {
[Link]();
}
}
public static void main(String[] args) throws InterruptedException {
SyncDemo t1 = new SyncDemo();
SyncDemo t2 = new SyncDemo();
[Link]();
[Link]();
[Link]();
[Link]();
[Link]("Final count: " + [Link]);
}
}
Synchronization ensures data consistency when threads share resources.
Implementing the Runnable Interface
Instead of extending the Thread class, Java allows threads to be created by
implementing the Runnable interface.
This approach is preferred when:
A class already extends another class
You want better separation between task and thread
Steps:
1. Implement Runnable
2. Override run()
3. Pass the object to a Thread
class MyTask implements Runnable {
public void run() {
[Link]("Task running in thread: " +
[Link]().getName());
}
public static void main(String[] args) {
MyTask task = new MyTask();
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
[Link]();
[Link]();
}
}
Comparison: Thread vs Runnable
Aspect Thread Class Runnable Interface
Inheritance Extends Thread Implements Runnable
Flexibility Limited More flexible
Object Sharing Difficult Easy
Recommended Less More
Summary
Thread methods control execution and coordination of threads
InterruptedException handles unexpected thread interruption
Thread priority influences scheduling but is not guaranteed
Synchronization prevents race conditions
Runnable interface provides a flexible and scalable way to create threads
Files in Java
Files in Java are used to store data permanently on secondary storage such as hard
disks or SSDs. Unlike variables and arrays, which exist only during program execution,
files allow data to be saved and reused even after the program terminates. Java
provides a rich set of classes in the [Link] package to work with files efficiently.
Concept of File Handling
File handling in Java is based on the idea of streams.
A stream represents a flow of data between a program and a file.
There are two directions:
Input stream: Data flows from file to program
Output stream: Data flows from program to file
Java abstracts file operations so programmers do not directly deal with hardware-
level details.
Stream Classes in Java
Streams are classified based on the type of data they handle:
Byte Streams → Handle raw binary data (images, audio, videos)
Character Streams → Handle text data (characters, strings)
All stream classes are part of the [Link] package.
Byte Stream Classes
Byte streams read and write data one byte (8 bits) at a time.
They are suitable for handling binary files.
Base classes:
InputStream → Read bytes
OutputStream → Write bytes
Common byte stream classes:
FileInputStream
FileOutputStream
BufferedInputStream
BufferedOutputStream
Example: Writing Data Using Byte Stream
import [Link].*;
public class ByteWriteDemo {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("[Link]");
[Link](65); // writes ASCII value of 'A'
[Link]();
[Link]("Data written using byte stream");
}
}
Character Stream Classes
Character streams handle 16-bit Unicode characters, making them ideal for text
files.
They automatically handle character encoding.
Base classes:
Reader
Writer
Common character stream classes:
FileReader
FileWriter
BufferedReader
BufferedWriter
Example: Writing Text Using Character Stream
import [Link].*;
public class CharWriteDemo {
public static void main(String[] args) throws IOException {
FileWriter fw = new FileWriter("[Link]");
[Link]("Welcome to Java File Handling");
[Link]();
[Link]("Text written using character stream");
}
}
The File Class
The File class represents a file or directory path.
It does not read or write data but provides information about files.
Common uses:
Check file existence
Create new files
Delete files
Get file properties (name, path, size)
Example: Using File Class
import [Link];
public class FileClassDemo {
public static void main(String[] args) {
File f = new File("[Link]");
[Link]("File exists: " + [Link]());
[Link]("File name: " + [Link]());
[Link]("Absolute path: " + [Link]());
}
}
Creation of Files
Files can be created using:
[Link]()
File output streams or writers
Example: Creating a File
import [Link].*;
public class FileCreateDemo {
public static void main(String[] args) throws IOException {
File f = new File("[Link]");
if([Link]()) {
[Link]("File created successfully");
} else {
[Link]("File already exists");
}
}
}
Reading from Files
Reading means retrieving data stored inside a file.
Reading Using Byte Stream
import [Link].*;
public class ByteReadDemo {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("[Link]");
int ch;
while((ch = [Link]()) != -1) {
[Link]((char) ch);
}
[Link]();
}
}
Reading Using Character Stream
import [Link].*;
public class CharReadDemo {
public static void main(String[] args) throws IOException {
FileReader fr = new FileReader("[Link]");
int ch;
while((ch = [Link]()) != -1) {
[Link]((char) ch);
}
[Link]();
}
}
Summary
Files provide permanent data storage
Streams represent data flow between program and file
Byte streams handle binary data
Character streams handle text data
File class manages file properties and creation
Java supports simple and efficient file reading mechanisms
Writing Bytes, Writing Characters, Buffering,
keyboard_arrow_down Concatenation, and Random Access Files
Java supports multiple techniques for writing data to files depending on the type of
data, performance needs, and access pattern. These techniques are built on top of
stream-based I/O provided by the [Link] package.
Writing Bytes to a File
Byte writing is used when working with binary data such as images, audio files, or
raw byte values.
Data is written one byte at a time using byte stream classes.
Writing Bytes Using FileOutputStream
import [Link].*;
public class WriteBytesDemo {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("[Link]");
[Link](65); // Writes ASCII value of 'A'
[Link](66); // Writes ASCII value of 'B'
[Link](67); // Writes ASCII value of 'C'
[Link]();
[Link]("Bytes written successfully");
}
}
Each call to write() sends one byte to the file.
If the file already exists, it is overwritten by default.
Writing Characters to a File
Character writing is used for text data.
Java uses Unicode characters, so character streams are preferred for text files.
Writing Characters Using FileWriter
import [Link].*;
public class WriteCharsDemo {
public static void main(String[] args) throws IOException {
FileWriter fw = new FileWriter("[Link]");
[Link]("Java File Handling\n");
[Link]("Writing characters to a file");
[Link]();
[Link]("Characters written successfully");
}
}
Character streams automatically handle character encoding, making them ideal for
text content.
Buffering Files
Buffering improves performance by reducing the number of physical read/write
operations.
Instead of writing data byte-by-byte or character-by-character, data is stored
temporarily in memory (buffer) and written in bulk.
Buffered Byte Writing
import [Link].*;
public class BufferedByteWriteDemo {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("[Link]");
BufferedOutputStream bos = new BufferedOutputStream(fos);
[Link](68); // 'D'
[Link](69); // 'E'
[Link](70); // 'F'
[Link](); // Automatically flushes buffer
[Link]("Buffered byte writing completed");
}
}
Buffered Character Writing
import [Link].*;
public class BufferedCharWriteDemo {
public static void main(String[] args) throws IOException {
FileWriter fw = new FileWriter("[Link]");
BufferedWriter bw = new BufferedWriter(fw);
[Link]("Buffered writing improves performance");
[Link]();
[Link]("Especially for large files");
[Link]();
[Link]("Buffered character writing completed");
}
}
Buffering is strongly recommended when dealing with large files.
Concatenating Files
File concatenation means combining the contents of multiple files into one file.
This is commonly done using streams by reading from one file and writing into
another.
Example: Concatenating Two Files
import [Link].*;
public class FileConcatDemo {
public static void main(String[] args) throws IOException {
FileInputStream fis1 = new FileInputStream("[Link]");
FileInputStream fis2 = new FileInputStream("[Link]");
FileOutputStream fos = new FileOutputStream("[Link]");
int ch;
while((ch = [Link]()) != -1) {
[Link](ch);
}
while((ch = [Link]()) != -1) {
[Link](ch);
}
[Link]();
[Link]();
[Link]();
[Link]("Files concatenated successfully");
}
}
This approach works for both text and binary files.
Random Access Files
Random Access Files allow reading and writing at any position in a file.
Unlike streams, which are sequential, random access enables jumping directly to a
specific location.
Java provides the RandomAccessFile class for this purpose.
Key Features of RandomAccessFile
Supports both reading and writing
Can move file pointer using seek(position)
Useful for large files and record-based storage
Example: Random Access File
import [Link].*;
public class RandomAccessDemo {
public static void main(String[] args) throws IOException {
RandomAccessFile raf = new RandomAccessFile("[Link]", "rw");
[Link]("Java Programming");
[Link](0); // Move pointer to beginning
String data = [Link]();
[Link]();
[Link]("Read from file: " + data);
}
}
The seek() method allows jumping to any byte position in the file.
Summary
Byte streams write raw binary data
Character streams write text data
Buffered streams improve performance