MokaByte Numero 28 - Marzo 1999    

   
  di Piergiuseppe Spinelli 
 
JDK: java.util
 
 
 

Il buon vecchio Kit del Programmatore Java è cresciuto fino a comprendere un framework per le collezioni e altre aggiunte.

Gli strumenti più utili sono quelli per la mente. Non solo pattern, astrazioni e modelli, ma anche più semplicemente classi predefinite per paradigmi ed algoritmi di uso frequente che, insieme, formano un bagaglio che senza esagerazioni definirei culturale.

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 Observable che 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 Date rappresenta 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 TimeZone e, in particolare, SimpleTimeZone che 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 Locale serve 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 PropertyResourceBundle per 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:

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:

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:

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 Comparator oppure 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:

Altri metodi sono propri di particolari interfacce:

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:

Sono state mantenute le classi legacy (presenti dagli arbori del JDK): Dictionary, Hashtable, Vector e 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à:

  1. 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.
  2. Se, per qualche strano motivo, nessuna classe standard fornisse una buona base, partire da una classe astratta
  3. Solo nei casi più disperati implementare direttamente dalle interfacce Set, List o Map
  4. Non datemi un dolore ed evitate di partire implementando direttamente Collection!

Infine esistono due classi di utilità, con metodi statici:

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

  1. Sotto la foresta di Java. Un articolo dell'autore sull'ottimizzazione del pattern Observer/Observable.
  2. JDK Documentation (JavaSoft website). Il sito ufficiale di documentazione per il JDK 1.2
  3. Changes and Release Notes for the JDK 1.2 Software. Cambiamenti e novità nell'ultima versione del JDK.
  4. Collections Framework
  5. Collection Class Changes in 1.2
  6. Essential Java Classes
  7. Internationalization
  8. Java Platform 1.2 API Specification

 

 

1