Hi there,
This article is mostly AI generated. I am aggregating all the concepts that I need to know to pass an Java assessment for my job on 9th September and an on-to-one interview on 10th September.
Today is 5th September. I need to complete the assignment of each Java module by 6th September.
Evolution of Java
1. Creation of Java (1991-1995)
Origin: Java was originally developed by James Gosling and his team at Sun Microsystems as part of a project called the Green Project in 1991. The goal was to create a language that could be used for embedded systems, such as televisions and set-top boxes.
Oak: The language was initially called Oak (after an oak tree outside Gosling's office) but was later renamed to Java in 1995 due to trademark issues.
Platform Independence: The key idea behind Java was "write once, run anywhere" (WORA). This meant that Java programs could run on any platform that supported the Java Virtual Machine (JVM), without the need for platform-specific adjustments.
2. Java 1.0 (1996)
Public Release: Java was officially released to the public in 1996. This version was called Java 1.0 and included core features like the JVM, a basic API, and key libraries for networking, graphics, and user interfaces.
Applets: Java 1.0 popularized the concept of applets, small programs that could run in web browsers, giving Java an edge in the early internet era.
3. Java 1.1 (1997)
Enhancements: Java 1.1 introduced several new features such as inner classes, JavaBeans (a component architecture), Reflection API, JDBC (Java Database Connectivity), and improved event handling for GUIs.
Standardisation: Java became a more standardized and mature language, gaining traction among developers.
4. Java 2 (1998-2004)
-
Rebranding: Java 2 was released in 1998 and marked a major milestone in its evolution. It was no longer referred to by version numbers but instead grouped into different editions:
- Java Standard Edition (J2SE) for desktop applications.
- Java Enterprise Edition (J2EE) for large-scale applications.
- Java Micro Edition (J2ME) for mobile devices.
Swing: Introduced a more powerful GUI library, Swing, replacing the older AWT (Abstract Window Toolkit).
Collections Framework: The Java Collections Framework was introduced, providing standardised interfaces for working with lists, sets, maps, and queues.
5. Java 5 (2004)
Generics: One of the biggest updates in Java 5 (initially called Java 1.5) was the introduction of Generics, which allowed for type-safe collections.
Annotations: Another major addition was the introduction of Annotations, which became essential in frameworks like Spring, Hibernate, and JUnit.
Enhanced for-loop, Varargs, Enums, Static Imports, and Concurrency Utilities were also introduced, making Java more powerful and flexible.
6. Java 6 (2006)
Improved Performance: Java 6 (also known as Mustang) focused on performance improvements, including better support for scripting languages, updates to the Swing framework, and support for Web Services.
Scripting Language Support: Introduced the javax.script package, allowing for integration with dynamic languages like JavaScript.
7. Java 7 (2011)
Language Enhancements: Java 7 (codenamed Dolphin) added several small but useful language features like the try-with-resources statement (to simplify resource management), diamond syntax for generics, and switch statements with strings.
Fork/Join Framework: Introduced to facilitate parallel programming and improve performance in multi-core systems.
8. Java 8 (2014)
Functional Programming: Java 8 was a revolutionary release, introducing Lambdas and Streams, which brought functional programming capabilities to the language.
Date and Time API: A new Date and Time API was introduced to replace the clunky old
java.util.Date
andCalendar
classes.Default Methods: Allowed interfaces to have default method implementations, improving backward compatibility.
Nashorn JavaScript Engine: Introduced for embedding JavaScript within Java applications.
9. Java 9 (2017)
Modular System: The major feature of Java 9 was the Java Platform Module System (also called Project Jigsaw), which aimed to modularise the JDK and make it more scalable and efficient.
REPL Tool (JShell): Introduced the JShell tool for interactively evaluating snippets of Java code, improving the learning and prototyping experience.
Improved Stream API and multi-release JARs.
10. Java 10 and 11 (2018)
Local Variable Type Inference: Introduced the
var
keyword in Java 10, allowing the compiler to infer types of local variables.Java 11 became the next Long-Term Support (LTS) release. It removed many deprecated features, introduced HTTP Client API, and continued enhancing modularity.
11. Java 12-17 (2019-2021)
Pattern Matching: Java 14 introduced Pattern Matching for
instanceof
, making type checks and casting more concise.Text Blocks: Java 13 introduced Text Blocks, simplifying multi-line string literals.
Records: Introduced in Java 16, Records allow concise syntax for creating immutable data classes.
Sealed Classes: Introduced in Java 17, these allow more control over which classes can extend a particular class, enhancing security and design.
12. Java 21 (2023)
Virtual Threads: A major feature of Java 21 is virtual threads (from Project Loom), which simplifies the development of concurrent applications by allowing developers to create millions of lightweight threads easily.
Pattern Matching for
switch
, String Templates, and Sequenced Collections were also among the added features.
Java Architecture
Java architecture revolves around the concept of platform independence, achieved through the use of the Java Virtual Machine (JVM) and the division of code execution into multiple stages. Here’s a breakdown of the main components and how they fit together:
1. Java Architecture Overview
The Java architecture consists of the following main layers:
- Java Source Code
* You write code in `.java` files using the Java programming language. This code is human-readable and needs to be compiled into machine-readable instructions.
- Java Compiler
* The Java compiler (`javac`) translates the `.java` source code into **byte-code**, which is an intermediate representation. This byte-code is stored in `.class` files. Unlike other languages that compile directly to machine code, Java byte-code is platform-independent.
- Java Virtual Machine (JVM)
* The JVM is responsible for executing the Java byte-code. The key feature of JVM is its **platform independence**, meaning it can run on different operating systems like Windows, Linux, macOS, etc. Each platform has its own JVM implementation that understands the underlying operating system while executing the byte-code.
- Java Runtime Environment (JRE)
* The JRE provides the necessary libraries and JVM for running Java applications. It includes the JVM, core libraries, and other components needed to execute Java byte-code. This is what end-users install to run Java applications.
- Java Development Kit (JDK)
* The JDK is a superset of the JRE. It includes everything that the JRE has, but also adds tools required for Java development, such as the Java compiler (`javac`), debuggers, profilers, and more.
Detailed Java Architecture Layers
1. Classloader
What it is: A part of the JVM that dynamically loads classes into memory as needed.
How it works: When a program is run, the class loader loads the
.class
files (byte-code) into memory for execution. It ensures that classes are loaded only when required.-
Types:
- Bootstrap ClassLoader: Loads core Java libraries.
- Extension ClassLoader: Loads additional libraries (optional packages).
- Application ClassLoader: Loads classes from the classpath defined by the user.
2. Bytecode
What it is: The intermediate code generated by the Java compiler, which is platform-independent.
How it works: This byte-code is interpreted or compiled by the JVM on any machine, making the code portable.
3. Execution Engine
-
What it is: The heart of the JVM that reads and executes the bytecode. It consists of:
- Interpreter: Interprets and executes the bytecode line by line.
- Just-In-Time (JIT) Compiler: Compiles the frequently used bytecode into native machine code to improve performance. JIT optimizes execution by reducing the overhead of interpreting bytecode repeatedly.
4. Java Native Interface (JNI)
What it is: A framework that allows Java code running in the JVM to interact with native applications and libraries written in other languages like C or C++.
How it works: JNI is used when you need to access system resources or optimise specific parts of your application with native code.
5. Java Native Method Libraries
- What it is: Sometimes Java uses native libraries to perform platform-specific tasks. These are linked to Java code via JNI and are typically written in low-level languages like C/C++.
Key Components:
- Java Runtime Environment (JRE):
* **Components**:
* **JVM**: Runs byte-code, platform-specific.
* **Core Libraries**: Standard Java libraries, including `java.lang`, `java.util`, and [`java.io`](http://java.io), among others.
- Java Virtual Machine (JVM):
* **Primary Responsibilities**:
* **Class Loading**: Dynamically loads and links classes.
* **Byte-code Execution**: Interprets or compiles bytecode into machine code.
* **Memory Management**: Manages heap memory for objects, stack memory for method execution, and garbage collection.
* **Thread Management**: Provides concurrency and multi-threading capabilities.
- Memory Areas in JVM:
* **Method Area**: Stores class-level information (e.g., class metadata, method data).
* **Heap**: The runtime data area from which memory for all class instances and arrays is allocated (object storage).
* **Stack**: Stores frames for each method invocation, including local variables and partial results.
* **Program Counter (PC) Register**: Keeps track of the address of the current JVM instruction being executed.
* **Native Method Stack**: Holds native method calls using JNI.
How Java Works (High-Level Flow):
Compilation: Java source code (
.java
file) is compiled byjavac
into platform-independent byte-code (.class
file).Class Loading: The class loader loads the compiled
.class
files (byte-code) into memory.Execution: The execution engine (JVM) reads and interprets the byte-code or compiles it to native code (JIT) and then executes it.
Garbage Collection: The JVM automatically reclaims memory by identifying and discarding objects that are no longer in use.
Diagram of Java Architecture
[ Java Source Code ]
↓
[ Java Compiler (javac) ]
↓
[ Java Bytecode (.class files) ]
↓
[ Classloader ]
↓
[ JVM - Java Virtual Machine ]
┌─────────────────────────────┐
│ 1. Class Loader │
│ 2. Bytecode Verifier │
│ 3. Interpreter / JIT │
│ 4. Garbage Collector │
│ 5. Memory Manager │
└─────────────────────────────┘
↓
[ Operating System & Hardware ]
Key Advantages of Java Architecture:
Platform Independence: Thanks to JVM, Java can run on any platform that supports the JVM without modification.
Security: Byte-code verification and JVM sandboxing ensure a secure execution environment.
Performance: JIT compilation optimises frequently executed code for faster execution.
Multithreading: Java provides built-in support for multithreading and concurrency.
Data Types
In Java, data types specify the type of data that a variable can hold. Java is a strongly typed language, which means every variable must be declared with a data type before it is used. Java has two broad categories of data types:
Primitive Data Types
Reference/Object Data Types
1. Primitive Data Types
These are the most basic data types, directly supported by the language. Java defines 8 primitive data types, each corresponding to a simple value like an integer, a character, or a boolean. These data types are not objects and hold raw data.
Primitive Data Types in Java
Data Type | Size | Default Value | Description |
---|---|---|---|
byte |
1 byte | 0 | Stores small integer values |
short |
2 bytes | 0 | Stores larger integer values than byte
|
int |
4 bytes | 0 | Most common integer data type |
long |
8 bytes | 0L | Stores large integer values |
float |
4 bytes | 0.0f | Single-precision floating-point |
double |
8 bytes | 0.0d | Double-precision floating-point |
char |
2 bytes | '\u0000' | Stores a single character (Unicode) |
boolean |
1 bit | false |
Stores a boolean value (true or false ) |
Primitive Data Type Examples
// Integer data types
byte a = 100; // Range: -128 to 127
short b = 10000; // Range: -32,768 to 32,767
int c = 100000; // Range: -2^31 to 2^31-1
long d = 100000L; // Range: -2^63 to 2^63-1
// Floating-point data types
float e = 12.5f; // Single-precision
double f = 123.45; // Double-precision
// Character data type
char g = 'A'; // Represents a single character
// Boolean data type
boolean h = true; // Represents true or false
byte
,short
,int
, andlong
are used for integers of different ranges.float
anddouble
are used for decimal numbers.float
requires thef
suffix, anddouble
is the default for floating-point numbers.char
represents single characters and is enclosed in single quotes ('A'
).boolean
holds true or false values.
2. Reference/Object Data Types
These data types refer to objects or instances of classes. Reference data types store the memory address (reference) of the object they point to, not the actual data.
Reference Types in Java
Data Type | Description |
---|---|
String |
Represents a sequence of characters |
Array |
Represents a collection of fixed-size elements of the same type |
Class |
Represents user-defined types and objects |
Interface |
Represents a reference to an interface |
Reference Data Type Examples
// String type
String name = "John Doe";
// Array type
int[] numbers = {1, 2, 3, 4, 5};
// Object type
Car myCar = new Car("Tesla", 2024); // Car is a class
String
is a commonly used reference type in Java. Unlike primitive types, it is a class that represents a sequence of characters.Array
is a reference type that stores multiple values of the same type.Objects are instances of classes. When a class is created, it serves as a blueprint for creating objects.
Differences Between Primitive and Reference Types
Feature | Primitive Types | Reference Types |
---|---|---|
Storage | Store actual values | Store references (memory addresses) to objects |
Default Values | Default values like 0 , false
|
Default value is null
|
Size | Fixed size (depends on data type) | Size depends on the object being referenced |
Example |
int , boolean , char
|
String , Array , Object
|
Primitive Data Type Ranges
byte
: 1 byte (8 bits), Range: -128 to 127short
: 2 bytes (16 bits), Range: -32,768 to 32,767int
: 4 bytes (32 bits), Range: -2^31 to 2^31 - 1long
: 8 bytes (64 bits), Range: -2^63 to 2^63 - 1float
: 4 bytes (32 bits), Range: approximately ±3.40282347E+38Fdouble
: 8 bytes (64 bits), Range: approximately ±1.79769313486231570E+308char
: 2 bytes (16 bits), Unicode character range (0 to 65,535)boolean
: 1 bit (true/false)
Wrapper Classes for Primitive Data Types
Java provides wrapper classes that correspond to each primitive type. These classes allow primitive types to be used as objects.
Primitive Type | Wrapper Class |
---|---|
byte |
Byte |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
char |
Character |
boolean |
Boolean |
Example of Wrapper Classes
int a = 5; // Primitive int
Integer aWrapper = a; // Autoboxing: primitive to object
int b = aWrapper; // Unboxing: object to primitive
Type Casting
You may need to convert one data type into another. This is known as type casting.
Implicit Casting (Widening Conversion)
When you convert a smaller type to a larger type, Java performs the conversion automatically (implicit casting).
int a = 100;
long b = a; // Automatic conversion from int to long
Explicit Casting (Narrowing Conversion)
When converting a larger type to a smaller type, you must explicitly cast the value (narrowing conversion).
long a = 100L;
int b = (int) a; // Explicit casting from long to int
Operators
In Java, operators are special symbols or keywords used to perform operations on variables and values. Operators are classified into different types based on the kind of operation they perform.
Types of Operators in Java
Arithmetic Operators
Relational (Comparison) Operators
Logical Operators
Bitwise Operators
Assignment Operators
Unary Operators
Ternary Operator
1. Arithmetic Operators
These operators are used to perform basic mathematical operations.
Operator | Description | Example |
---|---|---|
+ |
Addition | a + b |
- |
Subtraction | a - b |
* |
Multiplication | a * b |
/ |
Division | a / b |
% |
Modulus (Remainder) | a % b |
int a = 10;
int b = 3;
System.out.println(a + b); // 13
System.out.println(a - b); // 7
System.out.println(a * b); // 30
System.out.println(a / b); // 3
System.out.println(a % b); // 1
2. Relational (Comparison) Operators
These operators are used to compare two values and return a boolean (true
or false
).
Operator | Description | Example |
---|---|---|
== |
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 | a <= b |
int a = 5;
int b = 10;
System.out.println(a == b); // false
System.out.println(a != b); // true
System.out.println(a > b); // false
System.out.println(a < b); // true
3. Logical Operators
Logical operators are used to combine multiple boolean expressions.
Operator | Description | Example |
---|---|---|
&& |
Logical AND | (a > 5 && b < 10) |
` | ` | |
! |
Logical NOT | !(a > 5) |
boolean x = true;
boolean y = false;
System.out.println(x && y); // false
System.out.println(x || y); // true
System.out.println(!x); // false
4. Bitwise Operators
Bitwise operators operate on binary representations of integers.
Operator | Description | Example |
---|---|---|
& |
Bitwise AND | a & b |
` | ` | Bitwise OR |
^ |
Bitwise XOR | a ^ b |
~ |
Bitwise Complement | ~a |
<< |
Left shift | a << 2 |
>> |
Right shift | a >> 2 |
int a = 5; // 0101 in binary
int b = 3; // 0011 in binary
System.out.println(a & b); // 1 (0001)
System.out.println(a | b); // 7 (0111)
System.out.println(a ^ b); // 6 (0110)
System.out.println(~a); // -6 (Inverts all bits)
System.out.println(a << 1); // 10 (Shifts bits left by 1)
System.out.println(a >> 1); // 2 (Shifts bits right by 1)
5. Assignment Operators
These operators are used to assign values to variables.
Operator | Description | Example |
---|---|---|
= |
Assigns value | a = b |
+= |
Adds then assigns |
a += b (a = a + b) |
-= |
Subtracts then assigns |
a -= b (a = a - b) |
*= |
Multiplies then assigns |
a *= b (a = a * b) |
/= |
Divides then assigns |
a /= b (a = a / b) |
%= |
Modulus then assigns |
a %= b (a = a % b) |
int a = 5;
a += 3; // a = a + 3, so a becomes 8
a *= 2; // a = a * 2, so a becomes 16
6. Unary Operators
Unary operators are used with a single operand.
Operator | Description | Example |
---|---|---|
+ |
Unary plus (positive number) | +a |
- |
Unary minus (negative number) | -a |
++ |
Increment (adds 1) |
++a or a++
|
-- |
Decrement (subtracts 1) |
--a or a--
|
! |
Logical NOT | !a |
int a = 5;
System.out.println(++a); // 6 (pre-increment)
System.out.println(a++); // 6 (post-increment, value increments after expression)
System.out.println(a); // 7
System.out.println(-a); // -7 (Unary minus)
7. Ternary Operator
The ternary operator is a shorthand for if-else
statements and has the following syntax:
condition ? value_if_true : value_if_false
int a = 10;
int b = 20;
int max = (a > b) ? a : b; // If a is greater than b, max will be a; otherwise, max will be b.
System.out.println(max); // 20
Operator Precedence
Operators have different levels of precedence that determine the order in which expressions are evaluated. For example, multiplication and division have higher precedence than addition and subtraction.
int result = 10 + 5 * 2; // Multiplication happens first: 10 + (5 * 2) = 20
System.out.println(result); // Output: 20
To change the order of operations, you can use parentheses:
int result = (10 + 5) * 2; // Addition happens first: (10 + 5) * 2 = 30
System.out.println(result); // Output: 30
Console Input and Output
Input and output (I/O) handling in Java is crucial for interacting with users, files, and other external sources. Java provides several classes and methods for handling input and output efficiently, particularly in the java.io
and java.util
packages.
Input Handling
1. Console Input Using Scanner
The Scanner
class is the most commonly used for reading input from the console. It can read data of different types (e.g., int
, float
, String
).
Example: Reading Input from the Console
import java.util.Scanner;
public class InputExample {
public static void main(String[] args) {
// Create a Scanner object
Scanner scanner = new Scanner(System.in);
// Reading a string input
System.out.print("Enter your name: ");
String name = scanner.nextLine();
// Reading an integer input
System.out.print("Enter your age: ");
int age = scanner.nextInt();
// Output the values
System.out.println("Name: " + name);
System.out.println("Age: " + age);
// Close the scanner
scanner.close();
}
}
2. Reading Input Using BufferedReader
The BufferedReader
class provides another way to read input. It is efficient for reading text from character-based input streams and is commonly used for reading data from the console or files.
Example: Reading Input Using BufferedReader
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class BufferedReaderExample {
public static void main(String[] args) throws IOException {
// Create BufferedReader using InputStreamReader
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
// Reading a string input
System.out.print("Enter your name: ");
String name = reader.readLine();
// Reading an integer input
System.out.print("Enter your age: ");
int age = Integer.parseInt(reader.readLine());
// Output the values
System.out.println("Name: " + name);
System.out.println("Age: " + age);
}
}
Output Handling
Java provides several ways to handle output, primarily using the System.out
stream and file handling classes.
1. Console Output Using System.out.println
The System.out.println()
method is used to print output to the console. It automatically appends a newline at the end, while System.out.print()
does not.
Example: Console Output
public class OutputExample {
public static void main(String[] args) {
// Print output using println
System.out.println("Hello, World!");
// Print without a newline
System.out.print("This is ");
System.out.print("a single line.");
}
}
Control Statements
Control statements in Java are used to control the flow of execution of a program. These include decision-making statements, looping statements, and branching statements. Let's explore the different types of control statements in Java.
1. Decision-Making Statements
These statements allow the program to choose different paths of execution based on certain conditions.
if
Statement
The if
statement evaluates a boolean expression and executes the block of code if the condition is true
.
Syntax:
if (condition) {
// Code to be executed if the condition is true
}
Example:
int number = 10;
if (number > 0) {
System.out.println("The number is positive.");
}
if-else
Statement
The if-else
statement provides an alternative path if the condition in the if
statement is false.
Syntax:
if (condition) {
// Code to be executed if the condition is true
} else {
// Code to be executed if the condition is false
}
Example:
int number = -10;
if (number > 0) {
System.out.println("The number is positive.");
} else {
System.out.println("The number is negative or zero.");
}
else-if
Ladder
The else-if
ladder allows for multiple conditions to be checked sequentially. If one condition is true, its corresponding block of code is executed, and the rest are skipped.
Syntax:
if (condition1) {
// Code to be executed if condition1 is true
} else if (condition2) {
// Code to be executed if condition2 is true
} else {
// Code to be executed if none of the above conditions are true
}
Example:
int number = 0;
if (number > 0) {
System.out.println("The number is positive.");
} else if (number < 0) {
System.out.println("The number is negative.");
} else {
System.out.println("The number is zero.");
}
switch
Statement
The switch
statement allows a variable to be tested against a list of values. Each value is called a "case," and the matching case block is executed.
Syntax:
switch (expression) {
case value1:
// Code for case value1
break;
case value2:
// Code for case value2
break;
// More cases...
default:
// Code if no cases match
}
Example:
int day = 2;
switch (day) {
case 1:
System.out.println("Monday");
break;
case 2:
System.out.println("Tuesday");
break;
default:
System.out.println("Invalid day");
}
2. Looping Statements
Looping statements allow a block of code to be executed repeatedly based on a condition.
for
Loop
The for
loop is used when the number of iterations is known. It consists of three parts: initialization, condition, and update.
Syntax:
for (initialization; condition; update) {
// Code to be executed
}
Example:
for (int i = 0; i < 5; i++) {
System.out.println("i = " + i);
}
while
Loop
The while
loop continues executing as long as the condition remains true
.
Syntax:
while (condition) {
// Code to be executed
}
Example:
int i = 0;
while (i < 5) {
System.out.println("i = " + i);
i++;
}
do-while
Loop
The do-while
loop is similar to the while
loop, but the code is executed at least once before the condition is checked.
Syntax:
do {
// Code to be executed
} while (condition);
Example:
int i = 0;
do {
System.out.println("i = " + i);
i++;
} while (i < 5);
3. Branching Statements
Branching statements allow for changing the flow of control within loops or conditional statements.
break
Statement
The break
statement is used to exit a loop or switch statement prematurely.
Example (In a for
Loop):
for (int i = 0; i < 10; i++) {
if (i == 5) {
break; // Exit the loop when i equals 5
}
System.out.println(i);
}
continue
Statement
The continue
statement skips the current iteration of a loop and moves on to the next iteration.
Example:
for (int i = 0; i < 10; i++) {
if (i == 5) {
continue; // Skip the iteration when i equals 5
}
System.out.println(i);
}
return
Statement
The return
statement exits from the current method and returns a value (if any) to the caller.
Example:
public int addNumbers(int a, int b) {
return a + b; // Return the sum
}
OOP
Object-Oriented Programming (OOP) is a programming paradigm that uses objects and classes to design and structure software. Java is fundamentally based on OOP principles, making it easier to model real-world problems. Let’s dive into the core concepts of OOP in Java:
1. Classes and Objects
Class:
- A class is a blueprint or template for creating objects. It defines the properties (attributes) and behaviours (methods) that the objects created from the class will have.
class Car {
// Properties (Fields)
String model;
String color;
int year;
// Constructor
Car(String model, String color, int year) {
this.model = model;
this.color = color;
this.year = year;
}
// Method (Behavior)
void drive() {
System.out.println(model + " is driving.");
}
}
Object:
- An object is an instance of a class. When a class is instantiated, an object is created in memory. Objects have their own states and behaviours defined by the class.
Car car1 = new Car("Tesla Model S", "Red", 2024);
car1.drive(); // Output: Tesla Model S is driving.
2. Core OOP Principles
1. Encapsulation
Encapsulation is the concept of bundling data (variables) and methods (functions) that operate on the data into a single unit or class. It also restricts access to certain details of an object to maintain a clear separation between an object's interface and its internal implementation (data hiding).
In Java, encapsulation is achieved using access modifiers (
private
,public
,protected
).
class Account {
private double balance; // Encapsulated data
// Getter method
public double getBalance() {
return balance;
}
// Setter method
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
}
2. Inheritance
- Inheritance allows a new class (subclass or derived class) to inherit the properties and behaviours (fields and methods) of an existing class (superclass or base class). This promotes code reusability and establishes a hierarchical relationship between classes.
// Superclass (Parent Class)
class Animal {
void eat() {
System.out.println("This animal eats food.");
}
}
// Subclass (Child Class)
class Dog extends Animal {
void bark() {
System.out.println("The dog barks.");
}
}
// Usage
Dog dog = new Dog();
dog.eat(); // Inherited method from Animal
dog.bark(); // Method in Dog class
3. Polymorphism
Polymorphism allows one entity (method or object) to take many forms. In Java, polymorphism is achieved in two main ways: method overriding and method overloading.
Method Overriding: When a subclass provides a specific implementation of a method that is already defined in its superclass.
class Animal {
void sound() {
System.out.println("This animal makes a sound.");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("The dog barks.");
}
}
// Usage
Animal myAnimal = new Dog(); // Polymorphism
myAnimal.sound(); // Output: The dog barks.
- Method Overloading: Multiple methods in the same class have the same name but different parameter lists (different type or number of parameters).
class MathOperations {
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
}
4. Abstraction
Abstraction is the concept of hiding the complex implementation details and exposing only the essential features. It allows a programmer to focus on "what an object does" rather than "how it does it."
In Java, abstraction is achieved through abstract classes and interfaces.
Abstract Class: A class that cannot be instantiated and may have abstract methods (methods without a body). Subclasses must provide implementations for abstract methods.
abstract class Vehicle {
abstract void start(); // Abstract method
}
class Car extends Vehicle {
@Override
void start() {
System.out.println("The car starts with a key.");
}
}
// Usage
Vehicle myCar = new Car();
myCar.start(); // Output: The car starts with a key.
- Interface: An interface is a contract that a class must follow. It can contain abstract methods (without implementations) that any implementing class must define.
interface Flyable {
void fly(); // Abstract method
}
class Airplane implements Flyable {
public void fly() {
System.out.println("The airplane flies in the sky.");
}
}
// Usage
Flyable plane = new Airplane();
plane.fly(); // Output: The airplane flies in the sky.
Constructors
In Java, a constructor is a special type of method that is called automatically when an object of a class is instantiated (i.e., created). Constructors are used to initialize the state of an object. Unlike regular methods, constructors have no return type, not even void
, and their name must match the class name.
Key Features of Constructors
Name: A constructor must have the same name as the class in which it is declared.
No Return Type: Constructors do not have a return type, not even
void
.Automatically Invoked: A constructor is called automatically when an object is created using the
new
keyword.Purpose: The main purpose of a constructor is to initialize objects.
Types: There are two main types of constructors: default constructors and parameterized constructors.
Types of Constructors
1. Default Constructor
A default constructor is the constructor that Java provides automatically if no constructors are defined in the class. If you define your own constructor, Java will not provide the default constructor anymore.
The default constructor has no parameters and simply initializes the object without setting any specific values.
Example:
class Person {
// Default constructor
public Person() {
System.out.println("Person object is created.");
}
}
public class Main {
public static void main(String[] args) {
// Creating an object; default constructor is called
Person p = new Person(); // Output: Person object is created.
}
}
In this case, the Person
class has a default constructor, which is invoked when an object is instantiated.
2. Parameterized Constructor
A parameterized constructor is a constructor that accepts parameters to initialize the object with specific values. This allows for greater control over how an object is initialized.
Example:
class Person {
String name;
int age;
// Parameterized constructor
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void displayInfo() {
System.out.println("Name: " + name + ", Age: " + age);
}
}
public class Main {
public static void main(String[] args) {
// Creating an object and passing values to the parameterized constructor
Person p1 = new Person("Alice", 30);
p1.displayInfo(); // Output: Name: Alice, Age: 30
Person p2 = new Person("Bob", 25);
p2.displayInfo(); // Output: Name: Bob, Age: 25
}
}
In this case, the Person
class has a parameterized constructor that accepts the name
and age
of the person and initializes the corresponding fields of the object.
Constructor Overloading
Just like methods, constructors in Java can be overloaded. This means that a class can have multiple constructors, each with a different parameter list.
Example:
class Person {
String name;
int age;
// Default constructor
public Person() {
this.name = "Unknown";
this.age = 0;
}
// Parameterized constructor with two parameters
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Parameterized constructor with one parameter
public Person(String name) {
this.name = name;
this.age = 18; // Default age
}
public void displayInfo() {
System.out.println("Name: " + name + ", Age: " + age);
}
}
public class Main {
public static void main(String[] args) {
// Using different constructors
Person p1 = new Person(); // Default constructor
p1.displayInfo(); // Output: Name: Unknown, Age: 0
Person p2 = new Person("Alice", 30); // Parameterized constructor with two parameters
p2.displayInfo(); // Output: Name: Alice, Age: 30
Person p3 = new Person("Bob"); // Parameterized constructor with one parameter
p3.displayInfo(); // Output: Name: Bob, Age: 18
}
}
In this example, the Person
class has three constructors:
A default constructor that sets default values.
A constructor that accepts both the
name
andage
.A constructor that only accepts the
name
and assigns a default value to theage
.
this() Keyword in Constructors
In Java, the this()
keyword can be used to call one constructor from another within the same class. This is useful when you want to reuse constructor logic.
Example:
class Person {
String name;
int age;
// Constructor with two parameters
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Constructor with one parameter
public Person(String name) {
this(name, 18); // Calling another constructor using this()
}
public void displayInfo() {
System.out.println("Name: " + name + ", Age: " + age);
}
}
public class Main {
public static void main(String[] args) {
Person p1 = new Person("Alice", 30);
p1.displayInfo(); // Output: Name: Alice, Age: 30
Person p2 = new Person("Bob");
p2.displayInfo(); // Output: Name: Bob, Age: 18
}
}
In this example, the constructor with one parameter calls the constructor with two parameters using the this()
keyword, reusing the logic for initializing the name
and age
.
Super Constructor
If a class is a subclass (i.e., inherits from another class), the constructor of the superclass can be called using the super()
keyword. This must be the first statement in the subclass's constructor.
Example:
class Animal {
String name;
// Constructor of superclass
public Animal(String name) {
this.name = name;
}
}
class Dog extends Animal {
String breed;
// Constructor of subclass
public Dog(String name, String breed) {
super(name); // Calling the constructor of superclass
this.breed = breed;
}
public void displayInfo() {
System.out.println("Name: " + name + ", Breed: " + breed);
}
}
public class Main {
public static void main(String[] args) {
Dog d = new Dog("Buddy", "Golden Retriever");
d.displayInfo(); // Output: Name: Buddy, Breed: Golden Retriever
}
}
In this example, the Dog
class inherits from the Animal
class. The constructor of Dog
calls the constructor of Animal
using super()
to initialize the name
.
Rules for Constructors
Constructors cannot be abstract, final, static, or synchronized.
Constructors can throw exceptions if necessary.
If a class has no constructors defined, the Java compiler automatically provides a default constructor.
If you define any constructor, Java will not provide a default constructor.
Conclusion
Constructors in Java are essential for object initialization and provide a way to set initial values for object attributes. With features like constructor overloading, use of this()
and super()
, and the ability to customize constructors with parameters, constructors play a crucial role in making Java programs more robust and flexible.
Interface and Abstract class
In Java, interfaces and abstract classes are two fundamental mechanisms for achieving abstraction and defining the structure of classes. Both serve different purposes and are used in different scenarios based on the design requirements of a program.
1. Interfaces
An interface in Java is a reference type, similar to a class, that can contain only constants, method signatures, default methods, static methods, and nested types. Interfaces are used to specify a contract that other classes can implement. They provide a way to achieve abstraction and multiple inheritance.
Key Features of Interfaces:
Method Signatures Only: Interfaces can declare methods, but they cannot provide method implementations (except for default and static methods).
Multiple Inheritance: A class can implement multiple interfaces, allowing for multiple inheritance.
Default Methods: Interfaces can provide default implementations for methods using the
default
keyword.Static Methods: Interfaces can contain static methods, which must be called using the interface name.
Constants: Interfaces can contain constants, which are
public
,static
, andfinal
by default.
Syntax and Example:
Declaring an Interface:
interface Animal {
// Abstract method (does not have a body)
void makeSound();
// Default method
default void sleep() {
System.out.println("Zzz...");
}
// Static method
static void info() {
System.out.println("Animals are living organisms.");
}
}
Implementing an Interface:
class Dog implements Animal {
// Providing implementation for the abstract method
@Override
public void makeSound() {
System.out.println("Woof");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.makeSound(); // Output: Woof
dog.sleep(); // Output: Zzz...
// Calling static method of interface
Animal.info(); // Output: Animals are living organisms.
}
}
2. Abstract Classes
An abstract class in Java is a class that cannot be instantiated on its own and is meant to be subclassed. It can have abstract methods (methods without implementations) as well as concrete methods (methods with implementations). Abstract classes are used to provide a common base for subclasses and to define methods that must be implemented by the subclasses.
Key Features of Abstract Classes:
Abstract Methods: Abstract classes can have abstract methods that must be implemented by subclasses.
Concrete Methods: Abstract classes can also have concrete methods with implementations.
Fields: Abstract classes can have fields (instance variables) and constructors.
Cannot Be Instantiated: You cannot create instances of an abstract class directly.
Single Inheritance: A class can only inherit from one abstract class, enforcing single inheritance.
Syntax and Example:
Declaring an Abstract Class:
abstract class Animal {
// Abstract method (does not have a body)
abstract void makeSound();
// Concrete method
void eat() {
System.out.println("This animal eats food.");
}
}
Extending an Abstract Class:
class Dog extends Animal {
// Providing implementation for the abstract method
@Override
void makeSound() {
System.out.println("Woof");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.makeSound(); // Output: Woof
dog.eat(); // Output: This animal eats food.
}
}
Comparison Between Interfaces and Abstract Classes
1. Purpose and Use Cases:
Interfaces are used to define a contract that other classes must follow. They are ideal for defining capabilities that can be shared across multiple classes, regardless of their place in the class hierarchy.
Abstract Classes are used to provide a common base for classes and to share code among related classes. They are ideal when you want to create a common base with shared functionality and common state.
2. Method Implementation:
Interfaces can have default methods with implementations. Methods in interfaces are abstract by default unless explicitly specified as default or static.
Abstract Classes can have both abstract methods and concrete methods. Abstract methods must be implemented by subclasses, while concrete methods can be used directly or overridden.
3. Inheritance:
Interfaces support multiple inheritance, meaning a class can implement multiple interfaces.
Abstract Classes support single inheritance, meaning a class can extend only one abstract class.
4. Fields and Constructors:
Interfaces cannot have instance fields or constructors.
Abstract Classes can have instance fields and constructors.
5. Access Modifiers:
Interfaces: Methods in interfaces are
public
by default.Abstract Classes: Methods in abstract classes can have any access modifier (e.g.,
public
,protected
,private
).
Conclusion
Both interfaces and abstract classes are important tools for designing robust and flexible Java applications. Interfaces are ideal for defining a contract that multiple classes can follow, while abstract classes are useful for providing a common base with shared functionality. Understanding when and how to use each can help you create more organized and maintainable code.
Sealed Class and Interface
In Java, sealed classes and sealed interfaces are part of the Java programming language's support for a more controlled and expressive form of inheritance. Introduced in Java 15 as a preview feature and made a standard feature in Java 17, sealed classes and interfaces allow developers to restrict which classes or interfaces can extend or implement them. This feature enhances the ability to model domain-specific hierarchies and enforce constraints on type hierarchies.
1. Sealed Classes
A sealed class is a class that restricts which classes can extend it. Sealed classes are used to provide more control over inheritance, ensuring that only a specific set of subclasses can extend a class.
Key Features of Sealed Classes:
Restrict Subclassing: Only the classes specified in the
permits
clause can extend a sealed class.Inheritable Subclasses: The permitted subclasses can be either final classes, non-sealed classes, or other sealed classes.
Controlled Inheritance: Helps in creating a more predictable and controlled class hierarchy.
Syntax and Example:
Declaring a Sealed Class:
// Sealed class
public sealed class Shape permits Circle, Rectangle {
// Common properties and methods for all shapes
}
// Permitted subclass: Circle
public final class Circle extends Shape {
// Implementation for Circle
}
// Permitted subclass: Rectangle
public final class Rectangle extends Shape {
// Implementation for Rectangle
}
In this example:
Shape
is a sealed class that can only be extended byCircle
andRectangle
.Circle
andRectangle
are bothfinal
, meaning they cannot be further subclassed.
Trying to Extend a Sealed Class with an Unauthorized Class:
// This will result in a compilation error
public class Triangle extends Shape {
// Compilation error: 'Triangle' is not permitted to extend 'Shape'
}
2. Sealed Interfaces
A sealed interface works similarly to a sealed class, restricting which interfaces or classes can implement or extend it. This allows for controlling the implementation hierarchy of the interface.
Key Features of Sealed Interfaces:
Restrict Implementations: Only the classes or interfaces specified in the
permits
clause can implement or extend a sealed interface.Controlled Implementations: Helps in creating a controlled set of implementations or extending interfaces in a predictable manner.
Syntax and Example:
Declaring a Sealed Interface:
// Sealed interface
public sealed interface Animal permits Dog, Cat {
void makeSound();
}
// Permitted implementation: Dog
public final class Dog implements Animal {
@Override
public void makeSound() {
System.out.println("Woof");
}
}
// Permitted implementation: Cat
public final class Cat implements Animal {
@Override
public void makeSound() {
System.out.println("Meow");
}
}
In this example:
Animal
is a sealed interface that can only be implemented byDog
andCat
.Dog
andCat
arefinal
classes, meaning they cannot be further subclassed.
Trying to Implement a Sealed Interface with an Unauthorized Class:
// This will result in a compilation error
public class Bird implements Animal {
@Override
public void makeSound() {
System.out.println("Chirp");
}
}
Comparison: Sealed Classes vs. Sealed Interfaces
1. Purpose:
Sealed Classes: Used to control which classes can extend a class, ensuring a restricted and well-defined inheritance hierarchy.
Sealed Interfaces: Used to control which classes or interfaces can implement an interface, ensuring a restricted set of implementations.
2. Inheritance and Implementation:
Sealed Classes: Subclasses can be either final, non-sealed, or other sealed classes.
Sealed Interfaces: Implementations can be either final or other sealed interfaces.
3. Usage:
Sealed Classes: Useful when you want to control subclassing of a class and prevent external classes from extending it.
Sealed Interfaces: Useful when you want to restrict which classes or interfaces can implement or extend the interface.
Date Time API
The Date-Time API in Java, introduced in Java 8 as part of the java.time
package, provides a comprehensive framework for handling date and time operations. It improves upon the older java.util.Date
and java.util.Calendar
classes by offering a more robust, immutable, and thread-safe approach to date and time manipulation.
Core Components of the Date-Time API
1. LocalDate
Represents a date without time-zone (e.g.,
2024-09-05
).Useful for representing dates such as birthdays, anniversaries, or any event that does not require time.
Example:
import java.time.LocalDate;
public class Main {
public static void main(String[] args) {
LocalDate today = LocalDate.now();
LocalDate birthDate = LocalDate.of(1990, 9, 5);
System.out.println("Today's Date: " + today);
System.out.println("Birth Date: " + birthDate);
}
}
2. LocalTime
Represents a time without date or time-zone (e.g.,
14:30:00
).Useful for representing time such as meeting times or schedules.
Example:
import java.time.LocalTime;
public class Main {
public static void main(String[] args) {
LocalTime now = LocalTime.now();
LocalTime meetingTime = LocalTime.of(9, 30);
System.out.println("Current Time: " + now);
System.out.println("Meeting Time: " + meetingTime);
}
}
3. LocalDateTime
Combines
LocalDate
andLocalTime
to represent date and time without time-zone (e.g.,2024-09-05T14:30:00
).Useful for representing date and time together.
Example:
import java.time.LocalDateTime;
public class Main {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime event = LocalDateTime.of(2024, 9, 5, 14, 30);
System.out.println("Current DateTime: " + now);
System.out.println("Event DateTime: " + event);
}
}
4. ZonedDateTime
Represents date and time with time-zone information (e.g.,
2024-09-05T14:30:00+02:00[Europe/Paris]
).Useful for handling dates and times across different time zones.
Example:
import java.time.ZonedDateTime;
import java.time.ZoneId;
public class Main {
public static void main(String[] args) {
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime meeting = ZonedDateTime.of(2024, 9, 5, 14, 30, 0, 0, ZoneId.of("Europe/Paris"));
System.out.println("Current ZonedDateTime: " + now);
System.out.println("Meeting ZonedDateTime: " + meeting);
}
}
5. Instant
Represents a point in time with nanosecond precision (e.g.,
2024-09-05T12:30:00Z
).Useful for timestamps or recording events.
Example:
import java.time.Instant;
public class Main {
public static void main(String[] args) {
Instant now = Instant.now();
System.out.println("Current Instant: " + now);
}
}
6. Duration
Represents the amount of time between two
Instant
objects or between twoLocalTime
objects (e.g.,P1DT1H
for a period of one day and one hour).Useful for measuring elapsed time or time intervals.
Example:
import java.time.Duration;
import java.time.LocalTime;
public class Main {
public static void main(String[] args) {
LocalTime start = LocalTime.of(9, 0);
LocalTime end = LocalTime.of(17, 0);
Duration duration = Duration.between(start, end);
System.out.println("Duration: " + duration.toHours() + " hours");
}
}
7. Period
Represents a period of time in terms of years, months, and days (e.g.,
P1Y2M3D
for a period of one year, two months, and three days).Useful for date-based calculations like age or contract periods.
Example:
import java.time.LocalDate;
import java.time.Period;
public class Main {
public static void main(String[] args) {
LocalDate startDate = LocalDate.of(2024, 1, 1);
LocalDate endDate = LocalDate.of(2024, 9, 5);
Period period = Period.between(startDate, endDate);
System.out.println("Period: " + period.getMonths() + " months, " + period.getDays() + " days");
}
}
Formatting and Parsing
1. Formatting Dates and Times
- The
DateTimeFormatter
class is used for formatting date and time objects into strings.
Example:
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class Main {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDateTime = now.format(formatter);
System.out.println("Formatted DateTime: " + formattedDateTime);
}
}
2. Parsing Dates and Times
- The
DateTimeFormatter
class is also used for parsing strings into date and time objects.
Example:
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class Main {
public static void main(String[] args) {
String dateTimeString = "2024-09-05 14:30:00";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime parsedDateTime = LocalDateTime.parse(dateTimeString, formatter);
System.out.println("Parsed DateTime: " + parsedDateTime);
}
}
Benefits of the Date-Time API
Immutability: All classes in the
java.time
package are immutable, which makes them thread-safe and predictable.Clarity: The API provides a clear and concise way to handle date and time operations.
Flexibility: It supports a wide range of date and time operations, including time-zone calculations, formatting, and parsing.
No Legacy Issues: Unlike
java.util.Date
andjava.util.Calendar
, the Date-Time API does not suffer from many of the design flaws of the old classes.
Exception Handling
Exception handling in Java is a mechanism for managing runtime errors, allowing a program to continue its execution even when unexpected events occur. This is crucial for developing robust and user-friendly applications. Java provides a structured way to handle exceptions using the try
, catch
, finally
, throw
, and throws
keywords.
Key Concepts
1. Exception
An exception is an event that disrupts the normal flow of a program's execution. It represents an error or unusual condition that a program encounters, such as a division by zero or a file not found.
2. Error vs. Exception
Error: Represents serious issues that a program cannot reasonably handle, such as
OutOfMemoryError
orStackOverflowError
.Exception: Represents conditions that a program can handle, such as
FileNotFoundException
orArithmeticException
.
Exception Handling Keywords
1. try
The try
block is used to enclose code that might throw an exception. It is followed by one or more catch
blocks or a finally
block.
Syntax:
try {
// Code that may throw an exception
}
2. catch
The catch
block is used to handle exceptions thrown by the try
block. You can have multiple catch
blocks to handle different types of exceptions.
Syntax:
catch (ExceptionType1 e1) {
// Handle ExceptionType1
} catch (ExceptionType2 e2) {
// Handle ExceptionType2
}
Example:
try {
int result = 10 / 0; // This will throw ArithmeticException
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero!");
}
3. finally
The finally
block contains code that will always execute, regardless of whether an exception was thrown or not. It's typically used for cleanup code such as closing files or releasing resources.
Syntax:
finally {
// Cleanup code, executed whether an exception is thrown or not
}
Example:
try {
int result = 10 / 2;
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero!");
} finally {
System.out.println("This block always executes.");
}
4. throw
The throw
keyword is used to explicitly throw an exception from a method or block of code. This can be used to signal that an error condition has occurred.
Syntax:
throw new ExceptionType("Error message");
Example:
public void checkAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative.");
}
}
5. throws
The throws
keyword is used in a method signature to indicate that the method may throw exceptions that need to be handled by the caller. It specifies which exceptions can be thrown by the method.
Syntax:
public void method() throws ExceptionType1, ExceptionType2 {
// Code that may throw exceptions
}
Example:
public void readFile(String file) throws IOException {
FileReader fr = new FileReader(file);
// Code that may throw IOException
}
Custom Exceptions
You can create your own exception classes by extending the Exception
class (or any of its subclasses). This allows you to define exceptions specific to your application's needs.
Example:
// Custom Exception class
public class MyCustomException extends Exception {
public MyCustomException(String message) {
super(message);
}
}
// Usage
public void doSomething() throws MyCustomException {
boolean condition = true; // Just an example condition
if (condition) {
throw new MyCustomException("Something went wrong.");
}
}
Exception Hierarchy
Java's exception hierarchy is rooted in the Throwable
class:
-
Throwable: The base class for all errors and exceptions.
- Error: Represents serious issues that applications should not catch.
-
Exception: Represents conditions that applications might catch.
-
RuntimeException: Represents exceptions that can occur during the runtime of the program (e.g.,
NullPointerException
,ArrayIndexOutOfBoundsException
). -
Checked Exceptions: Represent conditions that a method must handle or declare in its
throws
clause (e.g.,IOException
,SQLException
).
-
RuntimeException: Represents exceptions that can occur during the runtime of the program (e.g.,
Best Practices
Handle Specific Exceptions: Catch specific exceptions rather than generic ones to handle errors more precisely.
Avoid Empty Catch Blocks: Do not use empty catch blocks; at least log the error or handle it appropriately.
Use Finally for Cleanup: Always use the
finally
block to close resources like files, streams, or database connections.Create Custom Exceptions: Use custom exceptions to provide more meaningful error messages and handle application-specific errors.
Document Exceptions: Use JavaDoc to document exceptions that your methods can throw to inform users of your API.
Collections in Java
The Java Collections Framework (JCF) is a unified architecture for representing and manipulating collections of objects in Java. It provides a set of interfaces, implementations, and algorithms that allow for efficient storage, retrieval, and manipulation of data. Here's an overview of the key components and concepts of the Java Collections Framework:
Core Interfaces
- Collection Interface:
* The root interface in the collection hierarchy.
* Common operations include adding, removing, and checking for elements.
```java
public interface Collection<E> {
boolean add(E e);
boolean remove(Object o);
boolean contains(Object o);
int size();
boolean isEmpty();
// other methods
}
```
- List Interface:
* Extends `Collection` and represents an ordered collection (sequence) that allows duplicate elements.
* Implementations: `ArrayList`, `LinkedList`, `Vector`.
```java
public interface List<E> extends Collection<E> {
void add(int index, E element);
E get(int index);
E remove(int index);
int indexOf(Object o);
// other methods
}
```
- Set Interface:
* Extends `Collection` and represents a collection that does not allow duplicate elements.
* Implementations: `HashSet`, `LinkedHashSet`, `TreeSet`.
```java
public interface Set<E> extends Collection<E> {
// no additional methods
}
```
- Queue Interface:
* Extends `Collection` and represents a collection designed for holding elements prior to processing.
* Implementations: `LinkedList`, `PriorityQueue`.
```java
public interface Queue<E> extends Collection<E> {
boolean offer(E e);
E poll();
E peek();
// other methods
}
```
- Deque Interface:
* Extends `Queue` and represents a double-ended queue that allows elements to be added or removed from both ends.
* Implementations: `ArrayDeque`, `LinkedList`.
```java
public interface Deque<E> extends Queue<E> {
void addFirst(E e);
void addLast(E e);
E removeFirst();
E removeLast();
// other methods
}
```
- Map Interface:
* Represents a collection of key-value pairs, where each key maps to exactly one value.
* Implementations: `HashMap`, `LinkedHashMap`, `TreeMap`.
```java
public interface Map<K, V> {
V put(K key, V value);
V get(Object key);
V remove(Object key);
boolean containsKey(Object key);
int size();
// other methods
}
```
Key Implementations
- ArrayList:
* Implements `List` and provides a resizable array implementation.
* Allows fast random access and efficient addition/removal from the end.
```java
ArrayList<String> list = new ArrayList<>();
list.add("Harry");
list.add("Hermione");
```
- LinkedList:
* Implements both `List` and `Deque` interfaces.
* Provides a doubly-linked list implementation, allowing efficient insertions/removals at both ends.
```java
LinkedList<String> list = new LinkedList<>();
list.add("Harry");
list.addFirst("Hermione");
```
- HashSet:
* Implements `Set` and uses a hash table for storage.
* Does not guarantee any specific order of elements.
```java
HashSet<String> set = new HashSet<>();
set.add("Harry");
set.add("Hermione");
```
- TreeSet:
* Implements `Set` and uses a red-black tree for storage.
* Elements are sorted according to their natural ordering or a provided comparator.
```java
TreeSet<String> set = new TreeSet<>();
set.add("Harry");
set.add("Hermione");
```
- HashMap:
* Implements `Map` and uses a hash table for storage.
* Provides constant-time performance for basic operations like `get` and `put`.
```java
HashMap<String, String> map = new HashMap<>();
map.put("Harry", "Gryffindor");
map.put("Hermione", "Gryffindor");
```
- TreeMap:
* Implements `Map` and uses a red-black tree for storage.
* Keys are sorted according to their natural ordering or a provided comparator.
```java
TreeMap<String, String> map = new TreeMap<>();
map.put("Harry", "Gryffindor");
map.put("Hermione", "Gryffindor");
```
Algorithms
The Collections
class provides static methods that operate on or return collections. These methods include:
Sorting:
Collections.sort(list)
Shuffling:
Collections.shuffle(list)
Searching:
Collections.binarySearch(list, key)
Reverse:
Collections.reverse(list)
import java.util.ArrayList;
import java.util.Collections;
public class CollectionsExample {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Harry");
list.add("Hermione");
list.add("Ron");
// Sorting the list
Collections.sort(list);
System.out.println("Sorted List: " + list);
// Shuffling the list
Collections.shuffle(list);
System.out.println("Shuffled List: " + list);
// Reversing the list
Collections.reverse(list);
System.out.println("Reversed List: " + list);
}
}
Memory management and Garbage Collection
Memory management in Java is an automatic process managed by the Java Virtual Machine (JVM). Java uses a combination of stack memory and heap memory to manage application data. One of the key features of Java is its automatic garbage collection mechanism, which helps developers by automatically reclaiming memory that is no longer in use.
Memory Management in Java
1. Stack Memory:
Purpose: Used for storing method calls, local variables, and references to objects in the heap.
-
Characteristics:
- Stack memory is organized in Last-In-First-Out (LIFO) order.
- Each time a method is called, a new block is added to the stack (called a stack frame), which holds local variables and references.
- When the method completes, the corresponding stack frame is removed.
- Stack memory is thread-specific, meaning each thread has its own stack.
2. Heap Memory:
Purpose: Used for dynamically allocated objects and variables.
-
Characteristics:
- All objects, arrays, and class instances are created in the heap memory.
- Heap memory is shared across all threads.
- The garbage collector reclaims memory from the heap when objects are no longer reachable.
3. Method Area:
Purpose: Stores metadata for classes, including static variables, method bytecode, and class-level data.
-
Characteristics:
- Part of the heap where the runtime constant pool, field, and method data are stored.
- Shared among all threads.
4. Program Counter (PC) Register:
Purpose: Holds the address of the current instruction being executed by the thread.
-
Characteristics:
- Each thread has its own PC register.
5. Native Method Stack:
Purpose: Holds information related to native method calls (non-Java methods).
-
Characteristics:
- Supports native libraries and functions invoked from Java (e.g., JNI - Java Native Interface).
Garbage Collection in Java
Garbage collection in Java is the process by which the JVM automatically reclaims memory by deleting objects that are no longer accessible in the program. Java uses a garbage collector, which runs in the background to manage this process.
Key Concepts in Garbage Collection
- Reachability:
* Objects are eligible for garbage collection if they are unreachable from any live thread or static references.
* An object is considered reachable if:
* It can be accessed directly by a live thread.
* It can be reached from any other reachable object.
- Phases of Garbage Collection:
* **Mark Phase**: The garbage collector identifies which objects are still reachable.
* **Sweep Phase**: It then deletes objects that are no longer reachable.
* **Compaction**: The heap memory is compacted to reduce fragmentation and consolidate free space.
- Generational Garbage Collection:
* The heap is divided into two main regions: **Young Generation** and **Old Generation**.
* **Young Generation**: Newly created objects are initially allocated in this area. It is further divided into:
* **Eden Space**: Where objects are first created.
* **Survivor Spaces**: After surviving one or more garbage collection cycles, objects are moved here.
* **Old Generation**: Objects that have survived several rounds of garbage collection are promoted to this space. It is collected less frequently than the young generation.
- Types of Garbage Collectors:
* **Serial Garbage Collector**: A single-threaded garbage collector, suitable for small applications with few threads.
* **Parallel Garbage Collector**: Uses multiple threads to perform garbage collection in parallel. Suitable for multi-threaded applications.
* **Concurrent Mark-Sweep (CMS) Collector**: Designed to minimize pause times by performing most of the garbage collection work concurrently with the application.
* **G1 Garbage Collector**: Designed for applications that require predictable pause times and is suitable for large heaps.
* **ZGC and Shenandoah**: Ultra-low pause-time garbage collectors designed for very large heap sizes.
- Stop-the-World Event:
* A pause where all application threads are stopped to allow the garbage collector to run. Some garbage collectors minimize or eliminate these pauses.
- Finalization:
* An object may define a `finalize()` method, which the garbage collector calls before reclaiming the object’s memory. This method is used to perform cleanup operations, but it is generally discouraged because of its unpredictability and performance overhead.
Manual Memory Management vs. Garbage Collection
In languages like C/C++, memory management is manual; developers need to allocate and deallocate memory explicitly using constructs like malloc()
, free()
, new
, and delete
.
In Java, memory management is automatic. You create objects and forget about them, and when they are no longer needed, the garbage collector handles the clean-up. This eliminates the risk of common issues such as memory leaks and dangling pointers but also introduces overhead due to garbage collection cycles.
Tuning Garbage Collection
You can configure and tune the JVM's garbage collector using various JVM flags, such as:
-Xms
: Sets the initial heap size.-Xmx
: Sets the maximum heap size.-XX:+UseG1GC
: Enables the G1 garbage collector.-XX:MaxGCPauseMillis=n
: Sets a target maximum pause time for garbage collection in milliseconds.
Tuning these parameters can optimize performance, depending on your application’s needs.
Best Practices for Memory Management
Avoid Memory Leaks: Ensure that unused object references are set to
null
so they can be garbage collected.Use Weak References: Use
WeakReference
orSoftReference
when necessary to allow the garbage collector to reclaim memory for objects that are referenced but not critical.Minimize Object Creation: Reuse objects where possible to reduce the overhead of garbage collection.
Optimize Data Structures: Choose appropriate data structures with minimal memory overhead, such as using
ArrayList
instead ofLinkedList
when random access is more common than insertions and deletions.
Conclusion
Memory management and garbage collection in Java ensure efficient memory usage by automatically reclaiming memory from unused objects. By understanding how memory is allocated and garbage collected, you can optimize your applications and avoid memory-related issues such as leaks and performance bottlenecks. The garbage collector offers various algorithms to suit different types of applications, and tuning them can help achieve optimal performance.
Annotations
Annotations in Java are a form of metadata that provide data about a program but are not part of the program itself. They can be used to give information to the compiler, generate code, or even influence runtime behavior. Annotations can be applied to different elements of code such as classes, methods, fields, parameters, and packages.
Key Characteristics of Annotations:
Metadata: Annotations do not affect program logic; instead, they provide additional information that can be used by the compiler, tools, or frameworks.
Processing: Annotations can be processed at compile-time, class-load time, or runtime depending on their retention policy.
Built-in Annotations in Java
Java provides a set of built-in annotations which are frequently used:
1. @Override
:
Indicates that a method is intended to override a method in a superclass.
class Parent {
public void display() {
System.out.println("Parent Display");
}
}
class Child extends Parent {
@Override
public void display() { // This ensures the method overrides the superclass method
System.out.println("Child Display");
}
}
2. @Deprecated
:
Marks a method or class as deprecated, indicating that it should no longer be used, and might be removed in future versions.
class Example {
@Deprecated
public void oldMethod() {
System.out.println("This method is deprecated");
}
}
3. @SuppressWarnings
:
Suppresses compiler warnings for specific issues, such as unchecked operations.
class Example {
@SuppressWarnings("unchecked")
public void unsafeOperation() {
List list = new ArrayList(); // This generates an unchecked warning
list.add("unchecked operation");
}
}
4. @SafeVarargs
:
Indicates that a method with a variable number of arguments does not perform unsafe operations on its varargs parameter.
public class Example {
@SafeVarargs
private final <T> void display(T... elements) {
for (T element : elements) {
System.out.println(element);
}
}
}
5. @FunctionalInterface
:
Indicates that an interface is intended to be a functional interface, which means it has exactly one abstract method.
@FunctionalInterface
interface MyFunctionalInterface {
void performAction();
}
Meta-Annotations
Java also provides meta-annotations, which are annotations that apply to other annotations. These help define how the annotations are processed.
1. @Retention
:
Specifies how long the annotation should be retained. The options are:
SOURCE
: Discarded by the compiler and not included in the bytecode.CLASS
: Kept in the bytecode but not available at runtime.RUNTIME
: Available at runtime via reflection.
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation {
String value();
}
2. @Target
:
Specifies the kinds of program elements to which an annotation type is applicable (e.g., methods, fields, classes).
@Target(ElementType.METHOD)
@interface MyMethodAnnotation {
String description();
}
3. @Documented
:
Indicates that the annotation should be included in the generated Javadoc.
@Documented
@interface MyDocumentedAnnotation {
String description();
}
4. @Inherited
:
Indicates that the annotation type can be inherited from a superclass.
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface MyInheritedAnnotation {
String value();
}
@MyInheritedAnnotation("Inherited Annotation")
class Parent {}
class Child extends Parent {}
Custom Annotations
You can define your own annotations by using the @interface
keyword. Custom annotations often include elements that can be accessed when the annotation is applied.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface MyCustomAnnotation {
String description();
int priority() default 1; // Default value
}
class Example {
@MyCustomAnnotation(description = "This is a custom method", priority = 2)
public void myMethod() {
System.out.println("Custom Annotation Method");
}
}
Processing Annotations at Runtime
Annotations with RetentionPolicy.RUNTIME
can be accessed at runtime using reflection. This is useful in frameworks like Spring and Hibernate, which make extensive use of annotations.
import java.lang.annotation.*;
import java.lang.reflect.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface MyAnnotation {
String description();
}
class Example {
@MyAnnotation(description = "This is a sample method")
public void sampleMethod() {
System.out.println("Method executed");
}
}
public class AnnotationProcessor {
public static void main(String[] args) throws Exception {
Example obj = new Example();
Method method = obj.getClass().getMethod("sampleMethod");
if (method.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
System.out.println("Annotation Description: " + annotation.description());
}
// Invoke the method
method.invoke(obj);
}
}
Use Cases of Annotations
- Frameworks and Dependency Injection:
* **Spring Framework**: Annotations like `@Component`, `@Autowired`, and `@Service` are used to define beans and inject dependencies.
* **JPA/Hibernate**: Annotations like `@Entity`, `@Id`, and `@Column` are used to map Java objects to database tables.
- Testing Frameworks:
* **JUnit**: Annotations like `@Test`, `@Before`, `@After`, and `@RunWith` are used to define and manage test cases.
- Serialization:
* **Jackson**: Annotations like `@JsonProperty`, `@JsonIgnore`, and `@JsonSerialize` are used to customize the serialization/deserialization process.
- Custom Validation:
* Used to enforce rules and constraints on method parameters, fields, or classes.
Conclusion
Annotations are a powerful tool in Java, offering a way to add metadata to code without cluttering the logic. They are widely used in modern Java frameworks and libraries for a variety of purposes like dependency injection, data validation, serialization, and testing. Understanding how annotations work and how to create custom annotations can greatly enhance your ability to write clean, declarative, and maintainable code.
JDBC API
The Java Database Connectivity (JDBC) API is a standard Java API that enables Java applications to interact with relational databases. It provides a set of classes and interfaces that allow developers to connect to a database, execute SQL queries, retrieve results, and manage transactions.
Key Features of JDBC API
Database Connectivity: JDBC allows Java applications to connect to different databases (such as MySQL, Oracle, SQL Server, PostgreSQL, etc.) using the appropriate drivers.
SQL Execution: JDBC supports executing SQL queries, updating records, and running stored procedures.
Transaction Management: JDBC provides support for managing database transactions.
Result Processing: JDBC helps in fetching and processing query results in a Java-friendly manner.
Architecture of JDBC
The JDBC API consists of two main layers:
JDBC API Layer: Provides the application-to-JDBC calls interface.
JDBC Driver Layer: Handles the communication between the Java application and the database. JDBC drivers convert the Java API calls into database-specific calls.
JDBC Components
The JDBC API includes the following core interfaces:
DriverManager: Manages a list of database drivers. It matches connection requests from the application with the appropriate driver.
Connection: Represents a connection to the database and is used to manage the database session.
Statement: Used to execute static SQL queries.
PreparedStatement: Used to execute parameterized SQL queries (more efficient than
Statement
).CallableStatement: Used to execute stored procedures.
ResultSet: Represents the result set obtained from executing a SQL query.
Steps to Use JDBC
To interact with a database using JDBC, you generally follow these steps:
1. Load the JDBC Driver
You need to load the database-specific driver class. This can be done using Class.forName()
. Most modern databases load the driver automatically when the connection is established.
Class.forName("com.mysql.cj.jdbc.Driver");
2. Establish a Connection
Use the DriverManager.getConnection()
method to establish a connection to the database. The connection requires a database URL, username, and password.
Connection connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mydatabase", "username", "password");
3. Create a Statement
Once connected, create a Statement
or PreparedStatement
object for executing SQL queries.
Statement statement = connection.createStatement();
Or, for a parameterized query:
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM students WHERE id = ?");
4. Execute SQL Queries
Use the executeQuery()
or executeUpdate()
methods to execute SQL statements. executeQuery()
is used for SELECT queries, while executeUpdate()
is used for INSERT, UPDATE, DELETE, and DDL statements.
ResultSet resultSet = statement.executeQuery("SELECT * FROM students");
For an update:
int rowsAffected = statement.executeUpdate("UPDATE students SET name = 'Harry' WHERE id = 1");
5. Process the Result
For SELECT queries, the result is stored in a ResultSet
. You can iterate through the rows of the ResultSet
using a while
loop.
while (resultSet.next()) {
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
System.out.println("ID: " + id + ", Name: " + name);
}
6. Close the Resources
Once the operations are complete, close the ResultSet
, Statement
, and Connection
objects to free up resources.
resultSet.close();
statement.close();
connection.close();
JDBC Example
import java.sql.*;
public class JdbcExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/mydatabase";
String user = "root";
String password = "password";
try {
// Load the JDBC driver
Class.forName("com.mysql.cj.jdbc.Driver");
// Establish a connection
Connection connection = DriverManager.getConnection(url, user, password);
// Create a statement
Statement statement = connection.createStatement();
// Execute a query
ResultSet resultSet = statement.executeQuery("SELECT * FROM students");
// Process the result
while (resultSet.next()) {
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
System.out.println("ID: " + id + ", Name: " + name);
}
// Close the resources
resultSet.close();
statement.close();
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
JDBC API Interfaces and Classes
Here are the primary interfaces and classes used in JDBC:
DriverManager: Manages a list of database drivers and establishes the connection between the database and the appropriate driver.
Connection: Represents a session with a specific database. You can use this interface to create
Statement
,PreparedStatement
, andCallableStatement
objects.Statement: Used to execute static SQL queries without parameters.
PreparedStatement: Used to execute dynamic SQL queries with parameters, preventing SQL injection.
CallableStatement: Used to call stored procedures.
ResultSet: Represents the result set of a query.
SQLException: Handles database-related errors and exceptions.
Types of JDBC Drivers
Type 1 (JDBC-ODBC Bridge Driver): Uses ODBC drivers to communicate with databases. This is now deprecated and not recommended for use.
Type 2 (Native-API Driver): Converts JDBC calls into database-specific API calls. Requires database-specific libraries.
Type 3 (Network Protocol Driver): Converts JDBC calls into a database-independent protocol, which is then translated into database-specific protocols by a server.
Type 4 (Thin Driver): Pure Java driver that converts JDBC calls directly into the database-specific protocol. Most commonly used because it does not require additional software.
Transaction Management in JDBC
By default, each SQL statement is committed to the database automatically. You can, however, manage transactions manually by disabling auto-commit mode and using commit()
and rollback()
.
connection.setAutoCommit(false);
try {
statement.executeUpdate("UPDATE students SET name = 'Ron' WHERE id = 2");
statement.executeUpdate("UPDATE students SET name = 'Hermione' WHERE id = 3");
connection.commit(); // Commit the transaction
} catch (SQLException e) {
connection.rollback(); // Roll back the transaction if something goes wrong
}
Conclusion
JDBC is a powerful and flexible API for working with relational databases in Java. It provides an abstraction over SQL and the underlying database, allowing Java applications to interact with various databases in a standard way. JDBC supports different types of queries, transactions, and error handling, making it a key component for database-driven applications.
Java9 Modularity
Java 9 introduced the Java Platform Module System (JPMS), commonly referred to as Project Jigsaw or Java Modularity. The main goal of Java modularity was to improve the scalability, maintainability, and performance of Java applications by organizing code into explicit modules. This addressed issues with the "monolithic" nature of earlier Java versions, where the JDK had grown increasingly large and complex over time.
Key Concepts of Java 9 Modularity
Modules: A module is a self-contained unit of code that contains packages, types (classes, interfaces, etc.), and resources. It explicitly defines what it exports to other modules and what it requires from other modules.
-
module-info.java
File: Every module contains amodule-info.java
file in its root directory, which defines the module's metadata. This file specifies the module name, what it exports (makes available to other modules), and what it requires (dependencies on other modules).Example of a
module-info.java
file:
module com.example.myapp { requires java.sql; exports com.example.myapp.api; }
* `requires java.sql;`: This module depends on the `java.sql` module.
* `exports com.example.myapp.api;`: This module exports the `com.example.myapp.api` package for other modules to use.
requires
Keyword: Specifies the modules that the current module depends on (its dependencies).exports
Keyword: Specifies which packages are made available to other modules.Encapsulation: Only the exported packages are accessible to other modules. The internal packages of a module are hidden and inaccessible to other modules unless explicitly exported.
Strong Encapsulation: It enforces better modularity by preventing internal implementation details from being accessed outside of the module.
Advantages of Modularity in Java 9
Better Organization: It allows developers to organize code into smaller, more manageable units (modules), which makes large projects easier to maintain.
Performance Improvements: The Java runtime can load only the necessary modules, which can reduce memory usage and startup time.
Improved Security: By encapsulating internal code, it becomes harder for external code to access internal classes and methods, reducing the risk of unintended security vulnerabilities.
Faster Development and Testing: Smaller modules can be developed, tested, and deployed independently of the entire system.
Reduced Complexity: By breaking the system into modules with explicit dependencies, developers can better manage code complexity.
Example of a Modular Application
Consider a basic Java application where you have three modules:
com.example.app
(Application Module)com.example.util
(Utility Module)com.example.service
(Service Module)
Here’s how the module-info.java
files might look for each module:
-
module com.example.app { requires com.example.util; requires com.example.service; }
-
com.example.util:
module com.example.util { exports com.example.util; }
-
com.example.service:
module com.example.service { requires com.example.util; exports com.example.service; }
How Java 9 Modularity Affects the JDK
One of the biggest changes in Java 9 was the modularization of the JDK itself. Prior to Java 9, the JDK was a monolithic platform, meaning all its components (like java.base
, java.xml
, java.sql
, etc.) were bundled together. Java 9 broke the JDK into many modules, allowing developers to use only the modules they needed, reducing the application's footprint.
For example:
java.base
: The base module, which contains essential classes likejava.lang
,java.util
, andjava.io
. Every module implicitly requiresjava.base
.java.sql
: The module for JDBC and SQL-related classes.java.desktop
: The module for GUI-related components likejavax.swing
andjava.awt
.
Automatic Modules
When transitioning legacy codebases to a modular system, Java 9 introduced the concept of automatic modules. If you include a non-modular JAR file on the module path (instead of the classpath), Java will treat it as an automatic module, giving it a name based on the JAR file’s name.
This feature provides backward compatibility for older libraries that are not yet modularized.
Challenges of Modularity
While modularity provides several benefits, it also introduces some challenges:
Migration: Transitioning large, existing codebases to the modular system can be time-consuming and difficult, especially when dependencies are not modularized.
Learning Curve: Developers familiar with the classpath-based system need to learn the new module system concepts like the
module-info.java
file,requires
, andexports
.Compatibility: Some third-party libraries may not yet be modularized, making it necessary to rely on automatic modules or maintain non-modular classpaths.
Java Modularity Tools
jdeps: A tool that helps you analyze your code’s dependencies and modularize your application.
jlink: A tool that creates a custom runtime image containing only the modules you need for your application.
Conclusion
Java 9’s modularity was a significant shift in how Java applications are built and maintained. It enables developers to create more scalable, maintainable, and efficient applications by organizing code into modules with explicit dependencies. Despite the challenges, it is a powerful tool that enhances the flexibility and performance of the Java ecosystem.
Multi Threading
Multithreading in Java is a powerful technique that allows concurrent execution of two or more threads for maximum CPU utilization. A thread is a lightweight process or the smallest unit of a task that can be executed concurrently by a Java program. Java's multithreading capabilities make it suitable for applications requiring tasks to be executed simultaneously, such as handling multiple requests on a web server or performing background computations while updating a user interface.
Key Concepts in Java Multithreading
1. Thread
A thread in Java represents an independent path of execution. Multiple threads can run concurrently in a Java application, sharing the same process resources (like memory and file handles) but executing independently.
2. Thread Lifecycle
A thread in Java goes through various states in its lifecycle:
New: A thread is created but not yet started.
Runnable: The thread is ready to run and is waiting for CPU time.
Blocked/Waiting: The thread is waiting for some condition or resource to become available.
Timed Waiting: The thread is waiting for a specific amount of time before becoming runnable again.
Terminated: The thread has completed its task or has been interrupted.
Creating Threads in Java
There are two main ways to create and start a thread in Java:
1. Implementing the Runnable
Interface
This method is preferred when you want to define a task that can be executed by multiple threads.
class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread is running");
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // Starts the thread and invokes the run() method
}
}
2. Extending the Thread
Class
Another way is to extend the Thread
class and override the run()
method.
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running");
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // Starts the thread and invokes the run() method
}
}
Important Methods in the Thread
Class
start()
: Starts the thread and executes itsrun()
method.run()
: Contains the code to be executed by the thread. Typically, this method is overridden.sleep(long millis)
: Puts the current thread to sleep for a specified duration.join()
: Waits for the thread to complete before continuing with the execution of the calling thread.yield()
: Pauses the currently executing thread to give a chance to other threads of the same priority to execute.isAlive()
: Returnstrue
if the thread is still running or waiting, andfalse
if it has terminated.
Thread Synchronization
When multiple threads access shared resources concurrently, it can lead to data inconsistency or race conditions. To prevent this, synchronization is used to control the access of multiple threads to shared resources.
Synchronized Block
You can synchronize a block of code by using the synchronized
keyword.
class Counter {
private int count = 0;
public void increment() {
synchronized(this) {
count++;
}
}
public int getCount() {
return count;
}
}
In this example, the increment()
method is synchronized to ensure that only one thread can execute this method at a time on a given object, preventing race conditions.
Synchronized Method
You can also synchronize an entire method by using the synchronized
keyword in the method signature.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
Inter-Thread Communication
Inter-thread communication allows threads to communicate with each other when working with shared resources. The key methods for this in Java are wait()
, notify()
, and notifyAll()
. These methods must be called inside a synchronized context.
wait()
: Causes the current thread to wait until another thread invokes thenotify()
ornotifyAll()
methods for the same object.notify()
: Wakes up a single thread that is waiting on this object's monitor.notifyAll()
: Wakes up all threads that are waiting on this object's monitor.
Example of inter-thread communication:
class SharedResource {
private boolean isAvailable = false;
public synchronized void produce() throws InterruptedException {
while (isAvailable) {
wait(); // Wait if resource is already available
}
System.out.println("Produced Resource");
isAvailable = true;
notify(); // Notify the consumer thread
}
public synchronized void consume() throws InterruptedException {
while (!isAvailable) {
wait(); // Wait if resource is not available
}
System.out.println("Consumed Resource");
isAvailable = false;
notify(); // Notify the producer thread
}
}
Thread Pooling
In applications where creating and destroying threads frequently is inefficient, thread pooling can be used. A thread pool is a collection of worker threads that execute tasks from a queue.
Java provides the Executor
framework to handle thread pooling efficiently.
Example:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3); // Create a thread pool with 3 threads
for (int i = 0; i < 5; i++) {
executor.submit(new MyRunnableTask());
}
executor.shutdown();
}
}
class MyRunnableTask implements Runnable {
public void run() {
System.out.println("Executing Task by: " + Thread.currentThread().getName());
}
}
Concurrency Utilities
Java also provides higher-level concurrency utilities in the java.util.concurrent
package to simplify multithreading and handle complex scenarios like synchronization and coordination between threads.
Locks:
ReentrantLock
,ReadWriteLock
Atomic Variables:
AtomicInteger
,AtomicBoolean
Blocking Queues:
ArrayBlockingQueue
,LinkedBlockingQueue
Futures and Callables: For handling tasks that return a result.
Java Multithreading Use Cases
Server applications: Handling multiple client requests simultaneously.
UI applications: Performing background operations (e.g., loading data) without freezing the user interface.
Games: Managing different tasks like AI, rendering, and user inputs concurrently.
Conclusion
Java multithreading is a powerful tool for executing tasks concurrently, improving the efficiency and responsiveness of applications. By understanding thread creation, synchronization, communication, and management, you can build robust multithreaded applications that handle concurrent operations effectively. The java.util.concurrent
package also provides a rich set of utilities for simplifying multithreading in modern Java applications.
Strings
Strings in Java are sequences of characters that are used to represent text. In Java, the String
class is immutable, meaning once a String
object is created, its value cannot be changed. However, Java provides a rich API for string manipulation through various methods in the String
, StringBuilder
, and StringBuffer
classes.
String Creation
You can create strings in two main ways:
-
Using String Literals:
String str1 = "Hello, World!";
-
Using the
new
Keyword:
String str2 = new String("Hello, World!");
Immutability of Strings
Strings in Java are immutable. When you perform any operation that seems to modify a string (such as concatenation), a new string object is created, and the original string remains unchanged.
For example:
String str = "Hello";
str = str.concat(" World"); // New string object created; "Hello World"
System.out.println(str); // Output: Hello World
Common String Operations
1. Length of a String
The length()
method returns the number of characters in a string.
String str = "Harry Potter";
int len = str.length(); // len is 12
2. Accessing Characters
You can access individual characters of a string using the charAt()
method.
String str = "Hogwarts";
char ch = str.charAt(0); // ch is 'H'
3. Substring
You can extract a part of a string using the substring()
method.
String str = "Hogwarts School";
String sub = str.substring(0, 8); // sub is "Hogwarts"
4. String Comparison
equals()
: Checks if two strings are equal in content.equalsIgnoreCase()
: Compares two strings ignoring case differences.compareTo()
: Compares two strings lexicographically.
String str1 = "Harry";
String str2 = "harry";
boolean isEqual = str1.equals(str2); // false
boolean isEqualIgnoreCase = str1.equalsIgnoreCase(str2); // true
int comparison = str1.compareTo(str2); // Negative value because "Harry" < "harry"
5. Concatenation
You can concatenate strings using the +
operator or the concat()
method.
String str1 = "Harry";
String str2 = "Potter";
String fullName = str1 + " " + str2; // "Harry Potter"
String fullNameConcat = str1.concat(" ").concat(str2); // "Harry Potter"
6. Replace
The replace()
method is used to replace all occurrences of a particular character or substring.
String str = "Harry Potter";
String newStr = str.replace("r", "z"); // "Hazzy Pottez"
7. Split
The split()
method divides a string into an array of substrings based on a specified delimiter.
String str = "Albus,Dumbledore";
String[] names = str.split(","); // ["Albus", "Dumbledore"]
8. Case Conversion
toLowerCase()
: Converts a string to lowercase.toUpperCase()
: Converts a string to uppercase.
String str = "Hogwarts";
String lower = str.toLowerCase(); // "hogwarts"
String upper = str.toUpperCase(); // "HOGWARTS"
StringBuilder and StringBuffer
While String
objects are immutable, both StringBuilder
and StringBuffer
are mutable classes that allow modification of strings without creating new objects for every change. They are used when you need to perform multiple string manipulations efficiently.
StringBuilder
: Used for creating mutable strings in a single-threaded environment.StringBuffer
: Similar toStringBuilder
, but it is thread-safe (synchronized).
Example of Using StringBuilder
:
StringBuilder sb = new StringBuilder("Harry");
sb.append(" Potter");
System.out.println(sb.toString()); // Output: Harry Potter
Example with a Harry Potter Theme
Here’s a simple example that demonstrates various string operations in the context of Harry Potter:
public class HarryPotterStrings {
public static void main(String[] args) {
// Hogwarts Motto
String motto = "Draco dormiens nunquam titillandus"; // "Never tickle a sleeping dragon"
// Length of Motto
int length = motto.length();
System.out.println("Motto Length: " + length); // Output: Motto Length: 34
// Convert to Uppercase
String upperMotto = motto.toUpperCase();
System.out.println("Uppercase Motto: " + upperMotto); // Output: Uppercase Motto: DRACO DORMIENS NUNQUAM TITILLANDUS
// Extracting "Draco"
String draco = motto.substring(0, 5);
System.out.println("Extracted Word: " + draco); // Output: Extracted Word: Draco
// Replace "dragon" with "serpent"
String modifiedMotto = motto.replace("dragon", "serpent");
System.out.println("Modified Motto: " + modifiedMotto); // No actual replacement in this case as "dragon" is not present
// Splitting words
String[] words = motto.split(" ");
System.out.println("First Word: " + words[0]); // Output: First Word: Draco
// StringBuilder example
StringBuilder wandSpell = new StringBuilder("Expelliarmus");
wandSpell.append("! ").append("Harry cast the spell.");
System.out.println(wandSpell.toString()); // Output: Expelliarmus! Harry cast the spell.
}
}
Conclusion
Strings in Java offer a wide range of operations for handling and manipulating text. The immutability of strings ensures their thread safety, while StringBuilder
and StringBuffer
are provided for more efficient string modifications. Understanding and utilizing Java’s string API is crucial for developing applications that require text processing, formatting, and manipulation.
File System
In Java, the File I/O (Input/Output) system provides classes and methods to work with files and directories, allowing developers to create, read, write, and manipulate files efficiently. The java.io
and java.nio.file
packages provide the primary classes for interacting with the file system.
Key Concepts in Java File System
-
File Class: The
File
class in thejava.io
package is used to represent file and directory pathnames in an abstract way. It does not provide direct file content manipulation but gives a way to perform file operations like creating files, deleting files, listing directory contents, etc.Example:
File file = new File("example.txt");
Basic Operations with File
Class
1. Creating a File
The createNewFile()
method creates a new, empty file if it doesn't already exist.
import java.io.File;
import java.io.IOException;
public class FileExample {
public static void main(String[] args) {
File file = new File("example.txt");
try {
if (file.createNewFile()) {
System.out.println("File created: " + file.getName());
} else {
System.out.println("File already exists.");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
2. Checking File/Directory Existence
You can check if a file or directory exists using the exists()
method.
File file = new File("example.txt");
if (file.exists()) {
System.out.println("File exists.");
} else {
System.out.println("File does not exist.");
}
3. Creating a Directory
You can create directories using the mkdir()
or mkdirs()
methods (for creating parent directories as well).
File dir = new File("newDirectory");
if (dir.mkdir()) {
System.out.println("Directory created.");
} else {
System.out.println("Failed to create directory.");
}
4. Deleting a File/Directory
Use the delete()
method to delete a file or directory. Directories must be empty before being deleted.
File file = new File("example.txt");
if (file.delete()) {
System.out.println("File deleted.");
} else {
System.out.println("Failed to delete the file.");
}
5. Listing Files in a Directory
You can list the contents of a directory using the list()
or listFiles()
methods.
File dir = new File("directoryPath");
String[] files = dir.list();
for (String file : files) {
System.out.println(file);
}
Reading and Writing Files
The java.io
package provides classes such as FileReader
, FileWriter
, BufferedReader
, and BufferedWriter
to read and write files.
1. Writing to a File
The FileWriter
class is used to write text data to a file. It can either append data to a file or overwrite it.
import java.io.FileWriter;
import java.io.IOException;
public class FileWriteExample {
public static void main(String[] args) {
try (FileWriter writer = new FileWriter("example.txt")) {
writer.write("Hello, Hogwarts!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
2. Reading from a File
The FileReader
and BufferedReader
classes are used to read text data from a file.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class FileReadExample {
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) {
e.printStackTrace();
}
}
}
Java NIO (New I/O) File System
Java NIO (java.nio.file
) provides a more modern and flexible approach to working with files and directories. This API supports efficient file operations and provides better error handling through Path
, Files
, and FileSystems
classes.
1. Working with Path
and Files
The Path
class represents file and directory locations, and the Files
class provides methods to perform file operations like creating, copying, moving, and deleting files.
-
Creating a Path:
Path path = Paths.get("example.txt");
-
Creating a File:
try { Path path = Files.createFile(Paths.get("example.txt")); System.out.println("File created: " + path); } catch (IOException e) { e.printStackTrace(); }
-
Writing to a File:
Path path = Paths.get("example.txt"); String content = "Hello, Hogwarts!"; try { Files.write(path, content.getBytes()); } catch (IOException e) { e.printStackTrace(); }
-
Reading from a File:
Path path = Paths.get("example.txt"); try { List<String> lines = Files.readAllLines(path); lines.forEach(System.out::println); } catch (IOException e) { e.printStackTrace(); }
-
Copying a File:
Path source = Paths.get("example.txt"); Path destination = Paths.get("copy_of_example.txt"); try { Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { e.printStackTrace(); }
-
Deleting a File:
Path path = Paths.get("example.txt"); try { Files.delete(path); System.out.println("File deleted."); } catch (IOException e) { e.printStackTrace(); }
File Attribute Operations
NIO also allows you to retrieve and modify file attributes, such as the file size, creation time, last modified time, and permissions.
-
Getting File Attributes:
Path path = Paths.get("example.txt"); try { BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class); System.out.println("File size: " + attrs.size()); System.out.println("Creation time: " + attrs.creationTime()); } catch (IOException e) { e.printStackTrace(); }
Conclusion
Java provides powerful APIs for interacting with the file system through java.io
for basic operations and java.nio.file
for more advanced file management and attributes handling. The File
class is useful for simple file operations, while the NIO package offers better performance, flexibility, and additional features such as working with symbolic links, file metadata, and file system watchers.
Searlization and Cloning
Serialization
Serialization in Java is the process of converting an object into a byte stream, so it can be easily saved to a file or transferred over a network. The byte stream represents the object in a platform-independent format, allowing the object to be recreated later by deserializing the byte stream back into an object. This is useful in scenarios like saving object state or sending objects over a network.
Key Points of Serialization
Serializable Interface: To serialize an object, the class of that object must implement the
Serializable
interface, which is a marker interface (i.e., it has no methods).ObjectOutputStream and ObjectInputStream: These classes are used to serialize and deserialize objects, respectively.
transient
Keyword: Fields marked withtransient
will not be serialized.serialVersionUID: This is a unique version identifier for a serializable class, used during deserialization to verify that the sender and receiver are compatible with respect to serialization.
Example of Serialization
import java.io.Serializable;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;
class Student implements Serializable {
private static final long serialVersionUID = 1L;
String name;
transient int age; // Won't be serialized
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
public class SerializationExample {
public static void main(String[] args) {
Student student = new Student("Harry", 17);
try (FileOutputStream fos = new FileOutputStream("student.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(student);
System.out.println("Student object serialized.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
Example of Deserialization
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.IOException;
public class DeserializationExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("student.ser");
ObjectInputStream ois = new ObjectInputStream(fis)) {
Student student = (Student) ois.readObject();
System.out.println("Name: " + student.name); // Output: Harry
System.out.println("Age: " + student.age); // Output: 0 (transient, not serialized)
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Cloning
Cloning in Java is the process of creating an exact copy of an existing object. Java provides the Cloneable
interface and the clone()
method for this purpose. The clone()
method creates a field-by-field copy (shallow copy) of the object unless deep cloning is implemented manually.
Key Points of Cloning
Cloneable Interface: The class must implement the
Cloneable
interface to allow its objects to be cloned.clone()
Method: Theclone()
method in theObject
class is protected and must be overridden to provide a public method for cloning.Shallow Cloning: By default, cloning is shallow, meaning only the object’s fields are copied, not the objects referenced by the fields.
Deep Cloning: Deep cloning involves creating a copy of the object and also cloning the objects it references.
Example of Shallow Cloning
class HogwartsStudent implements Cloneable {
String name;
int year;
public HogwartsStudent(String name, int year) {
this.name = name;
this.year = year;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class CloningExample {
public static void main(String[] args) {
try {
HogwartsStudent student1 = new HogwartsStudent("Harry", 7);
HogwartsStudent student2 = (HogwartsStudent) student1.clone();
System.out.println(student1.name + " - Year " + student1.year); // Output: Harry - Year 7
System.out.println(student2.name + " - Year " + student2.year); // Output: Harry - Year 7
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
Example of Deep Cloning
To implement deep cloning, you must clone the objects referenced by the fields manually, usually by calling the clone()
method on the field objects as well.
class Broomstick implements Cloneable {
String model;
public Broomstick(String model) {
this.model = model;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Wizard implements Cloneable {
String name;
Broomstick broomstick;
public Wizard(String name, Broomstick broomstick) {
this.name = name;
this.broomstick = broomstick;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Wizard clonedWizard = (Wizard) super.clone();
clonedWizard.broomstick = (Broomstick) broomstick.clone(); // Deep cloning of the broomstick
return clonedWizard;
}
}
public class DeepCloningExample {
public static void main(String[] args) {
try {
Broomstick broomstick = new Broomstick("Nimbus 2000");
Wizard wizard1 = new Wizard("Harry", broomstick);
Wizard wizard2 = (Wizard) wizard1.clone();
System.out.println(wizard1.broomstick.model); // Output: Nimbus 2000
System.out.println(wizard2.broomstick.model); // Output: Nimbus 2000
wizard2.broomstick.model = "Firebolt"; // Changing cloned object's broomstick model
System.out.println(wizard1.broomstick.model); // Output: Nimbus 2000 (deep copy, original remains unchanged)
System.out.println(wizard2.broomstick.model); // Output: Firebolt
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
Comparison: Serialization vs. Cloning
Serialization is typically used to save or transmit object states across different environments, while cloning is used within the same JVM to duplicate objects.
Serialization supports deep copying implicitly through object graphs, whereas cloning requires explicit handling for deep copies.
Serialization requires classes to implement
Serializable
, while cloning requires classes to implementCloneable
and overrideclone()
.
Conclusion
Serialization is ideal for storing and transferring object states over networks or files, while cloning is suited for creating copies of objects within your program.
Understanding both concepts allows you to handle object state management more effectively, depending on the use case.
Streams
Streams API in Java
The Streams API, introduced in Java 8, provides a powerful way to process sequences of elements (like collections) in a functional style. It enables you to perform complex operations such as filtering, mapping, and reducing with ease.
Key Concepts
1. Stream vs Collection
Collection: Represents a data structure and contains elements that can be manipulated.
Stream: Represents a sequence of elements and provides methods to perform functional-style operations on them.
Streams are not data structures but rather a view of data. They allow you to process data in a declarative way.
2. Creating Streams
Streams can be created from various sources:
Collections:
List
,Set
,Map
, etc.Arrays:
Arrays.stream
()
Other sources: Files, I/O channels, etc.
Examples
From a Collection:
List<String> names = Arrays.asList("Harry", "Hermione", "Ron");
Stream<String> stream = names.stream();
From an Array:
String[] namesArray = {"Harry", "Hermione", "Ron"};
Stream<String> stream = Arrays.stream(namesArray);
From a File:
import java.nio.file.*;
import java.util.stream.*;
Stream<String> lines = Files.lines(Paths.get("file.txt"));
3. Stream Operations
Stream operations are divided into two categories:
Intermediate Operations: These operations return a new stream and are lazy, meaning they are not executed until a terminal operation is invoked. Examples include
filter()
,map()
,distinct()
,sorted()
, etc.Terminal Operations: These operations produce a result or a side effect and mark the end of the stream pipeline. Examples include
forEach()
,collect()
,reduce()
,count()
, etc.
Examples
Intermediate Operations:
Stream<String> filteredStream = stream.filter(name -> name.startsWith("H"));
Stream<String> mappedStream = filteredStream.map(String::toUpperCase);
Terminal Operations:
mappedStream.forEach(System.out::println); // Prints: HARRY, HERMIONE
Common Stream Operations
1. Filtering
Filters elements based on a condition.
List<String> names = Arrays.asList("Harry", "Hermione", "Ron");
names.stream()
.filter(name -> name.startsWith("H"))
.forEach(System.out::println); // Output: Harry, Hermione
2. Mapping
Transforms elements into another form.
List<String> names = Arrays.asList("Harry", "Hermione", "Ron");
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println); // Output: HARRY, HERMIONE, RON
3. Sorting
Sorts elements based on a comparator.
List<String> names = Arrays.asList("Harry", "Hermione", "Ron");
names.stream()
.sorted()
.forEach(System.out::println); // Output: Harry, Hermione, Ron
4. Distinct
Removes duplicate elements.
List<String> names = Arrays.asList("Harry", "Hermione", "Harry", "Ron");
names.stream()
.distinct()
.forEach(System.out::println); // Output: Harry, Hermione, Ron
5. Limit and Skip
Limits the number of elements or skips the first few.
List<String> names = Arrays.asList("Harry", "Hermione", "Ron", "Draco", "Neville");
names.stream()
.limit(3)
.forEach(System.out::println); // Output: Harry, Hermione, Ron
names.stream()
.skip(2)
.forEach(System.out::println); // Output: Ron, Draco, Neville
6. Reducing
Reduces elements to a single value.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum); // Output: 15
7. Collecting
Gathers elements into a collection or other container.
List<String> names = Arrays.asList("Harry", "Hermione", "Ron");
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("H"))
.collect(Collectors.toList()); // Output: [Harry, Hermione]
Parallel Streams
Parallel streams allow you to perform operations in parallel, potentially improving performance with large data sets. You can convert a stream to a parallel stream using parallelStream()
.
List<String> names = Arrays.asList("Harry", "Hermione", "Ron");
names.parallelStream()
.filter(name -> name.startsWith("H"))
.forEach(System.out::println); // Output: Harry, Hermione
Stream Pipeline Example
Here’s an example of a complete stream pipeline that reads from a file, filters lines starting with "H", converts them to uppercase, and prints them:
import java.nio.file.*;
import java.util.stream.*;
public class StreamPipelineExample {
public static void main(String[] args) {
try (Stream<String> lines = Files.lines(Paths.get("file.txt"))) {
lines.filter(line -> line.startsWith("H"))
.map(String::toUpperCase)
.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Conclusion
The Streams API in Java provides a powerful and flexible way to process sequences of elements. By using streams, you can write more declarative and functional code, leveraging the full power of Java’s functional programming capabilities. The API makes it easy to perform operations such as filtering, mapping, and reducing, and supports both sequential and parallel processing.
Lambda Expression
Lambda expressions are a key feature introduced in Java 8, allowing you to write more concise code by simplifying the use of functional interfaces. They are a way to represent anonymous methods (or functions) and make functional programming easier in Java.
Syntax of Lambda Expressions
A lambda expression consists of:
Parameters: Zero or more input parameters in parentheses
()
.Arrow Token:
->
separates parameters from the body.Body: Contains a single expression or a block of code enclosed in
{}
.
(parameters) -> expression
(parameters) -> { statements }
Basic Example
// Without Lambda: Using an anonymous inner class
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Running...");
}
};
// With Lambda Expression
Runnable r = () -> System.out.println("Running...");
Key Points
Functional Interfaces: A lambda expression can only be used with interfaces that have a single abstract method (also known as functional interfaces). For example,
Runnable
,Comparator
, andCallable
are functional interfaces.Type Inference: The compiler can often infer the types of the parameters, so you can omit them.
No need for curly braces: If the lambda body contains a single statement, you can omit the curly braces and the
return
keyword.
Examples of Lambda Expressions
1. No Parameters
A lambda expression with no parameters is written with empty parentheses.
() -> System.out.println("Hello, World!");
2. Single Parameter
When there is only one parameter, you can omit the parentheses.
name -> System.out.println("Hello, " + name);
3. Multiple Parameters
If there are multiple parameters, you must enclose them in parentheses.
(a, b) -> a + b;
4. Multiple Statements
If the lambda body has multiple statements, you must use curly braces and an explicit return
if needed.
(a, b) -> {
int result = a + b;
return result;
};
Common Use Cases
1. Using Lambda with Collections
Lambdas are often used with collections for operations like sorting, filtering, and iterating.
Sorting
Without lambda:
List<String> names = Arrays.asList("Harry", "Ron", "Hermione");
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});
With lambda:
List<String> names = Arrays.asList("Harry", "Ron", "Hermione");
Collections.sort(names, (s1, s2) -> s1.compareTo(s2));
Iterating
Without lambda:
for (String name : names) {
System.out.println(name);
}
With lambda:
names.forEach(name -> System.out.println(name));
2. Using Lambda with Streams
Java's Stream API is where lambdas shine, enabling operations like filtering, mapping, and reducing.
Filtering
List<String> names = Arrays.asList("Harry", "Hermione", "Ron");
names.stream()
.filter(name -> name.startsWith("H"))
.forEach(System.out::println); // Output: Harry, Hermione
Mapping
names.stream()
.map(String::length)
.forEach(System.out::println); // Output: 5, 8, 3
3. Functional Interfaces
Java provides several functional interfaces in the java.util.function
package that work with lambdas:
-
Predicate<T>
: Represents a function that takes one argument and returns a boolean.
Predicate<String> isLongerThan5 = s -> s.length() > 5;
-
Function<T, R>
: Represents a function that takes one argument and returns a result.
Function<String, Integer> length = s -> s.length();
-
Consumer<T>
: Represents an operation that takes a single input argument and returns no result.
Consumer<String> print = s -> System.out.println(s);
-
Supplier<T>
: Represents a function that takes no arguments and returns a result.
Supplier<String> greeting = () -> "Hello!";
Lambda Expression with Method References
You can replace some lambda expressions with method references for more concise code. A method reference is a shorthand notation for a lambda that calls a method.
// Lambda
names.forEach(name -> System.out.println(name));
// Method reference
names.forEach(System.out::println);
Conclusion
Lambda expressions simplify the code and make it more readable and concise. They are widely used in functional programming in Java, especially with collections and the Stream API, enhancing operations such as sorting, filtering, and iterating.
Top comments (0)