Massimiliano Tarquini
Esplora i contenuti del libro "Java Mattone dopo Mattone". Ogni capitolo tratta un argomento chiave del linguaggio Java.
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.
ApprofondisciJava è 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:
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
Abbiamo esplorato diverse forme di polimorfismo, ciascuna utile per rappresentare al meglio le entità di un'applicazione a oggetti:
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.
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
.
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.
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:
Una delle caratteristiche più potenti delle monadi è la possibilità di creare pipeline, che consentono di:
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:
In questa sezione esploreremo le Java Stream API per:
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 è 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.
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.
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?
Riassumendo, applicate i principi condivisi nel libro e seguite questi consigli per scrivere un codice di qualità.