Introduzione
l Culturale
nel senso che un tempo, con gli strumenti messi a disposizione dai linguaggi
in voga negli anni settanta/ottanta, si ragionava in termini di array,
indirizzi, record, indici e via dicendo, mentre ora è naturale,
in fase di design e codifica, pensare a collezioni, stack, insiemi di proprietà
astratte, eventi, interfacce ed altro. Nessun concetto nuovo, in fondo,
ma il solo fatto di avere tali strumenti a portata di mano al pari degli
oggetti di base di un linguaggio, consente di operare da una prospettiva
più ampia, maggiormente focalizzata sui problemi da risolvere piuttosto
che sull'implementazione degli algoritmi e delle strutture dati. Per il
programmatore moderno, forse, l'equazione di Nicolas Wirth, "Algortimi
+ Strutture Dati = Programmi", sta diventando obsoleta e potrebbe essere
sostituita da qualche cosa del tipo "Pattern + Componenti = Sistemi".
C'è
un altro grande vantaggio nell'usare dei package standard, almeno quelli
ben organizzati: è possibile scegliere in fase di prima implementazione
(o, se preferite, di prototipo) delle classi ad un livello base (p.e. HashSet)
e riferirsi ad esse mediante un'interfaccia generica (p.e. Collection)
e, in un secondo tempo eseguire delle ottimizzazioni mirate al particolare
programma semplicemente sostituendo la dichiarazione di tali oggetti con
l'adozione di classi più specializzate (p.e. TreeSet) o appositamente
create, lasciando intatto il resto del codice e seguendo il vecchio pattern
di codifica che recita "prima fallo funzionare, poi fallo più piccolo
e veloce".
Vediamo,
quindi, cosa ci offre la cassetta degli attrezzi di Java: il package java.util.
Java.Util
In
questo package sono raccolti strumenti di uso generico, come un tokenizzatore,
un array di bit, un generatore di numeri casuali e strutture dati con gestione
LIFO e FIFO, classi per la manipolazione di date ed orari e delle proprietà
di localizzazione che servono per far funzionare un programma sotto le
convenzioni diverse che vigono in diversi paesi (formati delle date, separatori
decimali etc...).
Vengono
fornite inoltre le classi da cui derivano i vari modelli ad eventi utilizzati
in Java così come un'implementazione di base del pattern Observer/Observable
che viene ritrovato, in varie salse, in molte altre parti del JDK.
Infine,
nella versione 1.2, viene dato un framework di collezioni dati di vario
genere sulla cui base sono state reimplementate anche le vecchie collezioni
(Vettori, Hashtable etc...) che non sono state deprecate ma il cui uso,
però, viene scoraggiato in favore delle nuove e meglio strutturate
classi collection.
Notifiche
ed Eventi
Il
pattern Observer/Observable è utile per sincronizzare diversi oggetti
passibili di variazioni di stato con altri oggetti che devono ricevere
notifica di tali cambiamenti. Si tratta di una relazione dinamica di tipo
N a N, nel senso che un osservatore può registrare il proprio interesse
contemporaneamente presso diversi oggetti osservabili ognuno dei quali
può, a sua volta, notificare i propri cambiamenti a vari osservatori
registrati su di esso. Questo pattern è, ad esempio, alla base del
paradigma model-view-controller che consente di separare la rappresentazione
dei dati da quella delle operazioni e dalle possibili presentazioni degli
stessi.
Da
notare che Observerè
un'interfaccia con un solo metodo, update, che deve essere implementato
per ricevere le notifiche dalla classi derivate dalla classe astratta Observableche
possono auto-marcarsi come modificate con il metodo setChanged e richiamare
in sequenza, senza garantire alcun ordinamento, tutti gli osservatori registratisi
tramite addObserver, eseguendo notifyObservers. Contrariamente al meccanismo
di notifica di tipo semaforico offerta dalla classe Object con i metodi
notify e wait, gli osservatori sono visti come oggetti passivi (non Runnable)
ed i metodi update sono, a tutti gli effetti, della call-back richiamate
nel contesto del thread che esegue il codice dell'oggetto osservato (Vedi
[1]).
Il
pattern Observer/Observable, come implementato in Java, invia al metodo
update un reference all'oggetto osservato, la sorgente della notifica,
ed un Object opzionale per eventuali informazioni accessorie. I progettisti
di AWT e, in seguito, di SWING, così come quelli della specifica
JavaBeans, hanno ritenuto che tale implementazione fosse troppo destrutturata
e mal si prestasse alla costruzione di un framework di interfaccia event-driven.
Essi hanno optato per un albero di oggetti Ascoltatori che implementassero
una semplice interfaccia tagging:
EventListener.
Un'interfaccia tagging è caratterizzata dall'assenza di metodi ed
ha l'unico scopo di marcare le classi (o le interfaccia) da essa derivate
come appartenenti ad una determinata famiglia (un esempio e dato da Serializable).
Tutti gli eventi, invece, sono stati fatti derivare dalla classe EventObject
con l'unico metodo getSource. Credo che uno dei motivi principali di tale
scelta progettuale, in un linguaggio con ereditarietà singola, sia
stata la necessità di liberare i vari oggetti componenti i package
come AWT dal giogo di dovere tutti derivare da Observable. Inoltre viene
definita una maggiore specializzazione per cui ogni evento può essere
ricevuto solo dalle intefacce appositamente progettate per ricevere quell'evento,
e non ogni tipo generico di notifica.
EventObject
è dichiarata Serializable (pur essendo transient l'unico campo in
essa contenuto: source); questo può far pensare che tale classe
sia stata pensata avendo in mente di far derivare da essa anche Oggetti-Evento
da diramare in remoto, ad esempio nel contesto di rmi.
Date, orari ed internazionalizzazioni
La
classe Daterappresenta
un istante di tempo con la precisione di un millisecondo. La maggior parte
dei metodi di questa classe sono stati deprecati dal JDK 1.1 e le loro
funzioni affidate a classi più specialistiche come Calendar o text.DateFormat.
Date è sempre utilie per eseguire confronti ed ordinamenti tra date
ed orari.
Calendarè
una classe astratta pensata allo scopo di eseguire tutte le consuete conversioni
ed estrazioni sul tipo data (ad esempio suddividere una data in componenti
interi rappresentanti anno, messe oppure settimana, giorno della settimana
e così via) tenendo conto delle regole di un particolare calendario
e della località. Ad esempio GregorianCalendarè
una implementazione di Calendar per il formato attualmente più diffuso
nel mondo.
Se
è necessario trattare con cambi di fuso orario (cosa sempre più
frequente programmando su INTERNET) possiamo utilizzare le classi derivate
da TimeZonee,
in particolare, SimpleTimeZoneche
usa il calendario Gregoriano. I metodi di tali classi consento di effettuare
rapide conversioni anche utilizzando abbreviazioni standard per le zone
con convenzioni temporali comuni (le abbreviazioni riconosciute sono ritornate,
sotto forma di array, dal metodo getTimeZone. Il metodo getDefault si basa
sulla località e le convenzioni della piattaforma ospite, compresa
l'ora legale.
I
problemi di internazionalizzazione non riguardano solo fusi orari e calendari,
ma una insieme di differenze su formati, rappresentazione e convenzioni
che sono la croce (una delle tante, per la verità) del programmatore
globalizzato. La classe Localeserve
proprio da identificatore per tutte queste peculiarità e molte altre
classi Java, disseminate in vari package, sono abilitate a ricevere un
oggetto Locale come parametro per adeguare il proprio comportamento a tali
regole (p.e. le classe Calendar e tutte le derivate da text.Format, come
DateFormat, NumberFormat etc...). Un oggetto Locale implementa alcune regole
ma non contiene tutte le risorse necessarie per l'adattamento linguistico
(localizzazione) del software. Allo scopo di far girare lo stesso codice
(proprio lo stesso, compilato una volta per tutte) in diverse versioni
per pesi diversi, è possibile salvare le risorse sensibili alla
localizzazione in dei bundle esterni, ovvero delle classi serializzabili,
derivate da ResourceBundle,
contenenti degli array di coppie (Chiave, Oggetto), dove chiave è
una stringa e Oggetto appartiene ad una classe qualsiasi (solitamente una
stringa tradotta nella lingua del particolare bundle). Il programma decide
quale Locale utilizzare, carica dinamicamente il relativo ResourceBundle,
al posto delle costanti di tipo stringa adoperera statement del tipo getString("chiave1").
La
classe ListResourceBundle,
derivata da ResourceBundle, consente anche di ottenere l'intero array delle
coppie (chiave, Oggetto) per poterle managgiare in modo più personalizzato.
Tuttavia il modo più semplice è quello di adoperare direttamente
la classe PropertyResourceBundleper
leggere un file esterno di risorse in formato .class o anche in formato
testo, come il seguente:
File:
RisorseProgramma_it
Titolo=Titolo
Cognome=Cognome
Nome=Nome
...
o, ad
esempio, la sua versione americana
File:
RisorseProgramma_en_US
Titolo=Title
Cognome=Last
name
Nome=First
name
...
L'utilizzo
è banalizzato nell'esempio:
ResourceBundle
locRes =
ResourceBundle.getBundle("RisorseProgramma",
Locale.getDefault());
Varie
Queste
due classi sono state schiaffate quì in quanto considerate di utilità
generale. Personalmente le avrei viste meglio, rispettivamente, in java.math
e java.text:
-
Random:
generatore di numeri pseudo-casuali.
-
StringTokenizer:
tokenizzatore di stringhe. Stabiliti i caratteri di punteggiatura e spaziatura
suddivide una stringa in tokens (letteralmente gettoni. il termine indica
le singole parole di un linguaggio, come variabili, costanti e keywork,
che vengono poi passate ad un parser per l'effettiva traduzione). Un tokenizzatore
completo, al pari di un parser, è un automa a stati finiti ma, per
molte occasioni, questo mini-tokenizer si rivela adatto allo scopo, pur
non potendo, ad esempio, ritornare un numero reale come un'unica entità
controllando che il punto decimale non sia presente più di una volta.
Collezioni
Una
collezione rappresenta un insieme di oggetti detti elementi. Tipi diversi
di collezione implementano particolari caratteristiche come l'ordinamento,
la non modificabilità, l'unicità degli elementi oppure una
particolare organizzazione interna che ne ottimizzi l'accesso sotto determinate
condizioni come, ad esempio, l'accesso per chiave.
Una
generica collezione dovrebbe provvedere sempre due tipi di costruttore:
uno senza parametri per la creazione di una collezione vuota ed uno avente
per argomento una collezzione pre-esistente per utilizzare i suoi elementi
come insieme iniziale.
Nel
framework offerto dal JDK 1.2 Esistono due tipi di insiemi:
-
Collection:
collezioni di singoli oggetti organizzati per sequenze ordinate (List)
che consentono elementi uguali (rispetto al metodo equals) o per insiemi
di elementi univoci (Set),
eventualmente ordinati (SortedSet).
-
Map:
collezioni di coppie chiave/valore (Map.Entry),
eventalmente ordinate (SortedMap).
Il concetto
di ordinamento implica l'esistenza di altre due interfacce:
Comparator
ed Iterator.
Un
Comparator
è un "oggetto funzione" che implementa una determinata regola di
ordinamento tra due oggetti con il metodo
int
compare(Object
o1, Object o2)
che restituisce
i valori:
-
1 se o1>o2
-
-1 se
o1<o2
-
0 se ((Object)o1).equals((Object)o2)
La consistenza
della relazione di eguaglianza con il metodo Object.equals deve essere
garantita per un buon funzionamento delle collezioni ordinate. Quando si
crea una collezione ordinata (List,
SortedSet,SortedMap)
si applica una determinata classe di tipo Comparatoroppure
ci si affida all'ordine naturale degli oggetti se questi implementano l'interfaccia
Comparable. Un esempio elementare di comparatore è la seguente classe:
public
class OrdinaStringhe implements Comparator {
public int compare(Object o1, Object o2) {
String s1 = (String)o1;
String s2 = (String)o2;
return s1.toLowerCase().compareTo(s2.toLowerCase());
}
}
Un Iterator
consente di scandire sequenzialmente tutti gli elementi di una collezione
e di compiere eventualmente operazioni di rimozione su alcuni di essi in
modo controllato (anche in situazioni di accesso concorrente), questo non
possibile utilizzando la vecchia interfaccia Enumeration
che è mantenuta per compatibilità con il codice esistente.
Un iteratore, inoltre, lancia subito un'eccezione ConcurrentModificationException
se la collezione scandita viene modificata direttamente o tramite un'altro
iteratore evitando così risultati impredicibili.
Alcuni
metodi sono comuni a tutti i tipi di collection:
-
add: aggiunge
un nuovo elemento (o anche un doppione, se la collezione supporta questa
possibilità)
-
contains:
testa l'esistenza di un elemento
-
isEmpty:
testa se una collezione è vuota (priva di elementi)
-
iterator:
ottiene un iteratore per la collezione
-
remove:
rimuove un elemento
-
size:
ritorna il numero di elementi attualmente contenuti
-
toArray:
restituisce un array (Object[]) con tutti gli elementi della collezione
Altri
metodi sono propri di particolari interfacce:
-
List:
-
get: restituisce
l'elemento presente ad un certo indice
-
indexOf:
cerca l'indice di un determimato elemento
-
Map
-
entrySet():
restituisce un Set di tutte le entry (coppie chiave/valore) nella Map
-
put():
crea una entry e la inserisce in una Map
Finora
abbiamo parlato di interfacce ma, al fine di semplificare l'implementazione
delle proprie classi collezione, vengono fornite delle classi astratte
che realizzano buona parte del lavoro; ne esiste una per ogni interfaccia
principale: AbstractCollection,
AbstractList,
AbstractMap,
AbstractSequentialList
(utile per implementazioni ad accesso sequenziale come le liste collegate),
AbstractSet.
Il
JDK offre anche un insieme di implementazioni specifiche che potranno essere
usate nella maggior parte delle occasioni senza bisogno di realizzare classi
apposite. L'idea è quella di fornire al programmatore uno scaffale
di componenti software, in parte interscambiabili, ognuno con particolare
predisposizione ad essere utilizzato in determinate circostanze. Ad esempio
potremmo iniziare un prototipo utilizzando LinkedList e, in un secondo
tempo, renderci conto di aver bisogno di un accesso non strettamente sequenziale
e sostituire la collezione, con il minimo sforzo, con l'implementazione
basata sugli array. Vediamo quali sono le collezioni del JDK direttamente
utilizzabili:
-
ArrayList:
implementazione di List tramite array ridimensionabili.
-
BitSet:
implementa un insieme indicizzato di bit di dimensione dinamica con possibilità
di leggere/impostare ogni singolo valore e di applicare gli operatori booleani
tra diversi insiemi.
-
HashMap:
implementazione base di una Map. Simile alla vecchia HashTable tranne che
consente l'uso di valori null sia come oggetti che come chiave.
-
HashSet:
implementazione base di un Set fatta "sulle spalle" di una HashMap.
-
LinkedList:
implementazione di List derivata da AbstractSequencialList. Si tratta di
una lista doppiamente collegata, il cui accesso è quindi altrettanto
rapido sia dalla testa che dalla coda. Si presta all'implementazione di
stack e code di vario tipo.
-
Properties:
collezione serializzabile di coppie di stringhe chiave-valore.
-
PropertyPermission:
collezione di permessi per compiere determinate azioni implementata sotto
forma di Properties.
-
TreeMap:
implementazione di base di una SortedMap basata su una struttura ad albero.
L'ordinamento garantito è quello della chiave secondo l'ordine naturale
se questa implementa Comparable o secondo il Comparator passato in fase
di creazione della collezione.
-
TreeSet:
implementazione di base di un SortedSet basata su una SortedMap. L'ordinamento
garantito è quello delgli elementi secondo l'ordine naturale se
questa implementa Comparable o secondo il Comparator passato in fase di
creazione della collezione.
-
WeakHashMap:
si tratta di una realizzazione per situazioni in speciali. Utilizza le
nuove capacità di controllo sulla memoria del JDK 1.2 per realizzare
una HasMap le cui chiavi possono essere comunque reclamate dal garbage
collector se queste non hanno riforimenti forti da qualche altra parte.
Sono state
mantenute le classi legacy (presenti dagli arbori del JDK): Dictionary,
Hashtable,
Vectore
Stack;
queste classi non sono state deprecate ma sono state re-implementate sulla
base del nuovo framework: sembra comunque una buona idea evitarne l'uso
futuro in favore delle nuove Map, HashMap, ArrayList e LinkedList.
Che
si utilizzi una delle implementazioni prefabbricate o piuttosto una propria
classe derivata, è comunque buona norma utilizzare reference del
tipo adatto più generale. Utilizzare quindi:
List
lst = new ArrayList();
è
meglio di:
ArrayList lst = new ArrayList();
questo
rende più agevole posticipare ottimizzazioni dovute a test fatti
sull'uso di un prototipo funzionante. Nel precedente esempio potremmo renderci
conto che la quantità di editazione sulla lista è preponderante
rispetto alla ricerca e, quindi, decidere di modificare la dichiarazione
con:
List lst = new LinkedList();
il che,
adoperando una generica List come reference, non dovrebbe richiedere ulteriori
modifiche al codice.
Un
altro consiglio è quello di prediligere le classi già esistenti
ad implementazioni custom (se proprio non dovete fare cose strane) e, quando
una classe apposita debba porprio essere creata, attenersi alla seguente
scaletta di priorità:
-
Derivare
da un'implementazione standard: la maggior parte delle volte che si rende
necessaria una nuova classe collezione è per ottimizzare il compertamento
di una classe esistente rispetto a determinate condizioni d'uso.
-
Se, per
qualche strano motivo, nessuna classe standard fornisse una buona base,
partire da una classe astratta
-
Solo nei
casi più disperati implementare direttamente dalle interfacce Set,
List o Map
-
Non datemi
un dolore ed evitate di partire implementando direttamente Collection!
Infine
esistono due classi di utilità, con metodi statici:
-
Arrays:
per operazioni sugli array come ordinamento, ricerche binarie, conversioni
in liste etc...
-
Collections:
contiene vari metodi per la manipolazione di intere collection. In particolare
funge da factory per alcune classi wrapper che vengono create sulla base
di collezioni esistenti per aggiungere particolari caratteristiche, come:
-
Collections.unmodifiableInterface:
rende una collezione non modificabile.
-
Collections.synchronizedInterface:
rende sincronizzati gli accessi ad una collezione. E' stata un'importante
innovazione quella di rendere le classi di base di questo framework non
sincronizzate: questo permette un grande aumento di performances ove non
avvengano accessi concorrenti o dove la sincronizzazione viene controllata
in modo specifico dagli oggetti che utilizzano le collezioni.
Inoltre
questa classe implementa gli algoritmi principali sui vari tipi di collezione:
-
sort:
ordinamento di una lista mediante l'algoritmo di merge-sort
-
binarySearch:
ricerca (dicotomica) di un oggetto in una lista ordinata
-
reverse:
rovesciamento dell'ordine degli elementi in una lista
-
shuffle:
mescolamento casuale degli elementi di una lista
-
fill:
sovrascrittura con un oggetto dato di tutti gli elementi di una lista
-
copy:
copia di una lista
-
min:
restituisce il valore minimo in una lista (secondo l'ordine naturale)
-
max:
restituisce il valore massimo in una lista (secondo l'ordine naturale)
Questa
è stata una breve introduzione alle potenzialità offerte
dal framework di collezioni del JDK 1.2, secondo lo stile di questa rubrica.
In risorse sono riportati vari link per approfondire ogni aspetto dell'argomento.
Bisogna aggiungere che le collezioni, oltre ad costituire un utile paradigma
di programmazione, sono anche propedeutiche all'uso dei DB ad oggetti,
ma questo è un discorso che avremo modo di riprendere in futuro.
Package java.util.zip
Sapere
di avere questo strumento nella propria cassetta degli attrezzi renderà
felice ogni programmatore. Si tratta di un sotto-package dedicato al trattamento
dei file compressi con i diffusissimi formati standard ZIP e GZIP (in Java,
in realtà, non esiste il concetto di annidamento tra package. Il
fatto che questo sia collocato sotto java.util è solo un modo di
classificazione per attitudine comune. Tuttavia, ad esempio, una variabile
con visibilità package in java.util.zip non sarà accessibile
alla classi di java.util e vice versa).
Ecco
l'elenco delle classi e la loro funzione:
Adler32 |
Computa
la somma di controllo (checksum) di un data stream con l'algoritmo Adler-32. |
CheckedInputStream |
Un
input stream che mantiene la checksum dei dati letti. |
CheckedOutputStream |
Un
output stream che mantiene la checksum dei dati scritti. |
CRC32 |
Computa
la somma di controllo (checksum) di un data stream con l'algoritmo CRC-32. |
Deflater |
Fornisce
il supporto generico per la compressione con ZLIB. |
DeflaterOutputStream |
Implementa
un output stream filter per comprimere dati nel formato "deflate". |
GZIPInputStream |
Implementa
un input stream filter per leggere dati compressi nel formato GZIP. |
GZIPOutputStream |
Implementa
un output stream filter per scrivere dati compressi nel formato GZIP. |
Inflater |
Fornisce
il supporto generico per la decompressione con ZLIB. |
InflaterInputStream |
Implementa
un input stream filter per decomprimere dati nel formato "deflate". |
ZipEntry |
Rappresenta
una entry in un file ZIP. |
ZipFile |
Usata
per leggere le entry di un file ZIP. |
ZipInputStream |
Implementa
un input stream filter per decomprimere dati nel formato ZIP. |
ZipOutputStream |
Implementa
un output stream filter per scrivere dati compressi nel formato ZIP. |
Package java.util.jar
I
file JAR (Java ARchive), come i file CAB di ActiveX, sono un formato di
distribuzione del software basato sullo standard ZIP. Un classico uso di
un file JAR è la distribuzione di applet internamente a pagine HTML:
<applet
code=Animator.class
archive="classes.jar , images.jar , sounds.jar"
width=460 height=160>
<param name=foo value="bar">
</applet>
Le
seguenti classi sono derivate direttamente dalle corrispettive in java.util.zip:
JarEntry |
Rappresenta
una entry in un file JAR. (Derivata da ZipEntry) |
JarFile |
Usata
per leggere le entry di un file JAR ed accedere all'eventuale file manifest
(Derivata da ZipFile, può usare ogni file aperto con java.io.RandomAccessFile). |
JarInputStream |
Implementa
un input stream filter per decomprimere dati nel formato JAR. |
JarOutputStream |
Implementa
un output stream filter per scrivere dati compressi nel formato JAR. |
Un
file JAR può includere un file MANIFEST contenente una lista di
file presenti nel medesimo archivio JAR. Non tutti i file nell'archivio
debbono essere listati nel manifest; l'obbligo vale solo per i file che
debbono essere segnati. Il file manifest stesso non deve essere listato.
Il manifest può contenere informazioni di policy e di parametrizzazione
dell'applicazione distribuita, segnature digitali per la validazione del
contenuto, sezioni con informazioni per ogni file listato e, nel JDK 1.2,
ha delle nuove feature per supportare le Java Extentions che, a loro volta,
si basano sui file JAR.
Ecco
le classi di java.util.jar per manipolare il file manifest:
Attributes |
Mappa
i nomi degli attributi nel file Manifest ai volori delle stringhe ad essi
associate. |
Manifest |
Gestisce
i nomi delle entri ed i loro attributi. |
Conclusioni
C'è
una cosa da dire sulle classi di java.util: meglio averle che non averle.
Si possono rendere davvero utili in molte situazioni e, una volta assimilate
come strumenti standard, possono agevolare anche le fasi di design.
La
prossima volta concluderemo il ciclo dei package principali con java.net.
Infatti si può ben dire che le funzionalità di rete caratterizzano
la piattaforma Java al pari delle altre funzionalità di base fornite
dai package java.lang, java.io e java.util.
Risorse
-
Sotto
la foresta di Java. Un articolo dell'autore sull'ottimizzazione del
pattern Observer/Observable.
-
JDK
Documentation (JavaSoft website). Il sito ufficiale di documentazione
per il JDK 1.2
-
Changes
and Release Notes for the JDK 1.2 Software. Cambiamenti e novità
nell'ultima versione del JDK.
-
Collections
Framework
-
Collection
Class Changes in 1.2
-
Essential
Java Classes
-
Internationalization
Java
Platform 1.2 API Specification
|