![]() di Fabrizio Giudici |
Java
e C++
seconda parte |
Dopo aver esaminato le caratteristiche più generiche del linguaggio Java, in questo articolo parleremo finalmente di programmazione ad oggetti. Vedremo quali sono i paradigmi di programmazione supportati, che differenze ci sono rispetto al C++... ed ancora una volta come Java semplifichi la vita al programmatore in modo sostanziale, anche se le soluzioni che adotta non piacciono a tutti...
Da buon linguaggio
orientato agli oggetti, Java supporta tutti i costrutti più importanti,
come information hiding ("mascheramento" dei componenti interni
di un oggetto per ottenere una buon separazione tra interfaccia ed implementazione),
ereditarietà (capacità di creare un nuovo tipo di oggetto
prendendo come base un tipo pre-esistente), polimorfismo (capacità
di definire diversi comportamenti di risposta relativi a stimoli esterni,
a seconda del tipo di ciascun oggetto). Tutto esattamente come il C++...
almeno a prima vista. Incominciamo dall'information hiding. In C++ si possono
specificare tre diverse modalità di accesso ai membri di una classe:
private:per i membri che devono
essere visibili solo all'interno della classe;
protected: per i membri che possono
essere visibili anche a sottoclassi, ma non all'esterno;
public:per i membri che possono
essere visibili sempre.
In C++ i membri non public possono essere resi accessibili a classi qualsiasi
se esse vengono dichiarate friend della classe che li contiene. La friendship
di classi ha sempre suscitato polemiche, un po' come per il goto nei linguaggi
strutturati. C'è chi dice che non serve ed anzi è dannosa,
mentre altri la difendono. Apparentemente la friendship è una violazione
al principio dell'information hiding, perché è un modo per
"forzare" le regole che ci si è imposte. Questo è
vero se se ne fa un abuso, ma considerate questo esempio (in C++) in cui
una classe Collection può contenere un insieme di interi:
class Collection { private: int a[20]; public: inline void addElement (int x) { /* aggiunge un elemento */ } };
Se
non si vuole rivelare come viene implementata (con un array come nell'esempio
oppure con una lista linkata) è possibile evitare di usare metodi
per l'accesso diretto agli elementi contenuti, ma usare una seconda classe
specializzata, detta enumeratore o iteratore:
class Enumerator { private: Collection &c; public: inline Enumerator (Collection &cc) { c = cc }; inline int next(){ /* accede agli elementi di Collection */ } inline int end() { /* restituisce 1 quando si arriva in fondo */ } }; ... Collection c; for (Enumerator e(c);!e.end(); ) cout << e.next() << "\n";
Il ciclo for accede serialmente a tutti gli elementi di c in modo completamente indipendente dall'implementazione di Collection. Il problema è evidente: Collection deve garantire solo ad Enumerator l'accesso ai suoi membri, ma Enumerator non è una sottoclasse di Collection. L'uso della friendship è quindi necessario, però il programmatore deve assicurarsi di usarla correttamente, senza abusarne per "mettere delle pezze" a problemi derivanti da cattiva progettazione.
I
modi di accesso in Java sono simili al C++, con qualche piccola, ma significativa,
differenza. I modi private e public conservano il loro significato, mentre
protected vuol dire una cosa leggermente diversa. Esiste poi un ulteriore
modo, che può essere chiamato package friend, che permette l'accesso
a tutte e sole le classi che appartengono allo stesso package, anche se
non sono direttamente sottoclassi. Questo limita un po' i danni della friendship:
è logico attendersi che le classi dentro uno stesso package abbiano
delle affinità tra di loro per cui concedere loro l'accesso ad alcuni
membri non dovrebbe essere troppo pericoloso. Curiosamente non esiste nessuna
parola chiave per questo modo, che viene assunto per default se non viene
esplicitamente specificato nient'altro (questa è un po' una contraddizione
con i principi generali di chiara leggibilità dei sorgenti: tutto
sommato una parola chiave package friend non avrebbe guastato...). Chi
legge documentazione obsoleta potrà trovare accenno ad un modo chiamato
"private protected" che però è stato eliminato
dalla versione più recente del linguaggio.
Ereditarietà
multipla
Vediamo ora il secondo problema: l'ereditarietà multipla. Supponiamo di avere una classe A da cui deriviamo B1 e B2 ed una quarta classe C che eredita sia da B1 che da B2, come in figura:
class A { int a; }; [ a ] class B1 : public A { int b1; }; [ a | b1 ] class B2 : public A { int b2; }; [ a | b2 ] class C : public B1, public B2 { int c; }; [ a | b1 | a | b2 ]
Quante
variabili a sono contenute in C? Ricordando come viene definito il layout
in memoria delle classi C++ e la risposta è immediata: due (si veda
ancora la figura). Ora è vero che un puntatore a C è anche
un valido puntatore sia a B1 che ad A, ma non lo è per B2! Per ottenere
un valido puntatore a B2 bisogna aggiungere un offset per "saltare"
la prima variabile a e b1. Un moderno compilatore C++ è in grado
di accorgersene... ma se facciamo maldestramente un po' di aritmetica di
puntatori per conto nostro facilmente commetteremo errori disastrosi. E
non basta! Avere due copie della variabile a in C può andare bene
o no a seconda di quello che stiamo facendo. Usando la cosiddetta ereditarietà
virtuale possiamo eliminare la seconda copia:
class A { int a; }; [ a ] class B1 : public A { int b1; }; [ a | b1 ] class B2 : public A { int b2; }; [ a | b2 ] class C : public B1, virtual public B2 { int c; }; [ a | b1 | b2 ]
Però adesso non è più neanche possibile ottenere un
valido puntatore a B2! La situazione cambia a seconda dell'ordine di ereditarietà
e di quale classe è ereditata con virtual public. Insomma, una situazione
piuttosto complessa che va tenuta sotto controllo con molta attenzione.
Anche per risolvere questi problemi Java è molto drastico: semplicemente
viene proibita qualsiasi forma di ereditarietà multipla! Però
l'ereditarietà multipla certamente aiuta a modellare meglio il mondo
reale. Pensiamo per esempio alla definizione di un metodo per stampare
un oggetto: in C++ possiamo dichiarare una classe base Printable come segue:
class Printable { virtual void print(); }
Per qualsiasi classe Derived derivata da una classe Base possiamo fare così:
class Derived : public Base, virtual public Printable { ... };
ed il gioco è fatto. Java non permette ereditarietà multipla, ma fortunatamente offre un'alternativa: le interfacce (intefaces). Il concetto è il seguente: la proprietà di essere stampabile è considerata troppo generica perché abbia senso legare più classi con relazioni di ereditarietà, pertanto ci limitiamo ad indicare semplicemente che esse possiedono tutte un metodo con lo stesso prototipo (e probabilmente stessa semantica). Ecco come fare:
interface Printable { void print(); } class Derived extends Base implements Printable { void print() {...} } class Derived2 extends Base2 implements Printable { void print() {...} }
void DoSomething (Printable pr) { pr.print() }
Printable
si limita a dichiarare il metodo print(), ma non specifica alcun codice
per esso. Notate la clausola implements per segnalare che Derived implementa
tutti i metodi di Printable. Come mostrato dal metodo DoSomething(), è
possibile eseguire il metodo print() per qualsiasi classe che implementi
Printable, ma tutte queste classi non necessitano di essere imparentate
tra di loro. Questo meccanismo può apparire molto simile a definire
una classe astratta Printable in C++, ma la realtà è molto
diversa (anche se in alcune circostanze con ereditarietà multipla
di classi astratte si può fare effettivamente qualcosa di simile).
La differenza fondamentale è che Printable proprio non esiste come
oggetto: non possiamo definire variabili membro né specificare codice
per i suoi metodi. Stiamo semplicemente dicendo che sia Derived e Derived2
possiedono un metodo con lo stesso nome, poi ci penserà Java, durante
l'esecuzione del programma, a cercare l'indirizzo del metodo giusto, volta
per volta. La differenza più profonda è che le classi che
implementano le interfacce devono esplicitamente definire il codice per
i metodi implementati. Questo può voler dire scrivere qualche riga
di codice in più, minimizzabile delegando ad una classe di appoggio
esterna l'implementazione:
interface Printable { void print(); } class PrintableObject implements Printable { void print() {...} } class Derived extends Base implements Printable { PrintableObject pobj = new PrintableObject(); void print() { pobj->print(); } } class Derived2 extends Base2 implements Printable { PrintableObject pobj = new PrintableObject(); void print() { pobj->print(); } }
Le
interfacce esistono in Objective C (un C con oggetti, molto legato alla
piattaforma Next ed al suo sistema operativo NextStep, di cui ha condiviso
la poca fortuna) e sono state proposte per il nuovo ANSI/ISO C++. Lo GNU
C le implementa, per il momento come estensione allo standard attuale,
con il nome di signatures.
Gestione degli oggetti: niente più puntatori
In C++ esistono ben tre modi diversi per riferirsi ad un oggetto: per valore, per puntatore e per reference
class Object { void doSomething(); {...} };
Object value; Object *pointer = new Object(); Object &reference = value; ... value.doSomething(); pointer-> doSomething(); reference.doSomething();
In
Java ne esiste uno solo: per reference (che ha un significato diverso che
in C++):
Object reference = new Object(); reference.doSomething();
Come si vede, il reference è una via di mezzo tra il puntatore (richiede allocazione esplicita con new) ed il modo per valore (si usa il punto "." e non la freccia "->"): Da un punto di vista implementativo certamente il reference è di fatto un puntatore, perché contiene l'indirizzo dell'oggetto a cui "punta"; ma, a differenza dei puntatori tradizionali, non è possibile conoscere tale indirizzo. In questo modo è completamente abolita qualsiasi forma di aritmetica dei puntatori, una delle caratteristiche più "pericolose" del C/C++. Questa "protezione" da parte di Java è addirittura estesa al livello del codice macchina. Se si va a leggere la documentazione della Java Virtual Machine si scopre che tutte le istruzioni sono fortemente typed, cioè conoscono il tipo esatto degli operandi con cui hanno a che fare. Non esistono assolutamente modi di convertire gli indirizzi espressi dai reference in interi, neanche con "bassi trucchi di programmazione" direttamente in assembler (come per esempio fare un push di un intero ed un pop di un reference): il code verifier, che è parte integrante della Virtual Machine, compie controlli in tal senso al momento del caricamento di un programma e abortisce la sua esecuzione se riscontra sequenze di istruzioni non legali. Possono esistere reference nulli, che cioè non puntano ad alcun oggetto (tutti i reference sono inizializzati al valore nullo per default), proprio come i puntatori nulli del C/C++; ma le stesse istruzioni della Virtual Machine compiono sempre un controllo su ogni reference prima di usarlo ed in caso di tentativo di dereferenziazione di reference nulli generano un'eccezione.
Garbage
Collection
Un altro meccanismo relativo alla gestione degli oggetti che Java ci mette a disposizione è la garbage collection. In pratica, il runtime di Java è in grado di rendersi conto automaticamente quando un oggetto non serve più ed in tal caso provvede a distruggerlo. Per questo motivo in Java non esistono distruttori né operatore delete, ma possiamo dire che in un certo senso è come se essi venissero generati ed usati automaticamente senza che il programmatore se ne renda conto. Come funziona la garbage collection? Ci sono molti diversi algoritmi per questo meccanismo, ma in generale possiamo dire che il fondamento di tutto è il seguente approccio, detto mark and release: ad intervalli prefissati di tempo viene eseguito del codice che esamina prima di tutto le variabili statiche del programma, cercando eventuali puntatori ad oggetti. Ogni volta che si trova un oggetto si marca (mark) come allocata la memoria che usa, poi si procede ricorsivamente ispezionando tutti i puntatori che l'oggetto stesso contiene. Alla fine di questa fase si è giunti per forza a marcare tutti e soli i blocchi di memoria allocati ancora usati; siccome tutti gli altri non sono referenziati da nessuno, per definizione non servono più a niente. Quindi si passa alla seconda fase, il rilascio effettivo (release) dei blocchi di memoria non referenziati.
In
Java tutto questo lavoro viene svolto da un processo in background che
viene "risvegliato" ad intervalli regolari di tempo, oppure quando
c'è necessità di liberare la memoria allocata. La garbage
collection può apparire un processo dispendioso e pesante al punto
tale di penalizzare la performance del programma. Essa è già
stata utilizzata in linguaggi come i vecchi gloriosi Basic interpretati
o in linguaggi moderni come lo Smalltalk ed il Lisp, che non sono certamente
esempi di velocità di esecuzione (l'autore ha ricordi personali
molto indicativi al riguardo), soprattutto se paragonati al C/C++. Generalmente
la scelta di usare o non usare la garbage collection viene presa in conseguenza
di modi di pensiero assolutamente complementari: tanto per fare un esempio,
esiste una vecchia battuta che dice:
"I programmatori di Lisp sanno che la gestione della memoria è così importante che non può essere lasciata agli utenti, mentre i programmatori di C (e C++, nota personale) sanno che la gestione della memoria è così importante che non può essere lasciata al sistema.".
Cioè: le opinioni sulla garbage collection sono spesso frutto di "ideologie". I vantaggi della garbage collection sono numerosi e evidenti: non essendo più necessario occuparsi della gestione della memoria ci si può completamente dedicare al problema da risolvere. La gestione della memoria spesso è una responsabilità molto complessa: ci si deve affidare a schemi di progettazione in cui volta per volta deve essere sempre chiaro chi è che "possiede" gli oggetti e quindi ha il "diritto/dovere" di distruggerli. Non è il caso di entrare in dettagli, ma questo approccio quasi sempre porta a situazioni molto intricate se si ha a che fare con applicazioni complesse. Gli svantaggi della garbage collection sono genericamente una riduzione della performance: parte della potenza di calcolo viene distolta dall'elaborazione vera e propria, a volte possono verificarsi brevi interruzioni dei programmi quando il garbage collector entra in azione e possono verificarsi dei problemi nel rilascio di risorse. Per esempio, se la chiusura di un file o di una finestra vengono delegate completamente al garbage collector, a causa del suo funzionamento asincrono può capitare che esse "rimangano in agonia" per qualche tempo. Non è certo bello premere un pulsante per chiudere una finestra e vedersela sullo schermo ancora per un minuto... Come rispondere a queste critiche? Be', le risorse come file o finestre dovrebbero essere comunque gestite manualmente, visto che il momento della loro chiusura generalmente può essere definito senza troppi problemi, ed il garbage collector non ci impedisce assolutamente di farlo. Per quanto riguarda lo scadimento di performance, negli ultimi anni sono stati sviluppati algoritmi molto più raffinati che riducono sensibilmente questo problema. Esistono addirittura algoritmi con capacità di tempo reale che garantiscono l'assenza di pause durante l'esecuzione entro limiti prefissati (chi ha qualche interesse in questo campo troverà dei riferimenti bibliografici in fondo all'articolo). Java non pone alcuna restrizione in riguardo all'implementazione effettiva del suo garbage collector, così sarà in grado di tenersi aggiornato con gli sviluppi del settore. Alcune società che sviluppano software hanno messo in commercio librerie di garbage collection perfettamente compatibili con linguaggi come il C e il C++, e forse questo è un segno che la mentalità potrebbe cambiare in futuro.
La comunità C++ ha rifiutato l'uso di garbage collector per il prossimo ANSI/ISO C++, argomentando, con Bjarne Stroustrup in testa, che ciò andrebbe contro la filosofia del linguaggio. Questo è certamente vero, perché il C++ è sempre stato un linguaggio flessibile che ha sempre permesso al programmatore di scegliere le proprie strategie; e non è molto difficile implementare un garbage collector in C++. Rimane da discutere se questa "delega di responsabilità" è un bene o un male. Certo è che la filosofia di Java in proposito è completamente all'opposto: la garbage collection, unita alla politica di implementazione dei reference, porta alla notevole conseguenza per cui in Java non è più possibile avere puntatori appesi (dangling pointers)! Infatti o un reference è nullo (e in caso di uso improprio Java se ne accorge) oppure non può fare altro che puntare ad un oggetto valido. Secondo molte statistiche i puntatori appesi sono la causa principale di problemi durante lo sviluppo ed il debugging dei programmi.
Le
librerie standard avranno più probabilità di essere tali,
in quanto Sun ha considerato il problema parallelamente alla nascita del
linguaggio, mentre per il C++ si sta arrivando solo adesso alla definizione
della Standard Template Library (STL), dopo che la proliferazione è
già avvenuta.
Eliminazione
dell'overloading degli operatori
In Java non è semplicemente possibile effettuare l'overloading degli operatori. Secondo i creatori del linguaggio, questo è un costrutto che può rendere i programmi poco chiari (ricordiamoci che Java elimina tutto ciò che può essere pericoloso). Questo concetto può essere chiarificato dal seguente esempio in C++:
class BrokenComplex { private: float re, im; public: inline float abs() { return sqrt(re * re + im * im); } inline int operator < (BrokenComplex c) { return abs() < c.abs(); } inline void operator = (BrokenComplex c) { re = c.re; im = c.im; } };
Sebbene
non ci siano errori di sintassi e tutto il codice possa essere compilato
correttamente, la classe BrokenComplex è decisamente stata mal progettata.
L'operatore "minore di" è stato implementato magari perché
il programmatore ha necessità di memorizzare in modo univoco una
serie di numeri in una lista, ma è concettualmente sbagliato: i
numeri complessi non hanno relazione d'ordine. Anche l'operatore di assegnamento
contiene un errore più sottile: è vero che è in grado
di ricopiare un numero complesso in un altro, ma la sua dichiarazione non
rispetta la semantica degli operatori di assegnazione del C++. Per definizione
gli operatori di assegnazione in C/C++ ritornano un valore, che è
quello dell'oggetto assegnato. In caso contrario non sarà possibile
effettuare assegnazioni multiple del tipo a = b = c. Tirando le somme,
il C++ consente di ridefinire gli operatori senza verificare che ne venga
rispettata la semantica corretta (cosa che d'altronde è praticamente
impossibile fare automaticamente). Al posto degli operatori si usano consueti
metodi dal nome alfanumerico. Per esempio, per verificare l'uguaglianza
tra due oggetti a e b anziché scrivere a == b si usa a.equals(b).
C'è un altro aspetto molto sottile in tutto questo ragionamento:
di fatto a == b è corretto in Java, ma equivale a verificare se
due differenti reference puntano allo stesso oggetto. La stessa sintassi
non può quindi essere usata per un confronto tra oggetti differenti,
perché le due operazioni sono ben diverse. In C++ non c'è
questo problema: se a e b fossero due oggetti specificati by value, cioè
dichiarati come
MyObject a, b;
allora chiaramente a == b sarebbe un confronto tra oggetti diversi. Se fossero invece puntatori
MyObject *a, *b;
allora a == b sarebbe il confronto tra riferimenti allo stesso oggetto, mentre il confronto tra oggetti diversi sarebbe *a == *b. Tutto questo perché in C++ ci sono diversi modi per riferirsi ad un oggetto. Per questi motivi, quando leggiamo in un programma C++ a == b, siamo obbligati ad andare a vedere come sono stati dichiarati a e b per capire di che operazione si tratta. Java, in omaggio ai criteri di semplicità e chiarezza, elimina queste ambiguità.
Supporto
per il multithreading
Nel primo articolo di questa serie ho scritto come il C++ sia una serie di stratificazioni successive, dovute alla necessità di aggiornamento, ma spesso incompatibili tra di loro. Il supporto per il multithreading ne è la migliore (o peggiore...) riprova. Il multithreading è una forma di programmazione concorrente per cui esistono più "flussi di esecuzione" (threads) attivi contemporaneamente condividendo gli spazi di indirizzamento, cioè di fatto avendo accesso alle stesse istanze di variabili. Prima del multithreading esisteva il multitasking, per cui i "flussi di esecuzione" (chiamati in questo caso task o processi) avevano un loro proprio spazio degli indirizzi, pertanto tutte le istanze di variabili erano duplicate. Il C nacque quando il multithreading di fatto non era ancora stato proposto, pertanto le risorse erano non condivise per default. Si potevano creare risorse condivise (per esempio memoria), ma bisognava farlo esplicitamente e pertanto era chiaro che il programmatore dovesse prendersene cura in maniera opportuna. Con il multithreading il C evidenzia i suoi limiti. A parte il fatto che ogni variabile condivisa richiede di essere protetta esplicitamente, una buona parte delle funzioni di libreria standard risulta inutilizzabile in quanto non solo la loro implementazione, ma anche la semantica fa uso di variabili statiche per restituire i risultati. Più thread paralleli possono causare conflitti usando contemporaneamente la stessa variabile statica e non è possibile implementare trasparentemente un metodo di sincronizzazione perché la variabile statica è di fatto accessibile dall'esterno. Il C++ non può modificare questa situazione a causa dei vincoli i compatibilità che lo legano al suo predecessore e si limita a permetterci di definire librerie di classi (per le quali peraltro non esiste alcuno standard universalmente riconosciuto) per facilitare il controllo dei thread. Il modello di multithreading di Java è invece elegante e compatto. Interi metodi o solo sezioni specifiche possono essere semplicemente definite come synchronized e la gestione della sincronizzazione viene effettuata trasparentemente. Tutti i package standard di Java ovviamente sono stati progettati coerentemente e possono essere usati senza alcun problema. La facilità con cui è possibile creare e controllare un thread parallelo in Java ne rendono possibile l'utilizzo senza doversi porre troppi problemi, consentendo di utilizzare modelli di programmazione più potenti.
Velocità
di esecuzione
Veniamo al "tallone d'Achille" di Java: la velocità di esecuzione. È chiaro che, in quanto linguaggio interpretato, Java non potrebbe neanche essere paragonato al C++. Infatti, in media, possiamo dire che Java è circa un ordine di grandezza più lento del suo rivale. La situazione migliorerà sensibilmente con tecniche innovative che permetteranno di tradurre "al volo" il codice macchina Java in codice macchina nativo, senza che il programmatore se ne debba preoccupare esplicitamente. La traduzione può avvenire durante la prima esecuzione del codice in questione o addirittura contemporaneamente al suo downloading dalla rete. L'applicazione delle più moderne tecnologie sull'ottimizzazione del codice dovrebbe far riguadagnare una buona parte dello svantaggio... ma non tutto: ci sono limiti alla performance intrinseci nel linguaggio. Basti considerare un paio di esempi: per gli array, l'accesso ai singoli elementi è sempre controllato per quanto riguarda l'indice ed inoltre è anche sincronizzato per permettere il funzionamento in multithreading. Chiaramente l'approccio di sincronizzare sempre, anche quando non serve, rende il linguaggio più sicuro, ma meno efficiente. Come secondo esempio pensiamo al fatto che in C++ si possono definire classi di calcolo, come numeri complessi e vettori, che se ben progettate portano a codice efficiente quasi come se si esplicitassero tutti i calcoli a mano. In Java ogni oggetto di ogni classe deve essere sempre allocato dinamicamente, così l'efficienza va a farsi benedire... Insomma, se vogliamo realizzare un programma che vuole sfruttare al massimo tutta la potenza della macchina, Java non è certamente competitivo con il C++. Però anche questo fa parte della filosofia di Java. Il linguaggio di Sun ci dice esplicitamente di barattare parte della potenza di calcolo con la maggior safety dei programmi realizzati. Se pensiamo a questo concetto con l'approccio di una software house che fa i conti su quanto può guadagnare vendendo i suoi programmi, allora possiamo renderci conto che spesso un programma meno efficiente, ma più robusto ed affidabile, può essere preferibile ad un campione di velocità che però dà una non buona impressione di sé presso i clienti, magari richiedendo lunghe e costose sessioni di debugging aggiuntivo, con la conseguente distribuzione di patch... per non tenere conto delle spese aggiuntive necessarie a portare i programmi su piattaforme diverse.
Conclusione
Con questo articolo si chiude la serie dedicata al confronto tra i linguaggi Java e C++. Dovrebbe essere chiaro a tutti che Java non è attualmente in grado di sostituire il C++ come "linguaggio per ogni stagione", mentre appare molto interessante per tutta quella serie di applicazioni dove la robustezza e la portabilità sono più importanti dell'efficienza. Ribadisco ovviamente che stiamo parlando di linguaggio e non sistema-di-sviluppo, qualsiasi considerazione riguardante il network computing (per il quale Java è una vera e propria rivoluzione) è stata deliberatamente esclusa dal nostro discorso.
Cosa
si può dire in conclusione? Vi dirò il mio parere personale.
Il C++ soffre di gravi problemi, dovuti al fatto che è di fatto
un mezzosangue tra un linguaggio quick and dirty come il C e un linguaggio
orientato agli oggetti. Il comitato ANSI/ISO C++ che sta studiando le specifiche
del prossimo standard se ne rende perfettamente conto e sta correndo ai
ripari e molti problemi verranno risolti con un progressivo allontanamento
del C++ dal suo progenitore C; ma non sempre ciò avverrà
in maniera elegante. La brutta impressione di "linguaggio con una
serie di pezze sovrapposte" rimarrà e secondo me non potrà
mai essere eliminata del tutto, anche a causa della necessità di
procedere gradualmente mantendendo certe compatibilità con il passato.
Java
invece è stato riprogettato da zero tenendo conto organicamente
di tutte le esigenze a cui deve rispondere e come conseguenza è
molto più raffinato ed elegante del C++. Essendo legato al concetto
di network computing, per cui la macchina virtuale è un componente
fondamentale, deve però pagare un prezzo alla sua eleganza: l'efficienza.
Volete sapere qual'è il mio sogno? Un qualcosa di intermedio tra
C++ e Java (che avrebbe potuto chiamarsi J++ se Microsoft non avesse già
registrato un trademark su questo nome per il suo ambiente di sviluppo...),
per sostituire il C++ in quei casi in cui l'efficienza è uno dei
requisiti più importanti, magari con la reintroduzione (limitata)
dell'overloading dei puntatori, ovviamente producendo direttamente codice
nativo. Non penso di essere l'unico ad avere avuto quest'idea, così
potrebbe non essere improbabile vedere qualcosa di simile nascere nel prossimo
futuro... Staremo a vedere!
Fabrizio Giudici (fritz@dibe.unige.it) sta svolgendo il Dottorato di Studi in Ingegneria Elettronica ed Informatica presso l’Università di Genova. Opera presso il Networking Competence Center (http://dibe.unige.it/department/ncc) ed il suo interesse di ricerca primario è la sicurezza nell’ambito delle reti di calcolatori. Si interessa anche di programmazione orientata agli oggetti (OOP), con particolare riferimento ai linguaggi C++ e Java. |