Object
Questa
classe è la radice della gerarchia di classi Java essendo, per definizione,
l’ancestor di tutte le altre, comprese quelle implicite comegli
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 setDaemonsu
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:
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
Condivisi
da System e Runtime:
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
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:
File
: java.io.FilePermission
Socket
: java.net.SocketPermission
Net
: java.net.NetPermission
Security
: java.security.SecurityPermission
Runtime
: java.lang.RuntimePermission
Property
: java.util.PropertyPermission
AWT
: java.awt.AWTPermission
Il Class
Tree dei vari tipi di permesso basilari è il seguente:
java.security.Permission
java.io.FilePermission
java.net.SocketPermission
java.security.BasicPermission
java.net.NetPermission
java.security.SecurityPermission
java.lang.RuntimePermission
java.util.PropertyPermission
java.awt.AWTPermission
java.security.AllPermission
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
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.
1.1
Packages- java.lang, java.net, java.text, java.util, java.math
The
Java Class Libraries
The
Java Class Libraries-Second Edition, Vol. 1-1.2 Supplement
Security
Enhancements
Security
in JDK 1.2
Security
and Signed Applets
Fundamentals
of Java Security
Serialization
Enhancements
Reference
Objects, including weak references
Reference
Objects
Reflection
Essential
Java Classes
Concurrent
Programming in Java
|