Camillo Bucciarelli Camillo Bucciarelli
Current language: English. Switch to italiano Current language: English. Switch to italiano
Home Blog Talks CV Contact
← All articles
javaspring-bootprogramming

Java & Spring Boot - Introduction

Camillo Bucciarelli
Camillo Bucciarelli
Java & Spring Boot - Introduction

Index

Java Fundamentals

Collections and Stream API

Error Handling

Spring Boot Frameworks


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:

  1. Writing Source Code: The developer writes the code in files with the extension .java.
  2. 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 .class extension. During this phase, the compiler also performs syntactic and semantic analysis of the code to detect errors.
  3. Execution on the JVM: The Java Virtual Machine (JVM) loads the .class files, 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:

  1. Class Loader Subsystem: Takes care of loading, linking and initializing classes.

    • Loading: Reads .class files 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.
  2. 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++).
  3. 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:

  1. Compilation:

    • Open a terminal or command prompt.
    • Navigate to the directory where the HelloWorld.java file 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.
  2. Execution:

    • In the same terminal, run the command: java HelloWorld (note the absence of the .class extension).
    • Explanation:
      • The java command starts the JVM.
      • The JVM, through its Class Loader, searches for and loads the HelloWorld.class file.
      • The Execution Engine looks for the public static void main(String[] args) method.
      • The interpreter (or JIT compiler) executes the bytecode of the main method, which instructs the system to print the string “Hello, World!” on the console.

Exercise:

  1. Edit the HelloWorld.java file to print a different message.
  2. Recompile the file.
  3. Run the program again and verify that the output has changed.
  4. Try running the program without recompiling it after a change. What happens? Why?
  5. Introduce a syntax error into the .java file (e.g. omitting a semicolon) and try to compile it. Observe the error returned by the javac compiler.---

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 double to a int) are not permitted without an explicit “cast” by the programmer.

In Java, data types fall into two main categories:

  1. 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.
  2. 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), the tryToModify method receives a copy of the 50 value. The cash = 1000 change occurs only on this local copy. The original variable myCash in main is not affected.
  • In the second case (myWallet), the tryToModify method receives a copy of the reference to the Wallet object. Both references (the one in the main and the one in the method) point to the same object in memory. So, when the method invokes wallet.addMoney(100), it is changing the state of the only existing object, and the change is also visible from the main.

Exercises Primitive Types vs. Reference Types

Increasing difficulty: from basic syntax to a deep understanding of memory and immutable objects.

  1. Exercise 1 (Basic): Declaration and Assignment
  • Write a program that declares and initializes a int primitive variable called eta and a String reference variable called nome. Print them on screen.

  1. 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. In main, declare an integer, pass it to this method, and then print it again in main. Observe and explain why the original value has not changed.

  1. Exercise 3 (Medium-Easy): Changing the State of an Object
  • Use the StringBuilder class. Create a aggiungiTesto(StringBuilder builder) method that appends the string “World!” to the StringBuilder passed as a parameter. In the main, create a StringBuilder with “Hello”, pass it to the method, and then print it again. Observe and explain why the change is visible this time.

  1. Exercise 4 (Medium): Reassignment of the Reference
  • Create a simple class Punto with two int x, y attributes. Write a riassegna(Punto p) method that creates a new point (p = new Punto(100, 100);). In main, create a Punto, print it, pass it to method riassegna, and print it again. Explain why the original object in main has not changed.

  1. Exercise 5 (Medium-Difficult): Array Manipulation
  • Create a modificaArray(int[] array) method that sets the first element of the array to 99. Create a second method riassegnaArray(int[] array) that creates a new array (array = new int[]{0, 0, 0};). In main, create a {1, 2, 3} array, pass it first to modificaArray and print it, then pass it to riassegnaArray and print it again. Explain the two different results.

  1. Exercise 6 (Difficult): Wrapper Classes and Immutability
  • Create a modificaWrapper(Integer numero) method that tries to change the value of the wrapper (numero = 20;). In main, create a Integer 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 of StringBuilder (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 Automobile describes characteristics (attributes like colore, marca, velocitàMassima) and behaviors (methods like accelera(), frena(), accendi()) that all cars have in common.
  • Object: Is a concrete instance of a class. If Automobile is 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 Guidatore object can send the accelera() message to the Automobile object.

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.

AppearanceProcedural ProgrammingObject Oriented Programming
Main FocusOn procedures and algorithms. The program is a sequence of steps.About objects and data. The program is an interaction between objects.
OrganizationDivided into functions.Divided into classes and objects.
Data vs. FunctionsThe 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).
ApproachTop-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 SecurityLess safe. Global data can be modified by any function.Safer thanks to information hiding. The internal state of an object is protected.
Code ReuseLimited 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:

  1. 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.
  2. 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 getter and setter) 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 balance attribute, another piece of code could set it to a negative value, which the withdraw() (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.

  1. Exercise 1 (Basic): The “Anemic” Class> * Create a class ContoCorrente with two attributes public String titolare; and public double saldo;. In main, create an instance and set a negative balance directly (conto.saldo = -500;). Explain why this is a problem.

  2. Exercise 2 (Easy): Getters and Setters

  • Modify the ContoCorrente class from Exercise 1. Make the attributes private and add public getTitolare(), setTitolare(), getSaldo(), and setSaldo() methods.

  1. Exercise 3 (Medium-Easy): Validation Logic
  • Improve exercise 2. Inside the setSaldo(double nuovoSaldo) method, add a check to ensure that nuovoSaldo is not negative. If it is, print an error message and do not change the balance. Do the same for the deposita(double importo) and preleva(double importo) methods.

  1. Exercise 4 (Medium): Read-Only Fields
  • Add a private final String IBAN attribute to the ContoCorrente class. 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.

  1. Exercise 5 (Medium-Difficult): Encapsulation of Collections
  • Add a private List<String> listaMovimenti attribute to the ContoCorrente class. In the getListaMovimenti() 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)).

  1. Exercise 6 (Hard): Encapsulation of Mutable Internal Objects
  • Add a private Date dataApertura attribute (use java.util.Date which is mutable). If the getDataApertura() method returns this.dataApertura, external code can do conto.getDataApertura().setTime(0); and change the internal state of your object. Fix the problem by returning a copy of the Date object 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:

  1. Code Reuse: This is the most obvious benefit. Common code is written once in the superclass and reused by all subclasses.
  2. Organization Logic: Create clear, understandable hierarchies that model problem domain relationships.
  3. 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.

  1. Exercise 1 (Basic): Superclass and Subclass
  • Create a Personaggio class with nome and puntiVita attributes. Creates a subclass Guerriero that extends Personaggio and adds a arma attribute. In the main, create a Guerriero and access both inherited and specific attributes.

  1. Exercise 2 (Easy): Constructors and super()> * Add a constructor to the Personaggio class that initializes nome and puntiVita. The compiler will now report an error in Guerriero. Fix this by creating a constructor in Guerriero that takes nome, puntiVita, and arma, and that uses super(nome, puntiVita); to call the superclass constructor.

  2. Exercise 3 (Medium-Easy): Method Overriding

  • Add a descrivi() method to Personaggio that prints basic information. Override (@Override) the descrivi() method in Guerriero so that it also prints the weapon. To avoid rewriting code, the Guerriero version must first call super.descrivi();.

  1. Exercise 4 (Medium): Modifier protected
  • Change the visibility of puntiVita in Personaggio from private to protected. Create a subisciDanno(int danno) method in Guerriero that directly modifies this.puntiVita. Prove it works. Now try accessing puntiVita from an unrelated class (e.g. from main) and observe the error.

  1. Exercise 5 (Medium-Difficult): Three-Level Hierarchy
  • Create a new class Paladino that extends Guerriero. Add a fede attribute. Override the descrivi() method in Paladino as well, making sure it calls super.descrivi() to reuse the logic from Guerriero (which in turn reuses that from Personaggio).

  1. Exercise 6 (Difficult): Classes and Methods final
  • Create a Ladro class. Make final its method scassina(). Try creating a subclass Assassino that overrides scassina() and observe the compiler error. Next, make the entire class Ladro final. Try having Assassino extend from Ladro and 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, and final methods, 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 mioAnimale reference 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 Cane or Gatto).
  • The JVM looks for the method address emettiSuono() within the VMT. This address will be that of the class-specific method Cane or Gatto.
  • 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 instanceof and downcasting to access subclass-specific functionality.

  1. Exercise 1 (Basic): Polymorphic Array
  • Create an abstract class StrumentoMusicale with an abstract method suona(). Create two concrete subclasses Chitarra and Pianoforte that implement suona(). In main, create an array of type StrumentoMusicale[] and insert an instance of Chitarra and one of Pianoforte inside it.

  1. Exercise 2 (Easy): Polymorphic Calls
  • Using the array from Exercise 1, write a for-each loop that iterates over each StrumentoMusicale and calls the suona() method. Observe how the correct method is executed for each object.
  1. Exercise 3 (Medium-Easy): Polymorphism in Parameters
  • Create a static method accordaStrumento(StrumentoMusicale strumento) that prints “Tuning the instrument…” and then calls strumento.suona(). In main, pass both the Chitarra object and the Pianoforte object to this method and verify that they work.

  1. Exercise 4 (Medium): instanceof and Downcasting
  • Add a specific method only to the Chitarra class called cambiaCorde(). In the for-each loop of exercise 2, add a control: if (strumento instanceof Chitarra), and if true, downcast (Guitar guitar = (Guitar) instrument;) e chiama il metodo guitar.cambiaCorde().

  1. Exercise 5 (Medium-Difficult): Polymorphism with Interfaces
  • Create a Elettrico interface with a collegaAllaCorrente() method. Make class Chitarra implement this interface, but Pianoforte does not. In the loop, add a if (strumento instanceof Elettrico) control, and if so, cast to the interface and call the specific method.

  1. Exercise 6 (Hard): Avoid if-else with instanceof (Simplified Visitor Pattern)
  • This exercise introduces a more elegant alternative to downcasting. Add a accetta(Visitor v) method to StrumentoMusicale. Create a Visitor interface with visit(Chitarra c) and visit(Pianoforte p) methods. In Chitarra, the implementation of accetta will be v.visit(this);. Now, instead of if-else, you can create a ManutentoreVisitor class that implements Visitor and contains logic specific to each tool. The loop in main will simply become strumento.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 Observer interface and therefore have a update() 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).

  1. Exercise 1 (Basic): Implementing an Interface
  • Create a Volante interface with a vola() method. Create two classes, Uccello and Aereo, that implement this interface. Each implementation of vola() will print a different message.

  1. Exercise 2 (Easy): Polymorphism with Interfaces
  • In the main, create a List<Volante>. Add one instance of Uccello and one of Aereo. Write a loop that iterates over the list and calls the vola() method for each element.

  1. Exercise 3 (Medium-Easy): Multiple Interfaces
  • Create a second Nuotante interface with a nuota() method. Create a class Anatra that implements Volante, Nuotante. In main, demonstrate that a Anatra object can be inserted into either a List<Volante> or a List<Nuotante>.

  1. Exercise 4 (Medium): Methods default
  • Add a default method to the Volante interface called atterra(), which prints “Standard landing.”. Prove that the Uccello and Aereo classes “inherit” this method without needing modification. Then, override atterra() in the Aereo class to provide a more specific implementation.
  1. Exercise 5 (Medium-Difficult): Interfaces for Decoupling
  • Create a Logger interface with a log(String messaggio) method. Create two implementations: ConsoleLogger (print to console) and FileLogger (write to dummy file). Create a Calcolatrice class that takes a Logger in the constructor. Every time the Calcolatrice performs an operation, it uses the logger to record the event. In main, show how you can pass to the same Calcolatrice first a ConsoleLogger and then a FileLogger without changing a line of code from the Calcolatrice.

  1. Exercise 6 (Difficult): Strategy Pattern
  • Create a StrategiaDiPrezzo interface with a calcolaPrezzo(double prezzoBase) method. Create two implementations: PrezzoStandard (returns prezzoBase) and PrezzoScontato (returns prezzoBase * 0.8). Creates a Carrello class that has a StrategiaDiPrezzo attribute. Add a setStrategia(StrategiaDiPrezzo s) method and a getPrezzoFinale(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/else or switch to 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 any Sottoclasse without 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:
    1. “High-level modules should not depend on low-level modules. Both should depend on abstractions.”
    2. “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, ArrayList contains 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 the i element can be calculated directly, resulting in a time complexity of O(1) (constant time).
  • Addition (add):
    1. Best Case: If there is still space in the internal array, the element is simply added to the end, and a size counter is incremented. This operation is O(1).
    2. Worst Case (Resizing): If the internal array is full and you try to add a new element, ArrayList performs 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 n is the number of current elements.

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 ArrayList with 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.

  1. Exercise 1 (Basic): CRUD Operations
  • Create a ArrayList of String. 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.

  1. Exercise 2 (Easy): Iteration> * Create a ArrayList of Integer and fill it with 5 numbers. Write three different loops to print all elements: a classic for loop with index, a for-each loop, and an iteration using forEach with a lambda expression (lista.forEach(n -> System.out.println(n));).

  2. 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, use indexOf() to find its location and print it.

  1. Exercise 4 (Medium): Insertion Performance
  • Creates a ArrayList and a LinkedList of Integer. 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 using System.nanoTime() before and after the loop. Explains the drastic difference in performance.

  1. Exercise 5 (Medium-Difficult): Removal during Iteration (Common Error)
  • Create a ArrayList of Integer from 1 to 10. Try to remove all even numbers using a for-each loop. Look at the ConcurrentModificationException. Explain why this happens.

  1. Exercise 6 (Difficult): Correct Removal with Iterator
  • Solve Exercise 5. Use a Iterator to scroll through the list. When you find an even number, use the iterator.remove() method to safely remove it. Alternatively, it shows how to solve the problem using the removeIf method 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:

  1. Hashing: When you insert a pair (K, V), HashMap takes the K key and calculates an integer value called hash code via the key’s hashCode() method. This hash code is then processed by an internal hashing function to determine an index into an internal array (called table or buckets).
  2. Bucket Array: The internal array is a series of “buckets”. The calculated index determines in which bucket the key-value pair will be stored.
  3. Retrieval: When you ask for the value associated with a key (get(K)), HashMap recalculates 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:

  1. Bucket Structure: Each bucket of the array does not contain a single element, but the head of a linked list (LinkedList) of nodes.
  2. 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.
  3. Search in a Bucket: When searching for a key in a bucket that contains multiple nodes, the HashMap must iterate through the linked list and use the equals() 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:

  1. If two objects are equal according to equals(), they must return the same hashCode().
  2. If two objects have the same hashCode(), they are not necessarily the same according to equals() (this is a collision). If this contract is not respected, the HashMap will behave unpredictably.

HashMap Exercises

Increasing difficulty: from basic use to creating a custom class as a key, understanding the hashCode/equals. contract

  1. 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.

  1. 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.

  1. Exercise 3 (Medium-Easy): Calculation of Frequencies
  • Write a program that takes a text (a String) and uses a HashMap<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.

  1. Exercise 4 (Medium): Managing Missing Values
  • Improve exercise 3 using the getOrDefault method. Instead of checking with containsKey, you can write mappa.put(carattere, mappa.getOrDefault(carattere, 0) + 1);. Explain how this single line simplifies the code.

  1. Exercise 5 (Medium-Difficult): Custom Key (Without hashCode/equals)
  • Create a Studente class with id and nome. Create a HashMap<Studente, Integer> to map students to grades. Creates two separate Studente objects but with the same data (new Studente(1, "Mario") and new 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 (get returns null). Explain why.

  1. Exercise 6 (Difficult): The Contract hashCode/equals
  • Solve Exercise 5. Override the hashCode() and equals() methods in the Studente class. The logic of equals should compare the id. The logic of hashCode should be based on id. 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 for loop 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.

Main Features of a Stream:

  1. 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().

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:
    1. 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).
    2. 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 as limit()).

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. ArrayList is great, LinkedList less 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 collectors and parallelization.

  1. Exercise 1 (Basic): filter and forEach
  • 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.

  1. Exercise 2 (Easy): map and collect
  • 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 be collect(Collectors.toList()).

  1. Exercise 3 (Medium-Easy): Chaining (Pipeline)
  • Given a List<Prodotto> (with attributes nome, categoria, prezzo), write a single stream pipeline that:
  1. Filter products from the “Electronics” category.

  2. Filter those with a price above 100.

  3. Extracts (maps) product names only.

  4. Collects the names in a new list.

  5. Exercise 4 (Medium): Different Terminal Operations (findFirst, anyMatch)

  • Given a List<Integer>, use a stream to:
  1. Check if there is at least one number greater than 50 (anyMatch).

  2. Find the first even number and print it (filter and findFirst). Handle the case where it doesn’t exist with Optional.

  3. Exercise 5 (Medium-Difficult): reduce and mapToInt

  • Given a List<Integer>, compute the sum of all numbers in two different ways using streams:
  1. Using the reduce method.> 2. Using mapToInt to convert the Stream<Integer> to a IntStream and then calling the sum() method, which is more efficient.

  2. Exercise 6 (Difficult): Grouping with Collectors.groupingBy

  • Given the List<Prodotto> from exercise 3, use a stream and a collector to create a Map<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:

  1. 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 of if-else. This mixed business logic with error handling, making the code difficult to read and maintain.
  2. 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 try block, while the management of anomalous situations is delegated to the catch blocks.
  • 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.

  1. 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).
  2. Search for a Handler: The runtime searches, within the current method, for a catch block that can handle that type of exception.
  3. Unrolling the Stack:
    • If an appropriate catch is 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 catch block in the caller.
    • This process continues, “unrolling” the call stack one frame at a time.
  4. Management or Termination:
    • If a compatible catch block is found, the stack stops unrolling and the code inside the catch block is executed.
    • If the exception makes it all the way to method main and is not caught, the current thread terminates and the stack trace of the exception is usually printed to the console.

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:
    1. Creation of Exception Object: Requires memory allocation on the heap.
    2. Stack Trace Capture: This is the most expensive operation. The JVM must traverse the entire call stack to build the stack trace.
    3. 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.

  1. Exercise 1 (Basic): try-catch
  • Write a program that divides two numbers. Put the division operation in a try block and catch the ArithmeticException that occurs when trying to divide by zero, printing an appropriate error message.

  1. Exercise 2 (Easy): Block finally
  • Modify Exercise 1. Add a finally block that prints “Operation completed.”. Run the program with both valid division and division by zero, and observe that the finally block executes in both cases.

  1. 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 ArrayIndexOutOfBoundsException and ArithmeticException separately with two distinct catch blocks.

  1. Exercise 4 (Medium): Checked vs. Unchecked Exceptions
  • Write a leggiFile(String nomeFile) method that could throw a IOException (which is a checked exception). Don’t handle it inside the method, but add throws IOException to the method signature. Call this method from main and observe that the compiler forces you to handle the exception with a try-catch or propagate it further.

  1. Exercise 5 (Medium-Difficult): Custom Exceptions
  • Create your own custom exception class (checked) SaldoInsufficienteException that extends Exception. Modify the ContoCorrente class (from a previous exercise) so that the preleva(double importo) method throws this exception if the balance is insufficient, instead of just printing a message.

  1. Exercise 6 (Difficult): try-with-resources> * Write a program that opens a file for reading using BufferedReader and FileReader. Instead of using a finally block to ensure the reader is closed (reader.close()), use the try-with-resources construct. 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.xml for 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:

  1. “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-web dependency, Spring Boot assumes that you are building a web application and automatically configures Tomcat, the DispatcherServlet, and other essential components without the developer having to write a single line of configuration.
  2. Auto-Configuration: This is the main mechanism behind “Convention over Configuration”. Spring Boot provides a wide range of conditional @Configuration classes 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.

  3. 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.

  4. Out-of-the-Box Application Health and Metrics: By including the spring-boot-starter-actuator starter, 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.

  1. 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 @RestController with a single method mapped to /hello that returns the string “Hello, Spring Boot!”. Launch the application and check the result in your browser.

  1. Exercise 2 (Easy): Using Properties
  • In the application.properties file, change the embedded server port to 8090 using the server.port property. Restart the application and verify that it now responds on the new port.

  1. Exercise 3 (Medium-Easy): Add Another Starter
  • Add the spring-boot-starter-actuator starter to your pom.xml. Restart the application. Access endpoints that Actuator automatically exposes, such as /actuator/health and /actuator/info. This demonstrates how Spring Boot adds functionality based on the starters present.

  1. Exercise 4 (Medium): Creating a Custom Configuration Class
  • Create a AppInfo class with two string fields, name and version. Annotate it with @ConfigurationProperties(prefix = "app") and @Configuration. Enable it with @EnableConfigurationProperties on the main class. Define the app.name and app.version properties in application.properties.

  1. Exercise 5 (Medium-Difficult): Using the Custom Configuration> * Inject the AppInfo bean created in the previous exercise into your controller. Modify the /hello endpoint to return a welcome message that includes the application name and version taken from the configuration.

  2. Exercise 6 (Difficult): Analyzing Self-Configuration

  • Start the application with the --debug flag (or set debug=true to application.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 as DataSourceAutoConfiguration.

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:
    1. 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.
    2. 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.
    3. Dirty Checking: Any changes made to a “managed” entity, i.e. present in the Persistence Context, are automatically tracked.
    4. Transaction Commit: At the end of the transaction (typically at the end of a @Service method 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 sends UPDATE queries to the database.

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 User has a UserProfile.
    • JPA Annotations: @OneToOne.
  • 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 Author has many Book.
    • JPA Annotations: @OneToMany on the Author side and @ManyToOne on the Book side. The two-way relationship is the most common.
  • Many-to-Many: Many instances of one entity can be associated with many instances of another.

    • Example: A Student may be enrolled in many Course, and a Course may have many Student.
    • Relational Implementation: Requires an intermediate junction table (e.g. student_course) that contains the foreign keys of both tables.
    • JPA Annotation: @ManyToMany.

Fetch Strategies:

When JPA loads an entity from the database, what should it do with its related entities? The Fetch Strategy defines this behavior.

  1. FetchType.EAGER (“Greedy” Loading):* Behavior: When loading the main entity (e.g. a Author), JPA immediately also loads all its related entities (all its Book).

    • 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:
      • @OneToOne and @ManyToOne are EAGER by default.
    • Pros: Pairing is always available. No LazyInitializationException errors.
    • Cons: Can be very inefficient. You could load large object graphs from memory when only the main entity is needed, wasting memory and time.
  2. FetchType.LAZY (“Lazy” Loading):

    • Behavior: When loading the main entity (e.g. a Author), its related entities (the Book) 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:
      • @OneToMany and @ManyToMany are LAZY by default.
    • Pros: Much more efficient. Only the data actually needed is loaded.
    • Cons: If you try to access a LAZY collection outside of an active transaction (i.e. when the Persistence Context is closed), you will get a LazyInitializationException.

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 @OneToMany relationship between Author and Book. 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.
Camillo Bucciarelli
Camillo Bucciarelli
Technical Leader · Flutter dev · Speaker. I write about code and how to survive a team.
Write me