Guida Java

Il Linguaggio Java: Una Guida Completa

1. Introduzione a Java

Storia e Origini

Java nacque inizialmente come progetto interno di Sun Microsystems nel 1991 con il nome “Oak”, sviluppato da un team guidato da James Gosling. L’obiettivo originale era creare un linguaggio per dispositivi elettronici di consumo. Tuttavia, con l’esplosione del World Wide Web nei primi anni ’90, il team di Sun riconobbe il potenziale del linguaggio per Internet e lo rinominò “Java” nel 1995.

Sun Microsystems ha gestito e sviluppato Java fino al 2010, quando l’azienda è stata acquisita da Oracle Corporation, che da allora ha continuato a sviluppare e promuovere il linguaggio. Sotto la guida di Oracle, Java ha mantenuto la sua rilevanza adattandosi alle nuove esigenze di sviluppo.

Un principio fondamentale che ha guidato lo sviluppo di Java è stato “Write Once, Run Anywhere” (WORA), che evidenzia la capacità del codice Java di essere eseguito su qualsiasi dispositivo dotato di una Java Virtual Machine (JVM), indipendentemente dall’hardware o dal sistema operativo sottostante.

Caratteristiche Chiave

Orientamento agli oggetti (OOP): Java è stato progettato come linguaggio completamente orientato agli oggetti, in cui tutto (eccetto i tipi primitivi) è un oggetto. Questo approccio favorisce la modularità, la riusabilità e la manutenibilità del codice.

Indipendenza dalla piattaforma (JVM): Il bytecode Java viene eseguito sulla Java Virtual Machine, che funge da livello di astrazione tra il codice e l’hardware del sistema, garantendo la portabilità tra diverse piattaforme.

Robustezza e sicurezza: Java include diverse caratteristiche per garantire codice robusto, come il strong typing (controllo rigoroso dei tipi), la gestione delle eccezioni e un modello di sicurezza sandbox che limita l’accesso alle risorse del sistema.

Portabilità e prestazioni: Grazie alla JVM, il codice Java può essere eseguito su qualsiasi sistema che supporti Java. Le moderne tecnologie come il Just-In-Time (JIT) compilation e le ottimizzazioni della JVM hanno notevolmente migliorato le prestazioni nel tempo.

Multithreading: Java supporta nativamente la programmazione concorrente attraverso il multithreading, consentendo l’esecuzione simultanea di più thread all’interno dello stesso programma.

Gestione automatica della memoria (garbage collection): Java libera automaticamente la memoria occupata dagli oggetti non più utilizzati, riducendo il rischio di memory leak e semplificando lo sviluppo.

Versioni di Java

Java si articola in tre principali edizioni:

  • Java Standard Edition (Java SE): La piattaforma base che include il core del linguaggio, le librerie fondamentali e la JVM per applicazioni desktop e server.
  • Java Enterprise Edition (Java EE), ora Jakarta EE: Un’estensione di Java SE che fornisce API e runtime per lo sviluppo di applicazioni enterprise scalabili, distribuite e multi-tier.
  • Java Micro Edition (Java ME): Una versione ridotta di Java SE per dispositivi embedded e mobili con capacità limitate.

Le versioni recenti di Java hanno adottato un ciclo di rilascio semestrale. Tra le innovazioni più significative delle ultime versioni ricordiamo:

  • Java 8: Introduzione delle espressioni lambda, Stream API, nuova API per la gestione delle date e orari.
  • Java 9: Sistema di moduli, JShell (REPL).
  • Java 10: Inferenza di tipo locale var.
  • Java 11: Esecuzione diretta di file sorgente, HTTP client.
  • Java 16: Records, sealed classes.
  • Java 17: Pattern matching per switch, sealed classes finali.
  • Java 21: Virtual threads, record patterns, switch expressions.

2. Fondamenti del Linguaggio

Sintassi di Base

La struttura di un programma Java si basa su classi che contengono metodi e variabili. Ecco un esempio base:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

Tipi di dati in Java:

  • Primitivi: byte, short, int, long, float, double, char, boolean
  • Di riferimento: Classi, interfacce, array

Operatori:

  • Aritmetici: +, -, *, /, %
  • Logici: &&, ||, !
  • Bitwise: &, |, ^, ~, <<, >>, >>>
  • Assegnazione: =, +=, -=, *=, /=, %=
  • Confronto: ==, !=, >, <, >=, <=

Strutture di controllo:

  • Condizionali: if-else, switch
  • Cicli: for, while, do-while
  • Controllo di flusso: break, continue, return

Array e stringhe: Gli array sono contenitori di lunghezza fissa che contengono elementi dello stesso tipo:

int[] numeri = new int[5];
numeri[0] = 10;

String[] parole = {"Java", "è", "potente"};

Le stringhe in Java sono oggetti immutabili della classe String:

String saluto = "Ciao";
String messaggio = saluto + ", mondo!"; // Concatenazione

Programmazione Orientata agli Oggetti (OOP)

Classi e oggetti: Le classi sono i blueprint per creare oggetti:

public class Persona {
    // Campi (attributi)
    private String nome;
    private int età;
    
    // Costruttore
    public Persona(String nome, int età) {
        this.nome = nome;
        this.età = età;
    }
    
    // Metodi
    public void saluta() {
        System.out.println("Ciao, sono " + nome);
    }
}

// Creazione di un oggetto
Persona persona = new Persona("Marco", 30);
persona.saluta();

Ereditarietà: L’ereditarietà permette di estendere funzionalità esistenti:

public class Studente extends Persona {
    private String corso;
    
    public Studente(String nome, int età, String corso) {
        super(nome, età);
        this.corso = corso;
    }
    
    // Override di un metodo
    @Override
    public void saluta() {
        System.out.println("Ciao, sono uno studente di " + corso);
    }
}

Polimorfismo: Il polimorfismo consente di utilizzare un’interfaccia comune per gestire diversi tipi di oggetti:

Persona p1 = new Persona("Giulia", 25);
Persona p2 = new Studente("Luca", 20, "Informatica");

p1.saluta(); // Metodo della classe Persona
p2.saluta(); // Metodo della classe Studente (override)

Incapsulamento: L’incapsulamento protegge i dati e permette di controllare l’accesso agli attributi:

public class ContoBancario {
    private double saldo;
    
    public double getSaldo() {
        return saldo;
    }
    
    public void deposita(double importo) {
        if (importo > 0) {
            saldo += importo;
        }
    }
}

Interfacce e classi astratte: Le interfacce definiscono contratti che le classi devono implementare:

public interface Suonabile {
    void suona();
}

public class Chitarra implements Suonabile {
    @Override
    public void suona() {
        System.out.println("La chitarra suona");
    }
}

Le classi astratte forniscono un’implementazione parziale:

public abstract class Strumento {
    private String nome;
    
    public Strumento(String nome) {
        this.nome = nome;
    }
    
    public abstract void suona();
    
    public void descrivi() {
        System.out.println("Questo è un " + nome);
    }
}

Pacchetti e modificatori di accesso: I pacchetti organizzano le classi e i modificatori di accesso controllano la visibilità:

  • public: accessibile da qualunque classe
  • protected: accessibile all’interno del pacchetto e dalle sottoclassi
  • default (package-private): accessibile solo all’interno del pacchetto
  • private: accessibile solo all’interno della classe

Gestione delle Eccezioni

Java utilizza un sistema di gestione delle eccezioni per gestire gli errori durante l’esecuzione:

Tipi di eccezioni:

  • Checked: eccezioni che devono essere gestite o dichiarate (IOException, SQLException)
  • Unchecked: non richiedono gestione esplicita (RuntimeException e sottoclassi come NullPointerException)

Blocchi try-catch-finally:

try {
    // Codice che potrebbe generare un'eccezione
    FileReader file = new FileReader("file.txt");
} catch (FileNotFoundException e) {
    // Gestione dell'eccezione
    System.out.println("File non trovato: " + e.getMessage());
} finally {
    // Codice eseguito sempre, indipendentemente dalle eccezioni
    System.out.println("Operazione completata");
}

Eccezioni personalizzate:

public class SaldoInsufficienteException extends Exception {
    public SaldoInsufficienteException(String messaggio) {
        super(messaggio);
    }
}

// Utilizzo
public void preleva(double importo) throws SaldoInsufficienteException {
    if (saldo < importo) {
        throw new SaldoInsufficienteException("Saldo insufficiente");
    }
    saldo -= importo;
}

3. Java Standard Edition (Java SE)

Librerie Fondamentali

java.lang: Package fondamentale importato automaticamente, contiene classi essenziali come String, Math, System e le classi wrapper (Integer, Double, ecc.).

String testo = "Java";
int lunghezza = testo.length();
double radice = Math.sqrt(16);

java.util: Fornisce strutture dati, utilità per date, gestione eventi, e molto altro.

import java.util.Random;
import java.util.Date;

Random random = new Random();
int numero = random.nextInt(100); // Numero casuale tra 0 e 99

Date oggi = new Date();

java.io: Supporta operazioni di input/output basate su stream.

import java.io.*;

try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
     BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        writer.write(line.toUpperCase());
        writer.newLine();
    }
} catch (IOException e) {
    e.printStackTrace();
}

java.nio: New I/O, fornisce API più moderne per operazioni I/O.

import java.nio.file.*;

Path path = Paths.get("file.txt");
List<String> lines = Files.readAllLines(path);
Files.write(Paths.get("output.txt"), lines);

java.net: Supporta la programmazione di rete.

import java.net.*;
import java.io.*;

URL url = new URL("https://example.com");
URLConnection conn = url.openConnection();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

java.time: Introdotta in Java 8, offre API moderne per gestire date e orari.

import java.time.*;

LocalDate oggi = LocalDate.now();
LocalTime adesso = LocalTime.now();
LocalDateTime dataOra = LocalDateTime.now();
ZonedDateTime dataOraZona = ZonedDateTime.now();

// Operazioni con le date
LocalDate domani = oggi.plusDays(1);

Collezioni

Il framework delle collezioni in Java fornisce un’architettura unificata per rappresentare e manipolare gruppi di oggetti.

List: Sequenze ordinate che possono contenere duplicati.

import java.util.*;

// ArrayList: implementazione basata su array
List<String> nomi = new ArrayList<>();
nomi.add("Marco");
nomi.add("Laura");
nomi.add("Giovanni");

// LinkedList: implementazione basata su lista concatenata
List<String> cognomi = new LinkedList<>();
cognomi.add("Rossi");
cognomi.add("Bianchi");

Set: Collezioni che non permettono duplicati.

// HashSet: implementazione basata su hash table
Set<Integer> numeri = new HashSet<>();
numeri.add(1);
numeri.add(2);
numeri.add(1); // Viene ignorato (duplicato)

// TreeSet: implementazione ordinata basata su albero
Set<String> parole = new TreeSet<>();
parole.add("Java");
parole.add("Python");
parole.add("C++");
// Restituisce elementi in ordine alfabetico

Map: Associazioni chiave-valore dove le chiavi sono uniche.

// HashMap: implementazione basata su hash table
Map<String, Integer> età = new HashMap<>();
età.put("Marco", 30);
età.put("Laura", 25);
int etàMarco = età.get("Marco"); // 30

// TreeMap: implementazione ordinata basata su albero
Map<String, String> capitali = new TreeMap<>();
capitali.put("Italia", "Roma");
capitali.put("Francia", "Parigi");
// Chiavi ordinate alfabeticamente

Iteratori e stream:

Iterazione tradizionale:

List<String> frutti = Arrays.asList("mela", "banana", "arancia");
for (String frutto : frutti) {
    System.out.println(frutto);
}

Iterator<String> iterator = frutti.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

Stream API (Java 8+):

List<String> frutti = Arrays.asList("mela", "banana", "arancia");

// Filtrare e trasformare
frutti.stream()
      .filter(f -> f.length() > 4)
      .map(String::toUpperCase)
      .forEach(System.out::println);

Multithreading e Concorrenza

Thread e Runnable:

Creazione di thread implementando l’interfaccia Runnable:

class TaskDemo implements Runnable {
    @Override
    public void run() {
        System.out.println("Task eseguito da: " + Thread.currentThread().getName());
    }
}

// Utilizzo
Thread thread = new Thread(new TaskDemo());
thread.start();

Creazione estendendo la classe Thread:

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread in esecuzione");
    }
}

// Utilizzo
MyThread thread = new MyThread();
thread.start();

Sincronizzazione e lock:

Metodi sincronizzati:

public synchronized void metodoSincronizzato() {
    // Solo un thread può accedere a questo metodo alla volta
}

Blocchi sincronizzati:

public void metodo() {
    synchronized(this) {
        // Sezione critica
    }
}

Lock espliciti:

import java.util.concurrent.locks.*;

private final ReentrantLock lock = new ReentrantLock();

public void metodoConLock() {
    lock.lock();
    try {
        // Sezione critica
    } finally {
        lock.unlock(); // Importante rilasciare sempre il lock
    }
}

Executor framework:

import java.util.concurrent.*;

// Pool di thread fisso
ExecutorService executor = Executors.newFixedThreadPool(5);

// Sottomissione di task
executor.submit(() -> {
    System.out.println("Task eseguito da: " + Thread.currentThread().getName());
});

// Chiusura ordinata
executor.shutdown();

Le classi Concurrent del package java.util.concurrent:

Collezioni thread-safe:

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
BlockingQueue<Task> taskQueue = new LinkedBlockingQueue<>();

Atomic variables:

AtomicInteger counter = new AtomicInteger(0);
int value = counter.incrementAndGet(); // Operazione atomica

CompletableFuture (Java 8+):

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // Operazione in background
    return "Risultato";
});

future.thenAccept(result -> {
    System.out.println("Risultato ottenuto: " + result);
});

4. Aspetti Avanzati

Java Generics

I generics permettono di creare classi, interfacce e metodi che operano su tipi parametrizzati.

Definizione di classi generiche:

public class Box<T> {
    private T content;
    
    public void set(T content) {
        this.content = content;
    }
    
    public T get() {
        return content;
    }
}

// Utilizzo
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String content = stringBox.get();

Wildcards:

// Unknown wildcard
void processList(List<?> list) {
    // Possiamo leggere oggetti (come Object)
}

// Bounded wildcard
void processNumbers(List<? extends Number> list) {
    // Possiamo leggere elementi come Number
}

// Lower bounded wildcard
void addIntegers(List<? super Integer> list) {
    list.add(42); // Possiamo aggiungere Integer
}

Vantaggi e limitazioni:

  • Vantaggi: type safety, eliminazione dei cast, riutilizzo del codice
  • Limitazioni: erasure (le informazioni sui tipi generici sono disponibili solo in fase di compilazione), impossibilità di creare array di tipi generici, restrizioni con tipi primitivi

Annotazioni

Le annotazioni forniscono metadati per il codice Java.

Annotazioni standard:

@Override // Verifica che il metodo faccia override
public String toString() {
    return "Custom toString";
}

@Deprecated // Marca un elemento come obsoleto
public void metodoVecchio() {}

@SuppressWarnings("unchecked") // Sopprime warnings
public void metodo() {
    // Codice che genera warnings
}

@FunctionalInterface // Interfaccia con un solo metodo astratto
interface Executor {
    void execute();
}

Annotazioni personalizzate:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME) // Quando è disponibile
@Target(ElementType.METHOD) // Dove può essere usata
public @interface Test {
    String description() default ""; // Elemento con valore default
    int priority();
}

// Utilizzo
@Test(description = "Test di funzionalità", priority = 1)
public void testMethod() {
    // Implementazione
}

Elaborazione delle annotazioni:

import java.lang.reflect.*;

// Ottenere le annotazioni a runtime
Method method = obj.getClass().getMethod("testMethod");
if (method.isAnnotationPresent(Test.class)) {
    Test test = method.getAnnotation(Test.class);
    System.out.println("Descrizione: " + test.description());
    System.out.println("Priorità: " + test.priority());
}

Reflection

La reflection permette di esaminare e modificare il comportamento delle classi, interfacce, campi e metodi a runtime.

Accesso a classi e oggetti a runtime:

// Ottenere Class object
Class<?> clazz = Class.forName("com.example.MyClass");
// oppure
Class<?> clazz = MyClass.class;

// Istanziare oggetti
Object obj = clazz.newInstance(); // Deprecated in Java 9+
// Metodo preferito (Java 9+)
Constructor<?> constructor = clazz.getDeclaredConstructor();
Object obj = constructor.newInstance();

// Accedere ai campi
Field field = clazz.getDeclaredField("myField");
field.setAccessible(true); // Permette di accedere anche a campi privati
field.set(obj, newValue);
Object value = field.get(obj);

// Invocare metodi
Method method = clazz.getDeclaredMethod("myMethod", String.class, int.class);
method.setAccessible(true);
Object result = method.invoke(obj, "param1", 42);

Casi d’uso e considerazioni sulla sicurezza:

  • Casi d’uso: framework di dependency injection, ORM, serializzazione/deserializzazione, test unitari
  • Sicurezza: può aggirare l’incapsulamento e le restrizioni di accesso, potenzialmente compromettendo la sicurezza

Java 8 e Successivi

Lambda expressions:

// Pre-Java 8
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Button clicked");
    }
});

// Con lambda (Java 8+)
button.addActionListener(e -> System.out.println("Button clicked"));

Stream API:

List<String> nomi = Arrays.asList("Marco", "Anna", "Luca", "Giulia");

// Filtrare, trasformare e raccogliere
List<String> risultato = nomi.stream()
                            .filter(n -> n.length() > 4)
                            .map(String::toUpperCase)
                            .sorted()
                            .collect(Collectors.toList());

// Operazioni di riduzione
int somma = numeri.stream().mapToInt(Integer::intValue).sum();
Optional<Integer> max = numeri.stream().max(Integer::compare);

Interfacce funzionali:

// Predefinite in java.util.function
Predicate<String> isLong = s -> s.length() > 5;
Consumer<String> printer = System.out::println;
Function<String, Integer> length = String::length;
Supplier<Double> random = Math::random;

Optional class:

Optional<String> optional = Optional.ofNullable(getValue());

// Uso sicuro
optional.ifPresent(System.out::println);

// Valore di default
String result = optional.orElse("default");

// Lancio di eccezione se vuoto
String value = optional.orElseThrow(() -> new NoSuchElementException());

// Trasformazione
Optional<Integer> length = optional.map(String::length);

Moduli Java 9+:

// module-info.java
module com.example.myapp {
    requires java.sql;
    requires transitive java.logging;
    
    exports com.example.myapp.api;
    exports com.example.myapp.internal to com.example.other;
    
    opens com.example.myapp.model to java.persistence;
    
    provides com.example.service.MyService 
        with com.example.myapp.impl.MyServiceImpl;
}

5. Framework e Tecnologie Java

Framework Popolari

Spring Framework: Spring è un framework leggero che fornisce un modello di programmazione e configurazione completo per applicazioni Java moderne.

Inversione di controllo (IoC):

@Component
public class UserService {
    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

Dependency Injection:

@Configuration
public class AppConfig {
    @Bean
    public UserRepository userRepository() {
        return new JdbcUserRepository();
    }
    
    @Bean
    public UserService userService(UserRepository userRepository) {
        return new UserService(userRepository);
    }
}

Hibernate (ORM): Hibernate è un framework ORM (Object-Relational Mapping) che semplifica l’accesso ai dati relazionali.

@Entity
@Table(name = "utenti")
public class Utente {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "nome")
    private String nome;
    
    @Column(name = "email", unique = true)
    private String email;
    
    @ManyToOne
    @JoinColumn(name = "dipartimento_id")
    private Dipartimento dipartimento;
    
    // Getter e setter
}

JavaServer Faces (JSF): JSF è un framework per la creazione di interfacce utente per applicazioni web Java.

<h:form>
    <h:inputText value="#{userBean.name}" />
    <h:commandButton value="Submit" action="#{userBean.save}" />
</h:form>

Apache Maven e Gradle: Strumenti per la gestione delle dipendenze e il build automation.

Maven (pom.xml):

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.6.3</version>
    </dependency>
</dependencies>

Gradle (build.gradle):

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web:2.6.3'
    testImplementation 'junit:junit:4.13.2'
}

Microservizi con Java

Spring Boot: Spring Boot semplifica lo sviluppo di applicazioni Spring riducendo la configurazione necessaria.

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

@RestController
@RequestMapping("/api/users")
public class UserController {
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        // Implementazione
    }
}

Spring Cloud: Fornisce strumenti per sviluppare sistemi distribuiti comuni.

@EnableDiscoveryClient
@SpringBootApplication
public class ServiceApplication {
    // ...
}

Docker e Kubernetes: Containerizzazione e orchestrazione per applicazioni Java.

Dockerfile:

FROM openjdk:11-jre-slim
COPY target/app.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

Test in Java

JUnit: Framework per unit testing in Java.

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class CalcolatriceTest {
    @Test
    public void testAddizione() {
        Calcolatrice calc = new Calcolatrice();
        assertEquals(5, calc.somma(2, 3), "2 + 3 dovrebbe essere 5");
    }
    
    @Test
    public void testDivisionePerZero() {
        Calcolatrice calc = new Calcolatrice();
        assertThrows(ArithmeticException.class, () -> {
            calc.dividi(10, 0);
        });
    }
}

Mockito: Framework per il mocking di oggetti nei test.

import static org.mockito.Mockito.*;

@Test
public void testUserService() {
    // Creazione del mock
    UserRepository mockRepository = mock(UserRepository.class);
    
    // Configurazione del comportamento
    when(mockRepository.findById(1L)).thenReturn(new User(1L, "Mario"));
    
    // Utilizzo del mock
    UserService service = new UserService(mockRepository);
    User user = service.getUserById(1L);
    
    // Verifica
    assertEquals("Mario", user.getName());
    verify(mockRepository).findById(1L);
}

6. Risorse e Comunità

Risorse Online

Documentazione ufficiale Oracle:

Tutorial e guide:

  • Baeldung (tutorials.baeldung.com)
  • Java Code Geeks (javacodegeeks.com)
  • Vogella (vogella.com/tutorials/java)
  • JournalDev (journaldev.com)
  • Mkyong (mkyong.com)

Forum e comunità online:

  • Stack Overflow (stackoverflow.com/questions/tagged/java)
  • Reddit r/java (reddit.com/r/java)
  • Java Ranch (javaranch.com)

Certificazioni Java

Oracle Certified Java Programmer (OCJP):

  • Oracle Certified Associate (OCA)
  • Oracle Certified Professional (OCP)
  • Oracle Certified Master (OCM)

Consigli per la preparazione:

  1. Studiare i materiali ufficiali e i libri consigliati
  2. Fare pratica con esercizi e progetti personali
  3. Utilizzare simulazioni d’esame
  4. Partecipare a forum e gruppi di studio
  5. Ripassare gli argomenti più complessi come concorrenza, collezioni e generics
  6. Esaminare attentamente il syllabus dell’esame scelto