Java4LAI Survival Guide


1. Introduzione

Questa esercitazione introduttiva e' indirizzata a quegli studenti del corso di LAI che non hanno ancora avuto modo di familiarizzare con il linguaggio Java e l'ambiente di sviluppo Eclipse in altri corsi. L'esercitazione non vuole assolutamente avere un valore didattico sul linguaggio, ma solo introdurre le funzionalita' essenziali per eseguire le varie esercitazioni previste nel corso di LAI.

2. Creazione di un Progetto in Eclipse

Creiamo un progetto Java in Eclipse:

  1. aprire eclipse

  2. Selezionare File > New > Java Project

  3. Inserire il nome del progetto, ad esempio "Tutorial", e la location dove verranno salvate le risorse necessarie.

  4. Selezionare "Next" e "Finish".

3. Creazione di una prima applicazione Standalone

Nel corso delle varie esercitazioni, utilizzeremo vari tipi di applicazioni Java (servlet, web services, ...). Per il momento utilizziamo una semplice applicazione standalone, in grado cioè di girare autonomamente.

Per creare una qualunque applicazione in Java, è sempre necessario creare una Classe principale che la rappresenti.

Definire una classe richiede sostanzialmente di:

  • definire i metodi che implementano le operazioni supportate dalla classe;

  • definire i dati interni alla classe (le variabili di classe), sui quali lavorano i vari metodi di quella classe;

  • Individuare un package di appartenenza per questa classe, per evitare conflitti di naming con eventuali classi omonime contenute in altri package ed utilizzate nella stessa applicazione.

Un'applicazione Standalone si caratterizza per il fatto di includere una classe che definisce un metodo main che sarà attivato come punto di ingresso allo start dell'applicazione.

Applicando le nozioni introdotte sinora, vediamo ora come definire una prima applicazione Standalone Test nel package lai.tutorial che stampi la classica stringa hello world a video.

  • Creare il package con New > Package inserendo come nome lai.tutorial.

  • Creare la classe Test con New > Class avendo cura che sia indicato il package creato in precedenza e di spuntare sotto la voce "Which method stubs would you like to create?" la casella "public static void main(String[] args)".

La classe generata avra' la struttura seguente:

package lai.tutorial;

public class Test {
	public static void main(String[] args) {

	}
}

Eclipse ha creato per noi il metodo main che rende la classe Test il punto di ingresso della nostra applicazione. Da notare che si tratta di un metodo public static, che significa che il metodo è invocabile non solo dai metodi della classe (public) e che può essere invocato sulla classe, senza bisogno di creare un oggetto di quella classe. Questi concetti saranno chiariti meglio pi? avanti.

L'applicazione è ora già un'applicazione formalmente completa, che però non fa nulla. Implementiamo quindila logica applicativa richiesta, inserendo nel main l'istruzione che stampa la stringa voluta:

System.out.println("Hello World");

La classe System è una classe predefinita nel runtime di Java che include facility di base per l'interazione con il sistema sottostante. In questo caso utilizziamo una facility di stampa sullo standard output.

[Nota] Nota

In Eclipse, premendo ctrl + spazio durante la digitazione si ottengono i suggerimenti di completamento.

Adesso possiamo eseguire la classe selezionando Run > Run oppure con ctrl + F11 ottenendo la stampa a video della stringa "Hello World".

4. Import di classi esterne nel programma

Nella scrittura di una nuova classe, si può fare in vario modo uso di altre classi già definite. Ogni classe che utilizziamo può essere indicata con il suo nome esteso, comprensivo del package di appartenenza, oppure solo con il nome breve, se ne e' stato effettuato l'import.

[Nota] Nota

E' implicito l'import del package java.lang. Per questo non e' stato necessario per l'uso della classe java.lang.System nell'esempio precedente.

Proviamo ora ad utilizzare delle librerie di I/O in Java per scrivere la stringa di saluto questa volta in un file. Le classi che vogliamo utilizzare sono definite nel package java.io (documentate in: api doc), in particolare useremo la classe FileOutputStream. I passi da fare per poter usare la libreria sono i seguenti:

  • importare il package java.io.FileOutputStream

    import java.io.FileOutputStream;
    					
    [Nota] Nota

    Quando usiamo una classe senza importarla, Eclipse segnala un errore suggerendo i possibili import come soluzione.

  • creare un oggetto di tipo FileOutputStream, passandogli il nome del file al momento della creazione. Per creare nuovi oggetti si può usare il costrutto new, come nell'esempio seguente:

    FileOutputStream fos = new FileOutputStream("/tmp/tutorial.txt");
    					

    [Nota] Nota

    Notare come, usando la funzionalità di Ctrl-Space di Eclipse, dopo aver digitato FileOutputStream fos = new, vengano visualizzati tutti i costruttori disponibili per quell'oggetto. I costruttori sono speciali metodi usati per inizializzare i nuovi oggetti e che si differenziano tra loro per i parametri di input. Nel nostro caso, useremo il costruttore:

    FileOutputStream(string filename)
    			

    che prende in input il nome del file in cui scrivere.

  • invocare i metodi write e close sull'oggetto fos, come nell'esempio seguente:

    fos.write("Hello World".getBytes());
    fos.close();
    					

    [Nota] Nota

    Anche qui possiamo usare la funzionalità di completamento di Eclipse dopo aver digitato fos.. Tra i metodi offerti da questa classe il pi? adatto risulta il metodo write(byte[]) che permette di scrivere un array di byte nel file. Ci resta quindi da convertire la stringa "Hello World" in array di byte, usando il metodo getBytes() sull'oggetto stringa. Infine dobbiamo ricordarci di chiudere il file descriptor fos tramite il metodo close().

Una volta completato il nostro codice, ci accorgeremo però che Eclipse ci segnala degli errori. Portando il mouse sulle linee segnate da errore vediamo che lamenta errori di tipo "Unhandled Exceptions", che dovremo risolvere prima di poter far girare il programma.

5. La Gestione delle Eccezioni nell'invocazione dei metodi

Le invocazioni dei metodi possono sollevare delle eccezioni in caso di problemi a run-time. Nel nostro caso eccezioni di tipo "FileNotFoundException" e "IOException" possono verificarsi manipolando il file.

In questi casi si usa il costrutto try/catch, con la logica di eseguire il codice nel blocco try e, in caso di eccezione, saltare nel blocco catch corrispondente. Il finally viene eseguito alla fine, in qualsiasi caso.

[Nota] Nota

Nel caso Eclipse segnali un errore di tipo "Unhandled Exceptions", e' possibile auto generare il codice di try/catch cliccando su "Surround with try/catch"

Una volta inserita anche la gestione delle eccezioni, il nostro codice dovrebbe apparire come segue ed essere finalmente eseguibile con il comando Run

package lai.tutorial;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Test {

	public static void main(String[] args) {
		
		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream("/tmp/tutorial.txt");
			fos.write("Hello World".getBytes());
		} catch (FileNotFoundException e) {
			System.out.println("Errore nell'apertura del file: " + e);
		} catch (IOException e) {
			System.out.println("Errore nella scrittura del file: " + e);
		} finally {
			try { fos.close(); } catch (Exception e) {}
		}
	}
}
[Nota] Nota

L'uso di classi esterne può richiedere o meno di importare gli archivi jar che le contengono. Finora non è stato necessario perchè le classi usate sono già incluse nel runtime del linguaggio Java. Vedremo invece pi? avanti come gestire l''importazione dei jar file contenenti il codice della libreria, per l'uso di Classi che non facciano già parte del runtime del linguaggio.

6. Definizione di nuove classi

Finora ci siamo limitati ad usare all'interno del metodo main classi preesistenti. Vediamo ora come definire delle nuove classi, implementando la classe lai.tutorial.Cubo che simula il comportamento di un cubo.

Per prima cosa otteniamo lo scheletro della nuova classe tramite il comando Eclipse File->New->Class, ottenendo il codice che segue:

package lai.tutorial;

public class Cubo {

}

Definiamo quindi una variabile di tipo intero "lato" che indica appunto la dimensione del nostro Cubo:

package lai.tutorial;

public class Cubo {
	private int lato;
}

Come prima cosa, osserviamo che sia i metodi che le variabili possono avere diversi scope:

  • Private: indica la visibilità solo nella classe di appartenenza.

  • Protected: indica la visibilità anche nelle classi che estendono quella di appartenenza (vedremo meglio pi? avanti la definizione di estensione).

  • Package: indica la visibilità anche nelle classi dello stesso package.

  • Public: visibile a tutti.

[Nota] Nota

Se non viene indicato nessuno scope, e' implicito l'uso di Package

Ora definiamo un costruttore, ovvero un metodo per l'instanziazione della nostra classe. Un costruttore, che abbiamo già usato pi? avanti per istanziare la classe FileOutputStream, e' un metodo senza valore di ritorno che si chiama esattamente come la classe che instanzia. Puo' avere dei parametri, quindi possono esserci piu' costruttori per una stessa classe differenziati dal numero o tipo di parametri passati. Vogliamo che il costruttore della nostra classe prenda in input la dimensione del Cubo e valorizzi l'attributo lato, come nel codice che segue:

package lai.tutorial;

public class Cubo {
	private int lato;
	
	public Cubo (int l) {
		lato = l;
	}
}

Aggiungiamo ora alla classe un metodo getVolume che non richiede parametri e restituisce il valore del Volume del nostro solido, come nel codice seguente:

	public double getVolume(){
		return lato * lato * lato;
	}
}

Estendiamo ora la nostra applicazione per farle creare un Cubo di lato di dimensione 2, stampando poi a video il valore del suo volume. Il codice è mostrato nello schema seguente.

		Cubo c = new Cubo(2);
		double volume = c.getVolume();
		System.out.println("Il volume del cubo e': " + volume);

dove si introduce anche l'uso dell'operatore + di concatenazione tra stringhe. Eseguiamo il codice e verifichiamo il risultato.

7. Gestione delle eccezioni nell'implementazione di un metodo'

Sinora abbiamo visto come gestire le eccezioni potenzialmente generate al momento dell'invocazione di un metodo. Perchè un metodo possa generare delle eccezioni, però, queste devono essere eplicitamente previste per le condizioni di errore al momento della scrittura del metodo.

Vediamo come farlo, estendendo la nostra classe lai.tutorial.Cubo, in modo da restituire una condizione di errore in caso di invocazione del costruttore con parametro con valore negativo. In particolare il metodo deve essere rivisto per usare il costrutto throws sia in firma che al momento del verificarsi dell'eccezione, come nel codice seguente.

	
public Cubo(int l) throws Exception{
	if(l<0) throw new Exception("Non e' possibile instanziare un cubo con lato < 0.");
	lato = l;
}
	

Adesso chi utilizza il costruttore deve prevedere la possibilita' che venga sollevata l'eccezione e gestirla adeguatamente tramite lo statement try/catch, come nell'esempio che segue.

			
package lai.tutorial;

public class Test {

	public static void main(String[] args) {
		Cubo c = null;
		int lato = -2;
		
		try {
			c = new Cubo(lato);
		} catch (Exception e) {
			try {
				// In caso di errore, lato e' negativo. 
				// Uso il suo valore assoluto per portarlo in positivo 
				c = new Cubo(Math.abs(lato));
			} catch (Exception e1) {
				// Impossibile che capiti questa eccezione!
				// Prevedo comunque di stampare lo stack trace e uscire
				e.printStackTrace();
				return;
			}
		}
		double volume = c.getVolume();
		System.out.println(volume);
	}
}

8. Interfacce

A volte è necessario definire un'interfaccia comune a cui un certo numero di classi debba essere conforme. La cosa è molto utile perchè permette di scrivere codice riusabile, indipendente dall'implementazione di classe utilizzata effettivamente a run-time, come avviene ad esempio per i driver. Un tale modello è realizzato tramite il costrutto interface, analogo ad una classe ma contenente esclusivamente le signature dei metodi e non la loro implementazione.

Nel nostro caso, possiamo ad esempio definire un'interfaccia Solido che preveda che tutte le classi che la implementano debbano avere il metodo getVolume. Creiamo dunque l'interfaccia con File > New > Interface

package lai.tutorial;

public interface Solido {
	public int getVolume();
}

Modifichiamo adesso la classe Cubo in modo che implementi l'interfaccia Solido, utilizzando il costrutto implements come nell'esempio che segue.

package lai.tutorial;

public class Cubo implements Solido{
	
	[...]
	
	public double getVolume(){
		return lato * lato * lato;
	}
}

E' ora possibile creare nuove classi che implementano la stessa interfaccia ma con una diversa implementazione, come il Cono dell'esempio seguente:

package lai.tutorial;

public class Cono implements Solido {
	private int raggio;
	private int altezza;
	
	public Cono(int r, int h) throws Exception{
		if(r<0 || h<0) throw new Exception("Non e' possibile instanziare un cono con raggio o altezza < 0.");
		raggio = r;
		altezza = h;
	}
	
	public double getVolume(){
		return raggio * raggio * Math.PI * altezza / 3;
	}
}

9. Classi astratte ed ereditarieta'

Aggiungiamo un nuovo metodo, getNumeroFacce all'interfaccia Solidi che restituisce il numero di facce del solido. Tutti i parallelepipedi (anche il cubo) avranno 6 facce, quindi l'implementazione del metodo per questi solidi sara' sempe la stessa, ma non vogliamo duplicare (e poi manutenere) lo stesso codice per tutte le implementazioni. In questo caso e' possibile usare una classe abstract, cioè una normalissima classe java che implementa però solo parte dei metodi previsti dall'interface. Aggiorniamo dunque l'interfaccia Solido:

package lai.tutorial;

public interface Solido {
	public double getVolume();
	public int getNumeroFacce();
}

Creiamo la nuova classe astratta Parallelepipedo:

package lai.tutorial;

public abstract class Parallelepipedo implements Solido {
	public int getNumeroFacce() {
		return 6;
	}
}

[Nota] Nota

Una classe astratta non può essere istanziata, ma solo usata come classe di base per realizzare sottoclassi che la estendono, come nell'esempio seguente di riscrittura della classe Cubo:

package lai.tutorial;

public class Cubo extends Parallelepipedo {
	private int lato;
	
	public Cubo(int l) throws Exception{
		if(l<0) throw new Exception("Non e' ammesso un cubo con lato < 0.");
		lato = l;
	}
	
	public double getVolume(){
		return lato * lato * lato;
	}
}

Mentre il Cubo eredita il nuovo metodo getNumeroFacce dalla classe astratta Parallelepipedo, la riscrittura della classe Cono dovrà reimplementare una nuova versione di questo metodo:

package lai.tutorial;

public class Cono implements Solido {
	
	[...]

	public int getNumeroFacce() {
		return 2;
	}
	
}

Questo tipo di organizzazione del codice ci permette ora di scrivere codice riusabile per tutti i solidi nella nostra applicazione, come nell'esempio che segue.


	Random r = new Random();			
	boolean tipoSolido = r.nextBoolean();

	Solido s = null;
	
	if (tipoSolido)
		s = new Cono(2, 3);
	else
		s = new Cubo(3);
	
	double d = s.getVolume();
	System.out.print(d);

10. Aggiunta di nuove librerie

Tutte le classi usate negli esempi sono incluse nelle librerie del JDK. Se pero' usiamo classi che non lo sono, dobbiamo scaricarne i binari in formato .jar e aggiungerli nel Build Path del progetto per renderli disponibili alla JVM. Selezioniamo quindi Project > Properties > Java Build Path e aggiungiamole.

Footer BGFooter BG
Tito Flagella - © 2007-2015