MokaByte Numero 26 - Gennaio 1999    

   
  di Piergiuseppe Spinelli 
 
JDK: guida per gli esploratori
 
 
 

Parte una serie di piccole mappe per orientarsi nelle terre sconfinate del JDK.

 

Con l’inizio dell’anno 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 d’uso. 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 sull’uso 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 l’argomento di questa nuova serie.

 


Dalla funzione al componente

Linguaggi General-Purpose e Librerie di Funzioni

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 dall’hardware (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 dall’avere 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, l’idea di Oggetto, o megglio di Classe, intesa come definizione di tipo di dato comprensivo dell’insieme di operazioni (metodi) consentiti su di esso.

Classi, Package e Framework

Tutti i linguaggi ad oggetti sono, più o meno consciamente (per molti sarà un’eresia, 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 un’impostazione 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 un’applicazione che è una finestra

Definisco un menù dentro la finestra

Al limite, non c’è bisogno d’altro. 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).

Componenti, Collezioni, Object Model e Scripting

Per concludere, la new wave della programmazione, o se preferiamo, l’estrema 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 l’accesso 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 l’intero 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 l’attivazione di tale opzione.

L’esempio 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 dell’amministrazione 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.

Package di base

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 l’inclusione di librerie atte ad interfacciarsi con il sistema operativo ospite è votato all’insuccesso (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 nell’apposita 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 anch’esse 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 l’uso 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 un’interfaccia 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 tutt’altro 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 un’infinità 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 un’acchiata ai tre package che ogni programmatore Java, indipendentemente dal suo campo di interesse, dovrebbe conoscere in modo approfondito.

 


Java.lang

Iniziamo a tracciare questa guida all'esplorazione del JDK con il package usato da tutti i programmi Java.

Supporto al linguaggio

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, l’ancestor 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:

Object.notify(), Object.notifyAll(), Object.wait(…)

Insieme alla keyword syncronized, alla classe Thread ed all’interfaccia 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…).

Object.equals(), Object.clone(), Object.hashCode()

Il metodo equals implementa la relazione di equivalenza tra due oggetti appartenenti alla stessa classe. E’ importante ricordare che, in Java, l’operatore uguale di confronto ‘==’ non determina oggetti distinti con il medesimo contenuto ma solo se due distinte variabili reference puntano al medesimo oggetto. Insieme all’interfaccia Comparable, questo metodo virtuale (che necessità di essere riscritto per la maggior parte delle nuove classi), è l’unica 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 l’interfaccia Cloneable. E’ necessario tener presente che l’operatore Java uguale di assegnazione ‘=’ non copia un oggetto ma si limita ad assegnare ad una variabile reference l’indirizzo dell’oggetto bassegnato. L’esempio seguente può chiarire l’uso 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"); 

Object.toString()

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. L’operatore di concatenazione "+" utilizza implicitamente tale metodo.

Object.finalize()

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 dall’oggetto moribondo.

Object.getClass()

Restituisce la classe di un oggetto. L’identificazione run-time delle classi di oggetti accumunati, ad esempio, dalla stessa interfaccia ha numerosi tipici utilizzi in sistemi Object Oriented.

String, Thread, ThreadGroup, Runnable

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 dell’interfaccia 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.

Runnable.run()

Unico metodo dell’interfaccia 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 dell’oggetto 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();

Controllo dei thread

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 quest’ultimo, 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();
		}
	}
}

Variabili con visibilità locale ai thread

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 un’intera 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.

Comparable, Cloneable

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 l’eguaglianza (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.

java.io.Serializable

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 l’esecuzione in ambienti distribuiti, prima fra tutte RMI che consente di vedere come locali oggetti che, in realtà, sono eseguiti su altri computer.

Throwable, eccezioni ed errori di sistema

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 dell’istruzione throw o errore di esecuzione di un’istruzione) 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.

Eccezioni

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 l’eccezione venga lanciata piuttosto che controllare preventivamente il codice, come nell’esempio 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 è l’elenco 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

Errori

Gli errori sono anomalie talmente gravi che, normalmente, non ha senso cercare di catturarle. Gli errori sono spesso causati dall’hardware, dall’esaurimento 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

Wrappers

In Java, è risaputo, non esistono puntatori. Se questo fa piacere ad un primo pensiero, potrebbe creare difficoltà all’atto 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

Dynamic loading

Il caricamento dinamico delle classi è una delle caratteristiche principali di Java. Buona parte del lavoro viene svolto dal compilatore (javac), e dall’esecutore (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 un’intero 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.

Gestione dell’ambiente operativo

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.

System e Runtime

Le classi System e Runtime contengono proprietà e metodi per gli scopi più diversi. Ecco elencate alcune delle principali funzionalità offerte:

Process

E’ una classe ritornata dai metodi Runtime.exec e serve a rappresentare un’applicazione (normalmente non Java) lanciata in un processo dell’OS ospite. Consente di redirigere l’IO standard del processo rappresentato e di sincronizzarsi sulla sua terminazione.

Gestione della Sicurezza

La nascita di Java è coincisa con il boom dell’elaborazione 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; l’applicazione 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.

Gestione della memoria dinamica

Abbiamo già dato un’occhiata 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 all’uso 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 un’altra 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 dell’epoca e, a fronte di un effettivo aumento della velocità e dell’omogeneità di esecuzione, si ritornava, di fatto, ad una gestione manuale della memoria.

Il problema doveva esser ben noto alla SUN che, nell’ultimo JDK, ha aggiunto il package java.lang.ref che dovrebbe consentire di creare gestioni personalizzate dell’allocazione 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à.

Supporto alla Compilazione JIT

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.

Conclusioni

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 nell’offrire al programmatore quei meccanismi di base che ricorrono in quasi tutte le applicazioni.

Risorse

  1. JDK Documentation (JavaSoft website). Il sito ufficiale di documentazione per il JDK 1.2
  2. Changes and Release Notes for the JDK 1.2 Software. Cambiamenti e novità nell'ultima versione del JDK.
  3. 1.1 Packages- java.lang, java.net, java.text, java.util, java.math
  4. The Java Class Libraries
  5. The Java Class Libraries-Second Edition, Vol. 1-1.2 Supplement
  6. Security Enhancements
  7. Security in JDK 1.2
  8. Security and Signed Applets
  9. Fundamentals of Java Security
  10. Serialization Enhancements
  11. Reference Objects, including weak references
  12. Reference Objects
  13. Reflection
  14. Essential Java Classes
  15. Concurrent Programming in Java-