Articolo originale: https://www.freecodecamp.org/news/the-java-handbook/

Java è in circolazione dagli anni '90 e nonostante il suo grande successo in molti settori, questo linguaggio di programmazione multi-piattaforma e orientato agli oggetti, è spesso criticato.

Indipendentemente da ciò che le persone pensano riguardo a Java, per la mia esperienza, posso dirti che è un eccellente linguaggio di programmazione. La sua prima apparizione risale al 1995, ed è ancora ampiamente usato – ed è probabile che questo non cambi a breve.

Puoi usare Java per costruire server, creare applicazioni desktop, giochi, applicazioni mobile e molto altro. Esistono anche altri linguaggi JVM (discuteremo a breve di cosa vuol dire) come Kotlin, Groovy, Scala e Clojure che puoi usare per diversi scopi.

Java è multi-piattaforma, che vuol dire che il codice che scrivi e compili su una piattaforma può essere eseguito su qualsiasi altra piattaforma che ha Java installato. Discuteremo questo tema in maggiore dettaglio più avanti.

Per ora, posso dirti che sebbene Java abbia i suoi difetti, ha anche tanto da offrire.

Sommario

  1. Prerequisiti
  2. Hello World
    1. Cosa succede nel codice?
    2. JVM
    3. JRE e JDK
  3. Configurare Java
  4. Installare un IDE
  5. Creare un nuovo progetto su IntelliJ IDEA
  6. Variabili
    1. Regole per dichiarare le variabili
    2. Variabili final
  7. Tipi di dati primitivi
    1. Conversione di tipo
    2. Classi wrapper
  8. Operatori
    1. Operatori aritmetici
    2. Operatori di assegnazione
    3. Operatori di confronto
    4. Operatori logici
    5. Operatori unari
  9. Stringhe
    1. Formattare una stringa
    2. Ottenere la lunghezza di una stringa e verificare se è vuota
    3. Dividere e unire stringhe
    4. Convertire una stringa in maiuscolo o minuscolo
    5. Confrontare due stringhe
    6. Sostituire caratteri e sottostringhe
    7. Verificare se una stringa contiene una sottostringa
  10. Input e output
  11. Istruzioni condizionali
  12. Istruzione switch-case
  13. Visibilità delle variabili
  14. Valori predefiniti delle variabili
  15. Array
    1. Ordinare un array
    2. Ricerca binaria su un array
    3. Riempire un array
    4. Copiare un array
    5. Confrontare due array
  16. Loop
    1. Loop for
    2. Loop for-each
    3. Loop while
    4. Loop do-while
  17. ArrayList
    1. Aggiungere o rimuovere elementi multipli
    2. Rimuovere elementi in base a una condizione
    3. Clonare e confrontare ArrayList
    4. Verificare le presenza di un elemento e se un ArrayList è vuoto
    5. Ordinare un ArrayList
    6. Tenere gli elementi comuni in due ArrayList
    7. Eseguire un'azione su tutti gli elementi di un ArrayList
  18. HashMap
    1. Inserire e sostituire elementi multipli
    2. Verificare se un elemento è presente e se una HashMap è vuota
    3. Eseguire un'azione su tutti gli elementi di una HashMap
  19. Classi e oggetti
    1. Metodi
    2. Overloading dei metodi
  20. Costruttori
  21. Modificatori di accesso
  22. Getter e setter
  23. Ereditarietà
  24. Sovrascrivere un metodo
  25. Conclusione

Prerequisiti

L'unico requisito per questo corso è avere familiarità con un qualsiasi altro linguaggio di programmazione come Python, JavaScript e via dicendo.

Anche se spiegherò concetti di programmazione fondamentali nel contesto di Java, non parlerò di cose come cos'è una variabile nell'ambito della programmazione in generale.

Come scrivere Hello World in Java

Idealmente, il primo passo dovrebbe essere impostare Java sul tuo computer, ma non voglio annoiarti con il download e l'installazione di un mucchio di software proprio all'inizio. Per questo esempio, userai la piattaforma https://replit.com/.

Prima di tutto, vai su https://replit.com/ e crea un nuovo account se non ne hai già uno. Puoi usare il tuo account Google/GitHub/Facebook esistente per accedere. Una volta fatto il login, ti troverai nella tua home. Da qui, usa il pulsante Create sotto My Repls per creare un nuovo repl.

image-201

Nella finestra Create a Repl, scegli Java com Template, imposta un titolo descrittivo (nel campo Title) come Hello World e premi il pulsante Create Repl.

image-202

Ti verrà mostrato un editor di codice con un terminale integrato, come il seguente:

image-203

Sul lato sinistro c'è la lista dei file di questo progetto, al centro c'è l'editor di codice e sulla destra il terminale.

Il template contiene già del codice predefinito. Puoi eseguire il codice premendo il pulsante Run. Prosegui pure eseguendo il programma.

image-205

Se tutto funziona a dovere, dovresti vedere le parole "Hello world!" stampate sul lato destro. Congratulazioni, hai eseguito con successo il tuo primo programma in Java.

Cosa succede nel codice?

Il programma hello world è probabilmente il programma eseguibile Java più semplice che tu possa scrivere – e capirlo è cruciale.

class Main {
  public static void main(String[] args) {
    System.out.println("Hello world!");
  }
}

Partiamo dalla prima riga:

class Main {
  //...
}

Questa riga crea una classe Main. Una classe raggruppa insieme una porzione di codice correlato in una singola unità.

Questa è una classe public, che vuol dire che la classe è accessibile ovunque nel codebase. Un file sorgente Java (un file con l'estensione .java) può contenere al suo interno soltanto una classe public di livello più alto.

Questa classe pubblica con il livello più alto deve avere esattamente lo stesso nome del file con il codice sorgente. Ecco perché il file denominato Main.java contiene la classe Main in questo progetto.

Per capirne il motivo, clicca sui tre puntini nella lista dei file e clicca sull'opzione Show hidden files.

image-204

Così facendo vedrai alcuni nuovi file all'interno del progetto, tra cui il file Main.class. È ciò che viene chiamato bytecode. Quando premi il pulsante Run, il compilatore Java compila il tuo codice dal file Main.java in questo bytecode.

Adesso, modifica il codice Hello World esistente come segue:

class Main {
  public static void main(String[] args) {
    System.out.println("Hello world!");
  }
}

class NotMain {
  public static void main(String[] args) {
    System.out.println("Not hello world!");
  }
}

Come puoi vedere, una nuova classe chiamata NotMain è stata aggiunta. Prosegui e premi ancora il pulsante Run tenendo d'occhio il menu Files.

image-206

È apparso un nuovo bytecode chiamato NotMain.class. Questo significa che per ogni classe presente nell'intero codebase, il compilatore creerà un bytecode separato.

Ciò crea confusione riguardo a quale classe sia il punto d'ingresso (entry-point) di questo programma. Per risolvere questo problema, Java usa la classe che corrispondente al nome del file con il codice sorgente come punto d'ingresso del programma.

Ma basta parlare della classe, adesso diamo un'occhiata alla funzione al suo interno:

class Main {
  public static void main(String[] args) {
    System.out.println("Hello world!");
  }
}

La funzione public static void main (String[] args) ha un significato speciale in Java. Se hai esperienza con linguaggi come C, C++ o Go, dovresti già sapere che ogni programma in questi linguaggi possiede una funzione main. L'esecuzione del programma parte da questa funzione main.

In Java, devi scrivere questa funzione esattamente come public static void main (String[] args) altrimenti non funzionerà. Infatti, se la cambi anche di poco, Java inizierà a lamentarsi.

image-207

Il tipo del valore di ritorno è cambiato da void a int e la funzione adesso restituisce 0 alla fine. Come puoi vedere nella console, dice:

Error: Main method must return a value of type void in class Main, please 
define the main method as:
   public static void main(String[] args)

Ascolta il suggerimento e riporta il programma al suo stato precedente.

class Main {
  public static void main(String[] args) {
    System.out.println("Hello world!");
  }
}

Il metodo main è un metodo public e static vuol dire che puoi chiamarlo senza istanziare la sua classe.

void significa che la funzione non restituisce nessun valore e String[] args vuol dire che la funzione prende un array di stringhe come argomento. Questo array contiene argomenti da riga di comando passati al programma durante l'esecuzione.

System.out.println stampa delle stringhe sul terminale. Nell'esempio precedente, "Hello world!" è stata passata alla funzione, quindi otteniamo Hello world! stampato sul terminale.

In Java, ogni istruzione termina con un punto e virgola. A differenza di JavaScript o Python, il punto e virgola in Java è obbligatorio. Saltarne uno farà fallire la compilazione.

Sostanzialmente è tutto per questo programma. Se non hai capito ogni aspetto dei questa sezione parola per parola, non preoccuparti. Diventerà tutto più chiaro andando avanti.

Per ora, ricorda che la classe public di livello più alto in un file sorgente Java deve corrispondere al nome del file e che la funzione main di ogni programma Java deve essere definita come public static void main(String[] args).

Cos'è la JVM?

Nella sezione precedente, ho già usato un paio di volte la parola "bytecode". Ho anche detto che Java è "multi-piattaforma", che vuol dire che il codice scritto e compilato su una piattaforma può girare su qualsiasi piattaforma che ha Java installato.

Il tuo processore non comprende l'italiano o l'inglese. Infatti, l'unica cosa che comprende sono gli zero e gli uno, ovvero il linguaggio binario.

Quando scrivi e compili un programma in C++, il risultato è un file binario. Il processore lo comprende e, in base alla piattaforma designata del programma, questo file può essere diverso.

Ad esempio, prendiamo dei processori AMD64 e ARMv8-A. Questi processori hanno un set di istruzioni differenti. Quindi, per eseguire un programma su queste due diverse piattaforme, dobbiamo compilarlo separatamente.

Ma un programma Java può essere scritto una volta ed eseguito ovunque. Spero ti ricordi dei bytecode di cui abbiamo parlato nella sezione precedente. Quando compili del codice Java il risultato non è in binario ma in bytecode.

Il bytecode non è interamente binario, ma è comunque non leggibile da una persona, e nemmeno dal tuo processore.

Quindi invece di dare questo bytecode alla CPU, lo eseguiamo attraverso la Java Virtual Machine (o JVM in breve) che legge e interpreta il bytecode per la CPU.

Se vuoi comprendere l'architettura della JVM nel dettaglio, ti suggerisco questo articolo di Siben Nayak sull'argomento.

Cosa sono JRE e JDK?

JRE sta per Java Runtime Environment e JDK sta per Java Development Kit.

Il JRE o Java Runtime Environment racchiude un'implementazione della JVM insieme a un set di librerie richieste per eseguire i programmi Java.

Il JDK, d'altro canto, contiene il JRE insieme a tutte le librerie necessarie per sviluppare programmi in Java.

Quindi, se vuoi eseguire programmi Java sul tuo computer, dovrai installare il JRE. Mentre se vuoi sviluppare dei programmi Java, dovrai installare il JDK, di cui esistono diverse implementazioni.

Tra di esse, Java SE (Standard Edition) Development Kit di Oracle, OpenJDK, un'implementazione ufficiale di riferimento di Java SE (Standard Edition) Development Kit.

Come puoi immaginare dal suo nome, OpenJDK è open-source, quindi ne esistono svariate versioni. Se hai una macchina Linux e usi il tuo gestore di pacchetti di distro per installare JDK, è molto probabile che installerai una versione OpenJDK come Adoptium, Microsoft Build di OpenJDK e così via.

Spero che tu abbia capito che il JRE è un sovrainsieme della JVM e che il JDK è un sovrainsieme del JRE. Per ora, non preoccuparti delle diverse implementazioni o versioni, lo farai a tempo debito.

Come configurare Java sul tuo computer

Prima di tutto, vai su https://www.oracle.com/java/technologies/downloads/ e scarica l'ultima versione di Java SE Development Kit in base alla piattaforma su cui ti trovi:

image-208

Una volta terminato il download, avvia l'installazione e segui il processo di installazione premendo i pulsanti Avanti. Termina premendo il pulsante Chiudi nell'ultima pagina.

image-235

Il processo di installazione può variare su macOS e Linux ma dovresti essere in grado di venirne a capo.

Una volta che l'installazione è terminata, esegui il seguente comando sul tuo terminale:

java --version

# java 18.0.2 2022-07-19
# Java(TM) SE Runtime Environment (build 18.0.2+9-61)
# Java HotSpot(TM) 64-Bit Server VM (build 18.0.2+9-61, mixed mode, sharing)

Se funziona, hai installato con successo Java SE Development Kit sul tuo computer. Se invece vuoi usare OpenJDK, scarica pure Microsoft Build di OpenJDK o Adoptium e segui il processo di installazione.

Per i semplici programmi di esempio che scriveremo in questo articolo, non importa quale JDK utilizzi, ma per un utilizzo pratico, assicurati che la tua versione di JDK sia adatta al tipo di progetto su cui lavori.

Come installare un IDE Java sul tuo computer

Quando si tratta di Java, IntelliJ IDEA è senza dubbio il migliore ambiente di sviluppo integrato (IDE) in circolazione. Persino Google lo utilizza come base per Android Studio.

L'ultima versione dell'IDE può costare fino a € 169.00 all'anno. Ma se sei uno studente, puoi ottenere gratuitamente una licenza per studenti per tutti i prodotti JetBrains.

Esiste anche una edizione community open-source, completamente gratuita, che utilizzerò per tutto il manuale.

Vai sulla pagina di download di IntelliJ IDEA e scarica l'edizione community per la tua piattaforma.

image-344

Una volta finito il download, installa IntelliJ IDEA come qualsiasi altro software.

Come creare un nuovo progetto su IntelliJ IDEA

Usa la scorciatoia dal menu start per avviare IntelliJ IDEA. Ti apparirà la seguente finestra:

image-346

Usa il pulsante New Project e vedrai una finestra New Project:

image-347

Dai un nome descrittivo al tuo progetto. Lascia le altre opzioni come sono e premi il pulsante Create.

La creazione del progetto non dovrebbe richiedere più di qualche secondo e, fatto questo, ti apparirà la finestra qui sotto:

image-348

A sinistra trovi la finestra degli strumenti del progetto. Tutti il codice sorgente sarà all'interno di questa cartella src.

Clicca con il tasto destro sulla cartella src e vai su New > Java Class.

image-349

Per il prossimo passaggio, dai un nome come Main alla classe e assicurati che il tipo sottolineato sia Class.

image-350

Una nuova classe sarà creata con poche righe di codice.

image-351

Aggiorna il codice come segue:

public class Main {
    public static void main (String[] args) {
        System.out.println("Hello World!");
    }
}

Per eseguire questo codice, usa il pulsante verde sul lato destro della barra superiore.

image-352

Il codice sarà eseguito e l'output verrà mostrato nel terminale integrato nella parte inferiore della finestra.

image-353

Congratulazioni, hai ricreato con successo in IntelliJ IDEA il programma Hello World precedentemente discusso.

Come lavorare con le variabili in Java

Per lavorare con diversi tipi di dati in Java, puoi creare variabili di vario tipo. Ad esempio, se vuoi memorizzare la tua età in una nuova variabile, puoi farlo in questo modo:

public class Main {

	public static void main(String[] args) {
		// <tipo> <nome>
		int eta;

	}

}

Inizia a scrivere il tipo di dato o variabile. Visto che eta è un numero intero, il suo tipo sarà un intero, o int, seguito dal nome della variabile eta e da un punto e virgola.

Per ora, hai dichiarato la variabile ma non l'hai ancora inizializzata. In altre parole, la variabile non ha nessun valore. Puoi inizializzare la variabile come segue:

public class Main {

	public static void main(String[] args) {
		// <tipo> <nome>
		int eta;
		
		// <nome> = <valore>
		eta = 27;
        
		// stampa sul terminale "Ho 27 anni."
		System.out.println("Ho " + eta + " anni.");

	}

}

Per assegnare un valore, inizia a scrivere il nome della variabile che desideri inizializzare, seguito dal segno uguale (=, chiamato operatore di assegnazione) e dal valore che vuoi assegnare alla variabile. Non dimenticare il punto e virgola alla fine.

La chiamata della funzione System.out.println(); stamperà la frase Ho 27 anni. sulla console. Nel caso te lo stessi chiedendo, usare il segno più è uno dei tanti modi per stampare delle variabili in modo dinamico in mezzo a una frase.

Una cosa che devi tenere a mente è che non puoi usare una variabile non inizializzata in Java. Quindi se trasformi la riga eta = 27 in un commento, anteponendovi due barre oblique e provi a compilare il codice, il compilatore ti darà il seguente messaggio di errore:

Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
	The local variable eta may not have been initialized

	at variables.Main.main(Main.java:13)

La riga The local variable eta may not have been initialized indica che la variabile non è stata inizializzata.

Invece di dichiarare e inizializzare la variabile su righe diverse, puoi farlo in un colpo solo, in questo modo:

public class Main {

	public static void main(String[] args) {
		// <tipo> <nome> = <valore>
		int eta = 27;
        
		// stampa sul terminale "Ho 27 anni."
		System.out.println("Ho " + eta + " anni.");

	}

}

Il codice dovrebbe essere tornato alla normalità. Puoi anche cambiare il valore di una variabile quante volte desideri nel tuo codice.

public class Main {

	public static void main(String[] args) {
		int eta = 27;
        
		// aggiorna il valore a 28 invece di 27
		eta = 28;
        
		System.out.println("Ho " + eta + " anni.");

	}

}

In questo codice, il valore di eta cambia da 27 a 28 perché lo stiamo sovrascrivendo prima di stamparlo.

Tieni a mente che mentre puoi assegnare un valore a una variabile quante volte desideri, non puoi dichiarare la stessa variabile due volte.

public class Main {

	public static void main(String[] args) {
		// <tipo> <nome> = <valore>
		int eta = 27;
        
		int eta = 28;
        
		// stampa sul terminale
		System.out.println("Ho " + eta + " anni.");

	}

}

Se provi a compilare questo codice, il compilatore ti restituirà il seguente errore:

Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
	Duplicate local variable eta

	at variables.Main.main(Main.java:9)

La riga Duplicate local variable eta indica che la variabile è già stata dichiarata.

Oltre alle variabili, su internet potresti trovare il termine "letterale". I letterali sono variabili con valori con codifica fissa.

Ad esempio, in questo caso, eta = 27 non è calcolato dinamicamente. Il valore è scritto direttamente nel codice sorgente, quindi eta è un intero letterale.

Quali sono le regole per dichiarare le variabili?

Esistono alcune regole che riguardano la nomenclatura delle variabili in Java. Puoi utilizzare qualsiasi nome, a patto che non inizi con un numero e non contenga spazi.

Nonostante sia possibile iniziare il nome di una variabile con un trattino basso (_) o il simbolo del dollaro ($), non essere consapevole del loro utilizzo può rendere il codice difficile da leggere. I nomi delle variabili sono anche case sensitive, quindi eta e ETA sono due variabili diverse.

Un'altra cosa importante da ricordare è che non puoi usare nessuna delle parole chiave riservate in Java. Al momento, ne esistono circa 50. Puoi conoscere queste parole chiave grazie alla documentazione ufficiale, ma non preoccuparti di memorizzarle.

Man mano che continui a fare pratica, quelle importanti si annideranno nel tuo cervello automaticamente. E se stai ancora facendo confusione con la dichiarazione di una variabile, il compilatore sarà lì a ricordarti che c'è qualcosa di sbagliato.

Oltre alle regole, ci sono alcune convenzioni che dovresti seguire:

  • Il nome di una variabile dovrebbe iniziare con una lettera minuscola e non con un carattere speciale (come un trattino basso o il simbolo del dollaro).
  • Se il nome della variabile contiene più parole, usa il camel case: primaVariabile, secondaVariabile
  • Non usare lettere singole come nome: f, l

Finché segui queste regole e convenzioni, sei a posto. Se vuoi imparare di più sulle convenzioni di nomenclatura in generale, dai un'occhiata a questo mio articolo sull'argomento.

Cosa sono le variabili final?

Una variabile final in Java può essere assegnata solo una volta. Quindi se inizializzi una variabile come final, non puoi riassegnarla.

public class Main {

	public static void main(String[] args) {
		// final <tipo> <nome> = <valore>
		final int eta = 27;
		
		eta = 28;
        
		System.out.println("Ho " + eta + " anni.");

	}

}

Dato che la variabile eta è stata dichiarata come final, otterrai il seguente messaggio di errore:

Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
	The final local variable eta cannot be assigned. It must be blank and not using a compound assignment

	at variables.Main.main(Main.java:9)

Tuttavia, se lasci la variabile non inizializzata quando la dichiari, il codice funzionerà:

public class Main {

	public static void main(String[] args) {
		// final <tipo> <nome>
		final int eta;
		
		eta = 28;
        
		// stampa sul terminale "Ho 28 anni."
		System.out.println("Ho " + eta + " anni.");

	}

}

Quindi, dichiarare una variabile come final limiterà l'abilità di riassegnarle un valore. Se la lasci non inizializzata, poi sarai in grado di inizializzarla normalmente.

Quali sono i tipi di dati primitivi in Java?

A un livello elevato, ci sono due tipi di dati in Java: "primitivi" e "non primitivi" o "riferimenti" (reference).

I tipi primitivi contengono valori. Ad esempio, int è un tipo primitivo e contiene un valore intero.

Un tipo riferimento, d'altro canto, contiene il riferimento a una posizione in memoria in cui viene conservato un oggetto dinamico.

Esistono otto tipi di dati primitivi in Java.

Tipo Descrizione
byte 8 bit, numero intero con segno compreso tra -128 e 127
short 16 bit, numero intero con segno compreso tra -32,768 e 32,767
int 32 bit, numero intero con segno compreso tra -2147483648 e 2147483647
long 64 bit, numero intero con segno compreso tra -9223372036854775808 e 9223372036854775807
float 32 bit, numero in virgola mobile con precisione singola compreso tra 1.4E-45 e 3.4028235E38
double 64 bit, numero in virgola mobile con precisione doppia compreso tra 4.9E-324 e 1.7976931348623157E308
boolean true o false
char 16-bit, singolo carattere Unicode compreso tra \u0000 (o 0) e \uffff (o 65,535 incluso)

Sì, lo so che questa tabella spaventa ma non stressarti. Non devi memorizzarli.

Non hai bisogno di pensare a questi intervalli molto di frequente, e anche se lo fai, ci sono modi per visualizzarli nel tuo codice Java.

In ogni caso, se non sai cosa è un bit, ti consiglio questo breve articolo riguardo al linguaggio binario.

Hai già imparato come dichiarare un intero nella sezione precedente. Puoi dichiarare byte, short o long allo stesso modo.

Anche dichiarare un double funziona allo stesso modo, eccetto per il fatto che puoi assegnare un numero con un punto decimale invece di un intero:

public class Main {

	public static void main(String[] args) {
		double gpa = 4.8;
		
		System.out.println("Il mio GPA è " + gpa + ".");

	}
}

Se assegni un int a un double, come 4 invece di 4.8, l'output sarà 4.0 invece di 4, perché un double ha sempre il punto decimale.

Dato che double e float sono simili, potresti pensare che sostituire la parola chiave double con float convertirà la variabile in un numero in virgola mobile – ma non è corretto. Dovrai aggiungere una f o F dopo il valore:

public class Main {

	public static void main(String[] args) {
		float gpa = 4.8f;
		
		System.out.println("Il mio GPA è " + gpa + ".");

	}
}

Ciò accade perché, di default, ogni numero con un punto decimale è trattato come un double in Java. Se non aggiungi la f, il compilatore penserà che stai cercando di assegnare un valore double a una variabile float.

I dati di tipo boolean possono essere i valori true o false.

public class Main {

	public static void main(String[] args) {
		boolean fineSettimana = false;
		
		System.out.println(fineSettimana); // false

	}
}

Come puoi immaginare, false può essere trattato come no e true può essere trattato come sì.

I booleani diventeranno molto più utili una volta che imparerai le istruzioni condizionali. Per ora, ricorda soltanto che valori possono contenere.

Il tipo char può contenere qualsiasi carattere Unicode in un certo intervallo.

public class Main {

	public static void main(String[] args) {
		char segnoPercentuale = '%';
		
		System.out.println(segnoPercentuale); // %

	}
}

In questo esempio, abbiamo salvato il segno della percentuale in una variabile char per poi stamparla sul terminale.

Puoi usare anche delle sequenze Unicode di escape per stampare determinati simboli.

public class Main {

	public static void main(String[] args) {
		char simboloCopyright = '\u00A9';
		
		System.out.println(simboloCopyright); // ©

	}
}

La sequenza di escape Unicode per il simbolo del copyright, ad esempio, è \u00A9 e puoi trovare altre sequenze Unicode di escape in questo sito.

Tra questi 8 tipi di dati, quelli che userai più di frequente sono int, double, boolean e char.

Cos'è una conversione di tipo o casting?

Una conversione di tipo in Java può essere "implicita" o "esplicita". Quando il compilatore converte un tipo di dato con un intervallo più ristretto in uno più ampio automaticamente, si parla di conversione di tipo implicita o di widening.

public class Main {

	public static void main(String[] args) {
		int numero1 = 8;
		double numero2 = numero1;
		
		System.out.println(numero2); // 8.0
	}

}

Visto che un double è più grande di un intero, il compilatore può facilmente eseguire la conversione. Invece, se provi a fare il contrario, otterrai questo errore dal compilatore:

Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
	Type mismatch: cannot convert from double to int

	at operators.Main.main(Main.java:7)

Quando esegui una conversione implicita, il flusso di conversione dovrebbe essere il seguente:

widening-conversion

Naturalmente, puoi passare da short a double, ad esempio, saltando gli altri nel mezzo.

Puoi anche passare da dati con intervallo più ampio a dati con intervallo più ristretto. In questo caso, parliamo di conversione di tipo esplicita o di narrowing.

package datatypes;

public class Main {

	public static void main(String[] args) {
		double numero1 = 8.5;
		int numero2 = (int) numero1;
		
		System.out.println(numero2); // 8
	}

}

Precedentemente, abbiamo visto che se provi a convertire un tipo di dato più grande in uno più piccolo il compilatore si lamenta. Ma quando aggiungi l'operatore di cast (int) esplicitamente, fai vedere al compilatore chi è che comanda.

Facendo ciò, perdi una parte dei dati. Se cambi il numero double iniziale da 8.5 in 8, perdi delle informazioni. Quindi ogni volta che fai una conversione esplicita, fai attenzione.

Puoi anche convertire un char in un int come segue:

public class Main {

	public static void main(String[] args) {
		char carattere = 'F';
		int numero = carattere;
		
		System.out.println(numero); // 70
	}

}

70 è il codice ASCII per il carattere F – ecco il perché di questo output. Se vuoi saperne di più sui codici ASCII, il mio collega Kris Koishigawa ha scritto un eccellente articolo sull'argomento.

Il flusso di conversione in questo caso sarà opposto a quello visto in precedenza.

narrowing-conversion

Ti suggerisco di sperimentare convertendo diversi valori da un tipo all'altro e vedere cosa accade. In questo modo, approfondirai la tua comprensione e acquisirai sicurezza.

Cosa sono le classi wrapper in Java?

Le classi wrapper (o involucro) possono contenere i tipi di dati primitivi e trasformarli in riferimenti. Le classi wrapper sono disponibili per tutti gli otto tipi di dati primitivi.

Tipo Primitivo Classe Wrapper
int Integer
long Long
short Short
byte Byte
boolean Boolean
char Character
float Float
double Double

Puoi usare le classi wrapper come segue:

public class Main {
    public static void main (String[] args) {
        Integer eta = 27;
        Double gpa = 4.8;

        System.out.println(eta); // 27
        System.out.println(gpa); // 4.8
    }
}

Tutto ciò che devi fare è sostituire il tipo di dato primitivo con la classe wrapper equivalente. Questi riferimenti hanno anche metodi per estrarre da essi il tipo primitivo.

Ad esempio, eta.intValue() restituirà l'età come un intero primitivo e  gpa.doubleValue() restituirà il GPA come un primitivo double.

Esistono metodi del genere per tutti gli otto tipi di dati. Sebbene vedrai dati primitivi per la maggior parte del tempo, le classi wrapper torneranno utili in alcuni casi che discuteremo in una sezione successiva.

Come usare gli operatori in Java

Nell'ambito della programmazione, gli operatori sono determinati simboli che comunicano al compilatore di svolgere specifiche operazioni, come operazioni aritmetiche, di confronto o logiche.

Anche se in Java esistono sei tipi di operatori, qui non parlerò degli operatori bitwise. Parlare degli operatori bitwise in una guida per principianti potrebbe essere scoraggiante.

Cosa sono gli operatori aritmetici?

Gli operatori aritmetici sono quelli che puoi usare per svolgere operazioni aritmetiche. Ne esistono cinque:

Operatore Operazione
+ Addizione
- Sottrazione
* Moltiplicazione
/ Divisione
% Modulo (resto)

Le operazioni di addizione, sottrazione, moltiplicazione e divisione sono piuttosto intuitive. Dai un'occhiata all'esempio di codice qui sotto per capire:

public class Main {

	public static void main(String[] args) {
		int numero1 = 10;
		int numero2 = 5;
		
		System.out.println(numero1 + numero2); // 15
		System.out.println(numero1 - numero2); // 5
		System.out.println(numero1 * numero2); // 50
		System.out.println(numero1 / numero2); // 2
		System.out.println(numero1 % numero2); // 0

	}

}

Gli output delle prime quattro operazioni non hanno bisogno di spiegazioni. Nell'ultima operazione, abbiamo svolto un'operazione di modulo usando il simbolo %. Il risultato è 0 perché dividendo 10 per 2, il resto è nullo.

Addizione e moltiplicazione sono piuttosto semplici. Per la sottrazione, se il primo operando è minore del secondo, il risultato sarà un numero negativo, come nella vita reale.

Il tipo di dato con cui stai lavorando fa la differenza nel risultato delle operazioni di divisione e modulo.

public class Main {

	public static void main(String[] args) {
		int numero1 = 8;
		int numero2 = 5;
		
		System.out.println(numero1 / numero2); // 1
	}

}

Anche se il risultato di questa operazione dovrebbe essere 1.6, ciò non accade. In Java, se dividi un intero per un altro intero, il risultato sarà un intero. Ma se cambi entrambi o uno dei due in un float/double, tutto torna alla normalità.

public class Main {

	public static void main(String[] args) {
		double numero1 = 8;
		double numero2 = 5;
		
		System.out.println(numero1 / numero2); // 1.6
	}

}

Questo principio si applica anche all'operatore modulo. Se uno o entrambi gli operandi sono float/double, il risultato sarà un float/double.

Cosa sono gli operatori di assegnazione?

Abbiamo già usato l'operatore di assegnazione in una sezione precedente.

public class Main {

	public static void main(String[] args) {
		// <tipo> <nome> = <valore>
		int eta = 27;
        
		// stampa il valore di eta sul terminale
		System.out.println(eta);

	}

}

Quando usi il simbolo = per assegnare un valore a una variabile, funziona come un operatore di assegnazione. Ma questa non è l'unica forma di questo operatore.

Combinando il normale operatore di assegnazione con gli operatori aritmetici, puoi ottenere risultati differenti.

Operatore Operazione Equivalente a
+= a += b a = a + b
-= a -= b a = a - b
*= a *= b a = a * b
/= a /= b a = a / b
%= a %= b a = a % b

Il seguente esempio di codice dovrebbe rendere le cose più chiare:

package operators;

public class Main {

	public static void main(String[] args) {
		double numero1 = 10;
		double numero2 = 5;
		
		numero1 += numero2;
		
		System.out.println(numero1); // 15
	}

}

Gli altri operatori funzionano allo stesso modo. Svolgono l'operazione e poi ne assegnano il risultato all'operando di sinistra.

Potrei darti una dimostrazione degli altri ma credo che se li provi tu stesso li capirai meglio. Dopotutto, l'esercizio e la pratica sono i soli modi per consolidare le tue conoscenze.

Cosa sono gli operatori di confronto?

Gli operatori di confronto sono utilizzati per verificare la relazione tra due operandi. Ad esempio, se un operando è uguale a un altro oppure no.

Gli operatori di confronto restituiscono true o false a seconda dell'operazione svolta.

In Java, esistono sei operatori di confronto.

Operatore Descrizione Esempio
== Uguale 5 == 8 restituisce false
!= Diverso 5 != 8 restituisce true
> Maggiore 5 > 8 restituisce false
< Minore 5 < 8 restituisce true
>= Maggiore o uguale 5 >= 8 restituisce false
<= Minore o uguale 5 <= 8 restituisce true

Il seguente esempio di codice mostra l'utilizzo di questi operatori:

public class Main {

	public static void main(String[] args) {
		double numero1 = 10;
		double numero2 = 5;
		
		System.out.println(numero1 == numero2); // false
		System.out.println(numero1 != numero2); // true
		System.out.println(numero1 > numero2); // true
		System.out.println(numero1 < numero2); // false
		System.out.println(numero1 >= numero2); // true
		System.out.println(numero1 <= numero2); // false
	}

}

L'utilizzo pratico di questi operatori diventerà più chiaro una volta che imparerai a conoscere le istruzioni condizionali in una sezione successiva.

Puoi usare questi operatori anche con i caratteri.

public class Main {

	public static void main(String[] args) {
		char letteraMinuscola = 'a';
		char letteraMaiuscola = 'A';
		
		System.out.println(letteraMinuscola > letteraMaiuscola); // ???
	}

}

Quale credi che sarà l'output di questo codice? Ti invito a scoprirlo da solo. Ricordi il valore ASCII dei caratteri? Hanno un ruolo importante nell'output di questo programma.

Cosa sono gli operatori logici?

Immagina il caso in cui un programma che hai scritto può essere usato solo da persone che hanno più di 18 anni ma meno di 40. La logica dovrebbe essere la seguente:

puoi eseguire il programma se ->
	eta >= 18 e eta <= 40

Oppure il caso in cui un utente deve essere uno studente della tua scuola o un membro della libreria per prendere libri in prestito. In questa situazione, la logica sarebbe:

può prendere libri in prestito se ->
	studenteDellaScuola o membroDellaLibreria

Queste decisioni logiche possono essere prese usando operatori logici. Esistono tre operatori del genere in Java.

Operatore Esempio Descrizione
And logico (&&) eta >= 18 && eta <= 40 Il risultato è true se entrambe le condizioni sono true
Or logico (||) studenteDellaScuola || membroDellaLibreria Il risultato è true se una o entrambe le condizioni sono true
Not (!) !membroDellaLibreria Il risultato è false se la condizione è true e viceversa

Vediamo del codice con questi operatori, partendo dall'operatore logico and (&&):

public class Main {

	public static void main(String[] args) {
		int eta = 20;
		
		System.out.println(eta >= 18 && eta <= 40); // true
	}

}

In questo caso, ci sono due condizioni ai lati dell'operatore &&. Se e soltanto se entrambe le condizioni sono valutate come true, l'operazione and risulta essere true.

Se la prima condizione è false, il computer non valuterà l'altra condizione e restituirà false, perché se la prima è false, non c'è modo che l'intera operazione sia true.

L'operatore logico or (||) funziona in modo simile, ma in questo caso, se una delle due condizioni è vera, lo sarà anche il risultato dell'operazione:

public class Main {

	public static void main(String[] args) {
		boolean studenteDellaScuola = true;
		boolean membroDellaLibreria = false;
		
		System.out.println(studenteDellaScuola || membroDellaLibreria); // true
	}

}

Se la prima condizione di un'operazione logica or è true, il computer non valuterà l'altra condizione è restituirà true, perché se la prima condizione viene valutata come true il risultato dell'operazione sarà true indipendentemente dall'altra condizione.

Infine, l'operatore not (!) valuta l'opposto della condizione. Dai un'occhiata all'esempio di codice qui sotto:

public class Main {

	public static void main(String[] args) {
		boolean membroDellaLibreria = true;
		
		System.out.println(membroDellaLibreria); // true
		System.out.println(!membroDellaLibreria); // false
	}

}

Come puoi vedere, l'operatore not restituisce l'opposto di un dato valore booleano. L'operatore ! è un operatore unario, che vuol dire che opera su un singolo operando.

public class Main {

	public static void main(String[] args) {
		boolean membroDellaLibreria = true;
		boolean studenteDellaScuola = false;
		
		System.out.println(!studenteDellaScuola || membroDellaLibreria); // true
	}

}

In questo esempio, l'operatore not trasforma studenteDellaScuola in true, e l'operazione viene valutata true. Ma se modifichi il codice in questo modo:

public class Main {

	public static void main(String[] args) {
		boolean membroDellaLibreria = true;
		boolean studenteDellaScuola = false;
		
		System.out.println(!(studenteDellaScuola || membroDellaLibreria)); // false
	}

}

L'operazione logica or avverrà prima e sarà true. L'operatore not ne trasformerà il risultato in false.

Anche se abbiamo usato due operandi per ogni operatore, puoi usarne quanti ne desideri. Puoi anche usare diversi operatori insieme.

public class Main {

	public static void main(String[] args) {
		boolean studenteDellaScuola = true;
		boolean membroDellaLibreria = false;
		int eta = 10;
		
		System.out.println(studenteDellaScuola || membroDellaLibreria && eta > 18); // ???
	}

}

Quale pensi che sarà l'output di questo codice? Ti suggerisco di scoprirlo da solo. :)

Cosa sono gli operatori unari?

Esistono alcuni operatori che sono usati con un operando alla volta e vengono chiamati operatori unari. Nonostante ne esistano cinque, parlerò soltanto di due di loro.

Operatore Descrizione
Incremento (++) Incrementa un dato valore di 1
Decremento (--) Decrementa un dato valore di 1

Il seguente codice ne mostra bene il funzionamento:

public class Main {

	public static void main(String[] args) {
		int punteggio = 95;
		int turni = 11;
		
		punteggio++;
		turni--;
		
		System.out.println(punteggio); // 96
		System.out.println(turni); // 10
	}

}

Puoi anche usare questi operatori come prefissi:

public class Main {

	public static void main(String[] args) {
		int punteggio = 95;
		int turni = 11;
		
		++punteggio;
		--turni;
		
		System.out.println(punteggio); // 96
		System.out.println(turni); // 10
	}

}

Per ora, tutto semplice. Ma ci sono alcune leggere differenze tra la sintassi con suffisso e prefisso che devi conoscere. Guarda il codice qui sotto:

package operators;

public class Main {

	public static void main(String[] args) {
		int punteggio = 95;
		
		
		System.out.println(++punteggio); // 96
		System.out.println(punteggio); // 96
	}

}

Questo è il risultato atteso. L'operatore decremento usato come prefisso funziona allo stesso modo. Ma guarda cosa accade se passiamo alla sintassi con suffisso:

package operators;

public class Main {

	public static void main(String[] args) {
		int punteggio = 95;
		
		
		System.out.println(punteggio++); // 95
		System.out.println(punteggio); // 96
	}

}

Disorientante, vero? Quale credi che sia il valore reale della variabile adesso? È 96, vediamo perché.

Quando usiamo la sintassi con il suffisso all'interno della funzione print, la funzione incontra prima la variabile e poi la incrementa. Ed ecco perché nella seconda riga stampa il nuovo valore aggiornato.

Nel caso della sintassi con il prefisso, la funzione incontra l'operatore di incremento e svolge l'operazione. Poi prosegue stampando il valore aggiornato.

Stai attento perché questa piccola differenza può prenderti alla sprovvista. Oppure prova a evitare di incrementare o decrementare all'interno di chiamate di funzione.

Come usare le stringhe in Java

In Java, il tipo String è uno dei rifermenti usati più di frequente. È un insieme di caratteri che puoi usare per formare righe di testo in un programma.

Esistono due modi per creare nuove stringhe in Java. Il primo è quello letterale:

public class Main {
	public static void main(String[] args) {
		String nome = "Farhan";
        
		System.out.println("Il mio nome è " + nome + ".");
	}

}

Come puoi vedere, dichiarare e usare una variabile di tipo String in questo modo non è molto diverso da dichiarare tipi primitivi in Java.

Il secondo modo per creare una variabile di tipo String è usare l'operatore new.

public class Main {
	public static void main(String[] args) {
		// <tipo> <nome> = new <tipo>(<valore>)
		String nome = new String("Farhan");
        
		System.out.println("Il mio nome è " + nome + ".");
	}

}

Questo programma funziona come il precedente ma con una leggera differenza.

La JVM mantiene una porzione della memoria del computer per memorizzare le stringhe. Questa porzione è detta string pool (o pool di stringhe).

Ogni volta che crei una nuova String nel modo letterale, il JVM controlla prima se la stringa esiste già nel pool. Se esiste, il JVM la riusa, altrimenti la crea.

D'altro canto, quando utilizzi l'operatore new, il JVM crea sempre un nuovo oggetto String. Il seguente programma dimostra chiaramente questo concetto:

public class Main {

	public static void main(String[] args) {
		String stringaLetterale1 = "abc";
		String stringaLetterale2 = "abc";
		
		String oggettoStringa1 = new String("abc");
		String oggettoStringa2 = new String("abc");
		
		System.out.println(stringaLetterale1 == stringaLetterale2);
		System.out.println(oggettoStringa1 == oggettoStringa2);

	}

}

Come potresti già sapere, l'operatore == è usato per verificare l'uguaglianza. L'output del programma sarà:

true
false

Dato che abc era già nello string pool, la variabile stringaLetterale2 la riutilizza. Nel caso di oggetti stringa invece, si tratta di due entità diverse.

Come formattare una stringa

Abbiamo già visto l'utilizzo dell'operatore + per concatenare stringhe o formattarle in un modo specifico.

Questo approccio funziona finché non hai tante aggiunte a una stringa. È facile fare confusione con la posizione delle virgolette.

Un modo migliore di formattare una stringa è il metodo String.format().

public class Main {
	public static void main(String[] args) {
		String nome = "Farhan";
		int eta = 27;
		
		String stringaFormattata = String.format("Il mio nome è %s e ho %d anni.", nome, eta);
        
		System.out.println(stringaFormattata);
	}

}

Il metodo prende una stringa con degli specificatori di formato come primo argomento e gli argomenti per sostituire gli specificatori come argomenti successivi.

Nel codice qui sopra, i caratteri %s e %d sono specificatori di formato. Sono responsabili di comunicare al compilatore che quella parte della stringa deve essere sostituita con qualcosa.

Poi il compilatore sostituirà %s con nome e %d con eta. L'ordine degli specificatori deve corrispondere all'ordine degli argomenti e gli argomenti devono corrispondere al tipo indicato dallo specificatore.

I caratteri %s e %d non sono a caso. Sono specifici per stringhe e per interi decimali (in base dieci). Ecco una tabella degli specificatori usati comunemente:

Specificatore Tipo di dato
%b, %B Booleano
%s, %S Stringa
%c, %C Carattere Unicode
%d Intero in base dieci
%f Numero in virgola mobile

Ci sono anche %o per gli interi in base otto, %x o %X per i numeri esadecimali e %e o %E per le notazioni scientifiche, ma dato che non ne parlerò in questo manuale non li ho inseriti.

Proprio come gli specificatori %s e %d che hai visto, puoi usare gli altri specificatori per il tipo di dato corrispondente. E, nel caso te lo stessi chiedendo, lo specificatore %f funziona sia per i float che per i double.

Come ottenere la lunghezza di una stringa e verificare se una stringa è vuota

Controllare la lunghezza di una stringa o verificare se una stringa è vuota prima di svolgere delle operazioni è piuttosto comune.

Ogni oggetto stringa ha un metodo length() che ne restituisce la lunghezza. È come la proprietà length per gli array.

public class Main {
	public static void main(String[] args) {
		String nome = "Farhan";
        
		System.out.println(String.format("La lunghezza di questa stringa è: %d.", nome.length())); // 6
	}

}

Il metodo restituisce la lunghezza come un intero, che puoi usare insieme allo specificatore di formato per gli interi.

Per verificare se una stringa è vuota o meno, puoi usare il metodo isEmpty(). Proprio come il metodo length(), può essere usato per ogni oggetto stringa.

public class Main {
	public static void main(String[] args) {
		String nome = "Farhan";
		
		if (nome.isEmpty()) {
			System.out.println("Non c'è nessun nome qui");
		} else {
			System.out.println(String.format("Okay, ci penso io a %s.", nome));
		}
	}

}

Il metodo restituisce un valore booleano che puoi usare direttamente in una istruzione if. Il programma controlla se il nome è vuoto oppure no e stampa risposte diverse nei due casi.

Come dividere e unire delle stringhe

Il metodo split() può essere usato per dividere una stringa sulla base di un'espressione regolare.

import java.util.Arrays;

public class Main {
	public static void main(String[] args) {
		String nome = "Farhan Hasin Chowdhury";

		System.out.println(Arrays.toString(nome.split(" ")));
	}

}

Il metodo restituisce un array di stringhe. Ogni stringa nell'array è una sottostringa della stringa originale. Qui, ad esempio, abbiamo diviso la stringa Farhan Hasin Chowdhury in corrispondenza di ogni spazio, quindi l'output sarà [Farhan, Hasin, Chowdhury].

Come promemoria: un array è un insieme di più dati dello stesso tipo.

Dato che il metodo prende un'espressione regolare come argomento, possiamo sfruttarle per svolgere operazioni di suddivisione più complicate.

Puoi anche riunire insieme l'array per riformare la stringa, in questo modo:

public class Main {
	public static void main(String[] args) {
		String nome = "Farhan Hasin Chowdhury";
		
		String sottostringhe[] = nome.split(" ");
		
		String nomeUnito = String.join(" ", sottostringhe);

		System.out.println(nomeUnito); // Farhan Hasin Chowdhury
	}

}

Il metodo join() può anche aiutarti a unire più stringhe insieme fuori da un array.

public class Main {
	public static void main(String[] args) {
		System.out.println(String.join(" ", "Farhan", "Hasin", "Chowdhury")); // Farhan Hasin Chowdhury
	}

}

Come convertire una stringa in maiuscolo o minuscolo

Convertire una stringa in maiuscolo o minuscolo è un'operazione molto diretta in Java, che puoi svolgere rispettivamente con i metodi toUpperCase() e toLowerCase():

public class Main {
	public static void main(String[] args) {
		String nome = "Farhan Hasin Chowdhury";

		System.out.println(name.toUpperCase()); // FARHAN HASIN CHOWDHURY
		
		System.out.println(name.toLowerCase()); // farhan hasin chowdhury
	}

}

Come confrontare due stringhe

Dato che le stringhe sono di tipo riferimento, non puoi confrontarle con l'operatore ==.

Il metodo equals() controlla se due stringhe sono uguali o meno e il metodo equalsIgnoreCase() fa la stessa cosa ignorando le differenze nelle maiuscole/minuscole.

public class Main {
	public static void main(String[] args) {
		String nome = "Farhan Hasin Chowdhury";
		String nomeMaiuscolo = nome.toUpperCase();

		System.out.println(nome.equals(nomeMaiuscolo)); // false
		
		System.out.println(nome.equalsIgnoreCase(nomeMaiuscolo)); // true
	}

}

Come sostituire caratteri o sottostringhe in una stringa

Il metodo replace() può essere usato per sostituire caratteri o intere sottostringhe di una data stringa.

package strings;

public class Main {
	public static void main(String[] args) {
		String loremIpsumStd = "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.";

		System.out.println(String.format("Standard lorem ipsum text: %s", loremIpsumStd));
		
		String loremIpsumHalfTranslated = loremIpsumStd.replace("Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium", "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system");
		
		System.out.println(String.format("Translated lorem ipsum text: %s", loremIpsumHalfTranslated));
	}

}

Qui, la stringa loremIpsumStd contiene una porzione del testo lorem ipsum originale. In seguito, la prima riga della stringa viene sostituita e la nuova stringa viene salvata nella variabile loremIpsumHalfTranslated.

Come verificare se una stringa contiene una sottostringa

Il metodo contains() può verificare se una data stringa contiene una certa sottostringa oppure no.

public class Main {
	public static void main(String[] args) {
		String testo = "Le rose sono rosse, le viole sono blu";

		if (testo.contains("blu")) {
			System.out.println("Il testo contiene la parola blu.");
		} else {
			System.out.println("Il testo non contiene la parola blu.");
		}
	}

}

Il metodo restituisce un valore booleano, quindi puoi utilizzarlo in una qualsiasi istruzione condizionale.

Questi erano alcuni dei più comuni metodi per stringhe. Se ne vuoi conoscere altri, consulta la documentazione ufficiale.

Quali sono i diversi modi di inserire input e ottenere output di dati?

Finora abbiamo imparato il metodo System.out.println() per stampare informazioni sul terminale. Abbiamo anche usato il metodo String.format() in una sezione precedente.

In questa sezione, imparerai a conoscere alcuni metodi parenti di System.out.println() e anche come prendere un input da un utente.

Prendere un input da un utente è estremamente semplice in linguaggi come Python. Tuttavia, in Java richiede qualche riga di codice in più.

import java.util.Scanner;

public class Main {

	public static void main(String[] args) {
		Scanner scanner = new Scanner(System.in);
		
		System.out.print("Come ti chiami? ");
		String nome = scanner.nextLine();
		
		System.out.printf("Ciao %s. Quanti anni hai? ", nome);
		int eta = scanner.nextInt();
		
		System.out.printf("Grande! %d anni sono un'ottima età per iniziare a programmare.", eta);
		
		scanner.close();

	}

}

La classe java.util.Scanner è necessaria per ricevere input utente. Puoi importarla nel tuo programma usando la parola chiave import.

Poi, dovrai creare una nuova istanza della classe Scanner usando la parola chiave new. Quando crei una nuova istanza, dovrai esplicitare il flusso di input desiderato.

Potresti voler prendere un input da un utente o da un file. In ogni caso, devi comunicarlo al compilatore. System.in è il flusso standard di input e output.

L'oggetto scanner ha metodi come nextLine() per prendere degli input stringa, nextInt() per prendere input interi, nextDouble() per input double e così via.

Nel codice precedente, il metodo scanner.nextLine() chiederà una stringa all'utente e restituirà l'input inserito con un carattere nuova riga dopo di esso.

Poi, il metodo scanner.nextInt() chiederà un intero e restituirà l'input dato dall'utente.

Potrebbe essere la prima volta che vedi il metodo System.out.printf(). Oltre al metodo System.out.println(), esiste anche il metodo System.out.print() che stampa una data stringa senza aggiungere un carattere nuova a riga al suo termine.

System.out.printf() è una sorta di combinazione dei metodi System.out.print() e String.format(). Puoi usare gli specificatori di formato discussi precedente anche in questo metodo.

Una volta che l'input è stato accettato, è necessario chiudere l'oggetto scanner. Puoi farlo semplicemente chiamando il metodo scanner.close().

Semplice, vero? Complichiamo un po' le cose.

import java.util.Scanner;

public class Main {

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

        System.out.print("Come ti chiami? ");
        String nome = scanner.nextLine();

        System.out.printf("Ciao %s. Quanti anni hai? ", nome);
        int eta = scanner.nextInt();

        System.out.printf("Grande! %d anni sono un'ottima età per iniziare a programmare. \nChe linguaggio preferisci? ", eta);
        String linguaggio = scanner.nextLine();

        System.out.printf("Ah! %s è un linguaggio di programmazione molto valido.", linguaggio);

        scanner.close();

    }

}

Ho aggiunto un'istruzione scanner.nextLine() dopo la chiamata del metodo scanner.nextInt(). Funzionerà?

La risposta è no. Il programma salterà semplicemente l'ultimo prompt di input e stamperà l'ultima riga. Questo comportamento non è esclusivo di scanner.nextInt(). Se usi scanner.nextLine() dopo gli altri metodi nextQualcosa(), dovrai fare i conti con questo problema.

In breve, questo accade perché premi Invio per il metodo scanner.nextInt(), che prende l'intero e lascia il carattere nuova riga nel buffer dell'input.

Quindi, quando viene invocato scanner.nextLine(), consuma quel carattere nuova riga alla fine dell'input. La soluzione più semplice a questo problema è scrivere una chiamata aggiuntiva di scanner.nextLine() dopo le altre chiamate di un metodo scanner.

import java.util.Scanner;

public class Main {

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

        System.out.print("Come ti chiami? ");
        String nome = scanner.nextLine();

        System.out.printf("Ciao %s. Quanti anni hai? ", nome);
        int eta = scanner.nextInt();

        // consuma il carattere nuova riga
        scanner.nextLine();

        System.out.printf("Grande! %d anni sono un'ottima età per iniziare a programmare. \nChe linguaggio preferisci? ", eta);
        String linguaggio = scanner.nextLine();

        System.out.printf("Ah! %s è un linguaggio di programmazione molto valido.", linguaggio);

        scanner.close();

    }

}

Esiste un altro modo per risolvere questo problema, ma non ne parlerò qui. Se sei interessato, dai un'occhiata a questo mio articolo sull'argomento.

Come usare le istruzioni condizionali in Java

Puoi usare le istruzioni condizionali per prendere delle decisioni sulla base di condizioni specifiche.

Per questo, utilizziamo l'istruzione if come mostrato qui sotto:

public class Main {

	public static void main(String[] args) {
		int eta = 20;
		
		// if (condizione) {...}
		if (eta >= 18 && eta <= 40) {
			System.out.println("Puoi usare il programma");
		}
		
	}

}

L'istruzione inizia con un if, seguito dalla condizione all'interno di parentesi. Se la condizione viene valutata come true, il codice all'interno delle parentesi graffe sarà eseguito.

Il codice racchiuso all'interno di parentesi graffe è detto blocco di codice.

Se cambi il valore di eta in 50, l'istruzione print non sarà eseguita e non ci sarà alcun output sulla console. Per questo tipo di situazioni in cui la condizione viene valutata come false, puoi aggiungere un blocco else:

public class Main {

	public static void main(String[] args) {
		int eta = 20;
		
		if (eta >= 18 && age <= 40) {
			System.out.println("Puoi usare il programma");
		} else {
			System.out.println("Non puoi usare il programma");
		}
		
	}

}

Ora, se la condizione viene valutata come false, sarà eseguito il codice nel blocco else e vedrai Non puoi usare il programma stampato sul terminale.

Puoi anche avere condizioni multiple all'interno di una scaletta if-else if-else:

public class Main {

	public static void main(String[] args) {
		int eta = 50;
		boolean studenteDellaScuola = true;
		boolean membroDellaLibreria = false;
		
		// if (condizione) {...}
		if (eta >= 18 && eta <= 40) {
			System.out.println("Puoi usare il programma");
		} else if (studenteDellaScuola || membroDellaLibreria) {
			System.out.println("Puoi usare il programma per un tempo limitato");
		} else {
			System.out.println("Non puoi usare il programma");
		}
		
	}

}

Se la prima condizione viene valutata come false, viene testata la seconda condizione. Se la seconda condizione viene valutata come true, allora sarà eseguito il codice nelle parentesi graffe corrispondenti. Se le condizioni relative alle istruzioni if ed else if sono entrambe false, allora sarà eseguito il blocco else.

Puoi anche annidare delle istruzioni if all'interno di altre istruzioni if come segue:

package operators;

public class Main {

	public static void main(String[] args) {
		int eta = 20;
		
		if (eta >= 18 && eta <= 40) {
			boolean studenteDellaScuola = true;
			boolean membroDellaLibreria = false;
			
			if (studenteDellaScuola || membroDellaLibreria) {
				System.out.println("Puoi usare il programma");
			}
		} else {
			System.out.println("Non puoi usare il programma");
		}
		
	}

}

In questo caso, l'istruzione if interna sarà testata solo se la prima istruzione if sarà valutata come true.

Cos'è un'istruzione switch-case?

Oltre ai blocchi if-else, esistono altri casi in cui è possibile definire condizioni multiple in una struttura che funge da interruttore.

import java.util.Scanner;

public class Main {

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

        System.out.print("Qual è il primo operando? ");
        int a =scanner.nextInt();

        // consuma il carattere nuova riga
        scanner.nextLine();

        System.out.print("Qual è il secondo operando? ");
        int b = scanner.nextInt();

        // consuma il carattere nuova riga
        scanner.nextLine();

        System.out.print("Quale operazione vuoi svolgere? ");
        String operazione = scanner.nextLine();

        switch (operazione) {
            case "addizione":
                System.out.printf("%d + %d = %d", a, b, a+b);
                break;
            case "sottrazione":
                System.out.printf("%d - %d = %d", a, b, a-b);
                break;
            case "moltiplicazione":
                System.out.printf("%d * %d = %d", a, b, a*b);
                break;
            case "divisione":
                if (b == 0) {
                    System.out.print("Non puoi dividere per zero!");
                } else {
                    System.out.printf("%d / %d = %d", a, b, a / b);
                }
                break;
            default:
                System.out.printf("Operazione invalida!");
        }

        scanner.close();

    }

}

Questo è un programma per fare calcoli estremamente semplice. Il programma richiede due numeri all'utente e poi chiede che operazione eseguire.

Ogni istruzione switch-case ha uno switch e case multipli. Quando dici "addizione", il programma verifica se il valore dello switch o della variabile operazione è o meno addizione.

Se corrisponde, il corpo del case sarà eseguito. Se nessuno dei casi corrisponde, viene eseguito il default.

Per quanto riguarda l'istruzione break, fa esattamente quello che sembra: ferma il programma dall'eseguire i casi successivi.

Se rimuovi le istruzioni break, tutti i casi vengono eseguiti uno dopo l'altro fino al caso default.

Cos'è la visibilità di una variabile in Java?

La visibilità rappresenta il tempo di vita e l'accessibilità di una variabile. A seconda di dove dichiari una variabile potresti essere o non essere in grado di accedervi da altre posizioni.

Prendi come esempio il codice qui sotto:

public class Main {

	public static void main(String[] args) {
		int eta = 20;
		
		if (eta >= 18 && eta <= 40) {
			// la variabile eta è accessibile qui
			// i booleani non sono accessibili qui

			boolean studenteDellaScuola = true;
			boolean membroDellaLibreria = false;
			
			if (studenteDellaScuola || membroDellaLibreria) {
				// i booleani sono accessibili qui
				// la variabile eta è accessibile qui

				System.out.println("Puoi usare il programma");
			}
		} else {
			// la variabile eta è accessibile qui
			// i booleani non sono accessibili qui
            
			System.out.println("Non puoi usare il programma");
		}
		
	}

}

Qui, la variabile eta è dichiarata all'interno del blocco di codice di class. Ciò vuol dire che puoi accedere a questa variabile all'interno di tutto il blocco class senza alcun problema. Dato che la variabile è accessibile nell'intera istanza di classe, è una variabile d'istanza.

Invece, le variabili studenteDellaScuola e membroDellaLibreria sono state dichiarate nel blocco di codice della prima istruzione if, quindi non sono accessibili fuori da quel blocco di codice.

Ma possono essere raggiunte da qualsiasi blocco di codice annidato nel primo blocco if. Le variabili così dichiarate sono dette variabili locali.

Esistono anche variabili di classe dichiarate con la parola chiave static ma avrai modo di saperne di più nelle sezioni dedicate alla programmazione orientata agli oggetti.

Per ora, la regola generale è che è possibile accedere a una variabile all'interno del blocco di codice in cui è stata dichiarata o dai blocchi in esso annidati.

Quali sono i valori di default di una variabile in Java?

Hai imparato che in Java hai bisogno di inizializzare una variabile dopo averla dichiarata, altrimenti non sarai in grado di usarla. Beh, non è vero in tutti i casi.

Se dichiari una variabile nel livello di classe, le verrà assegnato un valore predefinito dal compilatore.

public class Main {
	
	// impostata a 0 come valore predefinito
	static int eta;

	public static void main(String[] args) {
		System.out.println(eta); // 0
	}
}

Dato che il metodo main è static, può accedere solo a variabili static nel livello di classe. Parleremo di static molto dettagliatamente nella sezione relativa alla programmazione orientata agli oggetti.

Ma se sposti la dichiarazione della variabile all'interno del metodo main, diventa locale e non ottiene alcun valore predefinito.

public class Main {
	public static void main(String[] args) {
		// resta non inizializzata
		int eta;
    
		System.out.println(eta); // errore di compilazione
	}
}

Questo codice restituirà l'errore di compilazione The local variable eta may not have been initialized.

Alle variabili viene assegnato un valore di default in base al loro tipo. Nella maggior parte dei casi, sarà 0 o null. Ecco una lista dei valori predefiniti dei tipi primitivi:

Tipo di dato Valore predefinito
byte 0
short 0
int 0
long 0L
float 0.0f
double 0.0d
char '\u0000'
boolean false

Ad ogni variabile di tipo riferimento sarà assegnato il valore null di default. Discuteremo di tipi, classi e oggetti di tipo riferimento nelle sezioni dedicate alla programmazione orientata agli oggetti.

Come lavorare con gli array in Java

Hai già imparato come dichiarare variabili singole e usarle in un programma. Qui entrano in scena gli array.

Gli array sono strutture di dati che contengono valori multipli dello stesso tipo in posizioni di memoria sequenziali. Gli array possono essere formati da qualsiasi tipo di dati primitivi e non.

Puoi creare un array in Java in questo modo:

public class Main {

	public static void main(String[] args) {
		// <tipo> <nome>[] = new <tipo>[<lunghezza>]
		char vocali[] = new char[5];

	}
}

Occorre scrivere il tipo di dato che l'array deve contenere, in questo caso char, scrivendo poi il nome dell'array, vocali, seguito da parentesi quadre. Le parentesi quadre comunicano a Java che stai dichiarando un array di caratteri e non una variabile carattere regolare.

Poi, bisogna aggiungere il simbolo uguale seguito dall'operatore new, usato per creare nuovi oggetti in Java. Dato che gli array sono di tipo riferimento in Java, new è necessario per creare nuove istanze.

La dichiarazione termina scrivendo di nuovo il tipo e delle parentesi quadre contenenti la lunghezza dell'array. Qui, 5 significa che l'array conterrà non più di cinque elementi.

Lavorando con una variabile singola, possiamo farvi riferimento semplicemente con il suo nome. Ma nel caso degli array, ogni elemento possiede un indicee gli array hanno indice a base zero. Questo vuol dire che il primo elemento di un array ha indice 0 e non 1.

Per accedere a un elemento di un array, puoi scrivere il nome dell'array – in questo caso vocali –  seguito dalle parentesi quadre con l'indice desiderato. Quindi se vuoi accedere al primo elemento nell'array, puoi farlo in questo modo:

public class Main {

	public static void main(String[] args) {
		char vocali[] = new char[5];
		
		vocali[0] = 'a';
	}
}

A questo punto, vocali[0] è simile a una normale variabile carattere. Puoi stamparla, assegnarle un nuovo valore, svolgere calcoli nel caso di variabili di tipo numerico e così via.

Visto che l'array è vuoto al momento, assegno il carattere a al primo indice. Puoi assegnare il resto delle vocali agli altri indici come segue:

public class Main {

	public static void main(String[] args) {
		char vocali[] = new char[5];
		
		vocali[0] = 'a';
		vocali[1] = 'e';
		vocali[2] = 'i';
		vocali[3] = 'o';
		vocali[4] = 'u';

	}
}

Dato che gli indici partono da 0, l'ultimo sarà uguale alla lunghezza dell'array -1, in questo caso 4. Se provi a assegnare altri elementi all'array, ad esempio vocali[5] = 'x';, il compilatore ti darà il seguente errore:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
	at arrays.Main.main(Main.java:18)

Gli array non possono essere stampati come le variabili normali. Occorre usare un loop oppure convertire l'array in una stringa. Visto che non abbiamo ancora parlato dei loop, userò il secondo metodo.

import java.util.Arrays;

public class Main {

	public static void main(String[] args) {
		char vocali[] = new char[5];
		
		vocali[0] = 'a';
		vocali[1] = 'e';
		vocali[2] = 'i';
		vocali[3] = 'o';
		vocali[4] = 'u';
		
		System.out.println("Le vocali sono: " + Arrays.toString(vocali));

	}
}

Dovrai prima importare java.util.Arrays; e usare il metodo Arrays.toString() per convertire l'array in una stringa. Questa classe possiede altri metodi interessanti, ma prima di parlane, vorrei mostrarti come dichiarare e inizializzare un array in un colpo solo.

import java.util.Arrays;

public class Main {

	public static void main(String[] args) {
		char vocali[] = {'a', 'e', 'i', 'o', 'u'};
		
		System.out.println("Le vocali sono: " + Arrays.toString(vocali));

	}
}

La parte sinistra della dichiarazione resta invariata, mentre, dopo l'operatore di assegnazione, invece di usare new dovrai scrivere i singoli elementi dell'array separati da una virgola all'interno delle parentesi graffe.

In questo caso il compilatore conterà gli elementi dell'array e utilizzerà questo conteggio come lunghezza dell'array.

Se non conosci la lunghezza di un array, puoi usare la proprietà length.

In questo caso, vocali.length sarà 5, visto che ci sono cinque elementi nell'array. La proprietà length dà un intero ed è utilizzabile per ogni array in Java.

Gli array possono anche essere multidimensionali. Finora abbiamo lavorato con array come questo:

single-dimensional-array

Gli array monodimensionali sono ideali per contenere una serie di valori. Ma immagina un piano terapeutico giornaliero in forma di una tabella:

multi-dimensional-array-1

La prima riga rappresenta i sette giorni della settimana e le colonne rappresentano quante volte un paziente deve prendere un farmaco durante tre momenti della giornata. 0 vuol dire no e 1 vuol dire sì.

Puoi descrivere questa routine giornaliera usando un array multidimensionale:

import java.util.Arrays;

public class Main {

	public static void main(String[] args) {		
		int pianoTerapeutico[][] = {
				{1, 2, 3, 4, 5, 6, 7},
				{0, 1, 1, 0, 1, 1, 0},
				{1, 0, 1, 0, 1, 0, 0},
				{0, 0, 1, 1, 0, 1, 0},
		};
		
		System.out.println(Arrays.deepToString(pianoTerapeutico)); // [[1, 2, 3, 4, 5, 6, 7], [0, 1, 1, 0, 1, 1, 0], [1, 0, 1, 0, 1, 0, 0], [0, 0, 1, 1, 0, 1, 0]]

	}
}

Gli array multidimensionali non possono essere stampati con il normale metodo Arrays.toString(), devi scavare più a fondo.

Anche se l'output non sembra proprio una tabella, puoi renderlo simile a una tabella con un po' di codice:

import java.util.Arrays;

public class Main {

	public static void main(String[] args) {		
		int pianoTerapeutico[][] = {
				{1, 2, 3, 4, 5, 6, 7},
				{0, 1, 1, 0, 1, 1, 0},
				{1, 0, 1, 0, 1, 0, 0},
				{0, 0, 1, 1, 0, 1, 0},
		};
		
		System.out.println(Arrays.deepToString(pianoTerapeutico).replace("], ", "]\n"));
	}
}

// [[1, 2, 3, 4, 5, 6, 7]
// [0, 1, 1, 0, 1, 1, 0]
// [1, 0, 1, 0, 1, 0, 0]
// [0, 0, 1, 1, 0, 1, 0]]

Hai già imparato a usare il metodo replace() per le stringhe. Qui, stiamo semplicemente sostituendo la parentesi finale in ogni riga con una parentesi e il carattere nuova riga.

La prima riga rappresenta i 7 giorni della settimana e il resto delle righe le medicina da prendere ogni giorno. Ogni riga della tabella è un array.

Per accedere a un singolo valore di un array multidimensionale ti servono due indici. Il primo indice determina la riga e il secondo la colonna.

Quindi pianoTerapeutico[2][3] selezionerà l'elemento di indice 3 del terzo array, che sarà 0. Lavorare con array multidimensionali può sembrare un po' complesso ma con la pratica diventa più semplice.

Visto che in Java puoi creare array di ogni tipo, perché non provi a creare altri tipi di array da solo?

Come ordinare un array

Una delle operazioni più comuni che puoi eseguire su un array è ordinarlo. java.utils.Arrays con il metodo Arrays.sort() è qui proprio per questo:

import java.util.Arrays;

public class Main {

	public static void main(String[] args) {		
		char vocali[] = {'e', 'u', 'o', 'i', 'a'};
		
		Arrays.sort(vocali);
		
		System.out.println("L'array ordinato è: " + Arrays.toString(vocali)); // [a, e, i, o , u]

	}
}

Il metodo Arrays.sort() accetta un array non ordinato come argomento e lo riordina. Quindi invece di ottenere un nuovo array ordinato, l'array originale viene ordinato in ordine ascendente.

Di default, il metodo tratta il primo indice dell'array come indice di partenza e la lunghezza dell'array come indice finale.

Puoi specificare i due indici manualmente. Ad esempio, se vuoi ordinare soltanto u, o, i in ordine ascendente e lasciare e, a come sono, puoi farlo in questo modo:

import java.util.Arrays;

public class Main {

	public static void main(String[] args) {		
		char vocali[] = {'e', 'u', 'o', 'i', 'a'};
		
		int indicePartenza = 1;
		int indiceFine = 4;
		
		Arrays.sort(vocali, indicePartenza, indiceFine);
		
		System.out.println("L'array ordinato è: " + Arrays.toString(vocali)); // [e, i, o, u , a]

	}
}

Stavolta, il metodo prende l'array come primo parametro, l'indice di partenza come secondo parametro e quello di fine come terzo parametro. Il resto rimane invariato.

Come eseguire una ricerca binaria su un array

Cercare dei valori in un array ordinato è un'operazione comune. Il metodo Arrays.binarySearch() ti permette di cercare degli elementi all'interno di un array ordinato usando l'algoritmo di ricerca binaria.

public class Main {

	public static void main(String[] args) {
		char vocali[] = {'a', 'e', 'i', 'o', 'u'};
        
        char chiave = 'i';
		
		int trovaIndiceElemento = Arrays.binarySearch(vocali, chiave);
		
		System.out.println("La vocale 'i' è all'indice: " + trovaIndiceElemento); // 2

	}
}

Il metodo Arrays.binarySearch() prende come primo parametro un array e la chiave di ricerca (l'elemento che stiamo cercando) come secondo parametro, per poi restituire l'indice dell'elemento desiderato come un intero.

Puoi memorizzare l'indice in un int e usarlo per accedere all'elemento dell'array con vocali[trovaIndiceElemento].

Nota che l'array è stato ordinato in modo ascendente. Se non sei sicuro dell'ordine dell'array, usa prima il metodo Arrays.sort() per ordinarlo.

Di default, il metodo tratta il primo indice dell'array come punto di partenza e la lunghezza dell'array come termine, ma puoi specificare gli indici manualmente.

Ad esempio, se vuoi che la ricerca avvenga dall'indice 2 all'indice 4, puoi farlo così:

import java.util.Arrays;

public class Main {

	public static void main(String[] args) {
		char vocali[] = {'a', 'e', 'i', 'o', 'u'};
        
        char chiave = 'i';
		int indicePartenza = 2;
		int indiceFine = 4;
		
		int trovaIndiceElemento = Arrays.binarySearch(vocali, indicePartenza, indiceFine, chiave);
		
		System.out.println("La vocale 'i' è all'indice: " + trovaIndiceElemento); // 2

	}
}

Stavolta il metodo prende l'array su cui vuoi effettuare la ricerca come primo parametro, l'indice di partenza come secondo, l'indice finale come terzo e la chiave di ricerca come quarto parametro.

Ora, la ricerca avverrà tra i, o e u.  Quindi, se cerchi la a, non la troverai. In casi in cui l'elemento dato non viene trovato, otterrai un indice negativo, variabile a seconda di una serie di fattori che non discuterò qui. Se sei interessato a saperne di più, dai un'occhiata a questo articolo sull'argomento.

Come riempire un array

Hai già imparato come inizializzare un array con dei valori, ma a volte potresti aver bisogno di riempire un intero array con lo stesso valore. Il metodo Arrays.fill() può farlo per te:

import java.util.Arrays;

public class Main {

	public static void main(String[] args) {		
		char vocali[] = {'e', 'u', 'o', 'i', 'a'};
		
		Arrays.fill(vocali, 'x');
		
		System.out.println("L'array riempito: " + Arrays.toString(vocali)); // [x, x, x, x, x]

	}
}

Come il metodo Arrays.sort(), Arrays.fill() può eseguire altre operazioni. Prende l'array come primo parametro, il valore che vuoi inserire come secondo parametro e aggiorna l'array originale in loco.

Anche questo metodo tratta il primo indice come punto di partenza e la lunghezza come indice finale. Puoi specificare questi indici manualmente come segue:

import java.util.Arrays;

public class Main {

	public static void main(String[] args) {		
		char vocali[] = {'e', 'u', 'o', 'i', 'a'};
		
		int indicePartenza = 1;
		int indiceFine = 4;
		
		Arrays.fill(vocali, indicePartenza, indiceFine, 'x');
		
		System.out.println("L'array riempito: " + Arrays.toString(vocali)); // [e, x, x, x, a]

	}
}

In questo caso, il metodo prende l'array come primo argomento, l'indice di partenza come secondo, l'indice finale come terzo e il valore da inserire come quarto argomento.

Come fare una copia di un array

Dato che in Java gli array sono di tipo riferimento, copiarli usando l'operatore di assegnazione può causare qualche comportamento inatteso.

import java.util.Arrays;

public class Main {

	public static void main(String[] args) {
		int numeriDispari[] = {1, 3, 5};
		int copiaNumeriDispari[] = numeriDispari;
		
		Arrays.fill(numeriDispari, 0);
		
		System.out.println("L'array copiato: " + Arrays.toString(copiaNumeriDispari)); // [0, 0, 0]

	}
}

I cambiamenti apportati all'array sorgente si riflettono anche sulla copia. Questo accade perché  quando usiamo l'operatore di assegnazione per copiare l'array, la copia fa riferimento all'array originale in memoria.

Per copiare un array in modo appropriato, puoi usare il metodo Arrays.copyOf() come segue:

import java.util.Arrays;

public class Main {

	public static void main(String[] args) {
		int numeriDispari[] = {1, 3, 5};
		int copiaNumeriDispari[] = Arrays.copyOf(numeriDispari, numeriDispari.length);
		
		Arrays.fill(numeriDispari, 0);
		
		System.out.println("L'array copiato: " + Arrays.toString(copiaNumeriDispari)); // [1, 3, 5]

	}
}

Il metodo prende l'array sorgente come primo argomento e la lunghezza del nuovo array come secondo argomento. Se vuoi che la lunghezza sia la stessa, passa semplicemente la lunghezza dell'array originale usando la proprietà length.

Se inserisci una lunghezza minore, ogni valore successivo sarà escluso e se inserisci una lunghezza maggiore, i nuovi indici saranno popolati con il valore di default per il tipo di dato nell'array.

Esiste anche  il metodo Arrays.copyOfRange() che può copiare una porzione di un array in un nuovo array:

import java.util.Arrays;

public class Main {

	public static void main(String[] args) {
		int numeriDispari[] = {1, 3, 5, 7, 9, 11, 13, 15};
		
		int indicePartenza = 2;
		int indiceFine = 7;
		
		int copiaNumeriDispari[] = Arrays.copyOfRange(numeriDispari, indicePartenza, indiceFine);
		
		System.out.println("L'array copiato: " + Arrays.toString(copiaNumeriDispari)); // [5, 7, 9, 11, 13]

	}
}

Questo metodo prende l'array sorgente come primo argomento, seguito dall'indice di partenza e di fine.

Tieni a mente che l'indice finale non è incluso. Ecco perché il 15 non è presente nel nuovo array. Ma se vuoi includere l'ultimo indice dell'array, usa la lunghezza dell'array originale come indice finale.

import java.util.Arrays;

public class Main {

	public static void main(String[] args) {
		int numeriDispari[] = {1, 3, 5, 7, 9, 11, 13, 15};
		
		int indicePartenza = 2;
		int indiceFine = numeriDispari.length;
		
		int copiaNumeriDispari[] = Arrays.copyOfRange(numeriDispari, indicePartenza, indiceFine);
		
		System.out.println("L'array copiato: " + Arrays.toString(copiaNumeriDispari)); // [5, 7, 9, 11, 13, 15]

	}
}

Ora il nuovo array includerà anche il 15. Puoi anche inserire un numero può grande della lunghezza dell'array sorgente. In questo caso, i nuovi indici aggiunti conterranno il valore di default del tipo di dato nell'array.

Come confrontare due array

Se propri a verificare se due array sono uguali o meno in Java usando l'operatore di uguaglianza relazionale, otterrai dei risultati inaspettati.

public class Main {

	public static void main(String[] args) {		
		int numeriDispari1[] = {1, 3, 5, 7, 9, 11, 13, 15};
		int numeriDispari2[] = {1, 3, 5, 7, 9, 11, 13, 15};
		
		System.out.println(numeriDispari1 == numeriDispari2); // false
	}
}

Anche se i due array sono identici, l'output del programma è false. Visto che gli array sono di tipo riferimento, l'operatore di confronto verificherà se sono la stessa istanza o no.

Per confrontare due array in Java, puoi usare il metodo Arrays.equals():

import java.util.Arrays;

public class Main {

	public static void main(String[] args) {		
		int numeriDispari1[] = {1, 3, 5, 7, 9, 11, 13, 15};
		int numeriDispari2[] = {1, 3, 5, 7, 9, 11, 13, 15};
		
		System.out.println(Arrays.equals(numeriDispari1, numeriDispari2)); // true
	}
}

Tuttavia, se cambi anche un solo elemento in questi array, l'output sarà false dato gli array non saranno più uguali.

Puoi anche confrontare array multidimensionali, ma per quello devi utilizzare il metodo Arrays.deepEquals() invece del metodo usuale.

import java.util.Arrays;

public class Main {

	public static void main(String[] args) {		
		int pianoTerapeutico1[][] = {
				{1, 2, 3, 4, 5, 6, 7},
				{0, 1, 1, 0, 1, 1, 0},
				{1, 0, 1, 0, 1, 0, 0},
				{0, 0, 1, 1, 0, 1, 0},
		};
		
		int pianoTerapeutico2[][] = {
				{1, 2, 3, 4, 5, 6, 7},
				{0, 1, 1, 0, 1, 1, 0},
				{1, 0, 1, 0, 1, 0, 0},
				{0, 0, 1, 1, 0, 1, 0},
		};
		
		System.out.println(Arrays.deepEquals(pianoTerapeutico1, pianoTerapeutico2)); // true
	}
}

Questo metodo chiama sé stesso ogni volta che incontra un nuovo array all'interno dell'array genitore.

Questi erano i metodi più comuni all'interno della classe java.util.Arrays. Se vuoi impararne altri, consulta la documentazione ufficiale.

Come usare i loop in Java

Se hai bisogno di ripetere un'operazione per un certo numero di volte, puoi usare un loop. I loop possono essere di tre tipi: loop for, loop for...each e loop while.

Loop for

I loop for sono probabilmente il tipo di loop più comune che troverai su internet.

Ogni loop for è formato da tre parti. L'inizializzazione, la condizione e un'espressione di aggiornamento. Le iterazioni avvengono in più passaggi.

for-loop-generic-2

Se vuoi stampare i numeri da 0 a 10 usando un loop for, puoi farlo in questo modo:

public class Main {

	public static void main(String[] args) {
		for (int number = 0; number <= 10; number++) {
			System.out.println(number);
		}
	}
}

// 0
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// 10

Il diagramma a flusso di questo loop sarà il seguente:

for-loop-number-1
  1. All'inizio del loop, inizializziamo un nuovo intero chiamato number con il valore iniziale 0.
  2. Poi, viene verificato se number è minore o uguale a 10 oppure no.
  3. Se è minore o uguale a 10, l'istruzione all'interno del blocco del loop viene eseguita e il valore della variabile stampato sul terminale.
  4. Poi, la variabile è aggiornata, incrementandone il valore di 1.
  5. Il loop torna indietro per verificare se il valore di number è ancora minore o uguale a 10 oppure no.

Finché il valore di number resta minore o uguale a 10, il loop prosegue. Nel momento in cui il suo valore diventa 11, il loop termina.

Puoi usare un loop for per iterare su un array, in questo modo:

public class Main {

	public static void main(String[] args) {
		int serieFibonacci[] = {0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55};
		
		for(int index = 0; index < serieFibonacci.length; index++) {
			System.out.println(serieFibonacci[index]);
		}
	}
}

Il diagramma di flusso di questo loop sarà:

for-loop-fibo

Dato che l'ultimo indice dell'array è minore della sua lunghezza, il loop viene eseguito per tutti gli elementi dell'array. Nel momento in cui il valore di index diventa uguale alla lunghezza dell'array, usciamo dal loop.

Una delle cose simpatiche che puoi fare con i loop è stampare tabelline di moltiplicazione. Ad esempio, la tabellina del 5:

public class Main {

	public static void main(String[] args) {
		int numero = 5;
		
		for (int moltiplicatore = 1; moltiplicatore <= 10; moltiplicatore++) {
			System.out.println(String.format("%d x %d = %d", numero, moltiplicatore, numero * moltiplicatore));
		}
	}
}

// 5 x 1 = 5
// 5 x 2 = 10
// 5 x 3 = 15
// 5 x 4 = 20
// 5 x 5 = 25
// 5 x 6 = 30
// 5 x 7 = 35
// 5 x 8 = 40
// 5 x 9 = 45
// 5 x 10 = 50

I loop possono anche essere annidati. Ciò vuol dire che puoi inserire un loop in un altro loop. Possiamo stampare le tabelline dei numeri da 1 a 10 usando dei loop annidati:

public class Main {

	public static void main(String[] args) {
		for (int numero = 1; numero <= 10; numero++) {
			System.out.println(String.format("\ntabellina del %d", numero));
			for (int moltiplicatore = 1; moltiplicatore <= 10; moltiplicatore++) {
				System.out.println(String.format("%d x %d = %d", numero, moltiplicatore, numero * moltiplicatore));
			}
		}
	}
}

Non aggiungo l'output di questo codice. Provalo tu stesso e scrivi le iterazioni del loop su carta in modo da capire meglio cosa accade in ogni passaggio.

Loop for-each

Se vuoi iterare su un insieme come un array e svolgere delle operazioni su ogni elemento dell'insieme, puoi usare un loop for-each.

public class Main {

	public static void main(String[] args) {
		int serieFibonacci[] = {0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55};
		
		for(int numero : serieFibonacci) {
			System.out.println(numero);
		}
	}
}

Nel caso di un loop for-each, il tipo di elemento deve corrispondere al tipo di elementi dell'insieme con cui stai lavorando. Qui, l'array è di tipo int e quindi anche l'elemento del loop è un intero.

Questo loop svolge esattamente le stesse operazioni viste in precedenza, ma stavolta, non devi tenere traccia dell'indice o usare le parentesi quadre per accedere agli elementi. Ha un aspetto più pulito ed è meno favorevole alla generazione di errori.

Loop while

Se vuoi eseguire un gruppo di codice finché una determinata condizione è verificata, puoi usare un loop while.

while-loop-generic

Non ci sono passaggi di inizializzazione o aggiornamento in un loop while. Accade tutto nel corpo del loop. Se riscriviamo il programma per stampare la tabellina del 5 usando un loop while in questo modo:

public class Main {

	public static void main(String[] args) {
		int numero = 5;
		int moltiplicatore = 1;
		
		while (moltiplicatore <= 10) {
			System.out.println(String.format("%d x %d = %d", numero, moltiplicatore, numero*moltiplicatore));
			
			moltiplicatore++;
		}
	}
}

// 5 x 1 = 5
// 5 x 2 = 10
// 5 x 3 = 15
// 5 x 4 = 20
// 5 x 5 = 25
// 5 x 6 = 30
// 5 x 7 = 35
// 5 x 8 = 40
// 5 x 9 = 45
// 5 x 10 = 50

Anche se i loop while sono meno comuni dei loop for, è bene imparare a utilizzarli.

Loop do-while

L'ultimo tipo di loop di cui parleremo è il loop do-while. Inverte il normale ordine delle operazioni di un loop while – quindi invece di controllare prima la condizione e poi eseguire il corpo del loop, esegue prima il corpo del loop e poi verifica la condizione.

do-while-loop-generic

Il codice per stampare la tabellina del 5 implementato con un loop do-while sarà:

public class Main {

	public static void main(String[] args) {
		int numero = 5;
		int moltiplicatore = 1;
		
		do {
			System.out.println(String.format("%d x %d = %d", numero, moltiplicatore, numero*moltiplicatore));
			
			moltiplicatore++;
		} while (moltiplicatore <= 10);
	}
}

// 5 x 1 = 5
// 5 x 2 = 10
// 5 x 3 = 15
// 5 x 4 = 20
// 5 x 5 = 25
// 5 x 6 = 30
// 5 x 7 = 35
// 5 x 8 = 40
// 5 x 9 = 45
// 5 x 10 = 50

Questo tipo di loop è molto utile quando hai bisogno di svolgere alcune operazioni finché un utente non ti dà un input specifico. Come mostrare un menu finché l'utente preme il tasto "x".

Come lavorare con gli ArrayList in Java

In Java, gli array non sono ridimensionabili. Una volta che hai impostato una lunghezza per un array non puoi cambiarla in nessun modo. In Java, la classe ArrayList rimedia a questa limitazione.

import java.util.ArrayList;

public class Main {
    public static void main (String[] args) {
        ArrayList<Integer> numeriDispari = new ArrayList<>();

        numeriDispari.add(1);
        numeriDispari.add(3);
        numeriDispari.add(5);
        numeriDispari.add(7);
        numeriDispari.add(9);

        System.out.println(numeriDispari.toString()); // [1, 3, 5, 7, 9]
    }
}

Per creare un ArrayList, devi importare la classe java.util.ArrayList in cima al tuo file sorgente.

Poi, devi scrivere ArrayList, il tipo di dato degli elementi all'interno di parentesi angolate, aggiungere il numero dell'ArrayList seguito dall'operatore di assegnazione e new ArrayList<>().

Non puoi creare ArrayList di tipo primitivo, quindi dovrai usare le classi wrapper corrispondenti.

Anche se questi elementi hanno indice a base zero come gli array, non puoi utilizzare la notazione con parentesi quadre per accedervi. Invece, occorre usare il metodo get():

import java.util.ArrayList;

public class Main {
    public static void main (String[] args) {
        ArrayList<Integer> numeriDispari = new ArrayList<>();

        numeriDispari.add(1);
        numeriDispari.add(3);
        numeriDispari.add(5);
        numeriDispari.add(7);
        numeriDispari.add(9);

        System.out.println(numeriDispari.get(2)); // 5
    }
}

Il metodo get() ti darà il valore presente all'indice dato. Allo stesso modo di get(), puoi usare il metodo set() per aggiornare il valore di un elemento.

import java.time.LocalDate;
import java.util.ArrayList;

public class Main {
    public static void main (String[] args) {
        ArrayList<Integer> numeriDispari = new ArrayList<>();

        numeriDispari.add(1);
        numeriDispari.add(3);
        numeriDispari.add(5);
        numeriDispari.add(7);
        numeriDispari.add(9);

        numeriDispari.set(2, 55);

        System.out.println(numeriDispari.get(2)); // 55
    }
}

Il primo parametro del metodo set() è l'indice e il secondo è il valore aggiornato.

Non esiste una proprietà length come per gli array ma puoi usare il metodo size() su qualsiasi ArrayList per trovarne la lunghezza.

import java.util.ArrayList;

public class Main {
    public static void main (String[] args) {
        ArrayList<Integer> numeriDispari = new ArrayList<>();

        numeriDispari.add(1);
        numeriDispari.add(3);
        numeriDispari.add(5);
        numeriDispari.add(7);
        numeriDispari.add(9);

        System.out.println(numeriDispari.size()); // 5
    }
}

Puoi rimuovere elementi da un ArrayList usando il metodo remove():

import java.util.ArrayList;

public class Main {
    public static void main (String[] args) {
        ArrayList<Integer> numeriDispari = new ArrayList<>();

        numeriDispari.add(1);
        numeriDispari.add(3);
        numeriDispari.add(5);
        numeriDispari.add(7);
        numeriDispari.add(9);

        numeriDispari.remove(Integer.valueOf(7));
        numeriDispari.remove(Integer.valueOf(9));

        System.out.println(numeriDispari.toString()); // [1, 3, 5]
    }
}

Il metodo remove() può rimuovere un elemento in base al suo valore o al suo indice. Se passi al metodo un valore intero primitivo, rimuoverà l'elemento corrispondente a quell'indice.

Ma se passi un oggetto, come in questo codice, il metodo troverà e cancellerà l'elemento dato. Il metodo valueOf() è presente in tutte le classi wrapper e può convertire un valore primitivo in un tipo riferimento.

Come aggiungere o rimuovere elementi multipli

Abbiamo già visto esempi dei metodi add() e remove(), ma esistono anche i metodi addAll() e removeAll() per lavorare con elementi multipli.

import java.util.ArrayList;

public class Main {
    public static void main (String[] args) {
        ArrayList<Integer> numeriDispari = new ArrayList<>();

        numeriDispari.add(1);
        numeriDispari.add(3);
        numeriDispari.add(5);

        ArrayList<Integer> altriNumeriDispari = new ArrayList<>();

        altriNumeriDispari.add(7);
        altriNumeriDispari.add(9);
        altriNumeriDispari.add(11);

        numeriDispari.addAll(altriNumeriDispari); // [1, 3, 5, 7, 9, 11]

        System.out.println(numeriDispari.toString());

        numeriDispari.removeAll(altriNumeriDispari);

        System.out.println(numeriDispari.toString()); // [1, 3, 5]
    }
}

Entrambi i metodi accettano delle collezioni come parametri. Nel codice qui sopra, abbiamo creato due ArrayList separati per poi unirli con il metodo addAll().

In seguito abbiamo rimosso gli elementi del secondo ArrayList usando il metodo removeAll() per far tornare l'ArrayList nel suo stato originario.

Puoi anche eliminare tutti gli elementi di un ArrayList con il metodo clear():

import java.util.ArrayList;

public class Main {
    public static void main (String[] args) {
        ArrayList<Integer> numeriDispari = new ArrayList<>();

        numeriDispari.add(1);
        numeriDispari.add(3);
        numeriDispari.add(5);

        numeriDispari.clear();
        
        System.out.println(numeriDispari.toString()); // []
    }
}

Il metodo non richiede nessun parametro e non restituisce nessun valore. Svuota semplicemente l'ArrayList con una sola chiamata.

Come rimuovere elementi in base a una condizione

Il metodo removeIf() può essere usato per rimuovere elementi da un ArrayList in base a una condizione:

import java.util.ArrayList;

public class Main {
    public static void main (String[] args) {
        ArrayList<Integer> numeri = new ArrayList<>();

        for (int i = 0; i <= 10; i++) {
            numeri.add(i);
        }

        System.out.println(numeri.toString()); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

        numeri.removeIf(numero -> numero % 2 == 1);

        System.out.println(numeri.toString()); // [0, 2, 4, 6, 8, 10]
    }
}

Il metodo accetta un'espressione lambda come parametro. Le espressioni lambda sono dei metodi senza nome, che ricevono dei parametri e svolgono delle operazioni su di essi.

In questo caso, il metodo removeIf() itera sull'ArrayList e passa ogni elemento all'espressione lambda come valore della variabile numero.

Poi l'espressione lambda verifica se il numero dato è divisibile per 2 oppure no e restituisce true o false in base al risultato.

Se l'espressione lambda restituisce true, il metodo removeIf() conserverà il valore, altrimenti lo cancellerà.

Come clonare e confrontare degli ArrayList

Per duplicare un ArrayList puoi usare il metodo clone():

import java.util.ArrayList;

public class Main {
    public static void main (String[] args) {
        ArrayList<Integer> numeri = new ArrayList<>();

        for (int i = 0; i <= 10; i++) {
            numeri.add(i);
        }

        ArrayList<Integer> numeriClonato = (ArrayList<Integer>)numeri.clone();

        System.out.println(numeriClonato.equals(numeri)); // true
    }
}

Il metodo clone() restituisce un oggetto, quindi occorre definire correttamente il tipo dell'ArrayList manualmente. Puoi confrontare due ArrayList con il metodo equals(), proprio come per gli array.

Come verificare la presenza di un elemento e se un ArrayList è vuoto

Puoi usare il metodo contains() per controllare se un ArrayList contiene o meno un dato elemento:

import java.util.ArrayList;

public class Main {
    public static void main (String[] args) {
        ArrayList<Integer> numeriDispari = new ArrayList<>();

        numeriDispari.add(1);
        numeriDispari.add(3);
        numeriDispari.add(5);
        numeriDispari.add(7);
        numeriDispari.add(9);

        System.out.println(numeriDispari.isEmpty()); // false
        System.out.println(numeriDispari.contains(5)); // true
    }
}

Se vuoi verificare se un ArrayList è vuoto oppure no, chiama semplicemente il metodo isEmpty() e otterrai un booleano come valore di ritorno.

Come ordinare un ArrayList

Puoi ordinare un ArrayList in diversi modi usando il metodo sort():

import java.util.ArrayList;
import java.util.Comparator;

public class Main {
    public static void main (String[] args) {
        ArrayList<Integer> numeriDispari = new ArrayList<>();

        numeriDispari.add(5);
        numeriDispari.add(7);
        numeriDispari.add(1);
        numeriDispari.add(9);
        numeriDispari.add(3);

        System.out.println(numeriDispari.toString()); [5, 7, 1, 9, 3]

        numeriDispari.sort(Comparator.naturalOrder());

        System.out.println(numeriDispari.toString()); [1, 3, 5, 7, 9]
    }
}

Il metodo sort() prende come parametro un comparator che impone l'ordine sull'ArrayList.

Puoi riordinare l'ArrayList in ordine inverso cambiando il comparator passato:

import java.util.ArrayList;
import java.util.Comparator;

public class Main {
    public static void main (String[] args) {
        ArrayList<Integer> numeriDispari = new ArrayList<>();

        numeriDispari.add(5);
        numeriDispari.add(7);
        numeriDispari.add(1);
        numeriDispari.add(9);
        numeriDispari.add(3);

        System.out.println(numeriDispari.toString()); // [5, 7, 1, 9, 3]

        numeriDispari.sort(Comparator.reverseOrder());

        System.out.println(numeriDispari.toString()); // [9, 7, 5, 3, 1]
    }
}

I comparator hanno anche altri utilizzi, ma questi vanno oltre lo scopo di questo manuale.

Come tenere gli elementi comuni in due ArrayList

Immagina di avere due ArrayList e di dover trovare quali elementi sono presenti in entrambi, rimuovendo gli altri dal primo ArrayList.

import java.util.ArrayList;

public class Main {
    public static void main (String[] args) {
        ArrayList<Integer> numeriDispari = new ArrayList<>();

        numeriDispari.add(1);
        numeriDispari.add(3);
        numeriDispari.add(5);

        ArrayList<Integer> altriNumeriDispari = new ArrayList<Integer>();

        altriNumeriDispari.add(5);
        altriNumeriDispari.add(7);
        altriNumeriDispari.add(9);

        numeriDispari.retainAll(altriNumeriDispari);

        System.out.println(numeriDispari.toString()); // [5]
    }
}

Il metodo retainAll() può sbarazzarsi per te degli elementi non comuni ai due ArrayList. Dovrai chiamare il metodo sull'ArrayList su cui vuoi operare e passare l'altro ArrayList come parametro.

Come eseguire un'azione su tutti gli elementi di un ArrayList

Hai già imparato come usare i loop nelle sezioni precedenti. Bene, gli ArrayList possiedono già un metodo forEach() che accetta un'espressione lambda come parametro e può svolgere un'azione su tutti gli elementi dell'ArrayList.

import java.util.ArrayList;

public class Main {
    public static void main (String[] args) {
        ArrayList<Integer> numeriDispari = new ArrayList<>();

        numeriDispari.add(1);
        numeriDispari.add(3);
        numeriDispari.add(5);
        numeriDispari.add(7);
        numeriDispari.add(9);

        numeriDispari.forEach(numero -> {
            numero = numero * 2;
            System.out.printf("%d ", numero); // 2 6 10 14 18
        });

        System.out.println(numeriDispari.toString()); // [1, 3, 5, 7, 9]
    }
}

L'ultima volta, l'espressione lambda che abbiamo visto era su una singola riga – ma ce ne possono essere di più grandi. Qui, il metodo forEach() itererà sull'ArrayList passando ogni elemento all'espressione lambda come valore della variabile numero.

L'espressione lambda moltiplicherà il valore fornito per 2 e lo stamperà sul terminale. Tuttavia, l'ArrayList originario resterà invariato.

Come lavorare con le HashMap in Java

In Java, le HashMap possono contenere elementi in coppie chiave-valore. Questo tipo di collezioni è comparabile ai dizionari in Python o agli oggetti in JavaScript.

import java.util.HashMap;

public class Main {
    public static void main (String[] args) {
        HashMap<String, Double> prezzi = new HashMap<>();

        prezzi.put("mela", 2.0);
        prezzi.put("arancia", 1.8);
        prezzi.put("guava", 1.5);
        prezzi.put("mirtillo", 2.5);
        prezzi.put("banana", 1.0);

        System.out.printf(prezzi.toString()); // {arancia=1.8, banana=1.0, mela=2.0, mirtillo=2.5, guava=1.5}
    }
}

Per creare una HashMap, devi prima importare la classe java.util.HashMap in cima al tuo file sorgente.

Poi, devi scrivere HashMap seguito dal tipo di dati della chiave e del valore all'interno di parentesi angolate e separati da una virgola.

In questo caso, le chiavi sono stringhe e i valori dei double. Dopo di ciò, aggiungi l'operatore di assegnazione seguito da new HashMap<>().

Per inserire dei dati nell'HashMap puoi usare il metodo put(), che accetta la chiave come primo parametro e il valore corrispondente come secondo parametro.

Esiste anche il metodo putIfAbsent(), che aggiunge un determinato elemento solo se non è già presente nell'HashMap.

import java.util.HashMap;

public class Main {
    public static void main (String[] args) {
        HashMap<String, Double> prezzi = new HashMap<>();

        prezzi.put("mela", 2.0);
        prezzi.put("arancia", 1.8);
        prezzi.put("guava", 1.5);
        prezzi.put("mirtillo", 2.5);
        prezzi.put("banana", 1.0);

        prezzi.putIfAbsent("guava", 2.9);

        System.out.println(prezzi.toString()); // {arancia=1.8, banana=1.0, mela=2.0, mirtillo=2.5, guava=1.5}
    }
}

Puoi usare il metodo get() per ottenere un valore da una HashMap. Il metodo prende la chiave come parametro.

import java.util.HashMap;

public class Main {
    public static void main (String[] args) {
        HashMap<String, Double> prezzi = new HashMap<>();

        prezzi.put("mela", 2.0);
        prezzi.put("arancia", 1.8);
        prezzi.put("guava", 1.5);
        prezzi.put("mirtillo", 2.5);
        prezzi.put("banana", 1.0);

        System.out.println(prezzi.get("banana")); // 1.000000
    }
}

Esiste anche una variante di questo metodo. Il metodo getOrDefault() funziona come get() ma se la chiave data non viene trovata, restituisce un valore di default specificato.

import java.util.HashMap;

public class Main {
    public static void main (String[] args) {
        HashMap<String, Double> prezzi = new HashMap<>();

        prezzi.put("mela", 2.0);
        prezzi.put("arancia", 1.8);
        prezzi.put("guava", 1.5);
        prezzi.put("mirtillo", 2.5);
        prezzi.put("banana", 1.0);

        System.out.println(prezzi.getOrDefault("jackfruit", 0.0)); // 0.0
    }
}

Il valore di default deve corrispondere al tipo di valori nell'HashMap. Puoi aggiornare un valore in una HashMap usando il metodo replace():

import java.util.HashMap;

public class Main {
    public static void main (String[] args) {
        HashMap<String, Double> prezzi = new HashMap<>();

        prezzi.put("mela", 2.0);
        prezzi.put("arancia", 1.8);
        prezzi.put("guava", 1.5);
        prezzi.put("mirtillo", 2.5);
        prezzi.put("banana", 1.0);

        prezzi.replace("mirtillo", 2.8);

        System.out.printf(prezzi.toString()); // {arancia=1.8, banana=1.0, mela=2.0, mirtillo=2.8, guava=1.5}
    }
}

Per rimuovere elementi da una HashMap, puoi usare il metodo remove():

import java.util.HashMap;

public class Main {
    public static void main (String[] args) {
        HashMap<String, Double> prezzi = new HashMap<>();

        prezzi.put("mela", 2.0);
        prezzi.put("arancia", 1.8);
        prezzi.put("guava", 1.5);
        prezzi.put("mirtillo", 2.5);
        prezzi.put("banana", 1.0);

        prezzi.remove("guava");

        System.out.printf(prezzi.toString()); // {arancia=1.8, banana=1.0, mela=2.0, mirtillo=2.5}
    }
}

Nel caso avessi bisogno di conoscere quanti dati sono presenti all'interno di una HashMap, puoi saperlo usando il metodo size():

import java.util.HashMap;

public class Main {
    public static void main (String[] args) {
        HashMap<String, Double> prezzi = new HashMap<>();

        prezzi.put("mela", 2.0);
        prezzi.put("arancia", 1.8);
        prezzi.put("guava", 1.5);
        prezzi.put("mirtillo", 2.5);
        prezzi.put("banana", 1.0);

        System.out.println(prezzi.size()); // 5
    }
}

Infine, se desideri svuotare una HashMap, puoi farlo grazie al metodo clear().

import java.util.HashMap;

public class Main {
    public static void main (String[] args) {
        HashMap<String, Double> prices = new HashMap<>();

        prices.put("apple", 2.0);
        prices.put("orange", 1.8);
        prices.put("guava", 1.5);
        prices.put("berry", 2.5);
        prices.put("banana", 1.0);

        prices.clear();

        System.out.println(prices.toString()); // {}
    }
}

Proprio come per gli ArrayList, il metodo non prende argomenti né restituisce valori.

Come inserire o sostituire elementi multipli in una HashMap

Se vuoi inserire più elementi in una HashMap in un colpo solo, puoi farlo usando il metodo putAll():

import java.util.HashMap;

public class Main {
    public static void main (String[] args) {
        HashMap<String, Double> prezzi = new HashMap<>();

        prezzi.put("mela", 2.0);
        prezzi.put("arancia", 1.8);
        prezzi.put("guava", 1.5);
        prezzi.put("mirtillo", 2.5);
        prezzi.put("banana", 1.0);

        HashMap<String, Double> altriPrezzi = new HashMap<>();

        altriPrezzi.put("jackfruit", 2.9);
        altriPrezzi.put("ananas", 1.1);
        altriPrezzi.put("pomodoro", 0.8);

        prezzi.putAll(altriPrezzi);

        System.out.println(prezzi.toString()); // {arancia=1.8, banana=1.0, mela=2.0, mirtillo=2.5, ananas=1.1, pomodoro=0.8, guava=1.5, jackfruit=2.9}
    }
}

Il metodo prende un'altra HashMap come parametro e aggiunge i suoi elementi all'HashMap su cui è stato chiamato.

Puoi anche usare il metodo replaceAll() per aggiornare valori multipli in una HashMap.

import java.util.HashMap;

public class Main {
    public static void main (String[] args) {
        HashMap<String, Double> prezzi = new HashMap<>();

        prezzi.put("mela", 2.0);
        prezzi.put("arancia", 1.8);
        prezzi.put("guava", 1.5);
        prezzi.put("mirtillo", 2.5);
        prezzi.put("banana", 1.0);

        prezzi.replaceAll((frutto, prezzo) -> prezzo * 2);

        System.out.println(prezzi.toString()); // {arancia=3.6, banana=2.0, mela=4.0, mirtillo=5.0, guava=3.0}
    }
}

Il metodo replaceAll() itera sull'HashMap e passa ogni coppia chiave-valore all'espressione lambda.

Il primo parametro dell'espressione lambda è la chiave e il secondo è il valore. All'interno dell'espressione lambda puoi eseguire le azioni che desideri.

Come verificare se una Hash Map contiene un elemento o è vuota

Puoi usare i metodi containsKey() e containsValue() per controllare se una HashMap contiene un elemento oppure no.

import java.util.HashMap;

public class Main {
    public static void main (String[] args) {
        HashMap<String, Double> prezzi = new HashMap<>();

        prezzi.put("mela", 2.0);
        prezzi.put("arancia", 1.8);
        prezzi.put("guava", 1.5);
        prezzi.put("mirtillo", 2.5);
        prezzi.put("banana", 1.0);

        System.out.println(prezzi.containsKey("banana")); // true
        System.out.println(prezzi.containsValue(2.5)); // true
    }
}

La differenza tra i due  metodi è che il metodo containsKey() verifica se una data chiave è presente o meno, mentre il metodo containsValue() verifica se un dato valore è presente o meno.

Se vuoi sapere se una HashMap è vuota oppure no, puoi usare il metodo isEmpty():

import java.util.HashMap;

public class Main {
    public static void main (String[] args) {
        HashMap<String, Double> prezzi = new HashMap<>();

        prezzi.put("mela", 2.0);
        prezzi.put("arancia", 1.8);
        prezzi.put("guava", 1.5);
        prezzi.put("mirtillo", 2.5);
        prezzi.put("banana", 1.0);

        System.out.println(prezzi.isEmpty()); // false
    }
}

Dato che il metodo restituisce un valore booleano, puoi utilizzarlo in istruzioni if-else.

Come eseguire un'azione su tutti gli elementi di una HashMap

Proprio come gli ArrayList, anche le HashMap possiedono un metodo forEach() che puoi usare per iterare sull'HashMap e ripetere determinate azioni su ogni dato.

import java.util.HashMap;

public class Main {
    public static void main (String[] args) {
        HashMap<String, Double> prezzi = new HashMap<>();

        prezzi.put("mela", 2.0);
        prezzi.put("arancia", 1.8);
        prezzi.put("guava", 1.5);
        prezzi.put("mirtillo", 2.5);
        prezzi.put("banana", 1.0);

        System.out.println("prezzi scontati");

        prices.forEach((frutto, prezzo) -> {
            System.out.println(frutto + " - " + (prezzo - 0.5));
        });
    }
}

// prezzi scontati
// arancia - 1.3
// banana - 0.5
// mela - 1.5
// mirtillo - 2.0
// guava - 1.0

Il metodo itera su ogni dato e passa la chiave e il valore all'espressione lambda. All'interno del corpo dell'espressioni lambda, puoi fare ciò che desideri.

Classi e Oggetti in Java

Ecco un'utile definizione di programmazione orientata agli oggetti:

La programmazione orientata agli oggetti (OOP, Object-oriented programming) è un paradigma di programmazione basato sul concetto di "oggetti", che possono contenere dati e codice: dati in forma di campi (spesso chiamati attributi o proprietà) e codice in forma di procedure (spesso chiamate metodi).

Immagina il sistema di gestione di una libreria, dove i membri della libreria possono accedere e vedere i libri che hanno già preso in prestito, richiederne nuovi e così via.

In questo sistema, gli utenti e i libri possono essere tutti oggetti. Questi oggetti avranno le loro proprietà come nome e data di nascita, nel caso di un utente, e titolo e autore, nel caso dei libri.

Le classi nella programmazione orientata agli oggetti sono i modelli per gli oggetti precedentemente menzionati. Abbiamo già parlato delle possibili proprietà degli oggetti utente e libro.

Per creare una nuova classe Utente, clicca ancora con il tasto destro sulla cartella src. Poi vai su New > Java Class, chiamala Utente e premi invio.

Tenendo a mente le proprietà discusse in precedenza, il codice della classe Utente dovrebbe essere come segue:

import java.time.LocalDate;

public class Utente {
    String nome;
    LocalDate dataDiNascita;
}

LocalDate è un tipo di dato riferimento che rappresenta una data. Ora torna al file Main.java e crea una nuova istanza di questa classe:

import java.time.LocalDate;

public class Main {
    public static void main (String[] args) {
        Utente utente = new Utente();

        utente.nome = "Farhan";
        utente.dataDiNascita = LocalDate.parse("1996-07-15");

        System.out.printf("%s è nato il %s.", utente.nome, utente.dataDiNascita.toString()); // Farhan è nato il 1996-07-15.
    }
}

Creare un nuovo utente non è molto diverso da creare una nuova stringa o un array. Inizi scrivendo il nome della classe, poi il nome dell'istanza o dell'oggetto.

Aggiungi l'operatore di assegnazione seguito dalla parola chiave new e dalla chiamata del costruttore. Il costruttore è un metodo speciale che inizializza l'oggetto.

Il costruttore inizializza le proprietà dell'oggetto con dei valori predefiniti, e cioè null per tutti i tipi riferimento.

Puoi accedere alle proprietà di un oggetto scrivendo il nome dell'oggetto seguito da un punto e dal nome della proprietà.

Il metodo LocalDate.parse() interpreta la data dalla stringa specificata. Dato che dataDiNascita è un riferimento, occorre usare il metodo toString() per stamparla sulla console.

Cos'è un metodo?

Le variabili o le proprietà di una classe descrivono lo stato dei suoi oggetti. I metodi invece, ne descrivono il comportamento.

Ad esempio, nella classe Utente possiamo avere un metodo che calcola l'età dell'utente.

import java.time.Period;
import java.time.LocalDate;

public class Utente {
    String nome;
    LocalDate dataDiNascita;

    int eta() {
        return Period.between(this.dataDiNascita, LocalDate.now()).getYears();
    }
}

Qui, la parola chiave this rappresenta l'istanza attuale della classe. Iniziamo scrivendo il tipo del valore di ritorno del metodo. Dato che l'età di un utente è un intero, il valore restituito da questo metodo sarà di tipo int.

Poi scriviamo il nome del metodo, seguito dalle parentesi.

È il momento di scrivere il corpo del metodo, all'interno di parentesi graffe. La classe Period in Java esprime un periodo di tempo secondo il sistema del calendario ISO-8601. Il metodo LocalDate.now() restituisce la data corrente.

Quindi la chiamata Period.between(this.dataDiNascita, LocalDate.now()).getYears() restituisce la differenza tra la data corrente e la data di nascita in anni.

Ora torniamo al file Main.java file e chiamiamo il metodo come segue:

import java.time.LocalDate;

public class Main {
    public static void main (String[] args) {
        Utente utente = new Utente();

        utente.nome = "Farhan";
        utente.dataDiNascita = LocalDate.parse( "1996-07-15");

        System.out.printf("%s ha %s anni.", utente.nome, utente.eta()); // Farhan ha 26 anni.
    }
}

I metodi possono anche accettare dei parametri. Ad esempio, se vuoi creare un metodo prestito() per inserire nuovi libri nella lista dei libri presi in prestito da un utente, puoi farlo in questo modo:

import java.time.LocalDate;
import java.time.Period;
import java.util.ArrayList;

public class Utente {
    String nome;
    LocalDate dataDiNascita;
    
    ArrayList<String> libriPresiInPrestito = new ArrayList<String>();

    int eta() {
        return Period.between(this.dataDiNascita, LocalDate.now()).getYears();
    }

    void prestito(String titoloLibro) {
        this.libriPresiInPrestito.add(titoloLibro);
    }
}

Tornando al file Main.java, possiamo chiamare il metodo in questo modo:

public class Main {
    public static void main (String[] args) {
        Utente utente = new Utente();

        utente.nome = "Farhan";

        utente.prestito("Carmilla");
        utente.prestito("Hard West");

        System.out.printf("%s ha preso in prestito questi libri: %s", utente.nome, utente.libriPresiInPrestito.toString()); // Farhan ha preso in prestito questi libri: [Carmilla, Hard West]
    }
}

Allo stesso modo, creiamo una classe per i libri:

import java.util.ArrayList;

public class Libro {
    String titolo;
    ArrayList<String> autori = new ArrayList<String>();
}

Spesso i libri hanno più di un autore. Adesso creiamo una nuova istanza di Libro nel file Main.java.

import java.time.LocalDate;

public class Main {
    public static void main (String[] args){
        Utente utente = new Utente();
        utente.nome = "Farhan";
        utente.dataDiNascita = LocalDate.parse("1996-07-15");

        Libro libro = new Libro();
        libro.titolo = "Carmilla";
        libro.autori.add("Sheridan Le Fanu");

        System.out.printf("%s è stato scritto da %s", libro.titolo, libro.autori.toString()); // Carmilla è stato scritto da [Sheridan Le Fanu]
    }
}

Ora torniamo al file Utente.java e creiamo una relazione tra gli utenti e i libri:

import java.time.LocalDate;
import java.time.Period;
import java.util.ArrayList;

public class Utente {
    String nome;
    LocalDate dataDiNascita;

    ArrayList<Libro> libriPresiInPrestito = new ArrayList<Libro>();

    int eta() {
        return Period.between(this.dataDiNascita, LocalDate.now()).getYears();
    }

    void prestito(Libro libro) {
        this.libriPresiInPrestito.add(libro);
    }
}

Invece di usare un ArrayList di stringhe, adesso stiamo usando un ArrayList di libri per conservare i libri presi in prestito da un utente.

Dato che il tipo dell'argomento del metodo è cambiato, di conseguenza dovrai aggiornare il codice nel file Main.java:

import java.time.LocalDate;

public class Main {
    public static void main (String[] args){
        Utente utente = new Utente();
        utente.nome = "Farhan";
        utente.dataDiNascita = LocalDate.parse("1996-07-15");

        Libro libro = new Libro();
        libro.titolo = "Carmilla";
        libro.autori.add("Sheridan Le Fanu");

        utente.prestito(libro);

        System.out.printf("%s ha preso in prestito questi libri: %s", utente.nome, utente.libriPresiInPrestito.toString()); // Farhan ha preso in prestito questi libri: [Libro@30dae81]
    }

Tutto funziona a dovere, eccetto il fatto che le informazioni sui libri non vengono stampate in modo appropriato.

Spero che ti ricordi del metodo toString(). Quando chiamiamo utente.libriPresiInPrestito.toString() il compilatore realizza che gli elementi contenuti nell'ArrayList  sono oggetti o riferimenti. Quindi inizia a chiamare il metodo toString() in questi elementi.

Il problema è che non esiste un'implementazione appropriata di toString() nella classe Libro. Apri Libro.java e aggiorna il suo codice come segue:

import java.util.ArrayList;

public class Libro {
    String titolo;
    ArrayList<String> autori = new ArrayList<String>();

    public String toString() {
        return String.format("%s di %s", this.titolo, this.autori.toString());
    }
}

Adesso, il metodo toString() restituisce una stringa ben formattata invece del riferimento all'oggetto. Esegui ancora il codice e questa volta l'output dovrebbe essere Farhan ha preso in prestito questi libri: [Carmilla di [Sheridan Le Fanu]].

Come puoi vedere, essere in grado di progettare il tuo software con delle entità reali lo rende molto più comprensibile. Anche se è tutto incentrato su un ArrayList e qualche stringa, è come se l'operazione di prestito del libro avvenga davvero.

Cos'è l'overloading dei metodi?

In Java, più metodi possono avere lo stesso nome se i loro parametro sono diversi. Ciò viene detto overloading dei metodi.

Un esempio può essere il metodo prestito() nella classe Utente(). Adesso accetta un singolo libro come parametro. Creiamo una versione di overload che invece accetta un array di libri.

import java.time.LocalDate;
import java.time.Period;
import java.util.ArrayList;
import java.util.Arrays;

public class Utente {
    private String nome;
    private LocalDate dataDiNascita;

    private ArrayList<Libro> libriPresiInPrestito = new ArrayList<Libro>();

    public String getNome() {
        return this.nome;
    }

    public void setNome(String nome) {
        this.nome = nome;
    }

    public String getLibriPresiInPrestito() {
        return this.libriPresiInPrestito.toString();
    }

    Utente (String nome, String dataDiNascita) {
        this.nome = nome;
        this.dataDiNascita = LocalDate.parse(dataDiNascita);
    }

    int eta() {
        return Period.between(this.dataDiNascita, LocalDate.now()).getYears();
    }

    void prestito(Libro libro) {
        this.libriPresiInPrestito.add(libro);
    }

    void prestito(Libro[] libri) {
        libriPresiInPrestito.addAll(Arrays.asList(libri));
    }
}

Il tipo del valore di ritorno del nuovo metodo è identico al precedente, ma questo accetta un array di oggetti Libro invece di un singolo oggetto.

Aggiorniamo il file Main.java per poter usare questo metodo di overload.

public class Main {
    public static void main(String[] args) {
        Utente utente = new Utente("Farhan", "1996-07-15");

        Libro libro1 = new Libro("Carmilla", new String[]{"Sheridan Le Fanu"});
        Libro libro2 = new Libro("Frankenstein", new String[]{"Mary Shelley"});
        Libro libro3 = new Libro("Dracula", new String[]{"Bram Stoker"});

        utente.prestito(new Libro[]{libro1, libro2});

        utente.prestito(libro3);

        System.out.printf("%s ha preso in prestito questi libri: %s", utente.getNome(), utente.getLibriPresiInPrestito());
    }
}

Come puoi vedere, adesso il metodo prestito() accetta un array di libri o un singolo oggetto libro senza problemi.

Cosa sono i costruttori in Java?

Un costruttore è un tipo particolare di metodo presente in ogni classe e che viene chiamato dal compilatore ogni volta che crei un nuovo oggetto da una classe.

Dato che il metodo è chiamato durante la costruzione di un oggetto, prende il nome di costruttore. Di default, un costruttore assegna dei valori predefiniti a tutte le sue proprietà.

Per sovrascrivere il costruttore predefinito, devi creare un nuovo metodo in una classe con lo stesso nome della classe.

import java.time.LocalDate;
import java.time.Period;
import java.util.ArrayList;

public class Utente {
    public String nome;
    public LocalDate dataDiNascita;
    public ArrayList<Libro> libriPresiInPrestito = new ArrayList<Libro>();

    Utente (String nome, String dataDiNascita) {
        this.nome = nome;
        this.dataDiNascita = LocalDate.parse(dataDiNascita);
    }

    int eta() {
        return Period.between(this.dataDiNascita, LocalDate.now()).getYears();
    }

    void prestito(Libro libro) {
        this.libriPresiInPrestito.add(libro);
    }
}

Ora che abbiamo il costruttore, invece di prendere la data da una stringa nel file Main.java, possiamo farlo qui.

Questo perché il formato della data di nascita interessa la classe Utente e la classe Main non ha bisogno di preoccuparsene.

Stesso trattamento per la classe Libro:

import java.util.ArrayList;
import java.util.Arrays;

public class Libro {
    public String titolo;
    public ArrayList<String> autori = new ArrayList<String>();

    Libro(String titolo, String[] autori) {
        this.titolo = titolo;
        this.autori = new ArrayList<String>(Arrays.asList(autori));
    }

    public String toString() {
        return String.format("%s di %s", this.titolo, this.autori.toString());
    }
}

Di nuovo, il tipo della collezione autori non è una preoccupazione della classe Main. Il modo più semplice per lavorare con un gruppo di valori in Java è usare un array.

Quindi, otteniamo i nomi degli autori come un array dalla classe Main e da esso creiamo un ArrayList nella classe Libro.

Ora dobbiamo passare questi parametri al costruttore quando creiamo un nuovo oggetto utente o libro nel file Main.java.

import java.util.ArrayList;

public class Main {
    public static void main (String[] args) {
        Utente utente = new Utente("Farhan", "1996-07-15");

        Libro libro = new Libro("Carmilla", new String[]{"Sheridan Le Fanu"});

        utente.prestito(libro);

        System.out.printf("%s ha preso in prestito questi libri: %s", utente.nome, utente.libriPresiInPrestito.toString()); // Farhan ha preso in prestito questi libri: [Carmilla, Hard West]
    }
}

Guarda come sembra già tutto più pulito, e presto sarà ancora meglio.

Cosa sono i modificatori di accesso in Java?

Hai già visto più volte la parola chiave public. Si tratta di uno dei modificatori di accesso di Java.

Esistono quattro modificatori di accesso in Java:

Modificatore di accesso Descrizione
default Accessibile nel pacchetto
public Accessibile ovunque
private Accessibile nella classe
protected Accessibile nella classe e nelle sottoclassi

Per ora, parleremo dei modificatori di accesso default, public e private. protected sarà discusso in una sezione successiva.

Hai già imparato cosa sono le classi. I pacchetti sono collezioni di più classi separate secondo le loro funzionalità.

Ad esempio, se stai creando un gioco, puoi mettere tutte le classi relative alla fisica in un pacchetto e quelle relative alla grafica in un altro pacchetto.

I pacchetti vanno oltre lo scopo di questo manuale, ma man mano che continui a lavorare su progetti più grandi ci prenderai la mano.

Il modificatore di accesso public è abbastanza autoesplicativo. Variabili, metodi o classi public sono accessibili da ogni altra classe o pacchetto nel tuo progetto.

Quelli private, d'altro canto, sono disponibili soltanto nella loro classe.

Prendiamo come esempio la classe Utente. Il nome e la data di nascita di un utente non dovrebbero essere accessibili dall'esterno.

import java.time.LocalDate;
import java.time.Period;
import java.util.ArrayList;

public class Utente {
    private String nome;
    private LocalDate dataDiNascita;
    private ArrayList<Libro> libriPresiInPrestito = new ArrayList<Libro>();

    Utente (String nome, String dataDiNascita) {
        this.nome = nome;
        this.dataDiNascita = LocalDate.parse(dataDiNascita);
    }

    int eta() {
        return Period.between(this.dataDiNascita, LocalDate.now()).getYears();
    }

    void prestito(Libro libro) {
        this.libriPresiInPrestito.add(libro);
    }
}

Così va meglio. Aggiorniamo anche la classe Libro per rendere le informazioni relative a titolo e autore nascoste dall'esterno.

import java.util.ArrayList;
import java.util.Arrays;

public class Libro {
    private String titolo;
    private ArrayList<String> autori = new ArrayList<String>();

    Libro(String titolo, String[] autori) {
        this.titolo = titolo;
        this.autori = new ArrayList<String>(Arrays.asList(autori));
    }

    public String toString() {
        return String.format("%s di %s", this.titolo, this.autori.toString());
    }
}

Visto che le proprietà ora sono private, la riga System.out.println() nel file Main.java fallirà nell'accedervi direttamente e otterremo un errore.

La soluzione a questo problema è scrivere metodi pubblici che le altre classi possono usare per accedere a queste proprietà.

Cosa sono i metodi getter e setter in Java?

Getter e setter sono metodi pubblici usati per leggere e scrivere delle proprietà private.

import java.time.LocalDate;
import java.time.Period;
import java.util.ArrayList;
import java.util.Arrays;

public class Utente {
    private String nome;
    private LocalDate dataDiNascita;
    private ArrayList<Libro> libriPresiInPrestito = new ArrayList<Libro>();

    public String getNome() {
        return this.nome;
    }

    public void setNome(String nome) {
        this.nome = nome;
    }

    public String getLibriPresiInPrestito() {
        return this.libriPresiInPrestito.toString();
    }

    Utente (String nome, String dataDiNascita) {
        this.nome = nome;
        this.dataDiNascita = LocalDate.parse(dataDiNascita);
    }

    int eta() {
        return Period.between(this.dataDiNascita, LocalDate.now()).getYears();
    }

    void prestito(Libro libro) {
        this.libriPresiInPrestito.add(libro);
    }
}

I metodi getNome() e getLibriPresiInPrestito() sono responsabili di restituire il valore delle variabili nome e libriPresiInPrestito.

Non accediamo mai alla varabile dataDiNascita al di fuori dal metodo eta(), quindi un getter non è necessario.

Dato che il tipo della variabile libriPresiInPrestito non riguarda la classe Main, il getter ci assicura di restituire il valore nel formato appropriato.

Adesso aggiorniamo il codice nel file Main.java in modo che usi questi metodi:

public class Main {
    public static void main (String[] args) {
        Utente utente = new Utente("Farhan", "1996-07-15");

        Libro libro = new Libro("Carmilla", new String[]{"Sheridan Le Fanu"});

        utente.prestito(libro);

        System.out.printf("%s ha preso in prestito questi libri: %s", utente.getNome(), utente.getLibriPresiInPrestito());
    }
}

Ottimo. Ora il codice è addirittura più pulito e facile da leggere. Analogamente ai getter, i setter vengono usati per scrivere i valori di proprietà private.

Ad esempio, potresti desiderare di permettere all'utente di cambiare la sua data di nascita. Il metodo prestito() funziona già come un setter per l'ArrayList libriPresiInPrestito.

import java.time.LocalDate;
import java.time.Period;
import java.util.ArrayList;
import java.util.Arrays;

public class Utente {
    private String nome;
    private LocalDate dataDiNascita;
    private ArrayList<Libro> libriPresiInPrestito = new ArrayList<Libro>();

    public String getNome() {
        return this.nome;
    }

    public void setNome(String nome) {
        this.nome = nome;
    }

    public void setDataDiNascita(String dataDiNascita) {
        this.dataDiNascita = LocalDate.parse(dataDiNascita);
    }
    
    public String getLibriPresiInPrestito() {
        return this.libriPresiInPrestito.toString();
    }

    Utente (String nome, String dataDiNascita) {
        this.nome = nome;
        this.dataDiNascita = LocalDate.parse(dataDiNascita);
    }

    int eta() {
        return Period.between(this.dataDiNascita, LocalDate.now()).getYears();
    }

    void prestito(Libro libro) {
        this.libriPresiInPrestito.add(libro);
    }
}

Ora possiamo chiamare il metodo setNome() con qualsiasi nome vogliamo impostare all'utente. Analogamente, possiamo usare setDataDiNascita() per impostare la data di nascita.

Implementiamo dei getter e dei setter anche nella classe Libro.

import java.util.ArrayList;
import java.util.Arrays;

public class Libro {
    private String titolo;
    private ArrayList<String> autori = new ArrayList<String>();

    public String getTitolo() {
        return this.titolo;
    }

    public void setTitolo(String titolo) {
        this.titolo = titolo;
    }

    public String getAutori() {
        return this.autori.toString();
    }

    public void setAutori(String[] autori) {
        this.autori = new ArrayList<String>(Arrays.asList(autori));
    }
    
    Libro(String titolo, String[] autori) {
        this.titolo = titolo;
        this.autori = new ArrayList<String>(Arrays.asList(autori));
    }

    public String toString() {
        return String.format("%s di %s", this.titolo, this.autori.toString());
    }
}

Ora non puoi accedere direttamente a queste proprietà, ma devi utilizzare i getter e setter appropriati.

Cos'è l'ereditarietà in Java?

L'ereditarietà è un'altra importante caratteristica della programmazione orientata agli oggetti. Immagina di avere tre tipi di libri. Quelli normali, gli e-book e gli audiolibri.

Anche se hanno delle somiglianze, visto che hanno un titolo e un autore, hanno anche alcune differenze. Ad esempio, i libri normali e gli e-book hanno delle pagine mentre gli audiolibri hanno un tempo di esecuzione. Gli e-book possono essere in formato PDF e EPUB.

Quindi usare la stessa classe per tutti e tre non è un'opzione, ma questo non vuol dire che dobbiamo creare tre classi separate con piccole differenze. Possiamo creare classi separate per e-book e audiolibri e far ereditare loro le proprietà e i metodi della classe Libro.

Iniziamo aggiungendo il numero delle pagine alla classe Libro:

import java.util.ArrayList;
import java.util.Arrays;

public class Libro {
    private String titolo;
    private int pagine;
    private ArrayList<String> autori = new ArrayList<String>();

    Libro(String titolo, int pagine, String[] autori) {
        this.titolo = titolo;
        this.pagine = pagine;
        this.autori = new ArrayList<String>(Arrays.asList(autori));
    }

    public String length() {
        return String.format("%s ha %d pagine.", this.titolo, this.pagine);
    }

    public String toString() {
        return String.format("%s di %s", this.titolo, this.autori.toString());
    }
}

Visto che non useremo getter e setter in questi esempi, mi è sembrata una buona idea fare un po' di pulizia. Il metodo length() restituisce la lunghezza del libro come una stringa.

Ora creiamo una nuova classe Java chiamata AudioLibro con il seguente codice:

public class AudioLibro extends Libro{
    private int durata;

    AudioLibro(String titolo, String[] autori, int durata) {
        super(titolo, 0, autori);

        this.durata = durata;
    }
}

La parola chiave extends fa sapere al compilatore che questa classe è una sottoclasse della classe Libro. Ciò significa che questa classe eredita tutte le proprietà e i metodi della classe genitore.

All'interno del metodo costruttore AudioLibro, impostiamo la durata dell'audiolibro – ma occorre anche chiamare manualmente il costruttore della classe genitore.

La parola chiave super in Java fa riferimento alla classe genitore, quindi super(titolo, 0, autori) chiama essenzialmente il metodo costruttore genitore con i parametri necessari.

Visto che gli audiolibri non hanno pagine, impostare il numero di pagine a zero può essere una buona soluzione.

Oppure possiamo creare una versione di overload del metodo costruttore Libro che non richiede il numero di pagine.

Come prossimo passo, creiamo un'altra classe Java chiamata Ebook con il seguente codice:

public class Ebook extends Libro{
    private String formato;

    Ebook(String titolo, int pagine, String[] autori, String formato) {
        super(titolo, pagine, autori);

        this.formato = formato;
    }
}

Questa classe è praticamente identica alla classe Libro eccetto per il fatto che possiede una proprietà formato.

public class Main {
    public static void main (String[] args) {
        Libro libro = new Libro("Carmilla", 200, new String[]{"Sheridan Le Fanu"});
        Ebook ebook = new Ebook("Frankenstein", 220, new String[]{"Mary Shelley"}, "EPUB");
        AudioLibro audioLibro = new AudioLibro("Dracula", new String[]{"Bram Stoker"}, 160);

        System.out.println(libro.toString()); // Carmilla di [Sheridan Le Fanu]
        System.out.println(ebook.toString()); // Frankenstein di [Mary Shelley]
        System.out.println(audioLibro.toString()); // Dracula di [Bram Stoker]
    }
}

Per ora va tutto bene. Ma ricordi il metodo length() che abbiamo scritto nella classe Libro? Funzionerà per i libri ma non per gli e-book.

Questo perché la proprietà pagine è private e nessuna altra classe eccetto Libro può accedervi. Anche il titolo è una proprietà private.

Apriamo il file Book.java e segniamo le proprietà titolo e pagine come protected.

import java.util.ArrayList;
import java.util.Arrays;

public class Libro {
    protected String titolo;
    protected int pagine;
    private ArrayList<String> autori = new ArrayList<String>();

    Libro(String titolo, int pagine, String[] autori) {
        this.titolo = titolo;
        this.pagine = pagine;
        this.autori = new ArrayList<String>(Arrays.asList(autori));
    }

    public String length() {
        return String.format("%s ha %d pagine.", this.titolo, this.pagine);
    }

    public String toString() {
        return String.format("%s di %s", this.titolo, this.autori.toString());
    }
}

In questo modo saranno accessibili dalle sottoclassi. Gli audiolibri hanno un altro problema con il metodo length().

Gli audiolibri non hanno pagine, ma hanno una durata e questa differenza non farà funzionare il metodo length().

Un modo per risolvere questo problema è sovrascrivere il metodo.

Come sovrascrivere un metodo in Java

Come suggerisce il nome, sovrascrivere vuol dire cancellare gli effetti di un metodo sostituendolo con qualcos'altro.

public class AudioLibro extends Libro{
    private int durata;

    AudioLibro(String titolo, String[] autori, int durata) {
        super(titolo, 0, autori);

        this.durata = durata;
    }
    
    @Override
    public String length() {
        return String.format("%s dura %d minuti.", this.titolo, this.durata);
    }
}

Sovrascriviamo un metodo della classe genitore riscrivendolo nella sottoclasse. La parola chiave @Override è un'annotazione. In Java, le annotazioni sono metadati.

Non è obbligatorio annotare il metodo in questo modo, ma se lo fai il compilatore saprà che il metodo annotato sovrascrive un metodo genitore e farà in modo che vengano seguite tutte le regole di sovrascrittura.

Ad esempio, se fai un errore nel nome del metodo, così che non corrisponda più a nessun metodo del genitore, il compilatore ti farà sapere che il metodo non sta sovrascrivendo nulla.

public class Main {
    public static void main (String[] args) {
        AudioLibro audioLibro = new AudioLibro("Dracula", new String[]{"Bram Stoker"}, 160);

        System.out.println(audioLibro.length()); // Dracula dura 160 minuti.
    }
}

Fantastico, non è vero? Ogni volta che sovrascrivi un metodo in Java, tieni a mente che sia il metodo originale che quello che lo sovrascrive devono avere lo stesso tipo di valore di ritorno, stesso nome e stessi parametri.

Conclusione

Ti ringrazio di cuore per aver speso il tuo tempo leggendo questo manuale. Spero che tu ti sia divertito e che tu abbia imparato i concetti fondamentali di Java.

Questo manuale non è congelato nel tempo. Continuerò a lavorarci su e aggiornarlo con miglioramenti, nuovi contenuti e altro. Puoi darmi opinioni e suggerimenti sul manuale in forma anonima con questo form.

Oltre a questo, ho scritto altri manuali completi su altri argomenti complicati che sono disponibili gratuitamente su freeCodeCamp.

Questi manuali sono parte della mia missione di semplificare per tutti l'apprendimento di tecnologie difficili da comprendere. La stesura di ognuno di questi manuali richiede molto tempo e impegno.

Se ti è piaciuto leggere questo manuale e vuoi motivarmi, considera di lasciarmi delle stelle su GitHub e sostienimi per le mie competenze su LinkedIn.

Sono sempre aperto a suggerimenti e discussioni su Twitter o LinkedIn. Contattami con un messaggio privato.

Infine, considera di condividere queste risorse con altri, perché:

Nell'open source, sentiamo fortemente che per fare ben qualcosa, devi coinvolgere tante persone. — Linus Torvalds

Buono studio, ci vediamo alla prossima.