Warning
El presente documento debe ser leído considerándose que data del año 2006; a la fecha los lenguajes descritos han sufrido grandes cambios, al punto que muchas de las afirmaciones expuestas requieren actualizaciones y/o correcciones.

Intro

Este documento pretende hacer una comparación informal de los lenguajes C, C++ y Java, poniendo énfasis en sus mutuas ventajas y desventajas, así como en su aplicabilidad.

Revisiones

  • 0.1 2006-10-11 Primera versión preliminar - Introducción histórica

  • 0.2 2006-10-15 Sección Performance/Eficiencia

  • 0.3 2006-10-15 Sección de Portabilidad. Mejoras en Performance/Eficiencia

  • 0.4 2006-10-17 Secciones Expresividad, Bien definido y Modularidad

  • 0.5 2006-10-18 Mejoras en sección Expresividad. Sección Tipos de Datos. dos ejemplos ilustrativos

  • 0.6 2006-10-19 Secciones Facilidades de I/O y pedagogía

  • 1.0 2006-10-21 Se completan todas las secciones pendientes

  • 1.1 2006-11-11 Se agrega ejemplo de vector con Glib

Introducción Histórica

A fin de comprender mejor las ventajas y desventajas de estos lenguajes, es conveniente conocer al menos superficialmente la historia de los mismos, lo cual nos permitirá tener una idea preliminar acerca de su aplicabilidad.

Lenguaje C

El lenguaje C es creado entre los años 1972-1973 por un equipo de programadores de los antiguos Laboratorios Bell de AT&T.

Dennis Ritchie diseñó e implementó el primer compilador de lenguaje C en un (¿prehistórico?) computador PDP-11. El lenguaje C se basó en dos lenguajes (prácticamente desaparecidos): "BCPL", escrito por Martin Richards, y "B", escrito por Ken Thompson en 1970 para el primer sistema UNIX en un PDP-7. Un recuento bastante detallado de estos años se puede encontrar en [CHIST], en el que se puede apreciar el caracter experimental de su desarrollo, así como las diversas influencias de otros lenguajes de programación y los diversos compromisos de eficiencia que tuvieron que afrontar.

El lenguaje C originalmente "oficial" fue el "K&R C"[KRTUT]. Ese nombre proviene de los nombres de los autores del libro "The C Programming Language" (primera edición), a saber, Brian Kernigham y Dennis Ritchie, el cual fue durante muchos años la "referencia oficial" del lenguaje. Hacia 1988-1989 (luego de varios años de propuestas y acuerdos preliminares), el American National Standards Institute" (ANSI) adoptó una versión mejorada de C, conocida hasta hoy como "ANSI C" o C89. Esta es la versión descrita en la segunda edición de "The C Programming Language — ANSI C". La versión ANSI C contiene una importante revisión a la sintaxis, especialmente para el uso de funciones (que fue tomada del lenguaje C++), así como la estandarización (parcial) de las librerías del sistema.

La última revisión del lenguaje C es conocida como "C99"[C99K] y no está (en este momento) soportada en forma completa por la mayoría de compiladores, por lo cual gran parte de programadores siguen empleando (muchas veces por desconocimiento) sólo lo que proporciona la versión "C89". Además de libros como [ANSIC], existe mucha información online para aprender este lenguaje, por ejemplo [CTUT1],[CTUT2],[CTUT3].

Lenguaje C++

Bjarne Stroustrup crea una versión experimental denominada "C with Classes" hacia 1979, con la intención de proporcionar una herramienta de desarrollo para el kernel Unix en ambientes distribuidos. En particular, él considera que ciertas caracteristicas del lenguaje "Simula" (notablemente su orientación a objetos) son útiles en desearrollos de software complejos, pero a la vez que dicho lenguaje no proporciona la performance necesaria para el contexto de sistemas operativos. Es así como decide extender el lenguaje C.

En 1983 el lenguaje se rebautiza como C++ y en 1985 Stroustrup publica la primera edición del libro "The C++ Programming Language" que sirvió de estándar informal y texto de referencia. Posteriormente el lenguaje fue estandarizado (ISO C++) y paralelamente son publicadas la segunda y tercera ediciones de "The C++ Programming Language"[CPPSTR] de modo tal que reflejan estos cambios. Más detalles se pueden e interesantes opiniones se pueden leer en [CPPFAQ].

Desde sus inicios, C++ intentó ser un lenguaje que incluye completamente al lenguaje C (quizá el 99% del código escrito en C es válido en C++) pero al mismo tiempo incorpora 'muchas' caracteristicas sofisticadas no incluidas en aquél, tales como: POO, excepciones, sobrecarga de operadores, templates o plantillas. Una visión general del lenguaje se pude obtener en [CPPWIKI]. Diversos tutoriales y textos como [CPPECKEL], [CPPPROF] proporcionan abundante información educativa.

Lenguaje Java

Java originalmente fue denominado "Oak". Sus inicios datan de 1991 cuando James Gosling (en Sun Microsystems) encabezó un proyecto cuyo objetivo original era implementar una máquina virtual ampliamente portable y un lenguaje de programación ambos orientados a dispositivos "embedded"[JAVAHIST] (procesadores incorporados en diversos dispositivos de consumo masivo como VCR’s, tostadoras, PDA’s, teléfonos móbiles, etc.) La sintaxis del lenguaje heredó características de C y C++, explícitamente eliminando aquellas que para muchos programadores (según los diseñadores) resultan excesivamente complejas e inseguras.

Con el auge de Internet, pareció natural aprovechar este lenguaje para desarrollar aplicaciones distribuídas y portables. La primera implementación de Java data de 1995 y pronto los "navegadores web" incorporaron soporte Java para la ejecución de pequeñas aplicaciones interactivas (Applets.) En la actualidad su uso es promovido para el desarrollo de aplicaciones empresariales del lado del servidor, especialmente a través del estándar J2EE, así como en dispositivos móviles (a través del estándar J2ME.)

En realidad, Java hace referencia a un conjunto de tecnologías entre las cuales el lenguaje Java es sólo una de ellas. Por tal motivo muchas veces se habla de la "plataforma Java", la cual es indesligable del lenguaje.

Sun controla los estándares de Java a través de un mecanismo de apertura parcial denominado el Java Community Process (JCP.)

Una gran cantidad de documentación online para el aprendizaje se de Java se puede hallar en [JAVATUT].

Criterios de Comparación

Comparar lenguajes de programación nunca ha sido una tarea sencilla ni objetiva. En [TUCKER] se proporcionan nueve "criterios para la evaluación y comparación de lenguajes" (ver también [WIKICOMP] para una alternativa más sencilla.) Un listado de estos criterios (con ligeras modificaciones) se presenta a continuación:

  1. Expresividad: Facilidad del lenguaje para expresar los algoritmos

  2. Bien Definido: Consistencia y falta de ambigüedad

  3. Tipos y estructuras de datos

  4. Modularidad: permitir el desarrollo de componentes independientemente

  5. Facilidades de entrada-salida: Soporte para interacción con el entorno

  6. Transportabilidad/Portabilidad

  7. Eficiencia/Performance

  8. Pedagogía: Facilidad de aprendizaje y enseñanza

  9. Generalidad: Aplicabilidad, Uso

Esta lista es utilizada en dicha referencia para comparar lenguajes de programación en un espectro muy amplio (desde LISP hasta COBOL pasando por ALGOL) pero puede servirnos como punto de partida. Considero importante agregar los siguientes aspectos:

  1. Estandarización: ¿Quién controla el lenguaje?

  2. Evolución: ¿Qué está ocurriendo con el lenguaje?

  3. Soporte de Librerías: ¿Qué NO se debe reescribir?

Expresividad

El lenguaje C siempre fue distinguido como altamente expresivo y potencialmente muy económico dada su reducida cantidad de palabras clave y el poder de algunos de sus operadores (por ejemplo, de los punteros.) En la actualidad, sin embargo, es frecuente el deseo de soportar estructuras de programación cada vez más complejas (aunque con frecuencia con los mismos algoritmos) con lo cual las implementaciones en lenguaje C tienden a tornarse oscuras (e inseguras) frente a equivalentes en otros lenguajes.

El lenguaje C++ proporciona un gran salto cualitativo frente a C al proporcionar nuevas características útiles en diversos contextos. Por ejemplo, la sobrecarga de operadores dota al lenguaje de una expresividad notable cuando se implementan aplicaciones científico-matemáticas (aunque en otros contextos pueden crear confusión); la sintaxis de clases y objetos permite manipular convenientemente diversas estructuras de datos y operaciones; las excepciones permiten procesar de un modo claro (aunque a veces con más código) los casos de error; los templates se pueden considerar (superficialmente) como macros de precompilador pero con muchas más características, etc. sin embargo, todo esto no ha estado excento de errores, en gran parte causados por mantener la compatibilidad con C (por ejemplo, ver [CPPCRIT]) tanto a nivel de sintaxis de lenguaje (compilación) como durante las etapas de enlace y ejecución.

En suma, el C++ es más expresivo que el C para la mayoría de aplicaciones medianas a grandes, lo cual es de esperarse desde que fue diseñado para abarcar una mayor cantidad de problemas mediante "múltiples paradigmas".

A modo de ejemplo, el siguiente programa realiza la suma de dos números complejos. Nótese el uso del template "complex" instanciado con "double" (para que la parte real e imaginaria sean de este último tipo.)

#include <complex>
#include <iostream>

using namespace std;

int main()
{
complex<double> a(2,3), b(4,5);
cout << "a+b=" << (a+b) << endl;
return 0;
}

Por su parte, Java adopta una sintaxis muy similar a la del lenguaje C++, aunque eliminando algunas de sus características más oscuras. En particular, la eliminación de los punteros (arrastrados desde el lenguaje C) no lo ha hecho ni más ni menos expresivo, pero sí mucho más seguro.

El ejemplo que sigue cumple el mimsmo propósito que la versión anterior en C++, sin embargo, como Java actualmente no proporciona una clase estándar de números complejos (aunque existen diversas clases gratuítas en Internet), crearemos una muy básica:

class TestComplex {

double real;
double imag;
	
TestComplex(double r,double i)
{
real=r;
imag=i;
}

TestComplex suma(TestComplex sumando)
{
return new TestComplex(real+sumando.real,imag+sumando.imag);
}

public String toString()
{
return "("+real+","+imag+")";
}

public static void main(String[] args)
{
TestComplex a=new TestComplex(2,3),b=new TestComplex(4,5);
System.out.println("a+b="+a.suma(b));
}

}

Compárese la realización de la suma (a.suma(b)) con la versión en C++ (a&#43;b.) Claramente, C++ proporciona una mayor expresividad en su tipo "complex" (en este caso, gracias a la sobrecarga del operador '+'.) Los proponentes de Java, sin embargo, argumentan que esta ganancia es despreciable en comparación con la complejidad del lenguaje requerida para redefinir el operador (lo que no mostramos aquí) y que su uso más bien oscurece aquellos programas que no son esencialmente matemáticos como nuestro ejemplo.

Bien Definido

El lenguaje C fue considerado por mucho tiempo un buen ejemplo de un lenguaje consistente y sin ambigüedades notorias, especialmente entre sus contemporáneos. Los creadores le reconocen ciertos inconvenientes en la notación que promueven a confusiones pero esto no es estrictamente un error y suele ser evitable. Quizá el principal problema radica en la gran cantidad de aspectos que son dejados a criterio del implementador, entre los cuales destaca el tamaño de los tipos de datos. Por ejemplo, en los compiladores para PC de los años 80 era frecuente encontrar que el rango del tipo "int" se encontraba entre -32768 y 32767, lo cual era un claro reflejo de los procesadores de "palabra de 16 bits" y la representación de "complemento a 2". En computadores menos comunes y más antiguos, era frecuente encontrar computadores con otros tamaños de "palabra" y con otros rangos para los tipos de datos; asimismo, en la actualidad es frecuente asumir 32 bits para los enteros, con lo que el rango varía con frecuencia entre -2147483648 y 2147483647. Evidentemente esto puede crear serios problemas de portabilidad.

Estos inconvenientes lamentablemente fueron íntegramente heredados por C++ y hasta la fecha no tienen una clara solución (aunque el estándar C99 tiene algunas mejoras.)

El lenguaje Java, sin embargo, fue creado desde el inicio con la intención de desterrar las ambigüedades y dependencias del implementador del lenguaje y de sus clases auxiliares, con lo cual actualmente es tal vez el mejor definido de los lenguajes populares.

Tipos y estructuras de datos

El lenguaje C proporciona mecanismos que actualmente se consideran rudimentarios para proporcionar tipos de datos estructurados. Las estructuras (y uniones) se suelen utilizar para definir tipos complejos constituídos a partir de otros más simples (que a la vez pueden ser estructuras y/o uniones) con la posibilidad de crear identificadores auxiliares que simplifican la notación (typedef.) Asimismo, los arreglos o arrays permiten especificar colecciones homogeneas de longitud fija (en tiempo de compilación), los cuales tienen una relación muy cercana en su manipulación con los punteros. Una carencia notable (o ventaja según algunos) es la carencia de tipos de datos para representar cadenas de texto (strings), los cuales son soportados de un modo inusual mediante arrays de caracteres.

Si bien este "minimalismo" contribuye a la performance de la ejecución (o la optimización en la compilación), son muchos los casos donde se requiere el soporte de tipos más sofisticados (y sus operaciones asociadas) como por ejemplo, vectores, listas enlazadas, colas, etc. para los cuales el lenguaje obliga a construirlos desde sus componentes básicos. En la práctica, existen diversas librerías que complementan estos aspectos (por ejemplo, la popular Glib) pero su programación necesariamente es más laboriosa al no estar integrada internamente al lenguaje. El siguiente ejemplo ilustra la creación de un "vector" (array dinámico) en el que se insertan tres enteros. Como se aprecia, el código es oscuro y muy propenso a errores.

#include <stdio.h>
#include <stdlib.h>

void agregar_entero(int **ptr_vector,int *n_elementos,int elemento)
{
(*ptr_vector)=realloc(*ptr_vector,(*n_elementos+1)*sizeof(int));
(*ptr_vector)[*n_elementos]=elemento;
*n_elementos=*n_elementos+1;
}

int main()
{
int *v=NULL,n_elementos=0;
unsigned int z;

agregar_entero(&v,&n_elementos,5);
agregar_entero(&v,&n_elementos,6);
agregar_entero(&v,&n_elementos,7);

for(z=0;z<n_elementos;z++)
	printf("Elemento %d -> %d\n",z+1,v[z]);
return 0;
}

Normalmente, el programador debería evitar esta clase de implementaciones "desde cero". El siguiente ejemplo resuelve el mismo problema utilizando la librería Glib:

#include <stdio.h>
#include <glib.h>

int main()
{
GArray *v;
unsigned int n_elementos=0,z;
int valor;

v=g_array_new(FALSE,FALSE,sizeof(int));

valor=5; g_array_append_val(v,valor); n_elementos++;
valor=6; g_array_append_val(v,valor); n_elementos++;
valor=7; g_array_append_val(v,valor); n_elementos++;

for(z=0;z<n_elementos;z++)
	printf("Elemento %d -> %d\n",z+1,g_array_index(v,int,z));
return 0;
}

La compilación de aplicaciones Glib (o Gnome) se realiza con facilidad gracias al script "pkg-config":

cc $(pkg-config --cflags --libs glib-2.0) -o gvector gvector.c

Por su parte, C++ proporciona facilidades que permiten la creación de estructuras de datos muy poderosas y fuertemente integradas en el lenguaje. Por ejemplo, las estructuras contenedoras "clásicas" se proporcionan en su librería de templates, la STL; asimismo, el desarrollador puede crear sus propios tipos de dato con diversas operaciones asociadas. Gracias a esto, su uso resulta una extensión natural de los tipos de dato primitivos con lo cual se alcanza un alto grado de claridad.

El siguiente programa es una versión C++ del ejemplo anterior del vector:

#include <vector>
#include <iostream>

using namespace std;

int main()
{
vector <int> v;

v.push_back(5);
v.push_back(6);
v.push_back(7);

for(unsigned int z=0;z<v.size();z++)
	cout << "Elemento " << (z+1) << " -> " << v[z] << endl;
return 0;
}

Java proporciona tipos de datos primitivos similares (notablemente, careciendo de punteros) y mediante su librería de clases estándar proporciona todas las estructuras contenedoras "clásicas" antes mencionadas, aunque con una sintaxis que pone claramente de manifiesto que se trata de clases auxiliares.

El siguiente programa ilustra este punto, siendo una reescritura del mismo ejemplo del vector de esta sección [1]:

import java.util.Vector;

class PruebaVector {

public static void main(String[] args)
{
Vector<Integer> v=new Vector<Integer>();
v.add(5);
v.add(6);
v.add(7);
for(int z=0;z<v.size();z++)
	System.out.println("Elemento " + (z+1) + " -> " + v.elementAt(z));
}

}

Modularidad

En la referencia original ([TUCKER]) este criterio estaba referido a la posibilidad de desarrollar componentes de manera independiente los que eventualtamente interactuarían. Es ese sentido, los tres lenguajes analizados permiten desarrollar funciones, clases, y paquetes de modo independiente, cada cual con sus convenciones particulares.

En cuanto a los "niveles de empaquetamiento" de los componentes, el lenguaje C en la práctica proporciona sólo dos niveles: componentes visibles dentro del archivo de código fuente, y componentes visibles globalmente (concretamente, funciones y variables.) En C++ los conceptos de clase y "espacio de nombres" (namespace) proporcionan dos niveles adicionales de "empacado", mientras que en Java los equivalentes corresponden a las clases y los "paquetes".

Facilidades de entrada-salida

Siguiendo la referencia [TUCKER], este criterio está referido a las facilidades que proporcionan los lenguajes para acceder a archivos de disco, en particular el acceso secuencial, aleatorio e indexado. Asimismo, se hace referencia a la accesibilidad a sistemas de Base de Datos.

Manipulación de archivos

El lenguaje C como tal no proporciona instrucciones de I/O salvo mediante funciones de su "librería estándar", la cual fue diseñada en gran medida para aprovechar las facilidades del sistema de archivos Unix. En ese sentido, las funciones de I/O proporcionan acceso secuencial byte a byte o bloque a bloque, así como desplazamientos arbitrarios en la posición de lectura/escritura; esto en realidad depende de las facilidades inherentes al sistema operativo, pero están presentes en prácticamente todo sistema comercial moderno. En particular, la librería estándar no proporciona funciones de acceso rápido a través de índices (que algunos consideran "bases de datos"), las cuales son implementadas mediante muchas librerías correspondientes, incluso Open Source como GDBM, Berkeley libdb, etc.

C++ y Java proporcionan una interfaz alternativa a la misma funcionalidad a través de jerarquías de clases de I/O con diferente nivel de refinamiento, lo que los hace más extensibles aunque no necesariamente más convenientes. La tendencia en general apunta a no extender el lenguaje en este camino y por el contrario, crear nuevas librerías auxiliares para casos concretos.

Acceso a Sistemas de Base de Datos

Como quiera que "C" fue y es uno de los lenguajes más populares utilizados para el desarrollo de sistemas medianos y grandes, hasta el día de hoy está casi implícito que cualquier aplicación "de amplia audiencia" que proporciona una interfaz de programación, permitirá el acceso mediante el lenguaje C. Esto es cierto para prácticamente todas las bases de datos comerciales y no comerciales más populares con lo que C permite un acceso prácticamente ilimitado, aunque no necesariamente el más conveniente [2].

Por su parte, un programa escrito en C++ tiene normalmente la capacidad de hacer uso del API de lenguaje C, pero muchos sistemas de base de datos proporcionan una interfaz mejorada orientada a objetos disponible en este lenguaje. Nuevamente, su uso no es portable.

Los creadores de Java, gracias a las anteriores experiencias, estandarizaron una interfaz orientada a objetos para acceder de un modo portable a cualquier base de datos. Esta API se denomina Java Database Connectivity y gracias a la gran popularidad de Java, practicamente todos los vendedores importantes de bases de datos han creado implementaciones de esta interfaz. Esto promueve la portabilidad en cuanto al acceso a la base de datos, aunque las incompatibilidades y extensiones del SQL subsisten.

Transportabilidad/Portabilidad

El lenguaje C tradicionalmente se ha proporcionado como parte de la distribución del sistema operativo Unix (aunque en ocasiones, con un costo adicional) siempre siguiendo los lineamientos de sus creadores (K&R) por lo que es reconocido como uno de los lenguajes más difundidos y portables de la historia, al menos, hasta los años 80. Asimismo, su proceso de estandarización fue cuidadosamente elaborado, con lo cual se convirtió en uno de los primeros "caso de éxito" de portabilidad gracias a estándares abiertos. Salvo el estándar C99, el cual carece de una amplia base de compiladores que lo soporten, la versión C89/C90 es universalmente portable.

El lenguaje C++ siguió posteriormente un ciclo muy similar, y si bien no es un lenguaje automáticamente distribuído en los sistemas Unix, prácticamente todos lo pueden ejecutar ya sea en una variante comercial o mediante el popular GNU GCC/G++ con lo que la disponibilidad está asegurada. En cuanto a su portabilidad, el único inconveniente notorio radica en ciertos problemas (cada vez menos frecuentes) en las implementaciones de la STL.

No obstante lo indicado, tanto el C como el C++ presentan importantes dificultades de portabilidad, particularmente en cuanto a los siguientes aspectos:

  1. Características dependientes de la implementación: Lo que permite realizar fuertes optimizaciones en distintas arquitecturas, resulta con frecuencia una pesadilla para la portabilidad. Muchos detalles importantes son dejados a criterio de quien escribe el compilador, tales como los tamaños de diversos tipos de datos, juegos de caracteres, comportamiento ante ciertos errores, etc.

  2. Acceso al librerías del sistema operativo: Las interfaces y librerías principales no han seguido un proceso de estandarización tan riguroso como el lenguaje, lo que ha traído como consecuencia diversas soluciones incompatibles para los mismos problemas. Estrictamente este no es un problema del lenguaje, sino más bien de la plataforma utilizada (por ejemplo, las variantes de Unix.)

Estos problemas realmente nunca han tenido una solución definitiva, y a tal efecto existen algunas herramientas (por ejemplo, el "grupo" autoconf) orientadas mantenerlos "bajo control", mas no a eliminarlos. Asimismo, la escritura de un programa portable en C/C++ suele demandar la presencia de un programador experimentado que estructure adecuadamente el código a fin de facilitar el proceso de "portado" caso por caso.

En ese sentido Java introdujo un enfoque radical (aunque predecible) al diseñar un lenguaje prácticamente sin características dependientes del implementador (potencialmente algo menos eficiente), y con una extensa librería utilitaria cuya interfaz de programación está muy fuertemente estandarizada. Esto trajo consigo la famosa promesa: "write once, run everywhere" (escribir una sola vez, ejecutar en cualquier lugar) la cual ha sido muchas veces objeto de mofa debido a diversos errores de implementación y especificaciones poco claras ("write once, debug everywhere".) Con todo, la portabilidad alcanzada es cualitativamente superior a la que se puede obtener con los lenguaje C/C++, y se consigue de manera automática por cualquier desarrollador.

En conclusión, si es imprescindible una máxima portabilidad a "bajo costo", la respuesta es Java.

Eficiencia/Performance

Este es un aspecto sobre el cual se ha debatido hasta la saciedad y continúa siendo un tema de discusión encarnizada. Como de costumbre, en aquellos temas sobre los que se dice mucho, al final resulta que no se concluye en nada. Por lo tanto, proporcionaremos algunos argumentos bastante evidentes a favor y en contra de los lenguajes que estamos analizando y dejaremos al lector extraer su propia opinión.

Un primer aspecto corresponde a centrar el problema. Cuando nos referimos a la eficiencia/performance estamos hablando principalmente de la velocidad con la cual los programas escritos en los tres lenguajes de estudio, logran llevar a cabo diversas tareas. Asimismo podemos considerar los recursos del sistema requeridos (principalmente memoria) durante su ejecución.

Más cerca del fierro

Es bien sabido que prácticamente todos los computadores ejecutan los programas mediante una o más unidades centrales de procesamiento (CPU) las cuales (dependiendo de la marca y el modelo) sólo comprenden el llamado "lenguaje máquina" o "código máquina", el cual consiste de una serie de operaciones relativamente elementales o de muy "bajo nivel" tales como escribir bytes en memoria, sumar un par de números, leer bytes de un dispositivo externo, etc.

Por lo tanto, todos los lenguajes de programación deben ser "traducidos" en algún momento a "lenguaje máquina" para que los programas sean ejecutados; simplificando, a este proceso se le suele denominar "compilación" y tanto el lenguaje C como el lenguaje C++ siguen este esquema de ser "compilados" al "lenguaje máquina" del procesador en el que se van a utilizar. En particular, el lenguaje C posee estructuras de datos muy simples que resultan generalmente de traducción bastante directa al "lenguaje máquina", con lo cual el programador está muchas veces muy cerca de escribir en un lenguaje similar al que el CPU comprende. En muchos casos, esta simplicidad consigue que el programa tenga una excelente performance dada la simplicidad del lenguaje máquina producido.

En el caso de un programa en C++ en el cual se hace uso de sus más conspícuas facilidades (por ejemplo, objetos) resulta que la traducción a "lenguaje máquina" es bastante compleja puesto que los procesadores practicamente carecen de operaciones u operadores que faciliten las operaciones más abstractas de este lenguaje. Esto trajo como consecuencia que en los primeros compiladores de C++ [3], el "lenguaje máquina" generado sea típicamente extremadamente lento en comparación a un programa equivalente escrito en C (aunque con frecuencia el equivalente en "C" será mucho más extenso y difícil de escribir.)

Sin embargo, esta situación ha cambiado dramáticamente con el transcurso de los años, al punto que actualmente los compiladores de C++ generan un código muy difícil de superar por una hábil implementación equivalente en C, salvo excepciones [4]. Algo similar se puede afirmar con respecto al uso de la memoria.

Personalmente, considero que la performance entre C y C++ es similar; en caso de necesidad de elección aplicaría los otros criterios utilizados este texto.

Más lejos del fierro

Java fue creado desde el inicio para ser ejecutado en cualquier clase de dispositivo o CPU, y uno de sus aspectos más interesantes es que NO se compila directamente en el lenguaje máquina del CPU en uso, sino en un "pseudo lenguaje máquina" denominado "byte code". Este Java compilado en "byte code" puede ser transportado a cualquier computador en el cual se dispone de un programa especial encargado de la traducción del "byte code" al verdadero "lenguaje máquina" del CPU en uso. En otras palabras, este programa especial "interpreta" el "byte code", efectivamente ejecutando la aplicación Java original. Este programa intéprete se conoce (simplificando un poco) como "Java Virtual Machine" (JVM) o "Java Runtime Environmet".

Es evidente que un programa compilado en "byte code" en tanto debe ser además traducido (interpretado) en lenguaje máquina, en general resulta algo más lento que un programa ya traducido al lenguaje máquina del CPU donde este paso adicional ya no se requiere.

Un segundo inconveniente, particularmente en aplicaciones relativamente pequeñas, radica en los recursos de memoria que típicamente utiliza el Java Virtual Machine; si bien esto suele ser configurable, dichos ajustes no suelen ser sencillos ni bien documentados. Ante esto, no se ha hecho mucho salvo esperar a que los computadores se vendan con memorias mucho más amplias, al punto que esto ya no suele ser un problema.

Un tercer inconveniente para ciertas clases de aplicaciones se encuentra en la impredecibilidad del "garbage collector", el cual en muchas ocasiones no realiza su trabajo en el momento más apropiado y suele consumir mucho tiempo de CPU en su análisis, contribuyendo a la lentitud. Afortunadamente los implementadores de las JVM han optimizado mucho la inteligencia del garbage collector al punto que en la actualidad esto sólo es un problema en casos excepcionales.

A favor de Java cabe mencionar los avances en los entornos de ejecución "JIT" (Just In Time) los cuales precompilan el "byte code" a lenguaje máquina conforme el programa se ejecuta, con lo cual el esquema se convierte en una combinación de "compilación" e "interpretación" lo cual puede mejorar significativamente la performance de muchas aplicaciones.

En general la performance del JVM ha mejorado notoriamente a través de los años (pero también los CPU’s). Sin embargo, además de la "modernidad" (o la versión) de la misma, también se debe tener en cuenta la "implementación" utilizada. Por ejemplo, la performance puede variar de modo considerable entre las implementaciones de Sun e IBM de la máquina virtual para una misma versión de Java. En particular, ciertas arquitecturas de hardware populares (por ejemplo, la familia x86) suelen tener máquinas virtuales mejor implementadas y más optimizadas que las menos usuales.

No es importante ser veloz, sino no ser lento

Más allá de los benchmarks y pruebas diversas de "cálculo puro" (en los que Java suele ser más lento que sus contendores) se suele plantear el argumento de la importancia de la velocidad de ejecución del lenguaje en sí. Si bien a todos les interesa que las aplicaciones se ejecuten a máxima velocidad, muchas veces la sensación de velocidad o lentitud no es ocasionada por la performance del "código principal" de la aplicación (que puede estar programado en Java) sino de componentes auxiliares tales como bases de datos, librerías de terceros, dispositivos gráficos acelerados, etc. En esa línea algunos defensores de Java manifiestan que es poco relevante si el "código C/C++" es 10 o 50% más veloz, si al final este tiempo no es el verdaderamente percibido por el usuario; asimismo, la aparente reducida performance de Java podría ser frecuentemente superada gracias a la claridad del lenguaje, el cual permitiría implementar mejores algoritmos y de un modo más eficiente.

Claramente, todos estos argumentos son subjetivos (pero muchas veces válidos) y al mismo tiempo son discutibles caso por caso. A modo de conclusión, considero que un programa en Java suele ser notoriamente más lento si la tarea principal consiste en operaciones lógico/matemáticas, mientras que la performance suele ser ligeramente inferior a la correspondiente a C/C++ para aplicaciones que hacen uso de muchos otros componentes y librerías auxiliares.

Pedagogía

En breve, ni C ni C++ fueron creados para ser sencillos de aprender. C fue creado principalmente para ser eficiente, y C++ para ser a la vez eficiente y rico en características [5]. Java, por el contrario tuvo desde el principio la intención de ser un lenguaje muy fácil de comprender y utilizar, y si bien eso no significa que su aprendizaje sea rápido ni trivial, ciertamente libera al estudiante de diversos aspectos confusos y sintaxis oscura de los otros lenguajes. Esta es quizá una de las razones más importantes que ha contribuído a su rápida adopción (aparte del excesivo marketing.)

Generalidad

Los tres lenguajes estudiados se proponen como "de propósito general", es decir, serían adecuados para atacar prácticamente cualquier clase de problema. En la práctica, el C suele ser utilizado para construir componentes básicos o de bajo nivel (notablemente, el kernel de muchos sistemas operativos) mientras que C++ y Java tienen un espectro mucho más amplio (por ejemplo, aplicaciones comerciales de toda clase.) Notablemente Java, en gran medida gracias a la previsión y publicidad de Sun y diversos vendedores de "servidores de aplicación", es muy utilizado actualmente en el contexto de servidores Web (Servlets y JSP), acompañado en muchos casos de una arquitectura multicapa.

Estandarización

Como se indicó en la sección histórica, C y C++ son buenos ejemplos de lenguajes exitosos estandarizados "por comité" lo que promueve una competencia abierta entre las implementaciones, sin detrimento de la portabilidad.

Lamentablemente, no hay procesos de certificación formal para estos lenguajes y muchos implementadores simplemente ignoran algunas características de éstos estándares, lo que trae evidentes inconvenientes para los programadores que pretenden codificar "según el estándar".

En parte por este motivo, Sun en sus inicios descartó utilizar un mecanismo similar para la estandarización de Java (lenguaje y librerías) pero luego dio paso a una apertura parcial en la que diversos vendedores y usuarios promueven los cambios en los estándares futuros, proceso que siempre es monitoreado por Sun (Java Comunity Program.) Asimismo, Sun proporciona exigentes pruebas de certificación a fin de que los implementadores validen y publiciten su adherencia a los estándares, con el consiguiente beneficio de los desarrolladores.

Evolución: ¿Qué está ocurriendo con el lenguaje?

En cuanto al lenguaje C (y su "librería estándar"), el estándar C99 sigue a la espera de ser totalmente implementado por los vendedores. Notablemente, GNU GCC incorpora la gran mayoría de la funcionalidad requerida. Por lo demás, es uno de los lenguajes más estables disponibles.

C++ continúa en su camino apuntando a una nueva revisión comunmente conocida como C++0x (la idea es que aparezca antes de 2010) la cual a juzgar por el mismo Stroustrup (ver [CPP0X]) estará más orientada hacia el desarrollo de las librerías (potencialmente -pero sin muchas probabilidades- incluyendo un API de GUI.) Este desarrollo es relativamente lento aparentemente debido a la falta de entusiasmo de los vendedores que suelen financiar esta clase de proceso.

Por su parte, Java continúa -a un paso acelerado- haciendo añadidos y mejoras en sus librerías principales y también en el lenguaje base (aunque estos últimos son contados) por lo general orientados a lograr una plataforma moderna y muy completa para distintas clases de aplicaciones (ver la siguiente sección.)

Soporte de Librerías: ¿Qué NO se debe reescribir?

En esta sección nos referimos a la posibilidad de reutilizar código ya escrito y depurado a fin de no "reinventar la rueda".

Dada su larga permanencia, tanto C como C++ disponen de una extremadamente amplia variedad de opciones en cuanto a librerías para diversos propósitos; asimismo, dada su importancia y ubicuidad, la gran mayoría de nuevos sistemas proporcionan librerías que permiten la interacción con programas escritos en estos lenguajes.

El único aspecto cuestionable radica en que muy pocas de estas librerías están estandarizadas del mismo modo que el lenguaje. Por ejemplo, el estándar de lenguaje C actualmente incluye una librería (conocida como la "librería estándar") que permite realizar una gran cantidad de tareas esenciales (como mostrar un mensaje, grabar en un archivo de disco, calcular funciones trigonométricas, procesar cadenas de caracteres, etc.) pero que para muchas aplicaciones modernas, resulta francamente insuficiente. Esto obliga a la búsqueda de librerías de terceros, lo que presenta los siguientes inconvenientes:

  • Al no estar estandarizadas, en ocasiones no garantizan un funcionamiento predecible

  • Las diversas implementaciones suelen ser incompatibles

  • Su uso puede requerir la adquisición de licencias e imponer restricciones adicionales

Afortunadamente es posible hallar excelentes librerías muy bien soportadas para propósitos muy variados. Por ejemplo, la conocida Glib[GLIBDOC] utilizada en el proyecto GNOME, incorpora una tremenda gama de aspectos que van desde la portabilidad entre sistemas operativos hasta la implementación de contenedores estándar.

Por su parte, C++ dispone de una librería más extensa la cual incluye de hecho a la "librería estándar de C" así como la famosa "STL" (Standard Template Library) que implementa diversas estructuras de datos de manera genérica, así como muchos algoritmos populares. De igual modo, muchas librerías de terceros están disponibles para propósitos más especializados (por citar dos ejemplos, la librería Xerces[XERCES] de procesamiento de documentos XML, y las librerías Boost[BOOST] de propósito general.)

Java desde su creación tuvo la buena política de estandarizar muchas librerías (mediante clases e interfases) para una gran cantidad de aspectos que nunca fueron considerados en C ni C++ (como por ejemplo, la interfaz gráfica, el acceso a bases de datos, páginas Web, etc.) lo cual no excluye librerías más especializadas de terceros (por ejemplo, recuerdo haber empleado un excelente paquete de clases que permitían el procesamiento de documentos de Microsoft Excel.) Esto ha tenido un fuerte impacto en la comunidad de programadores que ven aquí una manera clara y segura de diseñar y codificar a partir de las especificaciones. El contexto de librerías estandarizadas en torno a Java es tan amplio que la "plataforma Java" se publicita como un conjunto de tecnologías orientas hacia distintos tipos de aplicaciones:

  • Java SE (Java Standard Edition) considera facilidades de propósito general y aplicaciones de escritorio en particular

  • Java EE (Java Enterprise Edition) para el desarrollo de aplicaciones empresariales (potencialmente sofisticadas) en servidores

  • Java ME (Java Micro Edition) dirigida a la programación de dispositivos móviles

Cuadro Resumen

Table 1. Resumen
Característica C C++ Java

Expresividad

Regular

Muy Buena a Excesiva

Muy Buena

Bien Definido

Regular

Muy Buena

Muy Buena

Tipos y Estructuras de datos

Deficiente

Muy Buena

Muy Buena

Modularidad

Regular

Muy Buena

Muy Buena

Facilidades de entrada/salida

Buena

Buena

Buena

Transportabilidad / Portabilidad

Buena

Buena

Excelente

Eficiencia / Performance

Excelente

Excelente

Buena

Pedagogía

Regular

Regular

Buena

Generalidad

Buena

Muy Buena

Muy Buena

Estandarización

Buena

Buena

Excelente

Evolución

Estable

Estable

Acelerada

Soporte de Librerías

Bueno

Muy Bueno

Excelente

Referencias


1. Este programa utiliza las facilidades presentes en Java SE 5.0. En particular, el vector fue definido como "Vector<Integer>" (en versiones anteriores hubiera sido sólo "Vector") lo que hubiera obligado a agregar un "cast a Integer" al momento de extraer los elementos. Asimismo, hemos agregado enteros mediante add(5) cuando anteriormente se necesitaba add(new Integer(5)). En la práctica casi siempre se debe preferir ArrayList, pero aquí mantenemos Vector por similitud con lo anterior.
2. Por ejemplo, se pierde la portabilidad: la interfaz de programación C en diversas bases de datos varía largamente (y su uso suele ser complicado) en comparación, por ejemplo, con el uso de "Embedded SQL en C".
3. Los verdaderamente primeros compiladores de C++, generaban un programa en C el cual era posteriormente compilado con un compilador de lenguaje C. Era de esperarse una mala performance comparativa en aquellos años.
4. Por ejemplo, en [KERPIKE] se describe el caso de un programa que resulta ser mucho más veloz en C que en C++; la misma referencia indica que el fallo se debe a una mala implementación de una librería de hash en el compilador de C++ que utilizaron los autores. De otro lado, en [CPPFAQ] se muestra y explica un sencillo pero importante caso en el cual la ordenación Quick Sort de la librería estándar de C++ (sort()) es más veloz que la rutina qsort() de la librería estándar de C.
5. Sin embargo, Stroustrup[STRLEARN] manifiesta que las características con que actualmente cuenta C++ requieren ser enseñadas y aprendidas de un modo distinto (especialmente no como una "extensión POO de C") con lo que se puede obtener mucha más claridad (facilidad) y programas más eficientes.