Massimiliano Tarquini

La Struttura del Libro

Manuale Java gratuito per imparare la programmazione
Scarica il Libro Gratis

Argomenti Trattati

Esplora i contenuti del libro "Java Mattone dopo Mattone". Ogni capitolo tratta un argomento chiave del linguaggio Java.

La programmazione ad oggetti

Questo capitolo introduce il paradigma Object Oriented, fornendo ai principianti i concetti essenziali per sviluppare applicazioni orientate agli oggetti in Java.

A differenza dei paradigmi Procedurale e Funzionale, che suddividono problemi complessi in sottoproblemi risolvibili, l’Object Oriented concentra l’attenzione sui dati, strutturando l’applicazione come un insieme di oggetti che interagiscono tra loro.

Questo approccio rappresenta un’evoluzione verso un modello più moderno e intuitivo, migliorando la comprensibilità e la manutenzione del codice.

Approfondisci

Il linguaggio Java

Java è un linguaggio di programmazione orientato agli oggetti, modellato su C e C++, progettato per essere indipendente dalla piattaforma. Lo slogan "Write Once, Run Anywhere" (WORA) evidenzia la sua capacità di eseguire il codice compilato su qualsiasi piattaforma supportata, grazie alla Java Virtual Machine (JVM), che traduce il bytecode in istruzioni eseguibili dalla macchina ospite.

Creato nel 1995 da James Gosling per Sun Microsystems e ora di proprietà di Oracle, Java è gratuito, basato su classi e staticamente tipizzato. È ampiamente utilizzato per applicazioni web, client-server, REST e microservizi.

L'ecosistema Java comprende tre componenti principali:

  • JVM: ambiente virtuale per l'esecuzione indipendente dalla piattaforma.
  • JRE: runtime necessario per eseguire applicazioni Java.
  • JDK: include JRE, compilatore e strumenti per lo sviluppo Java.
Approfondisci

Sintassi di java, dichiarazione di variabili ed operatori

La sintassi di Java riprende in parte quella di C e C++, introducendo variabili a dimensione fissa indipendenti dalla piattaforma. Le espressioni in Java, utilizzate per calcoli e controllo del flusso, combinano variabili e operatori, restituendo un singolo valore, ma non costituiscono unità eseguibili complete.

Le istruzioni, invece, sono unità complete che includono assegnamenti, valutazioni di espressioni e chiamate a oggetti, terminate da “;”.

Nel capitolo sono approfondite variabili, array e regole sintattiche e semantiche del linguaggio, essenziali per i capitoli successivi.

Istruzioni e controllo di flusso

Java eredita da C e C++ l’intero insieme di istruzioni per il controllo di flusso, apportando solo alcune modifiche. In aggiunta, Java introduce alcune nuove istruzioni necessarie alla manipolazione di oggetti per adattarle alla sua natura di linguaggio ad oggetti.

Questo capitolo tratta le istruzioni condizionali, le istruzioni cicliche, quelle relative alla gestione dei package per l’organizzazione di classi e l’istruzione import per risolvere il problema della posizione delle definizioni di classi in altri file o package.

Package Java

I package rappresentano per Java quello che i namespace rappresentano per il C++. Sono raggruppamenti di definizioni di classi sotto uno stesso nome e rappresentano il meccanismo utilizzato da Java per localizzare le definizioni di classi, durante la compilazione o durante l’esecuzione di un’applicazione.

I package sono anche lo strumento fornito dal linguaggio per distribuire librerie di classi Java affinché possano essere riutilizzate all’interno di altre applicazioni. In questi casi, è necessario utilizzare l’istruzione import per indicare quali classi includere all’interno della nostra applicazione.

In questo capitolo è mostrato in dettaglio il meccanismo di raggruppamento e le sue caratteristiche. Tuttavia, altri aspetti saranno trattati nei capitoli successivi.

Definizione di classi ed oggetti

In questo capitolo saranno trattati gli aspetti specifici del linguaggio Java relativi alla definizione di classi ed alla creazione di oggetti: le regole sintattiche base per la creazione di classi, l’allocazione di oggetti e la determinazione del punto di ingresso (entry point) di un’applicazione.

Per tutta la durata del capitolo sarà importante ricordare i concetti base discussi in precedenza, in particolar modo quelli relativi alla definizione di una classe di oggetti. Le definizioni di classe rappresentano il punto centrale dei programmi Java. Le classi hanno la funzione di contenitori logici per dati e codice e facilitano la creazione di oggetti che compongono l’applicazione.

Per completezza, il capitolo tratterà le caratteristiche del linguaggio necessarie a scrivere piccoli programmi, includendo la manipolazione di stringhe e la generazione di messaggi a video.

Incapsulamento

L'incapsulamento è il processo di nascondere i dettagli implementativi di un oggetto per proteggerne dati e funzionalità critiche. Questo approccio migliora la leggibilità del codice e limita la propagazione di errori.

Un'analogia comune è la leva del cambio dell'auto, che permette di cambiare marcia senza rivelare il funzionamento interno del cambio, proteggendo l'auto e l'autista.

In un design orientato agli oggetti, l'obiettivo è fornire dati e metodi che rappresentino l'oggetto senza esporre i dettagli interni, garantendo uno stato consistente, sebbene non sempre corrispondente alle aspettative dell'utente in caso di input errati.

Ereditarietà

L’ereditarietà è la caratteristica dei linguaggi Object Oriented che consente di utilizzare definizioni di classe come base per la definizione di nuove che ne specializzano il concetto.

L’ereditarietà offre inoltre un ottimo meccanismo per aggiungere funzionalità ad un programma con rischi minimi nei confronti di quelle già esistenti, nonché un modello concettuale che rende un programma Object Oriented auto-documentante rispetto ad un analogo scritto con linguaggi procedurali.

Per utilizzare correttamente l’ereditarietà, il programmatore deve conoscere a fondo gli strumenti forniti in supporto dal linguaggio. Questo capitolo introduce al concetto di ereditarietà in Java, alla sintassi per estendere classi, all’overloading e overriding di metodi.

Infine, in questo capitolo introdurremo ad una particolarità rilevante del linguaggio Java che include sempre la classe Object nella gerarchia delle classi definite dal programmatore.

Tipi di base

In questo capitolo esploreremo alcune classi e tipi essenziali per la programmazione in Java:

Classe Object

La classe madre di tutte le altre classi in Java. Grazie a essa, tutte le classi ereditano metodi essenziali che garantiscono il corretto funzionamento del linguaggio.

Classi Wrapper

Queste classi fungono da contenitori per tipi primitivi, permettendo una gestione più flessibile. Analizzeremo il loro funzionamento e come si sono evolute nel tempo.

Tipi Enumerati

I tipi enumerati rappresentano un insieme limitato di valori costanti, utili per definire stati predefiniti in modo chiaro e leggibile.

Stringhe

A differenza del C, dove le stringhe sono array di caratteri terminati da null, in Java le stringhe sono oggetti con tutte le caratteristiche di una classe.

La classe String, una delle più utilizzate, è immutabile e offre una sintassi semplificata per la creazione di istanze. Per operazioni più complesse esistono le sue versioni mutabili: StringBuilder e StringBuffer.

Le stringhe in Java beneficiano di una gestione efficiente della memoria grazie alla string pool, uno spazio nello heap che permette il riutilizzo delle istanze.

La classe String fornisce metodi per manipolazioni come concatenazioni, ricerche e sostituzioni.

Infine, vengono introdotti i text block, disponibili da Java 13, che semplificano la gestione di stringhe multilinea.

Eccezioni

Le eccezioni in Java vengono utilizzate per gestire condizioni anomale che interrompono il normale flusso di esecuzione di un programma, come malfunzionamenti hardware, errori di inizializzazione o divisioni per zero. Queste situazioni non possono essere gestite con i normali meccanismi di controllo del flusso.

Java cerca di prevenire alcuni errori già in fase di compilazione, ma non può gestire problemi complessi legati all'esecuzione, come l'inizializzazione non corretta dei costruttori. Le eccezioni forniscono un meccanismo flessibile per descrivere e gestire errori senza mescolare il codice applicativo con quello di gestione degli errori.

Ad esempio, nel caso di una classe Pila, il metodo push() potrebbe causare un trabocco, mentre pop() potrebbe restituire un valore arbitrario per segnalare un errore, introducendo ambiguità. L’uso delle eccezioni permette di descrivere e trattare in modo chiaro queste situazioni, semplificando il codice e migliorando la separazione delle responsabilità tra logica applicativa e gestione degli errori.

Polimorfismo di forma ed ereditarietà avanzata: interfacce

L'ereditarietà è un potente strumento di programmazione, ma il modello di ereditarietà singola presenta limiti e introduce potenziali problemi.

Uno dei limiti è l'impossibilità di utilizzare una classe base come modello puramente concettuale, privo di implementazioni. Ad esempio, una classe Pila per contenere dati richiederebbe riscrittura per supportare diversi tipi di dati, come interi o valori a virgola mobile.

Inoltre, il modello di ereditarietà singola in Java non consente l'ereditarietà multipla, poiché la parola chiave extends permette di derivare solo da una singola classe base.

Java affronta queste limitazioni con interfacce e classi astratte. Le interfacce definiscono contratti senza implementazioni, mentre le classi astratte permettono implementazioni parziali. Insieme, forniscono flessibilità per definire concetti generici e demandarne l'implementazione dettagliata alle classi derivate.

Programmazione dichiarativa: annotazioni

La programmazione dichiarativa è un paradigma in cui il programmatore si concentra sul definire cosa deve essere fatto, lasciando al sistema il compito di determinare come realizzarlo. Un esempio classico è SQL: attraverso una query, il programmatore dichiara il risultato desiderato, delegando al motore l'esecuzione.

Sebbene Java sia principalmente un linguaggio imperativo, ha incorporato principi dichiarativi per aumentare il suo potere espressivo. Un esempio significativo sono le annotazioni, introdotte con Java 5, che permettono di aggiungere metadati direttamente nel codice, migliorando la leggibilità, riducendo il codice boilerplate e offrendo controlli a compile-time.

In questa sezione esploreremo:

  • Cosa sono le annotazioni
  • Come dichiararle e utilizzarle
  • Le meta-annotazioni
  • Le annotazioni più comuni nel panorama Java

Generics Java

Abbiamo esplorato diverse forme di polimorfismo, ciascuna utile per rappresentare al meglio le entità di un'applicazione a oggetti:

  • Polimorfismo ad hoc: overloading dei metodi
  • Polimorfismo per inclusione: overriding dei metodi
  • Polimorfismo di forma: specializzazione da interfacce generiche

Tuttavia, queste forme si concentrano sulla specializzazione di classi, ma non affrontano la necessità di specializzazione di tipo. Per questo motivo, il polimorfismo di tipo diventa fondamentale, permettendo di rendere l'implementazione di una classe indipendente dal tipo tramite l'uso dei generics.

Sebbene Java disponga di un oggetto universale, Object, compatibile con tutti i tipi grazie all'ereditarietà, esso presenta limitazioni che possono causare malfunzionamenti ed effetti collaterali. In questa sezione vedremo come i generics risolvano questi problemi.

Programmazione funzionale

Il capitolo esplora la programmazione funzionale in Java, introdotta a partire da Java 8, evidenziando le sue caratteristiche principali.

Interfacce Funzionali e Lambda

Java supporta la programmazione funzionale tramite interfacce funzionali, definite con l'annotazione @FunctionalInterface. Queste interfacce contengono un solo metodo astratto e permettono al compilatore di rilevare errori se i requisiti non sono rispettati.

Le espressioni lambda, ispirate al lambda calcolo, offrono un modo conciso per definire implementazioni anonime di interfacce funzionali.

Method References

Le method references sono una forma semplificata di espressioni lambda, che consentono di riferirsi a metodi esistenti. Sono disponibili in quattro varianti principali.

Classe Optional

Java 8 introduce la classe java.util.Optional, che rappresenta un contenitore per un valore potenzialmente assente. Questo aiuta a gestire i valori null, offrendo un approccio più sicuro rispetto all'uso diretto di null.

Java Collections Framework

Le collezioni di oggetti sono essenziali nella programmazione, anche per problemi semplici. Possono avere comportamenti diversi: la dimensione può essere variabile, gli elementi potrebbero dover essere ordinati o unici, e i metodi di accesso possono variare (indicizzato, LIFO, FIFO o basato su chiavi).

Diverse tipologie di collezioni sono state sviluppate per bilanciare funzionalità e prestazioni. Alcune privilegiano la velocità di ricerca per collezioni statiche, altre ottimizzano l'inserimento per collezioni dinamiche.

Il framework delle collezioni Java è ricco di implementazioni e algoritmi, tali da richiedere una trattazione specifica. Questa sezione si concentra sulle implementazioni più comuni, mentre il supporto alla programmazione concorrente verrà approfondito nel capitolo sul multithreading.

La monade Stream - Java Stream API

Le monadi sono strutture che incapsulano un valore in un contesto, consentendo di operare su di esso tramite concatenazione di funzioni. Il risultato di una funzione diventa l'input della successiva, creando una pipeline di operazioni.

Vantaggi delle Monadi:

  • Riducono la duplicazione di codice.
  • Migliorano la manutenibilità e la leggibilità del codice.
  • Eliminano i side effect.
  • Nascondono la complessità implementativa.
  • Facilitano la composizione di funzioni.

Una delle caratteristiche più potenti delle monadi è la possibilità di creare pipeline, che consentono di:

  • Parallelizzare il flusso delle operazioni, aumentando il throughput.
  • Eseguire operazioni in modo asincrono, migliorando le prestazioni complessive.

Tuttavia, un blocco in una funzione può svuotare l'intera pipeline, un processo noto come pipelining.

A partire da Java 8, le monadi sono state integrate nel linguaggio attraverso implementazioni come gli Stream. Gli Stream permettono di lavorare con flussi di dati, concatenando metodi in pipeline tramite espressioni lambda.

Caratteristiche degli Stream:

  • Non sono strutture dati, ma flussi che usano collezioni, array o file come sorgenti.
  • Gestiscono automaticamente l'iterazione e il pipelining.

In questa sezione esploreremo le Java Stream API per:

  • Creare e utilizzare gli Stream.
  • Applicare diverse operazioni sui dati.
  • Implementare il parallelismo tramite parallel stream.

La monade Optional

Quando si combinano funzioni che accettano e restituiscono numeri interi, è comune dover gestire il caso in cui il risultato sia null. Questo spesso porta a inserire molte istruzioni if annidate per gestire i casi null.

Il Ruolo di Optional

La monade Optional è progettata per semplificare questa gestione. Trasformando le funzioni in modo che accettino e restituiscano oggetti Optional, è possibile combinare funzioni senza preoccuparsi di null. La gestione dei valori assenti viene delegata ad Optional.

Vantaggi di Optional

Il risultato è una sequenza semplice e lineare di funzioni, facile da leggere e comprendere. I problemi legati ai dati e alla loro gestione sono interamente delegati alla monade Optional.

Pipeline di Operazioni

Optional offre diversi metodi per eseguire operazioni sui dati in modo sequenziale, utilizzando un approccio a pipeline. Poiché gestisce tipi di dati disparati, il suo utilizzo è strettamente legato ai tipi generici, che verranno esplorati nei paragrafi successivi.

Java Threads

Java è un linguaggio multi-threaded, il che significa che un programma può essere eseguito logicamente in più punti contemporaneamente. Ogni parte di un programma multi-thread può gestire un'attività diversa, sfruttando al meglio le risorse disponibili, specialmente con CPU multi-core.

Multi-threading vs Multi-tasking

Il multi-tasking consente a più processi di condividere risorse comuni, come la CPU. Il multi-threading estende questa idea suddividendo le operazioni specifiche all'interno di un'applicazione in singoli thread. Ogni thread può essere eseguito in parallelo, e il sistema operativo gestisce la divisione del tempo di elaborazione tra le applicazioni e i thread.

Thread e Processi

Dal punto di vista dell'utente, i thread appaiono come processi paralleli. Per l'applicazione, i thread rappresentano unità logiche che condividono la stessa memoria dell'applicazione principale, ma competono per l'assegnazione della CPU.

Obiettivo del Capitolo

Questo capitolo descrive come programmare applicazioni multi-threaded in Java. Esploreremo il significato della programmazione concorrente, come scrivere applicazioni con questa tecnica e la differenza tra thread e processi.

Classloader Java

Il Java Classloader è una componente del Java Runtime Environment responsabile del caricamento dinamico delle classi richieste dalla Java Virtual Machine (JVM) per eseguire un'applicazione.

Grazie al classloader, il sistema runtime di Java non necessita di conoscere direttamente il filesystem. La responsabilità di reperire e caricare le classi necessarie è demandata al classloader. Le classi vengono caricate in memoria solo quando richiesto, un processo noto come dynamic loading and linking.

Il Java Runtime Environment utilizza diversi classloader per compiti specifici, e Java permette la creazione di classloader personalizzati in base alle esigenze applicative.

Gestione della memoria e Garbage Collector

Problemi di gestione della memoria in C++

In C++, la gestione della memoria è interamente responsabilità del programmatore. Questo può causare problemi come il consumo incontrollato di memoria e i memory leak, dove la memoria allocata non viene mai rilasciata, portando all'esaurimento delle risorse.

Il Garbage Collector in Java

Java adotta un approccio diverso, affidando la gestione della memoria al Garbage Collector (GC), un modulo della JVM. Il GC rimuove automaticamente dalla memoria gli oggetti non più utilizzati, ottimizzando l'uso della memoria e liberando il programmatore da questa responsabilità.

Memory Leak in Java

Sebbene il GC semplifichi la gestione della memoria, Java non è completamente leak-safe. Anche in Java, i memory leak possono verificarsi, ma con una corretta attenzione e l'uso di strumenti di profiling, è possibile identificarli e risolverli.

Diversi tipi di Garbage Collectors

Java offre diverse implementazioni di Garbage Collector, ognuna con strategie e performance specifiche per vari contesti di utilizzo. In questa sezione, analizzeremo il modello di gestione della memoria in Java e le differenti implementazioni del GC.

Se state leggendo questa sezione, significa che Java Mattone dopo Mattone vi è stato utile. Spero che vi abbia trasmesso l'amore per il buon codice e la voglia di migliorare continuamente.

Perché scrivere buon codice?

  • Migliora la produttività personale e professionale.
  • Rispetta il vincolo di professionalità verso la vostra azienda e i clienti.
  • Facilita la risoluzione dei problemi con strategie di problem solving efficaci.
  • Semplifica la manutenzione grazie a un codice leggibile e comprensibile.
  • Favorisce la collaborazione riducendo le incomprensioni, soprattutto nei progetti a lungo termine.

Riassumendo, applicate i principi condivisi nel libro e seguite questi consigli per scrivere un codice di qualità.