[go: up one dir, main page]

0% found this document useful (0 votes)
62 views145 pages

Important Question With Answers UNIT 1 To 5

Uploaded by

ydwdeep
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
62 views145 pages

Important Question With Answers UNIT 1 To 5

Uploaded by

ydwdeep
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 145

ABES Engineering College, Ghaziabad

Department of Computer Science Engineering (AIML)

Session: 2023-24 Semester: 4th Section: A, B, C


Course Code: BCS 403 Course Name: Object Oriented Programming with Java

Important question with Answers UNIT 1 TO 5

UNIT: 1
Q1: Why Choose Java for Software Development? What is the
History of Java and How Has It Evolved Over Time? Explain
the Role and Functionality of JVM, JRE, and Java
Environment?

Ans:

Why Choose Java for Software Development?

1. Platform Independence: Java is designed to be platform-


independent at both the source and binary levels. This is
achieved through the Java Virtual Machine (JVM) which
allows Java applications to be run on any device or operating
system that has a JVM installed, adhering to the principle of
"Write Once, Run Anywhere" (WORA).

2. Object-Oriented: Java is an object-oriented programming


language which promotes code reuse and modular design.
Concepts like inheritance, encapsulation, polymorphism, and
abstraction are central to Java, making it a robust language
for building complex software systems.

3. Robustness and Security: Java provides strong memory


management, automatic garbage collection, exception
handling, and a strong type-checking mechanism. It also has
built-in security features such as cryptographic algorithms,
secure communication protocols, and user authentication,
making it a secure choice for web applications.

4. Rich API and Libraries: Java has a rich Application


Programming Interface (API) and a vast set of libraries that
provide functionality for everything from data structures,
networking, and database connectivity to graphical user
interfaces and concurrency.

5. Large Developer Community: Java has a large and active


community which contributes to a wealth of knowledge,
libraries, frameworks, and tools. This community support
ensures continuous improvements and updates to the
language and its ecosystem.

6. Multithreading: Java supports multithreading at the


language level, allowing developers to write programs that
can perform multiple tasks simultaneously, which is essential
for high-performance and responsive applications.

7. Scalability: Java is highly scalable, making it suitable for


small applications as well as large enterprise solutions. It can
handle many users and transactions efficiently.

History of Java and Its Evolution

1. Creation and Early Years (1991-1995): Java was


initiated by James Gosling, Mike Sheridan, and Patrick
Naughton at Sun Microsystems in 1991. It was first designed
for interactive television but was too advanced for the digital
cable television industry. The project was called "Oak" after
an oak tree outside Gosling's office, and later renamed "Java"
from a list of random words.

2. Initial Release (1996): Java 1.0 was officially released by


Sun Microsystems in 1996. It provided the WORA capability,
allowing compiled Java code to run on any platform without
modification. Applets, which could run in web browsers,
were also introduced.

3. Expansion and Java 2 (1998): Java 2 was released in


1998 and was a significant update, introducing the Swing
graphical API and the Collections Framework. The platform
was also divided into three editions: Java Standard Edition
(J2SE), Java Enterprise Edition (J2EE), and Java Micro
Edition (J2ME).

4. Open Source and Community (2006): In 2006, Sun


Microsystems made most of Java’s implementation available
under the GNU General Public License (GPL), boosting its
adoption and innovation by the open-source community.

5. Oracle Acquisition (2010): Oracle Corporation acquired


Sun Microsystems in 2010, taking over the stewardship of
Java. Oracle continued to develop the language and released
several significant versions, enhancing performance, security,
and scalability.

6. Modern Features and Current State: Recent versions of


Java have introduced many modern features. Java 8 brought
lambda expressions and the Stream API, significantly
enhancing the language's functional programming
capabilities. Java 9 introduced the module system, Java 10
brought local variable type inference, and Java 11 extended
long-term support (LTS) for enterprise use. Subsequent
versions have continued to add new features, performance
improvements, and security enhancements.

Role and Functionality of JVM, JRE, and Java


Environment

1. Java Virtual Machine (JVM):

 Role: The JVM is the cornerstone of Java’s platform


independence. It is an abstract computing machine that
enables a computer to run Java programs.
 Functionality: When a Java program is compiled, it is
converted into bytecode. The JVM interprets this
bytecode and translates it into machine code for the host
system to execute. The JVM manages system memory
and provides garbage collection.

2. Java Runtime Environment (JRE):

 Role: The JRE is a subset of the Java Development Kit


(JDK) that provides the libraries, Java Virtual Machine,
and other components to run applications written in
Java.
 Functionality: The JRE does not include development
tools such as compilers or debuggers. It is mainly
designed to execute Java programs, providing the
necessary environment including JVM and core
libraries.

3. Java Development Kit (JDK):

 Role: The JDK is a full-featured software development


kit necessary for developing Java applications. It
includes the JRE as well as development tools.
 Functionality: The JDK provides tools like javac (Java
compiler), javadoc (documentation generator), jar
(archiver), and more, enabling developers to write,
compile, and debug Java applications.

Q2: How Do You Define and Use Classes, Constructors, and


Methods in Java? What are Access Specifiers in Java, and
How Do They Affect the Visibility and Accessibility of Class
Members?

Ans: Defining and Using Classes, Constructors, and


Methods in Java
1. Defining Classes

A class in Java is a blueprint for creating objects. It


encapsulates data for the object and methods to manipulate
that data.
Syntax:
public class ClassName {
// fields (variables)
// methods (functions)
}

Example:
public class Car {
// Fields
String color;
String model;
int year;

// Methods
void displayInfo() {
System.out.println("Model: " + model + ", Color: " +
color + ", Year: " + year);
}
}

2. Using Constructors

A constructor is a special method that is called when an


object is instantiated. It initializes the new object.
Syntax:
public ClassName(parameters) {
// initialization code
}

Example:

public class Car {


// Fields
String color;
String model;
int year;

// Constructor
public Car(String color, String model, int year) {
this.color = color;
this.model = model;
this.year = year;
}

// Methods
void displayInfo() {
System.out.println("Model: " + model + ", Color: " +
color + ", Year: " + year);
}
}

Using the Class and Constructor:


public class Main {
public static void main(String[] args) {
Car myCar = new Car("Red", "Toyota", 2020);
myCar.displayInfo(); // Output: Model: Toyota, Color:
Red, Year: 2020
}
}

3. Defining Methods

Methods in Java define the behavior of the objects created


from the class.
Syntax:
returnType methodName(parameters) {
// method body
}

Example:

public class Car {


// Fields
String color;
String model;
int year;

// Constructor
public Car(String color, String model, int year) {
this.color = color;
this.model = model;
this.year = year;
}

// Method
void startEngine() {
System.out.println("Engine started");
}
void displayInfo() {
System.out.println("Model: " + model + ", Color: " +
color + ", Year: " + year);
}
}

Using Methods:
public class Main {
public static void main(String[] args) {
Car myCar = new Car("Red", "Toyota", 2020);
myCar.startEngine(); // Output: Engine started
myCar.displayInfo(); // Output: Model: Toyota, Color:
Red, Year: 2020
}
}

Access Specifiers in Java

Access specifiers (or access modifiers) in Java define the


scope of access to the members of a class (fields, methods,
constructors, etc.). They control the visibility and
accessibility of these members.

Types of Access Specifiers

1. public
o Visibility: Everywhere. The member is accessible
from any other class.
o Example:
public class Car {
public String model;
}
2. private
o Visibility: Within the same class only. The
member is not accessible from outside the class.
o Example:
public class Car {
private String model;
}

3. protected
o Visibility: Within the same package and
subclasses. The member is accessible within its
own package and by subclasses.
o Example:

public class Car {


protected String model;
}

4. default (no modifier)


o Visibility: Within the same package. The member
is accessible only within its own package.
o Example:

public class Car {


String model; // default access
}

Effect on Class Members

 public: Members declared as public can be accessed


from any other class in any package.
 private: Members declared as private can only be
accessed within the class they are declared in. This
ensures encapsulation.
 protected: Members declared as protected can be
accessed within the same package and by subclasses
even if they are in different packages.
 default: Members with no access modifier are
accessible only within their own package.
Example:

public class Car {


public String color; // Accessible from anywhere
private String model; // Accessible only within Car class
protected int year; // Accessible within package and
subclasses
String owner; // Accessible within package (default)

// Constructor
public Car(String color, String model, int year, String
owner) {
this.color = color;
this.model = model;
this.year = year;
this.owner = owner;
}

// Methods
public void displayInfo() {
System.out.println("Model: " + model + ", Color: " +
color + ", Year: " + year);
}

private void startEngine() {


System.out.println("Engine started");
}
protected void service() {
System.out.println("Car serviced");
}

void changeOwner(String newOwner) {


owner = newOwner;
}
}

class Test {
public static void main(String[] args) {
Car myCar = new Car("Red", "Toyota", 2020, "Alice");
myCar.displayInfo(); // Accessible
// myCar.startEngine(); // Not accessible (private)
myCar.service(); // Accessible (protected, same
package)
myCar.changeOwner("Bob"); // Accessible (default,
same package)
}
}

Output:
Model: Toyota, Color: Red, Year: 2020
Car serviced

Q3: How Do Control Flow Statements (if, for, while, switch)


Work in Java, and What Are Their Common Use Cases?

Answer: Control Flow Statements in Java

Control flow statements are used to dictate the order in which


instructions in a program are executed. Java provides several
types of control flow statements, including conditional
statements (if, switch) and loops (for, while, do-while).

1. if Statement

The if statement allows conditional execution of code blocks


based on boolean expressions.

Syntax:

if (condition) {
// code to be executed if the condition is true
} else {
// code to be executed if the condition is false
}

Example:

int age = 18;


if (age >= 18) {
System.out.println("You are an adult.");
} else {
System.out.println("You are a minor.");
}

Use Case:

 Making decisions based on user input (e.g., age


verification).
 Checking preconditions before executing a block of
code.
2. for Loop

The for loop is used to execute a block of code a certain


number of times.

Syntax:

for (initialization; condition; update) {


// code to be executed
}

Example:

for (int i = 0; i < 5; i++) {


System.out.println("Iteration: " + i);
}

Use Case:

 Iterating over arrays or collections.


 Repeating a task several times.

3. while Loop

The while loop repeatedly executes a block of code if a


condition is true.

Syntax:

while (condition) {
// code to be executed
}

Example:

int count = 0;
while (count < 5) {
System.out.println("Count: " + count);
count++;
}

Use Case:

 Running a loop until a certain condition is met.


 Waiting for user input or some external event.

4. do-while Loop

The do-while loop is similar to the while loop, but it


guarantees that the code block is executed at least once.

Syntax:

do {
// code to be executed
} while (condition);

Example:
int count = 0;
do {
System.out.println("Count: " + count);
count++;
} while (count < 5);

Use Case:

 Running a block of code at least once regardless of the


condition.
 Menu-driven programs where the menu is displayed at
least once.

5. switch Statement

The switch statement allows the execution of a block of code


among many alternatives based on the value of an expression.

Syntax:

switch (variable) {
case value1:
// code to be executed if variable == value1
break;
case value2:
// code to be executed if variable == value2
break;
// more cases
default:
// code to be executed if variable doesn't match any case
}
Example:

int day = 3;
switch (day) {
case 1:
System.out.println("Monday");
break;
case 2:
System.out.println("Tuesday");
break;
case 3:
System.out.println("Wednesday");
break;
default:
System.out.println("Invalid day");
break;
}

Use Case:

 Replacing multiple if-else statements when comparing a


variable to multiple constant values.
 Implementing menu-driven programs.

Summary

Control flow statements are essential in Java programming as


they allow the creation of dynamic and responsive programs.
Here’s a quick overview:

 if Statement: Executes a block of code based on a


boolean condition. Used for decision making.
 for Loop: Iterates a block of code a specific number of
times. Used for counting iterations, iterating over arrays
or collections.
 While Loop: Repeatedly executes a block of code if a
condition is true. Used when the number of iterations
is not known beforehand.
 do-while Loop: Similar to while, but guarantees at least
one execution. Used for menu-driven programs.
 switch Statement: Executes one block of code among
many based on the value of an expression. Used to
simplify multiple if-else conditions.

Understanding these control flow statements is fundamental


for developing efficient and logical Java applications.

Q4: How Do Inheritance and Method Overriding Enhance Code


Reusability and Flexibility in Java? What Are the Differences
Between Overloading and Overriding in Java, and When
Should Each Be Used?

Ans: Inheritance and Method Overriding in Java

Inheritance

Inheritance is a fundamental concept in object-oriented


programming (OOP) that allows a new class (subclass) to
inherit properties and behaviors (fields and methods) from an
existing class (superclass). This promotes code reusability
and establishes a natural hierarchical relationship between
classes.
Syntax:

class Superclass {
// fields and methods
}

class Subclass extends Superclass {


// additional fields and methods
}

Example:

// Superclass
class Animal {
void eat() {
System.out.println("This animal eats food.");
}
}

// Subclass
class Dog extends Animal {
void bark() {
System.out.println("The dog barks.");
}
}

public class Main {


public static void main(String[] args) {
Dog myDog = new Dog();
myDog.eat(); // Inherited method
myDog.bark(); // Subclass-specific method
}
}
In the example above, the Dog class inherits the eat method
from the Animal class, demonstrating code reuse.

Method Overriding

Method overriding occurs when a subclass provides a specific


implementation for a method that is already defined in its
superclass. This allows a subclass to customize or extend the
behavior of a method.
Syntax:
class Superclass {
void display() {
System.out.println("Display from Superclass");
}
}

class Subclass extends Superclass {


@Override
void display() {
System.out.println("Display from Subclass");
}
}

Example:

// Superclass
class Animal {
void sound() {
System.out.println("This animal makes a sound.");
}
}
// Subclass
class Dog extends Animal {
@Override
void sound() {
System.out.println("The dog barks.");
}
}

public class Main {


public static void main(String[] args) {
Animal myAnimal = new Animal();
myAnimal.sound(); // Output: This animal makes a
sound.

Dog myDog = new Dog();


myDog.sound(); // Output: The dog barks.
}
}

In this example, the Dog class overrides the sound method of


the Animal class, providing its own specific implementation.

Overloading vs. Overriding

Method Overloading

Method overloading occurs when multiple methods in the


same class have the same name but different parameter lists
(different types, number, or both). Overloading is a compile-
time polymorphism (static binding).
Syntax:

class ClassName {
void methodName(int a) {
// implementation
}

void methodName(double a) {
// implementation
}

void methodName(int a, int b) {


// implementation
}
}

Example:

class Calculator {
int add(int a, int b) {
return a + b;
}

double add(double a, double b) {


return a + b;
}

int add(int a, int b, int c) {


return a + b + c;
}
}

public class Main {


public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(10, 20)); // Output: 30
System.out.println(calc.add(10.5, 20.5)); // Output:
31.0
System.out.println(calc.add(10, 20, 30)); // Output:
60
}
}

Method Overriding

Method overriding allows a subclass to provide a specific


implementation for a method that is already defined in its
superclass. Overriding is a runtime polymorphism (dynamic
binding).
Example: (Already provided above in the Method
Overriding section)

Differences Between Overloading and Overriding

When to Use

Use Method Overloading When:

 You need to perform similar operations with different


types or numbers of arguments.
 Example: Creating utility methods that accept different
types or counts of parameters (e.g., add methods for
integers, doubles, etc.).

Use Method Overriding When:

 You need to provide a specific implementation of a


method that is defined in a superclass.
 Example: Creating specialized behavior in a subclass
that differs from the generic behavior provided by the
superclass (e.g., overriding the sound method in a Dog
class to provide a specific sound).
Important Points

 Inheritance enhances code reusability by allowing new


classes to inherit properties and methods from existing
classes.
 Method Overriding provides flexibility by allowing
subclasses to modify the behavior of methods defined in
their superclasses.
 Method Overloading allows defining multiple methods
with the same name but different parameters to handle
various types of input within the same class.
 Method Overriding allows providing a new
implementation for a method inherited from a
superclass, thus enabling polymorphism.
Understanding these concepts and knowing when to use them
is essential for writing efficient, reusable, and flexible Java
code.

Q5: How Do Interfaces and Abstract Classes Support Abstraction


and Polymorphism in Java?

Ans: Interfaces and Abstract Classes in Java


Both interfaces and abstract classes are fundamental in Java
for achieving abstraction and polymorphism. They allow
developers to define methods that must be implemented by
derived classes, ensuring a certain level of abstraction and
enabling polymorphic behavior.

Abstract Classes

An abstract class is a class that cannot be instantiated on its


own and is intended to be subclassed. It can contain abstract
methods (methods without implementation) as well as
concrete methods (methods with implementation).
Syntax:

abstract class Animal {


// Abstract method (does not have a body)
abstract void sound();

// Concrete method
void eat() {
System.out.println("This animal eats food.");
}
}

Example:

// Abstract class
abstract class Animal {
abstract void sound();

void eat() {
System.out.println("This animal eats food.");
}
}

// Subclass (inheriting from Animal)


class Dog extends Animal {
@Override
void sound() {
System.out.println("The dog barks.");
}
}

public class Main {


public static void main(String[] args) {
Dog myDog = new Dog();
myDog.sound(); // Output: The dog barks.
myDog.eat(); // Output: This animal eats food.
}
}

Key Points:
 Abstraction: Abstract classes allow you to define a
template for other classes without implementing the
entire functionality.
 Polymorphism: Abstract classes enable polymorphism
by allowing derived classes to provide specific
implementations of abstract methods.

Interfaces

An interface in Java is a reference type, like a class, that can


contain only constants, method signatures, default methods,
static methods, and nested types. Interfaces cannot contain
instance fields or constructors.
Syntax:

interface Animal {
void sound(); // Abstract method
}

Example:

// Interface
interface Animal {
void sound();
default void eat() {
System.out.println("This animal eats food.");
}
}

// Implementing the interface


class Dog implements Animal {
@Override
public void sound() {
System.out.println("The dog barks.");
}
}

public class Main {


public static void main(String[] args) {
Dog myDog = new Dog();
myDog.sound(); // Output: The dog barks.
myDog.eat(); // Output: This animal eats food.
}
}
Key Points:
 Abstraction: Interfaces allow you to define a contract
that classes must follow, without providing the
implementation.
 Polymorphism: Interfaces enable polymorphism by
allowing objects of different classes to be treated as
objects of the interface type.

When to Use Abstract Classes and Interfaces

Use Abstract Classes When:

 You want to share code among several closely related


classes.
 You need to define non-static or non-final fields.
 You want to use constructors to enforce certain
initialization logic.

Use Interfaces When:

 You expect that unrelated classes will implement your


interface.
 You want to specify the behavior a class must
implement.
 You want to take advantage of multiple inheritance of
type.
 You want to provide a contract for classes with no
shared implementation.

Abstraction and Polymorphism

Abstraction:
 Abstract classes and interfaces both provide a way to
achieve abstraction in Java. They allow you to define
methods without implementation, which must be
implemented by subclasses or implementing classes.
This enforces a contract for what methods a class must
have without dictating how those methods should be
implemented.
Polymorphism:
 Polymorphism is the ability of different classes to
respond to the same method call in different ways. By
using abstract classes and interfaces, you can write code
that works on the abstract type (interface or abstract
class) rather than a specific concrete class. This allows
for flexibility and the ability to handle new classes that
implement the interface or extend the abstract class
without modifying the existing code.
Example of Polymorphism:
interface Animal {
void sound();
}

class Dog implements Animal {


@Override
public void sound() {
System.out.println("The dog barks.");
}
}

class Cat implements Animal {


@Override
public void sound() {
System.out.println("The cat meows.");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal1 = new Dog();
Animal myAnimal2 = new Cat();

myAnimal1.sound(); // Output: The dog barks.


myAnimal2.sound(); // Output: The cat meows.
}
}

In this example, myAnimal1 and myAnimal2 are both of type


Animal, but they refer to different concrete classes (Dog and
Cat). The sound method behaves differently depending on the
actual object that the Animal reference points to. This
demonstrates polymorphism.
Important Points:
 Abstract Classes: Used to define a common template
for related classes with shared code. They can contain
both abstract and concrete methods and support single
inheritance.
 Interfaces: Define a contract that multiple classes can
implement. They support multiple inheritance and can
contain abstract methods, default methods, and static
methods.
 Abstraction: Both abstract classes and interfaces
provide a way to achieve abstraction by defining
methods without implementation.
 Polymorphism: Allows different classes to be treated
as instances of the abstract type (interface or abstract
class), enabling flexibility and code reusability.
Understanding how to use abstract classes and interfaces
effectively can greatly enhance the design, flexibility, and
maintainability of your Java applications.
Q6: How Do You Create and Use Packages in Java, and What
Are the Benefits of Organizing Code into Packages? What Is
the CLASSPATH in Java, and How Do You Configure It for
Working with Packages and JAR Files?

Ans: What is a Package?

A package in Java is a namespace that organizes a set of


related classes and interfaces. Think of it as a folder in a file
directory that contains related files. Packages help avoid
naming conflicts and can control access to classes and
interfaces.

Creating a Package

To create a package in Java, you use the package keyword


followed by the package name at the top of your Java source
file.
Example: Let's create a package named
com.example.utilities.
Create a directory structure that matches the package name:

com/example/utilities

Create a Java file inside this directory:

// File: com/example/utilities/UtilityClass.java
package com.example.utilities;

public class UtilityClass {


public void printMessage() {
System.out.println("Hello from UtilityClass!");
}
}

Compile the Java file:


com/example/utilities/UtilityClass.java

Using a Package

To use the UtilityClass from the com.example.utilities


package in another class, you need to import the package.
Example:
// File: TestUtility.java
import com.example.utilities.UtilityClass;

public class TestUtility {


public static void main(String[] args) {
UtilityClass util = new UtilityClass();
util.printMessage();
}
}

Compile and run the TestUtility class:


javac TestUtility.java
java TestUtility

Benefits of Organizing Code into Packages

 Modularity: Packages help organize code into logical


units, making it easier to manage and maintain.
 Namespace Management: Packages prevent naming
conflicts by providing a namespace for classes.
 Access Control: Packages can restrict access to classes,
methods, and fields, enhancing security and
encapsulation.
 Reusability: Classes in packages can be reused across
different projects and applications.
 Ease of Maintenance: Grouping related classes and
interfaces together makes it easier to locate and fix
bugs.

Understanding CLASSPATH in Java

What is CLASSPATH?

The CLASSPATH variable in Java tells the JVM and Java


compiler where to look for user-defined classes and
packages. It can include directories, JAR (Java Archive) files,
and ZIP files.

Configuring CLASSPATH

You can set the CLASSPATH in several ways:


 Environment Variable: Set the CLASSPATH
environment variable on your operating system.
 Command Line: Use the -classpath or -cp option with
the javac and java commands.
 Manifest File in JAR: Specify the Class-Path attribute
in the manifest file of a JAR.
Example 1: Setting CLASSPATH as an Environment
Variable
Windows:

Set CLASSPATH=.;C:\path\to\classes;C:\path\to\library\
lib.jar

Linux/Mac:
CLASSPATH=.:/path/to/classes:/path/to/library/lib.jar

Example 2: Setting CLASSPATH on the Command Line

javac -cp .;C:\path\to\classes;C:\path\to\library\lib.jar


TestUtility.java
java -cp .;C:\path\to\classes;C:\path\to\library\lib.jar
TestUtility

Example 3: Setting CLASSPATH in a JAR Manifest File


Create a manifest.mf file with the following content:

Class-Path: lib/library.jar

5. Create the JAR file:

jar cfm MyApp.jar manifest.mf -C bin .

6. Run the application using the JAR file:

 java -jar MyApp.jar


Important Points:
 Packages: Use the package keyword to create packages
and import them. Packages help in organizing code,
preventing naming conflicts, and managing access
control.
 CLASSPATH: The CLASSPATH environment
variable or command-line option specifies the locations
where the JVM and compiler should look for classes
and packages. It can be set globally as an environment
variable or locally for individual commands.
 Benefits: Organizing code into packages enhances
modularity, reusability, ease of maintenance,
namespace management, and access control.
Understanding and effectively using packages and
CLASSPATH can greatly improve the structure,
maintainability, and scalability of your Java applications.
By mastering these concepts, you'll be able to build more
organized, efficient, and scalable Java applications

UNIT: 2
Q1: What is the Difference Between Exceptions and Errors in
Java, and How Does the JVM React to Each?
Ans: In Java, both exceptions and errors are subclasses of
Throwable, but they serve different purposes and are used in
different contexts. Here's a detailed explanation of the
differences between exceptions and errors, along with how
the JVM reacts to each.

Exceptions in Java

Definition

 Exceptions are conditions that a program might want to


catch and handle. They are typically problems that
occur due to conditions that the program should
anticipate and recover from, such as file not found,
invalid user input, or network issues.
Types of Exceptions

Checked Exceptions:
These are exceptions that are checked at compile-time.
If a method throws a checked exception, it must either
handle the exception with a try-catch block or declare
the exception using the throws keyword in the method
signature.
o Examples: IOException, SQLException
Unchecked Exceptions:
These are exceptions that are not checked at compile-
time. They are subclasses of RuntimeException. The
compiler does not force the programmer to handle or
declare these exceptions.
o Examples: NullPointerException,
ArrayIndexOutOfBoundsException,
ArithmeticException

JVM Reaction to Exceptions

 Checked Exceptions: When a checked exception


occurs, the JVM looks for an appropriate catch block to
handle the exception. If no catch block is found, the
program terminates, and the stack trace is printed.
 Unchecked Exceptions: When an unchecked
exception occurs, the JVM looks for a catch block to
handle it. If no catch block is found, the program
terminates, and the stack trace is printed.

Errors in Java

Definition
 Errors are serious problems that a reasonable
application should not try to catch. They are typically
problems that are outside the control of the program and
indicate issues with the runtime environment itself.

Types of Errors

Virtual Machine Errors:


These errors occur in the Java Virtual Machine (JVM) itself.
Examples: OutOfMemoryError, StackOverflowError
Linkage Errors:
These errors occur when a class has some incompatibility
with the current definition of the class.
Examples: NoClassDefFoundError, UnsatisfiedLinkError

JVM Reaction to Errors

 Errors: When an error occurs, the JVM generally does


not look for catch blocks. Errors are considered
unrecoverable, and the JVM will typically terminate the
program and print the stack trace. Errors signal serious
problems such as memory leaks, hardware failures, or
issues that arise from JVM internals, which the
application cannot handle.

Summary of Differences
Understanding the differences between exceptions and errors, as well
as how the JVM handles them, is crucial for robust Java programming.
While exceptions are recoverable and should be managed within the
application, errors typically indicate serious issues that the application
cannot control or recover from.
Q2: What are Checked and Unchecked Exceptions in Java, and
How Should They Be Handled? How Do the try, catch,
finally, throw, and throws Keywords Work Together in Java
Exception Handling? Write a program related to it.

Ans: Checked and Unchecked Exceptions in Java

Checked Exceptions

 Definition: Checked exceptions are exceptions that are


checked at compile-time. If a method can potentially
throw a checked exception, the method must either
handle the exception with a try-catch block or declare it
using the throws keyword in the method signature.
 Examples: IOException, SQLException

Unchecked Exceptions

 Definition: Unchecked exceptions are exceptions that


are not checked at compile-time. They are subclasses of
RuntimeException. The compiler does not force the
programmer to handle or declare these exceptions.
 Examples: NullPointerException,
ArrayIndexOutOfBoundsException,
ArithmeticException

Exception Handling Keywords

try: The block of code where exceptions are expected to


occur.
catch: The block of code that handles the exception thrown
by the try block.
finally: The block of code that executes regardless of whether
an exception was thrown or not. It is typically used for
cleanup activities.
throw: Used to explicitly throw an exception.
throws: Used in the method signature to declare that a
method can throw an exception.

Example Program

Below is a program that demonstrates the use of checked and


unchecked exceptions and the try, catch, finally, throw, and
throws keywords.

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class ExceptionHandlingExample {

public static void main(String[] args) {


// Handling unchecked exception
(ArrayIndexOutOfBoundsException)
try {
int[] numbers = {1, 2, 3};
System.out.println(numbers[3]); // This will throw
ArrayIndexOutOfBoundsException
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Caught an unchecked exception: "
+ e);
}

// Handling checked exception (FileNotFoundException)


try {
readFile("nonexistentfile.txt");
} catch (FileNotFoundException e) {
System.out.println("Caught a checked exception: " +
e);
} finally {
System.out.println("Finally block executed.");
}

// Using throw keyword


try {
validateAge(15); // This will throw an exception
} catch (IllegalArgumentException e) {
System.out.println("Caught an exception thrown by
throw keyword: " + e);
}
}

// Method that throws a checked exception


public static void readFile(String fileName) throws
FileNotFoundException {
File file = new File(fileName);
Scanner scanner = new Scanner(file);
while (scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
}
scanner.close();
}

// Method that uses throw keyword to throw an exception


public static void validateAge(int age) {
if (age < 18) {
throw new IllegalArgumentException("Age must be
at least 18");
}
System.out.println("Valid age: " + age);
}
}

Explanation

Unchecked Exception Handling:


 An ArrayIndexOutOfBoundsException is thrown
when trying to access an invalid array index.
 The exception is caught in the catch block and a
message is printed.
Checked Exception Handling:
 The readFile method throws a
FileNotFoundException, a checked exception.
 The main method calls readFile inside a try block
and catches the exception in a catch block.
 The finally block is executed regardless of whether
an exception is thrown, ensuring cleanup activities.
Using throw Keyword:
 The validateAge method uses the throw keyword to
throw an IllegalArgumentException if the age is less
than 18.
 The main method calls validateAge inside a try block
and catches the exception in a catch block.
This program demonstrates how to handle both checked and
unchecked exceptions, use the try, catch, finally, throw, and
throws keywords, and ensures proper cleanup with the finally
block.

Q3: What is the Control Flow of an Exception in Java, and How


Can It Affect Program Execution?

Ans: Control Flow of an Exception in Java

In Java, the control flow of an exception refers to how the


program execution proceeds when an exception is thrown.
Understanding this flow is crucial for writing robust and
error-resilient programs.

Basic Control Flow

Exception Occurrence:
o When an exception occurs, the normal flow of the
program is disrupted. The Java Virtual Machine
(JVM) looks for a block of code that can handle
the exception.
Exception Propagation:
o If the exception is not caught in the current
method, it propagates up the call stack. This
means that the JVM searches for a suitable catch
block in the calling method. This continues up the
call stack until a suitable handler is found or the
program terminates.
Exception Handling:
o When a matching catch block is found, control
transfers to that block. The exception is
considered handled, and the program execution
continues from the end of the catch block.
Finally Block Execution:
o Regardless of whether an exception is caught or
not, the finally block (if present) is executed. This
block is typically used for cleanup tasks, such as
closing files or releasing resources.
Program Termination:
o If no suitable catch block is found in the entire call
stack, the JVM will terminate the program and
print the stack trace to the standard error output.

Example of Exception Control Flow

Here's an example to illustrate the control flow of an


exception:

public class ExceptionControlFlowExample {

public static void main(String[] args) {


try {
methodA(); // Call methodA which may throw an
exception
} catch (ArithmeticException e) {
System.out.println("Caught ArithmeticException in
main: " + e);
} finally {
System.out.println("Finally block in main executed.");
}
}

public static void methodA() {


try {
methodB(); // Call methodB which may throw an
exception
} catch (NumberFormatException e) {
System.out.println("Caught NumberFormatException
in methodA: " + e);
} finally {
System.out.println("Finally block in methodA
executed.");
}
}

public static void methodB() {


try {
int result = 10 / 0; // This will throw
ArithmeticException
} catch (Exception e) {
System.out.println("Caught Exception in methodB: "
+ e);
// Rethrow exception to propagate it to the caller
throw new ArithmeticException("Rethrown
ArithmeticException from methodB");
} finally {
System.out.println("Finally block in methodB
executed.");
}
}
}
Explanation of the Control Flow

methodB Execution:
 methodB attempts to perform a division by zero, which
throws an ArithmeticException.
 The catch block in methodB catches the exception and
prints a message.
 The exception is then rethrown using throw new
ArithmeticException("Rethrown ArithmeticException
from methodB");.
Exception Propagation:
 The rethrown ArithmeticException propagates back to
methodA.
 The finally block in methodB is executed before the
exception propagates.
methodA Execution:
 methodA does not handle the rethrown
ArithmeticException, so it continues to propagate up to
the caller (main method).
 The finally block in methodA is executed before the
exception continues to propagate.
main Method Execution:
The main method catches the ArithmeticException and prints
a message.
The finally block in main is executed last.

Important points

 Exception Occurrence: Disrupts the normal flow of


execution.
 Exception Propagation: Moves up the call stack if not
handled locally.
 Exception Handling: A catch block handles the
exception and allows the program to continue.
 Finally Block: Always executes after the try and catch
blocks, regardless of whether an exception was thrown
or handled.
 Program Termination: Occurs if the exception is not
handled and propagates beyond the main method.
Understanding this flow helps in designing effective
exception handling strategies, ensuring that resources are
properly cleaned up and that the program can recover
gracefully from unexpected situations.

Q4: How Can You Create and Use User-Defined Exceptions in


Java, and When Would This Be Necessary? Write a Java
program that asks the user to enter a number between 1 and
10. If the user enters a number outside this range, throw an
InvalidInputException.

Ans: Creating and using user-defined exceptions in Java


allows you to handle specific error conditions in a more
meaningful way. This is often necessary when you need to
enforce certain constraints or business rules that are unique to
your application.

Steps to Create and Use User-Defined Exceptions

Define the Exception Class:

Create a new class that extends the Exception class or one of


its subclasses. You can provide constructors to initialize the
exception with a message or other details.

Throw the Exception:

In your code, use the throw keyword to throw the user-


defined exception when a specific condition occurs.

Catch and Handle the Exception:

Use a try-catch block to handle the exception where it might


be thrown. This allows you to provide a meaningful response
or recovery mechanism.

Example Program

Here’s a Java program that demonstrates creating and using a


user-defined exception. The program asks the user to enter a
number between 1 and 10. If the user enters a number outside
this range, it throws an InvalidInputException.

import java.util.Scanner;

// User-defined exception class


class InvalidInputException extends Exception {
public InvalidInputException(String message) {
super(message);
}
}

public class UserDefinedExceptionExample {

public static void main(String[] args) {


Scanner scanner = new Scanner(System.in);
try {
System.out.print("Enter a number between 1 and 10:
");
int number = scanner.nextInt();
validateNumber(number);
System.out.println("You entered a valid number: " +
number);
} catch (InvalidInputException e) {
System.out.println("Error: " + e.getMessage());
} finally {
scanner.close();
}
}

// Method that validates the input number


public static void validateNumber(int number) throws
InvalidInputException {
if (number < 1 || number > 10) {
throw new InvalidInputException("Number " +
number + " is out of range. Please enter a number between 1
and 10.");
}
}
}

Explanation

User-Defined Exception Class (InvalidInputException):

InvalidInputException extends Exception and provides a


constructor that accepts a custom error message. This allows
for more descriptive exception handling.

Main Method:

The main method prompts the user to enter a number and


reads the input using a Scanner.
It then calls the validateNumber method to check if the
number is within the valid range (1 to 10).

validateNumber Method:

This method checks if the number is outside the valid range.


If so, it throws an InvalidInputException with a custom
message.
If the number is valid, the method completes without
throwing an exception.

Exception Handling:

In the main method, a try-catch block is used to catch the


InvalidInputException and print an appropriate error message
if the exception is thrown.
The finally block ensures that the Scanner object is closed,
regardless of whether an exception occurred.

When to Use User-Defined Exceptions

User-defined exceptions are useful in the following scenarios:

 Business Logic: When you need to enforce specific


business rules that cannot be handled by standard Java
exceptions.
 Custom Error Handling: When you want to provide
more specific error messages or handle errors in a way
that is unique to your application.
 Code Clarity: When you want to make your code more
readable and maintainable by defining exceptions that
represent specific error conditions.

This approach allows for more granular control over error


handling and can make your code more robust and easier to
understand.

Q5: Write a Java program that performs the following tasks:


1. Copy the contents of a text file (e.g., source.txt) to
another text file (e.g., destination.txt) using byte
streams.
2. Copy the contents of a text file (e.g., source.txt) to
another text file (e.g., destination_char.txt) using character
streams.

Ans: 1. Copying Using Byte Streams

Byte streams are used for handling raw binary data, including
text files. Here’s how to copy a file using byte streams:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class ByteStreamFileCopy {

public static void main(String[] args) {


String sourceFile = "source.txt";
String destinationFile = "destination.txt";
try (FileInputStream fis = new
FileInputStream(sourceFile);
FileOutputStream fos = new
FileOutputStream(destinationFile)) {

byte[] buffer = new byte[1024];


int bytesRead;

while ((bytesRead = fis.read(buffer)) != -1) {


fos.write(buffer, 0, bytesRead);
}

System.out.println("File copied successfully using


byte streams.");

} catch (IOException e) {
System.out.println("An error occurred: " +
e.getMessage());
}
}
}

Explanation:

 FileInputStream: Reads the content of the source file


as bytes.
 FileOutputStream: Writes bytes to the destination file.
 Buffering: A byte buffer of size 1024 bytes is used to
read and write data in chunks.
 Exception Handling: Catches IOException in case of
errors like file not found or read/write issues.

2. Copying Using Character Streams

Character streams handle data in characters, making them


more suitable for text files. Here’s how to copy a file using
character streams:

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class CharacterStreamFileCopy {

public static void main(String[] args) {


String sourceFile = "source.txt";
String destinationFile = "destination_char.txt";

try (FileReader fr = new FileReader(sourceFile);


FileWriter fw = new FileWriter(destinationFile)) {

char[] buffer = new char[1024];


int charsRead;

while ((charsRead = fr.read(buffer)) != -1) {


fw.write(buffer, 0, charsRead);
}

System.out.println("File copied successfully using


character streams.");

} catch (IOException e) {
System.out.println("An error occurred: " +
e.getMessage());
}
}
}

Explanation:

 FileReader: Reads the content of the source file as


characters.
 FileWriter: Writes characters to the destination file.
 Buffering: A character buffer of size 1024 characters is
used to read and write data in chunks.
 Exception Handling: Catches IOException to handle
potential file-related issues.

Important Points

 Byte Streams: Best for raw binary data and binary


files. They handle data byte by byte.
 Character Streams: Best for text data. They handle
data character by character and are more convenient for
text files.
Make sure to replace "source.txt", "destination.txt", and
"destination_char.txt" with actual file paths if you are testing
this code in a real environment. The programs use try-with-
resources to ensure that file streams are closed automatically
after operations are complete.
Q6: Write a Java program that implements the Producer-
Consumer problem using multithreading. The program
should have two threads: a producer thread and a consumer
thread. The producer thread should produce data and put it
into a shared buffer, while the consumer thread should
consume the data from the buffer.

Ans: The Producer-Consumer problem is a classic example of


a multithreaded application where one or more producer
threads generate data and put it into a shared buffer, while
one or more consumer threads consume the data from the
buffer. To manage synchronization between these threads, we
can use Java's synchronization mechanisms such as wait()
and notify().
Here’s a Java program that demonstrates this using
synchronized blocks and wait()/notify() methods to handle
communication between the producer and consumer threads:

Java Program: Producer-Consumer Problem

import java.util.LinkedList;
import java.util.Queue;

public class ProducerConsumer {

// Shared buffer (Queue) and buffer capacity


private static final int BUFFER_CAPACITY = 5;
private static final Queue<Integer> buffer = new
LinkedList<>();

public static void main(String[] args) {


// Create producer and consumer threads
Thread producer = new Thread(new Producer());
Thread consumer = new Thread(new Consumer());

// Start threads
producer.start();
consumer.start();
}

// Producer class
static class Producer implements Runnable {
@Override
public void run() {
int value = 0;
while (true) {
try {
produce(value);
value++;
Thread.sleep(1000); // Simulate time taken to
produce
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Producer interrupted.");
}
}
}

private synchronized void produce(int value) throws


InterruptedException {
while (buffer.size() == BUFFER_CAPACITY) {
System.out.println("Buffer full. Producer
waiting...");
wait(); // Wait until there is space in the buffer
}
buffer.add(value);
System.out.println("Produced: " + value);
notify(); // Notify consumer that data is available
}
}

// Consumer class
static class Consumer implements Runnable {
@Override
public void run() {
while (true) {
try {
consume();
Thread.sleep(1500); // Simulate time taken to
consume
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Consumer interrupted.");
}
}
}

private synchronized void consume() throws


InterruptedException {
while (buffer.isEmpty()) {
System.out.println("Buffer empty. Consumer
waiting...");
wait(); // Wait until there is data to consume
}
int value = buffer.poll();
System.out.println("Consumed: " + value);

notify(); // Notify producer that space is available


}
}
}

Explanation:

Shared Buffer:
A Queue is used as the shared buffer. It has a fixed capacity
(BUFFER_CAPACITY).
Producer Class:
The Producer class implements Runnable and continuously
produces data.
The produce() method is synchronized to ensure mutual
exclusion.
It checks if the buffer is full. If so, it waits (wait()) until the
consumer consumes some data and notifies (notify()) when it
has added new data to the buffer.
Consumer Class:
The Consumer class implements Runnable and continuously
consumes data.
The consume() method is synchronized to ensure mutual
exclusion.
It checks if the buffer is empty. If so, it waits (wait()) until
the producer adds some data and notifies (notify()) when it
has consumed data from the buffer.
Synchronization:
o synchronized blocks ensure that only one thread
can execute the critical section of code that
modifies the buffer at a time.
o wait() makes the current thread wait until it is
notified.
o notify() wakes up one of the threads that are
waiting on the monitor.
Thread Management:
o producer.start() and consumer.start() initiate the
producer and consumer threads.
This program demonstrates basic inter-thread communication
and synchronization using Java's built-in methods and
ensures that the producer and consumer work together
effectively. Adjust the sleep durations as needed to simulate
different production and consumption rates.
//Thread life cycle is important; please cover the topic with
notes.
UNIT: 3
Q1: How Do Functional Interfaces and Lambda Expressions
Improve Code Readability and Maintainability in Java? What
Are Method References, and How Do They Simplify Lambda
Expressions? Provide an Example.

Ans: Functional Interfaces and Lambda Expressions in


Java

Functional Interfaces

A functional interface in Java is an interface that contains


only one abstract method. This single abstract method makes
it possible to define the behavior of the method using a
lambda expression. The functional interface can have
multiple default or static methods but only one abstract
method. Common examples of functional interfaces in Java
are Runnable, Callable, Comparator, and those in the
java.util.function package like Function, Predicate,
Consumer, and Supplier.

Lambda Expressions

Lambda expressions provide a clear and concise way to


represent one method interface using an expression. They
enable you to write more readable and maintainable code by
reducing boilerplate code. Here's a simple example to
illustrate:

Without Lambda:

List<String> names = Arrays.asList("John", "Jane", "Doe");


Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});

With Lambda:

List<String> names = Arrays.asList("John", "Jane", "Doe");


Collections.sort(names, (s1, s2) -> s1.compareTo(s2));

Benefits of Lambda Expressions

Conciseness: They reduce the verbosity of the code.


Readability: The code becomes easier to read and
understand.
Maintainability: Less boilerplate code makes it easier to
maintain.

Method References

Method references are a shorthand notation of a lambda


expression to call a method. They improve the readability of
the code even further by removing unnecessary lambda
expressions when the method reference can be used.

There are four types of method references:

 Reference to a static method


 Reference to an instance method of a particular
object
 Reference to an instance method of an arbitrary
object of a particular type
 Reference to a constructor

Example of Method Reference

Without Method Reference:

List<String> names = Arrays.asList("John", "Jane", "Doe");


names.forEach(name -> System.out.println(name));

With Method Reference:

List<String> names = Arrays.asList("John", "Jane", "Doe");


names.forEach(System.out::println);

Example in Context

Let's see a more comprehensive example combining


functional interfaces, lambda expressions, and method
references.

Suppose we want to filter and print a list of names that start


with a specific letter.

Without Lambda and Method References:

List<String> names = Arrays.asList("John", "Jane", "Doe",


"Alex", "Chris");
List<String> filteredNames = new ArrayList<>();
for (String name : names) {
if (name.startsWith("J")) {
filteredNames.add(name);
}
}
for (String name : filteredNames) {
System.out.println(name);
}

With Lambda Expressions:

List<String> names = Arrays.asList("John", "Jane", "Doe",


"Alex", "Chris");
names.stream()
.filter(name -> name.startsWith("J"))
.forEach(name -> System.out.println(name));

With Method References:

List<String> names = Arrays.asList("John", "Jane", "Doe",


"Alex", "Chris");
names.stream()
.filter(name -> name.startsWith("J"))
.forEach(System.out::println);

Functional interfaces and lambda expressions improve code


readability and maintainability by allowing more concise and
expressive code. Method references further simplify lambda
expressions by providing a clear and straightforward way to
call methods. By leveraging these features, Java developers
can write cleaner, more readable, and maintainable code.

Q2: Explain the Stream API using diagram and its benefits
over traditional collection operations. Write a Java
program that filters a list of integers, squares the filtered
values, and prints the result using Stream API.
Ans:
Stream API in Java
The Stream API, introduced in Java 8, provides a powerful
and efficient way to process sequences of elements
(collections) in a functional style. It allows you to perform
operations such as filter, map, and reduce on collections in a
more readable and concise manner.
Benefits of Stream API over Traditional Collection
Operations:
Declarative Code: Stream API allows you to write more
readable and maintainable code by specifying what you want
to achieve rather than how to achieve it.
Lazy Evaluation: Intermediate operations are lazy, meaning
they are not executed until a terminal operation is invoked.
This can lead to performance improvements.
Parallel Processing: Streams can be easily parallelized to
leverage multi-core processors, improving performance for
large data sets.
Reduced Boilerplate: Stream operations often require fewer
lines of code compared to traditional iterative approaches.
Functional Style: Promotes functional programming
principles such as immutability and statelessness.
Stream API Diagram
Here is a diagram illustrating the basic workflow of the
Stream API:
Source: A collection, array, or I/O channel.
Stream: A sequence of elements supporting sequential and
parallel aggregate operations.
Intermediate Operations: Transformations such as filter, map,
sorted, which are lazy and return a new stream.
Terminal Operation: Triggers the processing of the pipeline
and returns a result or side effect such as collect, forEach,
reduce.
Example Java Program Using Stream API
Let's write a Java program that filters a list of integers,
squares the filtered values, and prints the results using the
Stream API.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamApiExample {

public static void main(String[] args) {


// Sample list of integers
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7,
8, 9, 10);

// Using Stream API to filter, square, and print the


results
List<Integer> squaredNumbers = numbers.stream()
.filter(num -> num % 2 == 0) // Filter even numbers
.map(num -> num * num) // Square each filtered
number
.collect(Collectors.toList()); // Collect the results
into a list

// Print the results


squaredNumbers.forEach(System.out::println);
}
}

Explanation:
Source: The source is the list of integers numbers.
Stream: numbers.stream() creates a stream from the list.
Intermediate Operations:
filter(num -> num % 2 == 0): Filters the stream to include
only even numbers.
map(num -> num * num): Maps each element of the filtered
stream to its square.
Terminal Operation:
collect(Collectors.toList()): Collects the squared values into a
list.
Print: squaredNumbers.forEach(System.out::println); prints
each element of the resulting list.
This program demonstrates how to leverage the Stream API
to write concise and expressive code for common operations
on collections.

Q3:
Write a Java program that performs the following tasks:

a. Encode a given string into Base64 format.


b. Decode a Base64 encoded string back to its original
form.
c. Demonstrate the encoding and decoding process using
sample input.

Ans: Java program that performs Base64 encoding and


decoding. The program demonstrates these processes using a
sample input string.

import java.util.Base64;

public class Base64Example {


public static void main(String[] args) {
// Sample input string
String originalString = "Hello, World!";

// Encode the string into Base64 format


String encodedString = encodeBase64(originalString);
System.out.println("Encoded String: " + encodedString);

// Decode the Base64 encoded string back to its original


form
String decodedString = decodeBase64(encodedString);
System.out.println("Decoded String: " + decodedString);
}

/**
* Encodes the given string into Base64 format.
*
* @param input The string to be encoded.
* @return The Base64 encoded string.
*/
public static String encodeBase64(String input) {
return
Base64.getEncoder().encodeToString(input.getBytes());
}

/**
* Decodes the given Base64 encoded string back to its
original form.
*
* @param input The Base64 encoded string to be
decoded.
* @return The decoded original string.
*/
public static String decodeBase64(String input) {
byte[] decodedBytes =
Base64.getDecoder().decode(input);
return new String(decodedBytes);
}
}

Explanation

Imports: The program imports the java.util.Base64 package,


which provides methods for encoding and decoding Base64.

Main Method:
A sample input string originalString is defined.
The encodeBase64 method is called to encode the
originalString into Base64 format, and the result is printed.
The decodeBase64 method is called to decode the Base64
encoded string back to its original form, and the result is
printed.

encodeBase64 Method:

Takes a string input.


Converts the string to bytes using getBytes().
Encodes the byte array into a Base64 encoded string using
Base64.getEncoder().encodeToString(byte[]).
Returns the Base64 encoded string.

decodeBase64 Method:
Takes a Base64 encoded string input.
Decodes the Base64 string into a byte array using
Base64.getDecoder().decode(String).
o Converts the byte array back to a string using new
String(byte[]).
o Returns the decoded original string.

Running the Program

To run the program, copy the code into a Java file (e.g.,
Base64Example.java) and execute it. The output will
demonstrate the Base64 encoding and decoding processes:

Encoded String: SGVsbG8sIFdvcmxkIQ==


Decoded String: Hello, World!
Q4: Write a Java program that calculates the number of days in
a given month and year using switch expressions. Use the
yield keyword to return the number of days for each
month. The program should handle leap years correctly
and allow the user to input the month and year.

Ans: Here is a Java program that calculates the number of


days in a given month and year using switch expressions and
the yield keyword. The program also correctly handles leap
years and allows user input for the month and year.

import java.util.Scanner;

public class DaysInMonthCalculator {


public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);

// User inputs for month and year


System.out.print("Enter the month (1-12): ");
int month = scanner.nextInt();
System.out.print("Enter the year: ");
int year = scanner.nextInt();

// Calculate number of days in the given month and year


int days = getDaysInMonth(month, year);

// Output the result


System.out.println("Number of days: " + days);
}

/**
* Returns the number of days in a given month and year.
*
* @param month The month (1-12).
* @param year The year.
* @return The number of days in the month.
*/
public static int getDaysInMonth(int month, int year) {
return switch (month) {
case 1, 3, 5, 7, 8, 10, 12 -> 31;
case 4, 6, 9, 11 -> 30;
case 2 -> {
if (isLeapYear(year)) {
yield 29;
} else {
yield 28;
}
}
default -> throw new
IllegalArgumentException("Invalid month: " + month);
};
}

/**
* Determines if a given year is a leap year.
*
* @param year The year.
* @return True if the year is a leap year, false otherwise.
*/
public static boolean isLeapYear(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year %
400 == 0);
}
}

Explanation

Imports and Main Method:

The program uses Scanner for user input.


The user is prompted to input the month (1-12) and year.
The getDaysInMonth method is called to calculate the
number of days in the given month and year, and the result is
printed.

getDaysInMonth Method:

This method takes the month and year as input parameters


and uses a switch expression to determine the number of days
in the month.
For months with a fixed number of days, the method directly
returns 31 or 30.
For February (month 2), the method checks if the year is a
leap year using the isLeapYear method and returns 29 if it is
a leap year or 28 otherwise.
The yield keyword is used to return the result of the switch
expression.

isLeapYear Method:

This method determines if a given year is a leap year.


A year is a leap year if it is divisible by 4 but not by 100, or if
it is divisible by 400.
The method returns true if the year is a leap year and false
otherwise.

Running the Program

To run the program, copy the code into a Java file (e.g.,
DaysInMonthCalculator.java) and execute it. The program
will prompt you to enter a month and year, and then it will
output the number of days in that month and year. For
example:

Enter the month (1-12): 2


Enter the year: 2024
Number of days: 29

Q5: Explain diamond syntax with an inner anonymous class with


an example code with output.

Ans: The diamond syntax (<>) in Java was introduced in Java


7 to simplify the use of generics when creating instances of
generic classes. It allows the compiler to infer the type
parameters, reducing redundancy and making the code
cleaner and easier to read.

However, using the diamond syntax with inner anonymous


classes is not straightforward because the compiler needs to
know the type information for the inner class. In Java 9,
enhancements were made to support the diamond operator
with inner anonymous classes.

Example Code with Diamond Syntax and Inner


Anonymous Class

Here’s an example that demonstrates the use of the diamond


syntax with an inner anonymous class:

import java.util.ArrayList;
import java.util.List;

public class DiamondSyntaxExample {


public static void main(String[] args) {
// Create a list using diamond syntax
List<String> list = new ArrayList<>() {
{
// Initializer block for the anonymous inner class
add("Apple");
add("Banana");
add("Cherry");
}
};

// Print the elements of the list


list.forEach(System.out::println);
}
}

Explanation

Diamond Syntax with Inner Anonymous Class:

new ArrayList<>() { ... } uses the diamond syntax to create


an instance of ArrayList.
An anonymous inner class extends ArrayList and initializes
the list using an initializer block.
Initializer Block:

The initializer block { ... } is used to add elements to the list


when it is instantiated.
This block runs when the anonymous inner class is created.

Printing the Elements:

The forEach method with a method reference


(System.out::println) is used to print each element of the list.

Output

When you run the above code, the output will be:

Apple
Banana
Cherry

Advantages of Diamond Syntax

Type Inference: The compiler infers the generic type,


making the code less verbose.
Readability: Reduces clutter in the code, making it more
readable.
Consistency: Helps maintain consistency across codebases
by reducing the need for explicit type declarations.

The diamond syntax simplifies the use of generics in Java by


enabling type inference, which reduces redundancy and
improves code readability. The example demonstrates how to
use the diamond syntax with an inner anonymous class,
showing how to initialize a list with elements and print them
out. This feature, particularly when combined with the
enhancements in Java 9, allows for more concise and
maintainable code.

Q6: Write Short note on:


a. Local Variable Type Inference
b. Records
c. Sealed Classes
d. Switch Expressions
Write a program that uses text blocks to define multi-line
strings and print them to the console.

Ans: Short Notes

Local Variable Type Inference

Local Variable Type Inference, introduced in Java 10, allows


the compiler to infer the type of a local variable automatically
from the context. This is done using the var keyword. It
simplifies code by reducing verbosity without sacrificing type
safety.

Example:

var list = new ArrayList<String>(); // The type of 'list' is


inferred to be ArrayList<String>

Benefits:

 Reduces boilerplate code.


 Increases readability by focusing on variable names
rather than types.
Limitations:

 Only applicable to local variables.


 Cannot be used for class fields, method parameters, or
return types.

Records

Records, introduced in Java 14 as a preview feature and


finalized in Java 16, provide a compact syntax for declaring
classes that are primarily used to store data. A record
automatically generates boilerplate code such as constructors,
getters, equals(), hashCode(), and toString() methods.

Example:

public record Person(String name, int age) {}

Benefits:

 Reduces boilerplate code.


 Improves code readability and maintainability.

Limitations:

 Records are immutable, meaning fields cannot be


changed once set.
 Designed for simple data-carrying classes.

Sealed Classes

Sealed classes, introduced in Java 15 as a preview feature and


finalized in Java 17, restrict which classes can inherit from
them. This provides more control over the class hierarchy and
ensures a well-defined set of subclasses.

Example:

public sealed class Shape permits Circle, Rectangle {}

public final class Circle extends Shape {}


public final class Rectangle extends Shape {}

Benefits:

 Enhances control over class hierarchies.


 Helps in maintaining a strict and predictable class
hierarchy.

Limitations:

 Requires explicit declaration of all permitted subclasses.


 Not suitable for hierarchies that require dynamic
extensibility.

Switch Expressions

Switch expressions, introduced in Java 12 as a preview


feature and finalized in Java 14, extend the traditional switch
statement to return a value. This feature simplifies the switch
statement, making it more concise and powerful.

Example:

int day = 3;
String dayName = switch (day) {
case 1 -> "Monday";
case 2 -> "Tuesday";
case 3 -> "Wednesday";
case 4 -> "Thursday";
case 5 -> "Friday";
case 6 -> "Saturday";
case 7 -> "Sunday";
default -> throw new IllegalArgumentException("Invalid
day: " + day);
};

Benefits:

 More concise and readable syntax.


 Eliminates the need for break statements.
 Can return values directly.

Limitations:

 Requires Java 12 or later.

Program Using Text Blocks

Text blocks, introduced in Java 13, allow you to define multi-


line strings in a more readable and concise way. They are
enclosed in triple quotes (""").

Example Program:

public class TextBlocksExample {


public static void main(String[] args) {
String json = """
{
"name": "John Doe",
"age": 30,
"city": "New York"
}
""";

String html = """


<html>
<body>
<h1>Welcome to Java!</h1>
<p>Text blocks are great for multi-line
strings.</p>
</body>
</html>
""";

String sql = """


SELECT id, name, age
FROM users
WHERE age > 25
ORDER BY name;
""";

System.out.println("JSON Example:\n" + json);


System.out.println("HTML Example:\n" + html);
System.out.println("SQL Example:\n" + sql);
}
}

Explanation

Text Blocks:

Enclosed in triple quotes (""").


Preserve the format and indentation of multi-line strings.
Make it easier to work with JSON, HTML, SQL, and other
multi-line formats.

Program Output:

The program defines and prints multi-line strings for JSON,


HTML, and SQL using text blocks.

Running the Program

To run the program, copy the code into a Java file (e.g.,
TextBlocksExample.java) and execute it. The output will
display the formatted multi-line strings as defined in the text
blocks.

UNIT: 4
Q1: What is the Java Collections Framework, and What Are Its
Core Interfaces? List and describe the core interfaces of the
Java Collections Framework. Implement a simple program
demonstrating the use of Collection, List, Set, and Map
interfaces.

Ans: Java Collections Framework

The Java Collections Framework (JCF) is a unified


architecture for representing and manipulating collections of
objects. It includes a set of interfaces and classes that provide
a standardized way to handle collections, such as lists, sets,
and maps. The framework provides algorithms to manipulate
these collections and offers various concrete implementations
to choose from.

Core Interfaces of the Java Collections Framework


Collection: The root interface in the collections hierarchy. It
defines general methods that all collections must implement,
such as adding, removing, and checking if an element is
present.
List: Extends the Collection interface. Represents an ordered
collection that can contain duplicate elements. It provides
positional access and search operations. Common
implementations are ArrayList, LinkedList, and Vector.
Set: Extends the Collection interface. Represents an
unordered collection that does not allow duplicate elements.
Common implementations are HashSet, LinkedHashSet, and
TreeSet.
Map: Represents a collection of key-value pairs. Maps do not
extend the Collection interface but are part of the Collections
Framework. Common implementations are HashMap,
LinkedHashMap, and TreeMap.
Queue: Extends the Collection interface. Represents a
collection used to hold multiple elements prior to processing.
It typically orders elements in a FIFO (first-in, first-out)
manner. Common implementations are LinkedList and
PriorityQueue.
Deque: Extends the Queue interface. Represents a double-
ended queue that allows elements to be added or removed
from both ends. Common implementations are ArrayDeque
and LinkedList.
Program Demonstrating Collection, List, Set, and Map

import java.util.*;

public class CollectionsDemo {


public static void main(String[] args) {
// Using Collection interface with ArrayList
Collection<String> collection = new ArrayList<>();
collection.add("Apple");
collection.add("Banana");
collection.add("Cherry");
System.out.println("Collection: " + collection);

// Using List interface with ArrayList


List<String> list = new ArrayList<>(collection);
list.add("Date");
System.out.println("List: " + list);

// Using Set interface with HashSet


Set<String> set = new HashSet<>(list);
set.add("Elderberry");
set.add("Banana"); // Duplicate element
System.out.println("Set: " + set);
// Using Map interface with HashMap
Map<Integer, String> map = new HashMap<>();
map.put(1, "One");
map.put(2, "Two");
map.put(3, "Three");
System.out.println("Map: " + map);

// Iterate over Collection


System.out.println("Iterating over Collection:");
for (String item : collection) {
System.out.println(item);
}

// Iterate over List


System.out.println("Iterating over List:");
for (String item : list) {
System.out.println(item);
}

// Iterate over Set


System.out.println("Iterating over Set:");
for (String item : set) {
System.out.println(item);
}

// Iterate over Map


System.out.println("Iterating over Map:");
for (Map.Entry<Integer, String> entry : map.entrySet())
{
System.out.println(entry.getKey() + " = " +
entry.getValue());
}
}
}
Explanation

Collection Interface:
Demonstrated using an ArrayList.
Added elements to the collection and printed it.
List Interface:
Demonstrated using an ArrayList.
Copied elements from the collection to the list, added an
additional element, and printed it.
Set Interface:
Demonstrated using a HashSet.
Copied elements from the list to the set, added an additional
element, and printed it.
Attempted to add a duplicate element to show that sets do not
allow duplicates.
Map Interface:
Demonstrated using a HashMap.
Added key-value pairs to the map and printed it.
Iteration:
Iterated over the elements of the collection, list, set, and map
using enhanced for-loops.
For the map, iterated over the entry set to access keys and
values.

Running the Program

To run the program, copy the code into a Java file (e.g.,
CollectionsDemo.java) and execute it. The output will
demonstrate the usage of various collection interfaces and
their common operations.
Q2: How Does the Iterator Interface Work, and How Do You
Use It? Write a Java program that uses the Iterator
interface to traverse and print the elements of an
ArrayList.

Ans: Iterator Interface in Java

The Iterator interface provides a way to traverse a collection


and access its elements one by one without exposing its
underlying representation. It is part of the Java Collections
Framework and is used to iterate over any collection that
implements the Iterable interface, such as ArrayList, HashSet,
and LinkedList.

Key Methods of the Iterator Interface

hasNext(): Returns true if the iteration has more elements.


next(): Returns the next element in the iteration.
remove(): Removes from the underlying collection the last
element returned by this iterator (optional operation).

Example Program Using Iterator Interface

Here is a simple Java program that demonstrates how to use


the Iterator interface to traverse and print the elements of an
ArrayList:
import java.util.ArrayList;
import java.util.Iterator;

public class IteratorExample {


public static void main(String[] args) {
// Create an ArrayList and add some elements
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
fruits.add("Date");

// Get an iterator for the ArrayList


Iterator<String> iterator = fruits.iterator();

// Use the iterator to traverse the ArrayList


System.out.println("Elements of the ArrayList:");
while (iterator.hasNext()) {
String fruit = iterator.next();
System.out.println(fruit);
}
}
}

Explanation

Creating an ArrayList:
An ArrayList named fruits is created and populated with
elements.
Getting an Iterator:
The iterator() method of the ArrayList class is called to obtain
an Iterator for the list.
Traversing the ArrayList:
A while loop is used to traverse the list. The hasNext()
method checks if there are more elements to iterate over.
The next() method retrieves the next element in the list.
Each element is printed to the console.
Running the Program

To run the program, copy the code into a Java file (e.g.,
IteratorExample.java) and execute it. The output will display
the elements of the ArrayList as they are traversed using the
Iterator interface.

Output

When you run the program, the output will be:


Elements of the ArrayList:
Apple
Banana
Cherry
Date

Benefits of Using Iterator

 Decouples Traversal from Collection: The iterator


pattern allows traversal of collections without exposing
their underlying implementation.
 Supports Removal during Iteration: The remove()
method allows safe removal of elements during
iteration.
 Uniform Interface: Provides a uniform way to traverse
different types of collections.
The Iterator interface is a powerful and flexible way to
traverse collections in Java. By using the iterator pattern, you
can access elements sequentially, remove elements safely,
and decouple the iteration logic from the collection's
implementation. This example demonstrates how to use the
Iterator interface with an ArrayList, but the same principles
apply to other types of collections in the Java Collections
Framework.

Q3: How Do HashMap, LinkedHashMap, and TreeMap Differ


from Each Other? Write a Java program that shows how
HashMap, LinkedHashMap, and TreeMap handle key-value
pairs and their ordering.

Ans: Differences Between HashMap, LinkedHashMap,


and TreeMap

HashMap:
o Ordering: Does not maintain any order of the
keys. The elements are stored based on the hash of
the keys.
o Performance: Offers constant-time performance
for basic operations like get and put, assuming the
hash function disperses elements properly.
o Null Values: Allows one null key and multiple
null values.
LinkedHashMap:
o Ordering: Maintains a doubly-linked list running
through all of its entries. This ensures insertion-
order iteration, meaning the order in which keys
were inserted.
o Performance: Slightly slower than HashMap due
to the overhead of maintaining the linked list, but
still offers constant-time performance for basic
operations.
o Null Values: Allows one null key and multiple
null values.
TreeMap:
o Ordering: Implements the NavigableMap
interface and uses a Red-Black tree. It sorts the
keys according to their natural ordering or by a
specified comparator.
o Performance: Provides guaranteed log(n) time
cost for the containsKey, get, put, and remove
operations.
o Null Values: Does not allow null keys but allows
multiple null values.

Example Program

The following Java program demonstrates how HashMap,


LinkedHashMap, and TreeMap handle key-value pairs and
their ordering.
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;

public class MapExample {


public static void main(String[] args) {
// HashMap
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("Banana", 3);
hashMap.put("Apple", 1);
hashMap.put("Cherry", 2);
System.out.println("HashMap:");
printMap(hashMap);

// LinkedHashMap
Map<String, Integer> linkedHashMap = new
LinkedHashMap<>();
linkedHashMap.put("Banana", 3);
linkedHashMap.put("Apple", 1);
linkedHashMap.put("Cherry", 2);
System.out.println("LinkedHashMap:");
printMap(linkedHashMap);

// TreeMap
Map<String, Integer> treeMap = new TreeMap<>();
treeMap.put("Banana", 3);
treeMap.put("Apple", 1);
treeMap.put("Cherry", 2);
System.out.println("TreeMap:");
printMap(treeMap);
}

private static void printMap(Map<String, Integer> map) {


for (Map.Entry<String, Integer> entry : map.entrySet())
{
System.out.println(entry.getKey() + " = " +
entry.getValue());
}
System.out.println();
}
}

Explanation

HashMap:
The elements are inserted without order.
Output order may vary each time the program is run.
LinkedHashMap:
Maintains insertion order.
Elements are printed in the order they were added.
TreeMap:
Maintains natural ordering of the keys (alphabetical order in
this case).
Elements are printed in sorted order by key.

Running the Program

To run the program, copy the code into a Java file (e.g.,
MapExample.java) and execute it. The output will
demonstrate the different ordering behavior of HashMap,
LinkedHashMap, and TreeMap.

Sample Output

HashMap:
Apple = 1
Banana = 3
Cherry = 2

LinkedHashMap:
Banana = 3
Apple = 1
Cherry = 2

TreeMap:
Apple = 1
Banana = 3
Cherry = 2

 HashMap: Best for scenarios where order does not


matter and you need fast access to elements.
 LinkedHashMap: Useful when you need to maintain
the order of elements as they were inserted.
 TreeMap: Ideal for scenarios where you need sorted
order of keys or need to navigate the map based on the
key order.
This example demonstrates how each type of map handles
key-value pairs and their ordering, providing a clear
understanding of their differences and use cases.

Q4: How Can You Sort a List Using Comparable and Comparator
Interfaces? Write a Java program that sorts a list of custom
objects using both Comparable and Comparator interfaces.

Ans: Sorting a List Using Comparable and Comparator


Interfaces

In Java, the Comparable and Comparator interfaces are used


to define the natural ordering of objects and to provide
custom comparison logic, respectively. Here's how you can
use these interfaces to sort a list of custom objects.

Using Comparable Interface

The Comparable interface is used to define the natural


ordering of objects. It requires implementing the compareTo
method.

Using Comparator Interface

The Comparator interface is used to define a custom order. It


requires implementing the compare method.

Example Program

Let's create a class Person with fields name and age. We will
then sort a list of Person objects using both Comparable and
Comparator interfaces.
import java.util.ArrayList;

import java.util.Collections;

import java.util.Comparator;

import java.util.List;

// Person class implementing Comparable interface

class Person implements Comparable<Person> {

private String name;

private int age;

public Person(String name, int age) {

this.name = name;

this.age = age;

public String getName() {

return name;

}
public int getAge() {

return age;

// Implementing compareTo method for natural


ordering by name

@Override

public int compareTo(Person other) {

return this.name.compareTo(other.name);

@Override

public String toString() {

return "Person{name='" + name + "', age=" + age +


"}";

public class SortingExample {

public static void main(String[] args) {


// Create a list of Person objects

List<Person> people = new ArrayList<>();

people.add(new Person("Alice", 30));

people.add(new Person("Bob", 25));

people.add(new Person("Charlie", 35));

// Sort using Comparable (natural ordering by name)

Collections.sort(people);

System.out.println("Sorted by name using


Comparable:");

for (Person person : people) {

System.out.println(person);

// Sort using Comparator (custom ordering by age)

Comparator<Person> ageComparator = new


Comparator<Person>() {

@Override

public int compare(Person p1, Person p2) {

return Integer.compare(p1.getAge(),
p2.getAge());

};

Collections.sort(people, ageComparator);

System.out.println("Sorted by age using


Comparator:");

for (Person person : people) {

System.out.println(person);

// Using lambda expression for Comparator

Collections.sort(people, (p1, p2) -> p1.getAge() -


p2.getAge());

System.out.println("Sorted by age using Comparator


with lambda:");

for (Person person : people) {

System.out.println(person);

}
}

Explanation

Person Class:
Implements the Comparable<Person> interface.
Overrides the compareTo method to provide natural ordering
by name.
Sorting using Comparable:
The Collections.sort(people) method sorts the list using the
natural ordering defined by the compareTo method.
The list is sorted by name in ascending order.
Sorting using Comparator:
A custom Comparator<Person> is defined to sort by age.
The Collections.sort(people, ageComparator) method sorts
the list using the custom comparator.
The list is sorted by age in ascending order.
Sorting using Comparator with Lambda:
A lambda expression is used to simplify the Comparator
implementation for sorting by age.
The list is again sorted by age in ascending order.

Running the Program

To run the program, copy the code into a Java file (e.g.,
SortingExample.java) and execute it. The output will
demonstrate how the list of Person objects is sorted using
both Comparable and Comparator interfaces.

Sample Output
Sorted by name using Comparable:
Person{name='Alice', age=30}
Person{name='Bob', age=25}
Person{name='Charlie', age=35}

Sorted by age using Comparator:


Person{name='Bob', age=25}
Person{name='Alice', age=30}
Person{name='Charlie', age=35}

Sorted by age using Comparator with lambda:


Person{name='Bob', age=25}
Person{name='Alice', age=30}
Person{name='Charlie', age=35}

This example illustrates the use of Comparable for natural


ordering and Comparator for custom ordering, showcasing
how to sort a list of custom objects in Java.

Q5: What Are the Differences Between Hashtable and


HashMap? Write a Java program that demonstrates the use
of Hashtable and HashMap, both of which implement the
Map interface. Show their key differences in terms of
synchronization, null values handling, and performance.
Include examples that highlight these differences.

Ans: Differences Between Hashtable and HashMap

Synchronization:

Hashtable: Synchronized, meaning it is thread-safe. Multiple


threads can access it safely without any additional
synchronization code.
HashMap: Not synchronized. If multiple threads access a
HashMap concurrently and at least one of the threads
modifies it, it must be externally synchronized.

Null Values:

Hashtable: Does not allow null keys or null values.


Attempting to insert a null key or value will result in a
NullPointerException.
HashMap: Allows one null key and multiple null values.

Performance:

Hashtable: Due to its synchronized nature, it can be slower


compared to HashMap when used in a single-threaded
environment.
HashMap: Generally faster because it is not synchronized.

Example Program

The following program demonstrates the use of Hashtable


and HashMap, highlighting their key differences.

import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class MapExample {


public static void main(String[] args) {
// HashMap example
Map<String, String> hashMap = new HashMap<>();
hashMap.put("one", "1");
hashMap.put("two", "2");
hashMap.put("three", "3");
hashMap.put(null, "nullValue"); // Allows null key
hashMap.put("four", null); // Allows null value

System.out.println("HashMap:");
printMap(hashMap);

// Hashtable example
Map<String, String> hashtable = new Hashtable<>();
hashtable.put("one", "1");
hashtable.put("two", "2");
hashtable.put("three", "3");
// Uncommenting the following lines will cause
NullPointerException
// hashtable.put(null, "nullValue"); // Does not allow null
key
// hashtable.put("four", null); // Does not allow null
value

System.out.println("Hashtable:");
printMap(hashtable);

// Synchronization difference
// HashMap is not thread-safe
Runnable hashMapTask = () -> {
for (int i = 0; i < 1000; i++) {
hashMap.put("key" + i, "value" + i);
}
};

// Hashtable is thread-safe
Runnable hashtableTask = () -> {
for (int i = 0; i < 1000; i++) {
hashtable.put("key" + i, "value" + i);
}
};

Thread hashMapThread1 = new Thread(hashMapTask);


Thread hashMapThread2 = new Thread(hashMapTask);
Thread hashtableThread1 = new Thread(hashtableTask);
Thread hashtableThread2 = new Thread(hashtableTask);

hashMapThread1.start();
hashMapThread2.start();
hashtableThread1.start();
hashtableThread2.start();

try {
hashMapThread1.join();
hashMapThread2.join();
hashtableThread1.join();
hashtableThread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("HashMap after multithreaded


access:");
printMap(hashMap);

System.out.println("Hashtable after multithreaded


access:");
printMap(hashtable);
}
private static void printMap(Map<String, String> map) {
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + " = " +
entry.getValue());
}
System.out.println();
}
}

Explanation

Null Handling:

HashMap allows null keys and values, whereas Hashtable


does not. Attempting to insert null keys or values into a
Hashtable will throw a NullPointerException.

Synchronization:

The example demonstrates the synchronization difference by


creating and running two threads that concurrently modify the
HashMap and Hashtable.
Due to lack of synchronization in HashMap, concurrent
modifications can lead to inconsistent state, while Hashtable
handles it safely.

Running the Program

To run the program, copy the code into a Java file (e.g.,
MapExample.java) and execute it. The output will
demonstrate the differences between HashMap and Hashtable
in terms of null handling and synchronization.

Sample Output

HashMap:
null = nullValue
four = null
one = 1
three = 3
two = 2

Hashtable:
one = 1
three = 3
two = 2

HashMap after multithreaded access:


null = nullValue
four = null
one = 1
three = 3
two = 2
key997 = value997
key998 = value998
key999 = value999
...

Hashtable after multithreaded access:


one = 1
three = 3
two = 2
key0 = value0
key1 = value1
key2 = value2
key3 = value3
...

HashMap: Suitable for non-thread-safe environments where


null keys and values are acceptable. Offers better
performance in single-threaded scenarios.
Hashtable: Suitable for thread-safe environments where
synchronization is required. Does not support null keys or
values and may have lower performance due to
synchronization overhead.

Q6: How Do You Implement a Custom Comparator for Sorting a


Collection? Write a Java program that sorts a list of integers
using a custom Comparator to sort in descending order.
Ans: To implement a custom comparator for sorting a
collection in Java, you need to create a class that implements
the Comparator interface and override its compare method.
The compare method should define the custom sorting logic.
Here's a Java program that sorts a list of integers in
descending order using a custom comparator:

Step-by-Step Implementation

Create a Custom Comparator:


o Implement the Comparator interface and override
the compare method to sort integers in descending
order.
Use the Custom Comparator:
o Pass the custom comparator to the Collections.sort
method to sort the list.

Example Program

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

// Custom Comparator to sort integers in descending order


class DescendingOrderComparator implements
Comparator<Integer> {
@Override
public int compare(Integer num1, Integer num2) {
// Compare num2 to num1 to achieve descending order
return num2.compareTo(num1);
}
}

public class CustomComparatorExample {


public static void main(String[] args) {
// Create a list of integers
List<Integer> numbers = new ArrayList<>();
numbers.add(5);
numbers.add(1);
numbers.add(10);
numbers.add(3);
numbers.add(7);

System.out.println("Original list:");
System.out.println(numbers);

// Sort the list using the custom comparator


Collections.sort(numbers, new
DescendingOrderComparator());

System.out.println("Sorted list in descending order:");


System.out.println(numbers);
}
}

Explanation

Custom Comparator (DescendingOrderComparator):


o This class implements the Comparator<Integer>
interface and overrides the compare method.
o The compare method is designed to sort integers
in descending order by comparing num2 to num1
instead of the usual num1 to num2.
Main Program (CustomComparatorExample):
o A list of integers is created and populated with
values.
o The original list is printed.
o The list is then sorted using Collections.sort with
an instance of the custom comparator.
o The sorted list in descending order is printed.

Running the Program

To run the program, copy the code into a Java file (e.g.,
CustomComparatorExample.java) and execute it. The output
will demonstrate the sorting of integers in descending order
using a custom comparator.

Sample Output
Original list:
[5, 1, 10, 3, 7]

Sorted list in descending order:


[10, 7, 5, 3, 1]

By creating a custom comparator and overriding the compare


method, you can define any custom sorting logic you need. In
this example, the custom comparator sorts a list of integers in
descending order, demonstrating how flexible and powerful
the Comparator interface can be for sorting collections in
Java.

Unit 5
Q1: What is Dependency Injection (DI) in Spring, and How
Does It Promote Loose Coupling? Write a Spring
application that demonstrates Dependency Injection.
Create a simple service and a controller where the service
is injected into the controller. Show how changing the
service implementation does not require modifying the
controller.

Ans: Dependency Injection (DI) in Spring

Dependency Injection (DI) is a design pattern used to


implement IoC (Inversion of Control), allowing the creation
of dependent objects outside of a class and providing those
objects to a class in different ways. DI promotes loose
coupling by reducing the dependencies between objects,
making the code more modular, testable, and maintainable.

How DI Promotes Loose Coupling

Separation of Concerns: By injecting dependencies, classes


focus on their primary responsibilities rather than managing
the lifecycle of their dependencies.
Easier Testing: Dependencies can be easily mocked or
stubbed out, simplifying unit testing.
Flexibility: Changing the implementation of a dependency
does not require modifying the dependent class.
Configuration: Dependencies can be configured externally,
often through configuration files or annotations.

Example Spring Application

We'll create a simple Spring application that demonstrates DI.


The application will have a GreetingService interface with
two implementations, and a GreetingController that uses the
service.

1. Create the Project Structure

2. Define the GreetingService Interface


package com.example.di;

public interface GreetingService {


String greet();
}

3. Create Implementations of GreetingService

package com.example.di;

import org.springframework.stereotype.Service;

@Service
public class GreetingServiceImpl implements
GreetingService {
@Override
public String greet() {
return "Hello, World!";
}
}

package com.example.di;

import org.springframework.stereotype.Service;

@Service
public class GreetingServiceSpanishImpl implements
GreetingService {
@Override
public String greet() {
return "¡Hola, Mundo!";
}
}

4. Create the GreetingController that Uses the Service

package com.example.di;

import
org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class GreetingController {
private final GreetingService greetingService;

@Autowired
public GreetingController(GreetingService
greetingService) {
this.greetingService = greetingService;
}

public void sayGreeting() {


System.out.println(greetingService.greet());
}
}

5. Create the Main Application Class

package com.example.di;

import
org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import
org.springframework.boot.autoconfigure.SpringBootApplicat
ion;
import org.springframework.context.annotation.Primary;

@SpringBootApplication
public class SpringDiExampleApplication implements
CommandLineRunner {

private final GreetingController greetingController;

@Autowired
public SpringDiExampleApplication(GreetingController
greetingController) {
this.greetingController = greetingController;
}

public static void main(String[] args) {

SpringApplication.run(SpringDiExampleApplication.class,
args);
}

@Override
public void run(String... args) throws Exception {
greetingController.sayGreeting();
}
}

6. Mark GreetingServiceImpl as Primary (Optional)

If you want to switch between implementations without


modifying the controller, you can use the @Primary
annotation to mark one of the implementations as the default.

package com.example.di;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

@Primary
@Service
public class GreetingServiceImpl implements
GreetingService {
@Override
public String greet() {
return "Hello, World!";
}
}

7. Update pom.xml to Include Spring Boot Dependencies

<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</groupId>
<artifactId>spring-di-example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

Running the Application

To run the application, execute the main method in


SpringDiExampleApplication. The application will print the
greeting message based on the primary implementation of the
GreetingService.

Sample Output

Hello, World!

Switching Implementations

To switch the implementation without modifying the


controller, you can change the @Primary annotation or adjust
your Spring configuration. For example, if you remove the
@Primary annotation from GreetingServiceImpl and add it to
GreetingServiceSpanishImpl, the output will change to:

¡Hola, Mundo!

This example demonstrates how Dependency Injection in


Spring promotes loose coupling by allowing you to switch
implementations without modifying dependent classes. The
GreetingController remains unchanged regardless of which
GreetingService implementation is used. This makes the code
more modular, testable, and maintainable.

Q2: Explain the Different Bean Scopes in Spring and Provide


Examples for Each Scope. Create a Spring application that
demonstrates different bean scopes. Implement beans with
@Singleton, @Prototype, @Request, @Session, and
@Application scopes, and show how each behaves.
Ans: Spring provides several bean scopes that define the
lifecycle and visibility of beans within the container. These
scopes are:
Singleton: This is the default scope. A single instance of the
bean is created, and the same instance is shared across the
application context.
Prototype: A new instance of the bean is created each time it
is requested.
Request: A new instance of the bean is created for each
HTTP request. This scope is only available in a web-aware
Spring ApplicationContext.
Session: A new instance of the bean is created for each HTTP
session. This scope is only available in a web-aware Spring
ApplicationContext.
Application: A single instance of the bean is created for the
lifecycle of a ServletContext. This scope is only available in a
web-aware Spring ApplicationContext.

Example Spring Application

Let's create a Spring Boot application to demonstrate


different bean scopes. We'll implement beans with
@Singleton, @Prototype, @Request, @Session, and
@Application scopes and show how each behaves.

Project Structure

1. Create Services with Different Scopes


SingletonScopedService.java

package com.example.beanscopes.services;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

@Service
@Scope("singleton")
public class SingletonScopedService extends
UniqueIdentifierGenerator {
}

PrototypeScopedService.java

package com.example.beanscopes.services;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

@Service
@Scope("prototype")
public class PrototypeScopedService extends
UniqueIdentifierGenerator {
}

RequestScopedService.java

package com.example.beanscopes.services;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import
org.springframework.web.context.annotation.RequestScope;

@Service
@RequestScope
public class RequestScopedService extends
UniqueIdentifierGenerator {
}

SessionScopedService.java

Package com.example.beanscopes.services;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import
org.springframework.web.context.annotation.SessionScope;

@Service
@SessionScope
public class SessionScopedService extends
UniqueIdentifierGenerator {
}

ApplicationScopedService.java

package com.example.beanscopes.services;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import
org.springframework.web.context.annotation.ApplicationSco
pe;
@Service
@ApplicationScope
public class ApplicationScopedService extends
UniqueIdentifierGenerator {
}

UniqueIdentifierGenerator.java

package com.example.beanscopes.services;

import java.util.UUID;

public abstract class UniqueIdentifierGenerator {


private final String id;

public UniqueIdentifierGenerator() {
this.id = UUID.randomUUID().toString();
}

public String getId() {


return id;
}
}

2. Create a Controller to Demonstrate the Bean Scopes

ScopeController.java

package com.example.beanscopes.controllers;

import com.example.beanscopes.services.*;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.web.bind.annotation.GetMapping;
import
org.springframework.web.bind.annotation.RequestMapping;
import
org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/scopes")
public class ScopeController {

@Autowired
private SingletonScopedService singletonScopedService;

@Autowired
private PrototypeScopedService prototypeScopedService;

@Autowired
private RequestScopedService requestScopedService;

@Autowired
private SessionScopedService sessionScopedService;

@Autowired
private ApplicationScopedService
applicationScopedService;

@GetMapping("/singleton")
public String getSingletonScope() {
return "Singleton Scope Bean ID: " +
singletonScopedService.getId();
}

@GetMapping("/prototype")
public String getPrototypeScope() {
return "Prototype Scope Bean ID: " +
prototypeScopedService.getId();
}
@GetMapping("/request")
public String getRequestScope() {
return "Request Scope Bean ID: " +
requestScopedService.getId();
}

@GetMapping("/session")
public String getSessionScope() {
return "Session Scope Bean ID: " +
sessionScopedService.getId();
}

@GetMapping("/application")
public String getApplicationScope() {
return "Application Scope Bean ID: " +
applicationScopedService.getId();
}
}

3. Create the Main Application Class

BeanScopesApplication.java

package com.example.beanscopes;

import org.springframework.boot.SpringApplication;
import
org.springframework.boot.autoconfigure.SpringBootApplicat
ion;

@SpringBootApplication
public class BeanScopesApplication {
public static void main(String[] args) {
SpringApplication.run(BeanScopesApplication.class,
args);
}
}

4. Update pom.xml to Include Spring Boot Dependencies

<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</groupId>
<artifactId>spring-bean-scopes</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Running the Application

To run the application, execute the main method in


BeanScopesApplication. Access the endpoints to see the
behavior of different scopes:
 http://localhost:8080/scopes/singleton
 http://localhost:8080/scopes/prototype
 http://localhost:8080/scopes/request
 http://localhost:8080/scopes/session
 http://localhost:8080/scopes/application

Sample Output

 Singleton Scope: Always returns the same ID.


 Prototype Scope: Returns a different ID each time.
 Request Scope: Returns the same ID for a single HTTP
request but different IDs for different requests.
 Session Scope: Returns the same ID for the same HTTP
session but different IDs for different sessions.
 Application Scope: Always returns the same ID for the
entire application lifecycle.

This example demonstrates the different bean scopes in


Spring and how they manage the lifecycle and visibility of
beans within the application context. By using these scopes
appropriately, you can control how and when beans are
created and used, promoting better application design and
management.

Q3: What is Aspect-Oriented Programming (AOP) in Spring,


and How Is It Used for Cross-Cutting Concerns?
Implement a Spring application that uses AOP to log
method execution times. Create an aspect that logs the
time taken by methods annotated with a custom
annotation.

Ans: Aspect-Oriented Programming (AOP) in Spring


allows you to separate cross-cutting concerns (aspects)
from the main business logic. Common cross-cutting
concerns include logging, transaction management, and
security. AOP enables you to define these concerns
separately and apply them declaratively to your
application code.
Key Concepts of AOP:
Aspect: A module that encapsulates a concern that cuts
across multiple classes.
Join Point: A point during the program's execution, such
as the execution of a method or handling an exception.
Advice: Action taken by an aspect at a particular join
point. Types include before, after, and around.
Pointcut: A predicate that matches join points. Advice is
associated with a pointcut expression and runs at any join
point matched by the pointcut.
Weaving: The process of linking aspects with other
application types or objects to create an advised object.
Can be done at compile-time, load-time, or runtime.
Steps to Implement AOP in Spring:
 Create a Spring Boot Application
 Define a Custom Annotation
 Create an Aspect Class
 Configure AOP in the Application
Step 1: Create a Spring Boot Application
Use Spring Initializr to generate a Spring Boot project
with the following dependencies:
Spring Web
Spring AOP

Step 2: Define a Custom Annotation


Create a custom annotation to mark methods for logging
execution time.

package com.example.demo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogExecutionTime {
}

Step 3: Create an Aspect Class


Implement an aspect that logs the execution time of
methods annotated with @LogExecutionTime.

package com.example.demo.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

private static final Logger logger =


LoggerFactory.getLogger(LoggingAspect.class);

@Around("@annotation(com.example.demo.annotation.L
ogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint
joinPoint) throws Throwable {
long start = System.currentTimeMillis();

Object proceed = joinPoint.proceed();

long executionTime = System.currentTimeMillis() -


start;

logger.info("{} executed in {} ms",


joinPoint.getSignature(), executionTime);
return proceed;
}
}

Step 4: Configure AOP in the Application


Spring Boot automatically detects aspects if they are
annotated with @Aspect and @Component.
Example Service and Controller
Create a service and a controller to demonstrate the usage
of the custom annotation.
Example Service

package com.example.demo.service;

import com.example.demo.annotation.LogExecutionTime;
import org.springframework.stereotype.Service;

@Service
public class SampleService {

@LogExecutionTime
public String serve() throws InterruptedException {
Thread.sleep(2000); // Simulate a delay
return "Service is done!";
}
}

Example Controller

package com.example.demo.controller;

import com.example.demo.service.SampleService;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.web.bind.annotation.GetMapping;
import
org.springframework.web.bind.annotation.RestController;

@RestController
public class SampleController {

@Autowired
private SampleService sampleService;

@GetMapping("/serve")
public String serve() throws InterruptedException {
return sampleService.serve();
}
}

Running the Application


Start your Spring Boot application and access the /serve
endpoint. You should see logs in the console indicating the
execution time of the serve method.

Custom Annotation: Created @LogExecutionTime to


annotate methods for logging execution time.
Aspect Class: Created LoggingAspect to log the execution
time of annotated methods.
Service: Applied the custom annotation to a service
method.
Controller: Exposed an endpoint to call the service
method.
With these steps, you can use AOP in Spring to manage
cross-cutting concerns like logging, ensuring your main
business logic remains clean and focused.

Q4: How Can You Build and Test RESTful Web Services
with Spring Boot? Create a Spring Boot application
with a REST controller that supports CRUD
operations. Include endpoints for GET, POST, PUT,
and DELETE requests. Write a simple test class to
verify the functionality of these endpoints.
Ans: Building and testing RESTful web services with
Spring Boot is a common task that involves creating a
REST controller to handle CRUD (Create, Read,
Update, Delete) operations and writing tests to verify
the functionality. Here’s how you can do it step-by-
step:
Step 1: Set Up the Spring Boot Application

First, create a Spring Boot project with the Spring


Web dependency. You can use Spring Initializr
(https://start.spring.io/) for this.

Step 2: Create a Simple Model Class

Create a simple Item class to represent the data.

package com.example.demo.model;

public class Item {

private Long id;

private String name;

private String description;


// Constructors

public Item() {}

public Item(Long id, String name, String


description) {

this.id = id;

this.name = name;

this.description = description;

// Getters and Setters

public Long getId() { return id; }

public void setId(Long id) { this.id = id; }

public String getName() { return name; }

public void setName(String name) { this.name =


name; }
public String getDescription() { return description; }

public void setDescription(String description)


{ this.description = description; }

Step 3: Create a Service Class


Create a service class to handle the business logic.
Here, we use a simple list to store items.

package com.example.demo.service;
import com.example.demo.model.Item;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
public class ItemService {
private List<Item> items = new ArrayList<>();
private Long nextId = 1L;
public List<Item> getAllItems() {
return items;
}
public Optional<Item> getItemById(Long id) {
return items.stream().filter(item ->
item.getId().equals(id)).findFirst();
}
public Item createItem(Item item) {
item.setId(nextId++);
items.add(item);
return item;
}
public Item updateItem(Long id, Item itemDetails) {
Optional<Item> optionalItem = getItemById(id);
if (optionalItem.isPresent()) {
Item item = optionalItem.get();
item.setName(itemDetails.getName());
item.setDescription(itemDetails.getDescription());
return item;
}
return null;
}
public void deleteItem(Long id) {
items.removeIf(item -> item.getId().equals(id));
}
}
Step 4: Create a REST Controller
Implement a REST controller to handle CRUD
operations.

package com.example.demo.controller;
import com.example.demo.model.Item;
import com.example.demo.service.ItemService;
import
org.springframework.beans.factory.annotation.Autowi
red;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/api/items")
public class ItemController {
@Autowired
private ItemService itemService;
@GetMapping
public List<Item> getAllItems() {
return itemService.getAllItems();
}
@GetMapping("/{id}")
public ResponseEntity<Item>
getItemById(@PathVariable Long id) {
Optional<Item> item =
itemService.getItemById(id);
if (item.isPresent()) {
return ResponseEntity.ok(item.get());
} else {
return ResponseEntity.notFound().build();
}
}
@PostMapping
public Item createItem(@RequestBody Item item) {
return itemService.createItem(item);
}
@PutMapping("/{id}")
public ResponseEntity<Item>
updateItem(@PathVariable Long id, @RequestBody
Item itemDetails) {
Item updatedItem = itemService.updateItem(id,
itemDetails);
if (updatedItem != null) {
return ResponseEntity.ok(updatedItem);
} else {
return ResponseEntity.notFound().build();
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void>
deleteItem(@PathVariable Long id) {
itemService.deleteItem(id);
return ResponseEntity.noContent().build();
}
}
Step 5: Write Test Cases
Write a simple test class to verify the functionality of
the endpoints.
package com.example.demo;
import com.example.demo.model.Item;
import com.example.demo.service.ItemService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import
org.springframework.beans.factory.annotation.Autowi
red;
import
org.springframework.boot.test.autoconfigure.web.servl
et.AutoConfigureMockMvc;
import
org.springframework.boot.test.context.SpringBootTest
;
import org.springframework.http.MediaType;
import
org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import static
org.springframework.test.web.servlet.request.MockMv
cRequestBuilders.*;
import static
org.springframework.test.web.servlet.result.MockMvc
ResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
public class ItemControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ItemService itemService;
@Autowired
private ObjectMapper objectMapper;
@BeforeEach
public void setup() {
itemService.getAllItems().clear();
itemService.createItem(new Item(null, "Test
Item", "This is a test item."));
}
@Test
public void shouldReturnAllItems() throws
Exception {
mockMvc.perform(get("/api/items"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name").value("Te
st Item"));
}
@Test
public void shouldReturnItemById() throws
Exception {
List<Item> items = itemService.getAllItems();
Long itemId = items.get(0).getId();
mockMvc.perform(get("/api/items/{id}", itemId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Test
Item"));
}
@Test
public void shouldCreateNewItem() throws
Exception {
Item newItem = new Item(null, "New Item",
"This is a new item.");
mockMvc.perform(post("/api/items")
.contentType(MediaType.APPLICATION_J
SON)
.content(objectMapper.writeValueAsString(
newItem)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("New
Item"));
}
@Test
public void shouldUpdateItem() throws Exception {
List<Item> items = itemService.getAllItems();
Long itemId = items.get(0).getId();
Item updatedItem = new Item(null, "Updated
Item", "This is an updated item.");
mockMvc.perform(put("/api/items/{id}", itemId)
.contentType(MediaType.APPLICATION_J
SON)
.content(objectMapper.writeValueAsString(
updatedItem)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Upda
ted Item"));
}
@Test
public void shouldDeleteItem() throws Exception {
List<Item> items = itemService.getAllItems();
Long itemId = items.get(0).getId();
mockMvc.perform(delete("/api/items/{id}",
itemId))
.andExpect(status().isNoContent());

assert(itemService.getItemById(itemId).isEmpty());
}
}

Model Class: Created an Item class to represent the


data.
Service Class: Created ItemService to handle the
business logic using an in-memory list.
REST Controller: Created ItemController to define
REST endpoints for CRUD operations.
Test Cases: Wrote test cases using MockMvc to verify
the functionality of the endpoints.
By following these simplified steps, students can build
and test RESTful web services with Spring Boot.

Q5: How Do You Configure Logging in a Spring Boot


Application, and How Can You Use It for Debugging?
Configure a Spring Boot application to use different
logging levels. Create a service class that logs messages at
various levels (DEBUG, INFO, WARN, ERROR). Show
how to configure logging properties in
application.properties.

Ans: Configuring logging in a Spring Boot application is


straightforward. Spring Boot uses the spring-boot-starter-
logging dependency, which includes Logback, a popular
logging framework. You can easily configure logging
levels and formats in your application.properties file.
Here's how to configure logging in a Spring Boot
application, create a service class that logs messages at
various levels, and configure logging properties in
application.properties.

1. Configure Logging Levels in application.properties


In your application.properties file, you can set the logging
levels for different packages or classes. Here’s an example
configuration:

# Set the root logging level


logging.level.root=INFO

# Set logging level for your application package


logging.level.com.example=DEBUG
# Set logging level for specific classes
logging.level.com.example.demo.MyService=DEBUG

# Customize logging pattern (optional)


logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} -
%msg%n

2. Create a Service Class that Logs Messages


Create a service class named MyService and log messages
at various levels.

package com.example.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class MyService {
private static final Logger logger =
LoggerFactory.getLogger(MyService.class);

public void performTask() {


logger.debug("Debug level log: Performing a task");
logger.info("Info level log: Task is in progress");
logger.warn("Warn level log: Task might have some
issues");
logger.error("Error level log: Task encountered an
error");
}
}

3. Main Application Class


Ensure your main application class is properly set up to
run your Spring Boot application.

package com.example.demo;

import
org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import
org.springframework.boot.autoconfigure.SpringBootAppli
cation;

@SpringBootApplication
public class DemoApplication implements
CommandLineRunner {

@Autowired
private MyService myService;

public static void main(String[] args) {


SpringApplication.run(DemoApplication.class, args);
}

@Override
public void run(String... args) throws Exception {
myService.performTask();
}
}

4. Running the Application


Run your Spring Boot application, and you should see logs
printed to the console according to the levels set in
application.properties.
Example Output

2024-07-29 10:00:00 - Debug level log: Performing a task


2024-07-29 10:00:00 - Info level log: Task is in progress
2024-07-29 10:00:00 - Warn level log: Task might have
some issues
2024-07-29 10:00:00 - Error level log: Task encountered
an error

Explanation
application.properties Configuration:
logging.level.root=INFO: Sets the default logging level for
the entire application to INFO.
logging.level.com.example=DEBUG: Sets the logging
level to DEBUG for all classes in the com.example
package.
logging.level.com.example.demo.MyService=DEBUG:
Specifically sets the logging level to DEBUG for the
MyService class.
logging.pattern.console: (Optional) Customizes the log
format.
Service Class Logging:
The MyService class uses the SLF4J Logger to log
messages at different levels (DEBUG, INFO, WARN,
ERROR).
Main Application Class:
The DemoApplication class runs the performTask method
of MyService on application startup, demonstrating
logging in action.
By following these steps, you can configure and use
logging in a Spring Boot application for debugging and
monitoring purposes.

Q6: What Are the Key Components of a Spring Boot Application,


and How Is the Code Structure Organized? Write a basic
Spring Boot application that includes a simple REST
controller. Demonstrate the project structure and how to
configure and run the application.
Ans: A Spring Boot application is designed to make it easy to
create stand-alone, production-grade Spring-based
applications with minimal configuration. Key components of
a Spring Boot application include:
Spring Boot Starter: Provides dependency management for
commonly used libraries.
Spring Boot Auto-Configuration: Automatically configures
Spring and third-party libraries based on the project's
dependencies.
Spring Boot Actuator: Provides production-ready features
such as monitoring and metrics.
Spring Boot CLI: Allows you to quickly prototype with
Groovy.
Application Properties/YAML: Externalizes configuration.
Spring Boot Maven/Gradle Plugin: Simplifies building and
running Spring Boot applications
Steps to Create a Spring Boot Application Using Spring
Initializr
Visit Spring Initializr:
Go to https://start.spring.io/.
Configure Your Project:
Project: Choose "Maven Project" or "Gradle Project".
Language: Choose "Java".
Spring Boot: Select the latest stable version (e.g., 2.7.5).
Project Metadata:
Group: com.example
Artifact: demo
Name: demo
Description: Demo project for Spring Boot
Package Name: com.example.demo
Packaging: Choose "Jar".
Java: Choose the version you are using (e.g., 11, 17).
Add Dependencies:
Click on "Add Dependencies" and select the following:
Spring Web: To create web applications including RESTful
services using Spring MVC.
Optionally, you can add other dependencies as needed.
Generate the Project:
Click on the "Generate" button. This will download a ZIP file
containing your Spring Boot project.
Extract the ZIP File:
Extract the downloaded ZIP file to your desired location.
Project Structure
After extracting, your project structure should look like this:
Create the Main Application Class
Open DemoApplication.java under
src/main/java/com/example/demo:

package com.example.demo;
import org.springframework.boot.SpringApplication;
import
org.springframework.boot.autoconfigure.SpringBootApplicat
ion;

@SpringBootApplication
public class DemoApplication {

public static void main(String[] args) {


SpringApplication.run(DemoApplication.class, args);
}
}

Create a REST Controller


Create a new package controller under
src/main/java/com/example/demo and add a new class
HelloController.java:
package com.example.demo.controller;

import
org.springframework.web.bind.annotation.GetMapping;
import
org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

@GetMapping("/hello")
public String hello() {
return "Hello, World!";
}
}
Configure Application Properties
Create application.properties under src/main/resources:
server.port=8080

Running the Application


To run the application, you can use your IDE or the terminal.
Using the IDE:
Open the project in your IDE (e.g., IntelliJ IDEA, Eclipse).
Run the DemoApplication class.
Using the Terminal:
Navigate to the project directory.
Use the following Maven command:
./mvnw spring-boot:run

Testing the Application


Once the application is running, open your browser and go to
http://localhost:8080/hello. You should see the message:

Hello, World!

By using Spring Initializr, you can easily set up a Spring Boot


project with the necessary dependencies. This approach
simplifies the process and ensures that the project structure is
correctly set up, avoiding confusion for beginners.

You might also like