Author | Nejat Hakan |
nejat.hakan@outlook.de | |
PayPal Me | https://paypal.me/nejathakan |
Java programming language
Introduction
Welcome to the world of Java programming on the Linux platform! Java is a powerful, versatile, object-oriented programming language known for its "Write Once, Run Anywhere" (WORA) philosophy. This means compiled Java code can run on any platform that supports Java without needing recompilation, making it exceptionally suitable for the diverse landscape of Linux distributions and server environments.
What is Java?
Java was developed by Sun Microsystems (now owned by Oracle) and first released in 1995. Its design goals included simplicity, object-orientation, robustness, security, and platform independence. Key components enable this:
- Java Virtual Machine (JVM): An abstract computing machine that enables a computer to run Java programs. The JVM interprets compiled Java bytecode (intermediate language) and translates it into native machine code for the underlying operating system and hardware. This abstraction layer is the core of Java's platform independence. The JVM manages memory (garbage collection) and provides a secure environment. Different vendors offer JVM implementations optimized for various platforms, including numerous Linux distributions.
- Java Runtime Environment (JRE): The software package that contains what is required to run Java programs. It includes the JVM, Java class libraries (the standard library providing pre-built functionalities like I/O, networking, data structures, etc.), and other components necessary for execution. If you only need to run Java applications on your Linux system, installing the JRE is sufficient.
- Java Development Kit (JDK): The superset of the JRE. The JDK includes everything in the JRE plus development tools necessary for developing Java applications. These tools include the Java compiler (
javac
), the Java archiver (jar
), the Java debugger (jdb
), documentation generator (javadoc
), and other utilities. As a developer learning Java on Linux, you will need to install the JDK.
Why Learn Java on Linux?
Linux is a dominant operating system in server environments, cloud computing, and embedded systems, areas where Java is heavily utilized. Learning Java on Linux provides several advantages:
- Industry Relevance: Many enterprise applications, big data processing frameworks (like Hadoop and Spark), and Android applications are built using Java and often deployed on Linux servers.
- Open Source Ecosystem: Both Linux and much of the Java ecosystem (like OpenJDK, Apache libraries, build tools like Maven and Gradle) are open source, fostering a collaborative and accessible development environment.
- Powerful Command Line: Linux provides a powerful command-line interface (CLI) which integrates well with Java development tools, build systems, and version control systems like Git.
- Customization and Control: Linux offers deep control over the system environment, allowing developers to fine-tune performance and manage dependencies effectively.
Setting up the Java Development Environment on Linux
Before writing any Java code, you need to install the JDK on your Linux system. You generally have two main choices for JDK distributions: OpenJDK (the open-source reference implementation) and Oracle JDK (a commercial build from Oracle, though often free for development use). For most learning and development purposes, OpenJDK is excellent and readily available in most Linux distribution repositories.
Installation Steps (using package managers):
The exact package names might vary slightly between distributions and versions. Always prefer installing from your distribution's official repositories for better integration and easier updates.
-
Update Package Lists: Open your terminal and refresh your package manager's list of available packages.
- On Debian/Ubuntu-based systems (like Mint, Pop!_OS):
- On Fedora/CentOS/RHEL-based systems:
-
Search for Available JDK Packages: You can search to see which versions are available. We typically want a recent Long-Term Support (LTS) version like JDK 11, 17, or 21, or the latest stable release.
- On Debian/Ubuntu:
- On Fedora/CentOS/RHEL:
-
Install the Chosen JDK: Install the desired JDK version. Let's assume you choose OpenJDK 17.
- On Debian/Ubuntu:
- On Fedora/CentOS/RHEL: Follow the prompts to complete the installation.
-
Verify Installation: Check if Java is installed correctly and which version is active.
Both commands should output the version number you installed (e.g., OpenJDK 17.x.x). Ifjavac -version
gives an error butjava -version
works, you might have only installed the JRE, not the full JDK (ensure you installed the-jdk
or-devel
package). -
Managing Multiple Java Versions (Optional but common): Linux systems often provide tools to manage multiple installed Java versions.
- On Debian/Ubuntu: Use
update-alternatives
. - On Fedora/CentOS/RHEL: Use
alternatives
.
- On Debian/Ubuntu: Use
-
Setting the
JAVA_HOME
Environment Variable (Recommended): Many Java-based tools (like Maven, Gradle, Tomcat) rely on theJAVA_HOME
environment variable to locate the JDK installation directory. While not always strictly necessary if using alternatives correctly, it's good practice to set it.- Find the JDK installation path. This often involves following the symbolic links managed by
alternatives
. A common way is: - Edit your shell configuration file. For Bash (the default shell on most Linux systems), this is usually
~/.bashrc
. Open it with a text editor (likenano
,vim
, orgedit
): - Add the following line at the end, replacing the path with the one you found:
export JAVA_HOME=...
: Sets the variable.export PATH=$PATH:$JAVA_HOME/bin
: Adds the JDK'sbin
directory (containingjavac
,java
, etc.) to your system's execution path, ensuring the commands are found directly. This might be redundant ifupdate-alternatives
is correctly configured, but it doesn't hurt.
- Save the file (e.g., in
nano
, pressCtrl+X
, thenY
, thenEnter
). - Apply the changes to your current terminal session: Or simply close and reopen your terminal.
- Verify
JAVA_HOME
is set: This should print the path you set.
- Find the JDK installation path. This often involves following the symbolic links managed by
You now have a fully functional Java Development Kit installed and configured on your Linux system, ready to compile and run Java applications.
Workshop Install JDK and Run a Simple Hello Linux Program
This workshop guides you through compiling and running your very first Java program directly from the Linux terminal.
Objective: Verify your JDK installation by creating, compiling, and running a basic "Hello, Linux!" Java program.
Steps:
-
Open Your Terminal: Launch your preferred terminal emulator on Linux.
-
Create a Project Directory: Make a dedicated directory for this small project to keep things organized.
mkdir ~/javabasics
: Creates a directory namedjavabasics
inside your home directory (~
).cd ~/javabasics
: Changes the current working directory to the newly createdjavabasics
.
-
Create the Java Source File: Use a text editor (like
nano
,vim
,gedit
, or any other editor you prefer) to create a file namedHelloLinux.java
. -
Write the Java Code: Enter the following Java code into the text editor:
/* * This is a simple Java program. * File Name : HelloLinux.java */ public class HelloLinux { // The main method begins program execution. public static void main(String[] args) { // Print "Hello, Linux!" to the console. System.out.println("Hello, Linux!"); System.out.println("Java Version: " + System.getProperty("java.version")); System.out.println("Running on OS: " + System.getProperty("os.name") + " " + System.getProperty("os.version")); } }
public class HelloLinux
: Declares a public class namedHelloLinux
. The file name must match the public class name (HelloLinux.java
).public static void main(String[] args)
: This is the main method. It's the entry point for any Java application.public
: Accessible from anywhere.static
: Belongs to the class itself, not a specific object. You can run it without creating an object ofHelloLinux
.void
: Does not return any value.main
: Special name recognized by the JVM as the starting point.(String[] args)
: Accepts an array of strings as command-line arguments.
System.out.println(...)
: A standard Java statement to print a line of text to the console (standard output).System.getProperty(...)
: A way to access system properties like the Java version or OS name.
-
Save and Exit: Save the file and exit the text editor (e.g., in
nano
, pressCtrl+X
, thenY
, thenEnter
). -
Compile the Java Code: Use the
javac
command (the Java compiler) to compile your source file (.java
) into bytecode (.class
).- If there are no errors, this command will silently create a new file named
HelloLinux.class
in the same directory (~/javabasics
). - You can verify its creation using the
ls
command: - If you encounter errors, read them carefully. They usually indicate typos or syntax mistakes in your
.java
file. Re-open the file with your editor, fix the errors, save, and try compiling again.
- If there are no errors, this command will silently create a new file named
-
Run the Compiled Code: Use the
java
command (the Java Virtual Machine launcher) to execute your compiled bytecode. Note that you provide the class name without the.class
extension. -
Observe the Output: You should see the following output in your terminal:
Congratulations! You have successfully set up your Java development environment on Linux, written your first Java program, compiled it using javac
, and executed it using the java
command. This fundamental compile-run cycle is central to Java development, especially when working directly in the terminal.
1. Basic Java Concepts
This section covers the foundational building blocks of the Java language. Understanding these concepts is crucial before moving on to more complex topics like Object-Oriented Programming.
Your First Java Program Revisited
Let's dissect the HelloLinux.java
program more formally to understand its structure:
// Single-line comment
/*
* Multi-line comment block.
* Used for longer explanations.
*/
/**
* Javadoc comment. Used to generate documentation.
* Describes the class or method below it.
* @author Your Name
* @version 1.0
*/
public class HelloLinux { // Class declaration: defines a blueprint named HelloLinux
// Method declaration: defines the main entry point
public static void main(String[] args) {
// Statement: An instruction to be executed. Ends with a semicolon (;).
System.out.println("Hello, Linux!"); // Prints text to the console.
// Another statement accessing system properties.
System.out.println("OS: " + System.getProperty("os.name"));
} // End of main method
} // End of HelloLinux class
Key Components:
- Comments: Ignored by the compiler, used for human-readable explanations. Java supports single-line (
//
), multi-line (/* ... */
), and Javadoc (/** ... */
) comments. - Class Declaration:
public class ClassName { ... }
. Every Java application must contain at least one class. Thepublic
keyword is an access modifier (more on this later). The class name (HelloLinux
) should follow CamelCase convention (starting with an uppercase letter). The code for the class resides within the curly braces{}
. main
Method:public static void main(String[] args) { ... }
. This is the special method the JVM looks for to start execution.public
: Accessible from outside the class.static
: Can be called without creating an object of the class.void
: Indicates the method doesn't return a value.main
: The name the JVM searches for.String[] args
: Declares a parameter namedargs
, which is an array ofString
objects. This array holds any command-line arguments passed to the program when it's executed.
- Statements: Instructions within methods that perform actions. Each statement typically ends with a semicolon (
;
).System.out.println("...")
is a statement that calls theprintln
method from theSystem.out
object to display output. - Blocks: Code enclosed in curly braces
{}
defines a block. Blocks define scope for variables and group statements (e.g., the class body, the method body).
Variables and Data Types
Variables are containers for storing data values. In Java, every variable must be declared with a specific data type before it can be used. Java is a statically-typed language, meaning the type of a variable is checked at compile time.
Java has two categories of data types:
1. Primitive Data Types:
These are the most basic data types built into the language. They store simple values directly.
- Integer Types:
byte
: 8-bit signed integer. Range: -128 to 127. Use when memory saving is critical.short
: 16-bit signed integer. Range: -32,768 to 32,767. Less common.int
: 32-bit signed integer. Range: -231 to 231-1 (approx. -2.1 billion to 2.1 billion). The most commonly used integer type.long
: 64-bit signed integer. Range: -263 to 263-1. Used for very large whole numbers. SuffixL
orl
is required for literal values outside theint
range.
- Floating-Point Types: Used for numbers with fractional parts.
float
: 32-bit single-precision floating-point. SuffixF
orf
is required for literal values. Use when precision is less critical than memory usage.double
: 64-bit double-precision floating-point. More precise and generally the default choice for decimal values. SuffixD
ord
is optional.
- Character Type:
char
: 16-bit Unicode character. Represents a single character, enclosed in single quotes ('
).
- Boolean Type:
boolean
: Represents one bit of information, but its size isn't precisely defined by the spec. Can only have two values:true
orfalse
. Used for logical conditions.
2. Reference Data Types (Non-Primitive):
These refer to objects (instances of classes). They don't store the object data directly but hold a memory address (a reference) where the object is located on the heap.
- Classes: Blueprints for creating objects (e.g.,
String
,Scanner
,ArrayList
, or custom classes you define likeHelloLinux
). - Interfaces: A contract specifying methods a class must implement.
- Arrays: Fixed-size containers holding elements of the same type (primitive or reference).
Variable Declaration and Initialization:
// Declaration: reserves memory based on type
int count;
String message;
// Initialization: assigns an initial value
count = 10;
message = "Initialization complete.";
// Declaration and Initialization combined
double salary = 75000.50;
boolean isActive = true;
Type Casting: Converting a value from one data type to another.
- Widening Casting (Implicit): Converting a smaller type to a larger type size. Done automatically by Java.
- Narrowing Casting (Explicit): Converting a larger type to a smaller size type. Requires manual casting using
(targetType)
and may result in data loss.double piApprox = 3.14; int integerPi = (int) piApprox; // Explicit cast from double to int System.out.println(integerPi); // Output: 3 (fractional part lost) int largeInt = 300; byte smallByte = (byte) largeInt; // Explicit cast from int to byte System.out.println(smallByte); // Output: 44 (value wraps around due to overflow)
Variable Scope: The region of the program where a variable is accessible.
- Local Variables: Declared inside a method or block. Only accessible within that method/block after their declaration. They don't have default values and must be initialized before use.
- Instance Variables: Declared inside a class but outside any method. Belong to an object (instance) of the class. Each object has its own copy. They have default values (0 for numbers,
false
for boolean,\u0000
for char,null
for references). - Class Variables (Static Variables): Declared with the
static
keyword inside a class but outside any method. Belong to the class itself, not individual objects. There's only one copy shared among all objects of the class. Also have default values.
Operators
Operators perform operations on variables and values (operands).
- Arithmetic Operators:
+
(addition, also string concatenation),-
(subtraction),*
(multiplication),/
(division),%
(modulus/remainder).int a = 10; int b = 3; System.out.println("a + b = " + (a + b)); // 13 System.out.println("a / b = " + (a / b)); // 3 (integer division truncates) System.out.println("a % b = " + (a % b)); // 1 (remainder) double c = 10.0; System.out.println("c / b = " + (c / b)); // 3.333... (floating-point division)
- Relational Operators: Used for comparison, result in a
boolean
value.==
(equal to),!=
(not equal to),>
(greater than),<
(less than),>=
(greater than or equal to),<=
(less than or equal to). - Logical Operators: Combine boolean expressions.
&&
(logical AND - short-circuiting),||
(logical OR - short-circuiting),!
(logical NOT).Short-circuiting means the second operand is evaluated only if necessary (e.g., inboolean condition1 = true; boolean condition2 = false; System.out.println("condition1 && condition2: " + (condition1 && condition2)); // false System.out.println("condition1 || condition2: " + (condition1 || condition2)); // true System.out.println("!condition1: " + (!condition1)); // false
A && B
, ifA
is false,B
is not evaluated). - Bitwise Operators: Operate on individual bits of integer types.
&
(bitwise AND),|
(bitwise OR),^
(bitwise XOR),~
(bitwise complement),<<
(left shift),>>
(signed right shift),>>>
(unsigned right shift). Less common in general application programming but useful in specific low-level tasks. - Assignment Operators: Assign values to variables.
=
(simple assignment),+=
,-=
,*=
,/=
,%=
(compound assignment). - Ternary Operator: A shorthand for an
if-else
statement.condition ? value_if_true : value_if_false
. - Increment/Decrement Operators:
++
(increment by 1),--
(decrement by 1). Can be prefix (++var
) or postfix (var++
).- Prefix: Increments/decrements value before using it in the expression.
- Postfix: Increments/decrements value after using it in the expression.
Control Flow Statements
These statements alter the normal sequential execution of code, allowing for decision-making and repetition.
1. Decision Making:
if
Statement: Executes a block of code only if a condition is true.if-else
Statement: Executes one block if the condition is true, and another block if it's false.if-else if-else
Statement: Checks multiple conditions in sequence.switch
Statement: Selects one of many code blocks to be executed based on the value of an expression (typicallyint
,byte
,short
,char
,String
, orenum
).Modernint dayOfWeek = 3; // 1=Mon, 2=Tue, ... String dayName; switch (dayOfWeek) { case 1: dayName = "Monday"; break; // Exits the switch statement case 2: dayName = "Tuesday"; break; case 3: dayName = "Wednesday"; break; // IMPORTANT! Without break, execution "falls through" case 4: dayName = "Thursday"; break; case 5: dayName = "Friday"; break; case 6: case 7: // Multiple cases can lead to the same block dayName = "Weekend"; break; default: // Optional: Executes if no case matches dayName = "Invalid day"; break; } System.out.println("Today is " + dayName); // Output: Today is Wednesday
switch
expressions (Java 14+) offer a more concise syntax:
2. Looping (Iteration):
for
Loop: Executes a block of code a specific number of times. Ideal when the number of iterations is known beforehand.while
Loop: Executes a block of code as long as a condition is true. The condition is checked before each iteration.do-while
Loop: Similar towhile
, but the condition is checked after the block executes. Guarantees the block runs at least once.- Enhanced
for
Loop (For-Each Loop): Provides a simpler way to iterate over elements in an array or collection.String[] fruits = {"Apple", "Banana", "Orange"}; for (String fruit : fruits) { // For each String 'fruit' in the 'fruits' array System.out.println("Fruit: " + fruit); } // Also works with collections (covered later) // List<Integer> numbers = List.of(1, 2, 3); // for (int number : numbers) { // System.out.println(number); // }
3. Branching Statements:
break
: Terminates the innermostswitch
,for
,while
, ordo-while
statement immediately.continue
: Skips the current iteration of the innermostfor
,while
, ordo-while
loop and proceeds to the next iteration.return
: Exits the current method. Can optionally return a value if the method's return type is notvoid
. We sawreturn
implicitly at the end of thevoid main
method.
Input and Output
Basic interaction with the user or the system often involves reading input and displaying output.
Output:
We've already been using the primary method for standard output:
System.out.println(data)
: Prints thedata
followed by a newline character.System.out.print(data)
: Prints thedata
without a newline character.System.out.printf(formatString, args...)
: Prints formatted output, similar to C'sprintf
. Allows for precise control over alignment, padding, and number formats.String name = "Alice"; int age = 30; double balance = 123.456; System.out.printf("User: %s, Age: %d\n", name, age); // %s for string, %d for integer, \n for newline System.out.printf("Balance: %.2f\n", balance); // %.2f for float/double with 2 decimal places System.out.printf("Padded Age: [%5d]\n", age); // %5d reserves 5 spaces, right-aligned System.out.printf("Left Padded Name: [%-10s]\n", name); // %-10s reserves 10 spaces, left-aligned
Input:
Reading input, typically from the keyboard (standard input), commonly involves the Scanner
class from the java.util
package.
import java.util.Scanner; // Import the Scanner class
public class UserInputDemo {
public static void main(String[] args) {
// 1. Create a Scanner object linked to standard input (System.in)
Scanner input = new Scanner(System.in);
// 2. Prompt the user
System.out.print("Enter your name: ");
// 3. Read a line of text
String userName = input.nextLine();
// 2. Prompt for age
System.out.print("Enter your age: ");
// 3. Read an integer
int userAge = input.nextInt();
// 2. Prompt for salary
System.out.print("Enter your desired salary: ");
// 3. Read a double
double userSalary = input.nextDouble();
// Consume the leftover newline character after nextDouble() or nextInt()
// This is important if you plan to call nextLine() afterwards
input.nextLine();
// Display the collected information
System.out.println("\n--- User Information ---");
System.out.printf("Name: %s\n", userName);
System.out.printf("Age: %d\n", userAge);
System.out.printf("Desired Salary: %.2f\n", userSalary);
// 4. Close the Scanner (important to release system resources)
input.close();
}
}
Explanation:
import java.util.Scanner;
: Makes theScanner
class available.Scanner input = new Scanner(System.in);
: Creates aScanner
object namedinput
that reads from the standard input stream (System.in
, usually the keyboard).input.nextLine()
: Reads the entire line of text input until the user presses Enter.input.nextInt()
: Reads the next token of input as anint
.input.nextDouble()
: Reads the next token of input as adouble
.- Important: After reading numbers (
nextInt
,nextDouble
, etc.), the newline character (\n
) entered by the user remains in the input buffer. If the next input you want to read is a full line usingnextLine()
, it will consume this leftover newline and return an empty string immediately. To prevent this, callinput.nextLine();
after reading a number if you intend to read a line subsequently. input.close();
: Releases the resources associated with theScanner
. It's crucial to close scanners linked to system resources likeSystem.in
or files when you are finished with them.
Workshop Simple Calculator Application
Objective: Build a command-line calculator that takes two numbers and an operator from the user and performs the corresponding arithmetic operation. This will practice variables, data types, operators, control flow (switch
or if-else if
), and user input (Scanner
).
Steps:
-
Navigate to Your Project Directory: Open your Linux terminal and
cd
into the directory where you want to save this project (e.g.,~/javabasics
). -
Create the Java Source File: Create a file named
Calculator.java
. -
Write the Java Code: Enter the following code into the editor. Read the comments carefully to understand each part.
import java.util.Scanner; // Needed for user input public class Calculator { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); // Create Scanner object System.out.println("=== Simple Command-Line Calculator ==="); // --- Get First Number --- System.out.print("Enter the first number: "); double num1; // Basic input validation loop while (!scanner.hasNextDouble()) { System.out.println("Invalid input. Please enter a valid number."); System.out.print("Enter the first number: "); scanner.next(); // Consume the invalid input } num1 = scanner.nextDouble(); // --- Get Operator --- System.out.print("Enter the operator (+, -, *, /): "); char operator; // Loop until a valid operator is entered while (true) { String opInput = scanner.next(); // Read operator as a string if (opInput.length() == 1 && "+-*/".contains(opInput)) { operator = opInput.charAt(0); // Get the first character break; // Exit loop if valid } else { System.out.println("Invalid operator. Please enter +, -, *, or /."); System.out.print("Enter the operator: "); } } // --- Get Second Number --- System.out.print("Enter the second number: "); double num2; while (!scanner.hasNextDouble()) { System.out.println("Invalid input. Please enter a valid number."); System.out.print("Enter the second number: "); scanner.next(); // Consume invalid input } num2 = scanner.nextDouble(); // --- Perform Calculation --- double result = 0.0; boolean calculationSuccessful = true; // Flag to track success switch (operator) { case '+': result = num1 + num2; break; case '-': result = num1 - num2; break; case '*': result = num1 * num2; break; case '/': if (num2 != 0) { // Check for division by zero result = num1 / num2; } else { System.out.println("Error: Division by zero is not allowed."); calculationSuccessful = false; // Mark calculation as failed } break; // Default case is not strictly needed here due to input validation, // but good practice in general switches. default: System.out.println("Internal error: Invalid operator encountered."); calculationSuccessful = false; break; } // --- Display Result --- if (calculationSuccessful) { System.out.printf("\nResult: %.2f %c %.2f = %.2f\n", num1, operator, num2, result); } else { System.out.println("Calculation could not be performed."); } // --- Close Scanner --- scanner.close(); // Release resources System.out.println("\nCalculator finished."); } }
-
Save and Exit: Save the file (Ctrl+X, Y, Enter in
nano
). -
Compile the Code:
Fix any compilation errors if they occur. -
Run the Calculator:
-
Interact with the Program: The calculator will prompt you to enter the first number, the operator, and the second number. Try different inputs, including invalid ones (like letters for numbers or incorrect operators) to see how the input validation works. Also, test division by zero.
Example Interaction:
Another Example (Division by Zero):=== Simple Command-Line Calculator === Enter the first number: 10.5 Enter the operator (+, -, *, /): * Enter the second number: 3 Result: 10.50 * 3.00 = 31.50 Calculator finished.
Example (Invalid Input):=== Simple Command-Line Calculator === Enter the first number: 5 Enter the operator (+, -, *, /): / Enter the second number: 0 Error: Division by zero is not allowed. Calculation could not be performed. Calculator finished.
=== Simple Command-Line Calculator === Enter the first number: abc Invalid input. Please enter a valid number. Enter the first number: 12 Enter the operator (+, -, *, /): x Invalid operator. Please enter +, -, *, or /. Enter the operator: + Enter the second number: 5 Result: 12.00 + 5.00 = 17.00 Calculator finished.
This workshop provided practical experience with core Java syntax, input/output handling, error checking (division by zero, basic input validation), and control flow structures (while
, switch
).
2. Object-Oriented Programming (OOP) Fundamentals
Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects", which can contain data in the form of fields (often known as attributes or properties) and code in the form of procedures (often known as methods). Java was designed from the ground up as an object-oriented language. Understanding OOP principles is fundamental to writing effective Java code.
Introduction to OOP
OOP aims to model real-world entities and their interactions within software. It provides a way to structure programs so that properties and behaviors are bundled together. The four main principles of OOP are:
- Encapsulation: Bundling data (attributes) and methods (behaviors) that operate on the data within a single unit (an object). It also involves restricting direct access to some of an object's components (data hiding), which is a key aspect of protecting data integrity. Think of a car: the engine, wheels, and steering (data/attributes) are encapsulated within the car object. You interact with it through defined interfaces like the steering wheel, accelerator, and brakes (methods), without needing to know the intricate details of how the engine works internally.
- Inheritance: A mechanism where a new class (subclass or derived class) inherits properties and methods from an existing class (superclass or base class). This promotes code reuse and establishes a hierarchical relationship ("is-a" relationship). For example, a
Car
class and aTruck
class can both inherit common properties likenumberOfWheels
andengineType
from a more generalVehicle
class.Car
"is-a"Vehicle
, andTruck
"is-a"Vehicle
. - Polymorphism: Literally meaning "many forms". It allows objects of different classes to respond to the same message (method call) in different ways. This enables flexibility and extensibility. For example, both a
Car
object and aTruck
object might have astartEngine()
method, but the internal implementation of how the engine starts could be different for each. You can callvehicle.startEngine()
regardless of whethervehicle
currently refers to aCar
or aTruck
. - Abstraction: Hiding complex implementation details and exposing only the essential features of an object. It focuses on what an object does rather than how it does it. Abstract classes and interfaces are key tools for achieving abstraction in Java. For example, when you drive a car, you interact with the abstract concept of "steering" via the steering wheel, without needing to understand the underlying mechanics of the steering rack, power steering pump, etc.
Objects and Classes:
- Class: A blueprint or template for creating objects. It defines the common properties (attributes/fields) and behaviors (methods) that all objects of that class will have. Example: The blueprint for a "Dog". It defines that a dog has attributes like
breed
,age
,color
, and behaviors likebark()
,wagTail()
,eat()
. - Object: An instance of a class. It is a concrete entity created from the class blueprint, residing in memory. Each object has its own state (specific values for its attributes) but shares the behavior defined by the class. Example: Your specific dog, "Fido", is an object of the
Dog
class. Fido might havebreed = "Labrador"
,age = 3
,color = "Golden"
. Another dog object, "Buddy", might have different attribute values. Both Fido and Buddy can perform thebark()
action defined in theDog
class.
Classes and Objects
Let's see how to define a class and create objects from it in Java.
Defining a Class:
// File: Dog.java
public class Dog {
// Instance Variables (Attributes/Fields)
// These define the state of a Dog object
String breed;
int age;
String color;
String name;
// Constructor: A special method used to initialize objects when they are created.
// It has the same name as the class and no return type.
public Dog(String dogName, String dogBreed, int dogAge, String dogColor) {
System.out.println("Creating a new Dog object named " + dogName);
// 'this' keyword refers to the current object being created.
// It distinguishes instance variables from constructor parameters.
this.name = dogName;
this.breed = dogBreed;
this.age = dogAge;
this.color = dogColor;
}
// Instance Methods (Behaviors)
// These define what a Dog object can do
void bark() {
System.out.println(name + " says: Woof woof!");
}
void wagTail() {
System.out.println(name + " is wagging its tail.");
}
void displayInfo() {
System.out.println("--- Dog Info ---");
System.out.println("Name: " + this.name); // 'this' is optional here but clarifies
System.out.println("Breed: " + this.breed);
System.out.println("Age: " + this.age);
System.out.println("Color: " + this.color);
System.out.println("----------------");
}
}
Explanation:
- Instance Variables:
breed
,age
,color
,name
are declared within the class but outside any method. EachDog
object will have its own copy of these variables. - Constructor:
public Dog(...)
. This method is called automatically when you create a newDog
object using thenew
keyword. Its purpose is to initialize the object's state (instance variables). this
Keyword: Inside an instance method or constructor,this
refers to the current object whose method or constructor is being called. It's often used to differentiate between instance variables and parameters or local variables with the same name (e.g.,this.name = dogName;
).- Instance Methods:
bark()
,wagTail()
,displayInfo()
define the actions aDog
object can perform. They operate on the instance variables of the specific object they are called on.
Creating Objects (Instantiation):
You create objects using the new
keyword followed by a call to the class's constructor.
// File: DogPark.java
public class DogPark {
public static void main(String[] args) {
// Create (instantiate) Dog objects using the 'new' keyword and the constructor
Dog fido = new Dog("Fido", "Labrador", 3, "Golden"); // Calls the Dog constructor
Dog buddy = new Dog("Buddy", "Poodle", 5, "White");
// Access object attributes (generally discouraged directly, prefer methods - see Encapsulation)
// System.out.println(fido.name); // Output: Fido
// Call object methods
fido.displayInfo();
buddy.displayInfo();
fido.bark();
buddy.wagTail();
fido.wagTail();
}
}
Compiling and Running:
- Save the code above into two files:
Dog.java
andDogPark.java
in the same directory (e.g.,~/javabasics
). - Compile both files. You can use a wildcard or list them:
This will create
Dog.class
andDogPark.class
. - Run the
DogPark
class (because it contains themain
method):
Output:
Creating a new Dog object named Fido
Creating a new Dog object named Buddy
--- Dog Info ---
Name: Fido
Breed: Labrador
Age: 3
Color: Golden
----------------
--- Dog Info ---
Name: Buddy
Breed: Poodle
Age: 5
Color: White
----------------
Fido says: Woof woof!
Buddy is wagging its tail.
Fido is wagging its tail.
Methods
Methods define the behavior of objects. They encapsulate a sequence of statements to perform a specific task.
Defining Methods:
accessModifier staticKeyword returnType methodName(parameterList) {
// Method body: statements to perform the task
// Optionally includes a 'return' statement if returnType is not void
}
accessModifier
: Controls visibility (e.g.,public
,private
,protected
, default). See Encapsulation.staticKeyword
: (Optional) If present (static
), the method belongs to the class itself, not a specific object. It's called using the class name (e.g.,Math.sqrt(9)
). If absent, it's an instance method and must be called on an object (e.g.,fido.bark()
).returnType
: The data type of the value the method returns (e.g.,int
,double
,String
,Dog
,void
if it returns nothing).methodName
: Name of the method (usually followscamelCaseStartingWithLowercase
).parameterList
: (Optional) A comma-separated list of input parameters, each with a type and name (e.g.,(int count, String message)
). These act like local variables within the method.- Method Body: Contains the code executed when the method is called.
return
Statement: Used to send a value back from the method. The type of the returned value must match thereturnType
. Methods withvoid
return type don't need an explicitreturn
statement, or can usereturn;
to exit early.
Example Methods:
public class CalculatorUtil {
// Instance method (needs a CalculatorUtil object)
public double add(double n1, double n2) {
return n1 + n2; // Returns a double value
}
// Static method (can be called directly using CalculatorUtil.addStatic)
public static double addStatic(double n1, double n2) {
return n1 + n2;
}
// Method with no parameters and no return value
public void printWelcomeMessage() {
System.out.println("Welcome to CalculatorUtil!");
}
// Method with multiple parameters
public static void printSum(int a, int b) {
System.out.printf("%d + %d = %d\n", a, b, (a + b));
}
}
Method Overloading (Compile-time Polymorphism):
Defining multiple methods within the same class that have the same name but different parameter lists (different number of parameters, different types of parameters, or different order of types). The compiler determines which method to call based on the arguments provided.
public class Greeter {
public void greet() { // Method 1
System.out.println("Hello!");
}
public void greet(String name) { // Method 2: Overloaded with one String parameter
System.out.println("Hello, " + name + "!");
}
public void greet(String name, int times) { // Method 3: Overloaded with String and int
for (int i = 0; i < times; i++) {
System.out.println("Hello, " + name + "!");
}
}
// Invalid overloading: Only changing return type is NOT allowed
// public String greet(String name) { return "Hello, " + name; } // COMPILE ERROR
public static void main(String[] args) {
Greeter greeter = new Greeter();
greeter.greet(); // Calls Method 1
greeter.greet("Alice"); // Calls Method 2
greeter.greet("Bob", 3); // Calls Method 3
}
}
Passing Arguments:
- Pass-by-Value: Java always passes arguments by value.
- For primitive types, a copy of the value is passed to the method. Changes made to the parameter inside the method do not affect the original variable outside the method.
- For reference types (objects, arrays), a copy of the reference (memory address) is passed. This means the parameter inside the method points to the same object as the variable outside. If the method modifies the state of the object (changes its instance variables), the changes will be reflected outside the method. However, if the method reassigns the parameter to point to a new object, the original variable outside the method remains unchanged.
Encapsulation
Encapsulation protects the internal state of an object by restricting direct access to its instance variables. Access is typically controlled through public methods (getters and setters).
Access Modifiers: Keywords that control the visibility (accessibility) of classes, interfaces, variables, and methods.
public
: Accessible from any other class, anywhere. (Least restrictive).protected
: Accessible within its own package and by subclasses (even if they are in different packages).- (Default/Package-Private): (No keyword used) Accessible only within its own package.
private
: Accessible only within the class in which it is declared. (Most restrictive).
Best Practice: Make instance variables private
and provide public
methods (getters and setters) to access or modify them if needed.
// File: BankAccount.java
public class BankAccount {
// Instance variables are private - cannot be accessed directly from outside
private String accountNumber;
private double balance;
private String ownerName;
// Constructor
public BankAccount(String accountNumber, String ownerName, double initialBalance) {
this.accountNumber = accountNumber;
this.ownerName = ownerName;
// Apply validation: Balance cannot be negative initially
if (initialBalance >= 0) {
this.balance = initialBalance;
} else {
this.balance = 0;
System.err.println("Warning: Initial balance cannot be negative. Set to 0.");
}
}
// Getter method for balance (read-only access)
public double getBalance() {
return this.balance;
}
// Getter method for account number
public String getAccountNumber() {
return this.accountNumber; // Account number is usually immutable after creation
}
// Getter method for owner name
public String getOwnerName() {
return ownerName;
}
// Setter method for owner name (allows modification)
public void setOwnerName(String ownerName) {
// Could add validation here if needed (e.g., non-empty name)
this.ownerName = ownerName;
}
// Method to deposit funds (modifies balance)
public void deposit(double amount) {
if (amount > 0) {
this.balance += amount;
System.out.printf("Deposited: %.2f. New balance: %.2f\n", amount, this.balance);
} else {
System.out.println("Deposit amount must be positive.");
}
}
// Method to withdraw funds (modifies balance)
public boolean withdraw(double amount) {
if (amount <= 0) {
System.out.println("Withdrawal amount must be positive.");
return false;
} else if (amount > this.balance) {
System.out.println("Insufficient funds.");
return false;
} else {
this.balance -= amount;
System.out.printf("Withdrew: %.2f. New balance: %.2f\n", amount, this.balance);
return true;
}
}
}
// File: BankDemo.java
public class BankDemo {
public static void main(String[] args) {
BankAccount acc1 = new BankAccount("ACC123", "Alice", 1000.0);
// Cannot access private members directly:
// System.out.println(acc1.balance); // COMPILE ERROR!
// acc1.balance = -500; // COMPILE ERROR!
// Use public getter methods
System.out.println("Account Owner: " + acc1.getOwnerName());
System.out.printf("Initial Balance: %.2f\n", acc1.getBalance());
// Use public methods to interact with the object's state
acc1.deposit(500.0);
acc1.withdraw(200.0);
acc1.withdraw(2000.0); // Insufficient funds
// Modify owner name using setter
acc1.setOwnerName("Alice Wonderland");
System.out.printf("Final Balance for %s: %.2f\n", acc1.getOwnerName(), acc1.getBalance());
}
}
Benefits of Encapsulation:
- Data Hiding: Protects internal state from outside interference or misuse.
- Control: Allows validation logic within setters (e.g., ensuring balance doesn't become negative inappropriately).
- Flexibility: The internal implementation of the class can change without affecting the classes that use it, as long as the public methods (the interface) remain the same.
- Maintainability: Code is easier to understand and maintain because related data and behavior are grouped together.
Inheritance
Inheritance allows a class (subclass) to inherit attributes and methods from another class (superclass). It models an "is-a" relationship.
Syntax: Use the extends
keyword.
// File: Vehicle.java (Superclass)
public class Vehicle {
protected String brand; // protected: accessible by subclasses
protected int year;
public Vehicle(String brand, int year) {
System.out.println("Vehicle constructor called");
this.brand = brand;
this.year = year;
}
public void start() {
System.out.println("Vehicle starting...");
}
public void stop() {
System.out.println("Vehicle stopping...");
}
public void displayInfo() {
System.out.println("Brand: " + brand + ", Year: " + year);
}
}
// File: Car.java (Subclass)
public class Car extends Vehicle { // Car inherits from Vehicle
private int numberOfDoors;
// Constructor for Car
public Car(String brand, int year, int numberOfDoors) {
// Call the superclass constructor *first* using super()
super(brand, year); // Must be the first statement in subclass constructor
System.out.println("Car constructor called");
this.numberOfDoors = numberOfDoors;
}
// Method specific to Car
public void openTrunk() {
System.out.println("Car trunk opened.");
}
// Method Overriding: Providing a specific implementation for an inherited method
@Override // Annotation: Good practice, checks if method actually overrides
public void start() {
// Optionally call the superclass method
// super.start();
System.out.println(brand + " car engine starting with a key turn...");
}
// Overriding displayInfo to include door count
@Override
public void displayInfo() {
super.displayInfo(); // Call the superclass version first
System.out.println("Number of Doors: " + numberOfDoors);
}
}
// File: ElectricCar.java (Subclass of Car)
public class ElectricCar extends Car {
private int batteryCapacityKWh;
public ElectricCar(String brand, int year, int numberOfDoors, int batteryCapacity) {
super(brand, year, numberOfDoors); // Calls Car constructor
System.out.println("ElectricCar constructor called");
this.batteryCapacityKWh = batteryCapacity;
}
// Overriding start method again
@Override
public void start() {
System.out.println(brand + " electric car silently starting...");
}
public void charge() {
System.out.println("Charging the " + brand + "...");
}
@Override
public void displayInfo() {
super.displayInfo(); // Calls Car's displayInfo
System.out.println("Battery Capacity: " + batteryCapacityKWh + " kWh");
}
}
// File: InheritanceDemo.java
public class InheritanceDemo {
public static void main(String[] args) {
System.out.println("--- Creating a Car ---");
Car myCar = new Car("Toyota", 2021, 4);
myCar.start(); // Calls Car's overridden start()
myCar.displayInfo(); // Calls Car's overridden displayInfo()
myCar.openTrunk(); // Calls Car's specific method
myCar.stop(); // Calls inherited Vehicle's stop()
System.out.println("\n--- Creating an Electric Car ---");
ElectricCar myTesla = new ElectricCar("Tesla", 2023, 4, 100);
myTesla.start(); // Calls ElectricCar's overridden start()
myTesla.charge(); // Calls ElectricCar's specific method
myTesla.displayInfo(); // Calls ElectricCar's overridden displayInfo()
myTesla.openTrunk(); // Calls inherited Car's openTrunk()
myTesla.stop(); // Calls inherited Vehicle's stop()
}
}
Key Concepts in Inheritance:
extends
: Keyword used by the subclass to inherit from the superclass.super
Keyword:super()
: Used in the subclass constructor to call the superclass constructor. Must be the first statement. If not explicitly called, the compiler implicitly calls the superclass's no-argument constructor (super();
). If the superclass doesn't have a no-argument constructor, you must explicitly call another superclass constructor.super.methodName()
: Used to call a method from the superclass.super.variableName
: Used to access an instance variable from the superclass (less common, especially if variables are private).
- Method Overriding: When a subclass provides its own specific implementation of a method that is already defined in its superclass. The method signature (name, parameters) must be the same. The
@Override
annotation is recommended to ensure you are actually overriding and not accidentally overloading or creating a new method. final
Keyword:final
class: Cannot be subclassed (cannot be extended).public final class String { ... }
final
method: Cannot be overridden by subclasses.final
variable: Value cannot be changed after initialization (creates a constant if alsostatic
).
- Single Inheritance: Java supports single inheritance for classes, meaning a class can only extend one direct superclass. This avoids the "diamond problem" found in some languages with multiple inheritance. However, a class can implement multiple interfaces (see Abstraction).
Polymorphism
Polymorphism allows objects to be treated as instances of their superclass, yet invoke the subclass's specific overridden methods at runtime.
Run-time Polymorphism (Method Overriding):
This is achieved through inheritance and method overriding. A superclass reference variable can hold an object of any of its subclasses. When a method is called on that reference, the JVM determines at runtime which version of the method to execute based on the actual object type being referred to.
// Using Vehicle, Car, ElectricCar classes from Inheritance section
public class PolymorphismDemo {
public static void main(String[] args) {
// Superclass reference can hold subclass objects
Vehicle vehicle1 = new Car("Honda", 2020, 4); // Vehicle ref, Car object
Vehicle vehicle2 = new ElectricCar("Nissan", 2022, 5, 60); // Vehicle ref, ElectricCar object
Vehicle genericVehicle = new Vehicle("Generic", 2019); // Vehicle ref, Vehicle object
System.out.println("--- Calling start() via Vehicle references ---");
vehicle1.start(); // RUNTIME: Calls Car's start()
vehicle2.start(); // RUNTIME: Calls ElectricCar's start()
genericVehicle.start(); // RUNTIME: Calls Vehicle's start()
System.out.println("\n--- Calling displayInfo() via Vehicle references ---");
vehicle1.displayInfo(); // RUNTIME: Calls Car's displayInfo()
vehicle2.displayInfo(); // RUNTIME: Calls ElectricCar's displayInfo()
genericVehicle.displayInfo(); // RUNTIME: Calls Vehicle's displayInfo()
// Cannot call subclass-specific methods directly via superclass reference
// vehicle1.openTrunk(); // COMPILE ERROR! Vehicle type doesn't know openTrunk()
// vehicle2.charge(); // COMPILE ERROR! Vehicle type doesn't know charge()
// Example using an array of Vehicles
System.out.println("\n--- Processing Vehicles in an array ---");
Vehicle[] fleet = {vehicle1, vehicle2, genericVehicle};
for (Vehicle v : fleet) {
System.out.println("\nProcessing a " + v.getClass().getSimpleName()); // Get actual class name
v.start(); // Polymorphic call - correct start() is executed
v.displayInfo(); // Polymorphic call
// If we need to call subclass-specific methods, we need type checking and casting
if (v instanceof ElectricCar) {
System.out.println("It's an electric car!");
ElectricCar ec = (ElectricCar) v; // Downcasting (safe due to instanceof check)
ec.charge();
} else if (v instanceof Car) {
System.out.println("It's a regular car (or subclass like ElectricCar, but handled above).");
Car c = (Car) v; // Downcasting
c.openTrunk();
}
}
}
}
Explanation:
- We create
Car
andElectricCar
objects but store them inVehicle
reference variables (vehicle1
,vehicle2
). - When
vehicle1.start()
is called, even thoughvehicle1
is declared asVehicle
, the JVM knows the actual object is aCar
, so it executes theCar
class'sstart()
method. This decision happens at runtime. instanceof
Operator: Checks if an object is an instance of a particular class or interface (or a subclass/implementing class thereof). Returnstrue
orfalse
.- Casting:
- Upcasting: Casting a subclass object to a superclass reference (e.g.,
Vehicle v = new Car(...)
). This is done implicitly and is always safe. - Downcasting: Casting a superclass reference (that actually points to a subclass object) back to the subclass type (e.g.,
Car c = (Car) v;
). This requires an explicit cast and can cause aClassCastException
at runtime if the object isn't actually an instance of the target subclass (or one of its subclasses). Useinstanceof
before downcasting for safety.
- Upcasting: Casting a subclass object to a superclass reference (e.g.,
Compile-time Polymorphism (Method Overloading): As discussed earlier, this involves having multiple methods with the same name but different parameters in the same class. The compiler decides which method to call based on the method signature at compile time. This is less flexible than run-time polymorphism.
Abstraction
Abstraction hides implementation details and shows only essential features. It helps manage complexity. Java provides two primary mechanisms for abstraction:
1. Abstract Classes:
- A class declared with the
abstract
keyword. - Cannot be instantiated directly (you cannot do
new AbstractClassName();
). - Can contain both abstract methods (methods without a body) and concrete methods (methods with implementation).
- If a class contains one or more abstract methods, the class must be declared abstract.
- Subclasses that inherit from an abstract class must either implement (provide a body for) all inherited abstract methods, or they must also be declared abstract.
- Used to provide a common base class for related subclasses, enforcing a certain structure while allowing flexibility in implementation.
// File: Shape.java (Abstract Class)
abstract class Shape {
private String color;
public Shape(String color) {
this.color = color;
}
// Concrete method (implemented here)
public String getColor() {
return color;
}
// Abstract method (no implementation, must be implemented by subclasses)
// Represents an essential feature (calculating area) but how it's done depends on the specific shape.
public abstract double calculateArea();
// Another abstract method
public abstract void draw();
}
// File: Circle.java (Concrete Subclass)
class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color); // Call Shape constructor
this.radius = radius;
}
// Implementing the abstract method calculateArea
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
// Implementing the abstract method draw
@Override
public void draw() {
System.out.println("Drawing a " + getColor() + " circle with radius " + radius);
}
}
// File: Rectangle.java (Concrete Subclass)
class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(String color, double width, double height) {
super(color);
this.width = width;
this.height = height;
}
// Implementing the abstract method calculateArea
@Override
public double calculateArea() {
return width * height;
}
// Implementing the abstract method draw
@Override
public void draw() {
System.out.println("Drawing a " + getColor() + " rectangle with width " + width + " and height " + height);
}
}
// File: AbstractionDemo.java
public class AbstractionDemo {
public static void main(String[] args) {
// Shape myShape = new Shape("Red"); // COMPILE ERROR! Cannot instantiate abstract class
Shape circle = new Circle("Red", 5.0);
Shape rectangle = new Rectangle("Blue", 4.0, 6.0);
circle.draw(); // Calls Circle's draw()
System.out.println("Circle Area: " + circle.calculateArea()); // Calls Circle's calculateArea()
rectangle.draw(); // Calls Rectangle's draw()
System.out.println("Rectangle Area: " + rectangle.calculateArea()); // Calls Rectangle's calculateArea()
// Using polymorphism with abstract classes
Shape[] shapes = {circle, rectangle};
System.out.println("\n--- Processing shapes polymorphically ---");
for (Shape s : shapes) {
s.draw(); // Correct draw() method is called based on actual object type
System.out.printf("Area of %s %s: %.2f\n", s.getColor(), s.getClass().getSimpleName(), s.calculateArea());
}
}
}
2. Interfaces:
- A completely abstract "class" declared using the
interface
keyword. - Can only contain
abstract
methods (methods without a body - prior to Java 8),static
methods (Java 8+),default
methods (implemented methods - Java 8+),private
methods (Java 9+), andpublic static final
constants (fields). - Methods in an interface are implicitly
public abstract
(unlessdefault
,static
, orprivate
). - Fields in an interface are implicitly
public static final
. - A class uses the
implements
keyword to implement an interface. - A class can implement multiple interfaces (providing a form of multiple inheritance for type but not implementation, unless default methods are used).
- If a class implements an interface, it must provide implementations for all the abstract methods declared in that interface (or be declared abstract itself).
- Interfaces define a contract - what methods a class must provide, without specifying how. They are used to achieve loose coupling and define roles or capabilities.
// File: Playable.java (Interface)
interface Playable {
// public static final implicitly
int MAX_VOLUME = 100;
// public abstract implicitly
void play();
void stop();
void setVolume(int level); // Abstract method
}
// File: Loggable.java (Another Interface)
interface Loggable {
void log(String message); // public abstract implicitly
}
// File: MusicPlayer.java (Class implementing two interfaces)
class MusicPlayer implements Playable, Loggable {
private boolean isPlaying = false;
private int currentVolume = 50;
@Override
public void play() {
if (!isPlaying) {
isPlaying = true;
log("Music playback started.");
System.out.println("Music playing...");
}
}
@Override
public void stop() {
if (isPlaying) {
isPlaying = false;
log("Music playback stopped.");
System.out.println("Music stopped.");
}
}
@Override
public void setVolume(int level) {
if (level >= 0 && level <= MAX_VOLUME) { // Can use interface constant
this.currentVolume = level;
log("Volume set to " + level);
System.out.println("Volume set to " + this.currentVolume);
} else {
log("Invalid volume level attempt: " + level);
System.out.println("Invalid volume level.");
}
}
// Implementation for the Loggable interface
@Override
public void log(String message) {
// In a real app, this might write to a file or console with timestamp
System.out.println("[LOG] " + message);
}
}
// File: VideoPlayer.java (Class implementing one interface)
class VideoPlayer implements Playable {
private String currentVideo = "None";
@Override
public void play() {
System.out.println("Playing video: " + currentVideo);
}
@Override
public void stop() {
System.out.println("Stopping video: " + currentVideo);
}
@Override
public void setVolume(int level) {
if (level >= 0 && level <= Playable.MAX_VOLUME) { // Qualify constant if needed
System.out.println("Video volume set to " + level);
}
}
public void loadVideo(String filename) {
this.currentVideo = filename;
System.out.println("Loaded video: " + filename);
}
}
// File: InterfaceDemo.java
public class InterfaceDemo {
public static void main(String[] args) {
// Use interface type as reference (polymorphism)
Playable player1 = new MusicPlayer();
Playable player2 = new VideoPlayer();
player1.play();
player1.setVolume(75);
player1.stop();
// player1.log("Test"); // COMPILE ERROR! Playable type doesn't have log()
// To access MusicPlayer specific methods (like log), need casting or different reference
MusicPlayer mp = (MusicPlayer) player1;
mp.log("Music player test log.");
System.out.println();
player2.play(); // Calls VideoPlayer's play()
// player2.loadVideo("movie.mp4"); // COMPILE ERROR! Playable type doesn't know loadVideo()
// Need to cast to access VideoPlayer methods
if (player2 instanceof VideoPlayer) {
VideoPlayer vp = (VideoPlayer) player2;
vp.loadVideo("movie.mp4");
vp.play(); // Calls VideoPlayer's play() again
vp.setVolume(90);
vp.stop();
}
// Use the Loggable interface type
Loggable logger = new MusicPlayer();
logger.log("This message comes via the Loggable interface reference.");
// logger.play(); // COMPILE ERROR! Loggable type doesn't know play()
}
}
Abstract Class vs. Interface:
Feature | Abstract Class | Interface |
---|---|---|
Instantiation | Cannot be instantiated | Cannot be instantiated |
Methods | Abstract and Concrete methods | Abstract, Default, Static, Private methods |
Variables | Instance & Static variables allowed | Only public static final constants |
Implementation | Class extends (single) |
Class implements (multiple) |
Constructor | Has constructor(s) | No constructor |
Purpose | Base for closely related classes (is-a relationship), code sharing | Define a contract/capability (has-a capability), achieve loose coupling |
Keyword | abstract , extends |
interface , implements |
Access Modifiers | Can use all access modifiers | Methods implicitly public (or private ), Fields implicitly public static final |
State (Instance Vars) | Can have state | Cannot have instance state (only constants) |
Choose an abstract class
when you want to provide a common base implementation and state for related subclasses. Choose an interface
when you want to define a role or capability that unrelated classes can implement.
Workshop Modeling a Simple University System
Objective: Apply OOP principles (Classes, Objects, Encapsulation, potentially simple Inheritance) to model basic entities in a university context like Student
, Course
, and Professor
.
Steps:
-
Directory Setup: In your terminal, navigate to your Java projects directory (e.g.,
~/javabasics
) and create a subdirectory for this workshop. -
Define the
Code:Course
Class: CreateCourse.java
. This class will represent a university course. Use encapsulation.Save and exit (public class Course { private String courseCode; // e.g., "CS101" private String courseName; // e.g., "Introduction to Computer Science" private int maxCapacity; private int currentEnrollment; // Constructor public Course(String courseCode, String courseName, int maxCapacity) { this.courseCode = courseCode; this.courseName = courseName; if (maxCapacity > 0) { this.maxCapacity = maxCapacity; } else { this.maxCapacity = 30; // Default capacity System.err.println("Warning: Invalid capacity for " + courseCode + ". Setting to default 30."); } this.currentEnrollment = 0; // Starts empty } // Getters public String getCourseCode() { return courseCode; } public String getCourseName() { return courseName; } public int getMaxCapacity() { return maxCapacity; } public int getCurrentEnrollment() { return currentEnrollment; } // Method to enroll a student (if space available) public boolean enrollStudent() { if (currentEnrollment < maxCapacity) { currentEnrollment++; System.out.println("Student enrolled in " + courseCode + ". Current enrollment: " + currentEnrollment); return true; } else { System.out.println("Enrollment failed for " + courseCode + ". Course is full."); return false; } } // Method to drop a student public boolean dropStudent() { if (currentEnrollment > 0) { currentEnrollment--; System.out.println("Student dropped from " + courseCode + ". Current enrollment: " + currentEnrollment); return true; } else { System.out.println("Cannot drop student from " + courseCode + ". Course is empty."); return false; } } // Display course info public void displayInfo() { System.out.println("--- Course Information ---"); System.out.println("Code: " + courseCode); System.out.println("Name: " + courseName); System.out.println("Capacity: " + maxCapacity); System.out.println("Enrolled: " + currentEnrollment); System.out.println("------------------------"); } }
Ctrl+X
,Y
,Enter
). -
Define the
Code:Student
Class: CreateStudent.java
. This class represents a student.Save and exit.public class Student { private String studentId; private String name; private int yearLevel; // We could add a list of enrolled courses here later (Intermediate concepts) public Student(String studentId, String name, int yearLevel) { this.studentId = studentId; this.name = name; if (yearLevel > 0) { this.yearLevel = yearLevel; } else { this.yearLevel = 1; // Default year level } } // Getters public String getStudentId() { return studentId; } public String getName() { return name; } public int getYearLevel() { return yearLevel; } // Setters (Example) public void setName(String name) { this.name = name; } public void incrementYearLevel() { this.yearLevel++; System.out.println(name + " moved to year " + yearLevel); } // Display student info public void displayInfo() { System.out.println("--- Student Information ---"); System.out.println("ID: " + studentId); System.out.println("Name: " + name); System.out.println("Year Level: " + yearLevel); System.out.println("-------------------------"); } }
-
Define the
Code:Professor
Class (Optional - Simple Version): CreateProfessor.java
.Save and exit.public class Professor { private String employeeId; private String name; private String department; // Could add a list of courses taught public Professor(String employeeId, String name, String department) { this.employeeId = employeeId; this.name = name; this.department = department; } // Getters public String getEmployeeId() { return employeeId; } public String getName() { return name; } public String getDepartment() { return department; } // Setter public void setDepartment(String department) { this.department = department; } // Display info public void displayInfo() { System.out.println("--- Professor Information ---"); System.out.println("ID: " + employeeId); System.out.println("Name: " + name); System.out.println("Department: " + department); System.out.println("---------------------------"); } // Example behavior public void teachCourse(Course course) { System.out.println("Professor " + this.name + " is teaching " + course.getCourseCode() + " - " + course.getCourseName()); } }
-
Create the Main Application Class: Create
Code:UniversityDemo.java
to create instances and demonstrate interactions.Save and exit.public class UniversityDemo { public static void main(String[] args) { System.out.println("--- Setting up University Entities ---"); // Create Courses Course cs101 = new Course("CS101", "Intro to Programming", 3); // Small capacity for testing Course math201 = new Course("MA201", "Calculus II", 50); // Create Students Student alice = new Student("S1001", "Alice Smith", 1); Student bob = new Student("S1002", "Bob Johnson", 2); Student charlie = new Student("S1003", "Charlie Brown", 1); Student diana = new Student("S1004", "Diana Prince", 1); // Try to enroll 4th student in CS101 // Create Professor Professor davis = new Professor("P501", "Dr. Davis", "Computer Science"); System.out.println("\n--- Displaying Initial Info ---"); cs101.displayInfo(); math201.displayInfo(); alice.displayInfo(); bob.displayInfo(); davis.displayInfo(); System.out.println("\n--- Performing Actions ---"); // Professor teaches a course davis.teachCourse(cs101); davis.teachCourse(math201); // Students enroll in courses System.out.println("\nEnrolling students in " + cs101.getCourseCode() + ":"); if (cs101.enrollStudent()) { // Alice enrolls System.out.println(alice.getName() + " successfully enrolled."); } if (cs101.enrollStudent()) { // Bob enrolls System.out.println(bob.getName() + " successfully enrolled."); } if (cs101.enrollStudent()) { // Charlie enrolls System.out.println(charlie.getName() + " successfully enrolled."); } if (cs101.enrollStudent()) { // Diana tries (should fail) System.out.println(diana.getName() + " attempting enrollment..."); } else { System.out.println(diana.getName() + " enrollment failed."); } System.out.println("\nEnrolling students in " + math201.getCourseCode() + ":"); if (math201.enrollStudent()) { // Alice enrolls System.out.println(alice.getName() + " successfully enrolled."); } if (math201.enrollStudent()) { // Bob enrolls System.out.println(bob.getName() + " successfully enrolled."); } // Display updated course info System.out.println("\n--- Updated Course Info ---"); cs101.displayInfo(); math201.displayInfo(); // Student actions System.out.println("\n--- Student Actions ---"); bob.incrementYearLevel(); bob.displayInfo(); // Drop a student System.out.println("\nDropping a student from " + cs101.getCourseCode() + ":"); if (cs101.dropStudent()) { System.out.println("A student was dropped."); } cs101.displayInfo(); System.out.println("\n--- University Demo Finished ---"); } }
-
Compile All Java Files:
Address any compilation errors by checking your code in the respective files. -
Run the Demonstration:
-
Analyze the Output: Observe how objects are created, how their methods are called, how encapsulation protects data (e.g.,
currentEnrollment
is managed byenrollStudent
/dropStudent
), and how different objects interact (e.g.,Professor
interacting withCourse
).
This workshop demonstrates the practical application of fundamental OOP concepts like classes, objects, constructors, methods, and encapsulation to model a simplified real-world scenario. You can extend this model further by adding more attributes, methods, or even introducing inheritance (e.g., UndergraduateStudent
and GraduateStudent
extending Student
).
3. Intermediate Java Concepts
Building upon the fundamentals of OOP, this section delves into more advanced features of the Java language that are essential for building robust and scalable applications.
Packages
As applications grow, organizing code becomes crucial. Packages in Java are used to group related classes, interfaces, and sub-packages, preventing naming conflicts and controlling access. Think of them like directories or folders for your code.
Declaring a Package:
To place a class or interface inside a package, use the package
keyword as the very first statement in the source file.
// File: com/mycompany/utils/StringUtils.java
package com.mycompany.utils; // Package declaration
public class StringUtils {
public static boolean isEmpty(String str) {
return str == null || str.trim().length() == 0;
}
// Other utility methods...
}
Package Naming Convention:
Packages are named in reverse domain name order to ensure uniqueness globally. For example, if a company has the domain mycompany.com
, its packages would start with com.mycompany
. Sub-packages further categorize the code (e.g., com.mycompany. C
for core functionalities, com.mycompany.ui
for user interface components). Names are typically all lowercase.
Directory Structure:
The package structure must match the directory structure on the file system where the source (.java
) and compiled (.class
) files reside. For the com.mycompany.utils.StringUtils
class:
- The source file
StringUtils.java
must be located in<source_root>/com/mycompany/utils/
. - The compiled file
StringUtils.class
must be located in<class_output_root>/com/mycompany/utils/
.
<source_root>
and <class_output_root>
are base directories for your source code and compiled code, respectively (often src/main/java
and target/classes
in build tools like Maven/Gradle).
Using Classes from Packages (import
):
To use a class or interface from a different package, you have three options:
-
Fully Qualified Name: Refer to the class using its full package name every time.
// File: com/mycompany/app/MainApp.java package com.mycompany.app; public class MainApp { public static void main(String[] args) { String test = " "; // Using fully qualified name boolean empty = com.mycompany.utils.StringUtils.isEmpty(test); System.out.println("Is empty? " + empty); // true java.util.ArrayList<String> names = new java.util.ArrayList<>(); // Tedious names.add("Alice"); System.out.println(names.get(0)); } }
-
Single Type Import: Import a specific class or interface using the
import
keyword after thepackage
declaration (if any) and before the class definition. This is the most common approach.// File: com/mycompany/app/MainApp.java package com.mycompany.app; // Import specific classes import com.mycompany.utils.StringUtils; import java.util.ArrayList; // Import ArrayList from java.util public class MainApp { public static void main(String[] args) { String test = " "; // Now we can use the simple name boolean empty = StringUtils.isEmpty(test); System.out.println("Is empty? " + empty); ArrayList<String> names = new ArrayList<>(); // Much cleaner names.add("Bob"); System.out.println(names.get(0)); } }
-
Import on Demand (Wildcard): Import all public classes and interfaces from a package using the asterisk (
*
) wildcard. Generally discouraged for larger projects as it can make it unclear where a class comes from and potentially lead to naming conflicts if multiple imported packages have classes with the same name.package com.mycompany.app; import com.mycompany.utils.*; // Imports StringUtils and any other public class in utils import java.util.*; // Imports ArrayList, Scanner, List, Map, etc. public class MainApp { public static void main(String[] args) { boolean empty = StringUtils.isEmpty(""); // Works ArrayList<String> list = new ArrayList<>(); // Works Scanner scanner = new Scanner(System.in); // Works } }
Static Import:
Allows importing public static
members (fields and methods) of a class, so you can use them without qualifying them with the class name.
// Import static members PI and sqrt from Math class
import static java.lang.Math.PI;
import static java.lang.Math.sqrt;
// Import all static members from System.out
import static java.lang.System.out;
public class StaticImportDemo {
public static void main(String[] args) {
double radius = 5.0;
// Using imported static members directly
double area = PI * radius * radius;
double num = 16.0;
double root = sqrt(num);
// Using static member 'out' from System
out.println("Radius: " + radius); // Instead of System.out.println
out.printf("Area: %.2f\n", area);
out.printf("Square root of %.1f is %.1f\n", num, root);
}
}
CLASSPATH Environment Variable:
The CLASSPATH tells the Java Virtual Machine (JVM) and the Java compiler (javac
) where to find user-defined classes and packages. When you run java com.mycompany.app.MainApp
, the JVM needs to locate the MainApp.class
file within the com/mycompany/app
directory structure.
- Default: By default, the JVM looks in the current directory (
.
) and potentially internal libraries. - Setting CLASSPATH: You can explicitly set the
CLASSPATH
environment variable on Linux: - Using
-cp
or-classpath
: It's generally better practice to specify the classpath directly when compiling or running, rather than relying on the environment variable. This makes builds more reproducible.Modern build tools like Maven and Gradle handle classpath management automatically, which is highly recommended for any non-trivial project.# Assume compiled classes are in a 'bin' directory # Compiling (source files in 'src') javac -d bin -sourcepath src src/com/mycompany/app/MainApp.java src/com/mycompany/utils/StringUtils.java # Running (telling java where to find the compiled classes) java -cp bin com.mycompany.app.MainApp # Or if using a JAR file # java -cp bin:lib/someDependency.jar com.mycompany.app.MainApp
Exception Handling
Exceptions are events that occur during program execution that disrupt the normal flow of instructions (e.g., dividing by zero, trying to access a file that doesn't exist, accessing a null
object reference). Java's exception handling mechanism provides a robust way to detect, report, and manage these errors.
Exception Hierarchy:
All exception types are subclasses of the java.lang.Throwable
class. Key branches:
Error
: Represents serious problems that applications usually shouldn't try to catch (e.g.,OutOfMemoryError
,StackOverflowError
). These typically indicate unrecoverable conditions in the JVM itself.Exception
: Represents conditions that applications might want to catch and handle.- Checked Exceptions: Subclasses of
Exception
but not subclasses ofRuntimeException
. These are exceptions that the compiler forces you to handle explicitly, either by catching them or declaring that your method throws them. They represent exceptional conditions that can reasonably be expected to occur in a correct program (e.g.,IOException
,SQLException
,FileNotFoundException
). - Unchecked Exceptions (Runtime Exceptions): Subclasses of
RuntimeException
. The compiler does not require you to handle these explicitly. They often indicate programming errors (e.g.,NullPointerException
,ArrayIndexOutOfBoundsException
,ArithmeticException
,IllegalArgumentException
). While you can catch them, it's often better to fix the underlying code bug.
- Checked Exceptions: Subclasses of
Handling Exceptions (try
, catch
, finally
):
try
block: Encloses the code that might potentially throw an exception.catch
block: Follows thetry
block. Catches and handles a specific type of exception. You can have multiplecatch
blocks to handle different exception types. The first matchingcatch
block (based on the exception type or its superclasses) is executed.finally
block: (Optional) Follows thetry
block or the lastcatch
block. This block always executes, regardless of whether an exception occurred in thetry
block or whether a caught exception was handled. It's typically used for cleanup code (e.g., closing files, releasing network connections, closing database connections) to ensure resources are released properly.
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ExceptionHandlingDemo {
public static void main(String[] args) {
readFile("mydata.txt");
System.out.println("-----");
readFile("nonexistent.txt"); // This will cause FileNotFoundException
System.out.println("-----");
performCalculation(10, 0); // This will cause ArithmeticException
System.out.println("-----");
processString(null); // This will cause NullPointerException
System.out.println("Program finished main method.");
}
public static void readFile(String filename) {
BufferedReader reader = null; // Declare outside try to access in finally
try {
System.out.println("Attempting to open file: " + filename);
reader = new BufferedReader(new FileReader(filename)); // May throw FileNotFoundException
String line;
System.out.println("File opened successfully. Reading contents:");
while ((line = reader.readLine()) != null) { // May throw IOException
System.out.println(line);
}
System.out.println("Finished reading file.");
} catch (FileNotFoundException fnfEx) { // Catch specific checked exception
System.err.println("Error: File not found - " + fnfEx.getMessage());
// fnfEx.printStackTrace(); // Useful for debugging - prints stack trace
} catch (IOException ioEx) { // Catch more general checked exception
System.err.println("Error reading file: " + ioEx.getMessage());
// ioEx.printStackTrace();
} catch (Exception ex) { // Catch any other potential exceptions (less specific)
System.err.println("An unexpected error occurred: " + ex.getMessage());
// ex.printStackTrace();
} finally {
System.out.println("Entering finally block for readFile...");
try {
if (reader != null) {
System.out.println("Closing the file reader.");
reader.close(); // May also throw IOException, hence nested try-catch
}
} catch (IOException closeEx) {
System.err.println("Error closing file reader: " + closeEx.getMessage());
}
System.out.println("Exiting finally block for readFile.");
}
}
public static void performCalculation(int a, int b) {
try {
System.out.printf("Calculating %d / %d...\n", a, b);
int result = a / b; // May throw ArithmeticException (unchecked)
System.out.println("Result: " + result);
} catch (ArithmeticException ae) {
System.err.println("Error: Cannot divide by zero. " + ae.getMessage());
}
// No finally block needed here as no resources need cleanup
System.out.println("Calculation attempt finished.");
}
public static void processString(String text) {
try {
System.out.println("Processing string: " + text);
System.out.println("Length: " + text.length()); // May throw NullPointerException (unchecked)
} catch (NullPointerException npe) {
System.err.println("Error: Cannot process a null string. " + npe.getMessage());
}
System.out.println("String processing attempt finished.");
}
}
Declaring Exceptions (throws
):
If a method might throw a checked exception but doesn't handle it with try-catch
, it must declare that it throws the exception using the throws
keyword in its signature. This signals to the caller method that it needs to handle or declare the exception itself.
import java.io.IOException;
public class ThrowsDemo {
// This method might throw IOException (checked), so it declares it
public static void readFileAndThrow(String filename) throws IOException {
// Simplified example - assumes file exists for brevity
// In reality, use try-finally or try-with-resources here too
java.io.Reader reader = new java.io.FileReader(filename);
System.out.println("File opened in readFileAndThrow");
// ... read logic ...
reader.close(); // close can also throw IOException
System.out.println("File closed in readFileAndThrow");
// Simulate another potential IO error
if (filename.contains("error")) {
throw new IOException("Simulated I/O error in readFileAndThrow");
}
}
// This method calls readFileAndThrow, so it must handle or declare IOException
public static void main(String[] args) /* throws IOException */ { // Option 1: Declare
try { // Option 2: Handle
System.out.println("Calling readFileAndThrow with goodfile.txt");
// Create a dummy file first for this to run without FileNotFoundException
// In Linux terminal: touch goodfile.txt
// In Linux terminal: touch errorfile.txt
readFileAndThrow("goodfile.txt");
System.out.println("\nCalling readFileAndThrow with errorfile.txt");
readFileAndThrow("errorfile.txt"); // This will throw the simulated IOException
System.out.println("This line won't be reached if errorfile throws exception.");
} catch (IOException e) {
System.err.println("Caught IOException in main: " + e.getMessage());
// e.printStackTrace();
} finally {
System.out.println("Main method's finally block.");
}
System.out.println("Main method finished normally (or after catching).");
}
}
throw
Keyword:
Used to manually throw an exception object. You can throw existing exception types or custom exception types.
public class ThrowDemo {
public static void checkAge(int age) {
if (age < 18) {
// Manually throw an unchecked exception
throw new IllegalArgumentException("Access denied - You must be at least 18 years old.");
} else {
System.out.println("Access granted - Age is sufficient.");
}
}
public static void main(String[] args) {
try {
checkAge(25); // OK
checkAge(15); // Throws IllegalArgumentException
System.out.println("This won't be printed after age 15 check.");
} catch (IllegalArgumentException e) {
System.err.println("Error: " + e.getMessage());
}
System.out.println("Program continues after age check.");
}
}
Try-with-Resources (Java 7+):
A simplified syntax for handling resources (like file streams, network sockets, database connections) that implement the java.lang.AutoCloseable
or java.io.Closeable
interface. It ensures that the resource is automatically closed at the end of the try
block, whether it completes normally or throws an exception. This eliminates the need for explicit finally
blocks just for closing resources.
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
public class TryWithResourcesDemo {
public static void main(String[] args) {
readFileEfficiently("mydata.txt"); // Assumes mydata.txt exists
System.out.println("-----");
readFileEfficiently("nonexistent.txt");
}
public static void readFileEfficiently(String filename) {
System.out.println("Attempting to read with try-with-resources: " + filename);
// Declare resources within the try(...) parentheses
// They will be automatically closed in reverse order of declaration
try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
String line;
System.out.println("File opened. Reading contents:");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
System.out.println("Finished reading file.");
// No explicit reader.close() needed! It happens automatically.
} catch (IOException e) { // Still catch potential exceptions during read/open
System.err.println("Error during file operation: " + e.getMessage());
// Throwable.getSuppressed() can be used here to see exceptions during close
// if an exception was already thrown in the try block.
}
// The finally block for closing is implicitly handled by try-with-resources
System.out.println("Exiting readFileEfficiently method.");
}
}
finally
blocks for managing resources that implement AutoCloseable
.
Custom Exceptions:
You can create your own exception classes by extending Exception
(for checked) or RuntimeException
(for unchecked). This allows you to represent application-specific error conditions more clearly.
// Custom checked exception
class InsufficientFundsException extends Exception {
private double shortfall;
public InsufficientFundsException(String message, double shortfall) {
super(message); // Pass message to the superclass (Exception) constructor
this.shortfall = shortfall;
}
public double getShortfall() {
return shortfall;
}
}
// Example usage (simplified BankAccount)
class SimpleAccount {
private double balance;
public SimpleAccount(double balance) { this.balance = balance; }
// Method declares it can throw our custom checked exception
public void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
double needed = amount - balance;
throw new InsufficientFundsException("Withdrawal amount exceeds balance.", needed);
}
balance -= amount;
System.out.printf("Withdrew %.2f. New balance: %.2f\n", amount, balance);
}
}
// Demo
public class CustomExceptionDemo {
public static void main(String[] args) {
SimpleAccount account = new SimpleAccount(100.0);
try {
account.withdraw(50.0); // OK
account.withdraw(70.0); // Throws InsufficientFundsException
System.out.println("This line is skipped.");
} catch (InsufficientFundsException e) {
System.err.println("Transaction Failed: " + e.getMessage());
System.err.printf("Amount short: %.2f\n", e.getShortfall());
// e.printStackTrace();
}
System.out.println("Program continues.");
}
}
Working with Arrays and Collections
Storing and managing groups of objects or primitive values is a common requirement. Java provides built-in arrays and a rich Collections Framework.
Arrays:
- Fixed-size, ordered containers holding elements of the same data type (primitive or reference).
- Size is determined at creation and cannot be changed later.
- Elements are accessed using a zero-based index (
array[0]
,array[1]
, ...). - Provide fast access based on index but inefficient insertion/deletion in the middle.
public class ArrayDemo {
public static void main(String[] args) {
// --- Declaration and Initialization ---
// 1. Declare and allocate size, initialize with default values
int[] numbers = new int[5]; // Array of 5 integers, initialized to 0
String[] names = new String[3]; // Array of 3 Strings, initialized to null
boolean[] flags = new boolean[2]; // Array of 2 booleans, initialized to false
// 2. Declare and initialize with specific values (Array Initializer)
double[] scores = {95.5, 88.0, 75.5, 99.9}; // Size inferred from values (4)
String[] weekdays = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
// --- Accessing Elements ---
System.out.println("First number (default): " + numbers[0]); // Output: 0
System.out.println("First score: " + scores[0]); // Output: 95.5
System.out.println("Third weekday: " + weekdays[2]); // Output: Wed
// --- Modifying Elements ---
numbers[0] = 100;
numbers[1] = 200;
// numbers[5] = 500; // THROWS ArrayIndexOutOfBoundsException at runtime! Index 5 is invalid for size 5.
System.out.println("First number (modified): " + numbers[0]); // Output: 100
// --- Getting Array Length ---
System.out.println("Length of scores array: " + scores.length); // Output: 4
System.out.println("Length of names array: " + names.length); // Output: 3
// --- Iterating through Arrays ---
System.out.println("\nIterating through weekdays using standard for loop:");
for (int i = 0; i < weekdays.length; i++) {
System.out.print(weekdays[i] + " ");
}
System.out.println();
System.out.println("\nIterating through scores using enhanced for loop (for-each):");
for (double score : scores) {
System.out.print(score + " ");
}
System.out.println();
// --- Arrays Utility Class ---
// java.util.Arrays provides helpful static methods
java.util.Arrays.sort(scores); // Sorts the array in place
System.out.println("\nScores after sorting: " + java.util.Arrays.toString(scores)); // Convenient printing
int[] numsToSort = {5, 2, 8, 1, 9};
java.util.Arrays.sort(numsToSort);
System.out.println("Sorted numsToSort: " + java.util.Arrays.toString(numsToSort)); // [1, 2, 5, 8, 9]
// Binary search (requires sorted array)
int index = java.util.Arrays.binarySearch(numsToSort, 8);
System.out.println("Index of 8: " + index); // Output: 3
index = java.util.Arrays.binarySearch(numsToSort, 3);
System.out.println("Index of 3 (not present): " + index); // Output: negative value (-3)
// Filling an array
java.util.Arrays.fill(numbers, -1); // Fills the entire array with -1
System.out.println("Numbers after fill: " + java.util.Arrays.toString(numbers)); // [-1, -1, -1, -1, -1]
// Copying arrays
int[] numbersCopy = java.util.Arrays.copyOf(numsToSort, numsToSort.length);
System.out.println("Copied array: " + java.util.Arrays.toString(numbersCopy));
// Multi-dimensional Arrays (Arrays of arrays)
int[][] matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
System.out.println("\nElement at matrix[1][1]: " + matrix[1][1]); // Output: 5
System.out.println("Number of rows: " + matrix.length); // 3
System.out.println("Number of columns in row 0: " + matrix[0].length); // 3
}
}
Java Collections Framework:
Provides a suite of interfaces and classes for managing groups of objects more flexibly than arrays. Located primarily in the java.util
package.
Key Interfaces:
Collection<E>
: The root interface. Represents a group of objects (elements). Defines basic methods likeadd()
,remove()
,size()
,isEmpty()
,contains()
,iterator()
.List<E>
: An ordered collection (sequence) that allows duplicate elements. Elements can be accessed by their integer index. Common implementations:ArrayList<E>
: Resizable array implementation. Fast random access (get(index)
), relatively fast adds at the end. Slower insertion/deletion in the middle (requires shifting elements). Good general-purpose list.LinkedList<E>
: Doubly-linked list implementation. Fast insertion/deletion at the beginning, end, or middle (if you have an iterator at the position). Slower random access (requires traversing the list). Also implementsDeque
(double-ended queue).
Set<E>
: A collection that does not allow duplicate elements. Order is generally not guaranteed (except for specific implementations). Useful for storing unique items. Common implementations:HashSet<E>
: Uses a hash table for storage. Offers fastadd
,remove
,contains
operations (average O(1) time). Does not maintain insertion order. Requires elements to have consistenthashCode()
andequals()
methods.LinkedHashSet<E>
: LikeHashSet
, but also maintains the order in which elements were inserted. Slightly slower thanHashSet
due to the linked list overhead.TreeSet<E>
: Stores elements in a sorted order (natural ordering or specified by aComparator
). Uses a tree structure (Red-Black tree). Slower thanHashSet
(O(log n) for operations) but provides sorted iteration. Requires elements to be comparable (implementComparable
or provide aComparator
).
Map<K, V>
: An object that maps keys to values. Keys must be unique. Each key maps to exactly one value. Not technically aCollection
(doesn't extend it) but part of the framework. Common implementations:HashMap<K, V>
: Uses a hash table. Fastput
,get
,containsKey
operations (average O(1)). Order is not guaranteed. Allows onenull
key and multiplenull
values. Requires keys to have consistenthashCode()
andequals()
.LinkedHashMap<K, V>
: LikeHashMap
, but maintains insertion order (or access order). Slower thanHashMap
.TreeMap<K, V>
: Stores key-value pairs sorted by key (natural ordering orComparator
). Uses a tree structure. Slower thanHashMap
(O(log n)) but provides sorted iteration over keys. Keys must be comparable.
Queue<E>
: A collection designed for holding elements prior to processing. Typically orders elements FIFO (First-In, First-Out). Common implementations:LinkedList<E>
(also implementsQueue
)PriorityQueue<E>
(elements ordered by priority, not FIFO)
Deque<E>
: (Double-Ended Queue) ExtendsQueue
. Allows element addition/removal from both ends. Implementations:LinkedList<E>
,ArrayDeque<E>
.
Generics (<E>
, <K, V>
):
Collections use generics to provide type safety. List<String>
means the list can only hold String
objects. This allows the compiler to catch errors where you try to add an incompatible type (e.g., adding an Integer
to a List<String>
). It also eliminates the need for manual casting when retrieving elements.
Iterators:
An Iterator
is an object used to traverse (iterate over) the elements of a collection. It provides a standard way to access elements sequentially without exposing the underlying structure.
iterator()
: Method in theCollection
interface that returns anIterator
for the collection.hasNext()
: Returnstrue
if the iteration has more elements.next()
: Returns the next element in the iteration and advances the iterator.remove()
: (Optional) Removes the last element returned bynext()
from the underlying collection.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
public class CollectionsDemo {
public static void main(String[] args) {
// --- List Example (ArrayList) ---
System.out.println("--- List (ArrayList) ---");
List<String> fruits = new ArrayList<>(); // Use Interface type for reference
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
fruits.add("Apple"); // Duplicates allowed
System.out.println("Fruits list: " + fruits);
System.out.println("Element at index 1: " + fruits.get(1)); // Banana
System.out.println("Size: " + fruits.size()); // 4
System.out.println("Contains 'Banana'? " + fruits.contains("Banana")); // true
fruits.remove("Apple"); // Removes the first occurrence
System.out.println("After removing first 'Apple': " + fruits);
System.out.println("Iterating with enhanced for loop:");
for (String fruit : fruits) {
System.out.print(fruit + " ");
}
System.out.println();
System.out.println("Iterating with Iterator:");
Iterator<String> fruitIterator = fruits.iterator();
while (fruitIterator.hasNext()) {
String fruit = fruitIterator.next();
System.out.print(fruit + " ");
// Example of removing during iteration (use iterator's remove)
// if (fruit.equals("Banana")) {
// fruitIterator.remove();
// }
}
System.out.println();
// --- Set Example (HashSet & TreeSet) ---
System.out.println("\n--- Set (HashSet) ---");
Set<String> uniqueColors = new HashSet<>();
uniqueColors.add("Red");
uniqueColors.add("Green");
uniqueColors.add("Blue");
boolean addedRedAgain = uniqueColors.add("Red"); // Returns false, set not changed
System.out.println("Unique colors (HashSet - order not guaranteed): " + uniqueColors);
System.out.println("Added 'Red' again? " + addedRedAgain); // false
System.out.println("Contains 'Green'? " + uniqueColors.contains("Green")); // true
System.out.println("\n--- Set (TreeSet) ---");
Set<Integer> sortedNumbers = new TreeSet<>();
sortedNumbers.add(50);
sortedNumbers.add(10);
sortedNumbers.add(90);
sortedNumbers.add(10); // Duplicate, ignored
System.out.println("Sorted numbers (TreeSet - natural order): " + sortedNumbers);
// --- Map Example (HashMap) ---
System.out.println("\n--- Map (HashMap) ---");
Map<String, Integer> studentAges = new HashMap<>();
studentAges.put("Alice", 20);
studentAges.put("Bob", 22);
studentAges.put("Charlie", 20);
studentAges.put("Alice", 21); // Updates value for existing key "Alice"
System.out.println("Student ages map: " + studentAges);
System.out.println("Age of Bob: " + studentAges.get("Bob")); // 22
System.out.println("Contains key 'David'? " + studentAges.containsKey("David")); // false
System.out.println("Contains value 21? " + studentAges.containsValue(21)); // true
studentAges.remove("Charlie");
System.out.println("After removing Charlie: " + studentAges);
System.out.println("\nIterating through Map keys:");
for (String name : studentAges.keySet()) { // Get set of keys
System.out.println("Key: " + name);
}
System.out.println("\nIterating through Map values:");
for (Integer age : studentAges.values()) { // Get collection of values
System.out.println("Value: " + age);
}
System.out.println("\nIterating through Map entries (key-value pairs):");
for (Map.Entry<String, Integer> entry : studentAges.entrySet()) { // Get set of entries
System.out.println("Key=" + entry.getKey() + ", Value=" + entry.getValue());
}
}
}
String Manipulation
Strings (java.lang.String
) are used extensively to represent text. In Java, String
objects are immutable, meaning their state (the sequence of characters) cannot be changed after creation. Operations that appear to modify a String actually create and return a new String object.
public class StringDemo {
public static void main(String[] args) {
String greeting = "Hello";
String name = "World";
// Concatenation: Creates new strings
String message1 = greeting + ", " + name + "!"; // Using + operator
String message2 = greeting.concat(", ").concat(name).concat("!"); // Using concat() method
System.out.println("message1: " + message1);
System.out.println("message2: " + message2);
// Immutability example
String original = "Java";
String modified = original.toUpperCase(); // Creates a NEW string "JAVA"
System.out.println("original: " + original); // Output: Java (original is unchanged)
System.out.println("modified: " + modified); // Output: JAVA
// Common String methods
String text = " Java Programming Language ";
System.out.println("\nOriginal text: [" + text + "]");
System.out.println("Length: " + text.length()); // 29
System.out.println("Character at index 3: " + text.charAt(3)); // 'J'
System.out.println("Substring (index 7 to 18): [" + text.substring(7, 19) + "]"); // [Programming L] (end index is exclusive)
System.out.println("Substring (from index 19): [" + text.substring(19) + "]"); // [anguage ]
System.out.println("Lowercase: [" + text.toLowerCase() + "]");
System.out.println("Uppercase: [" + text.toUpperCase() + "]");
System.out.println("Trimmed (removes leading/trailing whitespace): [" + text.trim() + "]");
System.out.println("Starts with ' Java': " + text.startsWith(" Java")); // true
System.out.println("Ends with 'ge ': " + text.endsWith("ge ")); // true
System.out.println("Contains 'Program': " + text.contains("Program")); // true
System.out.println("Index of 'Lang': " + text.indexOf("Lang")); // 19
System.out.println("Index of 'a' (first): " + text.indexOf('a')); // 4
System.out.println("Index of 'a' (last): " + text.lastIndexOf('a')); // 23
System.out.println("Replace 'a' with '@': [" + text.replace('a', '@') + "]");
System.out.println("Replace 'Java' with 'Python': [" + text.replace("Java", "Python") + "]");
// Splitting a string
String csvData = "Apple,Banana,Orange,Grape";
String[] fruitsArray = csvData.split(","); // Split by comma
System.out.println("\nSplitting CSV:");
for (String fruit : fruitsArray) {
System.out.println("- " + fruit.trim());
}
// Joining an array into a string (Java 8+)
String joinedFruits = String.join(" | ", fruitsArray);
System.out.println("Joined fruits: " + joinedFruits);
// Checking for emptiness
String emptyStr = "";
String blankStr = " ";
System.out.println("\nChecking emptiness:");
System.out.println("emptyStr.isEmpty(): " + emptyStr.isEmpty()); // true
System.out.println("blankStr.isEmpty(): " + blankStr.isEmpty()); // false
System.out.println("emptyStr.isBlank(): " + emptyStr.isBlank()); // true (Java 11+)
System.out.println("blankStr.isBlank(): " + blankStr.isBlank()); // true (Java 11+)
// String comparison
String s1 = "Hello";
String s2 = "Hello"; // Literal, often points to same object in string pool
String s3 = new String("Hello"); // Explicitly creates new object
String s4 = "HELLO";
System.out.println("\nString Comparison:");
System.out.println("s1 == s2: " + (s1 == s2)); // true (usually, due to string pool)
System.out.println("s1 == s3: " + (s1 == s3)); // false (different objects)
System.out.println("s1.equals(s2): " + s1.equals(s2)); // true (compares content)
System.out.println("s1.equals(s3): " + s1.equals(s3)); // true (compares content)
System.out.println("s1.equals(s4): " + s1.equals(s4)); // false (case-sensitive)
System.out.println("s1.equalsIgnoreCase(s4): " + s1.equalsIgnoreCase(s4)); // true (ignores case)
// ALWAYS use .equals() or .equalsIgnoreCase() to compare string content, not ==.
}
}
StringBuilder
and StringBuffer
:
Since String
is immutable, performing many modifications (like concatenating in a loop) can be inefficient as it creates numerous intermediate String
objects. For mutable string operations, use:
StringBuilder
: Mutable sequence of characters. Not thread-safe (faster). Use when modifications happen within a single thread.StringBuffer
: Mutable sequence of characters. Thread-safe (synchronized methods, slower). Use when modifications might happen across multiple threads (less common nowadays; concurrent collections are often preferred).
public class StringBuilderDemo {
public static void main(String[] args) {
// Inefficient way using String concatenation in a loop
String resultString = "";
long startTime = System.nanoTime();
for (int i = 0; i < 10000; i++) {
resultString += i; // Creates many intermediate String objects
}
long endTime = System.nanoTime();
System.out.println("String concatenation time: " + (endTime - startTime) / 1e6 + " ms");
// System.out.println("Length: " + resultString.length());
// Efficient way using StringBuilder
StringBuilder sb = new StringBuilder();
startTime = System.nanoTime();
for (int i = 0; i < 10000; i++) {
sb.append(i); // Modifies the internal buffer of StringBuilder
}
endTime = System.nanoTime();
System.out.println("StringBuilder append time: " + (endTime - startTime) / 1e6 + " ms");
// Get the final String
String finalResult = sb.toString();
// System.out.println("Length: " + finalResult.length());
// Other StringBuilder methods
StringBuilder builder = new StringBuilder("Start");
builder.append(" Middle"); // Appends
builder.insert(5, "->"); // Inserts at index 5
builder.replace(0, 5, "Begin"); // Replaces characters from index 0 to 4
builder.delete(5, 8); // Deletes characters from index 5 to 7
builder.reverse(); // Reverses the content
System.out.println("\nBuilder result: " + builder.toString());
}
}
StringBuilder
(or StringBuffer
if thread safety is strictly required) when you need to perform multiple modifications to a sequence of characters.
File I/O
Java provides APIs for reading from and writing to files, essential for data persistence and interaction with the file system. There are two main packages:
java.io
(Older API): Stream-based (byte streams likeFileInputStream
,FileOutputStream
; character streams likeFileReader
,FileWriter
). Often uses Decorator pattern (wrapping streams likeBufferedReader
,PrintWriter
). Blocking I/O.java.nio
(New I/O - Java 1.4+): Buffer-oriented, channel-based. Offers non-blocking I/O capabilities (more scalable for high-concurrency network applications) and more comprehensive file system operations (Files
,Paths
,Path
). Generally preferred for new development, especially for more advanced file operations or non-blocking needs.
Reading Files (java.io
- Character Stream):
Using BufferedReader
for efficient reading of text files line by line.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.FileNotFoundException;
public class ReadFileIO {
public static void main(String[] args) {
String filename = "sample.txt"; // Create this file first
// Create a sample file using Linux terminal:
// echo "This is line 1." > sample.txt
// echo "This is line 2." >> sample.txt
// echo "End of file." >> sample.txt
System.out.println("--- Reading with java.io (BufferedReader) ---");
// Use try-with-resources for automatic closing
try (FileReader fileReader = new FileReader(filename); // Opens connection to file (character based)
BufferedReader bufferedReader = new BufferedReader(fileReader)) { // Wraps for efficiency
System.out.println("Reading file: " + filename);
String line;
int lineNum = 1;
while ((line = bufferedReader.readLine()) != null) { // Read line by line
System.out.printf("Line %d: %s\n", lineNum++, line);
}
System.out.println("Finished reading.");
} catch (FileNotFoundException e) {
System.err.println("Error: File not found '" + filename + "'. " + e.getMessage());
} catch (IOException e) {
System.err.println("Error reading file '" + filename + "': " + e.getMessage());
// e.printStackTrace();
}
}
}
Writing Files (java.io
- Character Stream):
Using FileWriter
and PrintWriter
for efficient writing of text data.
import java.io.FileWriter;
import java.io.PrintWriter;
import java.io.IOException;
public class WriteFileIO {
public static void main(String[] args) {
String filename = "output.txt";
boolean append = false; // Set to true to add to the end, false to overwrite
System.out.println("--- Writing with java.io (PrintWriter) ---");
System.out.println("Mode: " + (append ? "Append" : "Overwrite"));
// Use try-with-resources
try (FileWriter fileWriter = new FileWriter(filename, append); // Opens file connection, boolean for append mode
PrintWriter printWriter = new PrintWriter(fileWriter)) { // Wraps for println/printf methods
System.out.println("Writing to file: " + filename);
printWriter.println("This data is written from Java.");
printWriter.println("Using PrintWriter makes writing text easy.");
printWriter.printf("We can write formatted numbers like %.2f and integers %d.\n", 123.456, 789);
printWriter.println("End of write operation.");
System.out.println("Finished writing.");
} catch (IOException e) {
System.err.println("Error writing to file '" + filename + "': " + e.getMessage());
// e.printStackTrace();
}
// You can verify the contents using cat in Linux terminal:
// cat output.txt
}
}
Reading Files (java.nio
):
Using Files
and Paths
for a more modern approach.
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.Path;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.util.List;
import java.io.BufferedReader; // Can still combine nio Path with io BufferedReader
public class ReadFileNIO {
public static void main(String[] args) {
String filename = "sample.txt"; // Use the same file created earlier
Path filePath = Paths.get(filename); // Create a Path object
System.out.println("--- Reading with java.nio (Files.readAllLines) ---");
try {
// Reads the entire file into a List<String> - careful with very large files!
List<String> lines = Files.readAllLines(filePath, StandardCharsets.UTF_8); // Specify encoding
System.out.println("Reading all lines from: " + filePath.toAbsolutePath());
int lineNum = 1;
for (String line : lines) {
System.out.printf("Line %d: %s\n", lineNum++, line);
}
System.out.println("Finished reading all lines.");
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
}
System.out.println("\n--- Reading with java.nio (Files.newBufferedReader) ---");
// More memory-efficient for large files - combines nio Path with io stream processing
try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) {
System.out.println("Reading line by line using BufferedReader from Path:");
String line;
int lineNum = 1;
while ((line = reader.readLine()) != null) {
System.out.printf("Line %d: %s\n", lineNum++, line);
}
System.out.println("Finished reading line by line.");
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
}
System.out.println("\n--- Reading with java.nio (Files.lines - Stream API) ---");
// Java 8 Stream API approach - very concise, processes lazily
try {
System.out.println("Reading using Stream API:");
Files.lines(filePath, StandardCharsets.UTF_8) // Returns a Stream<String>
.map(line -> "Stream Line: " + line) // Example processing
.forEach(System.out::println); // Terminal operation
System.out.println("Finished reading with Stream.");
} catch (IOException e) {
System.err.println("Error reading file with stream: " + e.getMessage());
}
}
}
Writing Files (java.nio
):
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.charset.StandardCharsets;
import java.io.BufferedWriter; // Can combine nio Path with io BufferedWriter
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
public class WriteFileNIO {
public static void main(String[] args) {
String filename = "output_nio.txt";
Path filePath = Paths.get(filename);
System.out.println("--- Writing with java.nio (Files.write) ---");
List<String> linesToWrite = Arrays.asList(
"Written using Files.write.",
"This is line 2.",
"Supports StandardOpenOption for append/create."
);
try {
// Writes a collection of lines. Overwrites by default.
// Use StandardOpenOption.APPEND to append, CREATE to create if not exists, etc.
Files.write(filePath, linesToWrite, StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); // Overwrite
System.out.println("Wrote lines to " + filePath.toAbsolutePath());
// Append example
Files.write(filePath, Arrays.asList("This line was appended."), StandardCharsets.UTF_8,
StandardOpenOption.APPEND);
System.out.println("Appended a line.");
} catch (IOException e) {
System.err.println("Error writing file: " + e.getMessage());
}
System.out.println("\n--- Writing with java.nio (Files.newBufferedWriter) ---");
// More efficient for writing large amounts of text gradually
try (BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND /* Append again */)) {
System.out.println("Writing using BufferedWriter from Path (appending):");
writer.newLine(); // Write a newline separator
writer.write("Using BufferedWriter for efficient writing.");
writer.newLine();
writer.write("Line written via BufferedWriter.");
writer.newLine();
System.out.println("Finished writing with BufferedWriter.");
} catch (IOException e) {
System.err.println("Error writing file: " + e.getMessage());
}
// Verify with: cat output_nio.txt
}
}
Other java.nio.file.Files
utilities:
The Files
class provides many useful static methods for file system operations:
exists(Path path, LinkOption... options)
: Check if a file or directory exists.createFile(Path path, FileAttribute<?>... attrs)
: Create a new empty file.createDirectory(Path dir, FileAttribute<?>... attrs)
: Create a new directory.copy(Path source, Path target, CopyOption... options)
: Copy a file or directory.move(Path source, Path target, CopyOption... options)
: Move or rename a file or directory.delete(Path path)
: Delete a file or directory (directory must be empty).deleteIfExists(Path path)
: Delete if it exists, don't throw exception if not.size(Path path)
: Get the size of a file in bytes.isDirectory(Path path, LinkOption... options)
: Check if a path is a directory.isRegularFile(Path path, LinkOption... options)
: Check if a path is a regular file.walk(Path start, FileVisitOption... options)
: Traverse a file tree (useful for recursive operations).
Workshop Building a Simple File Explorer
Objective: Create a command-line tool in Java that allows the user to list the contents of a directory, display the contents of a text file, and copy a file. This will practice java.nio.file
APIs, exception handling, user input (Scanner
), and control flow.
Steps:
-
Directory Setup: Navigate to your Java projects directory (e.g.,
Create some dummy files and directories for testing within~/javabasics
) and create a subdirectory for this workshop.fileexplorer
: -
Create the Java Source File: Create
FileExplorer.java
. -
Write the Java Code: Enter the following code. Pay attention to the use of
Path
,Files
,try-catch
, and theScanner
.import java.io.BufferedReader; import java.io.IOException; import java.nio.file.*; import java.nio.charset.StandardCharsets; import java.util.Scanner; import java.util.stream.Stream; // For listing directory contents public class FileExplorer { private static Scanner scanner = new Scanner(System.in); private static Path currentDirectory = Paths.get(".").toAbsolutePath().normalize(); // Start in current dir public static void main(String[] args) { System.out.println("=== Simple Java File Explorer ==="); String command; do { System.out.println("\nCurrent Directory: " + currentDirectory); System.out.print("Enter command (ls, cd <dir>, cat <file>, cp <src> <dest>, exit): "); command = scanner.nextLine().trim(); executeCommand(command); } while (!command.equalsIgnoreCase("exit")); System.out.println("Exiting File Explorer. Goodbye!"); scanner.close(); } private static void executeCommand(String fullCommand) { String[] parts = fullCommand.split("\\s+", 3); // Split into max 3 parts (cmd, arg1, arg2) String command = parts[0].toLowerCase(); try { switch (command) { case "ls": listFiles(); break; case "cd": if (parts.length > 1) { changeDirectory(parts[1]); } else { System.out.println("Usage: cd <directory_name>"); } break; case "cat": if (parts.length > 1) { displayFileContent(parts[1]); } else { System.out.println("Usage: cat <filename>"); } break; case "cp": if (parts.length > 2) { copyFile(parts[1], parts[2]); } else { System.out.println("Usage: cp <source_file> <destination_file>"); } break; case "exit": // Handled by the main loop break; default: System.out.println("Unknown command: " + command); break; } } catch (InvalidPathException ipe) { System.err.println("Error: Invalid path specified. " + ipe.getMessage()); } catch (NoSuchFileException nsfe) { System.err.println("Error: File or directory not found. " + nsfe.getMessage()); } catch (DirectoryNotEmptyException dnee) { System.err.println("Error: Directory is not empty (cannot delete/overwrite?). " + dnee.getMessage()); } catch (FileAlreadyExistsException faee) { System.err.println("Error: Destination file already exists. " + faee.getMessage()); } catch (IOException ioe) { // Catch general IO errors System.err.println("An I/O error occurred: " + ioe.getMessage()); // ioe.printStackTrace(); // Uncomment for detailed debugging } catch (Exception e) { // Catch any other unexpected errors System.err.println("An unexpected error occurred: " + e.getMessage()); // e.printStackTrace(); // Uncomment for detailed debugging } } // --- Command Implementations --- private static void listFiles() throws IOException { System.out.println("\n--- Contents of " + currentDirectory + " ---"); // Use try-with-resources for the DirectoryStream try (Stream<Path> stream = Files.list(currentDirectory)) { stream.forEach(path -> { String type = Files.isDirectory(path) ? "[DIR]" : "[FILE]"; System.out.printf("%-6s %s\n", type, path.getFileName()); }); } System.out.println("---------------------------------"); } private static void changeDirectory(String target) throws IOException { Path newPath; if (target.equals("..")) { // Go up one level, handle potential null if already at root Path parent = currentDirectory.getParent(); newPath = (parent != null) ? parent : currentDirectory; } else { // Resolve target relative to current directory newPath = currentDirectory.resolve(target).normalize(); } if (Files.isDirectory(newPath)) { currentDirectory = newPath.toAbsolutePath().normalize(); // Update current directory System.out.println("Changed directory to: " + currentDirectory); } else { System.out.println("Error: '" + target + "' is not a valid directory."); } } private static void displayFileContent(String filename) throws IOException { Path filePath = currentDirectory.resolve(filename).normalize(); if (!Files.isRegularFile(filePath)) { System.out.println("Error: '" + filename + "' is not a regular file or does not exist."); return; } if (!Files.isReadable(filePath)) { System.out.println("Error: Cannot read file '" + filename + "'. Permission denied?"); return; } System.out.println("\n--- Content of " + filename + " ---"); // Use try-with-resources for BufferedReader try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } catch (IOException e) { System.err.println("Error reading file content: " + e.getMessage()); } // Other exceptions like MalformedInputException could be caught if needed System.out.println("--- End of Content ---"); } private static void copyFile(String sourceFilename, String destFilename) throws IOException { Path sourcePath = currentDirectory.resolve(sourceFilename).normalize(); Path destPath = currentDirectory.resolve(destFilename).normalize(); if (!Files.isRegularFile(sourcePath)) { System.out.println("Error: Source '" + sourceFilename + "' is not a regular file or does not exist."); return; } // Use StandardCopyOption.REPLACE_EXISTING to overwrite, or handle FileAlreadyExistsException CopyOption[] options = { StandardCopyOption.REPLACE_EXISTING }; // Overwrite if exists Files.copy(sourcePath, destPath, options); System.out.println("Copied '" + sourceFilename + "' to '" + destFilename + "'"); } }
-
Save and Exit: Save the file (
Ctrl+X
,Y
,Enter
innano
). -
Compile the Code:
Fix any compilation errors. -
Run the File Explorer:
-
Interact with the Program: Try the different commands:
ls
: Should showfile1.txt
,file2.doc
,subdir1
,emptydir
,binaryfile.bin
,FileExplorer.java
,FileExplorer.class
.cat file1.txt
: Should display "Content of file1.txt".cat non_existent.txt
: Should show a "File or directory not found" error.cd subdir1
: Should change the current directory prompt.ls
: Should now shownestedfile.txt
.cat nestedfile.txt
: Should display its content.cd ..
: Should go back to thefileexplorer
directory.cp file1.txt copy_of_file1.txt
: Should create a copy.ls
: Should show the newcopy_of_file1.txt
.cat copy_of_file1.txt
: Should show the same content asfile1.txt
.cp file1.txt subdir1
: Should copy file1.txt into the subdir1 directory (if subdir1 exists and is a directory, otherwise error). Check withls subdir1
.exit
: Should terminate the program.
This workshop provides hands-on experience with java.nio.file
for common file system tasks, reinforcing concepts of paths, file operations, directory listing, exception handling, and basic command-line argument parsing within a structured application.
4. Advanced Java Concepts
This section explores more sophisticated Java features essential for developing complex, concurrent, and networked applications, as well as techniques for introspection and modern modularity.
Multithreading and Concurrency
Modern applications often need to perform multiple tasks simultaneously (or appear to) for responsiveness and performance. Java provides extensive support for multithreading (allowing multiple threads of execution within a single program) and concurrency (managing access to shared resources by multiple threads safely).
Threads:
A thread is the smallest unit of execution within a process. A Java program starts with a main thread (the one executing the main
method). You can create additional threads to perform tasks concurrently.
Creating Threads:
-
Extend the
Thread
Class: Create a subclass ofjava.lang.Thread
and override itsrun()
method to define the task the thread will execute.class MyThread extends Thread { private String threadName; public MyThread(String name) { this.threadName = name; System.out.println("Creating " + threadName ); } @Override public void run() { // This method contains the code executed by the thread System.out.println("Running " + threadName ); try { for(int i = 4; i > 0; i--) { System.out.println("Thread: " + threadName + ", Count: " + i); // Let the thread sleep for a short time Thread.sleep(50); // Sleep for 50 milliseconds } } catch (InterruptedException e) { System.out.println("Thread " + threadName + " interrupted."); Thread.currentThread().interrupt(); // Re-set interrupt status } System.out.println("Thread " + threadName + " exiting."); } } public class ThreadDemo { public static void main(String args[]) { MyThread t1 = new MyThread("Thread-1"); MyThread t2 = new MyThread("Thread-2"); // Start the threads - calls the run() method implicitly t1.start(); t2.start(); System.out.println("Main thread finished initiating child threads."); // Main thread continues execution concurrently with t1 and t2 } }
-
Implement the
Runnable
Interface: Create a class that implements thejava.lang.Runnable
interface. This interface has a single method:run()
. Then, create aThread
object, passing an instance of yourRunnable
class to its constructor. This approach is generally preferred as it separates the task (Runnable
) from the execution mechanism (Thread
) and allows your task class to extend a different superclass if needed (Java only allows single class inheritance).class MyRunnable implements Runnable { private String taskName; private Thread thread; // Optional: reference back to the thread controlling this runnable public MyRunnable(String name) { this.taskName = name; System.out.println("Creating Runnable task: " + taskName); } @Override public void run() { System.out.println("Running task: " + taskName); try { for(int i = 0; i < 5; i++) { System.out.println("Task: " + taskName + ", Processing step: " + i); Thread.sleep(40); } } catch (InterruptedException e) { System.out.println("Task " + taskName + " interrupted."); Thread.currentThread().interrupt(); } System.out.println("Task " + taskName + " finished."); } // Optional method to start the thread associated with this runnable public void start() { System.out.println("Starting thread for task: " + taskName); if (thread == null) { thread = new Thread(this, taskName + "-Thread"); // Pass Runnable 'this' and a thread name thread.start(); } } } public class RunnableDemo { public static void main(String args[]) { MyRunnable r1 = new MyRunnable("Task-A"); MyRunnable r2 = new MyRunnable("Task-B"); // Option 1: Create Threads directly // Thread tA = new Thread(r1, "Thread-A"); // Thread tB = new Thread(r2, "Thread-B"); // tA.start(); // tB.start(); // Option 2: Use start method within Runnable (if implemented) r1.start(); r2.start(); System.out.println("Main thread finished initiating Runnable tasks."); } }
Thread Lifecycle:
Threads go through various states:
- NEW: Thread object created but
start()
not yet called. - RUNNABLE: Thread is eligible to be run by the JVM's thread scheduler. It might be currently running or waiting for the CPU.
- BLOCKED: Thread is waiting to acquire a monitor lock (e.g., entering a
synchronized
block/method). - WAITING: Thread is waiting indefinitely for another thread to perform a particular action (e.g., waiting on an object's monitor via
object.wait()
, waiting for another thread to finish viathread.join()
). - TIMED_WAITING: Thread is waiting for a specified amount of time (e.g.,
Thread.sleep(millis)
,object.wait(millis)
,thread.join(millis)
). - TERMINATED: Thread has completed its execution (exited the
run()
method).
Synchronization:
When multiple threads access and modify shared, mutable data, race conditions can occur, leading to unpredictable results. Synchronization mechanisms ensure that only one thread can access the critical section (code interacting with shared data) at a time.
-
synchronized
Methods: Add thesynchronized
keyword to a method declaration. When a thread invokes a synchronized instance method, it acquires an intrinsic lock (monitor lock) on the object (this
). No other thread can execute any synchronized instance method on the same object until the lock is released (when the method returns). For static methods, the lock is on theClass
object.class Counter { private int count = 0; // This method is synchronized on the Counter object instance public synchronized void increment() { int current = count; // Simulate some processing time - increases chance of race condition without sync try { Thread.sleep(1); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } count = current + 1; } public synchronized int getCount() { return count; } }
-
synchronized
Blocks: Synchronize only a specific block of code within a method, rather than the entire method. This allows for finer-grained locking and can improve concurrency if other parts of the method don't need synchronization. You specify the object whose lock should be acquired.Using a dedicated privateclass AnotherCounter { private long count = 0; private final Object lockObject = new Object(); // Explicit lock object public void increment() { // Non-critical code can go here... System.out.println(Thread.currentThread().getName() + " entering increment"); // Synchronize only the critical section accessing 'count' synchronized (lockObject) { // Acquire lock on lockObject System.out.println(Thread.currentThread().getName() + " acquired lock"); long current = count; try { Thread.sleep(1); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } count = current + 1; System.out.println(Thread.currentThread().getName() + " releasing lock, count=" + count); } // Lock released here // Other non-critical code... } public long getCount() { synchronized(lockObject) { // Need synchronization for reading too for visibility return count; } } }
Object
for locking (lockObject
) is often preferred over synchronizing onthis
, as it prevents external code from potentially interfering with your object's lock.
volatile
Keyword:
Ensures that reads and writes to a variable are atomic (for long
and double
) and that changes made by one thread are immediately visible to other threads (addresses memory visibility issues). It does not provide mutual exclusion like synchronized
. Useful for simple flags or status indicators shared between threads where complex atomic operations aren't needed.
class Worker extends Thread {
// Volatile guarantees visibility of changes across threads
private volatile boolean running = true;
public void run() {
while (running) {
System.out.println("Worker thread is running...");
try {
Thread.sleep(100);
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
System.out.println("Worker thread stopped.");
}
public void shutdown() {
System.out.println("Shutdown requested...");
this.running = false; // Change is guaranteed to be visible to the worker thread
}
}
public class VolatileDemo {
public static void main(String[] args) throws InterruptedException {
Worker worker = new Worker();
worker.start();
Thread.sleep(500); // Let the worker run for a bit
worker.shutdown(); // Request shutdown from main thread
worker.join(); // Wait for worker thread to terminate
System.out.println("Main thread finished.");
}
}
volatile
, the worker
thread might cache the value of running
and never see the change made by the main
thread, potentially leading to an infinite loop.
java.util.concurrent
Package:
Provides a higher-level, more flexible, and often more performant framework for managing concurrency, introduced in Java 5.
- Executor Framework (
Executor
,ExecutorService
,Executors
): Decouples task submission from task execution. Manages thread pools efficiently, reducing the overhead of creating new threads for each task.import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class ExecutorDemo { public static void main(String[] args) { // Create a fixed-size thread pool with 2 threads ExecutorService executor = Executors.newFixedThreadPool(2); System.out.println("Submitting tasks to executor..."); // Submit Runnable tasks for (int i = 1; i <= 5; i++) { int taskID = i; executor.submit(() -> { // Using lambda expression for Runnable System.out.println("Executing Task " + taskID + " on thread: " + Thread.currentThread().getName()); try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Finished Task " + taskID); }); } // Initiate shutdown - stops accepting new tasks, waits for existing tasks to finish System.out.println("Executor shutdown initiated."); executor.shutdown(); try { // Wait for tasks to complete for up to 1 minute if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { System.err.println("Tasks did not complete in time, forcing shutdown."); executor.shutdownNow(); // Attempt to interrupt running tasks } } catch (InterruptedException ie) { executor.shutdownNow(); Thread.currentThread().interrupt(); } System.out.println("All tasks completed. Executor terminated."); } }
- Concurrent Collections (
ConcurrentHashMap
,CopyOnWriteArrayList
, etc.): Thread-safe collection implementations designed for high concurrency, often offering better performance than synchronizing access to standard collections (e.g.,Collections.synchronizedMap(new HashMap<>())
). - Atomic Variables (
AtomicInteger
,AtomicBoolean
,AtomicReference
, etc.): Provide lock-free, thread-safe operations on single variables using hardware-level atomic instructions (Compare-And-Swap - CAS). Often more efficient than usingsynchronized
for simple counters or flags.import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class AtomicDemo { // Using AtomicInteger for thread-safe counting without explicit locks private static AtomicInteger atomicCount = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { ExecutorService executor = Executors.newFixedThreadPool(10); int numberOfTasks = 1000; for (int i = 0; i < numberOfTasks; i++) { executor.submit(() -> { // Atomically increment the count int currentVal = atomicCount.incrementAndGet(); System.out.println(Thread.currentThread().getName() + " incremented count to: " + currentVal); try { Thread.sleep(1); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } executor.shutdown(); executor.awaitTermination(10, TimeUnit.SECONDS); System.out.println("\nFinal Atomic Count: " + atomicCount.get()); // Should be 1000 } }
- Locks (
Lock
,ReentrantLock
,ReadWriteLock
): More flexible locking mechanisms than the intrinsicsynchronized
approach.ReentrantLock
provides features like timed lock waits, interruptible lock waits, and fairness policies.ReadWriteLock
allows multiple readers to hold the lock concurrently while writers require exclusive access, improving performance for read-heavy workloads. - Synchronizers (
CountDownLatch
,CyclicBarrier
,Semaphore
,Phaser
): Utility classes to coordinate the control flow between threads (e.g., waiting for multiple operations to complete, limiting concurrent access to a resource).
Concurrency is a complex topic. Key considerations include thread safety, liveness (avoiding deadlock, livelock, starvation), and performance. The java.util.concurrent
package provides powerful tools, but understanding the underlying principles is essential for using them correctly.
Networking
Java provides a comprehensive API for network programming, primarily in the java.net
package, allowing applications to communicate over networks using protocols like TCP/IP and UDP.
Sockets:
Sockets are the endpoints for network communication. Java provides classes for both client and server sockets.
Socket
(Client): Represents one end of a TCP connection. Used by a client to connect to a server.ServerSocket
(Server): Listens for incoming TCP connection requests from clients. When a connection is accepted, it creates aSocket
object to handle communication with that specific client.
Simple TCP Client-Server Example:
Server (SimpleServer.java
):
import java.net.ServerSocket;
import java.net.Socket;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class SimpleServer {
public static void main(String[] args) {
int port = 12345; // Port number to listen on (> 1024 recommended for non-root)
System.out.println("Server starting on port " + port + "...");
try (ServerSocket serverSocket = new ServerSocket(port)) { // Create server socket
while (true) { // Keep listening indefinitely
System.out.println("Waiting for a client connection...");
// Accept() blocks until a client connects, then returns a Socket for that client
try (Socket clientSocket = serverSocket.accept();
// Get input/output streams for the client socket
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); // Auto-flush enabled
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())))
{
System.out.println("Client connected from: " + clientSocket.getInetAddress().getHostAddress());
// Send a welcome message to the client
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME);
out.println("Welcome! Server time is " + timestamp);
// Read messages from the client and echo them back
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("Received from client: " + inputLine);
if ("bye".equalsIgnoreCase(inputLine.trim())) {
out.println("Goodbye!");
break; // Exit loop if client says bye
}
out.println("Server Echo: " + inputLine); // Echo back
}
System.out.println("Client disconnected.");
} catch (IOException e) {
System.err.println("Error handling client connection: " + e.getMessage());
}
// The client Socket, PrintWriter, and BufferedReader are auto-closed here by try-with-resources
}
} catch (IOException e) {
System.err.println("Could not listen on port " + port + ": " + e.getMessage());
// e.printStackTrace();
}
// The ServerSocket is auto-closed here
}
}
Client (SimpleClient.java
):
import java.net.Socket;
import java.net.UnknownHostException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.IOException;
import java.util.Scanner;
public class SimpleClient {
public static void main(String[] args) {
String hostname = "localhost"; // Server hostname or IP address
int port = 12345; // Server port number
System.out.println("Attempting to connect to server " + hostname + ":" + port + "...");
// Use try-with-resources for Socket and streams
try (Socket socket = new Socket(hostname, port); // Connect to the server
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
Scanner consoleInput = new Scanner(System.in)) // To read user input from console
{
System.out.println("Connected successfully!");
// Read the initial welcome message from the server
String serverResponse = in.readLine();
System.out.println("Server: " + serverResponse);
String userInput;
System.out.print("Enter message to send (or 'bye' to quit): ");
while ((userInput = consoleInput.nextLine()) != null) {
out.println(userInput); // Send user input to server
// Read the server's echo response
serverResponse = in.readLine();
System.out.println("Server: " + serverResponse);
if ("goodbye!".equalsIgnoreCase(serverResponse.trim())) {
break; // Exit if server said goodbye
}
if ("bye".equalsIgnoreCase(userInput.trim())) {
break; // Exit if user typed bye
}
System.out.print("Enter message to send (or 'bye' to quit): ");
}
} catch (UnknownHostException e) {
System.err.println("Don't know about host " + hostname + ": " + e.getMessage());
} catch (IOException e) {
System.err.println("Couldn't get I/O for the connection to " + hostname + ": " + e.getMessage());
// e.printStackTrace();
}
// Socket, streams, and Scanner are auto-closed here
System.out.println("Client finished.");
}
}
Running the Example:
- Compile both files:
- Open a terminal and run the server:
- Open another terminal and run the client:
- In the client terminal, type messages and press Enter. The server should receive them (check server terminal output) and echo them back to the client.
- Type
bye
in the client terminal to disconnect. Both client and server should indicate disconnection. The server will then wait for a new client. - Stop the server by pressing
Ctrl+C
in its terminal.
URL
and URLConnection
:
Used for working with higher-level protocols like HTTP/HTTPS.
URL
: Represents a Uniform Resource Locator. Used to parse and represent URLs.URLConnection
: An abstract class representing an active connection to the resource pointed to by a URL. Subclasses likeHttpURLConnection
handle specific protocols. Used to fetch content, get headers, etc.
import java.net.URL;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public class URLDemo {
public static void main(String[] args) {
String urlString = "https://www.google.com"; // Example URL
System.out.println("Attempting to connect to: " + urlString);
try {
URL url = new URL(urlString);
// Open a connection (returns URLConnection, cast to HttpURLConnection for HTTP specifics)
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// Set request method (GET is default)
connection.setRequestMethod("GET");
// Set request headers (optional)
connection.setRequestProperty("User-Agent", "Simple-Java-Client");
// Connect (implicitly done when getting response code or input stream)
// connection.connect(); // Not always necessary
// Get response code (e.g., 200 OK, 404 Not Found)
int responseCode = connection.getResponseCode();
System.out.println("Response Code: " + responseCode + " " + connection.getResponseMessage());
if (responseCode == HttpURLConnection.HTTP_OK) { // Success
System.out.println("\n--- Response Headers ---");
Map<String, List<String>> headers = connection.getHeaderFields();
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
// Header key can be null for the status line
String key = entry.getKey() == null ? "[Status Line]" : entry.getKey();
System.out.println(key + ": " + entry.getValue());
}
System.out.println("\n--- Response Body (first few lines) ---");
// Get the input stream to read the response body
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
String line;
int lineCount = 0;
while ((line = reader.readLine()) != null && lineCount < 15) { // Read only first 15 lines
System.out.println(line);
lineCount++;
}
if (line != null) {
System.out.println("... (content truncated)");
}
}
} else {
System.out.println("Request failed.");
// Can read error stream if needed: connection.getErrorStream()
}
// Disconnect (releases resources)
connection.disconnect();
System.out.println("\nConnection disconnected.");
} catch (MalformedURLException e) {
System.err.println("Invalid URL: " + e.getMessage());
} catch (IOException e) {
System.err.println("Error during connection: " + e.getMessage());
// e.printStackTrace();
}
}
}
Networking often involves handling I/O streams and exceptions carefully. For more complex applications, consider using libraries like Apache HttpClient or OkHttp, which provide more features and convenience. Non-blocking I/O using java.nio
(specifically SocketChannel
, ServerSocketChannel
, and Selector
) is crucial for building highly scalable servers that can handle many concurrent connections efficiently.
Generics In-Depth
Generics, introduced in Java 5, allow types (classes and interfaces) to be parameters when defining classes, interfaces, and methods. This enhances type safety and code reusability.
Type Erasure:
A key concept to understand is that generic type information exists only at compile time. The compiler uses generics to perform type checking, but then erases the generic type information, replacing type parameters with their bounds (usually Object
) or specific types if bounded. This ensures backward compatibility with older Java code that didn't use generics.
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// At runtime, both stringList and intList are just ArrayList objects.
// The JVM doesn't know they were declared with <String> or <Integer>.
System.out.println(stringList.getClass() == intList.getClass()); // Output: true
Consequences:
- You cannot use
instanceof
with parameterized types directly (e.g.,if (list instanceof ArrayList<String>)
is illegal). You check the raw type:if (list instanceof ArrayList)
. - You cannot create instances of type parameters (e.g.,
new T()
inside a generic classMyClass<T>
). - You cannot create arrays of parameterized types directly (e.g.,
new List<String>[10]
is illegal). You can createnew List<?>[10]
(using wildcards).
Wildcards (?
):
Wildcards represent an unknown type. They are useful when you want to write methods that can operate on generic types in a more flexible way.
-
Upper Bounded Wildcard (
? extends Type
): Represents an unknown type that isType
or a subclass ofType
. Used when you only need to read from the structure (producer). You can safely get elements asType
, but you cannot safely add elements (exceptnull
) because you don't know the exact subtype.import java.util.List; import java.util.ArrayList; class Animal { void makeSound() { System.out.println("Generic animal sound"); } } class Dog extends Animal { @Override void makeSound() { System.out.println("Woof!"); } } class Cat extends Animal { @Override void makeSound() { System.out.println("Meow!"); } } public class UpperBoundWildcard { // This method can accept List<Animal>, List<Dog>, List<Cat> public static void printAnimalSounds(List<? extends Animal> animals) { System.out.println("--- Printing Sounds ---"); for (Animal animal : animals) { // Safe to read as Animal animal.makeSound(); } // animals.add(new Dog()); // COMPILE ERROR! Cannot add - might be List<Cat> // animals.add(new Animal()); // COMPILE ERROR! Cannot add - might be List<Dog> } public static void main(String[] args) { List<Dog> dogs = new ArrayList<>(); dogs.add(new Dog()); dogs.add(new Dog()); List<Cat> cats = new ArrayList<>(); cats.add(new Cat()); List<Animal> mixed = new ArrayList<>(); mixed.add(new Animal()); mixed.add(new Dog()); printAnimalSounds(dogs); printAnimalSounds(cats); printAnimalSounds(mixed); } }
-
Lower Bounded Wildcard (
? super Type
): Represents an unknown type that isType
or a superclass ofType
. Used when you only need to add elements of typeType
(or its subtypes) to the structure (consumer). You can safely addType
or its subtypes, but when you read elements, you can only guarantee they areObject
.PECS Principle: Producer Extends, Consumer Super. Useimport java.util.List; import java.util.ArrayList; public class LowerBoundWildcard { // This method can accept List<Number>, List<Object> // It can add Integer objects (or subtypes like Integer itself) to the list public static void addIntegers(List<? super Integer> list, int count) { System.out.println("--- Adding Integers ---"); for (int i = 1; i <= count; i++) { Integer num = i * 10; list.add(num); // Safe to add Integer (or subtype) System.out.println("Added: " + num); } // Object obj = list.get(0); // Reading only guarantees Object } public static void main(String[] args) { List<Integer> intList = new ArrayList<>(); List<Number> numList = new ArrayList<>(); List<Object> objList = new ArrayList<>(); // List<Double> doubleList = new ArrayList<>(); // Cannot use List<Double> addIntegers(intList, 3); addIntegers(numList, 2); addIntegers(objList, 4); // addIntegers(doubleList, 1); // COMPILE ERROR! Double is not a supertype of Integer System.out.println("\nintList: " + intList); System.out.println("numList: " + numList); System.out.println("objList: " + objList); } }
extends
when you only get values out (produce), usesuper
when you only put values in (consume). -
Unbounded Wildcard (
?
): Represents any unknown type. Often used when the type doesn't matter (e.g.,printList(List<?> list)
just callslist.size()
or iterates to print usingObject
). It's read-only in practice (you can only retrieveObject
).
Bounded Type Parameters:
You can restrict the types that can be used as type arguments by specifying bounds.
// T must be a subtype of Number and must implement Comparable<T>
class Stats<T extends Number & Comparable<T>> {
private T[] nums;
public Stats(T[] nums) { this.nums = nums; }
public double average() {
double sum = 0.0;
for(T num : nums) {
sum += num.doubleValue(); // Safe because T extends Number
}
return sum / nums.length;
}
// Can compare because T implements Comparable<T>
public boolean isGreaterThan(Stats<?> other) { // Use wildcard for flexibility
// Basic comparison - could be more sophisticated
return this.average() > other.average();
}
}
public class BoundedTypeParamDemo {
public static void main(String[] args) {
Integer[] iNums = {1, 2, 3, 4, 5};
Stats<Integer> intStats = new Stats<>(iNums);
System.out.printf("Integer Average: %.2f\n", intStats.average());
Double[] dNums = {1.1, 2.2, 3.3, 4.4, 5.5};
Stats<Double> doubleStats = new Stats<>(dNums);
System.out.printf("Double Average: %.2f\n", doubleStats.average());
System.out.println("Int stats > Double stats? " + intStats.isGreaterThan(doubleStats)); // false
// String[] sNums = {"a", "b"};
// Stats<String> stringStats = new Stats<>(sNums); // COMPILE ERROR! String is not a Number
}
}
Reflection API
Reflection allows a Java program to examine or "introspect" itself at runtime. You can inspect classes, interfaces, fields, methods, and constructors, and even manipulate them dynamically (e.g., call methods or change field values) without knowing their names at compile time. Located in the java.lang.reflect
package.
Use Cases:
- Frameworks (like Spring, JUnit) use reflection extensively for dependency injection, annotation processing, test discovery, etc.
- Debugging tools and profilers.
- Object serialization/deserialization libraries.
- Creating dynamic proxies.
Key Classes:
Class
: Represents a class or interface. Get it usingobject.getClass()
,MyClass.class
, orClass.forName("com.example.MyClass")
.Field
: Represents a class field (member variable).Method
: Represents a class or instance method.Constructor
: Represents a class constructor.Modifier
: Provides static methods to decode access modifiers (public, private, static, final, etc.).
Example:
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
class ReflectTarget {
public String publicField = "Public Value";
private int privateField = 123;
protected static final double PI_APPROX = 3.14;
public ReflectTarget() {
System.out.println("Default constructor called.");
}
private ReflectTarget(String msg) {
System.out.println("Private constructor called with: " + msg);
}
public void publicMethod(String param) {
System.out.println("Public method called with: " + param);
}
private String privateMethod() {
System.out.println("Private method called.");
return "Private Result";
}
public static void staticMethod() {
System.out.println("Static method called.");
}
}
public class ReflectionDemo {
public static void main(String[] args) {
try {
// Get Class object
Class<?> clazz = Class.forName("ReflectTarget");
// Or: Class<?> clazz = ReflectTarget.class;
// Or: ReflectTarget obj = new ReflectTarget(); Class<?> clazz = obj.getClass();
System.out.println("--- Class Info ---");
System.out.println("Class Name: " + clazz.getName());
System.out.println("Simple Name: " + clazz.getSimpleName());
System.out.println("Package: " + clazz.getPackage().getName());
System.out.println("Is Interface? " + clazz.isInterface());
System.out.println("Modifiers: " + Modifier.toString(clazz.getModifiers())); // e.g., public
System.out.println("\n--- Constructors ---");
Constructor<?>[] constructors = clazz.getDeclaredConstructors(); // Get all constructors
for (Constructor<?> c : constructors) {
System.out.println(" " + Modifier.toString(c.getModifiers()) + " " + c.getName() +
Arrays.toString(c.getParameterTypes()));
}
System.out.println("\n--- Fields ---");
Field[] fields = clazz.getDeclaredFields(); // Get all declared fields (public, private, etc.)
// Field[] publicFields = clazz.getFields(); // Get only public fields (including inherited)
for (Field f : fields) {
System.out.println(" " + Modifier.toString(f.getModifiers()) + " " +
f.getType().getSimpleName() + " " + f.getName());
}
System.out.println("\n--- Methods ---");
Method[] methods = clazz.getDeclaredMethods(); // Get all declared methods
// Method[] publicMethods = clazz.getMethods(); // Get public methods (including inherited)
for (Method m : methods) {
System.out.println(" " + Modifier.toString(m.getModifiers()) + " " +
m.getReturnType().getSimpleName() + " " + m.getName() +
Arrays.toString(m.getParameterTypes()));
}
System.out.println("\n--- Dynamic Instantiation & Method Invocation ---");
// Instantiate using default public constructor
Constructor<?> defaultConstructor = clazz.getConstructor(); // Get public default constructor
Object instance = defaultConstructor.newInstance(); // Create instance
// Invoke public method
Method publicMethod = clazz.getMethod("publicMethod", String.class); // Get method by name and param types
publicMethod.invoke(instance, "Dynamic Message"); // Invoke on instance with arguments
// Invoke static method (instance is null)
Method staticMethod = clazz.getMethod("staticMethod");
staticMethod.invoke(null); // No instance needed for static methods
System.out.println("\n--- Accessing Private Members ---");
// Access private field
Field privateField = clazz.getDeclaredField("privateField");
privateField.setAccessible(true); // *** IMPORTANT: Bypass access control ***
int value = (int) privateField.get(instance); // Get value from instance
System.out.println(" Private field value (before): " + value);
privateField.set(instance, 456); // Set new value
System.out.println(" Private field value (after): " + privateField.get(instance));
privateField.setAccessible(false); // Best practice: Reset accessibility
// Invoke private method
Method privateMethod = clazz.getDeclaredMethod("privateMethod");
privateMethod.setAccessible(true); // Bypass access control
Object result = privateMethod.invoke(instance); // Invoke on instance
System.out.println(" Private method result: " + result);
privateMethod.setAccessible(false); // Reset accessibility
// Instantiate using private constructor
Constructor<?> privateConstructor = clazz.getDeclaredConstructor(String.class);
privateConstructor.setAccessible(true);
Object instance2 = privateConstructor.newInstance("Secret Message");
privateConstructor.setAccessible(false);
} catch (ClassNotFoundException e) {
System.err.println("Error: Class not found. " + e.getMessage());
} catch (NoSuchMethodException e) {
System.err.println("Error: Method not found. " + e.getMessage());
} catch (NoSuchFieldException e) {
System.err.println("Error: Field not found. " + e.getMessage());
} catch (IllegalAccessException e) {
System.err.println("Error: Access denied (check setAccessible). " + e.getMessage());
} catch (java.lang.reflect.InvocationTargetException e) {
System.err.println("Error: Exception thrown by invoked method/constructor. " + e.getTargetException());
// e.getTargetException().printStackTrace();
} catch (InstantiationException e) {
System.err.println("Error: Cannot instantiate abstract class or interface. " + e.getMessage());
} catch (Exception e) { // Catch other potential reflection exceptions
System.err.println("A reflection error occurred: " + e.getMessage());
// e.printStackTrace();
}
}
}
Caution: Reflection is powerful but has drawbacks:
- Performance Overhead: Reflection operations are significantly slower than direct code execution.
- Security Restrictions: Security Managers can restrict reflective access. Calling
setAccessible(true)
might fail in secured environments. - Reduced Compile-Time Safety: Errors like incorrect method names or parameter types are only caught at runtime, not compile time.
- Code Obscurity: Overuse can make code harder to understand and maintain. Use reflection judiciously when its benefits (like framework flexibility) outweigh the costs.
Annotations
Annotations provide a way to add metadata (data about data) to Java source code elements (classes, methods, fields, parameters, etc.). They don't directly affect code execution but can be processed by the compiler or at runtime (using reflection) for various purposes.
Built-in Annotations:
@Override
: Indicates that a method is intended to override a method in a superclass. The compiler checks if the method actually overrides; otherwise, it issues an error. (Seen previously).@Deprecated
: Marks a code element (class, method, etc.) as outdated and discouraged from use. The compiler generates a warning if deprecated elements are used.@SuppressWarnings
: Instructs the compiler to suppress specific warnings (e.g.,@SuppressWarnings("unchecked")
for unchecked generic operations,@SuppressWarnings("deprecation")
for using deprecated elements). Use sparingly.@FunctionalInterface
: (Java 8+) Indicates that an interface is intended to be a functional interface (having exactly one abstract method). The compiler verifies this.@SafeVarargs
: (Java 7+) Asserts that a method using varargs with generic types does not perform unsafe operations on its varargs parameter. Suppresses certain compiler warnings.
Defining Custom Annotations:
Use the @interface
keyword. Annotations can have elements (like methods in an interface) which define the metadata values.
import java.lang.annotation.*;
// Define where the annotation can be applied (e.g., TYPE, METHOD, FIELD)
@Target({ElementType.TYPE, ElementType.METHOD})
// Define when the annotation is available (SOURCE, CLASS, RUNTIME)
@Retention(RetentionPolicy.RUNTIME) // Make it available via reflection at runtime
@Inherited // Subclasses will inherit this annotation from superclass (only for TYPE target)
@Documented // Include this annotation in generated Javadoc
public @interface MyAnnotation {
// Annotation elements (like methods)
String description(); // Required element
String author() default "Unknown"; // Element with a default value
int version() default 1;
String[] reviewers() default {}; // Array element
}
Using Annotations:
Apply the annotation using @
followed by the annotation name, providing values for required elements.
@MyAnnotation(description = "Main application class", author = "Alice", version = 2, reviewers = {"Bob", "Charlie"})
public class AnnotatedClass {
@MyAnnotation(description = "Important field") // Uses default author and version
private String importantData;
@MyAnnotation(description = "Processing method", author = "Alice")
public void processData(@MyAnnotation(description = "Input parameter") String input) { // Annotate parameter
System.out.println("Processing: " + input);
}
@Deprecated // Using a built-in annotation
public void oldMethod() {
System.out.println("This is an old method.");
}
@SuppressWarnings("deprecation") // Suppress warning for using oldMethod
public void useOldMethod() {
oldMethod();
}
}
Processing Annotations (Runtime using Reflection):
import java.lang.reflect.Method;
import java.util.Arrays;
public class AnnotationProcessor {
public static void main(String[] args) {
Class<?> clazz = AnnotatedClass.class;
System.out.println("--- Processing Class Annotations ---");
if (clazz.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation classAnnotation = clazz.getAnnotation(MyAnnotation.class);
System.out.println("Class Annotation Found:");
System.out.println(" Description: " + classAnnotation.description());
System.out.println(" Author: " + classAnnotation.author());
System.out.println(" Version: " + classAnnotation.version());
System.out.println(" Reviewers: " + Arrays.toString(classAnnotation.reviewers()));
} else {
System.out.println("Class annotation not found.");
}
System.out.println("\n--- Processing Method Annotations ---");
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation methodAnnotation = method.getAnnotation(MyAnnotation.class);
System.out.println("Method '" + method.getName() + "' Annotation Found:");
System.out.println(" Description: " + methodAnnotation.description());
System.out.println(" Author: " + methodAnnotation.author());
// ... access other elements ...
}
if (method.isAnnotationPresent(Deprecated.class)) {
System.out.println("Method '" + method.getName() + "' is @Deprecated.");
}
}
// Can similarly process field annotations (clazz.getDeclaredFields())
// and parameter annotations (method.getParameterAnnotations())
}
}
Annotations are widely used in frameworks like JUnit (e.g., @Test
, @BeforeEach
), Spring (e.g., @Component
, @Autowired
, @RestController
), Jakarta EE/JPA (e.g., @Entity
, @Id
, @Column
), etc., to configure behavior, enable dependency injection, define ORM mappings, and more, often replacing older XML configuration approaches.
Introduction to Java Modules (JPMS)
The Java Platform Module System (JPMS), introduced in Java 9, addresses several long-standing issues with the traditional classpath mechanism, particularly for large applications:
- Strong Encapsulation: Prevents code in one JAR (module) from accessing internal implementation classes in another JAR, even if they are
public
. Only explicitlyexported
packages are accessible. - Reliable Configuration: Modules declare explicit dependencies (
requires
) on other modules. The JVM checks these dependencies at launch time, preventing "classpath hell" issues where missing dependencies cause runtime errors (NoClassDefFoundError
). - Scalability: Enables the creation of custom Java runtime images containing only the modules required by an application, reducing deployment size.
Key Concepts:
- Module: A collection of related packages, resources, and a module descriptor (
module-info.java
). Typically packaged as a modular JAR file. - Module Descriptor (
module-info.java
): A file at the root of the module's source code that defines the module's properties:module <module.name> { ... }
: Declares the module.requires <other.module.name>;
: Specifies a dependency on another module.exports <package.name>;
: Makes the public types in the specified package accessible to other modules.opens <package.name>;
: Allows other modules to use reflection on the types in the specified package.uses <service.interface>;
: Declares that the module uses a service (defined by an interface).provides <service.interface> with <implementation.class>;
: Declares that the module provides an implementation of a service interface.
Example Structure:
src/
├── com.greetings <-- Module Name
│ ├── module-info.java <-- Module Descriptor
│ └── com/greetings/Main.java
└── org.astro <-- Module Name
├── module-info.java <-- Module Descriptor
└── org/astro/World.java
src/com.greetings/module-info.java
:
module com.greetings {
// This module depends on the org.astro module
requires org.astro;
// It doesn't export any packages for others to use directly
}
src/com.greetings/com/greetings/Main.java
:
package com.greetings;
// Import class from the required module
import org.astro.World;
public class Main {
public static void main(String[] args) {
System.out.println("Greetings!");
System.out.println("From Astro: " + World.name());
}
}
src/org.astro/module-info.java
:
src/org.astro/org/astro/World.java
:
package org.astro;
// This class is public AND in an exported package, so it's accessible
public class World {
public static String name() {
return "World";
}
}
Compiling and Running (using module path):
Assume code is in src/
directory and we want compiled output in mods/
.
-
Compile
org.astro
: -
Compile
com.greetings
(referencing the compiledorg.astro
):javac --module-path mods \ -d mods/com.greetings \ src/com.greetings/module-info.java src/com.greetings/com/greetings/Main.java
--module-path mods
: Tellsjavac
where to find required modules (org.astro
in this case).
-
Run the application:
--module-path mods
: Tellsjava
where to find the application modules.--module com.greetings/com.greetings.Main
: Specifies the main module and the main class to execute.
Benefits: Improved reliability, security (strong encapsulation), maintainability, and potential for smaller deployment sizes. JPMS is particularly relevant for large applications and libraries. Build tools like Maven and Gradle have support for building modular projects.
Workshop Multi-threaded Web Crawler
Objective: Build a simple multi-threaded web crawler that starts from a given URL, fetches the page content, extracts links, and concurrently crawls those links up to a certain depth, while avoiding revisiting URLs. This demonstrates multithreading (ExecutorService
), networking (URL
, basic HTML parsing), collections (Set
, Queue
, ConcurrentHashMap
), and potentially atomic variables.
Disclaimer: This is a very basic educational crawler. Real-world crawlers are far more complex, respecting robots.txt
, handling errors robustly, managing politeness delays, using sophisticated parsing, etc. Run this responsibly and only on websites you have permission to crawl or on test sites. Do not overload servers.
Steps:
-
Directory Setup:
-
Create
WebCrawler.java
: -
Write the Java Code: This code uses an
ExecutorService
for concurrency, aConcurrentHashMap
to track visited URLs safely across threads, and a simple regex for link extraction.import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.concurrent.*; import java.util.regex.Matcher; import java.util.regex.Pattern; public class WebCrawler { private final ExecutorService executorService; // Use ConcurrentHashMap for thread-safe tracking of visited URLs // Key: URL string, Value: Boolean (true if visited/visiting) private final ConcurrentHashMap<String, Boolean> visitedUrls; private final int maxDepth; private final String startUrl; // Simple regex to find href attributes in <a> tags (basic, misses many cases) // Needs improvement for real-world scenarios (e.g., handling relative URLs, different protocols) private static final Pattern LINK_PATTERN = Pattern.compile( "href=\"(http[s]?://[^\"]+)\"", Pattern.CASE_INSENSITIVE); public WebCrawler(String startUrl, int maxDepth, int poolSize) { this.startUrl = startUrl; this.maxDepth = maxDepth; this.executorService = Executors.newFixedThreadPool(poolSize); this.visitedUrls = new ConcurrentHashMap<>(); } public void startCrawling() { System.out.println("Starting crawl from: " + startUrl + " with depth " + maxDepth); // Submit the initial task submitCrawlTask(startUrl, 0); // Shutdown executor when tasks are done executorService.shutdown(); try { // Wait a long time for completion (adjust as needed) if (!executorService.awaitTermination(60, TimeUnit.MINUTES)) { System.err.println("Crawler did not finish in time, forcing shutdown."); executorService.shutdownNow(); } } catch (InterruptedException e) { System.err.println("Crawler interrupted during shutdown wait."); executorService.shutdownNow(); Thread.currentThread().interrupt(); } System.out.println("\nCrawling finished. Visited " + visitedUrls.size() + " unique URLs."); } private void submitCrawlTask(String urlString, int depth) { // Check if already visited or processing THIS specific URL // putIfAbsent is atomic: returns null if key was absent, otherwise returns existing value if (depth > maxDepth || visitedUrls.putIfAbsent(normalizeUrl(urlString), Boolean.TRUE) != null) { // System.out.println("Skipping (visited or depth exceeded): " + urlString); return; // Already visited/visiting or depth exceeded } // Submit the task to the executor service executorService.submit(() -> crawl(urlString, depth)); } private void crawl(String urlString, int currentDepth) { String normalizedUrl = normalizeUrl(urlString); System.out.printf("[Depth %d] Crawling: %s (Thread: %s)\n", currentDepth, normalizedUrl, Thread.currentThread().getName()); try { URL url = new URL(normalizedUrl); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setConnectTimeout(5000); // 5 seconds connect timeout connection.setReadTimeout(10000); // 10 seconds read timeout connection.setRequestProperty("User-Agent", "SimpleJavaCrawler/1.0"); int responseCode = connection.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { // Only process HTML content (basic check) String contentType = connection.getContentType(); if (contentType != null && contentType.toLowerCase().contains("text/html")) { try (BufferedReader reader = new BufferedReader( new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) { String line; StringBuilder content = new StringBuilder(); while ((line = reader.readLine()) != null) { content.append(line).append("\n"); // Basic link extraction Matcher matcher = LINK_PATTERN.matcher(line); while (matcher.find()) { String foundUrl = matcher.group(1); // Submit new tasks for found links, increasing depth submitCrawlTask(foundUrl, currentDepth + 1); } } // Optionally process the content here (e.g., save to file, analyze) // System.out.println("Content fetched for: " + normalizedUrl); } } else { System.out.printf("[Depth %d] Skipped non-HTML content (%s) at: %s\n", currentDepth, contentType, normalizedUrl); } } else { System.err.printf("[Depth %d] Failed: %d %s for URL: %s\n", currentDepth, responseCode, connection.getResponseMessage(), normalizedUrl); } connection.disconnect(); } catch (MalformedURLException e) { System.err.printf("[Depth %d] Invalid URL format: %s - %s\n", currentDepth, urlString, e.getMessage()); } catch (IOException e) { System.err.printf("[Depth %d] I/O Error crawling %s: %s\n", currentDepth, urlString, e.getMessage()); } catch (Exception e) { System.err.printf("[Depth %d] Unexpected error crawling %s: %s\n", currentDepth, urlString, e.getMessage()); // e.printStackTrace(); // For debugging unexpected errors } } // Basic URL normalization (remove fragment, trailing slash) private String normalizeUrl(String urlString) { try { URL url = new URL(urlString); String path = url.getPath(); if (path.isEmpty() || path.equals("/")) { path = ""; // Use empty path instead of "/" for consistency if needed } else if (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } // Rebuild without fragment and potentially trailing slash return new URL(url.getProtocol(), url.getHost(), url.getPort(), path).toString(); } catch (MalformedURLException e) { return urlString; // Return original if invalid } } public static void main(String[] args) { // ** IMPORTANT: Use a URL you own or have permission to crawl, or a test site. ** // Example: Start with a simple site. Be mindful of server load. String startUrl = "http://info.cern.ch/hypertext/WWW/TheProject.html"; // Example: The first website int maxDepth = 2; // Limit crawl depth int poolSize = 5; // Number of concurrent threads if (args.length > 0) startUrl = args[0]; if (args.length > 1) maxDepth = Integer.parseInt(args[1]); if (args.length > 2) poolSize = Integer.parseInt(args[2]); WebCrawler crawler = new WebCrawler(startUrl, maxDepth, poolSize); crawler.startCrawling(); } }
-
Compile the Code:
-
Run the Crawler:
-
Observe the Output: You will see multiple threads concurrently fetching pages, extracting links, and submitting new tasks. The output will show which thread is crawling which URL and at what depth. Errors (like connection issues or non-HTML content) will also be reported. The crawl stops when all reachable links within the specified depth have been visited or attempted.
This workshop illustrates how to combine ExecutorService
for managing worker threads, ConcurrentHashMap
for thread-safe state management (visited URLs), and basic networking (HttpURLConnection
) to perform a concurrent task like web crawling. Remember the limitations and ethical considerations of web crawling.
5. Java Development Tools and Practices on Linux
Beyond the core language features, becoming a proficient Java developer involves mastering essential tools and practices commonly used in professional software development, especially within the Linux environment.
Build Tools (Maven, Gradle)
Manually compiling Java code with javac
and managing dependencies (JAR files) and the classpath becomes unmanageable for projects of any significant size. Build automation tools streamline this process. Maven and Gradle are the two most popular build tools in the Java ecosystem.
Why Use Build Tools?
- Dependency Management: Automatically download required libraries (dependencies) from central repositories (like Maven Central) and manage transitive dependencies (dependencies of your dependencies).
- Standardized Project Structure: Define conventional directory layouts for source code, resources, and tests, making projects easier to navigate and understand.
- Build Lifecycle: Define standard phases (like
compile
,test
,package
,install
,deploy
) that automate the build process. - Plugin Ecosystem: Extend functionality through numerous plugins (e.g., for code generation, documentation, static analysis, deployment).
- Reproducibility: Ensure builds are consistent across different environments.
Apache Maven:
- Uses XML (
pom.xml
- Project Object Model) for configuration. - Relies heavily on conventions and a fixed, well-defined lifecycle.
- Often considered easier to get started with for simpler projects due to its rigid structure.
Standard Maven Directory Layout:
my-maven-app/
├── pom.xml <-- Maven Project Configuration
└── src/
├── main/
│ ├── java/ <-- Application source code (e.g., com/mycompany/App.java)
│ └── resources/ <-- Application resources (e.g., config files, properties)
└── test/
├── java/ <-- Test source code (e.g., com/mycompany/AppTest.java)
└── resources/ <-- Test resources
Example pom.xml
:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- Basic project coordinates -->
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId> <!-- Usually reverse domain name -->
<artifactId>my-maven-app</artifactId> <!-- Project name -->
<version>1.0-SNAPSHOT</version> <!-- Project version -->
<packaging>jar</packaging> <!-- Type of artifact (jar, war, pom) -->
<properties>
<!-- Define properties for consistent versions, encodings etc. -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source> <!-- Java source version -->
<maven.compiler.target>11</maven.compiler.target> <!-- Java target bytecode version -->
<junit.version>5.9.2</junit.version> <!-- JUnit 5 version -->
</properties>
<dependencies>
<!-- Declare project dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope> <!-- Dependency only needed for testing -->
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- Add other dependencies here -->
<!-- Example: Google Guava library -->
<!--
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
-->
</dependencies>
<build>
<plugins>
<!-- Configure Maven plugins -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version> <!-- Plugin for running tests -->
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<!-- Specify the main class for executable JAR -->
<mainClass>com.mycompany.app.App</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
Common Maven Commands (run in the project root directory containing pom.xml
):
mvn compile
: Compiles source code (src/main/java
) intotarget/classes
.mvn test
: Compiles test code (src/test/java
) and runs unit tests using the Surefire plugin.mvn package
: Compiles, tests, and packages the application (e.g., creates a JAR file in thetarget/
directory).mvn install
: Compiles, tests, packages, and installs the artifact into your local Maven repository (~/.m2/repository
), making it available for other local projects.mvn clean
: Deletes thetarget/
directory (cleans previous build artifacts).mvn dependency:tree
: Shows the project's dependency hierarchy.mvn site
: Generates project documentation and reports.
Gradle:
- Uses Groovy or Kotlin Domain Specific Languages (DSLs) for build scripts (
build.gradle
orbuild.gradle.kts
). - Offers more flexibility and often faster performance (due to incremental builds and build cache).
- Can have a steeper learning curve initially due to its flexibility and DSLs.
- Widely used for Android development and increasingly popular for other Java projects.
Standard Gradle Directory Layout: (Often the same as Maven's)
my-gradle-app/
├── build.gradle <-- Gradle build script (Groovy DSL)
├── settings.gradle <-- Settings for multi-project builds (optional for single)
└── src/
├── main/
│ ├── java/
│ └── resources/
└── test/
├── java/
└── resources/
Example build.gradle
(Groovy DSL):
plugins {
// Apply the java plugin to add support for Java
id 'java'
// Apply the application plugin to add support for building executable applications
id 'application'
}
// Define project metadata
group = 'com.mycompany.app' // Same as groupId in Maven
version = '1.0-SNAPSHOT' // Same as version in Maven
repositories {
// Use Maven Central for resolving dependencies
mavenCentral()
}
// Define Java compatibility
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
// Specify the main class for the application plugin
application {
mainClass = 'com.mycompany.app.App' // Your main class
}
dependencies {
// Define dependencies
// Use JUnit Jupiter for testing.
testImplementation(platform('org.junit:junit-bom:5.9.2')) // JUnit 5 BOM
testImplementation('org.junit.jupiter:junit-jupiter')
// Example: Google Guava library
// implementation 'com.google.guava:guava:31.1-jre'
// Use 'implementation' for compile and runtime dependencies
// Use 'testImplementation' for test-only dependencies
// Use 'runtimeOnly' for runtime-only dependencies
}
// Configure the test task
tasks.named('test') {
// Use JUnit Platform for running tests.
useJUnitPlatform()
}
// Optional: Configure JAR manifest (often handled by application plugin)
/*
tasks.jar {
manifest {
attributes(
'Main-Class': application.mainClass // Reference main class from application plugin
)
}
}
*/
Common Gradle Commands (run in the project root directory):
Use the Gradle Wrapper (gradlew
script) which downloads and uses the correct Gradle version for the project.
./gradlew build
: Compiles, tests, and assembles the project (creates JAR inbuild/libs
). This runscompileJava
,processResources
,classes
,test
,jar
, etc../gradlew test
: Compiles and runs unit tests../gradlew run
: Compiles and runs the application (using theapplication
plugin)../gradlew clean
: Deletes thebuild/
directory../gradlew dependencies
: Shows the project's dependency hierarchy../gradlew tasks
: Lists all available tasks for the project.
Installation on Linux:
- Maven:
- Gradle: Often best installed using SDKMAN! (Software Development Kit Manager) for easy version management, or download from the Gradle website.
Alternatively, if your project includes the Gradle Wrapper (
# Install SDKMAN! (Follow instructions at https://sdkman.io/install) curl -s "https://get.sdkman.io" | bash source "$HOME/.sdkman/bin/sdkman-init.sh" # Install Gradle using SDKMAN! sdk install gradle <desired_version> # e.g., sdk install gradle 8.0.2 # Or install latest stable: sdk install gradle # Verify gradle -version
gradlew
), you usually don't need a system-wide Gradle installation.
Version Control (Git)
Version Control Systems (VCS) track changes to files over time, allowing developers to revert to previous versions, collaborate effectively, and manage different lines of development (branches). Git is the de facto standard distributed VCS.
Key Git Concepts:
- Repository (Repo): A database containing the entire history of your project files. Can be local (on your machine) or remote (hosted on services like GitHub, GitLab, Bitbucket).
- Working Directory: The local checkout of your project files where you make modifications.
- Staging Area (Index): An intermediate area where you prepare changes before committing them. Allows you to selectively choose which modifications go into the next commit.
- Commit: A snapshot of your project's state at a specific point in time, stored in the repository history. Each commit has a unique ID (SHA-1 hash), an author, a timestamp, and a commit message describing the changes.
- Branch: An independent line of development. The default branch is usually
main
(ormaster
). Branches allow developers to work on new features or bug fixes without affecting the main codebase until ready. - Merge: Combining changes from different branches back together.
- Remote: A reference to another Git repository, typically hosted on a server (e.g.,
origin
usually refers to the repository you cloned from). - Clone: Creating a local copy of a remote repository.
- Push: Sending your local committed changes to a remote repository.
- Pull: Fetching changes from a remote repository and merging them into your local branch.
- Fetch: Fetching changes from a remote repository without automatically merging them.
Basic Git Workflow on Linux:
-
Installation:
-
Configuration (First time only):
-
Initializing a Repository (New Project):
-
Cloning a Repository (Existing Project):
-
Making Changes: Edit files in your working directory.
-
Checking Status: See which files have been modified, added, or are untracked.
-
Staging Changes: Add modified/new files to the staging area.
-
Committing Changes: Save the staged changes to the repository history with a descriptive message.
Write clear, concise commit messages explaining why the change was made. -
Viewing History:
-
Working with Branches:
git branch # List local branches (* indicates current branch) git branch <new-branch-name> # Create a new branch git checkout <branch-name> # Switch to an existing branch (updates working directory) git checkout -b <new-branch-name> # Create and switch to a new branch in one step # Make changes, stage, and commit on the new branch... git checkout main # Switch back to the main branch git merge <branch-name> # Merge changes from <branch-name> into the current branch (main) # May result in merge conflicts if changes overlap git branch -d <branch-name> # Delete a local branch (after merging, usually)
-
Working with Remotes:
git remote -v # List configured remote repositories (usually 'origin') # Assuming you cloned or added a remote named 'origin': git fetch origin # Fetch changes from origin (doesn't merge) git pull origin main # Fetch changes from origin's main branch and merge into current local branch # Often simplified to 'git pull' if tracking is set up git push origin main # Push local commits from your main branch to origin's main branch # May require authentication (SSH key or HTTPS token) git push origin <local-branch-name> # Push a local feature branch to the remote
.gitignore
File:
Create a file named .gitignore
in the project root to list files and directories that Git should ignore (e.g., compiled class files, build output, IDE configuration, logs).
Example .gitignore
for Java projects:
# Compiled class files
*.class
# Build tool output directories
target/
build/
out/
# Log files
*.log
# IDE settings
.idea/
*.iml
.settings/
.classpath
.project
# Package files
*.jar
*.war
*.ear
# Maven wrapper artifacts
.mvn/
mvnw
mvnw.cmd
# Gradle wrapper artifacts
.gradle/
gradlew
gradlew.bat
gradle/wrapper/gradle-wrapper.jar
# OS generated files
.DS_Store
Thumbs.db
Integrating Git with build tools like Maven/Gradle is standard practice. Committing pom.xml
or build.gradle
ensures that collaborators can build the project with the correct dependencies.
Debugging Java Applications
Debugging is the process of finding and fixing errors (bugs) in your code. Java offers several debugging tools and techniques.
Using System.out.println
(Basic):
The simplest form of debugging is inserting print statements to check variable values or trace execution flow. Effective for simple issues but quickly becomes cumbersome and clutters code.
public void processValue(int x) {
System.out.println("DEBUG: Entering processValue with x = " + x); // Trace entry
int result = x * 2 + 5;
System.out.println("DEBUG: Intermediate result = " + result); // Check value
// ... more logic ...
System.out.println("DEBUG: Exiting processValue"); // Trace exit
}
Using an IDE Debugger (Recommended):
Integrated Development Environments (IDEs) like IntelliJ IDEA, Eclipse, and VS Code (with Java extensions) provide powerful graphical debuggers. These are the standard tools for efficient debugging on Linux (or any platform).
Common IDE Debugger Features:
- Breakpoints: Mark specific lines of code where execution should pause.
- Conditional Breakpoints: Pause only if a certain condition is true.
- Stepping: Control execution flow after pausing at a breakpoint:
- Step Over (
F8
common): Execute the current line and pause at the next line in the same method. If the current line contains a method call, execute the entire method without stepping into it. - Step Into (
F7
common): If the current line contains a method call, move execution to the first line inside that method. - Step Out (
Shift+F8
common): Continue execution until the current method returns, then pause at the line where the method was called. - Resume Program (
F9
common): Continue execution until the next breakpoint is hit or the program terminates.
- Step Over (
- Variable Inspection: View the current values of local variables, instance variables (
this
), and static variables when execution is paused. - Watch Expressions: Evaluate custom expressions or monitor specific variables continuously.
- Call Stack: Shows the sequence of method calls that led to the current execution point. Useful for understanding how execution got there.
- Changing Values: Modify variable values on-the-fly during a debug session to test different scenarios.
- HotSwap: (If supported by JVM) Allows replacing changed code while the application is running in debug mode, avoiding a full restart for minor changes (limited capabilities).
Steps for IDE Debugging (General):
- Set Breakpoints: Click in the editor's gutter next to the line numbers where you want execution to pause.
- Start in Debug Mode: Find the "Debug" button or menu option (often a bug icon) instead of the normal "Run" button. This starts your application with the debugger attached.
- Trigger Breakpoint: Perform actions in your application that will cause execution to reach a breakpoint.
- Use Stepping Controls: Use Step Over, Step Into, Step Out, and Resume to navigate the code.
- Inspect Variables & Call Stack: Examine the values in the Variables/Debug view and the Call Stack view to understand the program's state.
- Identify and Fix: Analyze the state and execution flow to find the cause of the bug. Stop the debugger, fix the code, and repeat.
Using jdb
(Command-Line Debugger):
Java includes a basic command-line debugger, jdb
. It's less user-friendly than IDE debuggers but can be useful in environments without a GUI or for specific scripting scenarios.
- Compile with Debug Info: Ensure your
.java
files are compiled with debug information enabled (this is the default forjavac
, but ensure the-g
flag isn't disabled). -
Start
jdb
and Attach/Launch:- Launch a new JVM:
jdb MyMainClass
- Attach to a running JVM: You need to start the target JVM with debug agent flags:
Then attach
# Start your app enabling remote debugging on port 8000 java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000 YourMainClass
jdb
in another terminal: (suspend=y
makes the JVM wait for debugger attachment before starting).
- Launch a new JVM:
-
jdb
Commands:stop at MyClass:<line_number>
: Set a breakpoint.stop in my.package.MyClass.myMethod
: Set a breakpoint at the beginning of a method.run
: Start execution (if launched withjdb
).cont
: Continue execution until next breakpoint.step
: Step into method calls.next
: Step over method calls.step up
: Step out of the current method.print <variable_name>
ordump <object_expr>
: Inspect variable values.locals
: Show local variables in the current stack frame.where
: Show the call stack.list
: Show source code around the current line.threads
: List all threads.thread <thread_id>
: Switch to a specific thread.exit
: Quitjdb
.
jdb
requires more manual command typing but provides the core debugging functionalities.
Unit Testing (JUnit)
Unit testing involves writing code to test individual units (typically methods or classes) of your application in isolation. This helps ensure components work correctly, catches regressions early, and facilitates refactoring. JUnit is the most popular framework for unit testing in Java.
Key Concepts:
- Test Case: A class containing test methods. Typically named
*Test
(e.g.,CalculatorTest
). - Test Method: A method within a test case designed to test a specific aspect of a unit. Annotated with
@Test
. Must bepublic void
and take no arguments (usually). - Assertions: Static methods (usually from
org.junit.jupiter.api.Assertions
) used within test methods to check expected outcomes. If an assertion fails, the test method fails. Examples:assertEquals()
,assertTrue()
,assertFalse()
,assertNotNull()
,assertThrows()
. - Test Runner: Executes the test methods and reports the results (e.g., IDE test runners, Maven Surefire plugin, Gradle test task).
- Setup/Teardown Annotations:
@BeforeEach
: Method runs before each test method in the class. Used for setting up common test fixtures.@AfterEach
: Method runs after each test method. Used for cleaning up resources.@BeforeAll
: Static method runs once before all tests in the class. Used for expensive setup (e.g., database connection).@AfterAll
: Static method runs once after all tests in the class. Used for expensive teardown.
Example (Testing a Simple Calculator):
Assume you have a Calculator.java
class:
// src/main/java/com/mycompany/calc/Calculator.java
package com.mycompany.calc;
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
public int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("Cannot divide by zero");
}
return a / b; // Integer division
}
}
Now, write the unit test class (src/test/java/.../CalculatorTest.java
):
// src/test/java/com/mycompany/calc/CalculatorTest.java
package com.mycompany.calc;
// Static imports for assertions and annotations make tests cleaner
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.*; // For @Test, @BeforeEach etc.
class CalculatorTest {
private Calculator calculator; // Instance under test
@BeforeEach // Runs before each @Test method
void setUp() {
System.out.println("Setting up calculator instance for a test...");
calculator = new Calculator(); // Create a fresh instance for each test
}
@AfterEach
void tearDown() {
System.out.println("Finished test. Tearing down.");
calculator = null; // Help garbage collection (optional here)
}
@BeforeAll // Runs once before any tests start
static void setupAll() {
System.out.println("--- Starting Calculator Tests ---");
}
@AfterAll // Runs once after all tests finish
static void tearDownAll() {
System.out.println("--- Finished All Calculator Tests ---");
}
@Test // Marks this as a test method
@DisplayName("Test adding two positive numbers") // Optional descriptive name
void testAddPositiveNumbers() {
// Arrange (Setup) - done in @BeforeEach mostly
int num1 = 5;
int num2 = 10;
int expected = 15;
// Act (Execute the method under test)
int actual = calculator.add(num1, num2);
// Assert (Verify the result)
assertEquals(expected, actual, "5 + 10 should equal 15"); // Message optional
}
@Test
void testAddNegativeNumbers() {
assertEquals(-5, calculator.add(-2, -3), "Adding negative numbers");
}
@Test
void testAddPositiveAndNegative() {
assertEquals(2, calculator.add(5, -3), "Adding positive and negative");
}
@Test
void testSubtract() {
assertEquals(5, calculator.subtract(10, 5));
assertEquals(-5, calculator.subtract(5, 10));
}
@Test
void testDivideValid() {
assertEquals(2, calculator.divide(10, 5));
assertEquals(0, calculator.divide(3, 5)); // Integer division
}
@Test
void testDivideByZero() {
// Assert that executing calculator.divide(1, 0) throws an IllegalArgumentException
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
calculator.divide(1, 0);
});
// Optionally, assert something about the exception message
String expectedMessage = "Cannot divide by zero";
String actualMessage = exception.getMessage();
assertTrue(actualMessage.contains(expectedMessage),
"Exception message should contain '" + expectedMessage + "'");
}
// Other test methods for different scenarios...
}
Running Tests:
- With Maven: Ensure JUnit 5 dependencies are in
pom.xml
(as shown previously) and the Surefire plugin is configured. Run: Maven will find classes ending inTest
,*Test
, orTestCase
, execute their@Test
methods, and generate reports intarget/surefire-reports/
. - With Gradle: Ensure JUnit 5 dependencies are in
build.gradle
(as shown previously) and thetest
task usesuseJUnitPlatform()
. Run: Gradle runs the tests and generates HTML reports inbuild/reports/tests/test/index.html
. - With IDE: Right-click on the test file, test class, or package and select "Run Tests". The IDE will execute the tests and display results in a dedicated test runner view.
Unit testing is a fundamental practice for building reliable software. Aim for good test coverage, write clear and focused tests, and run them frequently (ideally as part of an automated build process).
Logging Frameworks
While System.out.println
is useful for quick checks, it's inadequate for logging in real applications. Proper logging frameworks provide:
- Log Levels: Control verbosity (e.g., TRACE, DEBUG, INFO, WARN, ERROR, FATAL). You can configure the application to only output messages above a certain level (e.g., INFO and above in production, DEBUG in development).
- Configuration: Configure log output destinations (console, files, databases, network), formatting, rotation policies, etc., often externally without code changes.
- Performance: Optimized for minimal impact on application performance.
- Contextual Information: Automatically include timestamps, thread names, class/method names, etc.
Popular Choices:
- Logback: The successor to Log4j 1.x. Powerful, fast, flexible configuration via XML or Groovy. Native implementation of SLF4j. Often considered the preferred modern choice.
- Log4j 2: A complete rewrite of Log4j 1.x. Very powerful, high-performance (especially asynchronous logging), flexible configuration (XML, JSON, YAML, properties). Also implements SLF4j API.
- SLF4j (Simple Logging Facade for Java): Not a logging implementation itself, but an abstraction layer (facade). Allows you to write your code against the SLF4j API and then plug in a specific logging framework (Logback, Log4j 2,
java.util.logging
) at deployment time using binding JARs. This is the recommended approach as it decouples your application code from the specific logging implementation. java.util.logging
(JUL): Java's built-in logging framework. Less flexible and powerful than Logback/Log4j 2, configuration can be cumbersome.
Using SLF4j with Logback (Recommended):
-
Add Dependencies (Maven Example):
<dependencies> <!-- SLF4j API --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.7</version> <!-- Use latest SLF4j 2.x --> </dependency> <!-- Logback Implementation (Binding) --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.4.7</version> <!-- Use latest Logback --> <!-- <scope>runtime</scope> Usually runtime, but needed compile-time for direct config if needed --> </dependency> <!-- logback-core is pulled in transitively by logback-classic --> </dependencies>
-
Create Logback Configuration File (
src/main/resources/logback.xml
): Logback looks for this file on the classpath by default.<configuration> <!-- Appender to write to the Console --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <!-- Encoder to format log messages --> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <!-- %d: Date %thread: Thread name %-5level: Log level, left-justified in 5 chars %logger{36}: Logger name (usually class name), max 36 chars %msg: The log message %n: Newline --> </encoder> </appender> <!-- Appender to write to a File --> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/my-app.log</file> <!-- Log file name --> <!-- Rolling policy: Create new file daily or when size reaches 10MB --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- daily rollover --> <fileNamePattern>logs/my-app.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <!-- or whenever the file size reaches 10MB --> <maxFileSize>10MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> <!-- keep 30 days' worth of history --> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- Configure Log Levels --> <!-- Set the root logger level to INFO and attach appenders --> <root level="INFO"> <appender-ref ref="STDOUT" /> <!-- Log INFO and above to console --> <appender-ref ref="FILE" /> <!-- Log INFO and above to file --> </root> <!-- Set specific logger levels (e.g., for noisy libraries or specific packages) --> <logger name="com.mycompany.app.specific" level="DEBUG" additivity="false"> <!-- Log DEBUG and above from this package ONLY to console --> <appender-ref ref="STDOUT" /> </logger> <logger name="org.eclipse.jetty" level="WARN" /> <!-- Reduce logging from Jetty --> </configuration>
-
Use SLF4j API in Your Code:
package com.mycompany.app; // Import SLF4j classes import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class App { // Get a logger instance, typically named after the class private static final Logger logger = LoggerFactory.getLogger(App.class); public static void main(String[] args) { logger.info("Application starting..."); // Will be logged if root level is INFO or lower App app = new App(); app.performTask("Some Data"); logger.warn("This is a warning message."); // Will be logged logger.error("This is an error message."); // Will be logged // These will only be logged if the root level or specific logger level is DEBUG/TRACE logger.debug("Debugging information: {}", generateDebugInfo()); // Use {} for deferred construction logger.trace("Entering specific detailed block..."); try { int result = 10 / 0; // Cause exception } catch (Exception e) { logger.error("An exception occurred during calculation", e); // Log exception with stack trace } logger.info("Application finished."); } public void performTask(String data) { logger.debug("Performing task with data: {}", data); // Logged if level is DEBUG if (data == null) { logger.warn("Received null data for task."); return; } // ... task logic ... logger.info("Task performed successfully for data starting with: {}", data.substring(0, Math.min(5, data.length()))); } private static String generateDebugInfo() { System.out.println("--- Generating Debug Info ---"); // Illustrate deferred execution // Simulate complex debug string generation return "User=" + System.getProperty("user.name") + ", Java=" + System.getProperty("java.version"); } }
Using SLF4j with a robust backend like Logback provides flexible, configurable, and performant logging essential for developing and maintaining applications.
Workshop Setting up a Maven/Gradle Project with Git, JUnit, and Logging
Objective: Create a simple Java project using either Maven or Gradle, initialize a Git repository, add JUnit 5 for unit testing, and integrate SLF4j with Logback for logging.
Steps (Choose either Maven or Gradle):
Option A: Using Maven
-
Create Project Directory:
-
Initialize Git Repository:
-
Create
pom.xml
: Create a file namedpom.xml
in themaven-demo-project
directory with the following content:<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example.demoproject</groupId> <artifactId>maven-demo-project</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <junit.version>5.9.2</junit.version> <slf4j.version>2.0.7</slf4j.version> <logback.version>1.4.7</logback.version> </properties> <dependencies> <!-- SLF4j API --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <!-- Logback Implementation --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency> <!-- JUnit 5 Jupiter --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <!-- Compiler Plugin --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.10.1</version> <configuration> <source>${maven.compiler.source}</source> <target>${maven.compiler.target}</target> </configuration> </plugin> <!-- Surefire Plugin (for running tests) --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.0.0-M7</version> </plugin> <!-- JAR Plugin (to specify main class for executable JAR) --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.3.0</version> <configuration> <archive> <manifest> <mainClass>com.example.demoproject.App</mainClass> </manifest> </archive> </configuration> </plugin> </plugins> </build> </project>
-
Create Directory Structure:
-
Create Main Application Class (
src/main/java/com/example/demoproject/App.java
):package com.example.demoproject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class App { private static final Logger logger = LoggerFactory.getLogger(App.class); public String getGreeting() { logger.info("Generating greeting..."); return "Hello from Maven Demo Project!"; } public static void main(String[] args) { logger.info("Application Starting (Maven)"); App app = new App(); System.out.println(app.getGreeting()); logger.info("Application Finished (Maven)"); } }
-
Create Logback Configuration (
src/main/resources/logback.xml
): (Copy the examplelogback.xml
from the Logging section above). -
Create Unit Test Class (
src/test/java/com/example/demoproject/AppTest.java
):package com.example.demoproject; import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.*; class AppTest { private App app; @BeforeEach void setUp() { app = new App(); } @Test @DisplayName("App should have a greeting") void testAppHasGreeting() { assertNotNull(app.getGreeting(), "App greeting should not be null"); } @Test @DisplayName("Greeting should contain expected text") void testGreetingContent() { assertTrue(app.getGreeting().contains("Maven Demo Project"), "Greeting text mismatch"); } }
-
Create
.gitignore
: Create a.gitignore
file in themaven-demo-project
root with the content from the Git section example above. -
Build and Test:
You should seeBUILD SUCCESS
. Check thetarget/
directory for the JAR file andtarget/surefire-reports/
for test results. Log messages (INFO level) should appear during the build/test phase if tests trigger logging, or when running the app. -
Run the Application:
You should see the "Hello..." message printed to the console, and the INFO log messages fromApp.main
. -
Commit to Git:
Option B: Using Gradle
-
Create Project Directory:
-
Initialize Git Repository:
-
Initialize Gradle Project (using
init
task): This automatically creates basic files and the wrapper.This createsgradle init # Select project type: 2 (application) # Select implementation language: 3 (Java) # Select build script DSL: 1 (Groovy) or 2 (Kotlin) - We'll use Groovy for this example # Select test framework: 4 (JUnit Jupiter) # Project name: (accept default: gradle-demo-project) # Source package: (accept default or enter e.g., com.example.demoproject)
build.gradle
,settings.gradle
,gradlew
,gradlew.bat
, and thesrc
directory structure with sampleApp.java
andAppTest.java
. -
Modify
build.gradle
: Open the generatedbuild.gradle
file and add the SLF4j/Logback dependencies. Also ensure Java version is set.plugins { id 'java' id 'application' // Keep the application plugin } group = 'com.example.demoproject' // Adjust if needed version = '1.0.0' // Adjust if needed repositories { mavenCentral() } java { // Ensure Java compatibility is set sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } application { // Define the main class for the application. mainClass = 'com.example.demoproject.App' // Update if your package/class name differs } dependencies { // SLF4j API implementation 'org.slf4j:slf4j-api:2.0.7' // Use latest SLF4j 2.x // Logback Implementation (binding) - runtimeOnly is typical for logging implementations runtimeOnly 'ch.qos.logback:logback-classic:1.4.7' // Use latest Logback // JUnit Jupiter testing dependencies (likely added by 'gradle init') testImplementation(platform('org.junit:junit-bom:5.9.2')) testImplementation('org.junit.jupiter:junit-jupiter') } tasks.named('test') { // Use JUnit Platform for running tests (likely added by 'gradle init') useJUnitPlatform() }
-
Review/Modify Main Application Class (
src/main/java/.../App.java
):gradle init
creates a basicApp.java
. Modify it to include logging:// Adjust package declaration if needed package com.example.demoproject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class App { private static final Logger logger = LoggerFactory.getLogger(App.class); public String getGreeting() { logger.info("Generating greeting..."); return "Hello from Gradle Demo Project!"; } public static void main(String[] args) { logger.info("Application Starting (Gradle)"); System.out.println(new App().getGreeting()); logger.info("Application Finished (Gradle)"); } }
-
Create Logback Configuration (
src/main/resources/logback.xml
):(Copy the examplemkdir -p src/main/resources # Ensure directory exists # Create logback.xml inside src/main/resources nano src/main/resources/logback.xml
logback.xml
from the Logging section above). -
Review/Modify Unit Test Class (
src/test/java/.../AppTest.java
):gradle init
creates a basicAppTest.java
. Modify it if necessary:// Adjust package declaration if needed package com.example.demoproject; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class AppTest { @Test void appHasAGreeting() { App classUnderTest = new App(); assertNotNull(classUnderTest.getGreeting(), "app should have a greeting"); assertTrue(classUnderTest.getGreeting().contains("Gradle Demo Project"), "Greeting text mismatch"); } }
-
Create/Verify
.gitignore
:gradle init
usually creates a good starting.gitignore
. Review it and add any missing entries (likelogs/
if you log to files). -
Build and Test: Use the Gradle wrapper (
You should seegradlew
).BUILD SUCCESSFUL
. Check thebuild/libs/
directory for the JAR andbuild/reports/tests/test/index.html
for test reports. -
Run the Application: Use the
You should see the "Hello..." message and the INFO log messages printed to the console.run
task provided by theapplication
plugin. -
Commit to Git:
This workshop provides a practical foundation for starting Java projects using standard build tools, incorporating version control, testing, and logging – essential skills for any Java developer.
Conclusion
This exploration has taken you from the fundamental syntax and object-oriented principles of Java to intermediate concepts like collections, exceptions, and file I/O, culminating in advanced topics such as concurrency, networking, reflection, annotations, and the module system. We've also covered the essential tooling and practices – build automation with Maven/Gradle, version control with Git, debugging techniques, unit testing with JUnit, and structured logging with SLF4j/Logback – that underpin professional Java development, particularly within the versatile Linux environment.
Java's strength lies in its vast ecosystem, platform independence (WORA), strong community support, and its applicability to a wide range of domains, from enterprise backend systems and web applications to Android mobile development and big data processing. The combination of Java's robust features and Linux's power and openness creates a formidable platform for building scalable, reliable software.
Mastering Java is an ongoing journey. The concepts covered here provide a solid foundation, but the landscape continues to evolve.
Next Steps and Further Learning:
- Deep Dive into
java.util.concurrent
: Explore advanced concurrency utilities likeCompletableFuture
, advancedLock
implementations, and atomic operations in greater detail. - Java Streams API (Java 8+): Master functional-style operations on collections for more concise and expressive data processing.
- Frameworks:
- Spring Framework (Spring Boot): The dominant framework for building enterprise Java applications, simplifying web development, data access, security, microservices, and more.
- Jakarta EE (formerly Java EE): The standard platform for enterprise Java, providing APIs for web services, persistence (JPA), messaging (JMS), etc. Frameworks like Quarkus and Helidon are modern implementations.
- Databases and Persistence: Learn JDBC for direct database interaction and ORM (Object-Relational Mapping) frameworks like JPA (with implementations like Hibernate) or Jooq for easier data management.
- Web Development: Explore Servlets, JSP, and modern web frameworks built on top (Spring MVC, JSF, etc.).
- Testing Frameworks: Delve deeper into JUnit 5 features (parameterized tests, dynamic tests) and explore mocking frameworks like Mockito or EasyMock.
- Build Tools Mastery: Learn advanced features of Maven or Gradle, such as multi-module projects, custom plugins, and profile management.
- Containerization (Docker, Kubernetes): Understand how to package and deploy Java applications using containers, a standard practice in modern cloud environments, especially on Linux.
- Cloud Platforms: Explore deploying and managing Java applications on cloud providers like AWS, Google Cloud, or Azure.
- Android Development: If interested in mobile, Java (alongside Kotlin) is a primary language for native Android app development.
Continuously practice by building projects, contribute to open-source, and stay updated with the latest Java releases and ecosystem developments. The combination of theoretical knowledge and hands-on practice within the Linux environment will equip you well for a successful career in software development. Good luck!