Interfacce grafiche, membri statici e server di rete
Progettazione del Software
Formulario
Ascoltatore e finestra
tutto in main: un
componente per volta
classe come struttura:
un solo oggetto
finestra che estende JFrame:
riceve this
finestra che implementa
ActionListener:
nulla da passare
JOptionPane
showConfirmDialog: 0, 1, 2
showMessageDialog: messaggio
showInputDialog: stringa,
null se Cancel
Campi e metodi statici
campo static: uno per classe
campo normale: uno per oggetto
Classe.campo, Classe.metodo()
costruttore private: niente
oggetti
final: niente sottoclassi
Math.random() dà x con
0≤x<1
Seriale progressivo
ultimo è static, da 0
seriale = ultimo + 1
ultimo = seriale
Server a una connessione
ciclo: accept(), poi
il protocollo
le altre connessioni
attendono in coda
"quit".equals(linea)
Finestra e ascoltatore: quattro organizzazioni
Un ascoltatore deve raggiungere i componenti della finestra, per esempio per leggere il testo di un JTextField. Il problema è come passarglieli. Ci sono quattro soluzioni, tutte corrette.
Soluzione
Chi costruisce la finestra
Cosa riceve l'ascoltatore
Tutto separato
main
un componente per volta
Classe come struttura
main
un solo oggetto con i componenti
Finestra che estende JFrame
il costruttore
la finestra, cioè this
Finestra che è anche ascoltatore
il costruttore
niente, perché è lei stessa
Tutto separato.main costruisce la finestra e passa all'ascoltatore ogni componente che gli serve. È la più chiara, perché ogni pezzo è visibile. Con molti componenti diventa noiosa: ogni variabile esiste in main e di nuovo nell'ascoltatore, e vanno fatte coincidere.
Classe come struttura. In Java una struttura è un oggetto. Una classe ha come campi tutti i componenti, main crea l'oggetto e all'ascoltatore passa solo lui.
Finestra che estende JFrame. I componenti diventano campi della finestra stessa e il costruttore costruisce l'interfaccia, quindi main si riduce a new Finestra(). L'ascoltatore resta separato e riceve la finestra: nel costruttore this è la finestra che si sta costruendo. I campi non sono private, così l'ascoltatore li raggiunge.
import java.awt.BorderLayout;import javax.swing.JButton;import javax.swing.JFrame;import javax.swing.JTextField;public class Finestra extends JFrame { JTextField primo; JTextField secondo; JButton copia; public Finestra() { super("Copia"); primo = new JTextField(20); secondo = new JTextField(20); copia = new JButton("Copia"); Ascoltatore ascoltatore = new Ascoltatore(this); copia.addActionListener(ascoltatore); this.add(primo, BorderLayout.NORTH); this.add(secondo, BorderLayout.CENTER); this.add(copia, BorderLayout.SOUTH); this.pack(); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setVisible(true); }}
import java.awt.event.ActionEvent;import java.awt.event.ActionListener;public class Ascoltatore implements ActionListener { private Finestra finestra; public Ascoltatore(Finestra finestra) { this.finestra = finestra; } public void actionPerformed(ActionEvent e) { String testo = finestra.primo.getText(); finestra.secondo.setText(testo); }}
Gli oggetti della finestra che estende JFrame
L'ascoltatore conserva un solo riferimento, alla finestra, e attraverso i suoi campi raggiunge tutti i componenti.
Finestra che è anche ascoltatore. La classe estende JFrame e implementa ActionListener. Registra se stessa come ascoltatore e non passa niente, perché actionPerformed accede direttamente ai suoi campi.
public class Finestra extends JFrame implements ActionListener { // campi e costruttore come prima, con // copia.addActionListener(this); public void actionPerformed(ActionEvent e) { String testo = primo.getText(); secondo.setText(testo); }}
Il vantaggio è non passare dati. Lo svantaggio è che costruzione della finestra e gestione degli eventi stanno in una sola classe, che con finestre e ascoltatori lunghi diventa enorme.
Si sconsiglia di scrivere l'ascoltatore come classe anonima dentro addActionListener. Serve un ascoltatore per ogni pulsante, e un actionPerformed di duecento righe è ingestibile. È accettabile solo con poche istruzioni.
Finestre pronte con JOptionPane
Per un messaggio o una domanda semplice non serve costruire un JFrame con i suoi componenti e il suo ascoltatore: JOptionPane offre finestre già fatte. Il primo argomento è la finestra principale: negli esempi non c'è e vale null, in un programma con un JFrame si passa quello.
import javax.swing.JOptionPane;public class Dialoghi { public static void main(String[] args) { int s = JOptionPane.showConfirmDialog(null, "Continuare?"); System.out.println(s); JOptionPane.showMessageDialog(null, "Operazione conclusa"); String nome = JOptionPane.showInputDialog(null, "Nome?"); System.out.println(nome); }}
showConfirmDialog mostra i pulsanti Yes, No e Cancel e restituisce un intero: 0 per Yes, 1 per No, 2 per Cancel.
showMessageDialog mostra un messaggio.
showInputDialog chiede una stringa. Con OK o con Invio restituisce la stringa, con Cancel restituisce null.
Servono soprattutto negli ascoltatori: se l'ascoltatore trova un problema e deve fare una domanda semplice, la fa così.
Campi e metodi statici
Di Math si usa il nome della classe, senza creare un oggetto: Math.PI approssima π (stampa 3.141592653589793), Math.sqrt(16) dà 4.0 e Math.random() dà un numero x con
0≤x<1
Sono un campo e due metodi statici, cioè associati alla classe e non a un oggetto. Math ha il costruttore privato, quindi non esistono oggetti Math.
Per vederlo si scrive una classe fatta come Math.
public final class Due { private Due() { } static int due = 2; static int raddoppia(int n) { return 2 * n; }}
public class Principale { public static void main(String[] args) { System.out.println(Due.due); System.out.println(Due.raddoppia(5)); }}
Il programma stampa 2 e 10.
Campo normale: ce n'è una variabile per ogni oggetto, creata insieme all'oggetto.
Campo static: la variabile è una sola, appartiene alla classe ed esiste subito come Due.due, anche se non esistono oggetti.
Metodo static: si invoca sulla classe, come Due.raddoppia(5), senza un oggetto.
Costruttore private: si può invocare solo dentro la classe, e lì non lo si invoca mai. Da fuori new Due() non compila, quindi non esistono oggetti di Due.
Differenza da una classe astratta: una classe astratta si estende, e si creano oggetti delle sottoclassi. Qui una sottoclasse scritta fuori da Due non compila, perché il suo costruttore dovrebbe invocare Due(), che è privato.
final sulla classe: vieta le sottoclassi in modo esplicito.
Dentro la classedue equivale a Due.due.
Le costanti statiche si scrivono spesso in maiuscolo, come PI. È solo una convenzione.
Numeri seriali condivisi
Automobile ha targa, modello, anno e seriale. Il costruttore riceve i primi tre dati e assegna il seriale da sé, con un numero progressivo.
Senza assegnazione ogni seriale vale 0, il valore iniziale di un campo intero. Per dare un numero nuovo a ogni automobile serve ricordare l'ultimo assegnato. Quel dato non può stare negli oggetti, perché da un oggetto nuovo non si raggiungerebbe il precedente. Ce ne vuole uno solo per la classe: è un campo static.
public class Automobile { private String targa; private String modello; private int anno; private int seriale; private static int ultimo = 0; public Automobile(String targa, String modello, int anno) { this.targa = targa; this.modello = modello; this.anno = anno; this.seriale = Automobile.ultimo + 1; Automobile.ultimo = this.seriale; } public String toString() { return targa + ", " + modello + ", " + anno + ", seriale " + seriale; }}
Gli altri campi si scrivono con this, perché sono dell'oggetto. ultimo è della classe e si scrive Automobile.ultimo. Creando tre automobili, toString stampa i seriali 1, 2 e 3: il costruttore assegna ultimo + 1, poi pone ultimo uguale al seriale appena dato. Ogni automobile ha il proprio seriale, mentre ultimo è uno solo per tutte.
Server a una connessione per volta
Un server con un solo accept() termina dopo una connessione. Per servire connessioni successive si ripete il ciclo: dopo la fine di una connessione si torna ad accept(). Il protocollo è questo: il server manda ciao, legge una riga, risponde Hai detto seguito dalla riga e chiude la connessione. La riga quit termina il programma. I messaggi di avanzamento sulla console dicono a che punto è il programma.
ServerSocket ss = new ServerSocket(8080);String linea;do { System.out.println("attendo connessione"); Socket s = ss.accept(); System.out.println("connessione stabilita"); InputStream is = s.getInputStream(); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); OutputStream os = s.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(os); PrintWriter pr = new PrintWriter(osw); pr.write("ciao\r\n"); pr.flush(); linea = br.readLine(); System.out.println("ricevuto: " + linea); pr.write("Hai detto " + linea + "\r\n"); pr.flush(); s.close();} while (!"quit".equals(linea));ss.close();
Le stringhe si confrontano con equals. Il confronto parte da "quit" perché readLine() restituisce null se il client chiude senza inviare una riga, e linea.equals darebbe allora un errore.
Per provare il server non serve scrivere un client: basta Netcat, uno dei programmi contenuti in BusyBox. Si collega a un indirizzo e a una porta, invia ciò che si scrive e mostra le risposte. Con il server già avviato si usa busybox nc localhost 8080, oppure nc localhost 8080 se Netcat è installato da solo.
Si lanciano due nc insieme. Il primo riceve ciao. Il secondo non riceve niente: finché la prima connessione non finisce, accept() non viene richiamato. Le connessioni in arrivo restano in una coda, aperta dalla creazione del ServerSocket, e accept() ne prende una per volta. Quando il primo nc invia la riga, il server risponde, chiude e torna ad accept(): solo allora il secondo riceve ciao.
Ingresso e uscita sono canali separati, quindi se server e client scrivono insieme non si disturbano. Il programma scrive e poi legge, in ordine: ciò che arriva nel frattempo viene letto alla readLine() successiva.
Un server di questo tipo ha un solo thread, quindi serve un client per intero prima del successivo. Un server reale, come quello di un grande sito, non può far attendere gli utenti. Deve servire più connessioni insieme, con più thread. Creare thread è facile e accettare più connessioni richiede solo un po' di ragionamento. La parte difficile è sincronizzarli quando accedono agli stessi dati.