Vai al contenuto

Introduzione a Java

Parte 2 · Linguaggi di programmazione · Capitolo 8 · Dispensa, volume 2 (PDF) · Esercizi: Introduzione a Java

Introduzione

Nei capitoli «Introduzione a C++» e «Introduzione a Python» abbiamo tradotto lo pseudocodice dei nostri algoritmi in un linguaggio di programmazione compilato e in uno interpretato. In questo capitolo introduciamo un terzo linguaggio di programmazione, Java, che sta a metà strada: i suoi programmi sono compilati in un codice intermedio, che viene poi eseguito da una macchina virtuale. La sua sintassi è molto vicina a quella di C++, ma ogni programma è organizzato in classi.

Il capitolo segue lo stesso percorso dei due capitoli precedenti, in modo che i tre linguaggi possano essere confrontati sezione per sezione:

  • le caratteristiche principali del linguaggio e la struttura di un programma;

  • variabili, tipi di dato, ingresso e uscita;

  • operatori, istruzioni condizionali e cicli;

  • gli array, e le liste dinamiche della classe ArrayList;

  • i metodi (il nome che Java dà alle funzioni), i loro parametri e i loro valori di ritorno;

  • un algoritmo completo, la ricerca del valore massimo in un array, scritto in pseudocodice e in Java;

  • gli operatori bit a bit e una tabella riassuntiva che confronta Java con C++ e Python.

Ogni frammento di codice del capitolo è stato compilato con il compilatore javac ed eseguito con la macchina virtuale java (OpenJDK 25): gli output riportati negli esempi sono quelli stampati dai programmi.

Nello pseudocodice le componenti di un array \(a[1..n]\) sono indicizzate da \(1\) a \(n\); in Java, come in C++ e in Python, gli elementi di un array di dimensione \(n\) sono indicizzati da \(0\) a \(n-1\). La componente \(a[i]\) dello pseudocodice corrisponde all'elemento a[i-1] del codice Java.

8.1 Caratteristiche principali del linguaggio di programmazione

Le caratteristiche principali di Java sono le seguenti.

  • Java è un linguaggio compilato e interpretato: il codice sorgente (file .java) viene prima compilato in bytecode (file .class) dal compilatore Java javac, e poi il bytecode viene eseguito dalla Java Virtual Machine (JVM). Questo rende i programmi Java indipendenti dalla piattaforma: lo stesso bytecode funziona su qualunque macchina su cui sia installata una JVM («scrivi una volta, esegui ovunque»).

  • Java è un linguaggio fortemente e staticamente tipizzato: ogni variabile deve essere dichiarata con il suo tipo di dato prima dell'uso, e il suo tipo di dato non può cambiare durante l'esecuzione.

  • Java è orientato agli oggetti: ogni programma è organizzato in classi. Il punto di ingresso di ogni programma Java è il metodo main di una classe.

  • Java supporta i metodi (il termine Java per le funzioni), che permettono un approccio modulare e riutilizzabile alla programmazione.

  • Come in C++, la struttura a blocchi del codice è definita dalle parentesi graffe {}, e ogni istruzione deve terminare con il simbolo riservato “;”.

  • Come in C++, un commento su una sola riga inizia con il simbolo riservato “//”, e i commenti su più righe sono racchiusi fra i simboli riservati “/*” e “*/”.

Esempio 8.1: commenti in Java

Questo è un esempio di commento su una sola riga:

// Questa è una riga di commento

Questo è un esempio di commento su più righe:

/* Questo è un
   commento su più righe */

8.1.1 La struttura di un programma

Prima di descrivere le singole istruzioni, osserviamo lo scheletro che ogni programma ha in comune. Un programma Java è costituito da una o più classi; l'esecuzione parte dal metodo public static void main(String[] args) della classe che viene lanciata.

Esempio 8.2: lo scheletro di un programma

public class HelloWorld {

    public static void main(String[] args) {
        // Qui si scrivono le istruzioni del programma
        System.out.println("Ciao, mondo!");
    }
}

Una classe dichiarata public deve essere salvata in un file con lo stesso nome della classe: qui HelloWorld.java. Il programma si compila e si esegue con i comandi

javac HelloWorld.java
java HelloWorld

Il primo comando produce il file di bytecode HelloWorld.class; il secondo lo esegue (si noti che l'estensione non si scrive). L'output è

Ciao, mondo!

Negli esempi seguenti scriviamo solo le istruzioni che contano: a meno che non contengano un programma completo, i frammenti di codice vanno inseriti fra le parentesi graffe di main.

Ogni istruzione Java appartiene a un metodo, e ogni metodo appartiene a una classe. Ogni istruzione termina con “;”.

8.2 Variabili e tipi di dato

Il codice sorgente opera su variabili, e ogni variabile ha un tipo di dato.

  • I quattro tipi di dato fondamentali di Java sono:

    1. Il tipo di dato “int” rappresenta un intero memorizzato con \(32\) bit: un numero intero da \(-2^{31} = -2147483648\) a \(2^{31}-1 = 2147483647\). Per interi più grandi si usa il tipo di dato long (\(64\) bit).

    2. Il tipo di dato “boolean” rappresenta un valore booleano: true o false (minuscolo).

    3. Il tipo di dato “double” rappresenta un numero in virgola mobile a precisione doppia (\(64\) bit), come in C++. Il tipo di dato float ha precisione singola, e i suoi valori si scrivono con il suffisso f (ad esempio 3.14f).

    4. Il tipo di dato “char” rappresenta un singolo carattere, racchiuso fra apici singoli (ad esempio 'A', '7').

  • Il tipo di dato “String” (con la S maiuscola) rappresenta una sequenza di caratteri racchiusa fra virgolette doppie, ed è il modo standard di trattare il testo in Java.

  • Una variabile funziona come un contenitore che memorizza un valore di un tipo di dato specifico per un uso successivo.

  • Alle variabili si assegnano valori con l'operatore di assegnamento “=”. Quando si assegna un nuovo valore, il valore precedente viene sovrascritto.

  • Il compilatore controlla i tipi di dato: assegnare un valore di tipo sbagliato (per esempio la stringa "cinque" a una variabile di tipo int) o usare una variabile che non ha ricevuto un valore è un errore di compilazione, e il programma non viene prodotto.

Le variabili devono essere dichiarate con il loro tipo di dato prima dell'uso, come in C++.

Esempio 8.3: dichiarazioni e assegnamenti di variabili

int age = 25;              // Variabile intera
boolean isRaining = false; // Variabile booleana
double pi = 3.14159;       // Virgola mobile a precisione doppia
float small = 3.14f;       // Virgola mobile a precisione singola
double large = 123456.789; // Virgola mobile a precisione doppia
char letter = 'A';         // Variabile carattere
String name = "Alice";     // Variabile stringa

age = 26; // Il valore precedente 25 viene sovrascritto
System.out.println(age + " " + isRaining + " " + pi + " " + small);
System.out.println(large + " " + letter + " " + name);

Output:

26 false 3.14159 3.14
123456.789 A Alice

A differenza di C++, il valore booleano è visualizzato come false e il numero 123456.789 è visualizzato con tutte le sue cifre.

Esempio 8.4: i limiti del tipo di dato int

int big = Integer.MAX_VALUE;  // Il più grande int
System.out.println(big);
System.out.println(big + 1);  // Overflow: il risultato non entra in 32 bit

long bigger = (long) big + 1; // Calcolato con 64 bit
System.out.println(bigger);

Output:

2147483647
-2147483648
2147483648

Quando il risultato di un'operazione fra valori int non entra in \(32\) bit, «riparte» dall'altro estremo dell'intervallo, senza alcun messaggio di errore. Gli interi di Python non vanno mai in overflow.

8.3 Ingresso e uscita

  • L'oggetto System.out (l'uscita standard, fornita dalla classe System) ha i metodi che visualizzano valori sullo schermo. I più comuni sono:

    • System.out.println(): stampa un valore seguito da un a capo;

    • System.out.print(): stampa un valore senza l'a capo.

  • Valori e stringhe sono concatenati con l'operatore + dentro un'istruzione di stampa: quando uno dei due operandi di + è una String, l'altro viene convertito in testo. L'espressione è valutata da sinistra a destra, quindi un'operazione aritmetica dentro una concatenazione deve essere racchiusa fra parentesi.

  • Il metodo System.out.printf() stampa un testo formattato: nella prima stringa, %d è sostituito da un intero, %s da una stringa, %.kf da un numero in virgola mobile con esattamente \(k\) cifre dopo la virgola decimale, e %n da un a capo.

Esempio 8.5: stampe sullo schermo

int n = 42;
System.out.println("Il valore della variabile è " + n);
System.out.println("n = " + n + ", doppio = " + (n * 2));

System.out.println("n + 1 = " + n + 1);   // Concatenazione: "42" poi "1"
System.out.println("n + 1 = " + (n + 1)); // Addizione, poi concatenazione

System.out.print("Niente a capo ");
System.out.println("dopo print");
System.out.printf("%d diviso %d è %.2f%n", 10, 3, 10.0 / 3);

Output:

Il valore della variabile è 42
n = 42, doppio = 84
n + 1 = 421
n + 1 = 43
Niente a capo dopo print
10 diviso 3 è 3.33
  • Il separatore decimale stampato da printf dipende dalle impostazioni della lingua del calcolatore: su un calcolatore impostato in italiano l'ultima riga è 10 diviso 3 è 3,33, con la virgola. Il metodo println usa sempre il punto.

  • Per leggere l'ingresso da tastiera si usa la classe Scanner del pacchetto java.util. Va importata con l'istruzione import java.util.Scanner;, scritta all'inizio del file, prima della classe. I metodi nextLine(), nextInt() e nextDouble() leggono una riga di testo, un intero e un numero in virgola mobile.

Esempio 8.6: ingresso da tastiera con Scanner

import java.util.Scanner;

public class InputExample {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        System.out.print("Inserisci il tuo nome: ");
        String name = sc.nextLine();     // Legge una String

        System.out.print("Inserisci un intero: ");
        int n = sc.nextInt();            // Legge un int

        System.out.print("Inserisci un numero reale: ");
        double x = sc.nextDouble();      // Legge un double

        System.out.println("Ciao " + name + ", n = " + n + ", x = " + x);
        sc.close();
    }
}

Esempio di esecuzione (l'utente scrive Ada, 7 e 2.5):

Inserisci il tuo nome: Ada
Inserisci un intero: 7
Inserisci un numero reale: 2.5
Ciao Ada, n = 7, x = 2.5

Come printf, il metodo nextDouble() segue le impostazioni della lingua: su un calcolatore impostato in italiano il numero reale va scritto come 2,5.

8.4 Operatori

Gli operatori sono simboli che eseguono operazioni su variabili e valori. Gli operatori di Java sono gli stessi di C++: operatori aritmetici, di assegnamento, di incremento e decremento, relazionali e logici.

8.4.1 Operatori aritmetici

Operatori aritmetici, usati per eseguire le operazioni matematiche di base:

  • + (Addizione) \(\rightsquigarrow\) Somma due valori.

  • - (Sottrazione) \(\rightsquigarrow\) Sottrae un valore da un altro.

  • * (Moltiplicazione) \(\rightsquigarrow\) Moltiplica due valori.

  • / (Divisione) \(\rightsquigarrow\) Divide un valore per un altro. Divisione intera quando entrambi gli operandi sono interi.

  • % (Resto) \(\rightsquigarrow\) Restituisce il resto di una divisione.

Esempio 8.7: operatori aritmetici

int a = 10, b = 3;

System.out.println("Addizione:       " + (a + b));
System.out.println("Sottrazione:     " + (a - b));
System.out.println("Moltiplicazione: " + (a * b));
System.out.println("Divisione:       " + (a / b));  // Divisione intera
System.out.println("Resto:           " + (a % b));  // Resto della divisione
System.out.println("Divisione reale: " + ((double) a / b));

Output:

Addizione:       13
Sottrazione:     7
Moltiplicazione: 30
Divisione:       3
Resto:           1
Divisione reale: 3.3333333333333335
  • Come in C++, l'operatore di divisione / esegue la divisione intera quando entrambi gli operandi sono interi, e il quoziente è troncato verso lo zero: 10 / 3 vale 3 e -7 / 2 vale -3 (con resto -7 % 2 uguale a -1). Per ottenere un risultato in virgola mobile almeno un operando deve essere di tipo double, per esempio con la conversione di tipo (cast) (double) a / b.

  • La conversione (int) elimina la parte decimale di un double: (int) 3.9 vale 3. Non esiste un operatore per la potenza: il metodo Math.pow(x, y) restituisce \(x^y\) come double.

Esempio 8.8: divisione intera e in virgola mobile

int a = 10, b = 3;

System.out.println(a / b);             // Divisione intera
System.out.println(10.0 / 3);          // Divisione in virgola mobile
System.out.println((double) a / b);    // Conversione di a in double
System.out.printf("%.2f%n", (double) a / b); // Due cifre decimali
System.out.println(-7 / 2 + " " + -7 % 2);   // Troncamento verso zero
System.out.println((int) 3.9 + " " + Math.pow(2, 10));

Output:

3
3.3333333333333335
3.3333333333333335
3.33
-3 -1
3 1024.0

8.4.2 Operatori di assegnamento

Operatori di assegnamento, usati per assegnare valori alle variabili:

  • = (Assegnamento) \(\rightsquigarrow\) Assegna un valore a una variabile.

  • += (Assegnamento con addizione) \(\rightsquigarrow\) Somma un valore e assegna il risultato.

  • -= (Assegnamento con sottrazione) \(\rightsquigarrow\) Sottrae un valore e assegna il risultato.

  • *= (Assegnamento con moltiplicazione) \(\rightsquigarrow\) Moltiplica e assegna il risultato.

  • /= (Assegnamento con divisione) \(\rightsquigarrow\) Divide e assegna il risultato.

  • %= (Assegnamento con resto) \(\rightsquigarrow\) Calcola il resto e assegna il risultato.

Esempio 8.9: operatori di assegnamento

int x = 10;  // Assegnamento
System.out.println("Valore iniziale: " + x);

x += 5;  System.out.println("Dopo += 5: " + x); // x = x + 5
x -= 3;  System.out.println("Dopo -= 3: " + x); // x = x - 3
x *= 2;  System.out.println("Dopo *= 2: " + x); // x = x * 2
x /= 4;  System.out.println("Dopo /= 4: " + x); // x = x / 4
x %= 4;  System.out.println("Dopo %= 4: " + x); // x = x % 4

Output:

Valore iniziale: 10
Dopo += 5: 15
Dopo -= 3: 12
Dopo *= 2: 24
Dopo /= 4: 6
Dopo %= 4: 2

Due istruzioni possono essere scritte sulla stessa riga, perché ciascuna termina con “;”.

8.4.3 Operatori di incremento e decremento

Operatori di incremento e decremento:

  • ++ (Incremento) \(\rightsquigarrow\) Aumenta di 1 il valore di una variabile.

  • - - (Decremento) \(\rightsquigarrow\) Diminuisce di 1 il valore di una variabile.

  • Come in C++, il pre-incremento (++x) incrementa il valore prima di restituirlo, e il post-incremento (x++) restituisce prima il valore, poi lo incrementa. La stessa logica vale per l'operatore di decremento (- -x e x- -).

Esempio 8.10: operatori di incremento e decremento

int x = 5;
System.out.println("Valore iniziale: " + x);
System.out.println("Pre-incremento (++x): " + ++x);
System.out.println("Post-incremento (x++): " + x++);
System.out.println("Valore dopo il post-incremento: " + x);
System.out.println("Pre-decremento (--x): " + --x);
System.out.println("Post-decremento (x--): " + x--);
System.out.println("Valore dopo il post-decremento: " + x);

Output:

Valore iniziale: 5
Pre-incremento (++x): 6
Post-incremento (x++): 6
Valore dopo il post-incremento: 7
Pre-decremento (--x): 6
Post-decremento (x--): 6
Valore dopo il post-decremento: 5

8.4.4 Operatori relazionali

Operatori relazionali, usati per confrontare valori e restituire un risultato boolean:

  • == (Uguale a) \(\rightsquigarrow\) Verifica se due valori sono uguali.

  • != (Diverso da) \(\rightsquigarrow\) Verifica se due valori sono diversi.

  • < (Minore di) \(\rightsquigarrow\) Verifica se un valore è minore di un altro.

  • > (Maggiore di) \(\rightsquigarrow\) Verifica se un valore è maggiore di un altro.

  • <= (Minore o uguale a) \(\rightsquigarrow\) Verifica se un valore è minore o uguale a un altro.

  • >= (Maggiore o uguale a) \(\rightsquigarrow\) Verifica se un valore è maggiore o uguale a un altro.

  • Gli operatori relazionali restituiscono un valore di tipo boolean, visualizzato come true o false (non come 1 e 0 come in C++). Questi operatori sono usati comunemente nelle istruzioni condizionali e nei cicli.

  • L'operatore == confronta i valori dei tipi di dato primitivi (int, double, char, boolean); due valori String si confrontano con il metodo equals, per esempio name.equals("Alice").

Esempio 8.11: operatori relazionali

int a = 10, b = 5;

System.out.println("a == b: " + (a == b)); // Uguale a
System.out.println("a != b: " + (a != b)); // Diverso da
System.out.println("a < b: " + (a < b));   // Minore di
System.out.println("a > b: " + (a > b));   // Maggiore di
System.out.println("a <= b: " + (a <= b)); // Minore o uguale a
System.out.println("a >= b: " + (a >= b)); // Maggiore o uguale a

Output:

a == b: false
a != b: true
a < b: false
a > b: true
a <= b: false
a >= b: true

8.4.5 Operatori logici

Operatori logici, usati per le operazioni logiche, spesso nelle istruzioni condizionali:

  • && (AND logico) \(\rightsquigarrow\) Restituisce true se entrambe le condizioni sono true.

  • || (OR logico) \(\rightsquigarrow\) Restituisce true se almeno una condizione è true.

  • ! (NOT logico) \(\rightsquigarrow\) Inverte il valore booleano.

Esempio 8.12: operatori logici

boolean c1 = true, c2 = false;

System.out.println("c1: " + c1);
System.out.println("c2: " + c2);
System.out.println("c1 && c2: " + (c1 && c2)); // true solo se entrambe sono true
System.out.println("c1 || c2: " + (c1 || c2)); // true se almeno una è true
System.out.println("!c1: " + (!c1));           // Inverte il valore
System.out.println("!c2: " + (!c2));

Output:

c1: true
c2: false
c1 && c2: false
c1 || c2: true
!c1: false
!c2: true

8.5 Istruzioni condizionali

Gli operatori relazionali e logici producono le condizioni che decidono quali istruzioni vengono eseguite.

  • L'istruzione if valuta una condizione racchiusa fra parentesi (). Se la condizione è true, viene eseguito il blocco racchiuso fra parentesi graffe {}. La clausola opzionale else if verifica condizioni aggiuntive, e la clausola opzionale else tratta tutti i casi rimanenti.

  • A differenza di C++, la condizione deve essere di tipo boolean: un intero non è accettato come condizione, quindi if (n % 2) è un errore di compilazione e il test va scritto if (n % 2 != 0).

Esempio 8.13: istruzione condizionale

int number = 10;

// Verifica se il numero è positivo
if (number > 0) {
    System.out.println("Il numero è positivo.");
} else {
    System.out.println("Il numero non è positivo.");
}

Output:

Il numero è positivo.

Esempio 8.14: if / else if / else

int score = 85;
String grade;

if      (score >= 90) { grade = "A"; }
else if (score >= 80) { grade = "B"; }
else if (score >= 70) { grade = "C"; }
else if (score >= 60) { grade = "D"; }
else                  { grade = "F"; }

System.out.println("Punteggio: " + score + ", Voto: " + grade);

Output:

Punteggio: 85, Voto: B

Le condizioni sono valutate dall'alto verso il basso, e viene eseguito solo il blocco della prima condizione vera. La variabile grade è dichiarata senza un valore: il compilatore la accetta perché ogni ramo le assegna un valore prima che venga stampata.

8.5.1 Operatore ternario (operatore condizionale)

  • L'operatore ternario ?: è lo stesso di C++. Sintassi: condizione ? valore_se_vero : valore_se_falso.

Esempio 8.15: operatore ternario

int number = 10;

// condizione ? valore_se_vero : valore_se_falso
String result = (number > 0) ? "positivo" : "non positivo";
System.out.println("Il numero è " + result);

// Ricerca del massimo fra due numeri
int a = 15, b = 20;
int maximum = (a > b) ? a : b;
System.out.println("Massimo: " + maximum);

// Verifica se pari o dispari
int n = 7;
String parity = (n % 2 == 0) ? "pari" : "dispari";
System.out.println(n + " è " + parity);

// Valore assoluto
int x = -5;
int absValue = (x >= 0) ? x : -x;
System.out.println("Il valore assoluto di " + x + " è " + absValue);

Output:

Il numero è positivo
Massimo: 20
7 è dispari
Il valore assoluto di -5 è 5

La classe Math fornisce anche i metodi Math.max(a, b) e Math.abs(x), che restituiscono gli stessi valori.

8.6 Cicli

Le istruzioni condizionali scelgono se un blocco di codice viene eseguito; i cicli decidono quante volte viene eseguito.

  • I cicli servono a eseguire ripetutamente un blocco di codice finché una condizione è soddisfatta. Come in C++, i cicli più usati di Java sono il ciclo while e il ciclo for.

8.6.1 Il ciclo while

  • Il ciclo while esegue un blocco di codice finché la condizione racchiusa fra parentesi () vale true. Quando la condizione è false, il ciclo termina.

Esempio 8.16: ciclo while

int i = 1; // Inizializzazione

while (i <= 5) { // Condizione
    System.out.println("Numero: " + i);
    i++; // Incremento
}

Output:

Numero: 1
Numero: 2
Numero: 3
Numero: 4
Numero: 5

8.6.2 Il ciclo for

  • Il ciclo for combina inizializzazione, condizione e incremento (o decremento) in una sola istruzione, esattamente come in C++.

Esempio 8.17: ciclo for

// Inizializzazione; Condizione; Incremento
for (int i = 1; i <= 5; i++) {
    System.out.println("Numero: " + i);
}

L'output è lo stesso del ciclo while dell'Esempio 8.16.

8.6.3 Le istruzioni break e continue

  • L'istruzione break termina immediatamente il ciclo.

  • L'istruzione continue salta il resto dell'iterazione corrente e passa alla successiva.

Esempio 8.18: break e continue

for (int i = 1; i <= 10; i++) {
    if (i % 2 == 0) {
        continue; // Salta i numeri pari
    }
    if (i > 7) {
        break; // Interrompe il ciclo al primo dispari maggiore di 7
    }
    System.out.print(i + " ");
}
System.out.println();

Output:

1 3 5 7

L'output è lo stesso delle versioni C++ e Python: il valore 9 fa scattare il break.

8.7 Array

I cicli diventano davvero utili quando visitano collezioni di valori. Una collezione di variabili dello stesso tipo di dato è memorizzata in un array.

  • Un array in Java è una collezione ordinata di elementi dello stesso tipo di dato, con una dimensione fissata al momento della creazione: la dimensione non può cambiare in seguito.

  • Il tipo di dato di un array di interi si scrive int[]. Un array si alloca con l'operatore new, per esempio new int[n], dove la dimensione n può essere decisa durante l'esecuzione, come per gli array di dimensione dinamica di C++. Gli elementi di un nuovo array sono inizializzati a 0 (a false per boolean).

  • Gli elementi si raggiungono tramite il loro indice fra parentesi quadre [], a partire da 0: gli elementi di un array di dimensione \(n\) sono a[0], a[1], …, a[n-1]. Ogni array conosce la propria dimensione: la lunghezza di un array a è a.length.

  • A differenza di C++, la memoria di un array non deve essere liberata con delete[]: viene rilasciata automaticamente dal garbage collector della JVM quando l'array non è più usato.

Gli array si allocano con new prima dell'uso (oppure si creano con un elenco di valori fra parentesi graffe). Gli indici partono da 0, la dimensione è fissa ed è data da a.length.

Esempio 8.19: creazione e accesso a un array

int n = 5;

int[] a = new int[n]; // Allocazione di un array di n interi

for (int i = 0; i < n; i++) {
    a[i] = i * 2; // Esempio di assegnamento di valori
}

System.out.println("Lunghezza: " + a.length);
System.out.println("a[0] = " + a[0]); // Primo elemento
System.out.println("a[4] = " + a[4]); // Ultimo elemento, indice n-1

Output:

Lunghezza: 5
a[0] = 0
a[4] = 8

Accedere a un indice che non esiste, per esempio a[5], ferma il programma con l'errore ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5 (in C++ lo stesso accesso non è controllato).

  • Un array i cui valori sono noti quando si scrive il programma può essere creato elencando i suoi valori fra parentesi graffe, come in C++.

  • Il ciclo for esteso for (int x : a) visita direttamente gli elementi dell'array a, senza usare i loro indici, come il ciclo for su una lista di Python.

  • Il metodo Arrays.toString(a), della classe Arrays del pacchetto java.util, restituisce gli elementi di un array come un testo fra parentesi quadre.

Esempio 8.20: array scritto per esteso e iterazione

import java.util.Arrays;

int[] a = {9, 12, 3, 16, 19, 11, 12};
int n = a.length;

for (int i = 0; i < n; i++) {      // Ciclo sugli indici
    System.out.println("a[" + i + "] = " + a[i]);
}

for (int x : a) {                  // Ciclo sugli elementi
    System.out.print(x + " ");
}
System.out.println();

System.out.println(Arrays.toString(a));

Output:

a[0] = 9
a[1] = 12
a[2] = 3
a[3] = 16
a[4] = 19
a[5] = 11
a[6] = 12
9 12 3 16 19 11 12
[9, 12, 3, 16, 19, 11, 12]

L'istruzione import si scrive all'inizio del file, prima della classe.

8.7.1 Array bidimensionali

  • Gli array bidimensionali (matrici) si dichiarano con due coppie di parentesi quadre [][], e si allocano con new int[m][n] (\(m\) righe e \(n\) colonne) in una sola istruzione. L'elemento nella riga \(i\) e colonna \(j\) si raggiunge come matrix[i][j], con entrambi gli indici a partire da \(0\); matrix.length è il numero di righe e matrix[i].length è il numero di colonne della riga \(i\).

  • Un array bidimensionale può anche essere creato elencando i suoi valori riga per riga: int[][] c = {{3, 1, 4}, {1, 5, 9}}; crea un array con \(2\) righe e \(3\) colonne, e c[1][2] è l'elemento nella seconda riga e terza colonna, uguale a \(9\).

Esempio 8.21: array bidimensionale (matrice)

int m = 3; // Numero di righe
int n = 2; // Numero di colonne

int[][] matrix = new int[m][n]; // Allocazione di una matrice m x n

for (int i = 0; i < m; i++) {
    for (int j = 0; j < n; j++) {
        matrix[i][j] = i * j; // Esempio di assegnamento di valori
    }
}

for (int i = 0; i < m; i++) {
    for (int j = 0; j < n; j++) {
        System.out.println("matrix[" + i + "][" + j + "] = " + matrix[i][j]);
    }
}

Output:

matrix[0][0] = 0
matrix[0][1] = 0
matrix[1][0] = 0
matrix[1][1] = 1
matrix[2][0] = 0
matrix[2][1] = 2

8.7.2 Liste di dimensione variabile: ArrayList

  • Quando il numero di elementi non è noto in anticipo, la classe ArrayList del pacchetto java.util fornisce una lista che cresce durante l'esecuzione, come le liste di Python. Il tipo di dato di una lista di interi è ArrayList<Integer>: gli elementi di una ArrayList sono oggetti, e Integer è la classe che corrisponde a int.

  • Il metodo add(x) aggiunge l'elemento x in fondo alla lista, get(i) restituisce l'elemento di indice \(i\), set(i, x) lo sostituisce, e size() restituisce il numero di elementi.

Esempio 8.22: una lista di dimensione variabile

import java.util.ArrayList;

ArrayList<Integer> list = new ArrayList<>(); // Lista vuota

for (int i = 1; i <= 4; i++) {
    list.add(i * i);  // La lista cresce
}
list.set(0, 100);     // Sostituisce l'elemento di indice 0

System.out.println(list + " ha " + list.size() + " elementi");
System.out.println("list.get(2) = " + list.get(2));

Output:

[100, 4, 9, 16] ha 4 elementi
list.get(2) = 9

8.8 Metodi (funzioni)

Come nello pseudocodice, un algoritmo è scritto come una funzione che riceve l'input e restituisce l'output. In Java le funzioni si chiamano metodi e devono sempre essere definite dentro una classe.

  • Un metodo in Java è composto da:

    1. un modificatore di accesso opzionale (ad esempio public, private) e la parola chiave static quando il metodo appartiene alla classe invece che a un oggetto della classe (tutti i metodi di questo capitolo sono static);

    2. un tipo di ritorno: specifica il tipo del valore restituito (void se non viene restituito nulla);

    3. un nome del metodo: l'identificatore univoco del metodo;

    4. un elenco di parametri (opzionale): specifica i valori in ingresso, ciascuno con il suo tipo di dato;

    5. un corpo: il blocco di codice eseguito quando il metodo viene chiamato;

    6. un'istruzione return, obbligatoria se il tipo di ritorno non è void.

  • Il punto di ingresso di ogni programma Java è il metodo public static void main(String[] args), che svolge il ruolo della funzione main di C++. Il parametro args contiene le parole scritte dopo il nome della classe nel comando java.

  • A differenza di C++, un metodo può essere chiamato prima del punto in cui è definito nella classe: l'ordine dei metodi dentro una classe non conta.

  • Il codice sorgente Java può anche essere compilato ed eseguito online, per esempio al seguente indirizzo:
    www.mycompiler.io/online-java-compiler

Esempio 8.23: metodi

public class Main {

    // Definizione di metodo
    static int addition(int a, int b) {
        return a + b; // Restituisce la somma
    }

    // Definizione di metodo
    static int multiplication(int a, int b) {
        return a * b; // Restituisce il prodotto
    }

    public static void main(String[] args) {
        int sum = addition(5, 10);         // Chiamata di metodo
        System.out.println("Somma: " + sum);

        int prod = multiplication(5, 10);  // Chiamata di metodo
        System.out.println("Prodotto: " + prod);
    }
}

Output:

Somma: 15
Prodotto: 50

Esempio 8.24: metodo che opera su un array bidimensionale

public class Main {

    static int sumMatrix(int[][] matrix, int rows, int cols) {
        int sum = 0;
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                sum += matrix[i][j];
            }
        }
        return sum;
    }

    public static void main(String[] args) {
        int m = 3, n = 2;
        int[][] matrix = new int[m][n];

        for (int i = 0; i < m; i++)
            for (int j = 0; j < n; j++)
                matrix[i][j] = i * j;

        int total = sumMatrix(matrix, m, n);
        System.out.println("Somma degli elementi della matrice: " + total);
    }
}

Output:

Somma degli elementi della matrice: 3

Il corpo di un ciclo for fatto di una sola istruzione può essere scritto senza parentesi graffe, come in C++.

8.8.1 Parametri e valori di ritorno

  • In Java ogni parametro è passato per valore: il metodo riceve una copia dell'argomento. Per i tipi di dato primitivi (int, double, boolean, char) le modifiche fatte alla copia non influiscono sulla variabile di chi chiama; non esiste il passaggio per riferimento con il simbolo & di C++.

  • Una variabile di tipo int[] non contiene gli elementi, ma un riferimento all'array (simile al puntatore di C++). Il metodo riceve una copia del riferimento, che punta allo stesso array: le modifiche fatte agli elementi dell'array sono visibili a chi chiama.

  • Un metodo restituisce al più un valore. Quando un metodo deve restituire più risultati, può restituirli in un array, per esempio new int[]{q, r}.

Esempio 8.25: parametri e risultati multipli

public class Main {

    static void increment(int x) {
        x = x + 1; // Modifica solo la copia locale
    }

    static void fillWithZeros(int[] a) {
        for (int i = 0; i < a.length; i++) {
            a[i] = 0; // Modifica l'array di chi chiama
        }
    }

    static int[] divide(int n, int m) {
        return new int[]{n / m, n % m}; // Due risultati in un array
    }

    public static void main(String[] args) {
        int k = 5;
        increment(k);
        System.out.println("Dopo increment: " + k);

        int[] b = {4, 7, 1};
        fillWithZeros(b);
        System.out.println("Array: " + b[0] + " " + b[1] + " " + b[2]);

        int[] qr = divide(17, 5);
        System.out.println("Quoziente e resto: " + qr[0] + " " + qr[1]);
    }
}

Output:

Dopo increment: 5
Array: 0 0 0
Quoziente e resto: 3 2

8.9 Un algoritmo completo: il valore massimo in un array

Mettiamo ora insieme tutti i pezzi: un metodo, un ciclo, un'istruzione condizionale e un array.

Consideriamo il problema di trovare il valore massimo in un array \(a\) di \(n \ge 1\) numeri interi.

  • Questo è lo pseudocodice dell'algoritmo FindMaximum del capitolo «Pseudocodice»: le componenti sono indicizzate da \(1\) a \(n\).

Algoritmo: FindMaximum

Input: An array \(a[1..n]\) of \(n \ge 1\) integer numbers
Output: The maximum value in the array \(a\)
1function FindMaximum(a, n)
2\(max := a[1]\)
3for \(i := 2\) to \(n\) do
4if \(a[i] > max\) then
5\(max \leftarrow a[i]\)
6end
7end
8return \(max\)
9end

  • Questa è l'implementazione in Java. Gli elementi sono indicizzati da \(0\) a \(n-1\), quindi l'inizializzazione usa a[0] e il ciclo parte da i = 1. La dimensione \(n\) non è passata come parametro, perché il metodo la ottiene con a.length. Il metodo suppone \(n \ge 1\), come lo pseudocodice: per un array vuoto l'accesso ad a[0] ferma il programma con una ArrayIndexOutOfBoundsException.

    public class Main {
    
        // Metodo che trova il valore massimo in un array
        static int arrayMaxValue(int[] a) {
            int max = a[0]; // Inizializza con il primo elemento
    
            for (int i = 1; i < a.length; i++) {
                if (a[i] > max) {
                    max = a[i]; // Aggiorna il massimo
                }
            }
    
            return max;
        }
    
        public static void main(String[] args) {
            int[] a = {9, 12, 3, 16, 19, 11, 12};
    
            int max = arrayMaxValue(a);
            System.out.println("Valore massimo: " + max);
        }
    }
    

    Output:

    Valore massimo: 19
    
  • L'istanza di prova può anche essere generata a caso con la classe Random del pacchetto java.util: se rand è un oggetto di questa classe, rand.nextInt(100) restituisce un intero casuale fra \(0\) e \(99\), quindi rand.nextInt(100) + 1 è fra \(1\) e \(100\). L'oggetto creato con new Random(2026) parte dal valore fisso \(2026\) (il seme), in modo che ogni esecuzione produca lo stesso array; l'oggetto creato con new Random() produce un array diverso a ogni esecuzione.

Esempio 8.26: un'istanza casuale

import java.util.Arrays;
import java.util.Random;

public class Main {

    static int arrayMaxValue(int[] a) {
        int max = a[0];
        for (int i = 1; i < a.length; i++) {
            if (a[i] > max) {
                max = a[i];
            }
        }
        return max;
    }

    public static void main(String[] args) {
        int n = 10;  // Dimensione dell'array
        int[] a = new int[n];
        Random rand = new Random(2026); // Stesso array casuale a ogni esecuzione
        for (int i = 0; i < n; i++) {
            a[i] = rand.nextInt(100) + 1; // Intero casuale fra 1 e 100
        }

        System.out.println(Arrays.toString(a));
        System.out.println("Valore massimo: " + arrayMaxValue(a));
    }
}

Output:

[100, 51, 98, 31, 50, 39, 74, 37, 35, 79]
Valore massimo: 100

Esempio 8.27: esecuzione passo per passo

In ogni tabella, la colonna \(i\) è l'indice del ciclo Java e la colonna \(i+1\) è l'indice corrispondente dello pseudocodice. La prima riga è l'inizializzazione max = a[0].

Istanza 1: \(a = [9, 12, 3, 16, 19, 11, 12]\), \(n=7\)

\(i\) (Java) \(i+1\) (pseudocodice) a[i] a[i] > max? max
– – – – \(9\)
\(1\) \(2\) \(12\) Sì \(12\)
\(2\) \(3\) \(3\) No \(12\)
\(3\) \(4\) \(16\) Sì \(16\)
\(4\) \(5\) \(19\) Sì \(19\)
\(5\) \(6\) \(11\) No \(19\)
\(6\) \(7\) \(12\) No \(19\)

Restituisce: max \(= 19\)

Istanza 2: \(a = [3, 7, 2, 9, 1]\), \(n=5\)

\(i\) (Java) \(i+1\) (pseudocodice) a[i] a[i] > max? max
– – – – \(3\)
\(1\) \(2\) \(7\) Sì \(7\)
\(2\) \(3\) \(2\) No \(7\)
\(3\) \(4\) \(9\) Sì \(9\)
\(4\) \(5\) \(1\) No \(9\)

Restituisce: max \(= 9\)

8.10 Operatori bit a bit

Gli operatori bit a bit del capitolo «Introduzione a C++» esistono in Java con gli stessi simboli e lo stesso significato: & (AND), | (OR), (XOR), ~ (NOT), < < (scorrimento a sinistra) e > > (scorrimento a destra). Il metodo Integer.toBinaryString(x) restituisce la rappresentazione binaria di un intero.

  • Un valore di tipo int ha \(32\) bit, quindi ~12 inverte \(32\) bit e dà -13; il risultato a \(8\) bit di C++ si ottiene con la maschera 0xFF, come in Python.

  • Java ha un terzo operatore di scorrimento, > > > (scorrimento a destra senza segno), che aggiunge sempre zeri a sinistra, anche per i numeri negativi; per i numeri non negativi coincide con > >.

  • Poiché una condizione deve essere di tipo boolean, il test del bit meno significativo si scrive (n & 1) == 1, e non (n & 1) come in C++.

Esempio 8.28: operatori bit a bit

int a = 12; // Binario: 1100
int b = 10; // Binario: 1010

System.out.println((a & b) + " " + (a | b) + " " + (a ^ b)); // AND, OR, XOR
System.out.println(Integer.toBinaryString(a & b));           // 1000
System.out.println((5 << 2) + " " + (20 >> 2) + " " + (20 >> 3)); // Scorrimenti
System.out.println(~a + " " + (~a & 0xFF));  // NOT, e la sua versione a 8 bit
System.out.println((-8 >> 1) + " " + (-8 >>> 28)); // Scorrimento con e senza segno

int n = 15;
System.out.println(((n & 1) == 1) ? "dispari" : "pari"); // Bit meno significativo

Output:

8 14 6
1000
20 5 2
-13 243
-4 15
dispari

Il numero \(-8\) è memorizzato come \(11\dots11000\) (\(32\) bit): -8 > > 1 mantiene il segno e dà \(-4\), mentre -8 > > > 28 mantiene solo i \(4\) bit più a sinistra \(1111\), cioè \(15\).

8.11 Java, C++ e Python a confronto

La tabella seguente riassume le principali differenze fra i tre linguaggi di questa parte del corso.

C++ Java Python
Esecuzione compilato (g++) compilato in bytecode (javac), eseguito dalla JVM (java) interpretato (python3)
Tipi di dato dichiarati dichiarati non dichiarati
Blocchi {} {} : e indentazione
Fine dell'istruzione ; ; fine della riga
Valori booleani true, false (stampati 1, 0) true, false True, False
10 / 3 3 3 3.3333333333333335
-7 / 2 (intero) -3 -3 -7 // 2 vale -4
Intero più grande \(2^{31}-1\) per int \(2^{31}-1\) per int nessun limite
Operatori logici &&, ||, ! &&, ||, ! and, or, not
Collezioni array, liberati con delete[] array (a.length) e ArrayList, liberati automaticamente liste (len(a)), liberate automaticamente
Indice fuori intervallo non controllato eccezione eccezione
Più risultati parametri per riferimento restituzione di un array restituzione di una tupla
Punto di ingresso funzione main metodo main di una classe livello più esterno del file

Punti chiave del capitolo

  • Un programma Java è costituito da classi; è compilato in bytecode con javac ed eseguito dalla JVM con java, a partire dal metodo public static void main(String[] args).

  • Ogni variabile è dichiarata con il suo tipo di dato, che è controllato dal compilatore; il tipo di dato int ha \(32\) bit, e la divisione fra due interi è la divisione intera.

  • Le istruzioni condizionali (if, else if, else, ?:) e i cicli (while, for, e il for esteso) hanno la stessa sintassi di C++, ma una condizione deve essere di tipo boolean.

  • Gli array di Java si allocano con new, conoscono la loro dimensione a.length, sono indicizzati da \(0\) a \(n-1\) e sono liberati automaticamente; la classe ArrayList fornisce liste di dimensione variabile.

  • I parametri sono passati per valore; un metodo che riceve un array può modificarne gli elementi, e più risultati sono restituiti insieme in un array.


Esercizi su questo capitolo: Introduzione a Java