Scarica Programmazione 3: Introduzione a Java e ai Design Pattern e più Appunti in PDF di Programmazione Java solo su Docsity!
EMILIO GARZIA
JAVA
Introduzione al linguaggio JAVA
e al paradigma OOP
SOMMARIO
PREFAZIONE
In questo manuale si vuole introdurre il concetto di programmazione
orientata agli oggetti , inoltre, verranno mostrate tutte le potenzialità del
linguaggio JAVA , partendo dalle basi di questo linguaggio di
programmazione, che fa della programmazione a oggetti il suo punto di
forza.
Q: QUALI CONOSCENZE SONO NECESSARIE ALLA COMPRENSIONE?
A: Per comprendere al massimo tutti gli argomenti che tratteremo in
questo manuale, è consigliata una conoscenza di base dei classici
linguaggi di programmazione procedurali , come, per esempio, il
linguaggio C , anche perché, il JAVA, è fortemente influenzato da
quest’ultimo.
LINGUAGGIO DI PROGRAMMAZIONE (RE-CAP)
In questo primo capitolo andiamo a ricordare cosa s’intende per linguaggio di programmazione ,
nello specifico, si definiscono linguaggi di programmazione, tutti i linguaggi turing completi ,
ovvero, tutti quei linguaggi che sono in grado di risolvere i problemi che una macchina di Turing è
in grado di risolvere.
Inoltre, il codice sorgente di un linguaggio di programmazione deve essere dapprima compilato ,
ovvero, tradotto in linguaggio macchina , successivamente il programma è pronto a girare sul
computer.
Volendo fare un esempio gli script in BASH che vengono eseguiti sui sistemi UNIX-Like non sono
scritti con un linguaggio di programmazione, bensì, si definiscono linguaggi di scripting , questo
perché un codice sorgente di un linguaggio di programmazione viene compilato , mentre, uno
script viene interpretato da un interprete , quindi, non compilato.
PARADIGMI DI PROGRAMMAZIONE
Il linguaggio di programmazione in se rappresenta uno strumento con il quale possiamo
implementare algoritmi che risolvono problemi, tutto ciò però non basta, per questo motivo, si
sono sviluppati insieme ai linguaggi di programmazione vari paradigmi di programmazione , con il
termine paradigma di programmazione, intendiamo definire il livello di astrazione del linguaggio,
in altre parole, il paradigma definisce l’insieme di tutti gli strumenti concettuali su cui il linguaggio
si basa, vedremo successivamente vari esempi di paradigmi di programmazione e relativi linguaggi.
BASSO LIVELLO VS ALTO LIVELLO
I linguaggi di programmazione si suddividono in due grandi famiglie, abbiamo, infatti, i linguaggi di
ad alto livello e quelli a basso livello , diversamente da come ci si può aspettare queste due
proprietà non descrivono la qualità o la raffinatezza del linguaggio, piuttosto:
- BASSO LIVELLO → I linguaggi a basso livello sono tutti quei linguaggi che sono molto
dipendenti dallo Hardware , per questo motivo si dicono di basso livello, perché
comunicano direttamente con le componenti hardware, infatti, questi linguaggi si
utilizzano per la programmazione di sensori e altre componenti hardware.
ESEMPI: Un esempio di linguaggi a basso livello sono Assembly o anche il C , quest’ultimo
non è proprio di basso livello, ma, è considerato tale in quanto dipende molto dallo
hardware, per questo motivo per la programmazione dei controllori Arduino si utilizza il
linguaggio Wiring , derivazione del linguaggio Processing , a sua volta derivato del C.
- ALTO LIVELLO → Questa proprietà appartiene a tutti quei linguaggi con un elevata
astrazione, ovvero, fortemente indipendenti dallo hardware, per questo motivo considerati
più potenti, anche per una questione di portabilità del codice, poiché il codice non dipende
dal linguaggio macchina.
ESEMPI: Moltissimi dei linguaggi più moderni sono ad alto livello, a questo insieme di
linguaggi appartiene anche il JAVA.
PARADIGMI FONDAMENTALI DELLA PROGRAMMAZIONE OO
Abbiamo visto in maniera molto blanda e rapida il significato dei principali protagonisti di un
codice scritto con un linguaggio OO, ovvero:
Possiamo dire che un programma scritto con linguaggi OO è un insieme di oggetti che
interagiscono tra loro, scambiandosi messaggi, in modo tale da raggiungere uno scopo comune, lo
stato di ogni oggetto è consultabile in ogni momento mediante i propri attributi definiti a loro
volta all’interno della classe a cui appartiene l’oggetto.
Tutto ciò non basta a distinguere un linguaggio OO da un classico linguaggio imperativo , infatti, le
vere peculiarità di un linguaggio OO risiedono nei paradigmi che implementa, di seguito i tre
paradigmi della programmazione OO.
INCAPSULAMENTO
L’ incapsulamento prevede che tutto ciò che riguarda un oggetto deve essere necessariamente
definito al suo interno, colui che utilizza l’oggetto, infatti, può alterare il valore degli attributi di
quell’oggetto solo mediante il richiamo dei metodi , è da evitare assolutamente l’accesso diretto
agli attributi dell’oggetto.
Nell’esempio di cui prima, dove avevamo la classe “ AUTOMOBILE ”, se volessimo cambiare il colore
all’oggetto “FIAT Punto EVO”, questo lo dobbiamo fare mediante un metodo, per esempio, un
metodo chiamato setColore(String colore) , metodi che ricordiamo essere definiti nella
classe di appartenenza dell’oggetto.
L’incapsulamento è estremamente utile per svariati motivi, innanzitutto, si nascondono il più
possibile i dettagli relativi all’implementazione, inoltre, la gestione dei vari oggetti è molto più
semplice, in quanto il programmatore che utilizzerà i vari oggetti può concentrarsi pienamente
sugli oggetti senza dover impiegare tempo in ulteriori implementazioni, poiché la gestione di
questi oggetti è data dai metodi, aggiungiamo tra i vari vantaggi dell’incapsulamento anche quello
di rendere il codice molto più discorsivo, pulito ed elegante.
RAPPRESENTAZIONE GRAFICA DI INCAPSULAMENTO
NOME CLASSE ATTRIBUTI METODI PER INCAPSULAMENTO
Come si evince dall’immagine affianco, sono
stati creati metodi ad hoc che ci consentono
di modificare o leggere il valore degli attributi
di un oggetto, questi metodi prendono il
nome convenzionale di setter e getter.
È importante che per ogni attributo della
classe vi sia un getter e un setter , anche nel
caso in cui ci sembra non necessario.
EREDITARIETÁ
L’ereditarietà è un altro paradigma che ci viene garantito dai linguaggi OO, quando parliamo di
ereditarietà intendiamo quella proprietà che consiste nell’avere una classe padre, detta
superclasse , questa superclasse genera una classe figlia che eredita dalla superclasse tutti gli
attributi e i metodi (metodo costruttore escluso) , con la possibilità di aggiungerne altri.
Poniamo caso di avere l’esigenza di creare nel nostro progetto due classi, “ Automobile ” e
“ AutomobileAssicurata ”, senza conoscere il paradigma dell’ereditarietà creeremo due classi
distinte che avranno in comune molti metodi e attributi, piuttosto, estendiamo “Automobile” e
generiamo una sua classe figlia “ AutomobileAssicurata ” che eredita tutti i metodi e attributi,
aggiungendo a questi tutti gli attributi e metodi aggiuntivi che ci consentono di caratterizzare gli
oggetti della classe figlia.
Si evince fin da subito l’enorme potenziale di questa funzionalità, in quanto, non dobbiamo ogni
volta stare lì a riscrivere identiche porzioni di codice, vedremo nei capitoli successivi come è
possibili applicare questo paradigma nel Java con la keyword extends.
POLIMORFISMO
Abbiamo stabilito che con l’ereditarietà le classi figlie ereditano anche i metodi della superclasse ,
questi metodi avranno, quindi, gli stessi nomi nelle classi figlie, inoltre, anche se i metodi hanno lo
stesso nome e la stessa signature , possiamo ridefinirli ad hoc per quella determinata classe figlia,
così facendo avremo un metodo che ha molti nomi ma che si comporta in modo diverso in basi a
quale oggetto lo invoca, questo fenomeno prende il nome di polimorfismo.
Nel grafico di cui sopra abbiamo una superclasse “ Strumento ” che caratterizza gli strumenti
musicali in generale attraverso attributi e metodi che sono comuni a tutti gli strumenti, a questo
punto possiamo generare 𝒏 classi figlie di “ Strumento ” così da non dover ridefinire tutti gli
attributi e metodi per ogni strumento, per quanto riguarda il fenomeno polimorfo, lo si può
individuare dal fatto che il metodo “ suona() ” è definito nella superclasse ed ereditato nelle classi
figlie, ovviamente è possibile alterare il contenuto del metodo “ suona() ” qualora un particolare
strumento si suoni in un modo particolarmente diverso dagli altri, questa pratica di alterare il
contenuto di un metodo per una classe figlia prende il nome di overriding.
Override e Overload rappresentano il polimorfismo per metodi, ma, il principale esponente di
polimorfismo è quello per dati, infatti, è possibile dichiarare un oggetto di un tipo A e istanziarlo
come oggetto di tipo B , dove B è una sottoclasse di A , nel nostro esempio di “ Strumento ”:
Strumento prova = new Chitarra(); : Qualora invocassimo il metodo “ prova.suona() ”, verrà richiamato il metodo “ suona() ” di “ Chitarra ”. STRUMENTO suona() CHITARRA suona() PIANOFORTE suona()
JAVA DA LINEA DI COMANDO
La scrittura di un programma Java, soprattutto se per progetti di dimensioni elevate, è consigliata
mediante l’utilizzo di un IDE dedicato, vedremo però in questo paragrafo che è possibile compilare
il proprio sorgente mediante il JDK (Java Development Kit) , infatti, un IDE è consigliato ma non
obbligatorio, possiamo tranquillamente scrivere il nostro codice Java con un semplice editor di
testo (il file dovrà essere poi salvato come *.java ) e successivamente compilarlo con il JDK, anche
da linea di comando.
Di seguito andiamo ad introdurre quelli che sono i comandi di base per la gestione del JDK da
terminale:
- javac file.java → Questo comando ci consente di compilare il nostro sorgente Java,
dopo aver eseguito questo comando ci ritroveremo nella cartella del progetto un nuovo file
denominato file.class , che altri non è che il nostro bytecode.
- java file → Con questo comando si va ad eseguire il bytecode, non è necessario
specificare l’estensione ( .class ), ma è a quella che si riferisce il comando quando esegue
il programma.
Al comando in questione è possibile applicare molti overall che alterano l’output del
comando:
o - -nowite : Compilazione senza creare la classe.
o - nowarm : Compilazione senza visualizzare i warning.
o - verbose : Compilazione con visualizzazione delle informazioni delle sorgenti.
o - d dir : La directory dir è la prima directory per i package.
o - debug : Il compilatore viene eseguito in modalità debug.
o - g : Prepara il bytecode per il debug.
- javap filenames → Ci consente di diassemblare il bytecode compilato e vederne
“approssimativamente” il corrispettivo assembly del sorgente.
- javah filenames → Crea header file in C che estendono il codice Java.
- javadoc file.java → Crea dei file HTML che contengono la documentazione delle
classi o dei metodi, attenzione, il contenuto della documentazione viene estratto dai
commenti contenuti nel sorgente, ma, non ci si riferisce ai commenti classici, bensì, a dei
commenti inseriti nel sorgente utilizzando degli operatori in particolare che vedremo poi.
In altre parole, possiamo definire il Java come un sistema composto da tre componenti principali:
- JDK (Java Development Kit) → È il tool di sviluppo che ci consente di compilare i nostri
sorgenti Java in sorgenti bytecode.
- JRE (Java Runtime Enviroment) → Questa componente ci consente di lanciare il sorgente
bytecode, compilato con JDK.
- JVM (Java Virtual Machine) → È quella componente del sistema Java che materialmente
esegue il bytecode.
: Tutti questi comandi da linea di comandi sono stati menzionati per completezza, ma, non
useremo mai il terminale per gestire i nostri sorgenti Java, piuttosto, ci affideremo ad un IDE.
“HELLO WORLD!” IN JAVA
Cominciamo, in questo capitolo, ad addentrarci nella sintassi del Java scrivendo il nostro primo
programma, nello specifico andremo ad implementare il classico “HELLO WORLD!”.
STEP 1: SCRIVIAMO IL CODICE
Dato che dobbiamo implementare un semplice programma possiamo scrivere il nostro codice
sorgente in un semplice editor di testo, si consiglia però l’utilizzo di un IDE dedicato qualora
volessimo scrivere programmi più complessi.
Dunque, una volta aperto il text editor scriviamo il seguente codice rispettando alla lettera ogni
singola virgola e salviamo il file con lo stesso nome della classe pubblica seguito dall’estensione
java, nel nostro caso il file sorgente si chiamerà “HelloWorld.java”.
public class HelloWorld{ public static void main(String[] args){ System.out.println("Hello World!"); } }
STEP 2: COMPILIAMO IL CODICE
Possiamo ora aprire il terminale e generare il bytecode richiamando il compilatore di java,
inserendo il comando:
javac HelloWorld.java
Una volta eseguito il comando verrà generato un nuovo file nella directory del progetto, il file in
questione si chiamerà “HelloWorld.class”, ovvero, il file bytecode.
STEP 3: ESEGUIAMO IL PROGRAMMA
Ora che abbiamo il sorgente bytecode non ci resta che eseguirlo sulla Java Virtual Machine con il
comando:
java HelloWorld (non va specificato, ma, si riferisce al file *.class).
Se tutto andasse a buon fine, sul terminale dovrebbe comparire la stringa “ HELLO WORLD! ” in
output.
JAVA: NOZIONI E STRUMENTI DI BASE
In questo capitolo ci concentriamo sugli strumenti di base del Java, molte degli argomenti che
tratteremo in questo capitolo sono informazioni basilari che ci consentono di prendere confidenza
con la sintassi del linguaggio Java, qualora si conoscesse già il linguaggio C o C++ la comprensione
di questi argomenti risulterà estremamente semplice, poiché come vedremo la sintassi del Java è
fortemente influenzata dal C.
TIPI DI DATI NEL JAVA
I tipi di dati primitivi nel Java non si discostano molto da quelli già noti in ambiente C , comunque
sia andremo comunque a vederli, come vedremo saranno poche le differenze dal C.
- int → Le variabili di questo tipo contengono un valore intero , ovvero, un valore numerico
senza virgola, una variabile integer pesa 𝟒 𝒃𝒚𝒕𝒆𝒔 ( 32 𝑏𝑖𝑡).
- short → Le variabili short sono identiche a quelle integer , con la sola differenza che le
short pesano la metà, ovvero, 𝟐 𝒃𝒚𝒕𝒆𝒔 ( 16 𝑏𝑖𝑡).
- long → Anche questa tipologia di dato è destinata ad accogliere un valore intero, ma,
abbiamo a disposizione un intervallo numerico molto più ampio, dato che il peso di
quest’ultimi è di 𝟖 𝒃𝒚𝒕𝒆𝒔 ( 64 𝑏𝑖𝑡).
- float → Le variabili di tipo float sono quelle che ci consentono l’utilizzo dei valori a
virgola mobile, il peso delle variabili float è 𝟒 𝒃𝒚𝒕𝒆𝒔 ( 32 𝑏𝑖𝑡), bisogna specificare che
quando si vogliono utilizzare numeri float è necessario continuare il valore scelto con il
carattere “ f ” poiché di default Java tratta i numeri a virgola mobile come dati di tipo
double.
- double → Hanno la stessa funzionalità dei float , ma, con il doppio dei valori possibili, in
quanto il loro peso è 𝟖 𝒃𝒚𝒕𝒆𝒔 ( 64 𝑏𝑖𝑡).
- boolean → Variabile alla quale è possibile assegnare solo due valori, ovvero, true o false.
- char → Proprio come nel C , anche in questo caso le variabili di tipo char contengono un
carattere alfabetico, a differenza del C che utilizza una codifica ASCII , in questo caso
abbiamo a disposizione 𝟐 𝒃𝒚𝒕𝒆𝒔 ( 16 𝑏𝑖𝑡), poiché il Java appoggia la codifica UNICODE.
- byte → Nel C utilizzavamo il tipo char per memorizzare valori di 𝟏 𝒃𝒚𝒕𝒆𝒔 ( 8 𝑏𝑖𝑡), in
questo caso non possiamo più farlo, dato che i char pesano 𝟐 𝒃𝒚𝒕𝒆𝒔 ( 16 𝑏𝑖𝑡) nel Java, per
questo motivo si utilizza il tipo di dato byte che per l’appunto serve a memorizzare un
valore intero piccolo, più nello specifico, un valore compreso nell’intervallo [− 128 , + 127 ].
byte a = 127 ; short b = 500 ; int c = 10000 ; long d = 1000000000 ; float f = 12.56f; double g = 10.15; char carattere = 'f'; boolean check = true;
: Per quanto riguarda le stringhe di caratteri, vedremo più avanti come sono gestite nel Java.
CODIFICHE SUPPORTATE
I dati che trattiamo possiamo esprimerli nelle variabili con svariate codifiche, questo vale per i
valori interi , virgola mobile e letterali.
CODIFICHE PER I VALORI INTERI
int numeroDecimale = 10 ; int numeroBinario = 0b1010; int numeroOttale = 0012 int numeroHex = 0xA;
È possibile anche utilizzare una sintassi che sfrutta gli underscore per facilitare a noi umani la
lettura di un valore particolarmente grande, per esempio, se ho una variabile e le devo assegnare
il valore di un milione posso scriverla come segue:
int milioneClassico = 1000000 ; int milioneUnderscore = 1_000_000; int binarioUnderscore = 0b0110_1011_0110;
CODIFICHE PER I VALORI A VIRGOLA MOBILE
Con i numeri a virgola mobile ci è possibile esprimere i valori con notazione scientifica , proprio
come avveniva nel C.
double numero = 1.26E- 2 ; //equivale a 1.26 diviso 100
CODIFICHE PER I VALORI LETTERALI
Ricordiamo che il sistema Java utilizza una codifica UNICODE , dunque, abbiamo a disposizione un
alfabeto molto più ampio di quello ASCII messoci a disposizione dal C , ricordiamo che anche nel
Java i singoli caratteri vanno contenuti tra singoli apici, mentre, le stringhe tra doppi apici.
char carattere = '@'; char unicodeVariant = '\u00b5'; //corrisponde al carattere μ
Vanno menzionati anche i caratteri speciali elencati qui di seguito:
- \n → Carattere speciale per il new line , ovvero, ci consente di andare a capo.
- \t → Inserisce una tabulazione.
- \ → Serve ad inserire un solo backslash “ ** ”.
- \’ → Ci consente di inserire un singolo apice.
- \” → Inserisce un doppio apice.
VALORI COSTANTI
Per quanto riguarda le costanti, utilizziamo semplicemente la keyword final :
final double PI_GRECO = 3.14;
CLASSI E OGGETTI
Abbiamo già trattato abbondantemente il concetto di classe e oggetto , ritorniamo sull’argomento
in quanto questi concetti sono la base fondante del linguaggio Java, in questo paragrafo andremo
ad implementare una classe con annessi attributi e metodi.
Innanzitutto, creiamo una nuova classe che chiamiamo “ Persona ”
public class Persona{ ... }
Ora che abbiamo la nostra classe non ci resta che “farcirla” con tutti i membri che la definiscono,
con il termine membro s’intendono tutti gli attributi e metodi.
public class Persona{ //Attributi private String nome; private int eta; //Metodo Costruttore public Persona(){} //metodo costruttore di default public Persona(String nome, int eta){ this.nome = nome; this.eta = eta; } //Metodi getter e setter per incapsulamento public void setNome(String nome){ this.nome = nome; } public void setEta(int eta){ this.eta = eta; } public int getEta(){ return this.eta; } public String getNome(){ return this.nome; } }
Abbiamo inserito due attributi alla classe e tutti i metodi getter e setter che ci consentono di
implementare il paradigma dell’ incapsulamento.
Ora che abbiamo la nostra classe pronta non ci resta che istanziare un oggetto con new , in questo
caso istanziamo un oggetto della classe “Persona” nel main.
public class Esercizio{ public static void main(String[] args){ Persona individuo1 = new Persona(); //metodo costruttore default Persona individuo2 = new Persona("Emilio", 27 ); //costruttore con parametri System.out.println("Nome Individuo 1: " + individuo1.getNome()); System.out.println("Nome Individuo 2: " + individuo2.getNome()); } }
: Ricordiamo che ogni classe va salvata in un proprio file che ha lo stesso nome della classe
stessa, seguito dall’estensione “*.java”.
METODI
I metodi possono essere visti come delle normali funzioni definite all’interno di una classe che ci
consentono di eseguire azioni sugli oggetti istanziati per quella classe.
Nell’esempio “ Persona ” di cui sopra abbiamo implementato due tipologie di metodi:
- Metodo Costruttore → Il metodo costruttore è un particolare metodo che ci consente di
istanziare ed inizializzare un oggetto della relativa classe, nello specifico esistono diverse
tipologie di metodi costruttori, in questo paragrafo ne tratteremo solo due:
o Metodo Costruttore con Parametri : Ci consente di inizializzare l’oggetto appena
istanziato con dei parametri iniziali.
o Metodo Costruttore di Default : Questo costruttore serve a istanziare un oggetto
senza inizializzarlo, se non creiamo un metodo con parametri non abbiamo bisogno
di implementare quello default, poiché il Java implementa automaticamente quello
di default se non ne trova uno definito da noi.
- Metodi Getter e Setter → I getter e i setter sono metodi che semplicemente ci consentono
di ritornarci o modificare il valore degli attributi della classe, dato che abbiamo dichiarato
tutti gli attributi come private (visibili solo all’interno della loro classe) , con questa tecnica
si garantisce l’implementazione dell’incapsulamento.
Ovviamente, possiamo implementare infiniti metodi all’interno della nostra classe, per
semplicità nei nostri esempi sono presenti solo costruttori , getter e setter.
METODO EQUALS
In Java l’operatore " == " non ci consente di esprimere l’uguaglianza tra due oggetti, per questo
motivo ogni classe delle librerie standard del Java è fornita da un metodo chiamato “ equals ”,
questo metodo ci consente di eseguire un’operazione logica che ritorni true qualora i due oggetti
siano uguali, altrimenti false.
Dato che tutti i programmatori per convenzione implementano questo metodo all’interno delle
loro classi, non possiamo astenerci dall’adottare anche noi questo standard, nel nostro caso
l’implementazione del metodo equals per la classe “ Persona ” sarebbe:
public boolean equals(Persona x){ if(this.nome.equals(x.getNome()) && this.eta == x.getEta()) return true; return false; }
Ovviamente due oggetti sono uguali quando tutti gli attributi (che descrivono lo stato di un
oggetto) sono uguali, dunque, nel metodo non dobbiamo fare altro che accertarci che tutti gli
attributi del nostro oggetto siano uguali ad un altro oggetto passato come input al metodo
“ equals ” appena creato.
: Si noti come per il confronto di uguaglianza dell’attributo “ nome ” sia stato utilizzato “ equals ”,
questo perché l’attributo “nome” è di tipo string , come vedremo poi, le stringhe nel Java sono
oggetti appartenenti alla classe String e quell’equals nel confronto si riferisce all’equals della
classe String.