MokaByte Numero 26 - Gennaio 1999
di Piergiuseppe Spinelli |
|
Con linizio dellanno Mokabyte introduce questa serie di articoli incentrati sulle classi standard del JDK e, vista la quasi coincidenza con il rilascio della versione 1.2 (ma già ribattezzata Java 2), faremo riferimento ad essa ed eviteremo di soffermarci sulle classi ed i metodi dichiarati obsoleti e mantenuti per compatibilità con le precedenti versioni.
La documentazione ufficiale fornita da SUN è molto migliorata nel JDK1.2 ma, comunque, si presenta come un reference e non come un manuale duso. Con questa rubrica, quindi, non ci poniamo scopi di completezza o super-approfondimento (per quanto contiamo di dare qualche dritta dove capita); vogliamo, piuttosto, tracciare una mappa che consenta di navigare nell'oceano di classi e trovare rapidamente ciò che meglio si adatta ai nostri bisogni, che siano di calcolo, di comunicazione o di sicurezza, che richiedano di macinare stringhe o file, immagini o animazioni, compressioni o encriptaggi e, senza mettere limiti alla provvidenza, provare a scendere anche nei meandri dei domini di uso meno frequente e delle estensioni standard.
Questo primo articolo, prima di entrare nel vivo del package Java più usato, java.lang, presenta una breve panoramica sulluso delle librerie nei moderni linguaggi di programmazione. Si tratta di argomenti noti a molti ma che potrebbero aiutare qualche neofita di Java, magari già esperto di altri linguaggi come BASIC e C, ad inquadrare meglio largomento di questa nuova serie.
Inizialmente i linguaggi di terza generazione (FORTRAN, COBOL, BASIC, Pascal, etc ) non avevano una chiara linea di demarcazione tra linguaggio ed operazioni su specifici tipi di dati: tutto veniva fornito in un unico calderone sotto forma di parole del linguaggio; così nessuna distinzione era fatta tra print, shell, if, mid$, etc
Questo brodo primordiale si rivelò subito poco maneggievole e, ben presto, vennero fatte distinzioni tra le strutture del linguaggio (if, for, while etc ), dati di base (p.e. integer, string, float) e dati ed operazioni specifici di determinati domini (records, tipi utente, funzioni inerenti un certo tipo di dato).
In particolare il C Language modificò il modo di pensare dei programmatori che subito capirono la comodità di avere un linguaggio minimalista, che implementasse solo le strutture ed i paradigmi di base (puntatori, array, funzioni, regole di visibilità, dichiarazioni di tipo, etc ) ed i tipi di dati macchina, ovvero corrispondenti a quelli supportati direttamente dallhardware (interi, floating point, caratteri ed indirizzi). Si diffuse così il concetto di Function Library (in inglese biblioteca ma ormai universalmente tradotto come libreria di funzioni): con qualche difficoltà iniziale dei programmatori BASIC o Pascal, bisognava decidere i domini dei dato di interesse per ogni applicazione, includere gli specifici file di definizione (headers) e linkare le specifiche librerie.
Le funzioni venivano catalogate in modo che il programmatore non fosse costretto a linkare troppe funzioni non usate ma, al tempo stesso, non dovesse muoversi nella frammentazione di migliaia di librerie (solo in un secondo tempo i linker sarebbero diventati abbastanza intelligienti da scartare le funzioni non utilizzate). I criteri adottati per tale categorizzazione erano fondamentalmente due:
Venne individuato un insieme di operazioni, per quanto non incluse nel linguaggio, praticamente utilizzate in tutti i programmi: tali funzioni furono raggruppate in librerie standard: in particolare stdlib e stdio (per il C) fornivano da sole una potenza di fuoco quasi pari a quella dei linguaggi di tipo BASIC.
Le altre librerie raccoglievano operazioni accumunate dallavere per oggetto lo stesso tipo di dato (o un piccolo insieme di tipi correlati). Esempi di questo tipo sono math, string, file. A pensarci adesso, fu anche merito di questo tipo di raggruppamento se si fece largo, al di fuori degli ambienti accademici, lidea di Oggetto, o megglio di Classe, intesa come definizione di tipo di dato comprensivo dellinsieme di operazioni (metodi) consentiti su di esso.
Tutti i linguaggi ad oggetti sono, più o meno consciamente (per molti sarà uneresia, ma tantè), discendenti dalle filosofia dei moderni linguaggi General-Purpose. Ciò che intendo dire è che essi implementano i soli meccanismi di base, i tipi di dato elementari, le strutture di controllo e, in modo non dissimile dal buon vecchio linguaggio C, demandano le operazioni effettive a specifiche librerie di dominio, non Function Library, in questo caso, ma Class Library. Non solo C++, Objective-C o Java, che sono chiari eredi del C anche sotto il punto di vista sintattico, ma persino Smalltalk o Eifel, in fondo, hanno alla base questa impostazione.
In realtà le librerie di classi, partendo da tale base, vanno molto più in là. Se package Java come Math o classi come String possono essere visti come i corrispettivi delle omonime librerie C, altre, come Observer ed Observable, costituiscono delle alleaze allo scopo di implementare determinati patterns di uso frequente e non sono, quindi, accumunate dei medesimi dati ma, piuttosto, da un scopo comune. Tralasciando le varie liberie (STL, MFC, etc ) del C++ e limitandoci a Java, troviamo dei mastodontici esempi del genere in alcuni package (collezioni di classi java correlate: utilizzabile come sinonimo di Libreria di Classi). In particolare AWT e Swing: ci riferiamo a librerie di questo genere come framework.
Mentre le comuni Class library possono essere considerate come tools disponibili al bisogno per fare determinati lavori durante una elaborazione il cui corso viene definito dal programma che ne fa uso, i framework hanno unimpostazione esattamente opposta.
Quando creiamo, con Swing, una finestra con un menù, non scriviamo una riga di codice per programmare il loro comportamento; al massimo riscriviamo un metodo virtuale già esistente per modificare alcuni comportamenti standard predefiniti (tipicamente le azioni corrispondenti alle varie voci di menù). Il nostro approccio diviene, in buona parte, dichiarativo:
Definisco unapplicazione che è una finestra
Definisco un menù dentro la finestra
Al limite, non cè bisogno daltro. Il programma viene eseguito e la finestra si comporta come ci si attende debba comportarsi una finestra per bene, il menù si aprirà sotto al cursore ed eseguirà le azioni predefinite (normalmente azioni nulle). La differenza tra un framework ed una comune libreria di classi (per quanto entrambi siano definiti internamente a dei package) è quindi la seguente: le semplici librerie sono passive (implementano comportamenti su richiesta dei programmi) mentre i framework sono attivi (reagiscono in modo generico agli eventi esterni e si aspettano che i programmi si configurino come specializzazioni del comportamento standard da essi fornito).
Per concludere, la new wave della programmazione, o se preferiamo, lestrema conseguenza della filosofia ad oggetti, è data dal concetto di componente. Un componente è un insieme di classi che presenta alcune interfacce standard che consentono a vari linguaggi (anche diversi da quelli utilizzati per scrivere il componente) e tool visuali di navigare attraverso i metodi e le proprietà, scoprire i tipi di parametri e le convenzioni utilizzate e, infine, utilizzarli quando necessario.
Alla base dei componenti ci sono alcuni meccanismi determinanti. Prima di tutto quelli di interfaccia e di link dinamico: è importante che il sistema di sviluppo (p.e. VisualBasic o VisualAge) riconosca alcuni protocolli comuni con i componenti che utilizza. Ogni componente viene poi caricato in modo dinamico ed implementa, in modo diverso, i metodi definiti nel protocollo.
Prendiamo ad esempio un ipotetico componente per la gestione della stampa: chiamiamolo printing.
La sua interfaccia standard di componente (JavaBeans in Java, IUnknow per ActiveX, etc ) consente laccesso alle proprietà ed ai metodi da parte del tool di programmazione. Una possibile realizzazione è la seguente (metodi in verde, proprietà in rosso):
Non tutti i componenti presentano lintero insieme di interfacce: solo le stampanti laser consentono la scelta del cassetto di caricamento mentre solo quelle predisposte per il fronte retro avranno metodi per lattivazione di tale opzione.
Lesempio presenta la tipica struttura di un componente:
Si tratta, quindi, di una struttura gerarchica formata da oggetti che contengono collezioni ad altri oggetti e così via. Questa rappresentazione classica ad albero, che viene utilizzata comunemente per documentare la struttura di un componente, viene detta Object Model.
La forza di questo paradigma sta nel fatto che grandi applicazioni possono essere scomposte in un certo numero di componenti che interagiscono tra loro ed essere, quindi, descritte tramite uno specifico Object Model. In questo modo è possibile scrivere programmi che automatizzano il comportamento di altri programmi. A tale scopo si sono affermati linguaggi semplificati (p.e. senza type checking), detti di Scripting, facilmente utilizzabili dagli utenti finali (magari appena un po evoluti) che permettono di comporre pezzi di programmi diversi per nuovi scopi. Così, mentre voi professionisti sgobbate e scavate nei meandri del three tiers, degli Application Server ed altre amenità, il vostro collega dellamministrazione mette insieme, in mezza giornata, un grafico Excel ed una form HTML, scrive due righe in JavaScript e schiaffa il tutto su una pagina INTRANET che permette ai vari manager di calcolarsi al volo qualche dato di vitale importanza, guadagnando il plauso generale e qualche segreto anatema da parte vostra.
Se avete programmato in C, sarà diventato un automatismo quello di cominciare i vostri sorgenti con
#include <stdio.h>
#include <stdlib.h>
o, in ambiente Windows, con
#include <windows.h>
Ogni tentativo di scrivere programmi C senza linclusione di librerie atte ad interfacciarsi con il sistema operativo ospite è votato allinsuccesso (almeno che non siate esperti nel produrre software di base).
Se invece avete più dimestichezza con il VisualBasic, troverete normale cominciare a scrivere una subroutine dichiarando variabili di vari tipi, anche non elementari come collection e control, e immediatamente agire su di esse tramite i relativi metodi. Se, però, dovete usare componenti più complessi, come ad esempio un grafico Excel, siete obbligati ad andare nellapposita lista dei references per includere le classi necessarie. Nel fare questa operazione scoprirete di avere alcuni reference già collegati implicitamente dal sistema, alcuni addirittura obbligatori. Ecco svelata la magia delle classi standard di VB: sono anchesse delle librerie esterne ma collegate per default.
In Java accade qualcosa di molto simile: il compilatore, javac, provvede al link automatico del package java.lang. Nulla ci impedisce di dichiarare luso di tale package in modo esplicito, con:
import java.lang.*;
Anche se tale dichiarazione viene omessa, Java non può fare a meno delle classi contenute in tale package in quanto, molte di esse, sono da supporto ai meccanismi del linguaggio. Basti pensare che, per definizione, tutte le classi Java derivano da Object che è definita in java.lang.
Per quanto riguarda la grana delle sue librerie, Java è assai più modulare di VisualBasic e, per poter disporre di un volume di funzionalità paragonabile a quello della sola libreria standard VB, è necessario dichiarare, come minimo, i package java.util, java.io, java.math etc Se, inoltre, i nostri programmi devono disporre di uninterfaccia grafica, sara necessario ricorrere ad uno o più package, scegliando ad esempio tra java.awt, e java.swing; già la possibilità di fare questo tipo di scelte rappresenta un vantaggio rispetto al VB che, invece, deve portarsi appresso forzatamente un bagaglio standard tuttaltro che leggero.
Proseguendo con i paragoni, sia Java che VB collegano le proprie librerie in modo dinamico, potendo scegliere i componenti effettivi da usare anche a run-time. Tuttavia Java estrae dai suoi package le sole classi effettivamente utilizzate mediante un meccanismo di indirizzamento basato sui nomi e, fondamentalmente, simile a quello che consente di rintracciare archivi in un file system a partire dalla radice; VB, invece, effettua il collegamento con ogni classe in base ad un ID univoco che deve essere memorizzato nel registry di Windows. Probabilmente al fine di non sovraccaricare il registy con uninfinità di entry, la grana dei componenti collegati è solitamente abbastanza grande (spesso pari a diverse decine di classi) e, di conseguenza, un programma VB è spesso costretto ad inglobare un gran numero di funzionalità superflue ai suoi scopi.
Al termine di questa panoramica, che spero sia stata di qualche utilità per i Java Beginners prevenienti da altri ambienti di sviluppo, iniziamo a dare unacchiata ai tre package che ogni programmatore Java, indipendentemente dal suo campo di interesse, dovrebbe conoscere in modo approfondito.
Iniziamo a tracciare questa guida all'esplorazione del JDK con il package usato da tutti i programmi Java.
Sono le classi e le interfaccie senza le quali Java, semplicemente, non funziona. Molte sono di uso frequente, altre vengono utilizzate internamente dal linguaggio e da altre classi standard:
Object
Questa classe è la radice della gerarchia di classi Java essendo, per definizione, lancestor di tutte le altre, comprese quelle implicite come gli array. I metodi di Object sono fondamentali per un motivo banale: essi sono ereditati da tutte le altre classi; questo dovrebbe essere un buon motivo per familiarizzare con essi, ecco i principali:
Insieme alla keyword syncronized, alla classe Thread ed allinterfaccia Runnable costituiscono un set completo di primitive di multitascking con possibilità di implementare (cè un teorema, da qualche parte ) tutte le altre primitive classiche (regioni critiche, semafori, monitors, messaggi etc ).
Il metodo equals implementa la relazione di equivalenza tra due oggetti appartenenti alla stessa classe. E importante ricordare che, in Java, loperatore uguale di confronto == non determina oggetti distinti con il medesimo contenuto ma solo se due distinte variabili reference puntano al medesimo oggetto. Insieme allinterfaccia Comparable, questo metodo virtuale (che necessità di essere riscritto per la maggior parte delle nuove classi), è lunica base per il confronto di contenuto tra oggetti.
Il metodo hashCode garantisce di ritornare un intero (calcolato con un algoritmo di hash) sempre uguale nel corso della stessa elaborazione. Se due oggetti sono uguali secondo il metodo equals, hashCode restituisce lo stesso intero per entrambi. La classe java.util.Hashtable utilizza questo metodo.
Il metodo virtuale clone è pensato per essere implementato in ogni classe che necessiti della funzione di duplicazione. Tale funzionalità deve essere esplicitata dichiarando che le classi interessate implementano linterfaccia Cloneable. E necessario tener presente che loperatore Java uguale di assegnazione = non copia un oggetto ma si limita ad assegnare ad una variabile reference lindirizzo delloggetto bassegnato. Lesempio seguente può chiarire luso di queste due metodi fondamentali:
MyClass a=new MyClass("X"); MyClass b=a;
if(a==b) System.out.println("A==B"); //stampa: A==B else System.out.println("A!=B");
if(a.equals(b)) System.out.println("A equals B"); else System.out.println("A not equals B"); //stampa: A not equals B
b=a.clone();
if(a==b) System.out.println("A==B"); else System.out.println("A!=B"); //stampa: A!=B
if(a.equals(b)) System.out.println("A equals B"); //stampa: A equals B else System.out.println("A not equals B");
Ogni classe Java ha un metodo, ereditato da Object, che ritorna una sua rappresentazione sotto forma di stringa. Tale metodo viene spesso reimplementato per adattarsi alla specifica classe. Loperatore di concatenazione "+" utilizza implicitamente tale metodo.
Metodo chiamato dalla JVM prima che il garbage collector reclami la memoria di un oggetto non più referenziato. Si può implementare per le classi derivate da Object al fine di chiudere connessioni, file o sistemare qualsiasi altra questione lasciata in sospeso dalloggetto moribondo.
Restituisce la classe di un oggetto. Lidentificazione run-time delle classi di oggetti accumunati, ad esempio, dalla stessa interfaccia ha numerosi tipici utilizzi in sistemi Object Oriented.
Queste classi sono accumunate dal fungere da sostegno per alcuni meccanismi nativi del linguaggio Java. Il tipo String, ad esempio, è usato quasi come un tipo elementare, tanto che esiste un operatore, il + di concatenamento, che lavora su di esso. Tuttavia tale tipo è definito come una normale classe, al pari di tante altre, derivata da Object e la sua definizione nel package standard java.lang garantisce la sua presenza in ogni programma.
Stesso discorso vale per i thread che sono un meccanismo di base in Java ma che hanno bisogno dellinterfaccia Runnable e della classe Thread per poter essere acceduti dai programmi.
La classe ThreadGroup è una utilità per trattare un insieme di thread con caratteritiche comuni (ad esempio impostando setDaemon su un gruppo equivale a farlo su tutti i thread che ne fanno parte).
Non è il caso, in questa sede, di elencare tutti i metodi applicabili alle stringhe: per questo ci si può affidare alla documentazione on-line del JDK. Vale invece la pena di dare qualche ragguaglio sulle principali funzionalità fornite per governare il multithreading.
Unico metodo dellinterfaccia Runnable, contiene il codice eseguito nelle classi derivate da Runnable. Anche la classe Thread implementa Runnable e, per default, il suo metodo run chiama il methodo run delloggetto di tipo Runnable con cui viene inizializzato il thread:
class myCode implements Runnable { public void run(){ } }
Thread myThread = new Thread(new myCode());
E comunque possibile sovrascivere direttamente il metodo run della classe Thread:
Class myCode extends Thread{ Public void run(){ } }
MyThread myThread=new MyThread();
Un processo leggero, in Java, inizia la propria esecuzione solo dopo essere stato attivato con start, può cedere CPU ad altri task con yield, sospendersi per un certo tempo con sleep, venire marcato come interrotto con interrupt, attendere la fine esecuzione di un altro thread chiamando il metodo join di questultimo, impostare una priorità che, nella maggior parte delle JVM, non è bloccante e dichiararsi daemon. Il comportamento dei thread demoni è assolutamente analogo a quello degli altri thread tranne che la JVM termina la propria esecuzione quando tutti i thread ancora vivi sono di tipo daemon.
Molti metodi, inizialmente parte di questo gruppo, sono stati deprecati nelle versioni successive in quanto portavano possibili problemi di sincronizzazione, di pulizia o addirittura di blocco critico. Tra questi ricordiamo stop, suspend e resume. Un possibile metodo per governare la terminazione di un thread ad esecuzione ciclica (ovvero il classico task che contiene una grande ciclo while nel proprio metodo run) è il seguente:
class MyThread extends Thread{ private Thread t=null; boolean inChiusura=false;
public synchronized boolean Inizia(){ if(inChiusura) wait(); if(t!=null) return false; t=this; stillRunning=true; start(); return true; }
public synchronized boolean Termina(){ if(t==null) return false; inChiusura=true; t=null; return true; }
public void run(){ //Inizializzazioni synchronized(this){ ... }
//Loop principale del thread while(Thread.getCurrentThread()==t){ }
//Finalizzazioni syncronized(this){ ... inChiusura=false; notifyAll(); } } }
Una delle carenze del meccanismo standard di visibilità delle variabili è che esso non tiene affatto presente il concetto di thread. E quindi possibile dichiarare una variabile locale ad un oggetto oppure ad unintera classe ma non a tutti gli oggetti che sono eseguiti nello stesso thread. Per raggiungere questo scopo è stata creata la class ThreadLocal, con i metodi inizialize, get e set. Le variabili dichiarate di questo tipo (normalmente delle static di classe) gestiscono internamente un oggetto diverso per ogni diverso Thread, indipendentemente dalla classe in cui sono dichiarate.
Queste interfacce consentono lo svolgimento di alcune operazioni basilari per ogni linguaggio: ordinamento e copia. Un oggetto di una classe che non implementa tali interfacce può essere testato per leguaglianza (mediante il metodo equals di Object) ma non è possibile determinare se è maggiore o minore di un altro oggetto né creare una copia identica dello stesso.
Attenzione: questa interfaccia è un infiltrato in quanto non appartiene a java.lang e, per poterla usare, è necessario importare il package java.io di cui si parlerà prossimamente. Il motivo per cui ne parlo qui è che anche Serializable appartiene ai meccanismi di base di Java e, infatti, esiste una keyword del linguaggio, transient, il cui uso ha senso solo per le classi che implementano Serializable per dichiarare la prorpia disponibilità a generare oggetti permanenti, ovvero registrabili su un qualsiasi supporto persistente (p.e. file o database) o anche ad essere inviate ad un sistema remoto tramite socket. E importante capire bene il meccanismo di serializzazione in quanto esso è alla base delle tecniche Java più avanzate per lesecuzione in ambienti distribuiti, prima fra tutte RMI che consente di vedere come locali oggetti che, in realtà, sono eseguiti su altri computer.
Il lancio di un oggetto è una caratteristica interna di un linguaggio. Un oggetto viene scagliato con, allegata, una istantanea della situazione dello stack scattata in modo sincrono (esecuzione dellistruzione throw o errore di esecuzione di unistruzione) o asincrono dalla JVM per cause esterne. Il lancio deve essere acchiappato da apposite istruzioni nel blocco in esecuzione. Se la funzione in esecuzione non ha istruzioni per la cattura di un lancio (try{ }catch( ){ }), essa viene terminata forzatamente e il controllo torna alla funzione chiamante; se anche questa non ha istruzioni di cattura, il lancio si propaga risalendo tutto lo stack delle chiamate fino ad incontrare una funzione predisposta per la cattura o, in caso negativo, terminare il programma.
La classe Throwable non è utilizzata direttamente molto frequentemente mentre le due classi derivate, Error ed Exception sono, a loro volta, la radice di tutte le eccezioni ed errori generati da tutte le classi Java.
Le eccezioni sono anomalie di esecuzione o semplici condizioni al di fuori del normale flusso di un programma. E normale per un buon programma cercare di catturare tutte le eccezioni che si presume possano venir sollevate dai metodi chiamati e, a volte, può essere considerata una buona pratica lasciare che leccezione venga lanciata piuttosto che controllare preventivamente il codice, come nellesempio seguente:
Conteggio con controllo a condizione |
Conteggio con controllo ad eccezione |
final int max=100; int[] arr=new int[max]; for(int i=0;i<max;i++){ arr[i]=generaNumero(i); } stampaArray(arr); |
int[] arr=new int[100]; try{ for(int i=0;;i++){ arr[i]=generaNumero(i); } }catch(ArrayIndexOutOfBoundsException e){} stampaArray(arr); |
Quello che segue è lelenco di tutte le eccezioni definite in java.lang che riguardano tutte le situazioni anomale che possono venire generate dalla JVM:
ArithmeticException
ArrayIndexOutOfBoundsException
ArrayStoreException
ClassCastException
ClassNotFoundException
CloneNotSupportedException
Exception
IllegalAccessException
IllegalArgumentException
IllegalMonitorStateException
IllegalStateException
IllegalThreadStateException
IndexOutOfBoundsException
InstantiationException
InterruptedException
NegativeArraySizeException
NoSuchFieldException
NoSuchMethodException
NullPointerException
NumberFormatException
RuntimeException
SecurityException
StringIndexOutOfBoundsException
UnsupportedOperationException
Gli errori sono anomalie talmente gravi che, normalmente, non ha senso cercare di catturarle. Gli errori sono spesso causati dallhardware, dallesaurimento di risorse come la memoria, dalla corruzione dei file di classe o da strane situazioni, come classi modificati dopo essere state collegate ad altre.
Ecco tutti gli errori riconosciuti in Java e definiti in java.lang:
AbstractMethodError
ClassCircularityError
ClassFormatError
Error
ExceptionInInitializerError
IllegalAccessError
IncompatibleClassChangeError
InstantiationError
InternalError
LinkageError
NoClassDefFoundError
NoSuchFieldError
NoSuchMethodError
OutOfMemoryError
StackOverflowError
ThreadDeath
UnknownError
UnsatisfiedLinkError
VerifyError
VirtualMachineError
In Java, è risaputo, non esistono puntatori. Se questo fa piacere ad un primo pensiero, potrebbe creare difficoltà allatto pratico: visto che che tutti i parametri sono passati per valore e non posso passare il puntatore ad un intero, come posso far sì che un metodo ritorni i propri parametri numerici modificati?
Ecco dove entrano in scena le classi wrapper, ovvero classi corrispondenti ai tipi di base. Oltre che fungere da pacchetti di trasporto per i parametri in uscita, i wrapper forniscono metodi per la formattazione e la conversione di tipo. Tutti i wrapper sono definiti in java.lang:
Boolean
Byte
Character
Double
Float
Integer
Long
Number
Short
Void
Il caricamento dinamico delle classi è una delle caratteristiche principali di Java. Buona parte del lavoro viene svolto dal compilatore (javac), e dallesecutore (java). In molti casi, comunque, le classi da caricare sono conosciute solo a tempo di esecuzione e per questo motivo java.lang mette a disposizione le primitive per caricare classi, anche da sistemi remoti, interrogarle per conoscerne i contenuti e accedere metodi e proprietà pubbliche.
Anche un semplice elenco di tutti i metodi che concorrono a questa gestione richiederebbe unintero articolo. Basti sapere che le classi Package, Class, ClassLoader, Array, ed i package java.lang.reflect e java.lang.ref, consentono di fare qualsiasi cosa ci venga in mente, se consentita dal securityManager attivo, anche creare una classe nuova partendo dal suo byte code!!
La cosa importante da notare è che questo meccanismo di caricamento dinamico fa sì che, implicitamente, ogni classe ed applicazione Java sia un componente utilizzabile da altri programmi. La specifica JavaBean, infatti, si limita a fornire una convenzione sui nomi da utilizzare per far riconoscere le proprietà pubbliche ai tool di sviluppo visuale e a dare un insieme di classi di utilità per realizzare agevolmente ciò che SUN ha denominato introspezione: queste classi sono scritte in pure Java e, quindi, non è stato aggiunto alcun nuovo meccanismo del linguaggio per la creazione di componenti.
Tutti i linguaggi hanno bisogno di interfacciarsi al sistema operativo ospite. Java, in particolare, offre un set di classi che cercano di mascherare, per quanto possibile, le differenze tra gli ambienti più eterogenei in cui un programma Java può essere eseguito.
Le classi System e Runtime contengono proprietà e metodi per gli scopi più diversi. Ecco elencate alcune delle principali funzionalità offerte:
- Copia veloce di array con arraycopy
- Lettura del clock di sistema, in millisecondi, con currentTimeMillis
- Interazione con le variabili di ambiente: setProperty, setProperties, getProperty, getProperties
- Accesso all IO standard di console con le proprietà in, out e err e loro redirezione con setIn, setOut e setErr
- Impostazione dei livelli di sicurezza con setSecurityManager
- Esecuzione di processi nativi con exec
- Controllo della memoria con freeMemory, totalMemory, waitForMemoryAdvice
- Supporto al debug ed al profiling con traceInstructions e traceMethodCalls
- Terminazione della JVM con exit
- Forzatura del garbage collector con gc e della finalizzazione con runFinalization
- Supporto alle classi con metodi nativi tramite load e loadLibrary
E una classe ritornata dai metodi Runtime.exec e serve a rappresentare unapplicazione (normalmente non Java) lanciata in un processo dellOS ospite. Consente di redirigere lIO standard del processo rappresentato e di sincronizzarsi sulla sua terminazione.
La nascita di Java è coincisa con il boom dellelaborazione distribuita, in particolare quella basata sul paradigma INTERNET, tanto che per un lungo periodo il linguaggio della SUN è stato identificato un po da tutti come IL linguaggio di INTERNET. E normale che, trattando di ambienti distribuiti, il problema della sicurezza sia centrale in ogni aspetto del linguaggio. Ciò è confermato dal fatto che le classi ed i metodi inerenti la Security sono stati cambiati praticamente in tutte le successive release del linguaggio, evolvendo da un modello fortemente restrittivo a quello del JDK 1.2, estremamente potente e configurabile ma, proprio per questo, tanto complesso da richiede interi documenti dedicati solo a questo argomento (vedi Risorse).
Il package java.lang contiene alcuni meccanismi basilari per il controllo di sicurezza.
Come si è detto, la classe System consente di impostare una classa derivata da SecurityManager; lapplicazione potrà poi chiamare il metodo checkPermission per testare la fattibilità delle sensitive operations tenenedo anche presente il ClassLoader con cui ogni classe è stata caricata (classi scaricate da alcuni siti, per esempio, potrebbero avere meno privilegi di quelle caricate in locale), ed il SecurityContext che può dipendere da molti fattori (come, ad esempio, il thread in esecuzione).
I tipi di permosso testabili possono appartenere ad una delle seguenti categorie, ognua delle quali corrisponde ad una particalare classe:
Il Class Tree dei vari tipi di permesso basilari è il seguente:
Alcune classi derivate da Permission hanno specifici metodi per controllare i permessi di particolari operazioni, come la lettura e/o scrittura di file.
A questo meccanismo, già di per sé non semplice, si aggiungono livelli assai più articolati di controllo, come liste di ACL, Chiavi pubbliche e private, certificati ed altro, gestiti dal package java.security e dai suoi sotto-package: acl, cert, interfaces e spec.
Abbiamo già dato unocchiata alle classi Runtime e System ed ai loro metodi per la gestione della memoria. Uno dei più pubblicizzati punti di forza di Java è il meccanismo automatico di gestione della memoria dinamica, basato sul lavoro del Garbage Collector. E certamente vero che la maggior parte degli errori nel software professionale è dovuto alluso dei puntatori ed alla mancata restituzione della memoria allocata (ma questo, se lavorate sotto Windows, già lo sapete!!!). Tuttavia, anche se il meccanismo di garbage collection è comodissimo, esso porta a dei naturali abusi: non solo i programmi utente, ma tutto il JDK e pieno di righe come la seguente:
for(i=0;i<100000;i++) x[i] = new ClasseX(new ClasseY(new String("ABC")));
Anche se non ci si presenterà il problema di liberare la memoria allocata, il nostro programma andrà soggetto a frequenti mancamenti dovuti alla necessità del JVM di riordinare la memoria non più referenziata per renderla riutilizzabile. Tali blocchi momentanei possono essere solo fastidiosi nel programmi interattivi ma assolutamente inaccettabili in altri contesti, come ul riproduttore di filmati o un programma di controllo di processi industriali.
In un mio precedente articolo, PERC, la via dura per Java Real-Time, riportavo i risultati di una piccola ricerca svolta per confrontare le performances di un normale programma che si affidava al rilascio automatico della memoria con unaltra versione che creava una sorta di heap privata ed un meccanismo simile alle alloc/free del C. Era tutto ciò che si poteva fare con il JDK dellepoca e, a fronte di un effettivo aumento della velocità e dellomogeneità di esecuzione, si ritornava, di fatto, ad una gestione manuale della memoria.
Il problema doveva esser ben noto alla SUN che, nellultimo JDK, ha aggiunto il package java.lang.ref che dovrebbe consentire di creare gestioni personalizzate dellallocazione e rilascio della memoria conservando gli automatismi nativi di Java. In particolare, la nuova classe SoftReference consente di mantenere delle cache di oggetti, che possono essere riutilizzati senza riallocazione ma anche reclamati dal Garbage Collector in caso di necesità.
La classe Compiler, infine, è (incredibile) final di nome ed abstract di fatto!
In realtà essa contiene dei metodi che non fanno nulla ma fungona da interfaccia per il meccanismo di compilazione al volo usata dai Just In Time compiler (JIT).
Se la JVM, al proprio lancio, trova la proprietà di sistema java.compiler, la utilizza per individuare una libreria dinamica (p.e. una DLL Windows) che implementa le funzioni per la compilazione delle classi Java nel linguaggio macchina del sistema ospite. Tali funzioni verranno chiamate attraverso i metodi esposti dalla classe Compiler.
Nei moderni linguaggi Object Oriented esiste un labile confine tra il linguaggio stesso ed alcune Classi di base senza le quali il linguaggio stesso non potrebbe funzionare. In Java la maggior parte di tali classi sono racchiuse nel package java.lang.
Prossimamente parleremo di altri due package, java.io e java.util, che, se non basilari, sono altrettanto importanti nelloffrire al programmatore quei meccanismi di base che ricorrono in quasi tutte le applicazioni.