Java & Spring Boot - Introduction
Index
Java Fundamentals
- The history of Java
- The heart of Java, the JVM
- Data types in Java
- History of OOP and differences with the procedural paradigm
- Encapsulation and information hiding
- Inheritance
- Polymorphism
- Interfaces and design patterns
- SOLID Principles
Collections and Stream API
Error Handling
Spring Boot Frameworks
- History and principles of Spring Boot
- Dependency Injection and IoC
- Annotations and layer architecture
- REST API and architectural principles
- HTTP codes and design patterns
- Spring Data JPA
- Database relations and optimizations
Java Fundamentals
The history of Java
In the early 1990s, the computing landscape was dominated by languages like C++, powerful but complex and tied to the hardware platform on which they were compiled. The emergence of consumer electronic devices (such as set-top boxes and the first PDAs) and the nascent spread of the Internet created the need for a programming language that was simpler, more robust, and, above all, platform independent.
In this context, in 1991, a team of engineers from Sun Microsystems led by James Gosling initiated the “Green Project”. The initial goal was to create a language for intelligent devices. After several iterations, Java was released in 1995, with the famous motto “Write Once, Run Anywhere”. This philosophy proved perfect for the Internet boom, where applications needed to run on a wide range of operating systems and hardware architectures.
Design Philosophy and Principles:
Java’s philosophy is based on some key principles that have made it successful:
- Simple and Familiar: Java’s syntax was intentionally kept similar to that of C++, to facilitate its adoption by experienced developers, but eliminating more complex and error-prone features, such as manual memory management and multiple inheritance.
- Object-Oriented: Java was designed from the ground up as an object-oriented language. This paradigm, which models software as a set of interacting objects, favors modularity, code reuse and maintainability.
- Platform Independent: This is the key principle. Java code is not compiled into native machine code, but into an intermediate format called “bytecode”. This bytecode can be executed on any device equipped with a Java Virtual Machine (JVM).
- Robust and Secure: Java includes mechanisms for automatic memory management (garbage collection) that prevent common errors such as memory leaks. Additionally, the JVM’s security model limits application access to system resources, making it a secure choice for network applications.
- High Performance: Although initially criticized for lower performance than natively compiled languages, the introduction of Just-In-Time (JIT) compilers in the JVM has allowed Java to achieve competitive performance.
- Multithreaded: Java has native support for multithreading, allowing you to write programs that can perform multiple tasks simultaneously, a critical feature for interactive and network applications.---
The heart of Java, the JVM
The Compilation and Execution Process:
- Writing Source Code: The developer writes the code in files with the extension
.java. - Compilation to Bytecode: The Java compiler (
javac) translates source code into bytecode, a set of platform-independent instructions, and saves it in files with the.classextension. During this phase, the compiler also performs syntactic and semantic analysis of the code to detect errors. - Execution on the JVM: The Java Virtual Machine (JVM) loads the
.classfiles, checks them for security and integrity, and finally executes them.
JVM architecture:
The JVM is an abstraction of a physical machine and is composed of three main components:
-
Class Loader Subsystem: Takes care of loading, linking and initializing classes.
- Loading: Reads
.classfiles and loads the binary data into the JVM’s memory area. - Linking: Performs bytecode verification, prepares memory for static variables, and resolves symbolic references.
- Initialization: Executes the static initialization blocks of the classes.
- Loading: Reads
-
Runtime Data Areas: These are the memory areas that the JVM uses during program execution.
- Method Area: Shared among all threads, stores class-level information such as bytecode, metadata, and constant pool.
- Heap: This is also a shared memory area where all objects and arrays created during program execution are allocated. It is managed by the garbage collector.
- Stack Area: Each thread has its own private stack. The stack stores “frames”, each of which contains the local variables and partial results of a method.
- PC Registers: Each thread has its own PC (Program Counter) register that keeps track of the JVM instruction currently executing.
- Native Method Stacks: Contains information about native methods (written in other languages such as C/C++).
-
Execution Engine: It is the heart of the JVM, responsible for executing the bytecode.
- Interpreter: Reads, interprets, and executes bytecode instructions one by one.
- Just-In-Time (JIT) Compiler: To improve performance, the JIT compiler analyzes bytecode during execution and compiles the most frequently used pieces of code (“hotspots”) into native machine code, which is then executed directly by the CPU.
- Garbage Collector (GC): Is a background process that automatically manages memory in the heap. Identifies and removes objects that are no longer referenced anywhere in the program, thus freeing memory.---
Practical Example - “Hello, World!”
Goal: Visualize the compilation and execution process with the classic “Hello, World!” program.
Source Code (HelloWorld.java):
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
Steps:
-
Compilation:
- Open a terminal or command prompt.
- Navigate to the directory where the
HelloWorld.javafile was saved. - Run command:
javac HelloWorld.java - Result: A new file will be created in the same directory:
HelloWorld.class. This file contains the bytecode.
-
Execution:
- In the same terminal, run the command:
java HelloWorld(note the absence of the.classextension). - Explanation:
- The
javacommand starts the JVM. - The JVM, through its Class Loader, searches for and loads the
HelloWorld.classfile. - The Execution Engine looks for the
public static void main(String[] args)method. - The interpreter (or JIT compiler) executes the bytecode of the
mainmethod, which instructs the system to print the string “Hello, World!” on the console.
- The
- In the same terminal, run the command:
Exercise:
- Edit the
HelloWorld.javafile to print a different message. - Recompile the file.
- Run the program again and verify that the output has changed.
- Try running the program without recompiling it after a change. What happens? Why?
- Introduce a syntax error into the
.javafile (e.g. omitting a semicolon) and try to compile it. Observe the error returned by thejavaccompiler.---
Data types in Java
Java has a static and strongly typed type system.
- Static: The type of each variable and each expression is known at compile time. This allows the compiler to catch many common errors before the program even runs.
- Strongly Typed: Operations are allowed only on compatible data types. Implicit conversions that could lead to data loss (for example, from a
doubleto aint) are not permitted without an explicit “cast” by the programmer.
In Java, data types fall into two main categories:
-
Primitive Types:
- They are the fundamental building blocks of language and are not objects.
- They directly store their value in stack memory (for local variables) or in the heap (if they are fields of an object).
- They are more efficient in terms of memory and access speed.
- There are 8:
byte,short,int,long,float,double,char,boolean. - They are immutable.
-
Reference Types:
- Any instance of a class, interface, enumeration, or array is a reference type.
- A reference variable does not contain the object itself, but the memory address (a reference) to the location on the heap where the object is stored.
- Their default value is
null. - They allow you to model complex entities and use OOP principles.
Attention!!!
In Java, arguments to methods are always passed by value BUT:
- For primitive types, the value itself is copied. Any parameter changes within the method do not affect the original variable.
- For reference types, the value of the reference (the memory address) is copied. This means that the method receives a copy of the reference that points to the same object on the heap. Therefore, if the method changes the state of the object, the change will also be visible outside the method.---
Example - Primitive Types vs. Reference
Objective: Demonstrate the difference in behavior between primitive and reference types when passed to a method.
Example Code:
class Wallet {
private int balance;
public Wallet(int balance) {
this.balance = balance;
}
public void addMoney(int amount) {
this.balance += amount;
}
public int getBalance() {
return balance;
}
}
public class TypeExample {
public static void main(String[] args) {
// Test con tipo primitivo
int myCash = 50;
System.out.println("Valore iniziale del contante: " + myCash);
tryToModify(myCash);
System.out.println("Valore finale del contante: " + myCash);
System.out.println("--------------------");
// Test con tipo riferimento
Wallet myWallet = new Wallet(200);
System.out.println("Saldo iniziale del portafoglio: " + myWallet.getBalance());
tryToModify(myWallet);
System.out.println("Saldo finale del portafoglio: " + myWallet.getBalance());
}
public static void tryToModify(int cash) {
cash = 1000; // Modifica la copia locale
}
public static void tryToModify(Wallet wallet) {
wallet.addMoney(100); // Modifica lo stato dell'oggetto originale
}
}
Expected Output:
Valore iniziale del contante: 50
Valore finale del contante: 50
--------------------
Saldo iniziale del portafoglio: 200
Saldo finale del portafoglio: 300
Analysis:
- In the first case (
myCash), thetryToModifymethod receives a copy of the50value. Thecash = 1000change occurs only on this local copy. The original variablemyCashinmainis not affected. - In the second case (
myWallet), thetryToModifymethod receives a copy of the reference to theWalletobject. Both references (the one in themainand the one in the method) point to the same object in memory. So, when the method invokeswallet.addMoney(100), it is changing the state of the only existing object, and the change is also visible from themain.
Exercises Primitive Types vs. Reference Types
Increasing difficulty: from basic syntax to a deep understanding of memory and immutable objects.
- Exercise 1 (Basic): Declaration and Assignment
Write a program that declares and initializes a
intprimitive variable calledetaand aStringreference variable callednome. Print them on screen.
- Exercise 2 (Easy): Passage by Value with Primitives
Create a
incrementa(int numero)method that takes an integer, adds 10 to it, and prints it. Inmain, declare an integer, pass it to this method, and then print it again inmain. Observe and explain why the original value has not changed.
- Exercise 3 (Medium-Easy): Changing the State of an Object
Use the
StringBuilderclass. Create aaggiungiTesto(StringBuilder builder)method that appends the string “World!” to theStringBuilderpassed as a parameter. In themain, create aStringBuilderwith “Hello”, pass it to the method, and then print it again. Observe and explain why the change is visible this time.
- Exercise 4 (Medium): Reassignment of the Reference
Create a simple class
Puntowith twoint x, yattributes. Write ariassegna(Punto p)method that creates a new point (p = new Punto(100, 100);). Inmain, create aPunto, print it, pass it to methodriassegna, and print it again. Explain why the original object inmainhas not changed.
- Exercise 5 (Medium-Difficult): Array Manipulation
Create a
modificaArray(int[] array)method that sets the first element of the array to99. Create a second methodriassegnaArray(int[] array)that creates a new array (array = new int[]{0, 0, 0};). Inmain, create a{1, 2, 3}array, pass it first tomodificaArrayand print it, then pass it toriassegnaArrayand print it again. Explain the two different results.
- Exercise 6 (Difficult): Wrapper Classes and Immutability
- Create a
modificaWrapper(Integer numero)method that tries to change the value of the wrapper (numero = 20;). Inmain, create aInteger originale = 10;variable, pass it to the method, and print it again. Explain the result in light of the fact that the Wrapper classes (Integer,Double, etc.) are immutable. Compare this behavior with that ofStringBuilder(exercise 3).---
History of OOP, real world metaphors, procedural paradigm differences
Brief History of OOP (Object-Oriented Programming):
The roots of object-oriented programming date back to the 1960s with the Simula language, which was created to create simulations of the real world. The idea was to group data and operations on it into single units called “objects”.
In the 1970s, the concept was fully developed at Xerox PARC with the creation of Smalltalk, which introduced key terms and concepts such as inheritance and polymorphism.
The popularity of OOP exploded in the 1980s with C++, which added object-oriented features to the C language, and then in the 1990s with the arrival of Java, which was designed from the beginning as a language closely related to OOP.
Real World Metaphors:
OOP is based on the idea of modeling software to reflect real-world entities.
- Class: Is a project or template. For example, the class
Automobiledescribes characteristics (attributes likecolore,marca,velocitàMassima) and behaviors (methods likeaccelera(),frena(),accendi()) that all cars have in common. - Object: Is a concrete instance of a class. If
Automobileis the project, “my red Fiat Panda” is an object, with its specific attribute values (colore = "rosso",marca = "Fiat"). - Messages: Objects interact with each other by sending messages, which basically correspond to method calls. For example, a
Guidatoreobject can send theaccelera()message to theAutomobileobject.
Differences with the Procedural Paradigm:
The procedural paradigm, dominant before OOP (e.g. C, Pascal, FORTRAN), focuses on a sequence of instructions (procedures or functions) that operate on data.
| Appearance | Procedural Programming | Object Oriented Programming |
|---|---|---|
| Main Focus | On procedures and algorithms. The program is a sequence of steps. | About objects and data. The program is an interaction between objects. |
| Organization | Divided into functions. | Divided into classes and objects. |
| Data vs. Functions | The data and the functions that operate on it are separate. Data is often global and can move freely between functions. | Data (attributes) and functions (methods) are tightly bound within objects (encapsulation). |
| Approach | Top-down: we start from the main problem and break it down into sub-problems (functions). | Bottom-up: the basic objects are modeled first and then composed to create complex systems. |
| Data Security | Less safe. Global data can be modified by any function. | Safer thanks to information hiding. The internal state of an object is protected. |
| Code Reuse | Limited to functions. | High due to inheritance and composition. |
Encapsulation, information hiding and consequences of violation
The Encapsulation:
Encapsulation is one of the four fundamental pillars of OOP. His philosophy is based on two related concepts:
- Bundling: Consists of grouping data (attributes) and the methods that operate on that data into a single unit, the class. It can be thought of as a medicinal capsule that holds all its components together.
- Information Hiding: This is the true power of encapsulation. It involves hiding the internal implementation details of a class from the outside world. Access to the internal state of the object (its attributes) is controlled and occurs only through a well-defined public interface (the public methods).
How do you implement this in Java?
- Declare attributes (instance variables) as
private. This prevents direct access from outside the classroom. - We provide public methods (often called
getterandsetter) to read and modify attributes in a controlled manner.
Real World Metaphor:
Think of a car. You, as a driver, interact with a public interface: steering wheel, pedals, gear shift. You don’t need to know (and shouldn’t be able to directly modify) the internal workings of the engine, fuel injection, or electronics. The public interface hides the internal complexity.
Consequences of Violating Encapsulation:
If a class’s attributes were public, any other part of the code could modify them directly, leading to serious problems:
- Inconsistent State: An object may end up in an invalid state. For example, if a bank account had a
public double balanceattribute, another piece of code could set it to a negative value, which thewithdraw()(withdraw) method would have prevented. - High Coupling: If many parts of the code depend on the internal details of a class, it becomes almost impossible to modify that class without “breaking” everything else. Every internal change has cascade effects on the entire system.
- Difficult Maintenance: Code becomes brittle and difficult to understand. It is no longer clear who is responsible for modifying a certain data.
- Loss of Flexibility: It is no longer possible to change the internal implementation of a class (for example, change how something is stored) without forcing all “clients” of that class to be rewritten.
Encapsulation ensures that a class is solely responsible for the consistency of its internal state, promoting a robust, flexible, and maintainable design.
Encapsulation Exercises
Increasing difficulty: from basic implementation to managing internal mutable objects for fail-safe encapsulation.
Exercise 1 (Basic): The “Anemic” Class> * Create a class
ContoCorrentewith two attributespublic String titolare;andpublic double saldo;. Inmain, create an instance and set a negative balance directly (conto.saldo = -500;). Explain why this is a problem.Exercise 2 (Easy): Getters and Setters
Modify the
ContoCorrenteclass from Exercise 1. Make the attributesprivateand addpublicgetTitolare(),setTitolare(),getSaldo(), andsetSaldo()methods.
- Exercise 3 (Medium-Easy): Validation Logic
Improve exercise 2. Inside the
setSaldo(double nuovoSaldo)method, add a check to ensure thatnuovoSaldois not negative. If it is, print an error message and do not change the balance. Do the same for thedeposita(double importo)andpreleva(double importo)methods.
- Exercise 4 (Medium): Read-Only Fields
Add a
private final String IBANattribute to theContoCorrenteclass. Remove the setter for the IBAN and initialize it only via the constructor. It shows that once the object is created, its IBAN can no longer be changed.
- Exercise 5 (Medium-Difficult): Encapsulation of Collections
Add a
private List<String> listaMovimentiattribute to theContoCorrenteclass. In thegetListaMovimenti()method, if you return the list directly (return this.listaMovimenti;), the caller can modify it externally (conto.getListaMovimenti().clear();), breaking the encapsulation. Modify the getter to return a defensive copy (new ArrayList<>(this.listaMovimenti)) or an uneditable view (Collections.unmodifiableList(this.listaMovimenti)).
- Exercise 6 (Hard): Encapsulation of Mutable Internal Objects
- Add a
private Date dataAperturaattribute (usejava.util.Datewhich is mutable). If thegetDataApertura()method returnsthis.dataApertura, external code can doconto.getDataApertura().setTime(0);and change the internal state of your object. Fix the problem by returning a copy of theDateobject in both the getter (return new Date(this.dataApertura.getTime());) and receiving a copy in the constructor/setter.---
Inheritance from the biological world, internal mechanism, trade-offs
Metaphor from the Biological World:
Inheritance in object-oriented programming (OOP) is a concept directly inspired by biological inheritance. Just as a child inherits characteristics from its parents (eye color, height), in Java a class (called subclass or child class) can inherit attributes and methods from another class (called superclass or parent class).
This allows you to create a class hierarchy, establishing an “is a” (IS-A) relationship. For example, a Cane is a Animale. A Gatto is a Animale. Both Cane and Gatto will inherit the common characteristics of Animale (such as nome, età, and the mangia() method), but may also have specific behaviors and attributes (e.g. the Cane has the abbaia() method, the Gatto has miagola()).
Internal Mechanism in Java:
Keyword extends: Inheritance is implemented using the extends keyword.
class Animale {
String nome;
public void mangia() {
System.out.println("Questo animale mangia.");
}
}
class Cane extends Animale {
public void abbaia() {
System.out.println("Woof!");
}
}
What is inherited: The subclass inherits all public and protected members of the superclass. The private members are not directly accessible, but are part of the object’s state. Constructors are not inherited, but the subclass constructor must call (implicitly or explicitly with super()) a superclass constructor.
Single Inheritance: Java supports only single inheritance, which means that a class can extend at most only one other class. This prevents the “diamond problem” ambiguity problems found in multiple inheritance languages such as C++.
Trade-offs (Advantages and Disadvantages):
Advantages:
- Code Reuse: This is the most obvious benefit. Common code is written once in the superclass and reused by all subclasses.
- Organization Logic: Create clear, understandable hierarchies that model problem domain relationships.
- Polymorphism: Inheritance is the prerequisite for polymorphism (covered in slide 18), which allows objects of different subclasses to be treated uniformly through reference to the superclass.
Disadvantages and Risks:
- Tight Coupling: The subclass is closely linked to the implementation of the superclass. A change in the superclass can have unexpected effects and “break” the functioning of the subclasses.
- Fragile Hierarchies (Fragile Base Class Problem): If a superclass is modified, all subclasses may need to be recompiled and retested, even if they do not directly use the modified part.
- Break Encapsulation: Inheritance can weaken encapsulation. If the subclass depends on implementation details of the superclass (and not just its public interface), information hiding is compromised.
- Abuse of the “IS-A” Relationship: Sometimes developers use inheritance just to reuse code, even when the “is-a” relationship makes no sense. This leads to hierarchies that are illogical and difficult to maintain.
Alternative: Often, composition is preferable to inheritance (“Composition over Inheritance”). Instead of saying “A is a B”, we say “A has a B”. This creates a weaker coupling and a more flexible design.
Inheritance Exercises
Increasing difficulty: from creating a simple hierarchy to understanding visibility modifiers and keywords
final.
- Exercise 1 (Basic): Superclass and Subclass
Create a
Personaggioclass withnomeandpuntiVitaattributes. Creates a subclassGuerrierothatextends Personaggioand adds aarmaattribute. In themain, create aGuerrieroand access both inherited and specific attributes.
Exercise 2 (Easy): Constructors and
super()> * Add a constructor to thePersonaggioclass that initializesnomeandpuntiVita. The compiler will now report an error inGuerriero. Fix this by creating a constructor inGuerrierothat takesnome,puntiVita, andarma, and that usessuper(nome, puntiVita);to call the superclass constructor.Exercise 3 (Medium-Easy): Method Overriding
Add a
descrivi()method toPersonaggiothat prints basic information. Override (@Override) thedescrivi()method inGuerrieroso that it also prints the weapon. To avoid rewriting code, theGuerrieroversion must first callsuper.descrivi();.
- Exercise 4 (Medium): Modifier
protected
Change the visibility of
puntiVitainPersonaggiofromprivatetoprotected. Create asubisciDanno(int danno)method inGuerrierothat directly modifiesthis.puntiVita. Prove it works. Now try accessingpuntiVitafrom an unrelated class (e.g. frommain) and observe the error.
- Exercise 5 (Medium-Difficult): Three-Level Hierarchy
Create a new class
Paladinothatextends Guerriero. Add afedeattribute. Override thedescrivi()method inPaladinoas well, making sure it callssuper.descrivi()to reuse the logic fromGuerriero(which in turn reuses that fromPersonaggio).
- Exercise 6 (Difficult): Classes and Methods
final
- Create a
Ladroclass. Makefinalits methodscassina(). Try creating a subclassAssassinothat overridesscassina()and observe the compiler error. Next, make the entire classLadrofinal. Try havingAssassinoextend fromLadroand see the error.---
Detailed polymorphism, Virtual Method Table, dynamic binding
What is Polymorphism?
Polymorphism (Greek for “many forms”) is the ability of an object to take on multiple forms. In OOP, it means that a reference to a superclass can point to an object of any of its subclasses. This allows you to write generic code that operates on objects of the superclass, but which at execution time will behave in a specific way depending on the actual type of the object.
Example:
Animale mioAnimale;
mioAnimale = new Cane(); // Un Cane è un Animale
mioAnimale.emettiSuono(); // Chiama il metodo specifico del Cane
mioAnimale = new Gatto(); // Un Gatto è un Animale
mioAnimale.emettiSuono(); // Chiama il metodo specifico del Gatto
In the example above, the same line of code mioAnimale.emettiSuono() produces different behavior depending on what object mioAnimale is pointing to at the time.
Internal Mechanism: Dynamic Binding and Virtual Method Table (VMT)
How does the JVM know which emettiSuono() method to call at execution time? The answer lies in dynamic binding (or late binding), made possible by an internal mechanism called Virtual Method Table (VMT) or vtable.
Static Binding vs. Dynamic:
- Static Binding (or Early Binding): The decision about which method to execute is made at compilation time. This happens for the
static,private, andfinalmethods, since the compiler knows exactly which implementation should be called. - Dynamic Binding (or Late Binding): The decision is made at execution time (runtime). This applies to all other instance methods (virtual methods). The JVM must determine the actual type of the object and call the appropriate method implementation.
Virtual Method Table (VMT):
- When the JVM loads a class, it creates a VMT for that class.
- The VMT is essentially an array of method pointers. Each object created by that class contains a hidden pointer to its class’s VMT.
- When a subclass inherits from a superclass, its VMT starts as a copy of the superclass’s VMT.
- If the subclass overrides a method, the corresponding address in its VMT is updated to point to the new implementation. If not overridden, the pointer remains that of the superclass method.
How Method Calling Works
When mioAnimale.emettiSuono() runs:
- The JVM follows the
mioAnimalereference to find the object on the heap. - From the object, follows the hidden pointer to access the VMT of the object’s real class (which could be
CaneorGatto). - The JVM looks for the method address
emettiSuono()within the VMT. This address will be that of the class-specific methodCaneorGatto. - The code at that address is executed.
This process, although it seems complex, is extremely efficient and forms the heart of polymorphism in Java, allowing you to write flexible, extensible and maintainable code.
Polymorphism Exercises
Increasing difficulty: from basic concept to using
instanceofand downcasting to access subclass-specific functionality.
- Exercise 1 (Basic): Polymorphic Array
Create an abstract class
StrumentoMusicalewith an abstract methodsuona(). Create two concrete subclassesChitarraandPianofortethat implementsuona(). Inmain, create an array of typeStrumentoMusicale[]and insert an instance ofChitarraand one ofPianoforteinside it.
- Exercise 2 (Easy): Polymorphic Calls
- Using the array from Exercise 1, write a
for-eachloop that iterates over eachStrumentoMusicaleand calls thesuona()method. Observe how the correct method is executed for each object.
- Exercise 3 (Medium-Easy): Polymorphism in Parameters
Create a static method
accordaStrumento(StrumentoMusicale strumento)that prints “Tuning the instrument…” and then callsstrumento.suona(). Inmain, pass both theChitarraobject and thePianoforteobject to this method and verify that they work.
- Exercise 4 (Medium):
instanceofand Downcasting
Add a specific method only to the
Chitarraclass calledcambiaCorde(). In thefor-eachloop of exercise 2, add a control:if (strumento instanceof Chitarra), and if true, downcast (Guitar guitar = (Guitar) instrument;) e chiama il metodoguitar.cambiaCorde().
- Exercise 5 (Medium-Difficult): Polymorphism with Interfaces
Create a
Elettricointerface with acollegaAllaCorrente()method. Make classChitarraimplement this interface, butPianofortedoes not. In the loop, add aif (strumento instanceof Elettrico)control, and if so, cast to the interface and call the specific method.
- Exercise 6 (Hard): Avoid
if-elsewithinstanceof(Simplified Visitor Pattern)
- This exercise introduces a more elegant alternative to downcasting. Add a
accetta(Visitor v)method toStrumentoMusicale. Create aVisitorinterface withvisit(Chitarra c)andvisit(Pianoforte p)methods. InChitarra, the implementation ofaccettawill bev.visit(this);. Now, instead ofif-else, you can create aManutentoreVisitorclass that implementsVisitorand contains logic specific to each tool. The loop inmainwill simply becomestrumento.accetta(mioManutentore);.---
Interfaces, architectural advantages and design patterns
Interfaces as Contracts:
An interface in Java is a completely abstract reference type. It is a collection of method signatures (and static constants) without any implementation. When a implements class (implements) an interface, it enters into a contract.
The Contract establishes that:
- The class promises to provide a concrete implementation for all methods defined in the interface.
- The Java compiler acts as a notary, verifying that the class complies with the contract. If even one method of the interface is not implemented, the code does not compile.
This mechanism separates the definition of behavior (what an object must do) from its implementation (how it does it).
// IL CONTRATTO
interface Volante {
void decolla();
void atterra();
}
// Chi firma il contratto, DEVE rispettarlo
class Aereo implements Volante {
@Override
public void decolla() { /* implementazione per l'aereo */ }
@Override
public void atterra() { /* implementazione per l'aereo */ }
}
class Uccello implements Volante {
@Override
public void decolla() { /* implementazione per l'uccello */ }
@Override
public void atterra() { /* implementazione per l'uccello */ }
}
Architectural Advantages
Loose Coupling: Client code may depend on the interface (the abstraction) rather than concrete implementations. This means we can change or add new interface implementations without changing the client code.
public class TorreDiControllo {
public void autorizzaDecollo(Volante v) {
v.decolla(); // Non mi importa se è un Aereo o un Uccello, so solo che può decollare
}
}
Polymorphism: Interfaces allow you to obtain polymorphism even between classes that do not have a direct inheritance relationship. A Aereo and a Uccello have no common superclass (beyond Object), but can both be treated as Volante.
Simulating Multiple Inheritance: Because a class can implement multiple interfaces, you can “inherit” behavior from multiple sources, overcoming Java’s single inheritance limitation.
Testability: It is much easier to create “mocks” or “stubs” (mock implementations for testing) of an interface than of a complex class. This greatly facilitates unit testing.
Role in Design Patterns:
Interfaces are central to many design patterns, which are proven solutions to recurring problems in software design.
- Strategy Pattern: Allows you to define a family of algorithms, encapsulate them in separate classes that implement the same interface, and make them interchangeable. The client depends only on the interface and can change “strategy” at runtime.
- Factory Pattern: A “factory” method returns an object of type interface. The client receiving the object does not know (and does not care to know) which concrete class has been instantiated, only that it respects the interface contract.
- Observer Pattern: The “Observer” and the “Subject” communicate via interfaces. The Subject does not know the concrete classes of Observers, but only that they implement the
Observerinterface and therefore have aupdate()method to call. - Dependency Injection: (See Spring Boot section) Dependencies are injected as interfaces, allowing concrete implementations to be easily replaced (e.g. from a real database to an in-memory database for testing).
Interface Exercises
Increasing difficulty: from simple implementation to the use of interfaces for decoupling (Strategy Pattern).
- Exercise 1 (Basic): Implementing an Interface
Create a
Volanteinterface with avola()method. Create two classes,UccelloandAereo, that implement this interface. Each implementation ofvola()will print a different message.
- Exercise 2 (Easy): Polymorphism with Interfaces
In the
main, create aList<Volante>. Add one instance ofUccelloand one ofAereo. Write a loop that iterates over the list and calls thevola()method for each element.
- Exercise 3 (Medium-Easy): Multiple Interfaces
Create a second
Nuotanteinterface with anuota()method. Create a classAnatrathatimplements Volante, Nuotante. Inmain, demonstrate that aAnatraobject can be inserted into either aList<Volante>or aList<Nuotante>.
- Exercise 4 (Medium): Methods
default
- Add a
defaultmethod to theVolanteinterface calledatterra(), which prints “Standard landing.”. Prove that theUccelloandAereoclasses “inherit” this method without needing modification. Then, overrideatterra()in theAereoclass to provide a more specific implementation.
- Exercise 5 (Medium-Difficult): Interfaces for Decoupling
Create a
Loggerinterface with alog(String messaggio)method. Create two implementations:ConsoleLogger(print to console) andFileLogger(write to dummy file). Create aCalcolatriceclass that takes aLoggerin the constructor. Every time theCalcolatriceperforms an operation, it uses the logger to record the event. Inmain, show how you can pass to the sameCalcolatricefirst aConsoleLoggerand then aFileLoggerwithout changing a line of code from theCalcolatrice.
- Exercise 6 (Difficult): Strategy Pattern
- Create a
StrategiaDiPrezzointerface with acalcolaPrezzo(double prezzoBase)method. Create two implementations:PrezzoStandard(returnsprezzoBase) andPrezzoScontato(returnsprezzoBase * 0.8). Creates aCarrelloclass that has aStrategiaDiPrezzoattribute. Add asetStrategia(StrategiaDiPrezzo s)method and agetPrezzoFinale(double totale)method that uses the current strategy to calculate the price. Demonstrate how you can change the cart pricing strategy at runtime.
SOLID principles with Uncle Bob philosophy, precise definitions
The SOLID principles are an acronym that groups together five fundamental principles of object-oriented design, promoted by Robert C. Martin (known as “Uncle Bob”). The purpose of these principles is to create more understandable, flexible, and maintainable software.
Uncle Bob’s Philosophy: The central idea is that bad software design (“rigid”, “brittle”, “non-reusable” code) slows down development more than anything else. Writing “clean code” and following principles like SOLID is not an academic exercise, but an essential professional practice to manage complexity and allow software to evolve over time.---
S - Single Responsibility Principle
- Precise Definition: “A class should have one, and only one, reason to change.”
- Philosophy: This “reason to change” is tied to an “actor” (a user or stakeholder) who needs a change. If a class handles both business logic and database persistence, there are two actors (e.g. the business department and database administrators) that may require changes. This couples two responsibilities that should be separate. The class should only do one thing.---
O - Open/Closed Principle (Open Principle/Chiuso)
- Precise Definition: “Software entities (classes, modules, functions, etc.) should be open to extension, but closed to modification.”
- Philosophy: We should be able to add new functionality to a system without having to modify existing, already tested code. This is typically achieved through the use of abstractions (interfaces or abstract classes). You extend the behavior by creating new classes that implement these abstractions, rather than by adding
if/elseorswitchto existing code.---
L - Liskov Substitution Principle
- Precise Definition: “Subclasses must be substitutable for their superclasses without affecting the correctness of the program.”
- Philosophy: If you have a function that accepts an object of type
Superclasse, you must be able to pass it an object of anySottoclassewithout the function “breaking” or behaving unexpectedly. This means that the subclass must not restrict the behavior of the superclass (e.g. overriding a method to throw an exception where the superclass did not).---
I - Interface Segregation Principle
- Precise Definition: “No client should be forced to depend on methods it does not use.”
- Philosophy: It is better to have many small, specific interfaces (“roles”) than one large generic interface. If a class implements an interface with methods it doesn’t need, it’s a sign of bad design. Dividing the “fat” interface into smaller, more targeted interfaces allows classes to implement only the contracts that pertain to them.---
D - Dependency Inversion Principle
- Precise Definition:
- “High-level modules should not depend on low-level modules. Both should depend on abstractions.”
- “Abstractions should not depend on details. Details should depend on abstractions.”
- Philosophy: This principle is the basis of a decoupled architecture. Code that contains important business logic (high level) should not directly depend on implementation details such as a specific database or network service (low level). Instead, both should “talk” through an interface (an abstraction). This allows you to swap low-level details (e.g. switching from a MySQL database to PostgreSQL) without touching the high-level logic. It is the principle that makes Dependency Injection possible.---
Collections and Stream API
ArrayList
ArrayList: The Dynamic Array
ArrayList in Java is an implementation of the List interface that uses an internal array to store elements. Its main feature is that of being a “resizable array” or “dynamic”: its size can grow and decrease as needed.
Internal Algorithms:
- Helping Array: Internally,
ArrayListcontains an array of objects (e.g.Object[] elementData). - Access (get/set): Accessing an item via index (
get(int index)) is an extremely fast operation. Since this is an array, the memory address of theielement can be calculated directly, resulting in a time complexity of O(1) (constant time). - Addition (add):
- Best Case: If there is still space in the internal array, the element is simply added to the end, and a
sizecounter is incremented. This operation is O(1). - Worst Case (Resizing): If the internal array is full and you try to add a new element,
ArrayListperforms a resize operation:- Creates a new larger array (typically 50% larger than the previous one).
- Copy all elements from the old array to the new array.
- Adds the new element to the end of the new array.
- The internal reference is updated to point to the new array, and the old one is discarded (and subsequently collected by the garbage collector).
This copy operation has a complexity of O(n), where
nis the number of current elements.
- Best Case: If there is still space in the internal array, the element is simply added to the end, and a
Performance and Practical Considerations:
- Advantages:
- Quick Access: Excellent for index-based reads and writes (O(1)).
- Dynamic: Automatically manages the size.
- Disadvantages:
- Inserts/Rimozioni in the Center: Inserting or removing an element at the beginning or in the center of the list is expensive (O(n)). This is because all subsequent elements must be moved to make room or to close the hole left.
- Memory Overhead: There may be wasted space in the internal array if the capacity is much larger than the actual number of elements.
- Optimization: If you know in advance the number of elements that will be inserted, it is a good practice to create the
ArrayListwith a specific initial capacity (new ArrayList<>(capacita)) to avoid intermediate scaling and improve performance.
ArrayList Exercises
Increasing difficulty: from basic use to understanding performance implications and the use of iterators.
- Exercise 1 (Basic): CRUD Operations
Create a
ArrayListofString. Do the following:Add 3 names (
add).Read and print the middle name (
get).Update the first name with a new value (
set).Remove last name (
remove).Print the final list.
Exercise 2 (Easy): Iteration> * Create a
ArrayListofIntegerand fill it with 5 numbers. Write three different loops to print all elements: a classicforloop with index, afor-eachloop, and an iteration usingforEachwith a lambda expression (lista.forEach(n -> System.out.println(n));).Exercise 3 (Medium-Easy): Research and Content
Create a list of strings. Ask the user to enter a name. Use the
contains()method to check if the name is present. If it is, useindexOf()to find its location and print it.
- Exercise 4 (Medium): Insertion Performance
Creates a
ArrayListand aLinkedListofInteger. Write a loop that adds 100,000 numbers to the start of each list (list.add(0, i);). Measure the time taken by both lists usingSystem.nanoTime()before and after the loop. Explains the drastic difference in performance.
- Exercise 5 (Medium-Difficult): Removal during Iteration (Common Error)
Create a
ArrayListofIntegerfrom 1 to 10. Try to remove all even numbers using afor-eachloop. Look at theConcurrentModificationException. Explain why this happens.
- Exercise 6 (Difficult): Correct Removal with
Iterator
- Solve Exercise 5. Use a
Iteratorto scroll through the list. When you find an even number, use theiterator.remove()method to safely remove it. Alternatively, it shows how to solve the problem using theremoveIfmethod with a lambda expression, which is the modern and more concise approach.---
HashMap
Hash Table Theory:
A HashMap is an implementation of the Map interface that is based on the hash table data structure. It allows you to store key-value pairs and offers exceptional performance (on average O(1), constant time) for insert (put), fetch (get), and remove (remove) operations.
The mechanism is based on three concepts:
- Hashing: When you insert a pair (
K,V),HashMaptakes theKkey and calculates an integer value called hash code via the key’shashCode()method. This hash code is then processed by an internal hashing function to determine an index into an internal array (calledtableorbuckets). - Bucket Array: The internal array is a series of “buckets”. The calculated index determines in which bucket the key-value pair will be stored.
- Retrieval: When you ask for the value associated with a key (
get(K)),HashMaprecalculates the key’s hash code, finds the correct bucket index, and searches for the key in that bucket.
Collision Management:
The problem arises when two different keys produce the same bucket index. This event is called collision. This is inevitable, given that the number of possible keys is virtually infinite, while the number of buckets is finite.
Java HashMap handles collisions primarily with a technique called Separate Chaining:
- Bucket Structure: Each bucket of the array does not contain a single element, but the head of a linked list (LinkedList) of nodes.
- In case of Collision: If two keys map to the same bucket, the new key-value pair is simply added as a new node in the linked list of that bucket.
- Search in a Bucket: When searching for a key in a bucket that contains multiple nodes, the
HashMapmust iterate through the linked list and use theequals()method to find the exact key.
Optimization from Java 8 (Tree-ification):
To mitigate performance degradation in the case of many collisions in the same bucket (which would bring the search to O(n)), an important optimization was introduced from Java 8 onwards:
- If the number of nodes in a bucket exceeds a certain threshold (TREEIFY_THRESHOLD, default 8), the linked list of that bucket is converted to a Red-Black Tree.
- This improves the worst-case search complexity from O(n) to O(log n), where n is the number of items in the bucket.
Importance of hashCode() and equals():
The correct functioning of a HashMap depends critically on the contract between the hashCode() and equals() methods of the key:
- If two objects are equal according to
equals(), they must return the samehashCode(). - If two objects have the same
hashCode(), they are not necessarily the same according toequals()(this is a collision). If this contract is not respected, theHashMapwill behave unpredictably.
HashMap Exercises
Increasing difficulty: from basic use to creating a custom class as a key, understanding the
hashCode/equals. contract
- Exercise 1 (Basic): CRUD Operations
Create a
HashMap<String, String>to store state capitals (<Stato, Capitale>).Enter 3 state-capital pairs (
put).Retrieves and prints the capital of a state (
get).Check if a state is present (
containsKey).Remove a pair (
remove).Print the final map.
- Exercise 2 (Easy): Iteration on the Map
Create a
HashMap<String, Integer>that maps product names to prices. Write three ways to iterate and print content:Iterating over the
keySet()(set of keys).Iterating over the
values()(collection of values).Iterating over the
entrySet()(set of key-value pairs), which is the most efficient way.
- Exercise 3 (Medium-Easy): Calculation of Frequencies
Write a program that takes a text (a
String) and uses aHashMap<Character, Integer>to count the frequency of each character in the text. For each character, if it is not already in the map, insert it with a value of 1; otherwise, it increases its value.
- Exercise 4 (Medium): Managing Missing Values
Improve exercise 3 using the
getOrDefaultmethod. Instead of checking withcontainsKey, you can writemappa.put(carattere, mappa.getOrDefault(carattere, 0) + 1);. Explain how this single line simplifies the code.
- Exercise 5 (Medium-Difficult): Custom Key (Without
hashCode/equals)
Create a
Studenteclass withidandnome. Create aHashMap<Studente, Integer>to map students to grades. Creates two separateStudenteobjects but with the same data (new Studente(1, "Mario")andnew Studente(1, "Mario")). Use the first item to place a vote on the map. Try to recover the grade using the second item. Observe that it fails (getreturnsnull). Explain why.
- Exercise 6 (Difficult): The Contract
hashCode/equals
- Solve Exercise 5. Override the
hashCode()andequals()methods in theStudenteclass. The logic ofequalsshould compare theid. The logic ofhashCodeshould be based onid. Re-test Exercise 5 and see that it now works correctly. Explain the importance of this contract for the operation of hash tables.
Stream API and functional paradigm, lazy evaluation, parallelization
The Stream API and the Functional Paradigm in Java:
Introduced in Java 8, the Stream API is not a data structure, but a way to process sequences of elements from a source (such as a Collection, an array, or a file) in a declarative and functional style.
- Imperative paradigm vs. Declarative:
- Imperative: Describes the “how” to do something, step by step (e.g. a
forloop with counters and conditions).* Declarative (with Streams): You describe the “what” you want to achieve, leaving the details on how to do it to the API. The code is more concise, readable, and less error-prone.
- Imperative: Describes the “how” to do something, step by step (e.g. a
Main Features of a Stream:
- Operations Pipeline: A stream is a sequence of operations that are applied to elements. These operations are divided into two categories:
- Intermediate Operations: They transform one stream into another stream. Examples:
filter()(select elements),map()(transform elements),sorted(),distinct(). - Terminal Operations: They produce a result or a “side-effect”. Invoking a terminal operation starts processing the entire pipeline. Examples:
forEach(),collect()(collects items into a collection),reduce(),count().
- Intermediate Operations: They transform one stream into another stream. Examples:
Lazy Evaluation:
This is one of the most important stream optimizations.
- Definition: Intermediate operations in a stream pipeline are lazy. They are not executed until a terminal operation is invoked.
- How it works: Instead of processing all elements at each step of the pipeline, the stream processes elements vertically, one at a time (or in small batches). One item passes through the entire pipeline before the next item is processed.
- Advantages:
- Efficiency: Only the strictly necessary calculations are performed. If an operation like
findFirst()finds a result, the stream can stop without processing the rest of the elements (short-circuiting). - Management of Infinite Streams: Lazy evaluation allows you to create and operate on potentially infinite data streams (e.g.
Stream.iterate(0, n -> n + 2)which generates all even numbers), as long as there is an intermediate “short-circuiting” operation (such aslimit()).
- Efficiency: Only the strictly necessary calculations are performed. If an operation like
Example of Lazy Evaluation:
List<String> names = Arrays.asList("Anna", "Bruno", "Carlo", "Beatrice");
names.stream()
.filter(name -> {
System.out.println("Filtrando: " + name);
return name.startsWith("B");
})
.map(name -> {
System.out.println("Mappando: " + name);
return name.toUpperCase();
})
.findFirst(); // Operazione terminale di short-circuiting
The output will not be:
Filtrando: Anna
Filtrando: Bruno
Filtrando: Carlo
and then
Mappando: ...
But it will be:
Filtrando: Anna
Filtrando: Bruno
Mappando: Bruno
The stream stops as soon as it has found the first element that satisfies the condition, without even considering “Carlo”.
Parallelization (Parallel Streams):
The Stream API makes it extremely easy to parallelize operations to take advantage of multi-core processors.
- How to use it: Simply call the
.parallel()method on a stream (or.parallelStream()on a collection) to turn it into a parallel stream. - Internal Mechanism: A parallel stream uses the Fork/Join framework (introduced in Java 7). The data source is divided into sub-problems (forks), which are processed in parallel by a pool of threads. The partial results are then recombined (join).
- When to use them: Parallelization is not always faster. It introduces overhead for splitting, thread management, and recombination of results. It is effective on:
- Large amounts of data.
- Computationally intensive operations on each element.
- Data sources that can be split efficiently (e.g.
ArrayListis great,LinkedListless so). - stateless and associative operations.
Caution: Using parallel streams with stateful operations (that modify a shared state) can lead to non-deterministic results and concurrency issues if not handled properly.
Stream API Exercises
Increasing difficulty: from simple operations to complex pipelines with
collectorsand parallelization.
- Exercise 1 (Basic):
filterandforEach
Given a
List<String>of names, use a stream to filter out only the names starting with the letter “A” and print them to the screen.
- Exercise 2 (Easy):
mapandcollect
Given a
List<String>of words, use a stream to create a new list (List<Integer>) containing the length of each word. The final operation should becollect(Collectors.toList()).
- Exercise 3 (Medium-Easy): Chaining (Pipeline)
- Given a
List<Prodotto>(with attributesnome,categoria,prezzo), write a single stream pipeline that:
Filter products from the “Electronics” category.
Filter those with a price above 100.
Extracts (maps) product names only.
Collects the names in a new list.
Exercise 4 (Medium): Different Terminal Operations (
findFirst,anyMatch)
- Given a
List<Integer>, use a stream to:
Check if there is at least one number greater than 50 (
anyMatch).Find the first even number and print it (
filterandfindFirst). Handle the case where it doesn’t exist withOptional.Exercise 5 (Medium-Difficult):
reduceandmapToInt
- Given a
List<Integer>, compute the sum of all numbers in two different ways using streams:
Using the
reducemethod.> 2. UsingmapToIntto convert theStream<Integer>to aIntStreamand then calling thesum()method, which is more efficient.Exercise 6 (Difficult): Grouping with
Collectors.groupingBy
- Given the
List<Prodotto>from exercise 3, use a stream and acollectorto create aMap<String, List<Prodotto>>where the keys are the categories and the values are the lists of products belonging to that category. This is one of the most powerful operations of collectors.---
Error Management
Exceptions with historical evolution, stack unwinding, performance impact
Historical Evolution of Error Management:
Before structured exception handling, error handling in software was cumbersome and error-prone:
- Return Codes: Functions returned special values (e.g. -1, 0 or
null) to indicate an error. The calling code was responsible for checking these values with a series ofif-else. This mixed business logic with error handling, making the code difficult to read and maintain. - Global Error Variables: Some systems set a global error variable. This approach was also fragile, especially in multithreaded environments.
Languages such as C++ and later Java introduced structured exception handling with try-catch blocks. This allowed us to:
- Separate Business Logic from Error Management: The “happy” code (happy path) is placed in the
tryblock, while the management of anomalous situations is delegated to thecatchblocks. - Propagate Errors: An error can be “thrown” (
throw) by a method and caught by a caller further up the call chain, without each intermediate method having to explicitly handle the error.
Stack Unwinding:
When an exception is thrown (throw), the normal flow of execution is interrupted and the Java runtime begins a process called stack unwinding.
- Creation of the Exception Object: An object of type
Exception(or a subclass thereof) is created that contains information about the error, including the stack trace (the sequence of method calls that led to the error). - Search for a Handler: The runtime searches, within the current method, for a
catchblock that can handle that type of exception. - Unrolling the Stack:
- If an appropriate
catchis not found in the current method, the method ends abruptly. - Control returns to the calling method (the previous frame in the call stack).
- The runtime repeats the search for a
catchblock in the caller. - This process continues, “unrolling” the call stack one frame at a time.
- If an appropriate
- Management or Termination:
- If a compatible
catchblock is found, the stack stops unrolling and the code inside thecatchblock is executed. - If the exception makes it all the way to method
mainand is not caught, the current thread terminates and the stack trace of the exception is usually printed to the console.
- If a compatible
The finally Block: During stack unwinding, if a method has a finally block, the code inside it is always executed, regardless of whether the exception was caught in that method or not. This is crucial for releasing resources (e.g. closing files or network connections).
Performance Impact:
Exception handling in Java impacts performance, but it’s important to understand where and why.* Zero (or almost) cost in the try Block: Entering and exiting a try block when no exceptions are thrown has a negligible performance cost. Modern JIT compilers are very efficient at optimizing this path.
- High Cost when an Exception is Thrown: The act of creating and throwing an exception is expensive. The main reasons are:
- Creation of Exception Object: Requires memory allocation on the heap.
- Stack Trace Capture: This is the most expensive operation. The JVM must traverse the entire call stack to build the stack trace.
- Stack Unwinding: The process of finding a handler has a computational cost.
Best Practices:
Exceptions should be used for exceptional and abnormal conditions, not for normal program flow control. Using exceptions to, for example, handle the end of a loop or to return an “expected” result is an anti-pattern that unnecessarily degrades performance.
Exceptions Exercise
Increasing difficulty: from basic management to creating custom exceptions and using
try-with-resources.
- Exercise 1 (Basic):
try-catch
Write a program that divides two numbers. Put the division operation in a
tryblock and catch theArithmeticExceptionthat occurs when trying to divide by zero, printing an appropriate error message.
- Exercise 2 (Easy): Block
finally
Modify Exercise 1. Add a
finallyblock that prints “Operation completed.”. Run the program with both valid division and division by zero, and observe that thefinallyblock executes in both cases.
- Exercise 3 (Medium-Easy): Multiple Catches
Create an array of integers. Write a program that asks the user for an index and a divisor. Attempts to split the element at the index specified for the divisor. Manage
ArrayIndexOutOfBoundsExceptionandArithmeticExceptionseparately with two distinctcatchblocks.
- Exercise 4 (Medium): Checked vs. Unchecked Exceptions
Write a
leggiFile(String nomeFile)method that could throw aIOException(which is a checked exception). Don’t handle it inside the method, but addthrows IOExceptionto the method signature. Call this method frommainand observe that the compiler forces you to handle the exception with atry-catchor propagate it further.
- Exercise 5 (Medium-Difficult): Custom Exceptions
Create your own custom exception class (checked)
SaldoInsufficienteExceptionthat extendsException. Modify theContoCorrenteclass (from a previous exercise) so that thepreleva(double importo)method throws this exception if the balance is insufficient, instead of just printing a message.
- Exercise 6 (Difficult):
try-with-resources> * Write a program that opens a file for reading usingBufferedReaderandFileReader. Instead of using afinallyblock to ensure the reader is closed (reader.close()), use thetry-with-resourcesconstruct. It shows that it is more concise and secure, as closing the resource is automatic.---
SpringBoot
Spring Boot history, pre-Boot issues, architectural principles
Pre-Spring Boot Context: The Complexity of the Spring Framework
Spring Framework, born in the early 2000s, revolutionized Java development by introducing the principle of Dependency Injection and an approach based on POJO (Plain Old Java Object), significantly simplifying the development of enterprise applications compared to the complex EJBs (Enterprise JavaBeans) of the time.
However, over time, configuring a Spring application, especially for the web, had become a complex and laborious task:
- XML Configuration Hell: Early versions of Spring relied heavily on XML configuration files to define “beans” (Spring-managed objects) and their dependencies. For large applications, these files became huge, difficult to read and maintain.
- Dependency Management: It was necessary to manually declare in the build file (e.g.
pom.xmlfor Maven) every single required dependency, including transitive libraries, and ensure that the versions were compatible with each other, an often frustrating and error-prone task. - Boilerplate Configuration: To create a simple web application, you needed to manually configure the
DispatcherServlet,ViewResolver,DataSource,EntityManagerFactory,TransactionManager, and more. Much of this setup code was repetitive and identical across almost every project. - Complex Deployment: Web applications had to be packaged as Web Application Archive (WAR) files and deployed on an external application server (such as Tomcat or JBoss), which had to be installed and configured separately.
The Birth of Spring Boot:
Spring Boot, released in 2014, is not a new framework, but an evolution of Spring designed to solve these problems and radically simplify the process of building and launching Spring applications.
Architectural Principles and Philosophy:
Spring Boot’s philosophy is based on a few key principles:
-
“Convention over Configuration”: Spring Boot takes an “opinionated” approach. It parses the application’s classpath and, based on the libraries it finds, automatically configures most of the common features.
- Example: If it finds the
spring-boot-starter-webdependency, Spring Boot assumes that you are building a web application and automatically configures Tomcat, theDispatcherServlet, and other essential components without the developer having to write a single line of configuration.
- Example: If it finds the
-
Auto-Configuration: This is the main mechanism behind “Convention over Configuration”. Spring Boot provides a wide range of conditional
@Configurationclasses that fire only if certain conditions are met (e.g. a certain class is present in the classpath, a certain property is defined, etc.).3. Starter Dependencies: To solve the problem of managing dependencies, Spring Boot introduces “starter poms”. These are build descriptors (e.g.spring-boot-starter-data-jpa,spring-boot-starter-test) that group together all the common dependencies needed for a specific feature. By including a single starter, you get all the necessary libraries (including transitive ones) in tested and mutually compatible versions. -
Embedded Web Server: Spring Boot allows you to include a web server (such as Tomcat, Jetty, or Undertow) directly within your application. This means that the application can be run as a simple executable JAR file (
java -jar mia-app.jar), eliminating the need for an external application server. This greatly simplifies development, testing, and deployment, and is a cornerstone of microservice architectures. -
Out-of-the-Box Application Health and Metrics: By including the
spring-boot-starter-actuatorstarter, you immediately get HTTP endpoints to monitor health (/health), metrics (/metrics), configuration (/configprops), and more—essential features for production-ready applications.
In summary, Spring Boot does not replace Spring, but it drastically simplifies its use, allowing developers to focus on business logic rather than infrastructure configuration.
Spring Boot Fundamentals & Starters Exercises
Increasing difficulty: from creating a basic application to understanding auto-configuration.
- Exercise 1 (Basic): “Hello, Spring Boot!”
Use start.spring.io to generate a new Maven project with only the “Spring Web” dependency. Import it into your IDE. Create a
@RestControllerwith a single method mapped to/hellothat returns the string “Hello, Spring Boot!”. Launch the application and check the result in your browser.
- Exercise 2 (Easy): Using Properties
In the
application.propertiesfile, change the embedded server port to8090using theserver.portproperty. Restart the application and verify that it now responds on the new port.
- Exercise 3 (Medium-Easy): Add Another Starter
Add the
spring-boot-starter-actuatorstarter to yourpom.xml. Restart the application. Access endpoints that Actuator automatically exposes, such as/actuator/healthand/actuator/info. This demonstrates how Spring Boot adds functionality based on the starters present.
- Exercise 4 (Medium): Creating a Custom Configuration Class
Create a
AppInfoclass with two string fields,nameandversion. Annotate it with@ConfigurationProperties(prefix = "app")and@Configuration. Enable it with@EnableConfigurationPropertieson the main class. Define theapp.nameandapp.versionproperties inapplication.properties.
Exercise 5 (Medium-Difficult): Using the Custom Configuration> * Inject the
AppInfobean created in the previous exercise into your controller. Modify the/helloendpoint to return a welcome message that includes the application name and version taken from the configuration.Exercise 6 (Difficult): Analyzing Self-Configuration
- Start the application with the
--debugflag (or setdebug=truetoapplication.properties). Analyze the console output to see the “Auto-configuration report”. Find the “Positive matches” and “Negative matches” sections and try to understand why Spring Boot decided to configure (or not configure) certain beans, such asDataSourceAutoConfiguration.
Dependency Injection with IoC history, injection types, container mechanism
Story: Inversion of Control (IoC)
Inversion of Control (IoC) is a high-level software design principle.
- Traditional Control: In a traditional program, the developer’s code is in control. It is our code that creates the objects, connects them to each other and decides when to call the methods. Our code “controls” the flow.
public class MioServizio {
private AltroServizio dipendenza;
public MioServizio() {
// Io controllo la creazione della mia dipendenza
this.dipendenza = new AltroServizioImpl();
}
}
```
* **Inversion of Control:** With IoC, this control is **inverted**. It is no longer our code that creates and manages dependencies, but an external entity, typically a **framework** or a **container**. Our code just defines _what_ it needs, and the container takes care of providing it to it at the right time. The philosophy is "Don't call us, we'll call you".
### **Dependency Injection (DI): The IoC Implementation**
**Dependency Injection (DI)** is the most common design **pattern** to implement the IoC principle. It is the process by which the container "injects" dependencies (i.e., objects that a class needs) into another class.
### **The Spring IoC Container Mechanism (ApplicationContext):**
The heart of Spring is its IoC container, mainly represented by the `ApplicationContext` interface. His work takes place in two main phases:
1. **Configuration Phase:**
* **Component Scanning:** The container scans the classpath for classes annotated with stereotypes such as `@Component`, `@Service`, `@Repository`, `@RestController`.
* **Creation of "Bean Definitions":** For each class found, create a sort of "recipe" (a `BeanDefinition`) that describes how to create the object (the "bean"): what its class is, what its scope is (e.g. `singleton`, `prototype`), and what its dependencies are.
2. **Instantiation and Injection Phase:**
* **Bean Creation:** When a bean is requested (or at startup, in the case of singletons), the container uses the `BeanDefinition` to create an instance of the object.
* **Dependency Resolution and Injection:** The container examines the dependencies of the newly created bean. It searches its registry for a bean that matches the requested type and "injects" it into the object.
* **Lifecycle Management:** The container manages the entire lifecycle of the bean, from creation to destruction.
### **Spring Injection Types:**
Spring offers three main ways to inject dependencies.
1. **Constructor Injection:**
* **How it works:** Dependencies are declared as parameters of the class constructor.
```java
@Service
public class MioServizio {
private final AltroServizio dipendenza;
// Spring userà questo costruttore per l'iniezione
@Autowired
public MioServizio(AltroServizio dipendenza) {
this.dipendenza = dipendenza;
}
}
```
* **Advantages:**
* **Immutability:** Dependencies can be declared `final`, ensuring that they cannot be changed after the object is created.
* **Explicit Dependencies:** Mandatory dependencies are clear from the constructor signature. An object cannot be created in an invalid state (without its dependencies).
* **Recommended by Spring:** It is the preferred injection method and recommended by the Spring team.
2. **Setter Injection:**
* **How it works:** Dependencies are provided through public setter methods.
```java
@Service
public class MioServizio {
private AltroServizio dipendenza;
@Autowired
public void setDipendenza(AltroServizio dipendenza) {
this.dipendenza = dipendenza;
}
}
```
* **Usage:** Useful for **optional dependencies** or to allow reconfiguration of the bean at runtime.
3. **Field Injection:**
* **How it works:** The `@Autowired` annotation is placed directly on the field.
```java
@Service
public class MioServizio {
@Autowired
private AltroServizio dipendenza;
}
```
* **Advantages:** Very concise.
* **Disadvantages (and why it is not recommended):**
* **Hide Dependencies:** It is not clear what dependencies are needed by looking at constructors or public methods.
* **Testing Difficulty:** Makes unit testing difficult, as you need to use reflection to set up mock dependencies in the test.
* **Encourages Bad Practices:** Can lead to classes with too many dependencies.
* **Immutability Violation:** You cannot declare fields as `final`.
> ### **Dependency Injection & IoC Exercises**
>
> _Increasing difficulty: from basic injection to managing bean ambiguity and scope._
>
> 1. **Exercise 1 (Basic): Field Injection**
> * Create a `MessageService` class annotated with `@Service` that has a `getMessage()` method that returns "Service Message".
> Create a `MessageController` annotated with `@RestController` and inject the `MessageService` using `@Autowired` directly into the field. Create an endpoint that calls `getMessage()` and returns the result.
>
> 2. **Exercise 2 (Easy): Constructor Injection**
> * Rewrite exercise 1 using **constructor injection**, which is the recommended practice. Make the field `MessageService` in the controller `final`. Explain the advantages of this approach (immutability, explicit dependencies).
>
> 3. **Exercise 3 (Medium-Easy): Interface Injection**
> * Create a `GreetingService` interface. Create a `GreetingServiceImpl` class that implements the interface. In the controller, inject the `GreetingService` interface, not the concrete implementation. This demonstrates the principle of "programming to an interface".
>
> 4. **Exercise 4 (Medium): Managing Ambiguity with `@Qualifier`**
> * Create a second implementation of the `GreetingService` interface called `FormalGreetingServiceImpl` (e.g. one returns "Hello!" and the other "Good morning."). Annotate both with `@Service`. Launch the application and observe the `NoUniqueBeanDefinitionException` error. Fix the problem by annotating the implementations with `@Qualifier("informal")` and `@Qualifier("formal")` and using `@Qualifier` at the controller injection point to specify which bean to use.
>
> 5. **Exercise 5 (Medium-Difficult): Bean Scopes**
> * Create a `HitCounterService` with an internal counter (`private int count = 0;`) and a `incrementAndGet()` method. Annotate it with `@Service` and `@Scope("session")`. Inject this service into a controller and create an endpoint that calls `incrementAndGet()` and returns the result. Open two different browsers (or an incognito window) and call the endpoint from both to demonstrate that each session has its own separate counter.
>
> 6. **Exercise 6 (Difficult): Java-based configuration with `@Bean`**
> * Remove the `@Service` annotation from one of your service classes. Create a separate configuration class annotated with `@Configuration`. Inside this class, you define a method annotated with `@Bean` that manually creates and returns an instance of your service. It proves that dependency injection still works.## **Annotations and metaprogramming, layer architecture, component scanning**
### **Annotations and Metaprogramming:**
**Annotations** (such as `@Override`, `@Autowired`, `@Service`) are a form of **metadata**, which is data that describes other data (in this case, the code itself). In Java, annotations don't do anything by themselves; they require an **annotation processor** to read them and act on them.
**Metaprogramming** is the idea of writing code that reads, analyzes, or manipulates other code. Spring makes extensive use of metaprogramming:
* During startup, the Spring container (**processor**) does not run your code, but **parses** it.
* Reads annotations (`@Component`, `@Autowired`, etc.) to understand how your objects (beans) should be created, configured and connected to each other.
* Based on this metadata, dynamically generate the "glue" that holds the application together.
This approach allows you to have clean business code that is decoupled from the configuration logic, which is instead expressed declaratively through annotations.
### **Layered Architecture:**
A layered architecture is a common and robust way to organize an application's code. Each layer has a specific responsibility and communicates only with adjacent layers (typically, only with the layer below it). Spring Boot fits this model perfectly.
A typical 3-layer architecture in a Spring Boot web application is:
1. **Presentation Layer:**
* **Responsibilities:** Handle incoming HTTP requests and outgoing responses. Translate data from/a formats such as JSON.
* **Spring Components:** Classes annotated with `@RestController` or `@Controller`. These are the entry points of the application.
* **Contains no business logic.** Calls the service layer to perform operations.
2. **Service Layer (or Business Layer - Service Layer):**
* **Responsibilities:** Contain the core business logic of the application. Coordinates operations, applies business rules and manages transactions.
* **Spring Components:** Classes annotated with `@Service`.
* It is the heart of the application. It receives calls from the Presentation Layer and uses the Data Access Layer to interact with the data.
3. **Data Access Layer (or Persistence Layer - Data Access Layer):**
* **Responsibilities:** Interact with the database. It takes care of all CRUD (Create, Read, Update, Delete) operations.
* **Spring Components:** Interfaces that extend `JpaRepository` (or other Spring Data interfaces) and are annotated with `@Repository`.
* This layer abstracts data access logic, hiding the details of how data is stored and retrieved.
**Flow of a Request:**
`Richiesta HTTP` → `RestController` (Presentation) → `Service` (Business) → `Repository` (Data Access) → `Database`
### **Component Scanning:**
How does Spring find these classes (`@RestController`, `@Service`, `@Repository`) to handle? The answer is **Component Scanning**.* **Starting Point:** The `@SpringBootApplication` annotation on your project's main class is actually a composite annotation that includes `@ComponentScan`.
* **Mechanism:** By default, `@ComponentScan` tells Spring to scan the **main class package and all its sub-packages** for classes annotated with a component "stereotype".
* **Stereotypes:** The main annotations that `@ComponentScan` looks for are:
* `@Component` (the generic annotation)
* `@Service` (specialization of `@Component` for the service layer)
* `@Repository` (specialization for data access layer, also enables translation of database-specific exceptions)
* `@Controller` / `@RestController` (specialization for the presentation layer)
* `@Configuration` (for configuration classes)
Thanks to component scanning, you just need to annotate a class and make sure it is in the correct package (or a sub-package) for Spring to detect it, create an instance of it and manage it like a bean, making it available for dependency injection.
> ### **Layered Architecture & Component Scanning Exercises**
>
> _Increasing difficulty: from the creation of a single layer to their complete interconnection and management of DTOs._
>
> 1. **Exercise 1 (Basic): The Presentation Layer**
> * Create an application to manage a list of "Tasks". Create a `TaskController` (`@RestController`) with a `GET /tasks` method that returns a hard-coded list of strings (e.g. "Studying Spring", "Shopping").
>
> 2. **Exercise 2 (Easy): Adding the Service Layer**
> * Create a `TaskService` (`@Service`) class. Move the logic that creates the task list from the controller to the service. The controller must now inject the `TaskService` and call one of its methods (e.g.
> `findAll()`) to obtain the data.
>
> 3. **Exercise 3 (Medium-Easy): Adding the Repository Layer (Mock)**
> * Create a `TaskRepository` (`@Repository`) class. Simulate a database using an internal `Map<Long, String>`. The repository must have methods like `findAll()`, `findById(Long id)`, `save(String task)`. The `TaskService` must now use the `TaskRepository` to manage data.
>
> 4. **Exercise 4 (Medium): Define a Model and a DTO**
> * Create a `Task` model class (a POJO) with `id`, `description`, `completed`. The repository will now work with `Task` objects. Also create a `TaskDTO` class that exposes only the fields you want to show to the client (e.g. omitting the id).
>
> 5. **Exercise 5 (Medium-Difficult): Connect all Layers with DTO**
> * Implement the full flow:
> 1. The `TaskController` receives/restituisce `TaskDTO`.
> 2. The `TaskService` receives/restituisce `TaskDTO`, but internally converts them to `Task` entities to interact with the repository.
> 3. The `TaskRepository` works exclusively with `Task` entities.
> * Implements a `POST /tasks` method that takes a `TaskDTO` to create a new task.
>
> 6. **Exercise 6 (Difficult): Customizing the Component Scan**> * Move all repository classes into a completely separate package, outside the main package hierarchy (e.g. `com.example.data` instead of `com.example.demo.repository`). Launch the app and observe that it fails because it cannot find the repository bean. Fix the problem by using `@ComponentScan(basePackages = {"com.example.demo", "com.example.data"})` on your main class.
## **REST with protocol history, architectural principles, Richardson model**
**Brief History of API Protocols:**
* **1990s - RPC (Remote Procedure Call):** The idea was to call a function on a remote server as if it were a local function. Protocols like CORBA and DCOM were complex and tightly coupled.
* **Late 90s / Early 2000s - SOAP (Simple Object Access Protocol):** Became the standard for "Web Services". Based on XML, it defined a rigid messaging format with a schema (WSDL) that described the available operations. SOAP was robust and standardized, but also very verbose and complex.
* **2000 - The Birth of REST:** Roy Fielding, in his doctoral thesis, defined the **REST (Representational State Transfer)** architectural style. It is not a protocol, but a set of **constraints** and **principles** for designing distributed systems, based on the way the Web itself works (HTTP). REST emerged as a simpler, more flexible, and scalable alternative to SOAP.
### **Architectural Principles of REST:**
REST is based on six fundamental constraints:
1. **Client-Server:** Clear separation between the client (which takes care of the user interface) and the server (which takes care of the business logic and data persistence). They can evolve independently.
2. **Stateless:** Each request from the client to the server must contain **all the information necessary** to be understood and executed. The server does not store any client session state between requests. This improves scalability and reliability.
3. **Cacheable:** Server responses must indicate whether they can be cached by the client or intermediaries. This improves performance and reduces the load on the server.
4. **Uniform Interface:** This is the key constraint that sets REST apart. It is based on four sub-principles:
* **Resource Identification:** Each "thing" (a user, a product, an order) is a **resource** identified by a unique URI (e.g. `/users/123`).
* **Resource Manipulation via Representations:** The client interacts with resources through their **representations** (typically JSON or XML).
* **Self-Descriptive Messages:** Each message (request/risposta) contains enough information to be understood (e.g. use of HTTP verbs, header `Content-Type`).
* **HATEOAS (Hypermedia as the Engine of Application State):** The server response should contain links (hypermedia) that guide the client towards possible next actions.5. **Layered System:** The architecture can be composed of multiple layers (e.g. proxy, gateway, load balancer) without the client realizing it. Each layer communicates only with the adjacent one.
6. **Code-On-Demand (Optional):** The server can, optionally, send executable code (e.g. JavaScript script) to the client to extend its functionality.
### **Richardson Maturity Model:**
Leonard Richardson proposed a model to evaluate how "mature" and adherent to REST principles an API is. It is divided into four levels:
* **Level 0: The Swamp of POX (Plain Old XML):**
* Use HTTP only as a transport mechanism.
* Typically has a single URI and uses a single HTTP verb (almost always `POST`).
* Operations and parameters are contained in the body of the request (similar to RPC/SOAP).
* **Example:** `POST /service` with `<getUsers/>` or `<createOrder>...</createOrder>` body.
* **Level 1: Resources:**
* Introduces the concept of **resources** with multiple, specific URIs.
* Each resource has its own endpoint.
* Continue to use a single HTTP verb (usually `POST`) for all operations.
* **Example:** `POST /users`, `POST /orders/123`.
* **Level 2: HTTP Verbs:**
* Start using **HTTP verbs** in a semantically correct way for different resource operations.
* Use **HTTP status codes** to indicate the outcome of the request (e.g. `200 OK`, `201 Created`, `404 Not Found`).
* **Example:**
* `GET /users` (read the list of users)
* `POST /users` (create a new user)
* `PUT /users/123` (update user 123)
* `DELETE /users/123` (delete user 123)
* **Level 3: Hypermedia Controls (HATEOAS):**
* The highest level and the "glory of REST".
* Server responses include not only data, but also hypermedia links that tell the client what other actions it can take or related resources it can explore.
* This makes the API "explorable". The client only needs to know the starting URI and can then "navigate" the API by following the links provided by the server.
* **Example Response for an order:**
```json
{
"orderId": 123,
"total": 50.00,
"status": "SHIPPED",
"_links": {
"self": { "href": "/orders/123" },
"customer": { "href": "/customers/45" },
"tracking": { "href": "/orders/123/tracking" }
}
}
```
An API that reaches Level 3 is considered truly "RESTful".
> ### **REST APIs & Richardson Maturity Model exercises**
>
> _Increasing difficulty: from creating an RPC-style API to full use of HTTP verbs._
>
> 1. **Exercise 1 (Basic - RMM Level 0/1): RPC style API**
> * Create a single `POST /api/bookservice` endpoint. The body of the JSON request determines the action to be performed, e.g.: `{ "action":
> "getAllBooks" }` o `{ "action": "findBookById", "id": 1 }`. L'endpoint usa degli `if/else` to decide which logic to execute.
>
> 2. **Exercise 2 (Easy - RMM Level 1): Resources**
> * Rewrite exercise 1 introducing the resources. Create separate endpoints for each resource type, for example `POST /api/books` to get all books and `POST /api/books/1` to get a specific book. You're still just using the verb `POST`.
>
> 3. **Exercise 3 (Medium-Easy - RMM Level 2): HTTP Verbs (GET)**
> * Modify exercise 2 to use the correct HTTP verb. Change the endpoints to `GET /api/books` and `GET /api/books/{id}`. Use the `@PathVariable` annotation to retrieve the id from the URL.
>
> 4. **Exercise 4 (Medium - RMM Level 2): HTTP Verbs (POST)**
> * Implement creation of a new book. Create a `POST /api/books` method that accepts the new book data in the request body. Use the `@RequestBody` annotation to map the JSON to a Java Object (DTO).
>
> 5. **Exercise 5 (Medium-Difficult - RMM Level 2): HTTP Verbs (PUT & DELETE)**
> * Complete CRUD operations. Implement:
> * `PUT /api/books/{id}` to completely update an existing book.
> * `DELETE /api/books/{id}` to delete a book.
>
> 6. **Exercise 6 (Difficult - RMM Level 3): HATEOAS**
> * Add starter `spring-boot-starter-hateoas`. Change the book's DTO to extend `RepresentationModel`. Modify the `GET /api/books/{id}` method to add a "self" link to the response, which points to the endpoint itself. If a book has an author, add a link to the author's resource.
## **HTTP codes with precise semantics, patterns API design**
### **The Semantics of HTTP Status Codes:**
HTTP status codes are not just numbers, but have precise **semantics** that communicate the outcome of a request. Using them correctly is critical to good REST API design. They are divided into five categories:
* **1xx (Informational):** The request has been received, the process continues. (Rarely used in REST APIs).
* **2xx (Success):** The request has been received, understood and accepted successfully.
* `200 OK`: Generic success for a request. Typically used for successful `GET` and `PUT`/`PATCH`.
* `201 Created`: The request was fulfilled and resulted in the creation of a **new resource**. The response should include a `Location` header with the URI of the new resource. Used for `POST`.
* `202 Accepted`: The request has been accepted for processing, but processing is not yet complete (useful for asynchronous operations).
* `204 No Content`: The server successfully processed the request, but there is no content to return in the response body. Typically used for `DELETE` successes.
* **3xx (Redirection):** Further action is required by the client to complete the request.
* `301 Moved Permanently`: The requested resource has been permanently moved to a new URI.
* `304 Not Modified`: Used in response to a conditional `GET` request (e.g. with header `If-None-Match`). Indicates that the resource has not changed and the client can use its cached version.
* **4xx (Client Error):** The request contains incorrect syntax or cannot be satisfied. The error is from the **client**.
* `400 Bad Request`: Generic client error (e.g. malformed JSON, invalid request parameters). The response should contain details about the error.
* `401 Unauthorized`: The client must authenticate to get the requested response. Authentication is missing or has failed.
* `403 Forbidden`: The client is authenticated, but **does not have permission** to access the resource.
* `404 Not Found`: The server did not find a resource matching the requested URI.
* `405 Method Not Allowed`: The HTTP method used in the request is not supported for that resource (e.g. a `PUT` on a URI that only accepts `GET`).
* `409 Conflict`: The request could not be completed due to a conflict with the current state of the resource (e.g. attempting to create a resource that already exists).
* **5xx (Server Error):** The server failed to service a seemingly valid request. The error is with the **server**.
* `500 Internal Server Error`: Generic server error. Indicates that an unexpected condition occurred that prevented the server from satisfying the request (e.g. an unhandled exception).
* `503 Service Unavailable`: The server is currently unable to handle the request due to temporary or maintenance overload.
### **Common API Design Patterns:**
1. **Pagination:** For resources that return lists of items (e.g. `GET /products`), it is impractical to return thousands of records at once.
* **Pattern:** Use query parameters like `page` and `size` (or `limit` and `offset`).
* **Example:** `GET /products?page=2&size=20` (return 20 products from the second page).
* **Best Practice:** Include metadata about pagination (total number of elements, total number of pages) and, in a mature API (HATEOAS), links to next, previous, first, and last pages in the response.
2. **Sort:** Allow the client to specify how to sort the results of a list.
* **Pattern:** Use a query parameter like `sort`.
* **Example:** `GET /products?sort=price,asc` (sort by price, ascending) or `GET /products?sort=-price` (sort by price, descending).
3. **Filtering:** Allow the client to filter results based on certain criteria.
* **Pattern:** Use query parameters that match the resource attributes.
* **Example:** `GET /products?category=electronics&inStock=true`.4. **Field Selection:** Allow the client to request only the fields it needs, to reduce response size and network traffic.
* **Pattern:** Use a query parameter like `fields`.
* **Example:** `GET /users/123?fields=id,name,email`.
5. **Versioning:** Handle API changes that may "break" existing clients.
* **Common pattern:** Include the version number in the URI.
* **Example:** `/api/v1/products`, `/api/v2/products`.
* **Alternatives:** Use a custom HTTP header (e.g. `Accept-Version: v1`) or content negotiation via the `Accept` header.
6. **Error Format:** Define a standard and consistent JSON response format for all 4xx and 5xx errors, to facilitate error handling by the client.
* **Example body of a 400 response:**
```json
{
"timestamp": "2025-08-27T10:00:00Z",
"status": 400,
"error": "Bad Request",
"message": "Validation failed",
"errors": [
{ "field": "email", "message": "must be a well-formed email address" }
],
"path": "/users"
}
```
> ### **HTTP Codes & API Design Patterns exercises**
>
> _Increasing difficulty: from the use of success codes to centralized error management and pagination._
>
> 1. **Exercise 1 (Basic): Correct Success Codes**
> * Review the CRUD API from the previous exercise. Make sure that:
> * `POST` returns `201 Created` and the header `Location` with the URL of the new resource. Use `ResponseEntity` to construct this response.
> * `DELETE` returns `204 No Content`.
> * `GET` and `PUT` return `200 OK`.
>
> 2. **Exercise 2 (Easy): Managing the 404 Not Found**
> * Change the `GET /api/books/{id}` method. If the book with the specified ID is not found, the service must throw an exception.
> The controller must return a `ResponseEntity` with status `404 Not Found`.
>
> 3. **Exercise 3 (Medium-Easy): Centralized Exception Management**
> * Create a custom exception `ResourceNotFoundException`. Modify the service to throw this exception. Create a class annotated with `@RestControllerAdvice` that globally handles the `ResourceNotFoundException` and returns a `404` response with a standard error JSON body.
>
> 4. **Exercise 4 (Medium): Validation and 400 Bad Request**
> * Add `spring-boot-starter-validation` dependency. Annotate your book DTO fields with validation constraints (e.g.
> `@NotBlank`, `@Size(min=3)` on the title). Add the `@Valid` annotation to the `@RequestBody` in the controller. Extend your `@RestControllerAdvice` to handle `MethodArgumentNotValidException` and return a `400` error with invalid field details.
>
> 5. **Exercise 5 (Medium-Difficult): Pagination**
> * Edit your `BookRepository` to extend `PagingAndSortingRepository`. Change the `GET /api/books` method to accept `page` and `size` (`@RequestParam`) parameters. Use a `Pageable` object to retrieve a "page" of results from the repository. Return a `Page<Book>` object from the controller (Spring will serialize it to JSON with pagination metadata).
>
> 6. **Exercise 6 (Difficult): API Versioning**
> * Implement URL versioning. Create a new `BookControllerV2` that handles requests on `/api/v2/books`.
> This controller should return a `BookDTOV2` with a slightly different structure (e.g. one more or less field). The original controller (`BookController`) must be mapped to `/api/v1/books`.
> Both versions must be able to coexist.
## **Spring Data JPA with impedance mismatch, ORM evolution, persistence context**
### **The Problem: Object-Relational Impedance Mismatch**
This term describes the conceptual and technical difficulties that arise when trying to map the world of **object-oriented programming (OOP)** with the world of **relational databases (RDBMS)**. The two paradigms are fundamentally different:
| OOP Paradigm | Relational Paradigm |
| :--- | :--- |
| **Structure:** Graphs of interconnected objects. | **Structure:** Flat data tables. || **Relationships:** Direct references between objects in memory. | **Relations:** Foreign keys that connect rows of different tables. |
| **Identity:** Memory identity (two references can point to the same object). | **Identity:** Primary key. |
| **Inheritance:** Native and fundamental concept. | **Inheritance:** Does not exist. It must be simulated with various patterns (e.g. one table per hierarchy, one table per class). |
| **Granularity:** Objects can be very complex and nested. | **Granularity:** Table rows are typically "flat". |
This "mismatch" forces developers to write a lot of "glue" (boilerplate) code to translate data from one model to another.
### **Evolution of ORM (Object-Relational Mapping):**
ORM tools were created to resolve impedance mismatch, acting as a bridge between the two worlds.
1. **JDBC (Java Database Connectivity):** The basis of everything. It is a low-level API that allows Java to execute SQL queries against a database. With pure JDBC, the developer must manually write SQL queries, map the results (`ResultSet`) to Java objects on a field-by-field basis, and manage connections and transactions. It is very verbose and prone to errors.
2. **Early ORMs (e.g. Hibernate, TopLink):** Frameworks like Hibernate (born 2001) automated this process. The developer defines the mapping between Java classes (called **entities**) and database tables (often via XML or, later, annotations). The ORM is responsible for generating SQL queries, translating the results into objects and managing saving operations.
3. **JPA (Java Persistence API, now Jakarta Persistence):** To standardize the various ORMs, the JPA specification was born. JPA is not an implementation, but a set of **standard interfaces and annotations** (e.g. `@Entity`, `@Id`, `@OneToMany`). Hibernate is the most widespread implementation (or "provider") of JPA today. Using JPA means writing code that doesn't depend on a specific ORM, but on the standard.
4. **Spring Data JPA:** Represents an additional layer of abstraction on top of JPA. The goal of Spring Data JPA is to **minimize boilerplate code** for the data access layer. Instead of writing the implementation of a Data Access Object (DAO) class, the developer only needs to define an interface that extends `JpaRepository`.
### **The Role of Spring Data JPA:**
```java
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Spring Data JPA genererà automaticamente l'implementazione
// di metodi come save(), findById(), findAll(), delete().
// Basta definire la firma del metodo, e Spring Data JPA
// scriverà la query per noi!
Optional<User> findByEmail(String email);
}
Spring Data JPA parses the method name (findByEmail), figures out that it needs to create a query that looks for a User via its email attribute, and generates the implementation at runtime. This almost completely eliminates the need to write code for the data access layer.
The Persistence Context:
Persistence Context is a fundamental concept of JPA (and therefore of Hibernate and Spring Data JPA).
- Definition: The Persistence Context can be thought of as a top-level cache or “workspace” that sits between the application and the database. It is managed by JPA’s EntityManager.
- Operation:
- When loading an entity from the database (e.g. with
findById()), JPA not only returns the object, but also inserts it into the Persistence Context. - If you request the same entity again within the same transaction, JPA will return it directly from the Persistence Context (from the cache) without querying the database again.
- Dirty Checking: Any changes made to a “managed” entity, i.e. present in the Persistence Context, are automatically tracked.
- Transaction Commit: At the end of the transaction (typically at the end of a
@Servicemethod annotated with@Transactional), JPA compares the current state of the managed entities with their original state. If it detects changes (dirty checking), it automatically generates and sendsUPDATEqueries to the database.
- When loading an entity from the database (e.g. with
The Persistence Context is the mechanism that allows JPA to efficiently manage the state of entities, optimize queries and ensure data consistency within a transaction.
Database relations with ER theory, fetch strategies, N+1 problem
ER Theory and Mapping in JPA:
The Entity-Relationship (ER) model is a way to design relational databases. Its counterparts in JPA are relation annotations:
-
One-to-One: An instance of an entity is associated with one and only one instance of another entity.
- Example: A
Userhas aUserProfile. - JPA Annotations:
@OneToOne.
- Example: A
-
One-to-Many: An instance of one entity (the “one side”) can be associated with many instances of another entity (the “many side”).
- Example: One
Authorhas manyBook. - JPA Annotations:
@OneToManyon theAuthorside and@ManyToOneon theBookside. The two-way relationship is the most common.
- Example: One
-
Many-to-Many: Many instances of one entity can be associated with many instances of another.
- Example: A
Studentmay be enrolled in manyCourse, and aCoursemay have manyStudent. - Relational Implementation: Requires an intermediate junction table (e.g.
student_course) that contains the foreign keys of both tables. - JPA Annotation:
@ManyToMany.
- Example: A
Fetch Strategies:
When JPA loads an entity from the database, what should it do with its related entities? The Fetch Strategy defines this behavior.
-
FetchType.EAGER(“Greedy” Loading):* Behavior: When loading the main entity (e.g. aAuthor), JPA immediately also loads all its related entities (all itsBook).- How it works: Typically, JPA runs a single SQL query using a
JOIN(or multiple queries, depending on your configuration) to load everything at once. - Default JPA:
@OneToOneand@ManyToOneareEAGERby default.
- Pros: Pairing is always available. No
LazyInitializationExceptionerrors. - Cons: Can be very inefficient. You could load large object graphs from memory when only the main entity is needed, wasting memory and time.
- How it works: Typically, JPA runs a single SQL query using a
-
FetchType.LAZY(“Lazy” Loading):- Behavior: When loading the main entity (e.g. a
Author), its related entities (theBook) are not loaded. In their place, JPA inserts a proxy, a “placeholder” object. - How it works: Data from the related collection will be loaded from the database only when that collection is accessed for the first time (e.g.
author.getBooks().size()). - Default JPA:
@OneToManyand@ManyToManyareLAZYby default.
- Pros: Much more efficient. Only the data actually needed is loaded.
- Cons: If you try to access a
LAZYcollection outside of an active transaction (i.e. when the Persistence Context is closed), you will get aLazyInitializationException.
- Behavior: When loading the main entity (e.g. a
Best Practice: Always prefer LAZY for collections (@OneToMany, @ManyToMany) and often also for single relationships, and explicitly load the necessary data when needed (using JOIN FETCH in JPQL queries or EntityGraph).
The Problem N+1 Select:
This is one of the most common performance problems when using an ORM.
- Scenario: Suppose we have a
LAZY@OneToManyrelationship betweenAuthorandBook. We want to recover all the authors and print the title of their first book. - Naive code:
List<Author> authors = authorRepository.findAll(); // 1 query per trovare tutti gli autori
for (Author author : authors) {
System.out.println(author.getBooks().get(0).getTitle()); // N query, una per ogni autore, per inizializzare la sua lista di libri
}
```
* **The Problem:**
1. **A query** is run to select all authors.
2. Then, within the loop, for **each** author (`N` authors), when `.getBooks()` is accessed, JPA runs a **new query** to load the books of that specific author.
3. The result is **1 + N queries** to the database, which is extremely inefficient.
**Solutions to Problem N+1:**
1. **`JOIN FETCH` in JPQL:** This is the most common and effective solution. You write a custom query that tells JPA to load the authors and their book collections into a single SQL query using a `JOIN`.
```java
@Query("SELECT a FROM Author a JOIN FETCH a.books")
List<Author> findAllWithBooks();
```
This single query retrieves all the necessary data in one go.
2. **`@EntityGraph`:** A more modern and sometimes cleaner alternative to `JOIN FETCH`. It allows you to define, via annotations, which associations should be "eagerly" loaded for a specific query operation, without having to write the JPQL by hand.
3. **Batch Fetching:** A Hibernate-level configuration (`@BatchSize`) that, instead of running a query for each author, runs one for a "batch" of authors (e.g. `WHERE author_id IN (?, ?, ?, ...)`), significantly reducing the number of round trips to the database.
> ### **Spring Data JPA Exercises & N+1 Problem**
>
> _Increasing difficulty: from the creation of a basic entity to the diagnosis and resolution of the N+1 problem._
>
> 1. **Exercise 1 (Basic): Entities and Repositories**
> * Configure an H2 in-memory database. Create a JPA entity `Prodotto` (`@Entity`) with `id` and `nome`. Creates a `ProdottoRepository` interface that extends `JpaRepository<Prodotto, Long>`.
> Write a `@CommandLineRunner` to save some products at startup and then print them by reading them from the repository.
>
> 2. **Exercise 2 (Easy): Derived Queries**
> * Add a `categoria` attribute to the `Prodotto` entity. Add a `List<Prodotto> findByCategoria(String categoria);` method to the `ProdottoRepository` interface. Spring Data JPA will implement the query automatically. Write a test to verify that it works.
>
> 3. **Exercise 3 (Medium-Easy): Custom Query with `@Query`**
> * Add a `prezzo` attribute to the entity. Write a method in the repository to find all products whose price is higher than a given value. Implement it using the `@Query` annotation with a JPQL query (e.g. `SELECT p FROM Prodotto p WHERE p.prezzo > :prezzoMinimo`).
>
> 4. **Exercise 4 (Medium): One-to-Many Relationship**
> * Create a new entity `Produttore` and establish a `@OneToMany` relationship from `Produttore` to `Prodotto`. The inverse relationship (`@ManyToOne` from `Prodotto` to `Produttore`) should be `FetchType.LAZY`. Write a test to create a manufacturer and associate several products with it.
>
> 5. **Exercise 5 (Medium-Difficult): Detecting Problem N+1**
> * Enable display of SQL queries in `application.properties` (`spring.jpa.show-sql=true`). Write a service method that:
> 1. Retrieve all `Produttore` from database (1 query).
> 2. Iterate over each manufacturer and print the name of its first product (`produttore.getProdotti().get(0).getNome()`).
> * Look at the console: you will see an initial query for manufacturers, followed by N separate queries to retrieve products from each manufacturer. You just reproduced the N+1 problem.
>
> 6. **Exercise 6 (Difficult): Solving Problem N+1**
> * Solve the problem from exercise 5 in two different ways in `ProduttoreRepository`:
> 1. **JOIN FETCH:** Create a method with a `@Query` JPQL that uses `JOIN FETCH` to load manufacturers and their products in a single query (`SELECT p FROM Produttore p JOIN FETCH p.prodotti`).> 2. **Entity Graph:** Create a `findAll` method and annotate it with `@EntityGraph(attributePaths = "prodotti")` to specify to load the product collection in EAGER mode for that specific query.
> * Verify for both cases that the number of queries executed has been drastically reduced.---
## Conclusions
This article has covered the fundamental concepts of Java and Spring Boot, from the basics of object-oriented programming to modern architectures for enterprise application development.
**Key Points Learned:**
* **Java Fundamentals**: JVM, data types, OOP principles (encapsulation, inheritance, polymorphism), interfaces and SOLID principles
* **Collections and Stream API**: ArrayList, HashMap, functional programming and optimizations
* **Error Handling**: Exceptions, stack unwinding and best practices
* **Spring Boot**: Dependency Injection, layered architectures, REST API and persistence with JPA
These concepts form the foundation for developing robust and scalable Java applications. A deep understanding of these principles will allow you to successfully tackle projects of increasing complexity in the world of enterprise development.