What is Java?
Introduction
Java is a high-level, class-based, object-oriented programming language
that is designed to have as few implementation dependencies as possible.
The slogan “Write Once, Run Anywhere” reflects its capability to run on any device that
has a Java Virtual Machine (JVM).
Java was originally developed by Sun Microsystems and later acquired by Oracle Corporation. Over the years, Java became one of the most trusted programming languages for enterprise software, Android development, cloud applications, and large-scale systems.
Today, Java powers millions of applications worldwide — from banking systems and e-commerce platforms to desktop applications and backend APIs.
Why Learn Java?
- Ubiquitous – used in Android, enterprise back-ends, desktop apps and big-data ecosystems.
- Strong static typing helps catch many bugs at compile time.
- Rich standard library and a huge ecosystem of third-party libraries.
- Excellent tooling (IDEA, Eclipse, VS Code, Maven/Gradle, JUnit).
Java is also considered one of the best languages for beginners because its syntax is clean, structured, and widely documented. Learning Java builds a strong programming foundation that makes it easier to learn other languages later.
Where is Java Used?
- Android Development: Many Android applications use Java.
- Web Applications: Large-scale backend systems often use Java frameworks like Spring.
- Desktop Software: Java can build cross-platform desktop applications.
- Enterprise Systems: Banking, insurance, and large corporations rely heavily on Java.
- Cloud & Big Data: Technologies like Hadoop and Apache Kafka are built using Java.
Hello World – Your First Java Program
Save the following as HelloWorld.java and run it from the command line:
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } }
Compile and run:
javac HelloWorld.java # produces HelloWorld.class
java HelloWorld # prints “Hello, World!”
Understanding the Program
Let us break down the program line by line:
public class HelloWorld→ Defines a class namedHelloWorld.main()→ The entry point where program execution starts.System.out.println()→ Prints output to the console.String[] args→ Stores command-line arguments.
HelloWorld.java.
Install the latest JDK, create the HelloWorld.java file, compile it and run it.
Verify the output in your terminal.
Key Features of Java
- Object-Oriented: Everything in Java revolves around objects and classes.
- Platform Independent: Java code is compiled into bytecode that runs on the JVM.
- Secure: No direct pointer access and includes built-in security features.
- Robust: Strong memory management and exception handling.
- Multithreaded: Supports concurrent execution of two or more threads.
Additional Advantages of Java
- Automatic Garbage Collection: Java automatically manages memory allocation and cleanup.
- Large Community: Millions of developers contribute tutorials, libraries, and tools.
- Backward Compatibility: Older Java applications often continue to work on newer JVM versions.
- Performance: Modern JVM optimizations make Java extremely fast.
How Java Works
Java follows a two-step execution process:
- Compilation: The Java source file (
.java) is compiled into bytecode (.class) using the Java compiler (javac). - Execution: The JVM interprets or compiles the bytecode into machine code that your system can execute.
JDK vs JRE vs JVM
Beginners often confuse these three important Java components. Understanding them early will help you later.
- JVM (Java Virtual Machine) → Runs Java bytecode.
- JRE (Java Runtime Environment) → Contains the JVM and libraries needed to run Java programs.
- JDK (Java Development Kit) → Includes tools required to develop Java applications such as
javac.
JRE = Run Java Programs
JVM = Execute Java Bytecode
Basic Structure of a Java Program
- Class: The main building block of a Java program.
- Main Method: Entry point where execution begins.
- Statements: Instructions executed by the program.
Java Syntax Rules
- Java is case-sensitive.
- Every statement usually ends with a semicolon (
;). - Curly braces
{ }define blocks of code. - Program execution starts from the
main()method.
public class Example { public static void main(String[] args) { System.out.println("Learning Java"); } }
Common Errors Beginners Make
- Forgetting to match the file name with the class name.
- Missing semicolons (
;) at the end of statements. - Incorrect capitalization (Java is case-sensitive).
- Not installing or configuring the JDK properly.
system.out.println() instead of
System.out.println() will produce an error because Java is case-sensitive.
Best Practices for Beginners
- Practice writing code daily instead of only reading theory.
- Use meaningful class and variable names.
- Read compiler error messages carefully.
- Experiment with small programs frequently.
- Focus on understanding concepts instead of memorizing syntax.
Modify the HelloWorld program to print your name instead of "Hello, World!".
Try printing multiple lines using System.out.println() and observe how output is displayed.
Create a program named AboutMe.java that prints:
- Your name
- Your city
- Your favorite programming language
Summary
In this lecture, you learned what Java is, why it is widely used, and how to write and run your first Java program. You also explored its key features, execution process, Java components, and the basic structure of a Java application.
In the next lecture, we will dive deeper into setting up the development environment, installing the JDK correctly, and preparing your system for Java development.
Setting up the JDK
Introduction
Before writing larger Java applications, you need a proper Java development environment. This includes installing the JDK (Java Development Kit), configuring your system, and choosing a suitable code editor or IDE.
In this lecture, you will learn how to install Java correctly, verify the installation, and prepare your machine for Java development.
What is the JDK?
The Java Development Kit (JDK) is the complete package required to create, compile, and run Java applications.
The JDK includes:
- JVM (Java Virtual Machine) → Executes Java bytecode.
- JRE (Java Runtime Environment) → Provides libraries required to run programs.
- Java Compiler (
javac) → Converts Java source code into bytecode. - Development Tools → Debuggers, documentation generators, and more.
Download & Install
Head to Eclipse Adoptium or Oracle JDK and download the appropriate bundle for your operating system.
Choose the installer version that matches your system architecture:
- Windows: Usually
.msior.exe - macOS: Usually
.pkg - Linux: Usually
.tar.gzor package manager installation
Installing on Windows
- Download the JDK installer.
- Run the installer and follow the setup wizard.
- Allow the installer to configure environment variables if prompted.
- Finish installation and restart your terminal.
Installing on macOS
- Download the macOS package.
- Open the installer package.
- Follow the installation instructions.
- Open Terminal and verify the installation.
Installing on Linux
Many Linux distributions provide OpenJDK through package managers.
sudo apt update sudo apt install openjdk-21-jdk
Verify Installation
After installation, open your terminal or command prompt and run:
java -version
javac -version
Both commands should print the version number
(e.g. java 21.0.1) and indicate that the
JDK is correctly configured on your PATH.
Understanding PATH Variables
The PATH environment variable allows your operating system to locate Java commands
such as java and javac from any terminal window.
If the terminal says:
'java' is not recognized as an internal or external command
then Java may not be correctly added to your system PATH.
IDE Recommendation
For a smoother learning experience, choose a Java IDE or code editor:
- IntelliJ IDEA Community – Rich refactoring, smart code completion, debugging, and Maven/Gradle integration.
- Eclipse – A classic Java IDE with extensive plugin support.
-
VS Code –
Lightweight editor with the
Java Extension Pack.
Creating Your First Java Project
Once your IDE is installed:
- Create a new Java project.
- Create a new Java class named
Main. - Add a
main()method. - Run the program using the IDE's Run button.
public class Main { public static void main(String[] args) { System.out.println("Java setup successful!"); } }
Command Line vs IDE
Beginners often wonder whether they should use the terminal or an IDE. Both are useful and important.
- Command Line: Helps understand how Java compilation works internally.
- IDE: Speeds up development with autocomplete, debugging, and project tools.
Common Installation Problems
- Installing only the JRE instead of the full JDK
- Incorrect PATH configuration
- Using an outdated Java version
- Multiple Java versions conflicting on the same system
- Restarting neither the terminal nor the computer after installation
Useful Java Commands
java -version # Shows installed Java version javac -version # Shows Java compiler version java Main # Runs a compiled Java class javac Main.java # Compiles Java source code
Install your favourite IDE, open a new project, create a
Main.java file with a main method,
and run the “Hello World” program inside the IDE.
Open your terminal and run:
java -version
javac -version
Verify that both commands display version information correctly.
Create a Java program named EnvironmentCheck.java
that prints:
- Your Java version
- Your operating system
- Your user name
Research how to access system properties using System.getProperty().
Summary
In this lecture, you learned how to install the Java Development Kit, verify your installation, configure the PATH variable, and choose an IDE for Java development.
You also explored the differences between JDK, JRE, and JVM, along with common installation mistakes beginners encounter.
In the next lecture, we will begin working with variables, data types, and user input in Java.
Variables & Data Types
Introduction
Variables are one of the most fundamental concepts in programming. A variable is simply a named storage location used to hold data in memory.
In Java, every variable must have a specific data type. The data type determines:
- What kind of data can be stored
- How much memory is allocated
- What operations can be performed on the value
Declaring Variables
In Java, variables are declared using the following syntax:
dataType variableName = value;
Example:
int age = 25; double price = 99.99; String name = "Alice";
Primitive Types
Java has 8 primitive data types. Primitive types store simple values directly in memory.
| Type | Size | Example |
|---|---|---|
byte |
8 bit | byte b = 100; |
short |
16 bit | short s = 20000; |
int |
32 bit | int i = 123456; |
long |
64 bit | long l = 123456789L; |
float |
32 bit | float f = 3.14f; |
double |
64 bit | double d = 3.14159; |
char |
16 bit (Unicode) | char c = 'A'; |
boolean |
1 bit (logical) | boolean ok = true; |
Integer Types
Integer types are used to store whole numbers without decimal points.
byte→ Small memory usageshort→ Slightly larger integersint→ Most commonly used integer typelong→ Very large integer values
int for integers unless very large numbers are required.
Floating-Point Types
Floating-point types are used to store decimal numbers.
float→ Less precision, uses less memorydouble→ Higher precision, more commonly used
double by default.
Add f at the end when using a float.
float value = 3.14f; double pi = 3.1415926535;
Character Type
The char type stores a single Unicode character.
char grade = 'A'; char symbol = '#';
Boolean Type
The boolean type stores only two values:
true or false.
boolean isLoggedIn = true; boolean hasPermission = false;
Reference Types
Anything that is not a primitive type is called a reference type.
Reference types store memory addresses that point to objects.
Examples include:
String- Arrays
- Classes and objects
- Collections
Strings in Java
Strings are used to store text.
In Java, strings are objects of the String class.
String city = "London"; String message = "Welcome to Java";
VariablesDemo.java
public class VariablesDemo { public static void main(String[] args) { int age = 30; double price = 19.99; boolean isMember = true; String name = "Alice"; System.out.println(name + ", age " + age); } }
Type Conversion
Sometimes values must be converted from one data type to another.
Implicit Conversion
Java automatically converts smaller types into larger compatible types.
int num = 100; double result = num;
Explicit Conversion (Casting)
Larger types can be manually converted into smaller types using casting.
double value = 9.78; int result = (int) value;
9.78 to int removes the decimal part.
Variable Naming Rules
- Variable names are case-sensitive.
- Names cannot start with numbers.
- Spaces are not allowed.
- Use meaningful names for readability.
int studentAge = 20; double accountBalance = 1500.75;
Common Beginner Mistakes
- Using variables before initialization
- Assigning incorrect data types
- Forgetting quotes around strings
- Using single quotes for strings instead of double quotes
- Confusing
=with==
Create a program that declares one variable of each primitive type, prints them, then swaps two integer values without using a third temporary variable.
Create variables for:
- Your name
- Your age
- Your height
- Whether you are a student
Print all values using System.out.println().
Create a simple billing program that stores:
- Product name
- Price
- Quantity
Then calculate and print the total bill amount.
Summary
In this lecture, you learned about variables, primitive data types, reference types, type conversion, and variable naming rules.
Understanding data types is essential because every Java program relies heavily on storing and manipulating data correctly.
In the next lecture, we will explore operators and expressions in Java.
Control Flow
Introduction
Control flow determines how a program makes decisions and repeats tasks. Without control flow, Java programs would execute statements one after another without any logic or flexibility.
Using conditions and loops, programs can:
- Make decisions
- Repeat actions
- Handle different situations dynamically
- Process large amounts of data efficiently
Conditional Statements
Conditional statements allow programs to execute code only when certain conditions are true.
If / else
The if statement checks a condition.
If the condition evaluates to true, the code block executes.
int score = 85; if (score >= 90) { System.out.println("Grade A"); } else if (score >= 80) { System.out.println("Grade B"); } else { System.out.println("Needs improvement"); }
Understanding the Logic
if→ Executes when the first condition is trueelse if→ Checks additional conditionselse→ Executes if none of the conditions are true
Nested If Statements
An if statement can exist inside another if statement.
This is called nesting.
int age = 20; boolean hasTicket = true; if (age >= 18) { if (hasTicket) { System.out.println("Entry allowed"); } }
Comparison Operators
Conditions usually rely on comparison operators.
| Operator | Meaning |
|---|---|
== |
Equal to |
!= |
Not equal to |
> |
Greater than |
< |
Less than |
>= |
Greater than or equal to |
<= |
Less than or equal to |
Logical Operators
Logical operators combine multiple conditions.
| Operator | Description |
|---|---|
&& |
Logical AND |
|| |
Logical OR |
! |
Logical NOT |
int age = 22; boolean isStudent = true; if (age >= 18 && isStudent) { System.out.println("Eligible for student discount"); }
Switch Statement
The switch statement is useful when comparing one value against multiple possible cases.
Switch (Java 17+ enhanced switch)
String day = "MONDAY"; String result = switch (day) { case "MONDAY", "TUESDAY", "WEDNESDAY" -> "Mid-week"; case "THURSDAY", "FRIDAY" -> "Almost weekend"; case "SATURDAY", "SUNDAY" -> "Weekend"; default -> "Invalid day"; }; System.out.println(result);
Loops
Loops allow code to repeat automatically until a condition becomes false.
- for loop → Best for counting and fixed repetitions
- enhanced for loop → Best for arrays and collections
- while loop → Repeats while a condition is true
- do-while loop → Executes at least once before checking the condition
For Loop
for (int i = 1; i <= 5; i++) { System.out.println(i); }
Enhanced For Loop
Enhanced for loops simplify iteration through arrays and collections.
int[] numbers = { 2, 4, 6, 8 }; int sum = 0; for (int n : numbers) { sum += n; } System.out.println("Sum = " + sum);
While Loop
A while loop executes as long as its condition remains true.
int count = 1; while (count <= 5) { System.out.println(count); count++; }
Do-While Loop
A do-while loop executes the block first,
then checks the condition.
int number = 1; do { System.out.println(number); number++; } while (number <= 3);
Break and Continue
Java provides special statements to control loop execution.
break→ Exits the loop immediatelycontinue→ Skips the current iteration
for (int i = 1; i <= 10; i++) { if (i == 5) { continue; } if (i == 8) { break; } System.out.println(i); }
Common Beginner Mistakes
- Using
=instead of==in conditions - Creating infinite loops accidentally
- Forgetting curly braces in nested conditions
- Using incorrect loop conditions
- Confusing
breakandcontinue
Write a program that prints the first 20 Fibonacci numbers using a
while loop.
Write a program that checks whether a number is even or odd using
an if-else statement.
Create a multiplication table generator using a for loop.
Create a number guessing game where:
- The program stores a secret number
- The user repeatedly guesses the number
- The program prints whether the guess is too high or too low
- The loop ends when the correct number is guessed
Summary
In this lecture, you learned how Java programs make decisions and repeat tasks using conditions and loops.
You explored:
if-elsestatementsswitchexpressionsfor,while, anddo-whileloopsbreakandcontinuestatements
In the next lecture, we will begin working with methods and modular programming in Java.
Methods
Introduction
Methods are reusable blocks of code that perform specific tasks. They help organize programs into smaller, manageable pieces.
Instead of writing the same code repeatedly, developers create methods and call them whenever needed.
Why Methods Matter
- Reduce code duplication
- Improve readability
- Make debugging easier
- Encourage modular programming
- Allow code reuse across projects
Method Syntax
A method in Java consists of:
modifiers returnType name(parameters) { body }
accessModifier returnType methodName(parameters) { // method body }
Understanding Method Components
| Component | Description |
|---|---|
public |
Access modifier |
static |
Belongs to the class instead of an object |
int |
Return type |
add |
Method name |
(int a, int b) |
Parameters |
Creating Methods
public class MathUtils { public static int add(int a, int b) { return a + b; } public static double average(int[] values) { int sum = 0; for (int v : values) { sum += v; } return (double) sum / values.length; } }
Calling Methods
Methods do nothing until they are called.
int result = MathUtils.add(5, 10); System.out.println(result);
Return Values
Methods can return values back to the caller using the
return keyword.
The return type determines what type of value the method sends back.
void return type.
public static void greet() { System.out.println("Welcome!"); }
Method Parameters
Parameters allow methods to receive input values.
public static void displayName(String name) { System.out.println("Hello " + name); }
Method Arguments vs Parameters
| Term | Meaning |
|---|---|
| Parameter | Variable in the method definition |
| Argument | Actual value passed to the method |
Parameter Passing
- Primitives are passed by value — the method receives a copy.
- Objects are passed by reference value — the method receives a copy of the reference, allowing it to modify the object.
Method Overloading
Method overloading allows multiple methods to share the same name but use different parameter lists.
Java identifies overloaded methods based on:
- Number of parameters
- Parameter types
- Order of parameters
public class OverloadDemo { static int max(int a, int b) { return (a > b) ? a : b; } static double max(double a, double b) { return (a > b) ? a : b; } }
Recursive Methods
A recursive method is a method that calls itself.
Recursion is commonly used for mathematical problems, trees, and algorithms.
public static int factorial(int n) { if (n == 1) { return 1; } return n * factorial(n - 1); }
Scope of Variables
Variables declared inside methods are called local variables.
They can only be accessed within that method.
public static void test() { int number = 10; System.out.println(number); }
Main Method
Every Java application starts execution from the
main() method.
public static void main(String[] args) { System.out.println("Program started"); }
Common Beginner Mistakes
- Forgetting the return statement
- Using the wrong return type
- Calling non-static methods incorrectly
- Passing incorrect arguments
- Creating recursive methods without a base case
Write a utility class that provides overloaded
factorial methods for int
and long.
Add a simple main method that demonstrates both.
Create a method called isEven()
that returns true if a number is even,
otherwise returns false.
Write a calculator program that uses separate methods for:
- Addition
- Subtraction
- Multiplication
- Division
Build a menu-driven banking simulation using methods.
Include methods for:
- Deposit
- Withdraw
- Check balance
- Exit program
Summary
In this lecture, you learned how methods help organize and reuse code in Java.
You explored:
- Method syntax
- Parameters and return values
- Method overloading
- Recursion
- Variable scope
Methods are one of the most important building blocks in Java programming. They are heavily used in every real-world application.
In the next lecture, we will begin learning Object-Oriented Programming (OOP) concepts in Java.
Object-Oriented Programming
Introduction
Object-Oriented Programming (OOP) is a programming paradigm based on the concept of objects.
Objects combine both:
- Data → variables and fields
- Behavior → methods and actions
Java is primarily an object-oriented programming language, and most real-world Java applications are built using OOP principles.
Why OOP Matters
- Improves code organization
- Encourages code reuse
- Makes applications easier to maintain
- Supports scalable software development
- Helps teams collaborate efficiently
Core Pillars
- Encapsulation – hide internal state behind private fields and expose behaviour via methods.
-
Inheritance –
reuse code with
extends. - Polymorphism – treat objects of different subclasses uniformly, often via interfaces.
- Abstraction – define contracts with abstract classes or interfaces.
Classes and Objects
A class is a blueprint used to create objects.
An object is an instance of a class.
Class definition
public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } public void birthday() { age++; } }
Creating Objects
Objects are created using the new keyword.
Person person = new Person("Alice", 25); System.out.println(person.getName());
Encapsulation
Encapsulation means hiding internal data and controlling access through methods.
In Java, this is commonly done using:
privatefields- Getter methods
- Setter methods
Getter and Setter Methods
private double balance; public double getBalance() { return balance; } public void setBalance(double value) { if (value >= 0) { balance = value; } }
Constructors
Constructors are special methods used to initialize objects.
A constructor:
- Has the same name as the class
- Does not have a return type
- Runs automatically when an object is created
Inheritance
Inheritance allows one class to inherit properties and methods from another class.
The child class uses the extends keyword.
Inheritance example
public class Employee extends Person { private String department; public Employee(String name, int age, String dept) { super(name, age); department = dept; } public String getDepartment() { return department; } }
The super Keyword
The super keyword refers to the parent class.
It is commonly used to:
- Call parent constructors
- Access parent methods
- Reuse inherited functionality
Method Overriding
Method overriding allows subclasses to provide their own implementation of inherited methods.
class Animal { void sound() { System.out.println("Animal sound"); } } class Dog extends Animal { @Override void sound() { System.out.println("Bark"); } }
Polymorphism
Polymorphism allows one interface or parent class reference to represent many different object types.
Animal pet = new Dog(); pet.sound();
Abstraction
Abstraction focuses on hiding implementation details and showing only essential behavior.
Java supports abstraction using:
- Abstract classes
- Interfaces
Abstract Class Example
abstract class Shape { abstract double area(); }
Interface Example
interface Vehicle { void start(); }
Access Modifiers
| Modifier | Accessibility |
|---|---|
public |
Accessible everywhere |
private |
Accessible only within the same class |
protected |
Accessible in subclasses and same package |
| default | Accessible only within the same package |
Composition vs Inheritance
Inheritance represents an "is-a" relationship, while composition represents a "has-a" relationship.
Example:
- A Dog is an Animal → inheritance
- A Car has an Engine → composition
Common Beginner Mistakes
- Making everything public
- Confusing classes and objects
- Forgetting constructors
- Using inheritance unnecessarily
- Ignoring encapsulation principles
Model a simple library system:
create a base class Item
(with id, title) and subclasses
Book and Magazine.
Add a method printInfo()
that is overridden in each subclass.
Create a class called BankAccount
with:
- Private balance field
- Deposit method
- Withdraw method
- Getter method for balance
Create an abstract class called Employee
and subclasses:
DeveloperManager
Override a method called work().
Build a mini school management system using OOP concepts.
Include:
- Student class
- Teacher class
- Course class
- Inheritance and polymorphism
- Encapsulation using private fields
Summary
In this lecture, you explored the foundations of Object-Oriented Programming in Java.
You learned about:
- Classes and objects
- Encapsulation
- Inheritance
- Polymorphism
- Abstraction
- Constructors and access modifiers
OOP is one of the most important concepts in modern software development and is heavily used in enterprise Java applications, Android development, and backend systems.
In the next lecture, we will begin working with collections and advanced data handling in Java.
Collections Framework
Introduction to the Collections Framework
The Java Collections Framework is a powerful set of classes and interfaces used to store, organize, and manage groups of objects efficiently. Instead of manually creating arrays and complex data structures, Java provides ready-made implementations for common programming needs.
The framework includes dynamic lists, sets, maps, queues, iterators, sorting utilities, and algorithms that are optimized and widely used in real-world software development.
Why Collections?
They provide well-tested, efficient implementations of common data structures (lists, sets, maps) plus a rich set of utility methods.
- Dynamic sizing unlike fixed-size arrays
- Built-in algorithms for sorting and searching
- Improved code readability
- Reusable and optimized implementations
- Support for generics and type safety
Collection Hierarchy Overview
The Collections Framework is built using interfaces and classes.
Collection
├── List
│ ├── ArrayList
│ └── LinkedList
│
├── Set
│ ├── HashSet
│ └── TreeSet
│
└── Queue
└── PriorityQueue
Map
├── HashMap
└── TreeMap
Different collection types are designed for different use cases.
Generics in Collections
Collections usually use Generics to specify the type of data they store.
ArrayList<String> names = new ArrayList<>();
This ensures type safety and prevents storing incorrect object types.
List – ordered, duplicates allowed
A List stores elements in insertion order and allows duplicate values.
import java.util.*; // List, ArrayList public class ListDemo { public static void main(String[] args) { List<String> names = new ArrayList<>(); names.add("Alice"); names.add("Bob"); names.add("Alice"); // duplicate allowed for (String n : names) { System.out.println(n); } } }
Common List Methods
names.add("Charlie");
names.remove("Bob");
names.get(0);
names.size();
names.contains("Alice");
| Method | Purpose |
|---|---|
| add() | Adds an element |
| remove() | Removes an element |
| get() | Accesses an element by index |
| size() | Returns collection size |
| contains() | Checks if an element exists |
ArrayList vs LinkedList
| ArrayList | LinkedList |
|---|---|
| Fast random access | Fast insertions/removals |
| Uses dynamic arrays | Uses linked nodes |
| Better for reading data | Better for modifying data frequently |
Set – unique elements
A Set stores unique elements and automatically removes duplicates.
import java.util.*; // HashSet public class SetDemo { public static void main(String[] args) { Set<Integer> primes = new HashSet<>(); primes.add(2); primes.add(3); primes.add(2); // ignored – duplicate System.out.println(primes); } }
HashSet vs TreeSet
| HashSet | TreeSet |
|---|---|
| Unordered | Sorted automatically |
| Faster operations | Slightly slower |
| Uses hashing | Uses tree structure |
Map – key/value pairs
A Map stores data as key-value pairs.
import java.util.*; // Map, HashMap public class MapDemo { public static void main(String[] args) { Map<String, Integer> scores = new HashMap<>(); scores.put("Alice", 90); scores.put("Bob", 85); scores.put("Alice", 95); // overwrites previous System.out.println(scores); } }
Common Map Methods
scores.put("Charlie", 88);
scores.get("Alice");
scores.remove("Bob");
scores.containsKey("Alice");
scores.containsValue(95);
HashMap vs TreeMap
| HashMap | TreeMap |
|---|---|
| Unordered keys | Sorted keys |
| Faster performance | Maintains natural ordering |
| Uses hashing | Uses balanced trees |
Iterating Through Collections
Collections can be traversed using loops or iterators.
for (String name : names) {
System.out.println(name);
}
Enhanced for-loops are cleaner and easier to read.
Sorting Collections
The Collections utility class provides sorting methods.
Collections.sort(names);
You can also sort in reverse order:
Collections.sort(names, Collections.reverseOrder());
Queue Interface
A Queue processes elements in FIFO (First In First Out) order.
Queue<Integer> queue = new LinkedList<>();
queue.add(10);
queue.add(20);
System.out.println(queue.poll());
poll() removes and returns the first element.
Collections Utility Class
The Collections class provides useful helper methods.
Collections.max(numbers);
Collections.min(numbers);
Collections.shuffle(numbers);
Common Beginner Mistakes
- Using raw collections without generics
- Choosing the wrong collection type
- Forgetting that Sets remove duplicates
- Accessing invalid indexes in Lists
- Confusing HashMap ordering behavior
Implement a simple phone-book using a HashMap<String, String> where the key is a
person’s name and the value is a phone number. Add methods to add, lookup and remove entries.
Create an ArrayList<Integer> and write a program that sorts numbers in ascending and
descending order.
Create a HashSet<String> to store usernames and prevent duplicate registrations.
Generics
Introduction to Generics
Generics are one of Java’s most powerful features. They allow classes, interfaces, and methods to work with different data types while maintaining type safety.
Before Generics were introduced in Java 5, developers had to store objects using the Object class,
which required manual casting and increased the chance of runtime errors.
Why Generics?
They let you write type-safe collections and APIs without casting.
- Improves code reusability
- Eliminates unnecessary type casting
- Provides compile-time type checking
- Makes code cleaner and easier to maintain
- Used heavily in the Collections Framework
Problem Without Generics
Before Generics, collections stored data as Object types.
import java.util.*; public class WithoutGenerics { public static void main(String[] args) { ArrayList list = new ArrayList(); list.add("Hello"); list.add(100); String text = (String) list.get(0); System.out.println(text); } }
This approach is unsafe because incorrect casting can cause runtime exceptions.
Using Generics Properly
With Generics, you specify the exact type of data allowed.
import java.util.*; public class GenericList { public static void main(String[] args) { ArrayList<String> names = new ArrayList<>(); names.add("Alice"); names.add("Bob"); String first = names.get(0); System.out.println(first); } }
Now Java automatically ensures only String values can be stored.
Generic Type Parameters
Generic classes use placeholder type names inside angle brackets <>.
| Type Parameter | Meaning |
|---|---|
| T | Type |
| E | Element |
| K | Key |
| V | Value |
| N | Number |
Simple generic class
public class Box<T> { private T value; public Box(T value) { this.value = value; } public T get() { return value; } public void set(T value) { this.value = value; } }
Creating Generic Objects
Box<String> textBox = new Box<>("Hello");
Box<Integer> numberBox = new Box<>(100);
System.out.println(textBox.get());
System.out.println(numberBox.get());
The same class works with multiple data types.
Generic Methods
Methods can also use Generics independently of the class.
public class GenericMethod { public static <T> void printData(T value) { System.out.println(value); } public static void main(String[] args) { printData("Java"); printData(500); printData(3.14); } }
Using bounded type parameters
Restrict a type to subclasses of a given class or interface.
public class NumberUtils { public static <T extends Number> double sum(List<T> list) { double total = 0; for (T n : list) { total += n.doubleValue(); } return total; } }
This method only accepts numeric types such as Integer, Double, or
Float.
Wildcards in Generics
Wildcards make Generics more flexible.
List<?> items;
The ? symbol means “unknown type”.
| Wildcard | Description |
|---|---|
| ? | Any type |
| ? extends Number | Number or subclass |
| ? super Integer | Integer or parent class |
Generic Interfaces
Interfaces can also use Generics.
interface Storage<T> {
void save(T item);
T load();
}
Type Erasure
Java implements Generics using a concept called Type Erasure.
During compilation, generic type information is removed and converted into regular Java bytecode. This means Generics provide compile-time safety but do not exist at runtime.
new T() directly inside generic classes.
Common Beginner Mistakes
- Using raw types instead of generic types
- Forgetting angle brackets
<> - Using incorrect type parameters
- Mixing incompatible generic types
- Assuming Generics exist at runtime
Real-World Usage of Generics
Generics are heavily used throughout modern Java development.
- Collections Framework
- Spring Boot applications
- REST APIs
- Database frameworks like Hibernate
- Reusable utility libraries
Create a generic Pair<K,V> class that stores two values. Write a short demo that
creates a Pair<String,Integer> and prints both elements.
Create a generic method called display() that prints arrays of different types such as
integers, strings, and doubles.
Create a generic class called Storage<T> that stores and retrieves one object of any
type.
Streams & Lambdas (Java 8+)
Introduction to Functional Programming in Java
Java 8 introduced one of the biggest upgrades in Java history: Lambda Expressions and the Stream API. These features allow developers to write cleaner, shorter, and more expressive code using a functional programming style.
Instead of manually writing loops and temporary variables, Streams allow operations such as filtering, mapping, sorting, and collecting data in a concise pipeline.
What is a Lambda Expression?
A lambda expression is an anonymous function that can be passed as data.
It allows you to write compact code without creating separate classes for simple operations.
Lambda syntax
A lambda expression is an anonymous function that can be treated as an instance of a functional interface.
import java.util.function.*; public class LambdaDemo { public static void main(String[] args) { Predicate<Integer> isEven = n -> n % 2 == 0; System.out.println( isEven.test(42) ); // true } }
Understanding Lambda Structure
(parameters) -> expression
| Part | Purpose |
|---|---|
| (parameters) | Input values |
| -> | Lambda operator |
| expression | Code to execute |
More Lambda Examples
() -> System.out.println("Hello")
x -> x * x
(a, b) -> a + b
Lambdas become especially powerful when combined with collections and streams.
Functional Interfaces
A lambda expression works with a functional interface, which is an interface containing only one abstract method.
@FunctionalInterface
interface Calculator {
int operate(int a, int b);
}
You can then provide implementations using lambdas:
Calculator add = (a, b) -> a + b;
System.out.println(add.operate(5, 3));
Common Built-in Functional Interfaces
| Interface | Purpose |
|---|---|
| Predicate<T> | Returns true or false |
| Function<T,R> | Transforms data |
| Consumer<T> | Consumes data without returning |
| Supplier<T> | Provides data |
What is the Stream API?
The Stream API processes collections of data in a declarative and functional way.
Streams do not store data themselves — they operate on existing collections like lists and sets.
Stream pipeline
Streams enable functional-style processing of sequences of elements.
import java.util.stream.*; import java.util.*; public class StreamDemo { public static void main(String[] args) { List<Integer> numbers = Arrays.asList( 1, 2, 3, 4, 5 ); int sum = numbers.stream() .filter(n -> n % 2 == 0) .mapToInt(n -> n * n) .sum(); System.out.println( "Result = " + sum ); } }
Understanding Stream Operations
| Operation | Purpose |
|---|---|
| filter() | Select matching elements |
| map() | Transform elements |
| sorted() | Sort elements |
| collect() | Store results |
| forEach() | Iterate through elements |
Filtering Data
List<Integer> nums = Arrays.asList(1,2,3,4,5,6);
nums.stream()
.filter(n -> n % 2 == 0)
.forEach(System.out::println);
The filter() method keeps only matching elements.
Mapping Data
The map() method transforms each element.
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
Sorting Streams
numbers.stream()
.sorted()
.forEach(System.out::println);
You can also use custom comparators with lambdas.
numbers.stream()
.sorted((a, b) -> b - a)
.forEach(System.out::println);
Collecting Results
The collect() method converts stream results into collections.
List<String> result = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
Method References
Method references are a shorter form of lambdas.
names.forEach(System.out::println);
This is equivalent to:
names.forEach(name -> System.out.println(name));
Stream Pipeline Stages
A stream pipeline usually has three stages:
- Source → collection or array
- Intermediate operations → filter, map, sorted
- Terminal operation → collect, sum, forEach
Advantages of Streams
- Cleaner and shorter code
- Less manual looping
- Easier data transformations
- Supports parallel processing
- Improved readability
Parallel Streams
Java can process streams in parallel using multiple CPU cores.
numbers.parallelStream()
.forEach(System.out::println);
Common Beginner Mistakes
- Trying to reuse a stream after a terminal operation
- Using streams for extremely simple tasks unnecessarily
- Confusing map() with filter()
- Overusing parallel streams
- Writing overly complex lambda expressions
Real-World Usage
Streams and Lambdas are heavily used in modern Java applications.
- Spring Boot applications
- REST APIs
- Data processing systems
- Backend enterprise software
- Big-data pipelines
Read a list of words from an array, convert them to uppercase, filter those longer than 5 characters, sort them alphabetically and print each on its own line — all using the Stream API.
Create a list of integers and use streams to calculate the sum of all odd numbers.
Create a lambda expression that checks whether a number is positive, negative, or zero.
Exception Handling
Checked vs Unchecked
- Checked – compiler forces you to handle or declare (
IOException,SQLException). - Unchecked – subclasses of
RuntimeException(e.g.NullPointerException) need not be declared.
Try‑catch‑finally
import java.io.*; public class ExceptionDemo { public static void main(String[] args) { try { BufferedReader br = new BufferedReader(new FileReader("missing.txt")); System.out.println(br.readLine()); } catch (FileNotFoundException e) { System.err.println("File not found!"); } catch (IOException e) { e.printStackTrace(); } finally { System.out.println("Cleanup if necessary"); } } }
Custom exception
public class InvalidAgeException extends Exception { public InvalidAgeException(String message) { super(message); } }
Create a BankAccount class with a withdraw(double amount) method.
Throw a custom InsufficientFundsException when the balance would become negative,
and write a driver program that catches and reports the error.
Concurrency
Threads basics
Two ways to create a thread:
- Extend
Threadand overriderun(). - Implement
Runnable(orCallable) and pass it to aThreador anExecutorService.
class MyTask implements Runnable { public void run() { for (int i = 0; i < 5; i++) { System.out.println("Thread " + Thread.currentThread().getName() + ": " + i); try { Thread.sleep(500); } catch (InterruptedException e) { } } } } public class ThreadDemo { public static void main(String[] args) { Thread t1 = new Thread(new MyTask(), "Worker‑1"); Thread t2 = new Thread(new MyTask(), "Worker‑2"); t1.start(); t2.start(); } }
Executor framework
Higher‑level API for managing thread pools.
import java.util.concurrent.*; public class ExecutorDemo { public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(3); for (int i = 0; i < 10; i++) { int id = i; pool.submit(() -> { System.out.println("Task " + id + " executed by " + Thread.currentThread().getName()); }); } pool.shutdown(); } }
Implement a simple producer‑consumer pipeline using a BlockingQueue and two threads
(one producing numbers, the other consuming and printing them). Use an ExecutorService
to run both.
Capstone Project – Console To‑Do List
Build a **single‑file** console application that lets the user manage a to‑do list. The program should let you:
- Add a task (string description).
- Mark a task as completed.
- Delete a task.
- Display all tasks with status.
- Persist tasks to a plain‑text file on exit and reload them on start.
Suggested Architecture
- Model – a
Taskclass withid,descriptionandcompletedflag. - Repository – a
List<Task>stored in memory; useFilesAPI for persistence. - Service – static methods for add/save/load/delete.
- UI loop – a
while(true)menu that reads user input withScanner.
Starter Code (you’ll flesh it out)
import java.util.*; import java.nio.file.*; class Task { private static int counter = 1; private final int id; private String description; private boolean done; public Task(String description) { this.id = counter++; this.description = description; this.done = false; } public int getId() { return id; } public String getDescription() { return description; } public boolean isDone() { return done; } public void toggle() { done = !done; } public String serialize() { return id + "," + description.replaceAll(",", "\\,") + "," + done; } public static Task deserialize(String line) { String[] parts = line.split(",", 3); Task t = new Task(parts[1]); t.id = Integer.parseInt(parts[0]); // reset id t.done = Boolean.parseBoolean(parts[2]); return t; } } public class TodoApp { private static final Path FILE = Paths.get("tasks.txt"); private static final List<Task> tasks = new ArrayList<>(); public static void main(String[] args) throws Exception { load(); Scanner sc = new Scanner(System.in); while (true) { System.out.println("\n--- To‑Do List ---"); tasks.forEach(t -> System.out.println(t.getId() + ". [" + (t.isDone() ? "x" : " ") + "] " + t.getDescription())); System.out.println("\nChoose: (a)dd, (t)oggle, (d)elete, (q)uit"); String cmd = sc.nextLine().trim().toLowerCase(); switch (cmd) { case "a": System.out.print("Enter description: "); String desc = sc.nextLine(); tasks.add(new Task(desc)); break; case "t": System.out.print("Id to toggle: "); int id = Integer.parseInt(sc.nextLine()); tasks.stream() .filter(t -> t.getId() == id) .findFirst() .ifPresent(Task::toggle); break; case "d": System.out.print("Id to delete: "); int delId = Integer.parseInt(sc.nextLine()); tasks.removeIf(t -> t.getId() == delId); break; case "q": save(); System.out.println("Good‑bye!"); return; default: System.out.println("Unknown command"); } } } private static void load() throws Exception { if (Files.exists(FILE)) { Files.lines(FILE).forEach(l -> tasks.add(Task.deserialize(l))); } } private static void save() throws Exception { List<String> lines = new ArrayList<>(); tasks.forEach(t -> lines.add(t.serialize())); Files.write(FILE, lines); } }
What You’ll Demonstrate
- Class design & encapsulation.
- Collections (ArrayList) and streaming operations.
- File I/O with NIO.
- Basic console UI handling.
- Exception handling for I/O and parsing errors.
Extend the app with:
- A priority field (enum
LOW/MEDIUM/HIGH) and sort the output by priority. - Use
java.time.LocalDateto add a due‑date, and display overdue tasks. - Write JUnit tests for the
Taskclass and the persistence logic.
Interfaces & Abstract Classes
Content coming soon...
Enums & Annotations
Content coming soon...
File I/O & NIO
Content coming soon...
Networking & Sockets
Content coming soon...
JDBC & Databases
Content coming soon...
Unit Testing (JUnit)
Content coming soon...
Design Patterns
Content coming soon...
Build Tools (Maven & Gradle)
Content coming soon...
Introduction to Spring Boot
Content coming soon...
Final Project — Enterprise Java App
Content coming soon...