Corso JAVA

Corso linguaggio Java dalle basi - appunti per l'insegnamento

Lo scopo di questo tutorial vuole essere un tour per imparare le basi del linguaggio Java. E' una raccolta di fonti spero organizzata in modo utile e in parte di esercizi da me proposti durante due anni di esperienza didattica.

La cosa più utile è armarsi di pazienza e molta buona volontà (e non preoccuparsi se non si capisce tutto subito, è normale).

Quando si incomincia un viaggio, è bene avere una mappa mentale del territorio che si attraverserà. Così è importante anche nel caso di un viaggio intellettuale, come imparare a scrivere un programma.

In questo caso è importante avere una minima idea di cosa è un computer e di come funziona.

Il ciclo Fetch e Execute: il linguaggio macchina

Il processore è l'elemento attivo che opera eseguendo le istruzioni in linguaggio macchina che sono contenute nella memoria.

La memoria è costituita da una sequenza di celle o parole, ognuna delle quali è capace di contenere un certo numero di bit. Tale numero è detto lunghezza della parola. Ogni parola è identificata da un numero detto indirizzo.

Il processore contiene al proprio interno dei registri, capaci anch'essi di memorizzare un certo numero di bit come le celle di memoria. Il numero e il tipo dei registri posseduto varia da un tipo ad un altro di processore, ma praticamente tutti i processori possiedono un registro particolare detto contatore di programma o PC (Program Counter), che è destinato a contenere l'indirizzo della parola di memoria che contiene la prossima istruzione da eseguire.

Il processore opera eseguendo un ciclo fondamentale di interpretazione delle istruzioni, che può essere schematizzato nel modo seguente:

  1. legge in memoria l'istruzione contenuta all'indirizzo contenuto nel registro PC;

  2. interpreta l'istruzione letta e la esegue;

  3. determina il nuovo valore di PC e torna al passo 1.

E' importante tenere presenti alcune osservazioni rispetto a questo ciclo:

  • all'inizio (cioè all'avviamento del processore) il registro PC deve contenere l'indirizzo della prima istruzione eseguibile dal programma;

  • durante l'interpretazione dell'istruzione il processore può accedere a celle di memoria che contengono gli operandi; la memoria contiene quindi sia le istruzioni che gli operandi del programma;

  • nel determinare il nuovo valore del PC normalmente il processore suppone che la prossima istruzione sia contenuta nella cella successiva di memoria, quindi si limita ad incrementare il valore del PC stesso; però se l'istruzione appena eseguita è di salto, allora il prossimo valore del PC viene determinato dall'istruzione stessa.

Le istruzioni che possono essere eseguite sono molto elementari, ad esempio:

  • trasferisci il contenuto di una cella di memoria in un registro o viceversa;

  • somma il contenuto di due registri (o analogamente altre operazioni aritmetiche o logiche);

  • salta all'esecuzione di un'istruzione posta a un certo indirizzo (questa istruzione modifica il registro PC);

  • come sopra, ma il salto deve essere eseguito solamente se vale una certa condizione (salto condizionato); una tipica condizione è che un certo registro abbia un contenuto uguale (oppure maggiore o minore) di zero;

Le istruzioni di salto (condizionato e non) sono di fondamentale importanza per realizzare in linguaggio macchina i costrutti di controllo (cicli, if-then, ecc...) di un linguaggio di programmazione. Si tenga presente che, anche se la maggior parte dei processori possiede un numero elevato di istruzioni, un linguaggio macchina completo, nel senso di un linguaggio sufficiente per realizzare tutte le funzioni richieste dalla traduzione di qualsiasi linguaggio di alto livello, può essere costituito da 5 o 6 istruzioni solamente, di cui almeno una deve essere di salto condizionato. Moltissime operazioni possono essere infatti realizzate utilizzando istruzioni più elementari (ad esempio la sottrazione tramite la negazione e somma, la moltiplicazione tramite somme ripetute, ecc...).

I diversi processori differiscono moltissimo in base al tipo di istruzioni che possono eseguire, ma possiamo affermare che tutti i processori operano secondo lo schema illustrato sopra e che i tipi di istruzione che eseguono sono sufficienti per svolgere tutte le funzioni richieste da un programma eseguibile ottenuto dalla traduzione (compilazione e collegamento) di un programma qualsiasi scritto in un linguaggio di programmazione di alto livello.

Eventi asincroni: Polling loops e Interrupts

La CPU utilizza quasi tutto il tempo a caricare le istruzioni dalla memoria ed ad eseguirle. Tuttavia la CPU e la memoria principale sono solo due dei tanti componenti in un computer reale. Un sistema completo contiene altri device (periferiche) come:

  • Un hard disk o disco allo stato solido per immagazzinare programmi e file. (La memoria principale contiene solo una piccola parte di informazioni in paragone e le immagazzina solo fino a quando il sistema è acceso. Un disco fisso o un disco allo stato solido è utilizzato per l'immagazzinamento permanente di grosse quantità di informazioni, ma i programmi devono essere caricati nella memoria principale prima di essere eseguiti. Un hard disk immagazzina i dati su un disco magnetico, mentre un disco allo stato solido è un device puramente elettronico senza parti in movimento.)

  • una tastiera o un mouse per input dell'utente;

  • un monitor o una stampante per mostrare l'output di un computer;

  • un device audio per permettere al computer di emettere suoni;

  • una network interface (interfaccia di rete) che permette a un computer di comunicare con altri computer che sono connessi in rete, sia wireless o con cavi;

  • uno scanner che converte immagini in insieme di byte che possono essere immagazzinati e modificati dal computer.

La lista dei device è aperta, un computer è costruito in modo da essere facilmente espandibile aggiungendo altri device. In qualche modo la CPU deve comunicare e controllare questi device. La CPU può solo fare questo eseguendo delle istruzioni in linguaggio macchina (la CPU sa fare solo questo). Il modo in cui funziona è che per ogni device nel sistema, c'è un device driver (detto anche solo driver), che è un software che la CPU esegue quando deve interagire con il driver. Installare un nuovo device su un sistema generalmente consiste in due step: aggiungere fisicamente il device al computer, e installare il device driver. Senza il driver, il device fisico sarebbe inutilizzabile, perché la CPU non sarebbe in grado di comunicare con esso.

Un computer in quanto ha molti device è organizzato collegando questi device da uno o più bus. Un bus è un insieme di cavi che trasportano diversi tipi di informazioni tra i device connessi da questi cavi. I cavi portano dati, indirizzi e segnali di controllo (comandi). L' indirizzo serve per dirigere il dato a un device particolare o a un registro particolare del device. Il segnale di controllo potrebbe servire per dare comandi al device. Un semplice esempio di computer potrebbe essere:

Ora, device come periferiche, mouse e network interface possono produrre input che devono essere processati dalla CPU. Come fa la CPU a sapere che ci sono dei dati? Un modo semplice, ma non molto soddisfacente, potrebbe essere quello di continuare al controllare da parte della CPU se sono arrivati nuovi dati. Fino a quando non trova il dato e lo processa. Questo metodo si chiama polling, perché la CPU fa il 'polls' (sondaggio) del device di input continuamente per vedere se hanno dei dati di input arrivati. Sfortunatamente sebbene il polling sia molto semplice, è anche molto inefficiente. La CPU può sprecare inutilmente tanto tempo aspettando che arrivi l'input.

Per evitare questa inefficienza, gli interrupt sono generalmente utilizzati. Un interrupt è un segnale inviato dal device alla CPU. La CPU risponde a un segnale di interrupt, mettendo da parte quello che stava facendo in modo da rispondere immediatamente all'interrupt. Una volta che ha gestito l'interrupt, cioè ha fatto tutte le azioni necessarie che richiedeva la periferica che ha mandato il segnale di interrupt, solo allora ritorna a fare quello che stava facendo prima che l'interrupt accadesse. Ad esempio quando voi pigiate un tasto della tastiera del computer, un interrupt della tastiera è inviato alla CPU. La CPU risponde a questo interrupt interrompendo cosa stava facendo, leggendo il tasto che abbiamo premuto, processandolo, a poi ritornando al task (compito) che stava compiendo prima che avessimo pigiato il tasto.

E' da capire che questo è un processo puramente meccanico: Un device segnala un interrupt semplicemente inviano un impulso elettrico su un cavo. La CPU è costruita in modo, che quando arriva il segnale, la CPU salvi abbastanza informazioni di quello che stava facendo, così da poter tornare poi a riprendere quello che stava facendo. Questa informazione consiste nel contenuto di alcuni registri importanti della CPU come il program counter. Allora la CPU salta in alcuni zone di memoria predeterminate e comincia ad eseguire le istruzioni li immagazzinate. Queste istruzione fanno parte dell' interrupt handler (codice che gestisce l'interrupt) che fa le operazioni per rispondere all'interrupt. (Questo interrupt handler è parte del software del device driver della periferica che ha inviato l'interrupt). Al termine del interrupt handler c'è un istruzione che dice alla CPU di tornare a cosa stava facendo; lo fa ripristinando lo stato precedentemente salvato.

Gli interrupt permettono alla CPU di gestire eventi asincroni. Nel ciclo normale fetch-and-execute (carica l'istruzione ed eseguila), le cose avvengono nell'ordine predeterminato; ogni cosa che accade è "sincronizzata" con tutte le altre. Il meccanismo degli interrupt permette alla CPU di gestire in modo efficiente eventi che accadono in modo "asincrono", cioè, in momenti non predefiniti.

Un altro esempio di come gli interrupt sono usati, consideriamo cosa succede quando la CPU ha bisogno di accedere a dati che sono immagazzinati sull'hard disk. La CPU può accedere ai dati direttamente solo se sono nella memoria principale. I dati sul disco devono essere copiati nella memoria principale prima che possano essere acceduti. Sfortunatamente, rispetto alla scala di velocità con cui la CPU opera, l'hard disk è molto lento. Quando la CPU ha bisogno di dati dal disco, la CPU invia un segnale all'hard disk dicendo di prendere i dati e prepararli. (Il segnale è inviato in modo sincrono, sotto il controllo di un programma normale) A questo punto, invece di aspettare per un tempo indefinito e lungo (rispetto ai tempi della CPU) che l'hard disk completi l'operazione, la CPU va avanti ed esegue qualche altro task (compito - programma). Quando l'hard disk ha i dati pronti, esso invia un segnale di interrupt alla CPU. L'interrupt handler può a questo punto leggere i dati richiesti.

******************

Si potrebbe notare che tutto ciò a senso se la CPU ha diversi task (compiti) da svolgere. Se non avesse niente di meglio da fare, essa potrebbe utilizzare il tempo a fare polling per aspettare l'input o aspettare che si completino le operazioni sul disco. Tutti i moderni computer sono multitasking per completare molti task contemporaneamente. Alcuni computer possono essere utilizzati da più persone contemporaneamente. Visto che la CPU è così veloce, può servire velocemente diversi utenti, riservando poche frazioni di secondo a ogni utente alternativamente. Questa applicazione del multitasking è chiamata time sharing. Ma i moderni personal computer con anche solo un singolo utente utilizzano il multitasking. Per esempio, l'utente potrebbe essere impegnato a scrivere un documento mentre l'orologio continua a mostrare l'ora e un file è in fase di download.

Ognuno dei task (compiti) individuali su cui la CPU sta lavorando è chiamata un thread. (O un processo; ci sono delle differenze tecniche tra i thread e i processi, ma non è fondamentale spiegarle qui, dato che i thread sono la modalità utilizzata da Java). Molte CPU possono eseguire letteralmente più di un thread contemporaneamente (in parallelo) - queste CPU contengono più "cores", ognuno dei quali può eseguire un thread, ma c'è sempre un limite sul numero dei thread che possono essere eseguiti contemporaneamente. Siccome ci sono spesso più thread di quelli che possono essere eseguiti simultaneamente, il computer deve avere la possibilità di spostare l'esecuzione da un thread a un altro, come per il timesharing il computer sposta la sua attenzione da un utente a un altro. In genere, un thread che è in esecuzione continuerà finché una di queste situazioni non accade:

  • Il thread può volontariamente yield (cedere) il controllo, per dare a un altro thread la possibilità di essere eseguito;

  • Un thread potrebbe stare ad aspettare che qualche evento asincrono accada. Per esempio potrebbe richiedere qualche dato dal disco, o potrebbe aspettare che l'utente digiti qualche tasto sulla tastiera. Mentre sta aspettando, il thread è bloccato, e altri thread, se ci sono, hanno la possibilità di essere eseguiti. Quando l'evento di input/output avviene (il dato da disco è pronto o l'utente ha premuto un tasto), un interrupt "sblocherà" il thread così che potrà riprendere l'esecuzione;

  • Il thread potrebbe aver utilizzato tutto il suo slice di tempo e quindi essere sospeso per permettere ad altri thread di essere eseguiti. Molti sistemi operativi possono forzatamente sospendere un thread in questo modo; sistemi operativi che possono fare questo sono detti utilizzare il preentive multitasking. Per fare il preentive multitask, il computer necessita di uno speciale timer che genera un interrupt a intervalli regolari, tipo 100 al secondo. Quando l'interrupt del timer avviene, la CPU ha la possibilità di fare lo switch da un thread a un altro, indipendentemente dalla volontà del thread che sta attualmente in esecuzione. Tutti i moderni computer, desktop o laptop, anche gli smartphone e tablet, utilizzano il preentive multitasking.

Gli utenti normali e anche i programmatori, non hanno a che fare con interrupt e interrupt handler. Si possono concentrare unicamente sui differenti task che vogliono che il computer esegua; i dettagli di come il computer gestisce per ottenere che tutti i task siano eseguiti non è per loro importante. Infatti, molti utenti, così come molti programmatori, possono ignorare i thread e il multitsking del tutto. Tuttavia, i thread stanno diventando sempre più importanti così come i computer stanno diventando sempre più potenti e come hanno iniziato a fare sempre più uso del multitasking e multiprocessing. In effetti, l'abilità di lavorare con i thread sta diventando rapidamente una competenza essenziale per programmatori. Per fortuna, Java ha un buon supporto per i thread, che fanno parte del linguaggio Java come uno dei concetti fondamentali.

Così come è importante in Java e nei moderni linguaggi di programmazione, il concetto di eventi asincroni. Anche se i programmatori non hanno in genere a che fare con gli interrupt direttamente, essi spesso devono scrivere event handlers, che come gli interrupt handlers, sono invocati in modo asincrono quando un certo evento accade. Questa "event-driven programming" ha un sapore molto diverso rispetto alla tradizionale programmazione sincrona.

***********************

A proposito, il software che implementa il meccanismo dell'interrupt handling, si occupa della comunicazione con l'utente e con l'hardware, e controlla quale thread (o processo) può andare in esecuzione, è chiamato il sistema operativo. Il sistema operativo è il software di base, essenziale senza il quale il computer non potrebbe funzionare. Altri programmi, come gli editor di testi o i web browser, dipendono dai servizi del sistema operativo. I sistemi operativi comuni per computer sono Linux, le diverse versioni di Windows e Mac OS. I sistemi operativi per smartphone e tablet sono Android e iOS.

La Java Virtual Machine

Il linguaggio macchina consiste di semplici istruzioni che possono essere eseguite direttamente dalla CPU del computer. Tuttavia, quasi tutti i programmi, sono scritti in linguaggi di alto livello, come Java, Python o C++. Un programma scritto in un linguaggio di alto livello non può essere eseguito direttamente su nessun computer. Prima, deve essere tradotto in linguaggio macchina. Questa traduzione può essere fatta da un programma chiamato compilatore. Un compilatore prende un programma scritto in un linguaggio di alto livello e lo traduce in un programma in linguaggio macchina. Una volta che la traduzione è fatta, il programma in linguaggio macchina può essere eseguito qualsiasi numero di volte, ma può essere eseguito su un solo tipo di computer (poiché ogni tipo di computer ha il proprio linguaggio macchina). Se il programma deve essere eseguito su un altro tipo di computer, deve essere ricompilato (ritradotto), utilizzando un compilatore differente, nel linguaggio macchina appropriato per quel tipo di computer.

C'è un'alternativa a compilare un programma scritto in un linguaggio di alto livello. Invece di utilizzare un compilatore, che traduce tutto il programma in un sol colpo, si può utilizzare un interprete, che traduce istruzione per istruzione. Un interprete è un programma che agisce come una CPU, come in un ciclo fetch-and-execute.

Consiglio di leggere prima queste due fonti per avere una visione più generale e un po' di approfondimento:

Overview: The Mental Landscape e Introduction to Java Programming (for Novices & First-Time Programmers)

Step per eseguire un programma java

Gli step nella scrittura di un programma Java come illustrate sopra:

Step 1: Scriviamo il codice sorgente "Xxx.java".

Step 2: Compiliamo il codice sorgente "Xxx.java" in bytecode Java portabile "Xxx.class"utilizzando il compilatore Java tramite il comando javac Xxx.java.

Step 3: Esegui il bytecode "Xxx.class", utilizzando il Java Runtime tramite il comandojava Xxx.

Settare la variabile PATH nel vostro ambiente Windows

Una volta che avete scaricato il JDK (Java Development Kit) dal sito della Oracle, ma dalla versione 11 di Java, la JDK di Oracle è principalmente da utilizzare per scopi commerciali. Si può utilizzare la versione free OpenJDK dal sito:

Oppure l'OpenJDK può essere scaricata da AdoptOpenJDK a questo indirizzo:

Con le istruzioni per l'istallazione.

Dal prompt dei comandi deve essere possibile eseguire il programma javac, il compilatore java, che serve per compilare il vostro sorgente da codice scritto in linguaggio java a bytecode (.class) - lo step 1.

Quando installate il JDK (Java Development Kit) sul vostro PC, installate sia il compilatore javac (compila i file .java in .class) sia l'interprete java, la vera e propria virtual machine, che esegue il bytecode (i file .class).

Ad esempio sul mio PC ho installato il JDK in:

C:\dev\jdk-11.0.1

sotto la directory

C:\dev\jdk-11.0.1\bin ci sono i programmi javac e java.

Come per qualsiasi altro programma per far si che si possano eseguire da qualsiasi directory che sia diversa dalla directory in cui è stato installato, è necessario configurare la variabile d'ambiente PATH con il percorso agli eseguibili, javac e java.

Nel nostro caso dovremo aggiungere al PATH il percorso C:\dev\jdk-11.0.1\bin.

Il fatto di dover settare la variabile d'ambiente PATH perché un programma sia trovato vale sia per sistemi operativi Windows, Linux e Unix. Cambia solo la sintassi tra i vari sistemi su come settare la variabile d'ambiente.

Tornando al nostro caso su Windows, il primo modo è di creare uno script .bat che eseguite dal prompt dei comandi, contenete in questo caso:

set PATH=%PATH%;C:\dev\jdk-11.0.1\bin

In questo modo setto il PATH aggiungendo ai percorsi precedenti il nuovo percorso per trovare il JDK.

Per verifica potere dal prompt vedere il nuovo PATH:

Digitare da prompt: echo %PATH% per vedere il contenuto del PATH.

C:\Users\cam>echo %PATH%
C:\Program Files (x86)\Intel\iCLS Client\;C:\Program Files\Intel\iCLS Client\;C:\windows\system32;C:\windows;C:\windows\System32\Wbem;C:\windows\System32\WindowsPowerShell\v1.0\;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\dev\Git\cmd;C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit\;D:\opt\GtkSharp\2.12\bin;D:\opt\Yarn\bin;C:\Program Files\dotnet\;C:\Program Files (x86)\Intel\Intel(R) Management Engine Components\DAL;C:\Program Files\Intel\Intel(R) Management Engine Components\DAL;C:\Program Files (x86)\Intel\Intel(R) Management Engine Components\IPT;C:\Program Files\Intel\Intel(R) Management Engine Components\IPT;C:\WINDOWS\System32\OpenSSH\;C:\dev\nodejs_8.12.0\;D:\dev\Ruby24-x64\bin;C:\dev\python27;C:\dev\openshift-origin-client-tools-v1.3.0-3;C:\Users\cam\AppData\Local\Microsoft\WindowsApps;C:\dev\Git\usr\bin;C:\dev\Heroku\bin;C:\dev\python27\Scripts;C:\Users\cam\AppData\Local\atom\bin;C:\Users\cam\AppData\Local\Yarn\bin;D:\opt\sqlite3;D:\opt\sqlite-tools-win32-x86-3190300;C:\Users\cam\AppData\Local\Microsoft\WindowsApps;D:\dev\apache-maven-3.5.3\bin;D:\dev\emacs-25.3_1\bin;D:\dev\gradle-4.7\bin;C:\Users\cam\.dotnet\tools;D:\dev\VSCode\bin;C:\Users\cam\AppData\Roaming\npm;C:\dev\jdk-11.0.1\bin;

C:\Users\cam>

Potere vedere che in coda è stato aggiunto il percorso C:\dev\jdk-11.0.1\bin come volevamo.

Ora potete eseguire javac e il comando viene trovato.

Ad esempio digitando da prompt:

C:\Users\cam>javac -version
javac 11.0.1

C:\Users\cam>

N.B: se aprite un altro prompt di comandi il PATH qui non è con il nuovo PATH che avete configurato: dovete eseguite anche qui lo script per aggiungere al path il percorso a javac e java.

Tutto ciò è molto scomodo.

Il metodo alternativo e in genere utilizzato del programmatore è quello di configurarlo tra le variabili d'ambiente di Windows tramite il pannello di controllo:

Pannello di controllo -> Sistema e sicurezza -> Sistema -> Impostazioni di sistema avanzate

Qui si apre un pannello Proprietà di sistema e cliccate sul tab 'Avanzate' e da qui su il bottone 'Variabili d'ambiente'.

Qui settate le variabili d'ambiente che vi interessano con nuova o modifica.

Nel sosto caso settiamo la variabile d'ambiente PATH che è quella che ci interessa.

Compiliamo ed eseguiamo un primo programma

Ad esempio, con un editor, tipo Notepad++, si può scrivere il seguente file Hello.java e salvarlo ad esempio in una directory myProject, precedentemente creata, sotto la nostra directory home.

Hello.java

public class Hello {   // To save as "Hello.java" under "~/myProject"
   public static void main(String[] args) {
      System.out.println("Hello, world from Italy!");
   }
}

In seguito compilarlo con il comando javac Hello.java ed eseguirlo con java Hello, come si vede dell'output:

C:\Users\PC610>mkdir myProject

C:\Users\PC610>
C:\Users\PC610>cd myProject

C:\Users\PC610\myProject>
C:\Users\PC610\myProject>dir
 Il volume nell'unità C non ha etichetta.
 Numero di serie del volume: 9436-2FA0

 Directory di C:\Users\PC610\myProject

19/03/2020  17:50    <DIR>          .
19/03/2020  17:50    <DIR>          ..
19/03/2020  17:50               185 Hello.java
               1 File            185 byte
               2 Directory  10.282.962.944 byte disponibili

C:\Users\PC610\myProject>
C:\Users\PC610\myProject>javac Hello.java

C:\Users\PC610\myProject>
C:\Users\PC610\myProject>dir
 Il volume nell'unità C non ha etichetta.
 Numero di serie del volume: 9436-2FA0

 Directory di C:\Users\PC610\myProject

19/03/2020  17:53    <DIR>          .
19/03/2020  17:53    <DIR>          ..
19/03/2020  17:53               431 Hello.class
19/03/2020  17:50               185 Hello.java
               2 File            616 byte
               2 Directory  10.282.942.464 byte disponibili

C:\Users\PC610\myProject>
C:\Users\PC610\myProject>
C:\Users\PC610\myProject>
C:\Users\PC610\myProject>java Hello
Hello, world from Italy!

C:\Users\PC610\myProject>

Come si vede in seguito alla compilazione, javac, viene creato un file .class, Hello.class, il file bytecode del sorgente .java.

Con il comando java Hello, viene attivata la JVM che esegue il codice bytecode Hello.class e in questo caso produce in output il messaggio, "Hello, world from Italy!".

How to Install JDK 13 (on Windows, macOS & Ubuntu) and Get Started with Java Programming

RISORSE

  • Package and Classpath introduce il concetto anche dei package, ma utile come esercizio per prendere dimestichezza con compilazione ed esecuzione di programmi da riga di comando.

ESERCIZI

  1. Eseguite il tutorial Package and Classpath. Se fosse necessario configurare l'ambiente per eseguire la compilazione con javac per il vostro ambiente.

Last updated