Java Notes
Java Notes
What is Java?
• Java is a high-level, object-oriented programming language developed by Sun Microsystems
in 1995.
• Created by James Gosling, it was designed for platform independence.
• Key features: simplicity, portability, and robustness.
Features of Java:
1. Object-Oriented: Focuses on objects and data rather than procedures.
2. Platform-Independent: Write once, run anywhere using JVM.
3. Secure: Provides features like bytecode and exception handling.
4. Robust: Strong memory management and error handling.
5. Multithreaded: Allows multiple tasks in parallel.
Applications of Java:
1. Web Development: Servlets, JSP, Spring Framework.
2. Mobile Applications: Android development.
3. Desktop Applications: GUI-based applications using JavaFX or Swing.
4. Enterprise Applications: Backend systems, banking applications.
5. Embedded Systems: Devices like Blu-ray players, sensors.
2. Setting Up the Environment
Installing Java (JDK and IDE):
1. Install JDK:
• Download JDK from Oracle's website.
• Install it and configure the JAVA_HOME environment variable.
2. Install an IDE:
• Popular options: IntelliJ IDEA, Eclipse, or VS Code.
• Download and set up your chosen IDE.
6. Output:
Hello, World!
Keywords in Java:
• Reserved words with specific meanings.
• Examples: public, class, static, void, if, else, while, switch, return, etc.
Operators:
1. Arithmetic Operators: +, -, *, /, %.
2. Relational Operators: >, <, >=, <=, ==, !=.
3. Logical Operators: &&, ||, !.
4. Assignment Operators: =, +=, -=, *=, /=.
5. Unary Operators: ++, --.
Control Statements:
1. Conditional Statements:
• if: Executes code if condition is true.
if (x > 10) {
System.out.println("x is greater than 10");
}
2. Loops:
• for loop: Iterates a fixed number of times.
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
Arrays:
• Single-Dimensional Array:
int[] numbers = {1, 2, 3, 4, 5};
System.out.println(numbers[0]); // Output: 1
• Multi-Dimensional Array:
int[][] matrix = {
{1, 2, 3},
{4, 5, 6}
};
System.out.println(matrix[1][2]); // Output: 6
4. Object-Oriented Programming (OOPs)
Principles of OOPs:
1. Encapsulation:
• Wrapping data (variables) and methods in a single unit (class).
• Example:
class Person {
private String name; // Encapsulated variable
public void setName(String name) { this.name = name; }
public String getName() { return name; }
}
2. Inheritance:
• Reusing properties and methods of a parent class in a child class.
• Example:
class Animal { void eat() { System.out.println("Eating"); } }
class Dog extends Animal { void bark()
{ System.out.println("Barking"); } }
3. Polymorphism:
• Performing a single action in different ways.
• Example:
• Overloading (Compile-Time Polymorphism):
class Calculator {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
}
4. Abstraction:
• Hiding implementation details and showing only essential information.
• Example:
• Abstract class:
abstract class Shape {
abstract void draw();
}
class Circle extends Shape {
void draw() { System.out.println("Drawing Circle"); }
}
Constructors:
• Special methods to initialize objects.
• Default Constructor: No parameters.
class Car { Car() { System.out.println("Car Created"); } }
Access Modifiers:
• private: Accessible only within the class.
• protected: Accessible within package and subclasses.
• public: Accessible from anywhere.
• default: Accessible within the package.
Exception Handling
What is an Exception?
• An exception is an event that disrupts the normal flow of a program.
• It occurs during runtime and can be handled to prevent the program from crashing.
• Example: Division by zero, accessing a null object.
Types of Exceptions:
1. Checked Exceptions:
• Checked at compile-time.
• Must be handled using try-catch or declared using throws.
• Examples: IOException, SQLException.
• Example:
import java.io.*;
class Test {
void readFile() throws IOException {
FileReader file = new FileReader("file.txt");
}
}
2. Unchecked Exceptions:
• Occur at runtime and are not checked at compile-time.
• Examples: NullPointerException, ArithmeticException.
• Example:
int result = 10 / 0; // Throws ArithmeticException
Custom Exceptions:
• User-defined exceptions created by extending the Exception class.
Example:
class InvalidAgeException extends Exception {
InvalidAgeException(String message) {
super(message);
}
}
class Test {
void checkAge(int age) throws InvalidAgeException {
if (age < 18) throw new InvalidAgeException("Age must be 18 or above.");
}
}
6. Java Collections Framework
List:
• Definition: An ordered collection that allows duplicate elements.
• Implementations:
1. ArrayList:
• Dynamic array; fast for random access.
• Example:
ArrayList<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
System.out.println(list);
2. LinkedList:
• Doubly linked list; faster for insertions and deletions.
• Example:
LinkedList<String> list = new LinkedList<>();
list.add("Car");
list.add("Bike");
3. Vector:
• Synchronized version of ArrayList.
• Example:
Vector<String> vector = new Vector<>();
vector.add("Dog");
vector.add("Cat");
Set:
• Definition: A collection that does not allow duplicate elements.
• Implementations:
1. HashSet:
• No order guarantee; uses hashing.
• Example:
HashSet<String> set = new HashSet<>();
set.add("Java");
set.add("Python");
2. TreeSet:
• Maintains elements in sorted order.
• Example:
TreeSet<Integer> set = new TreeSet<>();
set.add(3);
set.add(1);
Map:
• Definition: A collection that stores key-value pairs.
• Implementations:
1. HashMap:
• Unordered; fast for lookups.
• Example:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "Apple");
map.put(2, "Banana");
2. TreeMap:
• Maintains keys in sorted order.
• Example:
TreeMap<Integer, String> map = new TreeMap<>();
map.put(2, "Car");
map.put(1, "Bike");
3. LinkedHashMap:
• Maintains insertion order.
• Example:
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
map.put("A", 100);
map.put("B", 200);
Queue:
• Definition: Follows FIFO (First-In-First-Out) principle.
• Implementations:
1. PriorityQueue:
• Elements are processed based on priority.
• Example:
PriorityQueue<Integer> queue = new PriorityQueue<>();
queue.add(5);
queue.add(1);
2. LinkedList (Queue):
• Can be used as a queue.
• Example:
Queue<String> queue = new LinkedList<>();
queue.add("Task1");
queue.add("Task2");
Stack:
• Definition: Follows LIFO (Last-In-First-Out) principle.
• Example:
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
System.out.println(stack.pop()); // Output: 2
Iterators:
• Definition: Used to traverse collections.
• Example:
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
Comparable vs Comparator:
1. Comparable:
• Used to define natural ordering of objects.
• Implemented using compareTo() method.
• Example:
class Student implements Comparable<Student> {
int roll;
Student(int roll) { this.roll = roll; }
public int compareTo(Student s) { return this.roll - s.roll; }
}
2. Comparator:
• Used for custom ordering.
• Implemented using compare() method.
• Example:
class StudentComparator implements Comparator<Student> {
public int compare(Student s1, Student s2) { return s1.roll -
s2.roll; }
}
7. Multithreading and Concurrency
Creating Threads:
1. Extending Thread Class:
• The Thread class can be extended, and the run() method can be overridden to
define the task.
• Example:
class MyThread extends Thread {
public void run() {
System.out.println("Thread running");
}
}
public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start(); // starts the thread
}
}
Thread States:
• A thread can be in one of the following states:
1. New: Thread is created but not yet started.
2. Runnable: Thread is ready to run and is either running or waiting for CPU time.
3. Blocked: Thread is waiting for a resource or lock.
4. Waiting: Thread is waiting indefinitely for another thread to perform a particular
action.
5. Timed Waiting: Thread is waiting for a specific amount of time.
6. Terminated: Thread has finished execution.
Synchronization:
• Synchronization ensures that only one thread can access a resource at a time to prevent data
inconsistency.
• It is achieved using the synchronized keyword.
• Example:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
• Synchronized Block:
• Used to synchronize only a specific block of code rather than the whole method.
Example:
public void increment() {
synchronized(this) {
count++;
}
}
Inter-thread Communication:
• Java provides methods like wait(), notify(), and notifyAll() to allow threads to
communicate with each other.
• wait(): Causes the current thread to release the lock and wait until another thread
notifies it.
• notify(): Wakes up one thread that is waiting on the object's monitor.
• notifyAll(): Wakes up all the threads that are waiting on the object's monitor.
Example:
class SharedResource {
synchronized void produce() throws InterruptedException {
while (condition) {
wait(); // Wait until notified
}
}
synchronized void consume() {
notify(); // Notify the waiting thread
}
}
• Types of Executors:
1. newFixedThreadPool(n): Creates a pool of fixed-size threads.
2. newCachedThreadPool(): Creates a thread pool that creates new threads as needed
but reuses idle threads.
3. newSingleThreadExecutor(): Creates a single-threaded pool to execute tasks
sequentially.
8. Java Input/Output (I/O)
• Character Streams:
• Deal with character data (text data).
• They read and write data in the form of characters (16-bit data).
• Classes: FileReader, FileWriter.
• Example:
FileReader fr = new FileReader("file.txt");
int data;
while ((data = fr.read()) != -1) {
System.out.print((char) data);
}
fr.close();
2. Writing to a File:
• FileWriter: Writes characters to a file.
• Example:
FileWriter writer = new FileWriter("file.txt");
writer.write("Hello, Java!");
writer.close();
3. BufferedReader and BufferedWriter:
• Used for efficient reading and writing.
• BufferedReader: Reads data in chunks for better performance.
• BufferedWriter: Writes data in chunks.
• Example:
BufferedReader br = new BufferedReader(new FileReader("file.txt"));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
• Deserialization:
• The process of converting a byte stream back into a copy of the original object.
• Example:
// Deserialization
FileInputStream fis = new FileInputStream("person.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
Person p1 = (Person) ois.readObject();
ois.close();
Generics:
• Generics enable types (classes and methods) to operate on objects of various types while
providing compile-time type safety.
• Example:
class Box<T> {
private T value;
public T getValue() { return value; }
public void setValue(T value) { this.value = value; }
}
// Using Generics
Box<Integer> intBox = new Box<>();
intBox.setValue(10);
• Wildcard:
• ? extends T: Represents an unknown type that is a subtype of T.
• ? super T: Represents an unknown type that is a supertype of T.
Annotations:
• Annotations provide metadata about the program but have no direct effect on its operation.
• Example:
@Override
public String toString() {
return "Example";
}
@Deprecated
public void oldMethod() {
// Method is no longer in use
}
• Common Annotations:
• @Override: Indicates a method overrides a method in the superclass.
• @Deprecated: Marks methods that are outdated and should no longer be used.
• @SuppressWarnings: Tells the compiler to suppress certain warnings.
Lambda Expressions:
• Lambda expressions provide a clear and concise way to represent one method interface
using an expression.
• Syntax: (parameters) -> expression
• Example:
// Using Lambda Expression for Runnable
Runnable r = () -> System.out.println("Running in a thread");
new Thread(r).start();
• Lambda expressions are used extensively in the Streams API and functional interfaces.
Streams API:
• Streams represent sequences of elements that can be processed in parallel or sequentially.
• Streams can be used to perform operations like filtering, mapping, and reducing.
• Types of Streams:
1. Stream: A sequence of elements that can be processed.
2. IntStream, LongStream, DoubleStream: Specialized streams for primitive types.
• Operations in Streams:
1. Intermediate Operations: Transform a stream into another stream (e.g.,
filter(), map()).
2. Terminal Operations: Produce a result or side-effect (e.g., collect(),
forEach()).
• Example:
List<String> names = Arrays.asList("John", "Jane", "Paul");
names.stream()
.filter(name -> name.startsWith("J"))
.forEach(System.out::println);
Parallel Streams:
• Parallel streams enable processing of elements in a stream in parallel, taking advantage of
multi-core processors.
• They use the ForkJoinPool to divide the workload across multiple threads.
• Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.mapToInt(Integer::intValue)
.sum();
• Caution: Parallel streams are not always faster. For small collections or simple operations,
they may even perform worse than sequential streams.
Functional Interfaces:
• A functional interface is an interface that has only one abstract method, and it can have
multiple default or static methods.
• Common functional interfaces:
• Runnable, Callable, Comparator, Consumer, Supplier, Function,
Predicate.
• Example:
@FunctionalInterface
interface MathOperation {
int operation(int a, int b);
}
Introduction to Servlets:
• Servlets are Java programs that run on a web server and handle HTTP requests and
responses.
• They are the foundation of Java web applications and are used to generate dynamic content.
• Servlet Lifecycle:
1. init(): Initializes the servlet.
2. service(): Handles client requests.
3. destroy(): Cleans up resources before the servlet is destroyed.
• Example of a basic servlet:
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
• The Java code is compiled into a servlet, and it runs on the server to produce dynamic
content.
• JSP Syntax:
•
Declarations: <%! int count = 0; %>
•
Scriptlets: <% count++; %>
•
Expressions: <%= count %>
•
Directives: <%@ page language="java" contentType="text/html"
%>
• Example of a simple JSP file:
<html>
<body>
<h1>Welcome to JSP</h1>
<%
String name = request.getParameter("name");
out.println("Hello, " + name);
%>
</body>
</html>
2. Establish a connection:
Connection conn =
DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb",
"username", "password");
3. Create a statement:
Statement stmt = conn.createStatement();
4. Execute a query:
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
• Closing Resources:
It's important to close Connection, Statement, and ResultSet to free up database
resources.
rs.close();
stmt.close();
conn.close();
Connecting to a Database:
• Steps to connect to a database:
1. Load the database driver.
2. Establish a connection using DriverManager.getConnection().
3. Create a Statement or PreparedStatement to send SQL queries.
4. Execute the query and process the results.
• Example of connecting to MySQL:
Connection conn =
DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root",
"password");
Executing Queries:
• Executing SQL Queries:
1. Statement.executeQuery(): Used for queries that return a result set, such as
SELECT.
2. Statement.executeUpdate(): Used for queries that do not return a result set, such as
INSERT, UPDATE, or DELETE.
• Example of executing a SELECT query:
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {
System.out.println(rs.getString("name"));
}
• PreparedStatement:
Used for executing SQL queries with parameters. It helps avoid SQL injection and improves
performance for repeated queries.
Example:
String query = "INSERT INTO users (name, age) VALUES (?, ?)";
PreparedStatement ps = conn.prepareStatement(query);
ps.setString(1, "John");
ps.setInt(2, 30);
ps.executeUpdate();
11. Frameworks in Java
Hibernate:
• What is Hibernate?
• Hibernate is an ORM framework that maps Java objects to database tables. It
eliminates the need for developers to write SQL queries to interact with the database,
instead allowing them to use Java methods.
• Advantages of Hibernate:
• Simplifies database interaction.
• Reduces boilerplate code for CRUD operations.
• Supports object-oriented query language (HQL).
• Handles database connection pooling and caching.
• Basic Hibernate Example:
SessionFactory factory = new
Configuration().configure("hibernate.cfg.xml")
.addAnnotatedClass(Employee.cl
ass)
.buildSessionFactory();
Apache Struts:
• What is Apache Struts?
• Apache Struts is a framework for creating Java-based web applications, especially
suited for large-scale enterprise-level applications.
• It is based on the Model-View-Controller (MVC) architecture, which separates the
application logic, user interface, and data.
• Struts Features:
• Supports action-based programming, where user actions trigger corresponding logic.
• Provides tag libraries for form handling, data display, and validation.
• Integrates easily with other Java technologies like EJB, Hibernate, and Spring.
• Basic Struts Example:
• In Struts, actions are defined in XML configuration files, and the logic is
implemented in Java classes.
• Example of an action mapping in struts-config.xml:
<action path="/greet"
type="com.example.GreetAction"
name="greetForm"
scope="request"
input="/greet.jsp">
<forward name="success" path="/welcome.jsp"/>
</action>
Basic Example Using Spring Boot:
1. Create a Spring Boot Application:
• Use the @SpringBootApplication annotation to indicate the main class of
your Spring Boot application.
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
2. Controller Class:
• Create a REST controller to handle HTTP requests.
@RestController
@RequestMapping("/api")
public class HelloController {
@GetMapping("/hello")
public String sayHello() {
return "Hello, Spring Boot!";
}
}
• Static Methods: You can also define static methods in interfaces, which are not
inherited by implementing classes.
interface MyInterface {
static void printStatic() {
System.out.println("Static method in interface");
}
}
2. Optional Class:
• Purpose: The Optional class is used to represent optional values that may or may
not be present. It helps to avoid NullPointerException and encourages more
explicit handling of missing values.
• Example:
Optional<String> optional = Optional.of("Hello");
optional.ifPresent(System.out::println); // prints "Hello"
Key Takeaways:
• Java 8 brought powerful changes like default methods in interfaces, the Optional class,
and a new Date-Time API.
• Java 9 introduced modules to improve code organization and maintainability.
• Java 10 introduced the var keyword for local-variable type inference.
• Java 13 added text blocks for easier handling of multi-line strings.
• Java 16 and beyond introduced pattern matching to simplify type checks and casting.
13. Java Best Practices
1. Naming Conventions:
• Classes and Interfaces: Class and interface names should be written in PascalCase
(uppercase first letter of each word).
• Example: StudentDetails, EmployeeService
• Methods and Variables: Method and variable names should be written in camelCase
(lowercase first letter, and uppercase for subsequent words).
• Example: getStudentName(), totalAmount
• Constants: Constants should be in UPPER_SNAKE_CASE (all uppercase, with
underscores separating words).
• Example: MAX_SIZE, PI_VALUE
• Package Names: Package names should be all lowercase with no underscores, often using
the company's domain name in reverse.
• Example: com.example.studentmanagement
• Avoid Abbreviations: Avoid using abbreviations in names unless they are well known (e.g.,
URL or ID).
• Minimize Memory Usage: Avoid holding unnecessary references to objects, and prefer
primitive types over wrapper classes when possible to save memory.
• Use StringBuilder for String Concatenation: Instead of concatenating strings in a loop
(which creates multiple objects), use StringBuilder or StringBuffer to efficiently
append strings.
• Example:
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" ");
sb.append("World");
• Avoid Repeated Calculations: Cache the result of expensive operations (like loops or
database queries) if the result is reused multiple times.
• Use Streams Efficiently: When using Java Streams, avoid performing unnecessary
intermediate operations or excessive parallelization, as it can add overhead.
• Close Resources Properly: Always close resources like file streams, database connections,
etc., in a finally block or use try-with-resources to ensure they are closed automatically.
3. Writing Testable Code (Unit Testing with JUnit):
• Write Small, Focused Methods: Each method should perform a single task. This makes
testing easier and ensures that methods are reusable.
• Use Dependency Injection: Instead of hardcoding dependencies in classes, inject them via
constructors or setters. This makes it easier to test individual components.
• Use Mocking for External Dependencies: When unit testing, use mocking frameworks like
Mockito to simulate external services, databases, etc.
@InjectMocks
UserService userService;
@Test
void testAddUser() {
when(databaseService.saveUser(any(User.class))).thenReturn(true);
assertTrue(userService.addUser(new User()));
}
• Write Tests for All Logic: Ensure that all logic, especially complex conditions and loops, is
tested. Also, test edge cases.
• Test-Driven Development (TDD): Consider adopting TDD, where you write tests first, then
write code to pass those tests. This ensures code is always written with testability in mind.
• Use JUnit 5 for Unit Testing: JUnit 5 provides a more powerful and flexible testing
framework. Use annotations like @Test, @BeforeEach, @AfterEach,
@ParameterizedTest for efficient testing.
• Example:
@Test
void testAddition() {
int result = calculator.add(2, 3);
assertEquals(5, result);
}
• Hard-Coding Values: Avoid hard-coding values such as file paths, URLs, or configuration
settings. Use constants or configuration files instead.
• Ignoring Exception Handling: Always handle exceptions appropriately. Avoid catching
generic exceptions and provide specific error messages where possible.
• Overusing Static Methods: Overusing static methods can make code harder to test and
maintain. Prefer instance methods when possible, especially in large projects.
• Premature Optimization: Don't optimize code before it's necessary. Focus on writing
clean, readable code first, and optimize only when performance is proven to be a concern.
• Ignoring Code Comments: While you should strive for clean and readable code, don't
neglect documentation where needed. Use comments to explain the reasoning behind
complex logic or design decisions.
• Failing to Follow the DRY Principle: Don’t Repeat Yourself. If you find yourself writing
the same code multiple times, refactor it into reusable methods or classes.
14. Hands-on Projects
1. Build a Calculator:
Objective: Build a simple calculator that performs basic arithmetic operations like addition,
subtraction, multiplication, and division.
Steps:
1. Create a User Interface (UI): Use a Java GUI framework like Swing or JavaFX to
design the calculator UI with buttons for numbers (0-9) and operations (+, -, *, /).
2. Handle User Input: Implement event listeners for buttons to capture user input and display
the result.
3. Perform Calculations: Write methods to handle each arithmetic operation. Store the input
and result, update the display as necessary.
4. Handle Errors: Implement error handling, such as division by zero or invalid input.
Example:
// A simple example of addition operation
public class Calculator {
private double num1, num2, result;
Steps:
1. Set Up Spring Boot Project: Use Spring Initializr to create a new Spring Boot project with
dependencies like Spring Web and Spring Data JPA.
2. Create a User Model: Define a User entity class with properties like id, name, and
email.
3. Create a Repository: Use JpaRepository to handle database operations for the User
entity.
4. Create a Controller: Define a controller with RESTful endpoints to create, retrieve, update,
and delete users.
5. Run the Application: Start the Spring Boot application and test the API using Postman or a
similar tool.
Example:
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserRepository userRepository;
@PostMapping
public User createUser(@RequestBody User user) {
return userRepository.save(user);
}
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id).orElse(null);
}
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
user.setId(id);
return userRepository.save(user);
}
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
userRepository.deleteById(id);
}
}