Una mirada en profundidad a C++ frente a Java

Publicado: 2022-07-22

Innumerables artículos comparan las características técnicas de C++ y Java, pero ¿cuáles son las diferencias más importantes a considerar? Cuando una comparación muestra, por ejemplo, que Java no admite herencia múltiple y C++ sí, ¿qué significa eso? ¿Y es algo bueno? Algunos argumentan que esto es una ventaja de Java, mientras que otros lo declaran un problema.

Exploremos las situaciones en las que los desarrolladores deberían elegir C ++, Java u otro lenguaje por completo y, lo que es más importante, por qué es importante la decisión.

Examinando los conceptos básicos: compilaciones de lenguaje y ecosistemas

C++ se lanzó en 1985 como una interfaz para los compiladores de C, similar a cómo se compila TypeScript en JavaScript. Los compiladores modernos de C++ suelen compilar en código de máquina nativo. Aunque algunos afirman que los compiladores de C++ reducen su portabilidad y requieren reconstrucciones para nuevas arquitecturas de destino, el código de C++ se ejecuta en casi todas las plataformas de procesador.

Lanzado por primera vez en 1995, Java no se compila directamente en código nativo. En su lugar, Java crea un código de bytes, una representación binaria intermedia que se ejecuta en la máquina virtual de Java (JVM). En otras palabras, la salida del compilador de Java necesita un ejecutable nativo específico de la plataforma para ejecutarse.

Tanto C ++ como Java pertenecen a la familia de lenguajes similares a C, ya que generalmente se parecen a C en su sintaxis. La diferencia más significativa son sus ecosistemas: mientras que C++ puede llamar sin problemas a bibliotecas basadas en C o C++, o la API de un sistema operativo, Java es más adecuado para bibliotecas basadas en Java. Puede acceder a las bibliotecas C en Java utilizando la API de Java Native Interface (JNI), pero es propensa a errores y requiere algún código C o C++. C++ también interactúa con el hardware más fácilmente que Java, ya que C++ es un lenguaje de nivel inferior.

Compensaciones detalladas: genéricos, memoria y más

Podemos comparar C++ con Java desde muchas perspectivas. En algunos casos, la decisión entre C++ y Java es clara. Las aplicaciones nativas de Android normalmente deberían usar Java a menos que la aplicación sea un juego. La mayoría de los desarrolladores de juegos deberían optar por C++ u otro lenguaje para obtener la animación en tiempo real más fluida posible; La gestión de memoria de Java a menudo provoca retrasos durante el juego.

Las aplicaciones multiplataforma que no son juegos están más allá del alcance de esta discusión. Ni C++ ni Java son ideales en este caso porque son demasiado detallados para un desarrollo de GUI eficiente. Para aplicaciones de alto rendimiento, es mejor crear módulos C++ para hacer el trabajo pesado y usar un lenguaje más productivo para el desarrollador para la GUI.

Las aplicaciones multiplataforma que no son juegos están más allá del alcance de esta discusión. Ni C++ ni Java son ideales en este caso porque son demasiado detallados para un desarrollo de GUI eficiente.

Pío

Para algunos proyectos, la elección puede no ser clara, así que comparemos más:

Rasgo C++ Java
Apto para principiantes No
Rendimiento en tiempo de ejecución Mejor Bueno
Latencia Previsible Impredecible
Punteros inteligentes de conteo de referencias No
Recolección global de basura de marcado y barrido No Requerido
Asignación de memoria de pila No
Compilación a ejecutable nativo No
Compilación a código de bytes de Java No
Interacción directa con las API del sistema operativo de bajo nivel Requiere código C
Interacción directa con bibliotecas C Requiere código C
Interacción directa con bibliotecas de Java A través de JNI
Gestión estandarizada de compilaciones y paquetes No Experto


Aparte de las funciones comparadas en la tabla, también nos centraremos en funciones de programación orientada a objetos (POO) como herencia múltiple, genéricos/plantillas y reflexión. Tenga en cuenta que ambos lenguajes admiten programación orientada a objetos: Java lo exige, mientras que C++ admite programación orientada a objetos junto con funciones globales y datos estáticos.

Herencia múltiple

En OOP, la herencia es cuando una clase secundaria hereda atributos y métodos de una clase principal. Un ejemplo estándar es una clase Rectangle que hereda de una clase Shape más genérica:

 // Note that we are in a C++ file class Shape { // Position int x, y; public: // The child class must override this pure virtual function virtual void draw() = 0; }; class Rectangle: public Shape { // Width and height int w, h; public: void draw(); };

La herencia múltiple es cuando una clase secundaria hereda de varios padres. Aquí hay un ejemplo, usando las clases Rectangle y Shape y una clase Clickable adicional:

 // Not recommended class Shape {...}; class Rectangle: public Shape {...}; class Clickable { int xClick, yClick; public: virtual void click() = 0; }; class ClickableRectangle: public Rectangle, public Clickable { void click(); };

En este caso tenemos dos tipos base: Shape (el tipo base de Rectangle ) y Clickable . ClickableRectangle hereda de ambos para componer los dos tipos de objetos.

C++ admite la herencia múltiple; Java no lo hace. La herencia múltiple es útil en ciertos casos extremos, como:

  • Creación de un lenguaje específico de dominio avanzado (DSL).
  • Realización de cálculos sofisticados en tiempo de compilación.
  • Mejorar la seguridad del tipo de proyecto de formas que simplemente no son posibles en Java.

Sin embargo, generalmente se desaconseja el uso de la herencia múltiple. Puede complicar el código y afectar el rendimiento a menos que se combine con la metaprogramación de plantillas, algo que solo pueden hacer los programadores de C++ más experimentados.

Genéricos y Plantillas

Las versiones genéricas de clases que funcionan con cualquier tipo de datos son prácticas para la reutilización de código. Ambos lenguajes ofrecen este soporte (Java a través de genéricos, C++ a través de plantillas), pero la flexibilidad de las plantillas de C++ puede hacer que la programación avanzada sea más segura y robusta. Los compiladores de C++ crean nuevas clases o funciones personalizadas cada vez que usa diferentes tipos con la plantilla. Además, las plantillas de C++ pueden llamar a funciones personalizadas en función de los tipos de parámetros de la función de nivel superior, lo que permite que determinados tipos de datos tengan un código especializado. Esto se llama especialización de plantilla. Java no tiene una función equivalente.

Por el contrario, cuando se utilizan genéricos, los compiladores de Java crean objetos generales sin tipos a través de un proceso denominado borrado de tipos. Java realiza una verificación de tipos durante la compilación, pero los programadores no pueden modificar el comportamiento de una clase o método genérico en función de sus parámetros de tipo. Para entender esto mejor, veamos un ejemplo rápido de una función genérica std::string format(std::string fmt, T1 item1, T2 item2) que usa una plantilla, template<class T1, class T2> , de un C++ biblioteca que creé:

 std::string firstParameter = "A string"; int secondParameter = 123; // Format printed output as an eight-character-wide string and a hexadecimal value format("%8s %x", firstParameter, secondParameter); // Format printed output as two eight-character-wide strings format("%8s %8s", firstParameter, secondParameter);

C++ produciría la función de format como std::string format(std::string fmt, std::string item1, int item2) , mientras que Java la crearía sin los tipos de objeto string e int específicos para item1 y item2 . En este caso, nuestra plantilla de C++ sabe que el último parámetro entrante es un int y, por lo tanto, puede realizar la conversión de std::to_string necesaria en la segunda llamada de format . Sin plantillas, una instrucción printf de C++ que intente imprimir un número como una cadena como en la segunda llamada de format tendría un comportamiento indefinido y podría bloquear la aplicación o imprimir basura. La función de Java solo podría tratar un número como una cadena en la primera llamada de format y no lo formatearía directamente como un entero hexadecimal. Este es un ejemplo trivial, pero demuestra la capacidad de C++ para seleccionar una plantilla especializada para manejar cualquier objeto de clase arbitraria sin modificar su clase o la función de format . Podemos producir la salida correctamente en Java utilizando la reflexión en lugar de los genéricos, aunque este método es menos extensible y más propenso a errores.

Reflexión

En Java, es posible averiguar (en tiempo de ejecución) detalles estructurales como qué miembros están disponibles en una clase o tipo de clase. Esta función se llama reflejo, presumiblemente porque es como acercar un espejo al objeto para ver qué hay dentro. (Se puede encontrar más información en la documentación de reflexión de Oracle).

C++ no tiene reflejo completo, pero el C++ moderno ofrece información de tipo de tiempo de ejecución (RTTI). RTTI permite la detección en tiempo de ejecución de tipos de objetos específicos, aunque no puede acceder a información como los miembros del objeto.

Gestión de la memoria

Otra diferencia fundamental entre C++ y Java es la gestión de la memoria, que tiene dos enfoques principales: manual, donde los desarrolladores deben realizar un seguimiento y liberar la memoria manualmente; y automático, donde el software rastrea qué objetos todavía están en uso para reciclar la memoria no utilizada. En Java, un ejemplo es la recolección de basura.

Java requiere memoria de recolección de elementos no utilizados, lo que proporciona una administración de memoria más sencilla que el enfoque manual y elimina los errores de liberación de memoria que comúnmente contribuyen a las vulnerabilidades de seguridad. C++ no proporciona administración de memoria automática de forma nativa, pero admite una forma de recolección de elementos no utilizados denominada punteros inteligentes. Los punteros inteligentes usan el conteo de referencias y son seguros y funcionan bien si se usan correctamente. C++ también ofrece destructores que limpian o liberan recursos tras la destrucción de un objeto.

Si bien Java solo ofrece la asignación de montones, C++ admite tanto la asignación de montones (usando new y delete o las funciones C malloc más antiguas) como la asignación de pilas. La asignación de pilas puede ser más rápida y segura que la asignación de montones porque una pila es una estructura de datos lineal, mientras que un montón se basa en un árbol, por lo que la memoria de pila es mucho más sencilla de asignar y liberar.

Otra ventaja de C++ relacionada con la asignación de pilas es una técnica de programación conocida como adquisición de recursos es inicialización (RAII). En RAII, los recursos como las referencias se vinculan al ciclo de vida de su objeto de control; los recursos serán destruidos al final del ciclo de vida de ese objeto. RAII es cómo funcionan los punteros inteligentes de C++ sin desreferenciarlos manualmente: un puntero inteligente al que se hace referencia en la parte superior de una función se desreferencia automáticamente al salir de la función. La memoria conectada también se libera si esta es la última referencia al puntero inteligente. Aunque Java ofrece un patrón similar, es más incómodo que RAII de C++, especialmente si necesita crear varios recursos en el mismo bloque de código.

Rendimiento en tiempo de ejecución

Java tiene un rendimiento de tiempo de ejecución sólido, pero C++ todavía tiene la corona, ya que la gestión manual de la memoria es más rápida que la recolección de elementos no utilizados para las aplicaciones del mundo real. Aunque Java puede superar a C++ en ciertos casos de esquina debido a la compilación JIT, C++ gana la mayoría de los casos no triviales.

En particular, la biblioteca de memoria estándar de Java sobrecarga el recolector de basura con sus asignaciones en comparación con el uso reducido de asignaciones de almacenamiento dinámico de C++. Sin embargo, Java todavía es relativamente rápido y debería ser aceptable a menos que la latencia sea una preocupación principal, por ejemplo, en juegos o aplicaciones con restricciones de tiempo real.

Gestión de compilación y paquetes

Lo que a Java le falta en rendimiento, lo compensa con facilidad de uso. Un componente que afecta la eficiencia del desarrollador es la gestión de compilación y paquetes: cómo creamos proyectos y traemos dependencias externas a una aplicación. En Java, una herramienta llamada Maven simplifica este proceso en unos pocos pasos y se integra con muchos IDE como IntelliJ IDEA.

En C++, sin embargo, no existe un repositorio de paquetes estandarizado. Ni siquiera existe un método estandarizado para compilar código C++ en aplicaciones: algunos desarrolladores prefieren Visual Studio, mientras que otros usan CMake u otro conjunto personalizado de herramientas. Además de la complejidad, ciertas bibliotecas comerciales de C ++ tienen formato binario y no hay una forma consistente de integrar esas bibliotecas en el proceso de compilación. Además, las variaciones en la configuración de compilación o las versiones del compilador pueden causar desafíos para que las bibliotecas binarias funcionen.

Amabilidad para principiantes

La fricción de compilación y administración de paquetes no es la única razón por la que C++ es mucho menos amigable para los principiantes que Java. Un programador puede tener dificultades para depurar y usar C++ de manera segura, a menos que esté familiarizado con C, los lenguajes ensambladores o el funcionamiento de nivel inferior de una computadora. Piense en C++ como una herramienta poderosa: puede lograr mucho, pero es peligroso si se usa mal.

El enfoque de administración de memoria de Java mencionado anteriormente también lo hace mucho más accesible que C++. Los programadores de Java no tienen que preocuparse por liberar la memoria de objetos, ya que el lenguaje se encarga de eso automáticamente.

Tiempo de decisión: ¿C++ o Java?

Un diagrama de flujo con una burbuja de "Inicio" azul oscuro en la esquina superior izquierda que eventualmente se conecta a uno de los siete cuadros de conclusión azul claro debajo de él, a través de una serie de cruces de decisión blancos con ramas azul oscuro para "Sí" y otras opciones. y ramas de color azul claro para "No". La primera es "¿Aplicación GUI multiplataforma?" de donde un "Sí" apunta a la conclusión, "Elija un entorno de desarrollo multiplataforma y use su idioma principal". Un "No" apunta a "¿Aplicación nativa de Android?" de donde un "Sí" apunta a una pregunta secundaria, "¿Es un juego?" De la pregunta secundaria, un "No" apunta a la conclusión, "Use Java (o Kotlin)", y un "Sí" apunta a una conclusión diferente, "Elija un motor de juego multiplataforma y use el lenguaje recomendado". Desde la "¿Aplicación nativa de Android?" pregunta, un "No" apunta a "¿Aplicación nativa de Windows?" de donde un "Sí" apunta a una pregunta secundaria, "¿Es un juego?" De la pregunta secundaria, un "Sí" apunta a la conclusión, "Elija un motor de juego multiplataforma y use su lenguaje recomendado", y un "No" apunta a una conclusión diferente, "Elija un entorno de GUI de Windows y use su principal (típicamente C++ o C#)". Desde la "¿Aplicación nativa de Windows?" pregunta, un "No" apunta a "¿Aplicación de servidor?" de lo cual un "Sí" apunta a una pregunta secundaria, "¿Tipo de desarrollador?" De la pregunta secundaria, una decisión de "Habilidad media" apunta a la conclusión, "Usar Java (o C# o TypeScript)", y una decisión de "Habilidades" apunta a una pregunta terciaria, "¿Prioridad máxima?" De la pregunta terciaria, una decisión de "Productividad del desarrollador" apunta a la conclusión, "Usar Java (o C# o TypeScript)", y una decisión de "Rendimiento" apunta a una conclusión diferente, "Usar C++ (o Rust)". Desde la aplicación "¿Servidor?" pregunta, un "No" apunta a una pregunta secundaria, "¿Desarrollo de impulsores?" De la pregunta secundaria, un "Sí" apunta a una conclusión, "Usar C++ (o Rust)", y un "No" apunta a una pregunta terciaria, "¿Desarrollo de IoT?" De la pregunta terciaria, "Sí" apunta a la conclusión, "Usar C++ (o Rust)", y un "No" apunta a una pregunta cuaternaria, "¿Negociación de alta velocidad?" De la pregunta cuaternaria, un "Sí" apunta a la conclusión, "Use C++ (o Rust)", y un "No" apunta a la conclusión final restante, "Pregunte a alguien familiarizado con su dominio de destino".
Una guía ampliada para elegir el mejor lenguaje para varios tipos de proyectos.

Ahora que hemos explorado en profundidad las diferencias entre C++ y Java, volvemos a nuestra pregunta original: ¿C++ o Java? Incluso con una comprensión profunda de los dos idiomas, no existe una respuesta única para todos.

Es mejor que los ingenieros de software que no estén familiarizados con los conceptos de programación de bajo nivel seleccionen Java cuando restringen la decisión a C++ o Java, excepto para contextos en tiempo real como los juegos. Los desarrolladores que buscan expandir sus horizontes, por otro lado, pueden aprender más eligiendo C++.

Sin embargo, las diferencias técnicas entre C++ y Java pueden ser solo un pequeño factor en la decisión. Ciertos tipos de productos requieren elecciones particulares. Si aún no está seguro, puede consultar el diagrama de flujo, pero tenga en cuenta que, en última instancia, puede indicarle un tercer idioma.