What is Java?
Java is a widely-used programming language known for its platform independence, scalability, and robust ecosystem. This guide starts with the basics and builds up to advanced topics, ensuring you have a clear understanding of the core concepts.
- Platform-Independent: "Write once, run anywhere" (WORA) principle.
- Object-Oriented: Focuses on using objects to model real-world entities.
- High-Level and Secure: Abstracts complex system operations while ensuring security.
Object-Oriented Programming Fundamentals
What is a Class?
A class is like a blueprint that defines what properties (attributes) and behaviors (methods) something should have. An object is an actual instance created from that blueprint.
public class Student {
// Properties (what a student has)
private String name;
private int grade;
// Constructor (how we create a student)
public Student(String name, int grade) {
this.name = name;
this.grade = grade;
}
// Method (what a student can do)
public void study() {
System.out.print("%s is studying!", name);
}
}
- The
Student
class has two properties:name
andgrade
private
means these properties can't be accessed directly from outside the class- The constructor is a special method that sets up a new student
- The
study()
method represents an action a student can take
Static vs. Non-Static Classes
What is a Static Class?
A static class is a class that is declared using the static
keyword. In Java, the concept of a truly static class does not exist for top-level classes, but inner classes can be declared as static. A static inner class belongs to the outer class, but it does not require an instance of the outer class to be instantiated.
Key characteristics of static classes include:
- They can contain only static methods and variables.
- They are used when functionality or data is not tied to an instance of a class.
- They cannot directly access non-static members of the enclosing class.
What is a Non-Static Class?
A non-static class is the default type of class in Java. It requires an instance of the class to access its members. Non-static classes can access both static and non-static members of the enclosing class and are generally used to represent entities that have unique instances.
Key characteristics of non-static classes include:
- They can contain both static and non-static methods and variables.
- They are instantiated using the
new
keyword. - They can directly access all members (static and non-static) of the enclosing class.
What is an Object?
An object is a specific instance
of a class, created using the new keyword. It represents a real-world entity.
// Create two students
Student alice = new Student("Alice", 95);
Student bob = new Student("Bob", 87);
// Make them study
alice.study(); // Outputs: Alice is studying!
bob.study(); // Outputs: Bob is studying!
Access Modifiers
Access modifiers determine the visibility or accessibility of classes, methods, and variables in Java. They control which parts of your program can use or modify certain elements.
Types of Access Modifiers
-
public
- When a class, method, or variable is declared as public, it can be accessed from anywhere in your program.
public class Example {
public String message = "This is public!";
}
-
private
- When a method or variable is declared as
private
, it can only be accessed within the same class. It is the most restrictive access level.
- When a method or variable is declared as
public class Example {
private String password = "12345";
public void showPassword() {
System.out.println(password);
}
}
-
protected
- When a method or variable is declared as
protected
, it can be accessed within the same package or by subclasses in other packages.
- When a method or variable is declared as
public class Parent {
protected String secret = "This is protected.";
}
class Child extends Parent {
public void showSecret() {
System.out.println(secret);
}
}
-
default
(no keyword)- When no access modifier is specified, the element is accessible only within the same package. This is known as package-private visibility.
class Example {
String message = "Default access modifier.";
}
Object-Oriented Programming Concepts
1. Encapsulation
Encapsulation hides the internal state of an object and allows controlled access via methods. It ensures better control over data and enhances security.
- Fields (attributes) are declared as
private
. - Public methods (getters and setters) control how these fields are accessed or modified.
public class BankAccount {
private double balance; // Private field
// Public getter
public double getBalance() {
return balance;
}
// Public setter
public void deposit(double amount) {
balance = balance + amount;
}
}
Benefits of Encapsulation:
- Data Security: Protects sensitive data from unintended modifications.
- Improved Maintenance: Internal changes to a class don't affect external code.
- Controlled Access: Ensures only valid data is entered using validation in setters.
2. Inheritance
Inheritance allows a class (child) to inherit methods and properties from another class (parent). It promotes code reuse and logical hierarchy.
- Use the
extends
keyword to establish a parent-child relationship.
public class Vehicle {
protected String brand;
public void honk() {
System.out.println("Beep!");
}
}
public class Car extends Vehicle {
public void startEngine() {
System.out.printf("%s engine starting!", brand);
}
}
Benefits of Inheritance:
- Code Reusability: Avoids duplicating code by reusing parent class methods and attributes.
- Improved Organization: Establishes a clear hierarchy (e.g., parent and child classes).
- Scalability: New functionalities can be easily added by extending classes.
3. Polymorphism
Polymorphism allows methods to perform different tasks depending on the object or the context in which they are invoked.
- Compile-time Polymorphism: Achieved using method overloading (same method name, different parameters).
- Runtime Polymorphism: Achieved using method overriding (child class redefines a parent method).
Method Overloading
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
}
Method Overriding
class Animal {
public void makeSound() {
System.out.println("Some generic sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Woof!");
}
}
Benefits of Polymorphism:
- Code Flexibility: Methods behave differently based on the object.
- Extensibility: Easily add new functionalities by overriding or overloading methods.
- Improved Readability: Simplifies how methods are invoked.
4. Abstraction
Abstraction hides complex implementation details and shows only the essential features, focusing on "what" rather than "how."
- Use
abstract
classes orinterfaces
to define methods that subclasses must implement.
What is super
?
The super keyword refers to the parent class (also called the superclass) of the current object. It is used to:
- Call the constructor of the parent class.
- Access the methods or fields of the parent class.
What is @Override
?
The @Override
annotation is used to indicate that a method in a child class is overriding a method in the parent class.
Why Use @Override
?
- Clarity: Makes it clear that the method is being overridden.
- Compile-Time Check: The compiler checks that the parent class method exists and has the same signature. This prevents accidental errors (e.g., incorrect method names).
Abstract Class
An abstract class is a class that cannot be instantiated and may contain abstract (unimplemented) and concrete (implemented) methods.
- Can have both abstract and non-abstract methods.
- Can define fields (variables) and provide implementations for some methods.
- Supports constructors.
- Allows
protected
andprivate
access modifiers for methods and fields. - Supports single inheritance (a class can extend only one abstract class).
abstract class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
// Abstract method (to be implemented by subclasses)
abstract void makeSound();
// Concrete method
public void eat() {
System.out.printf("%s is eating.", name);
}
}
class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
void makeSound() {
System.out.printf("%s says: Woof!", name);
}
}
public class Main {
public static void main(String[] args) {
Dog myDog = new Dog("Buddy");
myDog.makeSound(); // Buddy says: Woof!
myDog.eat(); // Buddy is eating.
}
}
Interface
An interface is a completely abstract type that defines a contract or a set of methods a class must implement.
- Can only have abstract methods (prior to Java 8). From Java 8, default and static methods are allowed.
- Does not contain constructors (cannot initialize fields).
- Fields are implicitly
public
,static
, andfinal
. - Supports multiple inheritance (a class can implement multiple interfaces).
interface AnimalActions {
void makeSound(); // Abstract method
void sleep(); // Abstract method
}
class Bird implements AnimalActions {
@Override
public void makeSound() {
System.out.println("Bird says: Tweet!");
}
@Override
public void sleep() {
System.out.println("Bird is sleeping.");
}
}
public class Main {
public static void main(String[] args) {
AnimalActions myBird = new Bird();
myBird.makeSound(); // Bird says: Tweet!
myBird.sleep(); // Bird is sleeping.
}
}
When to Use Abstract Classes vs Interfaces
Use Abstract Class When:
- You want to provide common functionality that all subclasses share.
- You need to define fields or methods with any access modifier (e.g.,
protected
). - You want to enforce a strict hierarchical relationship (parent-child).
Use Interface When:
- You want to define a contract for unrelated classes to follow (e.g.,
Comparable
orRunnable
). - You need multiple inheritance, as a class can implement multiple interfaces.
- You want to define default behavior for some methods (Java 8+).
Combining Abstract Class and Interface
abstract class Vehicle {
protected String brand;
public Vehicle(String brand) {
this.brand = brand;
}
public void start() {
System.out.printf("%s is starting.", brand);
}
abstract void move();
}
interface Electric {
void chargeBattery();
}
class ElectricCar extends Vehicle implements Electric {
public ElectricCar(String brand) {
super(brand);
}
@Override
void move() {
System.out.printf("%s is moving silently.", brand);
}
@Override
public void chargeBattery() {
System.out.printf("%s is charging its battery.", brand);
}
}
public class Main {
public static void main(String[] args) {
ElectricCar tesla = new ElectricCar("Tesla");
tesla.start(); // Tesla is starting.
tesla.move(); // Tesla is moving silently.
tesla.chargeBattery(); // Tesla is charging its battery.
}
}
Benefits of Abstraction:
- Simplifies Design: Focuses on essential features without worrying about implementation details.
- Encourages Modularity: Breaks down complex problems into simpler, smaller components.
- Enhances Security: Hides sensitive details from external users.
Singleton Design Pattern
The Singleton design pattern ensures that a class has only one instance and provides a global point of access to it. This is commonly used in scenarios like logging, database connections, or configuration management.
Key Features of Singleton Pattern:
- Private Constructor: Ensures that no other class can directly instantiate the Singleton class.
- Static Instance: Holds the single instance of the class.
- Public Static Method: Provides a way to access the instance.
public class Singleton {
// Static instance of the class
private static Singleton instance;
// Private constructor to prevent instantiation
private Singleton() {}
// Public method to provide access to the instance
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public void showMessage() {
System.out.println("Singleton Instance: Hello, World!");
}
}
public class Main {
public static void main(String[] args) {
// Access the Singleton instance
Singleton singleton = Singleton.getInstance();
// Call a method on the Singleton instance
singleton.showMessage();
}
}
Benefits of Singleton:
- Controlled Access: Ensures only one instance exists.
- Memory Optimization: Saves memory by not creating multiple instances.
- Global Access: Provides a single access point to the instance.
Exception Handling
Exception handling in Java allows developers to manage runtime errors in a controlled way, ensuring the program continues to operate without crashing unexpectedly. Exceptions are objects that represent an error or an unusual condition.
public class ExceptionHandling {
public static void main(String[] args) {
try {
int result = 10 / 0; // This will throw ArithmeticException
} catch (ArithmeticException e) {
System.out.println("Error: Cannot divide by zero.");
} finally {
System.out.println("Execution complete.");
}
}
}
Custom Exceptions
You can create your own exceptions by extending the Exception
or RuntimeException
class.
class InsufficientBalanceException extends Exception {
public InsufficientBalanceException(String message) {
super(message);
}
}
public class CustomException {
public static void withdraw(double amount, double balance) throws InsufficientBalanceException {
if (amount > balance) {
throw new InsufficientBalanceException("Insufficient balance for withdrawal.");
} else {
System.out.printf("Withdrawal successful. Remaining balance: %d", (balance - amount));
}
}
public static void main(String[] args) {
double balance = 500.0;
try {
withdraw(600.0, balance); // This will throw the exception
} catch (InsufficientBalanceException e) {
System.out.printf("Exception caught: %s", e.getMessage());
}
}
}
File Handling
File handling in Java allows programs to read, write, create, and manipulate files on the file system. Java provides the java.io
and java.nio
packages for file handling, offering simple and advanced functionality.
Types of File Handling Operations
- Create a File: Create a new file if it does not exist.
- Write to a File: Add content to a file.
- Read from a File: Retrieve data from a file.
- Delete a File: Remove a file from the file system.
Classes Used in File Handling
File
: Represents file and directory pathnames.FileWriter
andBufferedWriter
: Used for writing text to files.FileReader
andBufferedReader
: Used for reading text from files.Scanner
: Also used for reading files line by line.PrintWriter
: For writing formatted text to files.
1. Creating a File
Use the File class to create a new file. The createNewFile()
method returns true
if the file was created successfully.
import java.io.File;
import java.io.IOException;
public class CreateFileExample {
public static void main(String[] args) {
File file = new File("example.txt");
try {
if (file.createNewFile()) {
System.out.printf("File created: %s", file.getName());
} else {
System.out.println("File already exists.");
}
} catch (IOException e) {
System.out.println("An error occurred.");
e.printStackTrace();
}
}
}
2. Writing to a File
Use the FileWriter
class to write data to a file. You can append to a file by setting FileWriter
to append mode.
import java.io.FileWriter;
import java.io.IOException;
public class WriteToFileExample {
public static void main(String[] args) {
try {
FileWriter writer = new FileWriter("example.txt");
writer.write("Hello, World!\n");
writer.write("File handling in Java is easy!");
writer.close();
System.out.println("Successfully wrote to the file.");
} catch (IOException e) {
System.out.println("An error occurred.");
e.printStackTrace();
}
}
}
3. Reading from a File
You can read files using:
FileReader
andBufferedReader
for line-by-line reading.Scanner
for more flexibility (e.g., reading tokens).
Using BufferedReader
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class ReadFileExample {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("An error occurred.");
e.printStackTrace();
}
}
}
Using Scanner
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class ReadWithScanner {
public static void main(String[] args) {
try {
File file = new File("example.txt");
Scanner scanner = new Scanner(file);
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
}
scanner.close();
} catch (FileNotFoundException e) {
System.out.println("File not found.");
e.printStackTrace();
}
}
}
4. Deleting a File
The delete() method of the File class is used to delete a file.
import java.io.File;
public class DeleteFileExample {
public static void main(String[] args) {
File file = new File("example.txt");
if (file.delete()) {
System.out.printf("File deleted: %s", file.getName());
} else {
System.out.println("Failed to delete the file.");
}
}
}
Error Handling in File Operations
File operations can throw exceptions like:
IOException
: When file operations fail (e.g., write failure).FileNotFoundException
: When trying to read a file that doesn’t exist.
Always use try-catch
blocks or throws
declarations to handle these errors gracefully.
Threading
Threading is a technique in Java that allows multiple parts of a program (threads) to run simultaneously. This is especially useful for performing tasks concurrently, such as downloading files while processing user input.
What is a Thread?
- A thread is the smallest unit of execution in a program.
- Every Java application starts with a main thread that runs the
main()
method. - Multiple threads can be created to perform tasks concurrently.
Thread States
A thread in Java can be in one of the following six states as defined by the Thread.State
enum:
-
NEW
- The thread has been created but has not yet started.
- A thread is in this state immediately after the
Thread
object is instantiated, and before thestart()
method is called.
-
RUNNABLE
-
The thread is either running or ready to run whenever it gets CPU time.
-
This state includes what some operating systems may call “ready” and “running” states.
-
-
BLOCKED
- The thread is blocked, waiting for a monitor lock (e.g., attempting to enter a
synchronized
block or method) that another thread is currently holding.
- The thread is blocked, waiting for a monitor lock (e.g., attempting to enter a
-
WAITING
- The thread is waiting indefinitely for another thread to perform a particular action (e.g., a call to
wait()
without a timeout). - This usually happens when a thread calls
Object.wait()
and is waiting for anotify()
ornotifyAll()
on the same object.
- The thread is waiting indefinitely for another thread to perform a particular action (e.g., a call to
-
TIMED_WAITING
- The thread is waiting for another thread to perform an action for a specified waiting time (e.g.,
Thread.sleep(long millis)
,Object.wait(long timeout)
,Thread.join(long millis)
). - After this time elapses, or if the event it’s waiting for occurs, the thread transitions back to RUNNABLE.
- The thread is waiting for another thread to perform an action for a specified waiting time (e.g.,
-
TERMINATED (or DEAD)
- The thread has completed its execution, either by returning from the
run()
method or by throwing an uncaught exception.
- The thread has completed its execution, either by returning from the
You can retrieve a thread’s current state by calling the getState()
method on a Thread
object.
Creating Threads
Creating Threads in Java
- Extending the
Thread
class - Implementing the
Runnable
interface
1. Extending the Thread Class
In this approach, you create a new class that extends Thread and override its run()
method.
class SomeThread extends Thread {
@Override
public void run() {
System.out.printf("Thread is running: %s", Thread.currentThread().getName());
}
}
public class Example {
public static void main(String[] args) {
SomeThread thread1 = new SomeThread();
SomeThread thread2 = new SomeThread();
thread1.start(); // Starts thread1
thread2.start(); // Starts thread2
}
}
2. Implementing the Runnable
Interface
This is a more flexible approach because the class implementing Runnable
can extend another class.
class Task implements Runnable {
@Override
public void run() {
System.out.printf("Thread is running: %s", Thread.currentThread().getName());
}
}
public class Example {
public static void main(String[] args) {
Thread thread1 = new Thread(new Task());
Thread thread2 = new Thread(new Task());
thread1.start(); // Starts thread1
thread2.start(); // Starts thread2
}
}
Thread Synchronization
When multiple threads access shared resources (e.g., variables or files), synchronization ensures that only one thread can access the resource at a time to prevent data inconsistency.
class Counter {
private int count = 0;
public synchronized void increment() {
count = count + 1;
}
public int getCount() {
return count;
}
}
public class SynchronizedExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) counter.increment();
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) counter.increment();
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.printf("Final count: %d", counter.getCount());
}
}
Inter-Thread Communication
Java provides methods like wait()
, notify()
, and notifyAll()
to allow threads to communicate with each other and coordinate their work. All these methods are defined in the Object
class (not Thread
), and must be called inside a synchronized context.
-
wait()
- Causes the current thread to release the lock it holds on the object’s monitor and go into the WAITING state.
- The thread remains in this state until another thread calls
notify()
ornotifyAll()
on the same object (or until it is interrupted).
-
notify()
- Wakes up one thread that is waiting on the same object’s monitor.
- If more than one thread is waiting, the one chosen is not specified (i.e., it’s up to the JVM’s thread scheduler).
-
notifyAll()
- Wakes up all threads that are waiting on the same object’s monitor.
- The awakened threads will then compete to reacquire the lock.
import java.util.LinkedList;
class ProducerConsumer {
private LinkedList<Integer> queue = new LinkedList<>();
private final int CAPACITY = 5;
public synchronized void produce() throws InterruptedException {
int value = 0;
while (true) {
while (queue.size() == CAPACITY) {
wait(); // Wait if the queue is full
}
queue.add(value);
System.out.printf("Produced: %d", value);
value = value + 1;
notifyAll(); // Notify the consumer thread
Thread.sleep(1000);
}
}
public synchronized void consume() throws InterruptedException {
while (true) {
while (queue.isEmpty()) {
wait(); // Wait if the queue is empty
}
int value = queue.removeFirst();
System.out.printf("Consumed: %d", value);
notifyAll(); // Notify the producer thread
Thread.sleep(1000);
}
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();
Thread producer = new Thread(() -> {
try {
pc.produce();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
pc.consume();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
Other Thread Methods
-
Thread.sleep(long millis)
- Pauses the current thread for the specified time (milliseconds).
- The thread is placed in the TIMED_WAITING state.
- Does not release any locks held by the thread.
-
join()
- A calling thread will wait until the target thread (on which
join()
is called) finishes its execution (TERMINATED state). - Often used to make sure one thread completes before another thread starts or continues.
- A calling thread will wait until the target thread (on which
-
Thread.yield()
- A hint to the thread scheduler that the current thread is willing to yield its current use of the CPU.
- The scheduler may or may not choose to honor this hint.
-
Thread.interrupt()
- Signals the thread that it should stop its current activity if it’s designed to respond to interruption.
- Throws an
InterruptedException
if the thread is blocked (e.g., sleeping, waiting). - It’s up to the thread’s
run()
method to handleInterruptedException
properly.
Benefits of Multithreading
- Improved Performance: Multiple threads allow tasks to be executed in parallel, utilizing CPU cores efficiently.
- Responsiveness: Increases the responsiveness of applications, e.g., user interface stays active while a task runs in the background.
- Better Resource Utilization: Threads share the same memory and resources, reducing overhead compared to processes.
- Scalability: Makes it easier to design scalable systems that handle multiple tasks simultaneously.