18 articoli nella categoria Corso Java

Andrea Pastore 19/03/2020 0

13. Le eccezioni in Java

Quando creiamo un programma anche se la sintassi è corretta non significa che sia tutto perfettamente funzionante, possono verificarsi errori durante l'esecuzione che java non è stato in grado di rilevare in fase di compilazione.

Questo tipo di errori si definisce "a Runtime", perché avvengono durante l'esecuzione. Quando si verificano questi errori Java lancia un'eccezione, che termina immediatamente il programma. Vediamo un esempio: le due righe di codice sottostanti danno errore in esecuzione, perché la posizione 15 non esiste nel nostro array. Si tratta di un errore che non può essere individuato quando si scrive  il codice perché la sintassi è corretta: stiamo semplicemente chiedendo a java di darci la stringa presente ad una posizione. Solo al momento dell’esecuzione Java si accorgerà che l’array ha solo 10 elementi e non potrà fornire la posizione 15, per cui lancerà l’eccezione che terminerà il programma in maniera inaspettata.

public class HelloException {

public static void main(String[] args) {

String[] arrayString = new String[10];

System.out.println(arrayString[15]);

}

}

Questo codice genererà un’eccezione di tipo ArrayIndexOutOfBoundsException. Questa eccezione indica proprio che abbiamo chiesto una posizione non presente nell’array.

 

Anatomia di un’eccezione

Le eccezioni sono classi Java che estendono la classe Exception, hanno sempre un nome che contiene la parola Exception e altre parole che indicano la natura del problema.

Lo stack trace di un’eccezione

Quando si verifica un’eccezione Java stampa la riga di codice che ha generato quell’ecezione, con tutte le chiamate precedenti. Questo è molto utile per capire la sorgente dell’errore. Si veda l’immagine sottostante:

In questa immagine viene mostrato uno stack trace causato da un NullPointerException. L’ultima chiamata è in basso (difatti è nella classe Main che è la classe che fa partire i programmi Java), poi viene ripercorsa la storia delle chiamate: dalla classe Main si arriva alla classe GestoreOrdini e qui viene indicato il metodo che causa l’eccezione.

 

Vediamo alcune eccezioni comuni, che troveremo frequentemente nel nostro futuro lavoro:

Nome Funzionalità
NullPointerException  Compare quando stiamo cercando di accedere ad una risorsa che, al momento, ha valore null
ArrayIndexOutOfBoundsException  Compare quando stiamo provando ad accedere ad una posizione dell’array che non esiste (ad esempio quando l’array ha 10 elementi e noi proviamo ad accedere alla posizione 15.
IOException Comapre quando java non riesce ad eseguire operazioni di input/output (ad esempio la scrittura di un file).

 

Nota: gli errori a Runtime non dipendono sempre da una nostra disattenzione, ma potrebbero essere causati da una causa esterna. Quello che dobbiamo fare, però, è gestirle, in modo che il nostro programma sappia sempre come agire.

 

Gestire un’eccezione

Per gestire un’eccezione abbiamo in realtà due metodi: il primo è il costrutto  try ... catch. Il secondo è la parola chiave throws, che può essere utilizzata quando si vuole posporre la gestione dell’eccezione. I più importanti IDE (quindi anche Eclipse, quello consigliato in questo libro) consentono di scegliere tra i due metodi di gestione dell’eccezione. Osserviamo ora le differenze.

 

Il costrutto  try ... catch

All’interno del try deve essere inserito il codice che può generare un’eccezione, mentre all’interno del catch viene inserito il codice per gestire l’eccezione quando questa si verifica. Riscriviamo il codice visto prima in modo che l’eccezione venga gestita:

public class HelloException {

public static void main(String[] args) {

String[] arrayString = new String[10];

try {

System.out.println(arrayString[15]);

}

catch(Exception e) {

System.out.println("si è verificata un'eccezione");

}
}
}

Quando Java eseguirà questo codice si accorgerà nuovamente che non esiste la posizione 15, ma essendoci un costrutto catch ha le istruzioni su come comportarsi, quindi eseguirà il codice contenuto in esso e continuerà l’esecuzione del programma. Nel catch dobbiamo indicare l’eccezione che vogliamo catturare: nell’esempio sopra abbiamo passato come parametro Exception e, il che significa che verranno considerate tutte le eccezioni: qualsiasi eccezione si verificherà verrà eseguito sempre lo stesso codice (in questo caso verrà stampata la frase “Si è verificata un’eccezione”). È possibile specificare azioni diverse da intraprendere a seconda dell’eccezione che si verifica aggiungendo più clausole di catch. Si veda il codice sotto, che dopo il try presenta due clausole catch, che catturano ognuna un’eccezione diversa:

 

public class HelloException {

public static void main(String[] args) {

String[] arrayString = new String[10];

try {

System.out.println(arrayString[15]);

}

catch(ArrayIndexOutOfBoundsException e) {

System.out.println("si è verificata un'eccezione ArrayIndexOutOfBoundsException");

}

catch(NullPointerException e) {

System.out.println("si è verificata un'eccezione NullPointerException");

}

}

}

Non c’è limite al numero di eccezioni che possiamo catturare, e questa caratteristica ci consente di eseguire le giuste operazioni ad ogni eccezione.

 

La parola chiave throws

Ci sono casi in cui non vorremmo gestire le eccezioni nel metodo in cui si possono verificare, perché per qualche motivo può esserci più comodo gestirle in altre parti del codice. In questi casi si può usare la parola chiave throws seguita dal nome dell'eccezione.

L'eccezione non è gestita, ma Java considererà il metodo corretto e richiederà la gestione dell'eccezione ogni volta che quel metodo verrà invocato.

Leggi tutto

Andrea Pastore 19/03/2020 0

12. Polimorfismo in Java

Alcuni linguaggi di programmazione consentono di decidere il comportamento dei metodi e costruttori in base al numero di variabili che vengono passate loro come parametro.  Supponiamo ad esempio di avere una classe Java chiamata Utente, con tre variabili (id, nome, cognome) e due costruttori: il primo prenderà in input solo una variabile id, il secondo prenderà in input una variabile id, una variabile nome e una variabile cognome.

I due costruttori in java saranno i seguenti

public Utente(int id) {

this.id = id;

this.nome = “”;

this.cognome = “”;

this.email = “”;

}

public Utente(int id, String nome, String cognome) {

this.id = id;

this.nome = nome;

this.cognome = cognome;

}

Quando noi invochiamo il costruttore a seconda del numero di parametri che inseriamo Java userà il costruttore che combacia con i parametri inseriti ed eseguirà il codice contenuto all’interno. Java darà un errore solo se non trova un costruttore con il numeor di parametri da noi specificato. Ad esempio se scriviamo:

	Utente utente =  new Utente(3,”Mario”,”Rossi”,”mario@gmail.com”);

Otterremo un errore, perché non abbiamo definito un costruttore che prende in input quattro parametri. Tutto quello che abbiamo detto per i costruttori è valido anche per i metodi (del resto il costruttore non è altro che un metodo speciale). Prendiamo in esame il seguente metodo che stampa tutti i dati dell’utente:

public void stampaDati() {

System.out.println(this.id + “ ” + this.nome + “ ” + this.cognome + “ ” +this.email);

}

Come per i costruttori possiamo creare un altro metodo che ha lo stesso nome, ma prende in input un parametro per stabilire come devono essere stampati questi elementi. Stabiliamo due tipologie di stampa: essenziale che stampa solo nome e cognome, completa che stampa tutto. Se il parametro passato in input non corrisponde a nessuna di queste due cose allora questo metodo invoca a sua volta il metodo stampaDati senza parametri, che stamperà i dati senza formattazione.

	public void stampaDati(String tipoVisualizzazione) {

if(tipoVisualizzazione.equals("essenziale")) {

System.out.println("Nome:"+this.nome+"; Cognome:"+this.cognome);

}

else if (tipoVisualizzazione.equals("completa")){

System.out.println("Id: "+this.id + "; Nome:"+this.nome+"; Cognome:"+this.cognome+"; Email:"+this.email);

}

else {

stampaDati();

}

}

Ereditarietà

L’ereditarietà è una caratteristica di molti linguaggi di programmazione moderni che consente di ottimizzare il codice scritto ed evitare ripetizioni. Ci sono casi in cui dobbiamo creare classi molto simili tra loro. Per questo molti linguaggi di programmazione consentono di usare il meccanismo dell’ereditarietà. Supponiamo di avere bisogno di questa due classi:

  • Utente
    id, nome, cognome, email,telefono
  • Fornitore
    id, nome, cognome, email,telefono,partitaIva

notiamo che Fornitore è quasi uguale alla classe Utente, quindi non ha senso riscriverla. Quando creiamo la classe Fornitore che estenderà la classe Utente. Scriviamo le due classi per renderci conto del codice e delle differenze tra le due. Qui sotto vediamo la classe utente:

public class Utente {

private int id ;

private String nome;

private String cognome;

private String email;

private String telefono;



public Utente(int id, String nome, String cognome, String email, String telefono) {

this.id = id;

this.nome = nome;

this.cognome = cognome;

this.email = email;

this.telefono = telefono;

}

// Getter & setter

public int getId() {

return id;

}

public void setId(int id) {

this.id = id;

}

… tutti gli altri getter e setter

}

Vediamo ora la classe Fornitore. Come abbiamo capito Java ci consente di evitare di ricopiare la classe, basta dichiarare, al momento della creazione, che la nuova classe estende la classe Utente. Questo avviene con la parola chiave extends.

public class Fornitore extends Utente {

private String partitaIva;

public Fornitore(String partitaIva, int id, String nome, String cognome, String email, String telefono) {

super(id, nome, cognome, email, telefono, via, citta, cap);

this.partitaIva = partitaIva;

}

public String getPartitaIva() {

return partitaIva;

}

public void setPartitaIva(String partitaIva) {

this.partitaIva = partitaIva;

}

}


Notiamo alcune differenze con la classe Utente scritta sopra: innanzitutto vediamo che nel costruttore c’è la parola chiave super, seguita da una serie di parametri. Si tratta dei parametri del costruttore della classe Utente, che viene richiamato con il comando super. Ogni volta che noi estendiamo una classe il costruttore della nuova classe deve richiamare il costruttore della classe che estende. Fornitore estende la classe Utente, quindi il suo costruttore prende in input anche i parametri che richiede il costruttore di Utente, che invocherà con il metodo super().

Nota: quando una classe estende un’altra classe, la classe estesa è definita superclasse

 

Le interfacce in Java

Vediamo ora un altro concetto fondamentale della programmazione: le interfacce, che  servono per standardizzare il codice.

 

Interfacce

Le interfacce vengono utilizzate per definire un tipo di dato e una classe, che però non viene implementato. Vengono definite solo le firme dei metodi che poi saranno implementate in altre classi. Vediamo un esempio di interfaccia:

public interface Libro {

public void stampaLibro();

public void setTitolo(String titolo);

public String getTitolo();

public void setNumeroPagine(int numeroPagine);

public int getNumeroPagine();

}

Per usare una determinata interfaccia dopo il nome della classe dobbiamo usare la parola chiave implements seguita dal nome dell'interfaccia. Appena facciamo questo il nostro ide ci segnalerà che i metodi dell'interfaccia non sono ancora implementati, clicchiamo su add unimplementated methods (potrebbe cambiare a seconda dell’ide usato) e il nostro ide li aggiungerà per noi, dobbiamo solo completarli con il codice all'interno.

In sostanza le interfacce consentono di standardizzare il codice: dueprogrammatori si accordano sui metodi e su come vengono chiamati, e poiai fini dell'interoperabilità delle classi non è importante come vieneimplementato. Nel tempo le implementazioni possono cambiare, ma rispettandole interfacce non ci saranno problemi di incompatibilità

 

Il paradigma MVC

La programmazione ad oggetti è utilizzata nel paradigma MVC (MODEL, VIEW, CONTROLLER), che consente una separazione delle logiche. Vediamo i tre elementi nel dettaglio:

  • model
    sono le classi cosi come le abbiamo viste. Quando creiamo una classe infatti definiamo degli oggetti ma non li usiamo in quell’istante
  • view
    le viste rappresentano la parte del progetto che mostra i dati visivamente
  • controller
    sono i file che consentono di rendere permanenti le operazioni sugli oggetti: creazione di nuovi elementi, modifica di elementi esistenti, eliminazione di elementi e anche recupero dei dati di un elemento. I controller nascondono le logiche sottostanti alle altre classi: potremmo voler salvare i dati in un file oppure nel database, per le classi esterne viene chiamato lo stesso metodo. Facendo passare tutte le operazioni di salvataggio, modifica, caricamento per il controller avremo solo una classe che gestisce queste operazioni, che si traduce in un vantaggio per la manutenibilità del codice, in quanto per apportare modifiche alla gestione degli oggetti dovremmo modificare solo la classe relativa.
Leggi tutto

Andrea Pastore 19/03/2020 0

11. La programmazione a oggetti

Finora abbiamo programmato in modo procedurale, ovvero le istruzioni che abbiamo scritto venivano eseguite una dopo l’altra. Questo sistema di programmazione non è molto efficiente, perché i codici diventano molto grandi e difficilmente manutenibili. La programmazione ad oggetti è nata per risolvere questo problema. Si basa su due principi fondamentali: le classi e gli oggetti. Una classe è un file che definisce un tipo di dato, nonché le operazioni che possono essere effettuate dagli oggetti di quella classe. Sarà possibile poi creare oggetti di quel tipo di dato che possono eseguire le operazioni stabilite.

 

Anatomia di una classe

Ogni classe è composta da tre tipi di elementi:

 

Variabili

le variabili di una classe si dichairano come le variabili che abbiamo sempre visto, ovvero specificando il tipo di dato (int, double, String e cosi via) ma in più dobbiamo specificare il tipo di accesso, ovvero public, private, protected. Questo è necessario per utilizzare una caratteristica della programmazione ad oggetti, l’incapsulamento, che vedremo più avanti.

 

Metodi

I metodi rappresentano le operazioni che possono essere eseguite dagli oggetti di quella classe. Possono avere parametri in input e possono essere usati per eseguire le operazioni più varie.

Anche per i metodi vale l’incapsulamento, per cui per ogni metodo che creiamo dovremmo specificare un indicatore di accesso tra public, private, protected. Subito dopo abbiamo un altro indicatore, che specifica il tipo di valore ritornato dal metodo. Void indica che non viene restituito alcun valore, int indica invece che viene restituito un intero, String indica che viene restituita una stringa e cosi via.

 

Costruttore

una casse di per se è solo un elenco di metodi e variabili, definisce il comportamento di un oggetto ma non viene eseguita direttamente, per utilizzarla dobbiamo creare un oggetto di quel tipo. Per questo ogni classe ha un metodo speciale chiamato costruttore, che serve a creare l’oggetto su cui noi possiamo poi usare i metodi definiti nella classe.

Il costruttore si differenzia dagli altri metodi perché è chiamato con lo stesso nome della classe e non ha specifica alcun tipo di dato. Come gli altri metodi può prendere in input dei parametri con i quali inizializzerà le variabili di classe. Il costruttore si invoca con la parola chiave new seguita dal nome della classe e i parametri che abbiamo stabilito nel costruttore.

 

Cosa succede quando invochiamo un costruttore?

Quando invochiamo un costruttore di una classe viene creato un oggetto che ha al suo interno le variabili definite da quella classe, che ci mette a disposizione i metodi definiti nella classe. Per ogni classe possiamo creare tutti gli oggetti che vogliamo.

 

Caratteristiche della programmazione ad oggetti

Incapsulamento

Nelle classi abbiamo la possibilità di scegliere, oltre al tipo di variabile, anche il livello di accesso, tra questi tre valori:

  • public
    se dichiariamo una variabile public questa sarà accessibile anche all’esterno della classe, qualunque parte di un programma potrà leggere quei valori.
  • private
    Se invece la dichiariamo private questi valori potranno essere letti solo all’interno della classe.   
  • protected
    è una via di mezzo: consente la lettura di questa variabile alle classi che estendono la classe che stiamo scrivendo. Vedi il paragrafo successivo sull’ereditarietà.

la regola è quella di dichiarare le variabili private e poi utilizzare dei metodi per restituire o modificare dei valori

Facciamo un esempio, supponiamo di voler creare la classe ContoBancario che gestisce i conti correnti di una banca. Questa classe avrà questi parametri:

  • numeroConto
  • nome
  • cognome
  • saldo

Una possibile implementazione potrebbe essere questa:

public class ContoBancario {

public int numeroConto;

public String nome;

public String cognome;

public double saldo;



public ContoBancario(int numeroConto, String nome, String cognome, double saldo) {

this.numeroConto = numeroConto;

this.nome = nome;

this.cognome = cognome;

this.saldo = saldo;



}




… altri metodi

}


le variabili di classe sono state dichiarate public, quindi tutti hanno accesso al loro valore, anche i codici esterni. Questo però può creare dei problemi, perché potrebbe esserci un codice del genere:

ContoBancario contoMario = new ContoBancario(123456, “Mario”, “Rossi”, 10);

contoMario.saldo = 100000;

con la seconda istruzione un codice esterno può modificare direttamente il saldo del conto, perché la variabile è stata dichiarata public e tutti ne hanno accesso. Facciamo una modifica alla nostra classe, trasformiamo tutte le variabili da public a private:

public class ContoBancario {

private int numeroConto;

private String nome;

private String cognome;

private double saldo;


public ContoBancario(int numeroConto, String nome, String cognome, double saldo) {

this.numeroConto = numeroConto;

this.nome = nome;

this.cognome = cognome;

this.saldo = saldo;

}

// getter & setter

public void setNome(String nome) {

this.nome = nome;

}

public String getNome() {

return this.nome;

}

public void setCognome(String cognome) {

this.cognome = cognome;

}

public String getCognome() {

return this.cognome;

}

public void setSaldo(int ammontare) {

this.saldo = ammontare;

}

public int getSaldo() {

return this.saldo;

}


… altri metodi

}

 

Ora questa istruzione:

contoMario.saldo = 100000;

non potrà più essere eseguita, perché le variabili private di una classe sono accessibili solo ai metodi della classe e possono essere  modificate solo da loro. Per effettuare una modifica del saldo di questa classe bisognerà usare un metodo della classe, che abbiamo chiamato setSaldo(), che potrà avere dei sistemi di controllo che impediranno di scrivere cifre a caso. La stessa cosa vale per recuperare il valore del saldo, che può essere recuperato solo usando il metodo getSaldo(). Questi due metodi sono una tipologia di metodi molto comune: i metodi getter e setter, che hanno proprio il compito di far recuperare i valori dall’esterno della classe e di permettere la modifica delle variabili di classe. Notiamo che questi metodi devono essere dichiarati public, in modo che possono essere invocati dall’esterno.

 

Importare una classe

Quando dobbiamo usare una classe che non si trova nello stesso package dobbiamo importarla. Per farlo si scrive la parola chiave import più il nome del package seguito dal nome della classe. Ad esempio supponiamo di voler importare una classe Utente da un package chiamato model. Per utilizzarla dovremmo scrivere in alto, prima di ogni altro codice, il codice per importare:

import model.Utente;

se ci sono più classi da importare i vari comandi di import vanno scritte uno sotto l’altro.

Leggi tutto

Andrea Pastore 19/03/2020 0

10. Strutture di dati

Finora abbiamo visto solo tipi di dati semplici: int, double, boolean e cosi via. Tuttavia i programmi reali devono elaborare grandi quantità di dati, e sarebbe scomodo e limitante memorizzarli uno per volta. Per questo motivo già dagli anni Ottanta sono state create le strutture di dati, tipi di variabili più elaborati che possono contenere al loro interno delle  variabili.

 

Gli array

La struttura dati più semplice è l’array, che consente di memorizzare un numero stabilito di elementi di un determinato tipo. Ad esempio la riga sottostante crea un array di 10 numeri interi

int[] arrayInt = new int[10];

se non conosciamo la dimensione dell’array (perché magari ci è stato passato in input da un programma che non abbiamo realizzato noi) possiamo recuperarla con la proprietà length:

int arraySize = arrayInt.length;

Se sono noti tutti gli elementi che dobbiamo inserire nell’array possiamo anche inizializzarlo in questo modo:

int[] array2 = {2, 5, 6, 7, 11};

Essendo l’array un contenitore di variabili, quando lo utilizziamo dobbiamo specificare a quale variabile vogliamo accedere, ad esempio la seguente riga di codice:

		System.out.println(arrayInt[5]);

Stampa l’elemento alla posizione 5 dell’array.

Nota: la prima posizione di un array non è 1, ma 0; quindi stampare la quinta posizione significa in realtà stampare  il sesto elemento dell’array.

Frequentemente capita di dover elaborare in sequenza tutti gli elementi di un array, in questo caso possiamo utilizzare un ciclo for. Ad esempio il ciclo for sottostante inizializza tutti gli elementi dell’array creato precedentemente al valore 5.

		for(int i=0;i<arraySize;i++) {			

arrayInt[i] = 5;

}

Notiamo che nel ciclo abbiamo usato una variabile arraySize dove è memorizzata la dimensione dell’array. Questa variabile è stata creata qualche riga sopra. È anche possibile scrivere il ciclo for in questo modo:

		for(int i=0;i<arrayInt.length;i++) {			

arrayInt[i] = 5;

}

in questo modo evitiamo di scrivere la variabile esterna, ma stiamo sprecando tempo di calcolo, perché ad ogni ciclo Java dovrà calcolare la dimensione dell’array.

Leggi tutto

Andrea Pastore 19/03/2020 0

9. Input dell’utente: la classe Scanner

I programmi che abbiamo visto finora non prevedono l’interazione da parte dell’utente, possono solo essere avviati. Questo è diverso dai programmi reali, che danno la possibilità all’utente di interagire. Cominciamo a vedere la forma più semplice di interazione dell’utente, con dei programmi che saranno in grado di prendere input da tastiera.

Per questo utilizzeremo la classe Scanner, una delle tante classi messe a disposzione da Java per degli scopi specifici. Nel caso della classe Scanner questa è stata creata per consentire a Java di prendere in input dati testuali o numerici. Facciamo un esempio: il codice qui sotto chiede all’utente di scrivere qualcosa con la tastiera e poi stamperà quello che l’utente ha scritto.

import java.util.Scanner;

public class HelloScanner {

public static void main(String[] args) {

  Scanner input = new Scanner(System.in);

  System.out.println("Scrivi qualcosa");

  String s = input.nextLine();

  System.out.println("Hai scritto: "+s);

}

 }

Notiamo che questo programma ha una nuova riga di codice:

import java.util.Scanner;

Quando vogliamo usare una classe messa a disposizione da Java dobbiamo importarla. Faremo la stessa cosa in seguito quando importeremo classi realizzate da noi. Continuiamo l’analisi del nostro programma: vediamo subito dopo una classe e poi un metodo main, che sono cose a noi note. Poi vediamo un’altra riga di codice nuova:

Scanner input = new Scanner(System.in);

con questa riga stiamo creando un oggetto di tipo Scanner. Questa cosa ci sarà più chiara in seguito, per ora prendiamolo come un passaggio necessario per prendere in input questi dati. Proseguendo la lettura del codice troviamo un altro comando a noi noto, quello che ordina a Java di stampare:

System.out.println("Scrivi qualcosa");

Seguito da :

 String s = input.nextLine();

che consente a java di recuperare il testo inserito dall’utente. In sostanza Il programma chiede all’utente di scrivere qualcosa (con il comando precedente) e poi si mette in ascolto: resterà bloccato fino a quando l’utente non avrà scritto qualcosa e premuto invio.

La riga successiva che troviamo

System.out.println("Hai scritto: "+s);

stampa a video ciò che ha scritto l’utente.

 

Recuperare tipi di dati diversi da String

Per la maggior parte del tempo utilizzeremo lo scanner per recuperare stringhe, ma può capitarci di dover recuperare altri tipi di dati ad esempio numeri interi o decimali. In questo caso dovremmo usare il seguente codice:

int n = input.nextInt();		 // recuperiamo un numero intero

double d = input.nextDouble(); // recuperiamo un numero decimale

Nota: questi metodi possono creare dei problemi se subito dopo aver recuperato una numero dobbiamo recuperare una stringa. Se si verifica questa esigenza dobbiamo recuperare una stringa e poi fare la conversione, come mostrato in seguito:

 

	System.out.println("Scrivi un numero");

String s = input.nextLine();

int n    = Integer.parseInt(n);
Leggi tutto

Cerca...