Multithreading e concorrenza in Java

Da Head First Java O'Reilly, capitolo 15. Networking e threads

Abbiamo visto quanto sia facile creare applicazioni multithreading in Java, ma questo non ci libera dai problemi intrinseci a questo modello di sviluppo.

Un problema tipico si ha quando due diversi attori competono su di un unica risorsa. Nell'esempio che vediamo ci sono due utenti che cercano di accedere allo stesso conto corrente per fare un prelievo. Una progettazione poco attenta dello scenario può portare al codice sottostante che, come vedremo, causa grossi problemi.

La classe RMAccount definisce il conto corrente che viene acceduto da Ryan e Monica. Parte con un valore di 100 e su di esso possono essere operati dei prelievi. Il nostro scopo sarebbe quello di evitare che il conto vada in rosso.

package Chap15;

public class RMAccount {
private int balance = 100;

public int getBalance() {
return balance;
}

public void withdraw(int amount) {
balance -= amount;
}
}


La classe RMJob definisce la classe runnable. Il main crea una istanza di RMJob e due thread (Ryan e Monica) che operano sullo stesso oggetto. L'oggetto RMJob ha un solo account, definito come variabile di istanza, il metodo run() itera per dieci volte un prelievo chiamando il metodo withdrawl() che dovrebbe operare il prelievo solo se il bilancio del conto lo permette.

Si controlla dunque che il bilancio sia maggiore della somma che si vuole prelevare e, solo se é il caso, si effettua il prelievo. Per rendere evidente la debolezza di questo approccio mettiamo in pausa il thread per mezzo secondo tra il test sul bilancio e l'effettivo prelievo.

package Chap15;

public class RMJob implements Runnable {

private RMAccount account = new RMAccount();

private boolean withdrawl(int amount) {
if(account.getBalance() >= amount) {
System.out.println(Thread.currentThread().getName() + ": withdrawing");
try {
System.out.println(Thread.currentThread().getName() + ": sleeping a bit");
Thread.sleep(500);
}
catch (InterruptedException ie) {
ie.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": up again");
account.withdraw(amount);
System.out.println(Thread.currentThread().getName() + ": withdrawl done");
return true;
}

// can't withdraw
System.out.println(Thread.currentThread().getName() + ": out of money");
return false;
}

public void run() {
for (int i = 1; i < 10; ++i) {
if(withdrawl(10)) {
if(account.getBalance() < 0) {
System.out.println("Negative balance! ");
}
}
}
}

public static void main(String[] args) {
RMJob job = new RMJob();

new Thread(null, job, "Ryan").start();
new Thread(null, job, "Monica").start();
}
}

L'aver aggiunto lo sleep() rende il problema evidente. Un thread esegue il controllo, che riesce, ma prima che possa eseguire il prelievo, l'altro thread esegue nuovamente il controllo con successo. A questo punto solo uno dei due potrà prelevare correttamente i soldi dal conto, l'altro lo manderà inopinatamente in rosso.

Il nostro codice va ridisegnato: la risorsa condivisa deve essere protetta.

Multithreading in Java

Da Head First Java O'Reilly, capitolo 15. Networking e threads

Creare più thread di esecuzione in Java é davvero molto semplice. In pratica si tratta di creare una classe che implementi l'interfaccia Runnable, mettendo nel suo metodo run() la logica che deve essere eseguita dal nostro thread.
Si crea poi un nuovo oggetto di tipo Thread passando al costruttore un oggetto della classe che abbiamo appena definito, e chiamiamo su di esso il metodo start().

Nell'esempio a seguire creiamo due thread, in ognuno dei quali corre una istanza della classe MyRunnable. Nota che i due thread sono creati specificando un nome (alpha e beta, rispettivamente) per l'oggetto.


package Chap15;

public class MyRunnable implements Runnable {

public void run() {
try {
Thread.sleep(1000);
}
catch (InterruptedException ie) {
ie.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " is running");
}

public static void main(String[] args) {
Runnable r1 = new MyRunnable();
Runnable r2 = new MyRunnable();

new Thread(null, r1, "alpha").start();
new Thread(null, r2, "beta").start();

System.out.println("main");
}
}

Socket - un primo esempio

Da Head First Java O'Reilly, capitolo 15. Networking e threads

Il primo esempio sulle connessioni di rete é basato sulla costruzione di una semplice applicazione in cui il server attende un client, gli manda un messaggio - il consiglio del giorno - e poi termina l'esecuzione. Il client, dal canto suo, cerca di connettersi al server, legge il messaggio che questo gli manda, lo stampa e termina l'esecuzione.

Iniziamo a vedere il server.

Il cuore della classe é una istanza della classe ServerSocket, che viene creata per la porta 4242.

Sul socket server così creato chiamiamo il metodo accept(), che "appende" il server in attesa di una richiesta del client. Come il client si connette, il metodo accept() ritorna un socket, da cui estraiamo l'output stream che utilizziamo per creare un oggetto PrintWriter, su cui scriviamo il nostro messaggio.

Completato il nostro flusso d'esecuzione, chiudiamo il PrintWriter, operazione che implica la chiusura di tutti gli stream aperti.

Ecco il codice:

package Chap15;

import java.io.*;
import java.net.*;

public class DailyAdviceServer {

private String[] advices = {"Have fun", "Eat less", "No alcohol today"};

public DailyAdviceServer() {
try {
ServerSocket ss = new ServerSocket(4242);

System.out.println("Waiting for a client ...");
Socket socket = ss.accept();
PrintWriter pw = new PrintWriter(socket.getOutputStream());

String advice = advices[(int) (Math.random() * advices.length)];
pw.println(advice);
pw.close();
System.out.println("The advice sent is: " + advice);
}
catch (IOException ioe) {
ioe.printStackTrace();
}
}

public static void main(String[] args) {
new DailyAdviceServer();
}
}


Il client funziona in modo speculare. Si crea un socket passandogli l'indirizzo IP della macchina su cui corre il server - nel nostro caso localhost - si crea un lettore sullo stream, e lo si bufferizza. Leggiamo una riga dal buffer, che sarà il testo che ci manda il server, chiudiamo il lettore e terminiamo l'esecuzione.

Questo il codice risultante:

package Chap15;

import java.io.*;
import java.net.Socket;

public class DailyAdviceClient {
public DailyAdviceClient() {
try {
Socket s = new Socket("localhost", 4242);
InputStreamReader isr = new InputStreamReader(s.getInputStream());
BufferedReader reader = new BufferedReader(isr);

System.out.println("Your today's advice: " + reader.readLine());
reader.close();
}
catch(IOException ioe) {
ioe.printStackTrace();
}
}

public static void main(String[] args) {
new DailyAdviceClient();
}
}

Leggere file di testo

Da Head First Java O'Reilly, capitolo 14.

Come é lecito attendersi, la lettura di un file di testo avviene in modo speculare rispetto alla sua scrittura.

Se per scrivere usiamo la classe FileWriter, per leggere usiamo la simmetrica FileReader.

Nell'esempio che segue vediamo anche l'uso delle classi che permettono di bufferizzare le operazioni di lettura/scrittura, mirando all'ottimizzazione dell'accesso alla memoria di massa.

Già che ci sono riscrivo anche la parte, vista nel post citato, in cui si scrive per mezzo di FileWriter, anche qui usando la bufferizzazione.

package Chap14;

import java.io.*;

public class StringManager {
private File f;

public StringManager(String fileName) {
f = new File(fileName);
}

public void write() {
try {
BufferedWriter bw = new BufferedWriter(new FileWriter(f));

for(int i = 0; i < 3; ++i)
bw.write("Hello!\n");
bw.close();
} catch(Exception ex) {
ex.printStackTrace();
}
}

public void read() {
try {
BufferedReader br = new BufferedReader(new FileReader(f));

String line;
while((line = br.readLine()) != null) {
System.out.println("read: " + line);
}
br.close();
} catch(Exception ex) {
ex.printStackTrace();
}
}

public static void main(String[] args) {
StringManager sm = new StringManager("hi.txt");

sm.write();
sm.read();
}
}

La classe java.io.File

Da Head First Java O'Reilly, capitolo 14.

La classe File ha lo scopo di permettere una facile gestione delle metainformazioni di un file e di mettere a disposizione alcune funzionalità di base, come la creazione di file e directory. Per accedere alle informazioni contenute nel file stesso occorre usare uno stream.

Alcune operazioni messe a disposizione da File:
  • mkdir(): creazione di una directory;
  • list(): ritorna il contenuto di una directory come array di stringhe;
  • getAbsolutePath(): il path assoluto di un file;
  • delete(): rimuove il file, ritorna true in caso di successo.

Scrivere testo semplice su file

Da Head First Java O'Reilly, capitolo 14.

Rendere persistente un oggetto in Java, usando il meccanismo della serializzazione é relativamente semplice. Scrivere stringhe di testo é ancora più semplice.

Tutto quello che occorre é un oggetto FileWriter e invocare il metodo write() su di esso.

A seguire, un piccolo esempio:

package Chap14;

import java.io.FileWriter;

public class StringWriter {
public static void main(String[] args) {
try {
FileWriter fw = new FileWriter("hi.txt");
fw.write("Hello!");
fw.close();
} catch(Exception ex) {
ex.printStackTrace();
}

}
}

STL - container associativi ordinati

Dal quarto capitolo di Designing Components with the C++ STL, di Ulrich Breymann. Parte dedicata ai container associativi ordinati.

Un container associativo permette un veloce accesso ai dati per mezzo di una chiave che può anche non coincidere con i dati. Nei set e multiset i dati stessi sono usati come chiave, mentre in map e multimap chiavi e dati sono distinti.

STL mette a disposizione quattro tipi di container associativi:
  • set: le chiavi coincidono con i dati, non ci sono elementi che abbiano la stessa chiave;
  • multiset: é un set in cui possono esistere elementi con la stessa chiave;
  • map: chiavi e dati sono distinti, le chiavi sono uniche;
  • multimap: é una map in cui possono esistere elementi con la stessa chiave.
In più, questi container STL memorizzano le chiavi in modo ordinato.

STL - priority_queue

Dal quarto capitolo di Designing Components with the C++ STL, di Ulrich Breymann.

Una coda con priorità ritorna l'elemento che ha la priorità più alta. Il criterio con cui si determina la priorità va specificato all'atto della creazione della coda.

Nell'esempio che segue, creo prima una semplice coda con priorità che contiene interi e usa come container sottostante il default (vector, in alternativa é possibile usare anche deque) e come ordinamento ancora il default (less: il che vuol dire che il più grande intero é seleziona come primo). A seguire creo un'altra coda a priorità di interi che però usa come criterio di ordinamento greater, dunque il più piccolo intero della coda é considerato in testa:

#include <iostream>
#include <queue>
#include <vector>
#include <functional>

using namespace std;

int main() {
priority_queue<int> pq;

pq.push(20);
pq.push(10);
pq.push(30);

while(pq.empty() == false) {
cout << pq.top() << ' ';
pq.pop();
}
cout << endl;

priority_queue<int, vector<int>, greater<int> > pq2;

pq2.push(20);
pq2.push(10);
pq2.push(30);

while(pq2.empty() == false) {
cout << pq2.top() << ' ';
pq2.pop();
}
cout << endl;
}

Deserializzare un oggetto

Da Head First Java O'Reilly, capitolo 14.

Dato un oggetto reso persistente é semplice riportarlo in attività. In pratica si tratta di fare il percorso inverso compiuto nel post precedente.

Useremo un oggetto di tipo FileInputStream per leggere i byte dal file, trasformeremo i byte in oggetto per mezzo di una istanza della classe ObjectInputStream e quindi lo "leggiamo" in un oggetto. Nota che, nel processo, l'oggetto é riportato al suo stato salvato, non viene quindi invocato il suo costruttore, dato che ciò lo porterebbe al suo stato iniziale. D'altra parte, se nella gerarchia della classe dell'oggetto serializzato c'é uno o più ascendenti non serializzabili, il costruttore é chiamato per il più esterno di essi e da quel punto in poi l'oggetto e inizializzato usando gli specifici costruttori. Le eventuali componenti transienti di un oggetto serializzato vengono inizializzate con il valore di default corrispondente (gli oggetti vengono messi a null).

A seguire un semplice deserializzatore per l'oggetto Box che abbiamo serializzato nel post precedente:

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class BoxGetter {
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("box.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
Box myBox = (Box) ois.readObject();
ois.close();

System.out.println("My box is: " + myBox.toString());

} catch(Exception ex) {
ex.printStackTrace();
}
}
}

Come serializzare gli oggetti

Da Head First Java O'Reilly, capitolo 14.

In Java un oggetto può essere reso persistente per mezzo del meccanismo della serializzazione che, in pratica, consiste nel salvare lo stato dell'oggetto in qualche modo su una qualche periferica.

Per default una classe non é serializzabile. Bisogna implementare l'interfaccia Serializable. Non esiste alcun metodo in questa interfaccia, é utilizzata semplicemente come etichetta per indicare che siamo consapevoli che un oggetto di questa classe possa essere serializzato.

Il senso della cosa é che se per qualche motivo non vogliamo che oggetti di quella classe vengano resi persistenti abbiamo un mezzo per farlo (non implementando l'interfaccia).

Non é necessario implementare esplicitamente Serializable, é sufficiente che la nostra classe derivi da una classe che implementa questa interfaccia.

Cercare di serializzare un oggetto non serializzabile, anche solo in una sua componente, causa una eccezione di tipo java.io.NotSerializableException, ma é possibile esclude esplicitamente le componenti che non si vogliono serializzare.

Per compiere il processo ci si appoggia su due stream. Il primo si occupa della serializzazione vera e propria (traduce l'oggetto in una sequenza di byte), il secondo si occupa dei dettagli di basso livello per quanto riguarda la persistenza dello stream che gli viene passato.

Quello che facciamo nell'esempio che segue é creare un oggetto FileOutputStream, che si occupa di gestire il file, e un oggetto ObjectOutputStream che si occupa della serializzazione e usa il FileOutputStream per scrivere effettivamente su file.

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Box implements Serializable {
private int x;
private int y;
private transient InnerStuff stuff;

private class InnerStuff {
public InnerStuff(int internalCode) {
this.internalCode = internalCode;
}
private int internalCode;
}

public Box(int x, int y) {
this.x = x;
this.y = y;
this.stuff = new InnerStuff((int) (Math.random() * 1000));
}

public static void main(String[] args) {
Box box = new Box(12, 42);
try {
FileOutputStream fos = new FileOutputStream("box.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);

oos.writeObject(box);
oos.close();
}
catch (Exception ex) {
ex.printStackTrace();
}
}
}

La classe serializzabile Box ha due variabili di istanza di tipo primitivo (e quindi serializzabili) e un oggetto di tipo InnerStuff (classe che non implementando Serializable non é serializzabile). Se non specificassimo che l'oggetto InnerStuff sia transiente la serializzazione di Box fallirebbe con una eccezione.

Check box e lista swing

Da Head First Java O'Reilly, capitolo 13.

Un paio di utili widget sono il check box e la lista. Vediamo un piccolo esempio che li usa entrambi.

Lo scopo é quello di costruire una lista che permetta una selezione singola o multipla a seconda se il check box é spuntato o meno.

Vediamo subito il codice risultante:

import java.awt.BorderLayout;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

public class MyList implements ItemListener, ListSelectionListener {

private JCheckBox cb;
private JList list;

public MyList() {
JFrame f = new JFrame();

addCheckBox(f);
addList(f);

f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setSize(150, 220);
f.setVisible(true);
}

private void addCheckBox(JFrame f) {
cb = new JCheckBox("Multiple selection");
cb.setSelected(true);
cb.addItemListener(this);
f.add(BorderLayout.NORTH, cb);
}

private void addList(JFrame f) {
String[] items = {"alpha", "beta", "gamma", "delta", "epsilon"};
list = new JList(items);
list.addListSelectionListener(this);

JScrollPane sp = new JScrollPane(list);
sp.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
sp.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

f.add(BorderLayout.SOUTH, sp);
}

public void itemStateChanged(ItemEvent e) {
list.clearSelection();
if(cb.isSelected())
list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
else
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

System.out.println("Multiple selection is " +
(cb.isSelected() ? "on" : "off"));
}

public void valueChanged(ListSelectionEvent e) {
if(e.getValueIsAdjusting() == false) {
if(list.getSelectionMode() == ListSelectionModel.SINGLE_SELECTION) {
String item = (String) list.getSelectedValue();
if(item != null)
System.out.println("Selected item: " + item);
}
else {
Object[] items = list.getSelectedValues();
if(items.length > 0) {
System.out.print("Selected items:");
for(Object item : items) {
System.out.print(" " + item);
}
System.out.println();
}
}
}
}

public static void main(String[] args) {
new MyList();
}
}

Notiamo subito che la classe implementa i listener per eventi generati dalla check box (ItemListener) e dalla lista (ListSelectionListener). E dunque implementa i metodi itemStateChanged(), invocato quando lo stato della checkbox cambia, e valueChanged(), per un cambiamento nelle voci selezionate nella lista.

Ho scritto un paio di metodi privati alla classe che si occupano di istanziare le due widget. Con addCheckBox() creiamo la checkbox; la inizializziamo spuntata, con setSelected(true); registriamo la nostra classe per essere un listener della checkbox; e infine la aggiungiamo alla nostra frame.

Con addList() creiamo una lista di stringhe, che utilizziamo per creare la nostra JList su cui registriamo come listener la nostra classe e la aggiungiamo alla nostra frame.

La itemStateChanged() per prima cosa cancella tutte le eventuali selezioni (l'idea é che cambiando il modo di selezionare il lavoro precedente perde significato), poi modifica la modalità in cui lavora la lista a seconda del valore della checkbox.

La valueChanged() controlla lo stato dell'evento in ingresso per mezzo del suo metodo getValueIsAdjusting(), dato che se siamo nel mezzo di un cambiamento della selezione non dobbiamo fare alcun controllo aggiuntivo. Aspettiamo piuttosto che il cambiamento sia completato.

Controlliamo poi la modalità di selezione della lista. Se é in modalità di selezione singola, leggiamo la selezione con getSelectedValue(), se ritorna null vuol dire che non c'é alcuna selezione attiva, altrimenti scriviamo a consolle il valore della selezione.

In caso di modalità di selezione multipla reperiamo l'elenco di selezioni con getSelectedValues(), che torna un array di oggetti. Se c'é almeno una selezione (la length dell'array non é zero) le scriviamo a consolle.

Testo swing

Da Head First Java O'Reilly, capitolo 13.

JTextField permette all'utente di inserire una riga di testo. Nel caso si vogliano gestire più righe occorre passare a JTextArea, che richiede un po' più di configurazione.

Vediamo allora un piccolo esempio di uso di una text area.

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;

public class MyTextArea implements ActionListener {
private JTextArea text;

public MyTextArea() {
JFrame f = new JFrame();

text = new JTextArea(10, 20);
JScrollPane sp = new JScrollPane(text);
sp.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
sp.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);

JPanel p = new JPanel();
p.add(sp);
f.getContentPane().add(BorderLayout.CENTER, p);

JButton b = new JButton("Click me");
b.addActionListener(this);
f.getContentPane().add(BorderLayout.SOUTH, b);

f.setSize(300, 300);
f.setVisible(true);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}

public void actionPerformed(ActionEvent e) {
text.append("Button clicked.\n");
}

public static void main(String[] args) {
new MyTextArea();
}
}

Mettiamo a sud un bottone che ogni volta che viene cliccato scrive del testo sulla textarea. Per far questo registriamo la nostra classe, che implementa l'interfaccia ActionListener, sul bottone, ed eseguiamo l'azione richiesta nel metodo actionPerformed().

La costruzione del widget per la text area, come vediamo, é un po' più laboriosa del solito, in quanto dopo aver creato la text area vera e propria, la piazziamo in un un pannello specializzato (JScrollPane) che permette la gestione delle barre di scorrimento associate alla text area.

Layout per swing

Da Head First Java O'Reilly, capitolo 13.

Una utile divisione empirica tra le componenti swing in Java é quella tra componenti interattive (JButton, JCheckBox, JTextField, ...) e componenti di background (JFrame e JPanel).

Tipicamente le seconde stanno sullo sfondo e le prime sono messe sopra a queste.

Il layout manager é il processo che decide come le componenti vengono piazzate dentro un background.

I tre layout manager più comuni:
  • BorderLayout: divide il background in cinque regioni, é possibile aggiungere una sola componente in ognuna di queste regioni; é il default per una frame.
  • FlowLayout: agisce come un word processor con il "word wrap" attivo. Ovvero le componenti vengono messe una a fianco all'altra finché ci stanno su una "riga". E poi si va "a capo"; é il default per un pannello.
  • BoxLayout: simile al FlowLayout, ma normalmente opera verticalmente ed é possibile forzare l'inizio di una "nuova riga".
Questa piccola applicazione mostra come funziona il BorderLayout. I bottoni sui bordi prendono lo spazio necessario per mostrare la propria etichetta, e tendono a coprire tutto il bordo di loro competenza. Il bottone al centro occupa tutto lo spazio che resta disponibile:

import java.awt.BorderLayout;
import javax.swing.*;

public class Border {
public Border() {
JFrame f = new JFrame();

f.getContentPane().add(BorderLayout.CENTER, new JButton("Center"));
f.getContentPane().add(BorderLayout.EAST, new JButton("East"));
f.getContentPane().add(BorderLayout.NORTH, new JButton("North"));
f.getContentPane().add(BorderLayout.WEST, new JButton("West"));
f.getContentPane().add(BorderLayout.SOUTH, new JButton("South"));

f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setSize(300, 300);
f.setVisible(true);
}

public static void main(String[] args) {
new Border();
}
}

Una semplice animazione

Da Head First Java O'Reilly, capitolo 12.

Dovendo fare una animazione, e potendo scegliere il linguaggio di programmazione, io punterei su JavaFx, che semplifica molto la vita dello sviluppatore in questo contesto.

Questa piccola applicazione, disegnamo una palla che si muove di moto rettilineo uniforme, ha piuttosto l'intenzione di mostrare un altro utilizzo della classe interna (inner class) in Java.

L'idea é che abbiamo due oggetti di due classi diverse, uno fa da controller, l'altro cura la visualizzazione della palla. Il secondo oggetto deve accedere variabili private del primo, in modo da sapere dove disegnare la palla, deve quindi guadagnare in qualche modo un accesso privilegiato.

Il meccanismo della classe interna entra perfettamente in gioco in questa situazione. Vediamo il sorgente:

import java.awt.*;
import javax.swing.*;

public class SimpleAnimation {
private JFrame frame;
private int x = 70;
private int y = 70;

public SimpleAnimation() {
frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(new MyPanel());
frame.setSize(300, 300);
frame.setVisible(true);

go();
}

private void go() {
for(int i = 0; i < 130; ++i) {
++x;
++y;

frame.repaint();
try { Thread.sleep(50); } catch(InterruptedException ie) {}
}
}

private class MyPanel extends JPanel {
@Override
public void paintComponent(Graphics g) {
g.setColor(Color.GREEN);
g.fillOval(x, y, 40, 40);
}
}

public static void main(String[] args) {
new SimpleAnimation();
}
}

La inner class MyPanel usa x e y, variabili membro privati di SimpleAnimation, come base per disegnare la palla. Il metodo go() di SimpleAnimation modifica x e y, genera un repaint della finestra - e quindi chiama anche paintComponent() di MyPanel -, e poi dormicchia per mezzo decimo di secondo, per dare una sensazione di tranquillo spostamento della palla.

Due bottoni per una GUI

Da Head First Java O'Reilly, capitolo 12.

Riscriviamo la semplice Gui su cui stiamo lavorando da un po' per permetterle di gestire i comandi che arrivano da due bottoni diversi.

Questo richiede che la actionPerformed() discrimini la sorgente per decidere cosa fare o, meglio, che vi siamo due actionPerformed(), ognuna dedicata ad un compito.

Dato che non é possibile avere due metodi con lo stesso nome all'interno della stessa classe, e dato che sarebbe sgradevole delegare esternamente alla classe in cui sono definiti i bottoni la gestione dei comandi risultanti, conviene definire due classi interne (inner class) che agiscano come listener dei bottoni.

Data la MyDrawPanel definita nel precedente post, riscriviamo la classe che controlla la gui in questo modo:

import java.awt.BorderLayout;
import java.awt.event.*;
import javax.swing.*;

public class TwoButtons {
private JFrame frame;
private JLabel label;

public TwoButtons() {
frame = new JFrame();
label = new JLabel("Hello");

JButton btnLabel = new JButton("Change label");
btnLabel.addActionListener(new LabelListener());

JButton btnColor = new JButton("Change color");
btnColor.addActionListener(new ColorListener());

MyDrawPanel panel = new MyDrawPanel();

frame.getContentPane().add(BorderLayout.WEST, label);
frame.getContentPane().add(BorderLayout.CENTER, panel);
frame.getContentPane().add(BorderLayout.EAST, btnLabel);
frame.getContentPane().add(BorderLayout.SOUTH, btnColor);

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(300, 300);
frame.setVisible(true);
}

class LabelListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
label.setText("Hi!");
}
}

class ColorListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
frame.repaint();
}

}

public static void main(String[] args) {
new TwoButtons();
}
}

LabelListener e ColorListener, essendo definite come inner class, hanno accesso alle variabili di instanza della classe esterna che possono accedere direttamente.

Si può dire che le classi interne permettono di implementare la stessa interfaccia più di una volta nella stessa classe.

Un widget personalizzato

Da Head First Java O'Reilly, capitolo 12.

Creiamo ora un'altra piccola applicazione GUI che usa un widget custom.

Estendiamo la classe JPanel per creare un pannello che contiene un ovale colorato con colori casuali, in modo da creare un effetto di passaggio tra due tinte.

import java.awt.*;
import javax.swing.JPanel;

public class MyDrawPanel extends JPanel {
@Override
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;

int red = (int) (Math.random() * 255);
int green = (int) (Math.random() * 255);
int blue = (int) (Math.random() * 255);
Color begin = new Color(red, green, blue);

red = (int) (Math.random() * 255);
green = (int) (Math.random() * 255);
blue = (int) (Math.random() * 255);
Color end = new Color(red, green, blue);

g2d.setPaint(new GradientPaint(70, 70, begin, 150, 150, end));
g2d.fillOval(70, 70, 100, 120);
}
}

L'unico metodo che ridefiniamo (nota la direttiva override) é paintConponent(), che viene invocato dalla repaint() quando occorre ridisegnare a video la componente.

Per prima cosa la reference a Graphics viene castata a Graphics2D, questo é sempre possibile e ci permette di avere accesso a metodi più avanzati.

Creiamo due colori distinti in modo causale, poi specifichiamo il nuovo pennello che vogliamo usare, con setPaint(), specificando un gradiente costruito con i colori generati, e quindi riempiamo un ovale dipinto con quel pennello.

Questa é la classe che usa il nostro pannello:

import java.awt.BorderLayout;
import java.awt.event.*;
import javax.swing.*;

public class SimpleGuiC implements ActionListener {
private JFrame frame;

public SimpleGuiC() {
frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

JButton button = new JButton("Change colors");
button.addActionListener(this);
frame.getContentPane().add(BorderLayout.SOUTH, button);

MyDrawPanel panel = new MyDrawPanel();
frame.getContentPane().add(BorderLayout.CENTER, panel);

frame.setSize(300,300);
frame.setVisible(true);
}

public void actionPerformed(ActionEvent e) {
frame.repaint();
}

public static void main(String[] args) {
new SimpleGuiC();
}
}

Da notare, oltre all'uso del nostro widget custom, anche il fatto che abbiamo messo due componenti nel content del nostro frame, sfruttando le stringhe definite in BorderLayout che ci permettono di piazzare fino a 5 componenti che rispecchiano i punti cardinali, a cui si somma il CENTER.

In pratica piazziamo il bottone a sud (ovvero in basso) e il pannello in centro (che finisce per trasbordare in tutto il resto dello spazio a disposizione).

Cliccando sul bottone viene chiamata la actionPerformed(), che chiede un repaint dell'intero frame, e quindi anche della nostra componente custom, con conseguente nuova scelta di colori per il nostro oggetto ovoidale.

Una semplice GUI

Da Head First Java O'Reilly, capitolo 12.

Lasciamo la linea di comando per passare ad usare swing per costruirci una GUI.

Una prima semplice applicazione grafica:

import javax.swing.*;

public class SimpleGui {
public static void main(String[] args) {
JFrame f = new JFrame();
JButton b = new JButton("Hello!");

f.getContentPane().add(b);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setSize(300, 300);
f.setVisible(true);
}
}

In pratica:
  • Creo un oggetto JFrame, ovvero una finestra sullo schermo.
  • Creo un oggetto JButton, un widget (oggetto grafico) che può essere aggiunto a una finestra.
  • Aggiuno il bottone al pannello dei contenuti della finestra (ottenuto con una chiamata a getContentPane()).
  • Specifico l'operazione da compiere alla chiusura della finestra.
  • Specifico la dimensione della finestra.
  • Rendo visibile la finestra.
Il risultato sarà quello di creare una finestra che contiene il bottone, che riempe tutto lo spazio a disposizione. Nel chiudere la finestra il programma viene terminato.

Per fare in modo che un click sul bottone possa essere interpretato dobbiamo fare in modo che qualcuno sia in ascolto sul bottone, e che possa quindi reagire agli eventi che il bottone genera.

Il bottone, in questo contesto viene visto come sorgente di eventi che possono essere intercettati e gestiti da un listener.

Riscriviamo l'esempio precedente facendo in modo che pigiare il bottone causi il cambiamento del testo associato.

import java.awt.event.*;
import javax.swing.*;

public class SimpleGuiB implements ActionListener {
JButton button;

public SimpleGuiB() {
JFrame f = new JFrame();
button = new JButton("Hello!");
button.addActionListener(this);

f.getContentPane().add(button);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setSize(300, 300);
f.setVisible(true);
}

public void actionPerformed(ActionEvent e) {
button.setText("I've been clicked.");
}

public static void main(String[] args) {
SimpleGuiB gui = new SimpleGuiB();
}
}

La classe implementa ora l'interfaccia ActionListener, e quindi definisce il metodo actionPerformed(). Il bottone, a sua volta, registra la classe come listener per mezzo del metodo addActionListener().

Ogni evento generato dal bottone viene ora gestito dalla actionPerformed().

La sorgente di eventi, e l'oggetto evento, sono in genere gestiti da una classe preesistente (nel nostro caso JButton). Il lavoro normale dello sviluppatore consiste nello scrivere il listener. Si definisce una classe che implementa ActionListener, si scrive l'implementazione del metodo actionPerformed() per definire il comportamento in risposta all'azione osservata, e si registra il listener sulla sorgente di eventi.

Musica ed eccezioni

Da Head First Java O'Reilly, capitolo 11: eccezioni

Se utilizziamo un metodo che può generare un'eccezione, dobbiamo scrivere il codice che ne permetta una appropriata gestione. Ovvero dobbiamo usare il costrutto try/catch (o lasciare che l'eccezione si propaghi al chiamante).

Vediamo un esempio con la chiamata al metodo statico getSequencer() di MidiSystem:

package Chap11;

import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Sequencer;

public class MusicTest1 {
public void play() {
try {
Sequencer s = MidiSystem.getSequencer();
System.out.println("Done!");
}
catch (MidiUnavailableException ex) {
System.out.println("Something went wrong: " + ex.getMessage());
}
}

public static void main(String[] args) {
MusicTest1 mt = new MusicTest1();
mt.play();
}
}

Per generare una eccezione si usa throw e si marca il metodo dichiarando quale tipo di eccezione può lanciare. Tipo:

public void risky() throws MyException {
// ...
if (somethingBad) {
throw new MyException();
}
}

Il compilatore controlla tutte le eccezioni, tranne quelle che sono di tipo RuntimeException o derivati, che si considera possano avvenire ovunque nel codice.
Per tutte le altre deve valere lo schema indicato sopra (direttiva throws a marcare il metodo, codice cliente che tratta l'eccezione o delega il chiamante a farlo).

lo schema try/catch può essere completato dalla sezione finally: il blocco finally verrà sempre eseguito sia se c'é una eccezione nel blocco try (e quindi il blocco catch relativo all'eccezione generata é eseguito) sia se non si incontrano eccezioni.

static import

Da Head First Java O'Reilly, capitolo 10.

L'idea della direttiva static import é quella di risparmiare un po' di lavoro di tastiera. D'altro canto il codice che usa static import può risultare poco chiaro. Conviene quindi fare un uso molto limitato delle sue potenzialità.

Questo un'esempio d'uso:

//...
import static java.lang.System.out;
import static java.lang.Math.*;
//...

public void importStatic() {
out.println("sqrt 2: " + sqrt(2));
out.println("tan 60: " + tan(60));
}
//...

Nota che é possibile fare un'import static su un intera classe (Math, nel nostro caso) mettendo a disposizione tutti i suoi membri statici in un sol colpo. L'impressione che può avere il lettore causale del codice é che esista un oggetto statico (out) e metodi statici globali, il che é evidentemente impossibile in Java. Oppure che out e i metodi di Math facciano parte della classe corrente. Insomma, la cosa può creare qualche malinteso.

Date e calendari

Da Head First Java O'Reilly, capitolo 10. Parte dedicata alla formattazione delle date.

Per formattare le date si usa la funzione static format() di String in maniera molto simile a quanto visto per i numeri.

La differenza fondamentale é che nella formattazione delle date si usano due caratteri per specificare il tipo del parametro e non uno:
String.format("%tc", new Date());
  • %tc: la data completa (dom dic 13 21:11:42 CET 2009)
  • %tr: il solo tempo nel giorno corrente (09:16:34 PM)
  • %tA: il giorno della settimana (domenica)
  • %tB: il mese dell'anno (dicembre)
  • %tr: il giorno nel mese (13)

Un utile flag da usarsi come numero di argomento é "<", che permette di riferirsi facilmente allo stesso parametro. Esempio:

System.out.println(String.format("%tA %<td %<tB", new Date()));

stampa "domenica 13 dicembre".

Calendar

Se java.util.Date é la classe di elezione per ottenere il timestamp corrente, per manipolare le date é meglio usare la classe java.util.Calendar che, fra l'altro, é stata disegnata per supportare diversi calendari e non solo il nostro, il cosiddetto calendario gregoriano. In ogni caso, se non specifichiamo quale calendario vogliamo utilizzare otteniamo il default per localizzazione corrente. Il meccanismo per far ciò é implementato nel metodo getInstance() della classe Calendar che deciderà quale delle sue classi figlie instanziare per noi.

Ecco un esempio d'uso del Calendar:

public void calendar() {
Calendar c = Calendar.getInstance();
System.out.println(c.getTime());

c.set(2012, 11, 20, 20, 12); // Thu Dec 20 20:12:22 CET 2012
System.out.println(c.getTime());

long day = c.getTimeInMillis() + 1000 * 60 * 60; // + 1h
c.setTimeInMillis(day); // Thu Dec 20 21:12:29 CET 2012
System.out.println(c.getTime());
System.out.println("Hour: " + c.get(Calendar.HOUR_OF_DAY)); // Hour: 21
c.add(Calendar.DATE, 35); // Thu Jan 24 21:12:29 CET 2013
System.out.println("35 days later: " + c.getTime());
c.roll(Calendar.DATE, 35); // Mon Jan 28 21:12:29 CET 2013
System.out.println("35 days rolling: " + c.getTime());
c.set(Calendar.DATE, 1); // Tue Jan 01 21:12:29 CET 2013
System.out.println("set day to 1: " + c.getTime());
}

Alcuni tra i campi del calendario più usati sono:
  • DATE, DATE_OF_MONTH: il giorno del mese;
  • HOUR, HOUR_OF_DAY: l'ora del giorno;
  • MILLISECOND;
  • MINUTE;
  • MONTH: il mese;
  • YEAR: l'anno;
Alcuni tra i metodi più utilizzati:
  • add(int field, int amount): aggiunge o sottrae tempo dal campo specificato;
  • get(int field): ritorna il valore del campo specificato;
  • getInstance(): un calendario per la localizzazione corrente;
  • getTimeInMillis(): la data convertita in millisecondi (long);
  • roll(int field, boolean up): aggiunge o toglie una unità dal campo specificato, senza modificare gli altri campi;
  • roll(int field, int amount): varia il campo specificato, senza impattare sugli altri;
  • set(int field, int value): specifica direttamente il valore per un determinato campo;
  • set(int year, int month, int day, int hour, int minute): specifica il tempo;
  • setTimeInMillis(long millis): specifica la data del calendario a partire dal tempo in millisecondi.

Formattare numeri

Da Head First Java O'Reilly, capitolo 10. Parte dedicata alla formattazione dei numeri.

Ci sono svariate possibilità per formattare i numeri in Java.

Ad esempio, se vogliamo inserire punti e virgole tra le cifre, per rendere più leggibile grosse cifre, possiamo usare il metodo statico format() della classe String usando l'opzione ",":

public void commaDecimal() {
String s = String.format("%,11d", 10000000);
System.out.println(s);
}

public void commaFloating() {
String s = String.format("%,14.2f", 10000000.23);
System.out.println(s);
}

public void commaException() {
try {
String s = String.format("%,d", 10000000.23);
System.out.println(s);
}
catch (java.util.IllegalFormatConversionException ifce) {
System.out.println("Can't do that: " + ifce.getMessage());
}
}

public static void main(String[] args) {
Formatting f = new Formatting();

f.commaDecimal();
f.commaFloating();
f.commaException();
}

E il risultato dovrebbe essere questo:

10.000.000
10.000.000,230000
Can't do that: d != java.lang.Double

Da notare che i separatori utilizzati sono funzione della localizzazione utilizzata. Nel mio caso uso i settaggi italiani, e quindi una virgola per i decimali e un punto ogni tre cifre alla sua sinistra.

Il metodo format() opera in modo molto simile alle convenzioni della printf del linguaggio C. Il percento indica di inserire un parametro in quella posizione.

La stringa di formattazione "%,14.2f" va interpretata come: stampa il numero floating point (f) passato come parametro (%) inserendo i separatori (,) visualizzando almeno 14 caratteri (14) di cui due sono cifre frazionarie (.2).

La struttura generica di un formattatore é la seguente:

%[numero di argomento][opzioni][ampiezza][.precisione]tipo

Il tipo é l'unico elemento obbligatorio. In caso di mancata compatibilità con il parametro effettivamente passato viene generata una eccezione, come nell'esempio sopra riportato, dove si é specificato che ci si attendeva un decimale (un intero, quindi) ma in realtà é stato passato un floating point.
  • %d: decimale, é atteso un intero (byte, short, int, char o un wrapper a questi tipi)
  • %f: floating point, ovvero float, double o BigDecimal
  • %x: esadecimale, applicabile a byte, short, int, long e BigInteger.
  • %c: carattere, applicabile a byte, short, char, int.
Il metodo format() accetta un numero variabile di argomenti, secondo uno schema varargs mutuato dal linguaggio C, é dunque possibile specificare svariati parametri in una stringa di formattazione. Di default l'associazione é fatta in modo diretto: il primo % corrisponde al primo dei parametri specificati a seguire.

Wrapping e unwrapping

Da Head First Java O'Reilly, capitolo 10. Parte dedicata al wrapping di variabili primitive in oggetti delle corrispondenti classi wrapper, e del loro unwrapping.

Dalla versione 5.0 di Java é disponibile la feature di autoboxing, il che vuol dire che un valore primitivo viene automaticamente convertito in oggetto del tipo wrapper corrispondente dove richiesto. E viceversa, un oggetto di un tipo wrapper viene convertito automaticamente nel valore primitivo se necessario.

In questo modo, ad esempio, si semplifica la gestione dei container, dato che questi, in Java, posso lavorare solo con oggetti e non con tipi primitivi.

I wrapper mettono inoltre a disposizione dei metodi per convertire esplicitamente una stringa nel valore primitivo correlato. Per il boolean occorre invece passare dal costruttore, che ammette l'uso di una stringa. Ovviamente ci si deve aspettare la possibilità di una eccezione in caso la stringa non sia quanto atteso.

try {
int x = Integer.parseInt("2");
double d = Double.parseDouble("2.23");
boolean b = new Boolean("true").booleanValue();
}
catch (NumberFormatException nfe) {
//
}

Il modo più semplice per convertire un numero in una stringa é quello di sfruttare l'overloading dell'operatore + definito per le stringhe. Oppure si può fare riferimento al metodo toString() che é definito per ogni oggetto e, nel caso dei wrapper, anche come metodo static:

String s = "" + 23.4;
String t = Double.toString(12.3);

Un po' di matematica

Da Head First Java O'Reilly, capitolo 10. Si parla di variabili di istanza e metodi statici e, come esempio, della classe Math.

Math ha solo metodi statici. Tra l'altro il suo costruttore é dichiarato privato e perciò non é possibile instanziare oggetti di questa classe.

Tra i metodi citati:

Math.random(): ritorna un double in [0, 1)

public void doRandom() {
System.out.println("a random number:" + Math.random());

int[] result = new int[5];
for(int i = 0; i < 10000; ++i) {
result[(int)(Math.random() * 5)]++;
}
System.out.println("a few random numbers in [0, 4]");
for(int i = 0; i < 5; ++i) {
System.out.println(i + ": " + result[i]);
}
}

Math.abs(): ritorna un float, double, int o long che é il valore assoluto dell'argomento passato

public void doAbs() {
int i = Math.abs(-12);
System.out.println("absolute value of negative int: " + i);

long l = Math.abs(-12L);
System.out.println("absolute value of negative long: " + l);

float f = Math.abs(-12.24f);
System.out.println("absolute value of negative float: " + f);

double d = Math.abs(-12.24);
System.out.println("absolute value of negative double: " + d);
}

Math.round(): ritorna un int se il valore passato come argomento e float, un long se si passa un double, arrotondato al valore intero più vicino

public void doRound() {
int i = Math.round(12.24f);
System.out.println("rounding a float: " + i);

long l = Math.round(-12.24);
System.out.println("rounding a double: " + l);
}

Math.min(): ritorna il minore tra i due argomenti passati (di tipo int, long, float, o double)

public void doMin() {
double d = Math.min(12.34, -2.5);
System.out.println("min of two doubles: " + d);

float f = Math.min(12.34f, -2.5f);
System.out.println("min of two floats: " + f);

long l = Math.min(12L, 2L);
System.out.println("min of two longs: " + l);

int i = Math.min(12, 2);
System.out.println("min of two ints: " + i);
}

Math.max(): ritorna il maggiore tra i due argomenti passati

public void doMax() {
double d = Math.max(12.34, -2.5);
System.out.println("max of two doubles: " + d);

float f = Math.max(12.34f, -2.5f);
System.out.println("max of two floats: " + f);

long l = Math.max(-12L, 2L);
System.out.println("max of two longs: " + l);

int i = Math.max(-12, 2);
System.out.println("max of two ints: " + i);
}

Battaglia navale migliorata

Proseguo la lettura di Head First Java O'Reilly, capitolo 6.

Si era creata una prima versione semplificata del programma di battaglia navale, ora introduciamo cambiamenti per rendere l'applicazione più aderente alle specifiche iniziali.

Per prima cosa creo un nuovo enumeratore che definisce le celle disponibili nel tavoliere della battaglia navale:

package DotCom;

public enum Coord {
A1, A2, A3, A4, A5, A6, A7,
B1, B2, B3, B4, B5, B6, B7,
C1, C2, C3, C4, C5, C6, C7,
D1, D2, D3, D4, D5, D6, D7,
E1, E2, E3, E4, E5, E6, E7,
F1, F2, F3, F4, F5, F6, F7,
G1, G2, G3, G4, G5, G6, G7,
}
Creiamo poi la classe Buster che gestisce le navi sul tavoliere:

package DotCom;

import java.util.ArrayList;

public class Buster {

private ArrayList<Ship> ships = new ArrayList<Ship>();
private int guesses = 0;

public void add(Ship ship) {
ships.add(ship);
}

/**
*
* @param c where to shoot
* @return true if all ships are destroyed
*/
public boolean shoot(Coord c) {
guesses++;

System.out.println("[" + c + "] " + Result.Miss);
return false;
}

public int getGuesses() {
return guesses;
}
}

Le navi da colpire vengono passate all'istanza di Buster per mezzo del metodo add() che semplicemente aggiunge la nave alla lista privata. L'algoritmo per la creazione di una nave nel tavoliere é non banale, fra l'altro occorre evitare che due navi si sovrappongano, e aldilà dello scopo dell'esempio. Ce le dobbiamo costruire a mano e fare attenzione a costruirle in modo coerente.

Il metodo shoot() ha al momento una implementazione minimale, lo completo più avanti. Deve funzionare in questo modo:
  • accetta in input la coordinata del nostro colpo;
  • incrementa il contatore dei colpi sparati;
  • verifica se il colpo ha successo o meno e, se anche l'ultima nave é stata affondata, si ritorna true (parte ancora da implementare);
  • se il colpo é andato a vuoto lo si comunica all'utente prima di tornare false.

Abbiamo poi un getter per la variabile di istanza guesses, che tiene sotto controllo il numero di tentativi fatti.

Aggiungo un nuovo test, che utilizza Buster:

@Test
public void testShip() {
System.out.println("test a ship");

Buster buster = new Buster();

ArrayList<Coord> t1 = new ArrayList<Coord>();
t1.add(Coord.A1);
t1.add(Coord.B1);
t1.add(Coord.C1);
buster.add(new Ship("Endeavour", t1));

for(Coord c : Coord.values()) {
if(buster.shoot(c)) {
int guesses = buster.getGuesses();
assertEquals(Coord.C1.ordinal() + 1, guesses);
return;
}
}

fail("ship still floating");
}

Naturalmente fallisce, data l'implementazione che ho dato a shoot(), ma ora ne completo il codice in questo modo:

public boolean shoot(Coord c) {
guesses++;

for (Ship ship : ships) {
switch (ship.check(c)) {
case Hit:
System.out.print("[" + c + "] " + ship.getName() + ": ");
System.out.println(Result.Hit);

return false;
case Kill:
System.out.print("[" + c + "] " + ship.getName() + ": ");
System.out.println("Destroyed!");

ships.remove(ship);
return ships.isEmpty();
}
}

System.out.println("[" + c + "] " + Result.Miss);
return false;
}

Si cicla sulle navi presenti nella lista di Buster. Se check() riporta che la nave é stata colpita (ma non affondata) si segnala la cosa e si ritorna false, se la nave é affondata viene tolta dalla lista e si ritorna true se la lista é ora vuota.
Se nessuno di questi due casi sono verificati su nessuna delle navi in lista, allora segnaliamo che il colpo é andato a vuoto.

Il test viene passato correttamente, ne aggiungiamo un altro per verificare quello che sarebbe l'uso reale dell'applicativo:

@Test
public void testThreeShip() {
System.out.println("test three ships");

Buster buster = new Buster();

ArrayList<Coord> t1 = new ArrayList<Coord>();
t1.add(Coord.A1);
t1.add(Coord.B1);
t1.add(Coord.C1);
buster.add(new Ship("Endeavour", t1));

ArrayList<Coord> t2 = new ArrayList<Coord>();
t2.add(Coord.B3);
t2.add(Coord.B4);
t2.add(Coord.B5);
buster.add(new Ship("Enterprise", t2));

ArrayList<Coord> t3 = new ArrayList<Coord>();
t3.add(Coord.D1);
t3.add(Coord.D2);
t3.add(Coord.D3);
buster.add(new Ship("Beagle", t3));

for(Coord c : Coord.values()) {
if(buster.shoot(c)) {
int guesses = buster.getGuesses();
assertEquals(Coord.D3.ordinal() + 1, guesses);
return;
}
}

fail("ship still floating");
}

ArrayList

Da Head First Java O'Reilly, capitolo 6. Continuia trattazione della progettazione e sviluppo di un primo semplice programma in Java, una sorta di battaglia navale.

Abbiamo un bug nel nostro programma. Ci siamo dimenticati rimuovere i colpi che sono andati a segno dalla lista di caselle che compongono la nave da colpire.

Il modo più intuitivo di agire é quello di fare in modo che la nave sia un ArrayList di locazioni. Ogni volta che una cella della nave é individuata viene rimossa. Dunque la nostra nave contiene tutte e sole le celle non ancora scoperte.

ArrayList é una classe container template fornita da Java.
Alcuni dei metodi che mette a disposizione sono:
  • add(E elem): aggiunge l'elemento alla lista;
  • remove(int index): rimuove l'elemento all'indice passato;
  • remove(E elem): rimuove l'elemento passato, se presente in lista;
  • contains(E elem): true se c'é un match per l'elemento passato;
  • isEmpty(): true se la lista é vuota;
  • indexOf(E elem): l'indice dell'elemento, o -1;
  • size(): il numero di elementi nella lista;
  • get(int index): l'elemento all'indice passato.
La classe Simple cambia in questo modo:

package DotCom;

import java.util.ArrayList;

public class Simple {
private ArrayList<Integer> target;

public void setTarget(ArrayList<Integer> target) {
this.target = target;
}

public Result check(int x) {
int index = target.indexOf(x);
if(index == -1) {
return Result.Miss;
}

target.remove(index); // hit or kill!
return target.isEmpty() ? Result.Kill : Result.Hit;
}
}

Il target dei colpi ora é un ArrayList di interi, il metodo setTarget() cambia il suo parametro in input; check() risulta semplificata, in quanto usa i metodi di ArrayList.

I test cambiano di conseguenza, in quanto il bersaglio viene ora passato come ArrayList di interi. Ad esempio il finto main di prova prepara il target in questo modo:

int base = (int) (Math.random() * 5); // [0,4]
ArrayList<Integer> target = new ArrayList<Integer>();
target.add(base);
target.add(base + 1);
target.add(base + 2);

Baco risolto, i test ora corrono senza dare errori.

Battaglia navale

Da Head First Java O'Reilly, leggo il capitolo 5 che tratta la progettazione e lo sviluppo di un primo semplice programma in Java.

Si tratta di una piccola battaglia navale giocata dall'utente contro il computer. Si gioca su una tabella 7x7.
  • setup: tre navi vengono messe sulla tabella.
  • il gioco: si itera finché non ci sono più navi:
    • si chiedono all'utente le coordinate ("C6", "A0", ...)
    • il risultato può essere hit, miss, kill
    • In caso di miss si chiede la prossima mossa.
    • In caso di kill si rimuove la nave, se non ci sono più navi, il gioco termina, altrimenti si chiede la prossima mossa.
    • In caso di hit si rimuove la cella e si chiede la mossa successiva.
  • fine: si valuta il risultato dell'utente.
La prima versione é ancora più semplice: la tabella si riduce ad un array [0, 6], chiamiamo la classe che implementa questa soluzione DotCom.Simple, tale classe avrà un metodo per creare un bersaglio e un metodo per verificare un colpo:
package DotCom;

public class Simple {
private int[] target;
private int hits;

public void setTarget(int [] target) {
this.target = target;
}

public Result check(int x) {
return Result.Miss;
}
}
La classe Result é un enum che può assumere i valori Miss, Hit, Kill:
public enum Result {
Miss, Hit, Kill
}
Scrivo un primo test per l'applicazione:
public class SimpleTest {
//...

@Test
public void testSimple() {
System.out.println("first test");
int[] target = {2,3,4};
Simple instance = new Simple();
instance.setTarget(target);

Result result = instance.check(2);
assertEquals(Result.Hit, result);
}
}
Come atteso il test ritorna errore, ma scriviamo ora una prima implementazione reale del metodo check():
public Result check(int x) {
Result result = Result.Miss;

for(int cell : target) {
if(x == cell) {
result = Result.Hit;
hits++;
break;
}
}

if(hits == target.length) {
result = Result.Kill;
}

System.out.println(result);
return result;
}
Scrivo ora un test più completo che simula quello che sarà il main reale:
@Test
public void fakeSimpleMain() {
System.out.println("fake main");

int guesses = 0;
int base = (int) (Math.random() * 5); // [0,4]
int[] target = {base, base + 1, base + 2};
Simple instance = new Simple();
instance.setTarget(target);

for(int i = 0; i < 7; ++i) {
++guesses;
if(instance.check(i) == Result.Kill) {
assertEquals(guesses, base + 3);
return;
}
}
fail("can't kill");
}
Ma c'é un bug evidente, che viene evidenziato da questo test:
@Test
public void bug1() {
System.out.println("bug1");

int base = (int) (Math.random() * 5); // [0,4]
int[] target = {base, base + 1, base + 2};
Simple instance = new Simple();
instance.setTarget(target);

for(int i = 0; i < 7; ++i) {
if(instance.check(base) == Result.Kill) {
fail("no kill expected");
}
}
}
Ripetendo il colpo sulla prima cella del bersaglio si ottiene, incongruamente, l'affondamento della nave.

STL - queue

Dal quarto capitolo di Designing Components with the C++ STL, di Ulrich Breymann.

La coda (queue) permette di inserire oggetti ad un suo estremo e rimuoverli dalla parte opposta. Gli elementi ad entrambi i lati possono essere letti senza essere rimossi.

Può essere implementata usando list o deque.

Questi i metodi messi a disposizione:
  • bool empty() const;
  • size_type size() const: il numero di elementi in coda;
  • value_type& front(), const value_type& front() const: legge il valore all'inizio;
  • value_type& back(), const value_type& back() const: legge il valore alla fine;
  • void push(const value_type& x): immette un nuovo elemento in coda;
  • void pop(): elimina il primo elemento;
Un esempio di uso di uno stack:
#include<iostream>
#include<queue>

using namespace std;

int main() {
queue<int> aQueue;

if(aQueue.empty()) {
cout << "Empty queue created." << endl;
}

cout << "size is " << aQueue.size() << endl;

aQueue.push(5);
aQueue.push(7);
cout << "size is " << aQueue.size() << endl;
cout << "front is " << aQueue.front() << endl;

aQueue.pop();
cout << "size is " << aQueue.size() << endl;
cout << "front is " << aQueue.front() << endl;

aQueue.pop();
cout << "size is " << aQueue.size() << endl;
cout << "front is " << aQueue.front() << endl;
}

Un piccolo tutorial MVC

Il terzo capitolo di Head First - Servlets and JSP edito dalla O'Reilly, é dedicato alla creazione di una piccola web application usando il pattern MVC.

L'applicazione ha lo scopo di fornire suggerimenti su quale birra scegliere, usa:
  • BeerExpert.java (un POJO che fa da Model);
  • beerForm.html e result.jsp (View);
  • una servlet (Controller).
Le componenti suddette interagiscono tra loro in questo modo:
  • l'utente richiede l'accesso alla pagina web beerForm.html;
  • il container reperisce la risorsa;
  • il container rende disponibile beerForm.html all'utente;
  • l'utente specifica le informazioni richieste quindi genera una request per il container;
  • il container determina quale servlet richiamare e le passa la request;
  • la servler chiama BeerExpert per determinare la risposta;
  • la servlet aggiunge il risultato ottenuto da BeerExpert alla request;
  • la servlet inoltra le request a result.jsp;
  • la JSP legge la risposta dalla request;
  • la JSP genera una pagina che passa al container;
  • il container ritorna la pagina all'utente.
Per la creazione dell'ambiente di sviluppo utilizzo Netbeans, per il rilascio su Tomcat uso le funzionalità amministrative di Tomcat che permettono di fare il deploy di una web application (in formato WAR) con estrema semplicità.

Continuo a lavorare sullo stesso progetto creato nel capitolo precedente, HeadFirstWeb.

Iniziamo da beerForm.html, una pagina che contiene un form che richiama, via POST, la servlet SelectBeer.do, che creeremo in seguito. L'unico attributo che possiamo variare é il colore della birra, i cui possibili valori sono presentati in un menù a tendina:
<html>
<head>
<title>Beer selector</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<h1>Beer Selection Page</h1>
<form method="POST" action="SelectBeer.do">
Select beer color:
<select name="color">
<option value="light">light</option>
<option value="amber">amber</option>
<option value="brown">brown</option>
<option value="dark">dark</option>
</select>
<p><input type="SUBMIT"></p>
</form>
</body>
</html>
Facciamo il test la pagina, modificando leggermente il layout secondo il gusto personale. Evidentemente se clicchiamo sul bottone otteremo un messaggio di errore che ci dice che la risorsa che abbiamo richiesto non é disponibile.

Pensiamo ora alla servlet. Decidiamo di darle il nome interno BeerSelector, di mettere il file che la implementa nel package hf.beer con nome BeerSelect.java. L'URL utilizzato dall'utente per accederla é, come abbiamo già visto, SelectBeer.do.

Creo dunque la servlet hf.beer.BeerSelect.java e specifico i nomi come indicato sopra a Netbeans che mi aggiorna il DD, web.xml, in questo modo:
<web-app ...>
...
<servlet>
<servlet-name>BeerSelector</servlet-name>
<servlet-class>hf.beer.BeerSelect</servlet-class>
</servlet>
...
<servlet-mapping>
<servlet-name>BeerSelector</servlet-name>
<url-pattern>/SelectBeer.do</url-pattern>
</servlet-mapping>
</web-app>
Faccio un nuovo test del progetto, e verifico che ora cliccare sul bottone non mi dà un errore ma mi visualizza una pagina bianca. Questo perché l'ho lasciato creare la servlet da Netbeans che mi ha generato il codice di default che non fa nulla.

Dal nome logico al file del servlet:
  • Dalla pagina HTML il browser genera la richiesta di /HeadFirstWeb/SelectBeer.do dove la prima parte del path é la root della Web App e la parte finale é il nome logico della risorsa.
  • Il container cerca nel DD il nome logico della servlet tra gli url-pattern negli elementi servlet-mapping trovando il servlet-name (nome interno).
  • Andiamo a cercarci tra gli elementi servlet quello che ha il servlet-name che abbiamo trovato nel passo precedente.
  • E finalmente troviamo il servlet-class (nome fisico) che viene usato dal container. Se la servlet non é già stata inizializzata la classe viene caricata e la servlet inizializzata.
  • Il container inizia un nuovo thread per gestire la richiesta e passa l'oggetto request al thread.
  • Al termine dell'elaborazione da parte della servlet il container manda la risposta al cliente.
Prima versione della servlet

Modifico il codice di default proposto da Netbeans in questo modo:
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try {
out.println("<html>");
out.println("<head>");
out.println("<title>Beer Selection Advice</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>Got beer color: " + request.getParameter("color") + "</h1>");
out.println("</body>");
out.println("</html>");
} finally {
out.close();
}
}
Il punto fondamentale é che si usa il metodo getParameter() sulla request per accedere il parametro color passato dall'utente, che viene scritto nella response.

Compilo, faccio il deploy, e adesso cliccando sul bottone possiamo avere un primo feedback positivo.

Architettura delle Web App

Il secondo capitolo di Head First - Servlets and JSP edito dalla O'Reilly, é dedicato ad una introduzione ad alto livello sull'architettura di una web application.

Container

Le servlet non hanno un main(), sono sotto il controllo di un'altra applicazione Java, detta Container. Un esempio di Container é Tomcat.

L'applicazione web server delega al container le richieste che arrivano dal client per una servlet. Il Container si fa carico di passare alla servlet la request e la response HTTP chiamando i metodi della servlet.

Il Container gestisce:
  • il supporto alle comunicazioni tra le servlet e il web server;
  • il ciclo di vita delle servlet;
  • il supporto multithreading, creando un nuovo thread per ogni richiesta per servlet che riceve;
  • la sicurezza dichiarativa, usando un descrittore XML per il deployment;
  • il supporto a JSP.
Come il Container gestisce una request:
  • crea un oggetto HttpServlerResponse e un HttpServlerRequest;
  • in base alla URL passata determina la servlet richiesta, crea o alloca un thread e passa gli oggetti creati al passo precedente al thread della servlet;
  • chiama il metodo service() della servlet che, a seconda del tipo della richiesta, invoca doGet() o doPost();
  • il metodo della servlet genera la pagina dinamica nell'oggetto response;
  • il Container riprende il controllo, termina il thread, converte l'oggetto response in una response HTTP e la manda indietro al client, infine distrugge gli oggetti request e response.
I tre nomi di una servlet

Una servlet é identificata da un URL, un nome interno, e un nome reale:
  • dal punto di vista del programmatore Java, una servlet é una classe e avrà quindi un path name determinato dal nome del package in cui si trova e dal nome della classe stessa (classes/registration/SignupServlet.class);
  • a livello di progettazione del sistema, alla servlet può essere associato un nome diverso, slegato dai dettagli implementativi (EnrollServlet);
  • per l'utente la servlet ha un URL (register/registerMe).
Lo scopo di questa organizzazione é avere una migliore flessibilità e sicurezza.

Il mapping tra questi nomi é fatto in un documento XML, detto DD (Deployment Descriptor), che contiene un elemento servlet e un servlet-mapping per ogni servlet definita nella web application.

MVC: Model View Controller

Il principio di MVC sta nella separazione della business logic dalla presentazione, mettendoci qualcosa in mezzo, in modo da garantire l'indipendenza delle parti. In questo modo le applicazioni sono più semplici da gestire e si tende a favorire il riutilizzo delle classi.
  • Model: gestisce lo stato dell'applicazione e la sua logica business, é l'unica parte del sistema che parla con il database. Implementato con classi Java normali.
  • Controller: prende l'input dalla request e lo valuta in funzione del Model a cui dice di cambiare in funzione dell'input; rende disponibile il nuovo stato alla View. Implementato dalla servlet.
  • View: responsabile della presentazione, prende l'input e lo gira al Controller, mostra l'output all'user. Implementato via JSP.
J2EE

Un Application Server J2EE include un Web Container (per servlet e JSP) e un EJB Container (per Enterprise JavaBean). In una architettura J2EE "completa" sono gli EJB a farsi carico di implementare la business logic.

Tomcat é un Web Container, non gestisce EJB.

STL - stack

Dal quarto capitolo di Designing Components with the C++ STL, di Ulrich Breymann.

Lo stack é un container che permette inserimenti, letture, eliminazione a solo ad una estremità. Come tipo implicito é possibile usare ogni container sequenziale che supporti back(), push_back() e pop_back().

L'implementazione standard usa deque.

I metodi implementati sono:
  • bool empty() const: true se non ci sono elementi;
  • size_type size() const: numero di elementi;
  • value_type& top(), const value_type& top() const: l'elemento in cima;
  • void push(const value_type& x): aggiunge un elemento in cima;
  • void pop(): rimuove l'elemento in cima.
Un piccolo esempio d'uso:
#include<iostream>
#include<stack>

using namespace std;

int main() {
stack<int> aStack;

if(aStack.empty()) {
cout << "Empty stack created." << endl;
}

cout << "size is " << aStack.size() << endl;

aStack.push(5);
aStack.push(7);
cout << "size is " << aStack.size() << endl;
cout << "top is " << aStack.top() << endl;

aStack.pop();
cout << "size is " << aStack.size() << endl;
cout << "top is " << aStack.top() << endl;

aStack.pop();
cout << "size is " << aStack.size() << endl;
cout << "top is " << aStack.top() << endl;
}

STL - Abstract Data Types

Il quarto capitolo di Designing Components with the C++ STL, di Ulrich Breymann, é dedicato a stack, queue e priority_queue; e poi ai container associativi ordinati: set,
map, multiset e multimap.

Le classi template presentate qui sono chiamate anche container adaptor perché adattano un interfaccia. Ad esempio quando si usa un oggetto stack si usano i metodi i metodi propri di stack che usano un container che potrebbe essere un vector.

STL - deque

Dal terzo capitolo di Designing Components with the C++ STL, di Ulrich Breymann, dedicato ai container.

Il container deque (double ended queue: coda a doppia terminazione), come vector, permette l'accesso casuale e, come una list, permette l'inserimento e rimozione di elementi agli estremi in tempo costante. Inserimento e rimozione nel suo mezzo sono invece relativamente costosi, O(n).

Metodi aggiuntivi per deque:
  • reference operator[](n), const_reference operator[](n): ritorna una referenza all'ennesimo elemento.
  • reference at(n), const_reference at(n): ritorna una referenza all'ennesimo elemento, o lancia una eccezione.
  • void push_front(const T& t): inserisce un elemento all'inizio.
  • void pop_front(): elimina il primo elemento.

STL - List

Dal terzo capitolo di Designing Components with the C++ STL, di Ulrich Breymann, dedicato ai container.

Il container List ha una serie di metodi aggiuntivi:
  • void merge(list&), void merge(list&, Compare_object): fonde due liste ordinate - complessità O(n). E' possibile specificare la modalità di comparazione.
  • void push_front(const T& t): inserisce un elemento all'inizio.
  • void pop_front(): elimina il primo elemento.
  • void remove(const T& t): elimina tutti gli elementi uguali a t - O(n).
  • void remove_if(Predicate P): elimina tutti gli elementi per cui vale il predicato passato.
  • void reverse(): inverte l'ordine degli elementi nella lista.
  • void sort(), void sort(Compare_object): ordina gli elementi in una lista usando Compare_object o l'operatore < definito per gli elementi. O(n log n).
  • void splice(iterator pos, list& x): sposta il contenuto della lista x prima di pos.
  • void splice(iterator pos, list&x, iterator it): sposta l'elemento di x puntato da it prima di pos.
  • void splice(iterator pos, list& x, iterator first, iterator last): sposta gli elementi di x in [first, last) prima di pos. Se x == this pos deve essere esterno all'intervallo passato.
  • void unique(), void unique(binaryPredicate): elimina gli elementi identici consecutivi, ad eccezione del primo, se applicato ad una lista ordinata lascia solo elementi unici.
Faccio un esempio che riassume, modificando leggermente, gli esempi proposti dal libro:
#include<iostream>
#include<iterator>
#include<list>

using namespace std;

void display(const list<int>& aList) {
list<int>::const_iterator it = aList.begin();
while (it != aList.end())
cout << *it++ << ' ';
cout << "[size is " << aList.size() << ']' << endl;
}

int main() {
list<int> list1;
list<int> list2;
list<int> list3;

for (int i = 0; i < 10; ++i) {
list1.push_front(i * 2);
list2.push_back(i * 2 + 1);
list3.push_back(20 + i);
}
display(list1);
cout << "Sorting:" << endl;
list1.sort();
display(list1);

cout << "The other list:" << endl;
display(list2);

cout << "Merging:" << endl;
list1.merge(list2);
display(list1);
display(list2);

cout << "The third list:" << endl;
display(list3);

cout << "Splice:" << endl;
list<int>::iterator it = list3.begin();
advance(it, 4);
list1.splice(list1.end(), list3, list3.begin(), it);
display(list1);
display(list3);
}
La funzioncina display() ci mostra la scansione sequenziale di una lista via iteratore. Le tre liste sono inizializzate usando push_front() e push_back(), la prima lista viene ordinata usando sort(), dopodichè viene fusa con list2 usando merge(). La terza lista viene utilizzata per farne uno splice() in list1: i suoi primi quattro elementi vengono spostati alla fine di list1.

Una semplice servlet

Continuo la lettura di Head First - Servlets and JSP edito dalla O'Reilly, finisco il primo capitolo affrontando il semplice esempio che lì trova.

Secondo gli autori sarebbe meglio non usare alcun IDE e scriversi tutto a mano. Potrebbero aver ragione, ma é una seccatura immane. Uso perciò Netbeans per semplificarmi la vita.

Creo un nuovo progetto, una Web Application nella categoria Java Web, che chiamo HeadFirstWeb. Come server uso Tomcat 6 e faccio riferimento a Java EE 5.

Nel progetto mi creo una nuova servlet, la cui classe ha nome s01, che metto nel package hf. La servlet ha nome ASimpleServlet e pattern URL /servlet01, confermiamo e andiamo a vedere il codice generato.

web.xml

Il file si trova nel folder WEB-INF e contiene queste informazioni:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<servlet>
<servlet-name>ASimpleServlet</servlet-name>
<servlet-class>hf.s01</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ASimpleServlet</servlet-name>
<url-pattern>/servlet01</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>30</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>

Ci interessano le sezioni relative alle servlet. Il primo blocco definisce la correlazione tra il nome della servlet, ASimpleServlet, e la sua classe Java di riferimento, hf.s01; nel blocco servlet-mapping vediamo la correlazione tra il nome della servlet e l'URL utilizzata per accederla.

In pratica il nome della servlet fa da tramite tra il nome della classe Java che la implementa e l'indirizzo con cui é possibile richiamarla.

s01.java

Il file si trova nella sezione Source Packages, nel package hf.

Modifichiamo solo leggermente il codice proposto. L'intestazione del file resta tale e quale:

package hf;

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

La classe viene definita come una estensione di HttpServlet:

public class s01 extends HttpServlet {

Quello che cambiamo é il metodo processRequest() che viene utilizzato per processare sia POST che GET. In realtà a noi, a questo punto, interesserebbe solo considerare la GET, quindi potremmo semplificare il codice, eliminando la gestione per la POST e spostando il codice definito in processRequest() nel corpo di doGet(). Ma non ci pare un impresa particolarmente utile, e quindi non cambiamo questo codice:

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}

Nella processRequest() rendiamo attivo il codice che genera una response e facciamo in modo che venga visualizzato il timestamp corrente:

protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try {
out.println("<html>");
out.println("<head>");
out.println("<title>Servlet s01</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>Servlet @ " + new java.util.Date() + "</h1>");
out.println("</body>");
out.println("</html>");
} finally {
out.close();
}
}

Notiamo che settiamo il tipo di contenuto della response a testo html codificato in UTF-8, e che scriviamo il codice HTML nella response approfittando dell'oggetto PrintWriter che la response ci mette a disposizione.

Notiamo anche che la scrittura sulla response é racchiusa in un blocco try a cui corrisponde un finally in cui chiudiamo il writer. Così anche in caso di eccezioni durante la scrittura chiudiamo correttamente il writer.

Facciamo una build del progetto e otteniamo un .war (Web Archive) che contiene la nostra piccola web application. Ne facciamo un deploy per la nostra installazione Tomcat e possiamo vedere la nostra servlet in azione.

C'é una varietà di modi per eseguire l'installazione di un war su Tomcat. Di solito io faccio così:
  • startup di Tomcat (nel mio caso gli ho riservato la porta 8084)
  • accesso di Tomcat via browser (http://localhost:8084/)
  • accesso del Tomcat Web Application Manager (http://localhost:8084/manager/html). Nota che un utente Tomcat deve avere i privilegi di admin per poter accedere a questa pagina, vedi il file conf/tomcat-users.xml nell'installazione di Tomcat.
  • verso il fondo della pagina c'é la sezione Deploy, sottosezione WAR file to deploy. Basta selezionare il nostro file war, cliccare sul bottone Deploy e il gioco é fatto.


A questo punto per accedere la servlet dovrebbe bastare specificare questo URL:
http://localhost:8084/HeadFirstWeb/servlet01


Il difetto principale di questo approccio, ci fanno notare gli autori del libro, sta nella innaturalità dell'inserimento di codice HTML all'interno di codice Java. Generare una pagina complessa, e mantenerla, diventa un compito veramente spiacevole.

Fortunatamente si può fare il contrario, ovvero inserire codice Java all'interno di HTML, ricorrendo a JSP.

Torniamo al nostro progetto HeadFirstWeb, notiamo che Netbeans ha generato anche un index.jsp nella sezione WebPages. Andiamo a modificarla leggermente, per inserire il codice Java che ci interessa (la generazione del timestamp) e, già che ci siamo, un link alla servlet. Il corpo della pagina diventa questo:

<body>
<h1>Hello World!</h1>
JSP @ <%= new java.util.Date() %>
<br />
<a href="servlet01" target="_blank">Servlet</a>
</body>

Compiliamo, rimuoviamo il vecchio war da Tomcat e installiamo il nuovo e ora, specificando questa URL:
http://localhost:8084/HeadFirstWeb/

Dovremmo vedere la nostra pagina JSP, con link alla servlet che abbiamo creato in precedenza.

Head First - Servlets and JSP

Un buon libro introduttorio su questa parte di Java EE é Head First - Servlets and JSP edito dalla O'Reilly.

Lo leggo e scrivo qui qualche nota, a partire dal capitolo 1: perché usare servlets e JSP?

Il processo: un browser web permette all'utente di richiedere una risorsa (pagina html, immagine, suono, un documento pdf, ...); il server web riceve la richiesta, trova la risorsa e ritorna qualcosa all'utente.

Se non la trova l'utente ottiene l'errore 404 Not Found.

Il meccanismo richiesta (request) risposta (response) é gestito via HTTP per mezzo di metodi, i più comuni tra i quali sono GET e POST.
  • GET: semplice richiesta al server per una risorsa.
  • POST: é una richiesta a cui sono associati dei parametri.
In realtà é possibile mandare dati al server anche via GET ma non é solitamente una buona idea, dato che:
  • il numero di caratteri che si possono mandare é basso, e dipendente dal server;
  • i dati mandati via GET sono appesi all'URL, sono quindi visibili all'utente;
D'altro canto, se si vuole ripetere spesso una richiesta parametrizzata, per una GET si può creare un bookmark, mentre con la POST occorre rieseguire la pagina in cui sono settati i parametri.

Per una GET la richiesta é strutturata in questo modo:

Request line:
GET [path]?[param1]&[param2]& ... [paramN] HTTP/1.1

Request headers:
Host: ...

Per una POST, invece, le cose funzionano così:

Request line:
POST [path] HTTP/1.1

Request headers:
Host: ...
...
Accept: text/html, ...
...

Message body (o "payload"):
[param1]&[param2]& ... [paramN]

La risposta HTTP é strutturata così:

Response Headers:
HTTP/1.1 200 OK
Set-Cookie: ...
Content-Type: text/html
...

Response body:
<html>
...
</html>

Il Content-Type indica il tipo (MIME) che identifica la risorsa reperita. Vedi anche il campo Accept della request.

La richiesta di pagine web statiche é gestita direttamente dall'applicazione web server. Nel caso di richiesta di pagine dinamiche, si appoggia ad un altra applicazione sul server.

Le applicazioni classiche che si occupano della generazione di pagine dinamiche sono programmi CGI (Common Gateway Interface) scritti tipicamente in Perl, C, Python e PHP. Per Java si parla di servlet e JSP.