
El artículo refleja la experiencia personal del autor, un ávido programador de microcontroladores que, después de muchos años de experiencia en el desarrollo de microcontroladores en C (y un poco en C ++), tuvo la oportunidad de participar en un importante proyecto de Java para desarrollar software para decodificadores de TV con Android. Durante este proyecto, pude recopilar notas sobre diferencias interesantes entre los lenguajes Java y C / C ++, evaluar diferentes enfoques para escribir programas. El artículo no pretende ser una referencia; no examina la eficiencia y la productividad de los programas Java. Es más bien una colección de observaciones personales. A menos que se especifique lo contrario, esta es una versión de Java SE 7.
Diferencias de sintaxis y construcciones de control
En resumen: las diferencias son mínimas, la sintaxis es muy similar. Los bloques de código también están formados por un par de llaves {}. Las reglas para compilar identificadores son las mismas que para C / C ++. La lista de palabras clave es casi la misma que en C / C ++. Tipos de datos integrados: similares a los de C / C ++. Matrices: todas se declaran también entre corchetes.
Las construcciones de control if-else, while, do-while, for, switch también son casi completamente idénticas. Es de destacar que en Java había etiquetas familiares para los programadores en C (aquellas que se usan con la palabra clave goto y cuyo uso se desaconseja). Sin embargo, Java excluyó la posibilidad de cambiar a una etiqueta usando goto. Las etiquetas solo deben usarse para salir de bucles anidados:
outer: for (int i = 0; i < 5; i++) { inner: for (int j = 0; j < 5; j++) { if (i == 2) break inner; if (i == 3) continue outer; } }
Para mejorar la legibilidad de los programas en Java, se ha agregado una oportunidad interesante para separar los bits de números largos con un guión bajo:
int value1 = 1_500_000; long value2 = 0xAA_BB_CC_DD;
Externamente, un programa Java no es muy diferente de un programa familiar de C. La principal diferencia visual es que Java no permite funciones, variables, definiciones de nuevos tipos (estructuras), constantes, etc., que se encuentran "libremente" en el archivo fuente. Java es un lenguaje orientado a objetos, por lo que todas las entidades del programa deben pertenecer a alguna clase. Otra diferencia significativa es la falta de un preprocesador. Estas dos diferencias se describen con más detalle a continuación.
Enfoque de objeto en lenguaje C
Cuando escribimos programas grandes en C, básicamente tenemos que trabajar con objetos. El papel del objeto aquí lo realiza una estructura que describe una cierta esencia del "mundo real":
También en C hay métodos para procesar "objetos" -estructuras - funciones. Sin embargo, las funciones no se fusionan esencialmente con los datos. Sí, generalmente se colocan en un archivo, pero cada vez es necesario pasar un puntero al objeto para que se procese en la función "típica":
int process(struct Data *ptr, int arg1, const char *arg2) { return result_code; }
Puede usar el "objeto" solo después de asignar memoria para almacenarlo:
Data *data = malloc(sizeof(Data));
En un programa en C, generalmente se define una función que es responsable de la inicialización del "objeto" antes de su primer uso:
void init(struct Data *data) { data->field = 1541; data->str = NULL; }
Entonces el ciclo de vida de un "objeto" en C suele ser así:
struct Data *data = malloc(sizeof(Data)); init(data); process(data, 0, "string"); free(data);
Ahora enumeramos los posibles errores de tiempo de ejecución que puede realizar el programador en el ciclo de vida del "objeto":
- Olvídese de asignar memoria para el "objeto"
- Especifique la cantidad incorrecta de memoria asignada
- Olvídate de inicializar el "objeto"
- Olvídate de liberar memoria después de usar el objeto
Puede ser extremadamente difícil detectar tales errores, ya que el compilador no los detecta y aparecen durante la operación del programa. Además, su efecto puede ser muy diverso y afectar otras variables y "objetos" del programa.
Enfoque de objetos Java
Frente a la programación orientada a objetos OOP, probablemente escuchaste sobre una de las ballenas OOP: la encapsulación. En Java, a diferencia de C, los datos y los métodos para procesarlos se combinan y son objetos "verdaderos". En términos de OOP, esto se llama encapsulación. Una clase es una descripción de un objeto, el análogo más cercano de una clase en C es definir un nuevo tipo usando typedef struct. En términos de Java, esas funciones que pertenecen a una clase se denominan métodos.
La ideología del lenguaje Java se basa en la afirmación "todo es un objeto". Por lo tanto, no es sorprendente que Java prohíba la creación de ambos métodos (funciones) y campos de datos (variables) por separado de la clase. Incluso el método main () familiar, desde el cual se inicia el programa, debe pertenecer a una de las clases.
Una definición de clase en Java es análoga a una declaración de estructura en C. Al describir una clase, no crea nada en la memoria. Un objeto de esta clase aparece en el momento de su creación por el nuevo operador. La creación de un objeto en Java es un análogo de la asignación de memoria en el lenguaje C, pero, a diferencia de este último, se llama automáticamente a un método especial durante la creación del objeto: el constructor de objetos. El constructor asume el papel de la inicialización inicial del objeto, un análogo de la función init () discutida anteriormente. El nombre del constructor debe coincidir con el nombre de la clase. El constructor no puede devolver un valor.
El ciclo de vida de un objeto en un programa Java es el siguiente:
Tenga en cuenta que el número de posibles errores en el programa Java es mucho menor que en el programa C. Sí, aún puede olvidarse de crear el objeto antes del primer uso (que, sin embargo, conducirá a una NullPointerException fácilmente depurada), pero en cuanto a los otros errores inherentes C programas, la situación está cambiando fundamentalmente:
- No hay operador sizeof () en Java. El compilador de Java calcula la cantidad de memoria para almacenar el objeto. Por lo tanto, no es posible especificar el tamaño incorrecto de la selección.
- La inicialización del objeto ocurre en el momento de la creación. Es imposible olvidarse de la inicialización.
- La memoria ocupada por el objeto no necesita ser liberada; el recolector de basura hace este trabajo. Es imposible olvidar eliminar un objeto después de su uso; hay menos probabilidad de que se produzca un efecto de "pérdida de memoria".
Entonces, todo en Java es un objeto de una clase u otra. Las excepciones son primitivas que se han agregado al lenguaje para mejorar el rendimiento y el consumo de memoria. Más sobre primitivas está abajo.
Recolector de memoria y basura
Java conserva los conceptos familiares de montón y pila para C / C ++, un programador. Al crear un objeto con el nuevo operador, la memoria para almacenar el objeto se toma prestada del montón. Sin embargo, un enlace a un objeto (un enlace es un análogo de un puntero), si el objeto creado no es parte de otro objeto, se coloca en la pila. En el montón se almacenan los "cuerpos" de los objetos, y en la pila hay variables locales: referencias a objetos y tipos primitivos. Si el montón existe durante la ejecución del programa y está disponible para todos los subprocesos del programa, la pila pertenece al método y existe solo durante su ejecución, y también es inaccesible para otros subprocesos del programa.
Java es innecesario y aún más: no puede liberar manualmente la memoria ocupada por un objeto. El recolector de basura realiza este trabajo en modo automático. El tiempo de ejecución supervisa si es posible alcanzar cada objeto en el montón desde la ubicación actual del programa siguiendo los enlaces de un objeto a otro. De lo contrario, dicho objeto se reconoce como "basura" y se convierte en candidato para su eliminación.
Es importante tener en cuenta que la eliminación en sí no ocurre en el momento en que el objeto "ya no es necesario": el recolector de basura decide la eliminación, y la eliminación se puede retrasar tanto como sea necesario hasta que el programa finalice.
Por supuesto, el trabajo del recolector de basura requiere una sobrecarga del procesador. Pero a cambio, alivia al programador de un gran dolor de cabeza asociado con la necesidad de liberar memoria después del final del uso de "objetos". De hecho, "tomamos" la memoria cuando la necesitamos y la usamos, sin pensar que necesitamos liberarla después de nosotros mismos.
Hablando de variables locales, debemos recordar el enfoque de Java para su inicialización. Si en C / C ++ una variable local no inicializada contiene un valor aleatorio, entonces el compilador de Java simplemente no permitirá que se deje sin inicializar:
int i;
Enlaces - Punteros de repuesto
Java no tiene punteros; en consecuencia, un programador de Java no tiene la capacidad de cometer uno de los muchos errores que ocurren al trabajar con punteros. Cuando crea un objeto, obtiene un enlace a este objeto:
En C, el programador tenía una opción: cómo pasar, por ejemplo, una estructura a una función. Podrías pasar por valor:
Pasar por valor garantizaba que la función no cambiaría los datos en la estructura, pero era ineficaz en términos de rendimiento: en el momento en que se llamó a la función, se creó una copia de la estructura. Pasar por un puntero es mucho más eficiente: de hecho, la dirección en la memoria donde se encuentra la estructura se pasó a la función.
En Java, solo había una forma de pasar un objeto a un método: por referencia. Pasar por referencia en Java es análogo a pasar por un puntero en C:
- no se produce la copia (clonación) de memoria,
- de hecho, se transmite la dirección de la ubicación de este objeto.
Sin embargo, a diferencia del puntero del lenguaje C, un enlace Java no puede incrementarse / decrementarse. "Ejecutar" a través de los elementos de una matriz utilizando un enlace a él en Java no funcionará. Todo lo que se puede hacer con un enlace es darle un valor diferente.
Por supuesto, la ausencia de punteros como tales reduce el número de posibles errores, sin embargo, el análogo del puntero nulo permanece en el lenguaje, una referencia nula denotada por la palabra clave nula.
Una referencia nula es un dolor de cabeza para un programador de Java, ya que obliga a que la referencia del objeto sea comprobada por nula antes de usarla o que maneje las excepciones NullPointerException. Si esto no se hace, el programa se bloqueará.
Por lo tanto, todos los objetos en Java se pasan a través de enlaces. Los tipos de datos primitivos (int, long, char ...) se pasan por valor (a continuación se ofrece más información sobre primitivas).
Características del enlace Java
El acceso a cualquier objeto en el programa es a través de un enlace; esto claramente tiene un efecto positivo en el rendimiento, pero puede sorprender a un novato:
Argumentos del método y valores de retorno: todo se pasa a través del enlace. Además de las ventajas, hay un inconveniente en comparación con los lenguajes C / C ++, donde podemos prohibir explícitamente que las funciones cambien el valor pasado a través de un puntero usando un calificador const:
void func(const struct Data* data) {
Es decir, el lenguaje C le permite rastrear este error en la etapa de compilación. Java también tiene la palabra clave const, pero está reservada para futuras versiones y actualmente no se utiliza en absoluto. Hasta cierto punto, la palabra clave final debe cumplir su función. Sin embargo, no protege el objeto pasado al método de los cambios:
public class Main { void func(final Entity data) {
La cuestión es que la palabra clave final en este caso se aplica al enlace, y no al objeto al que apunta el enlace. Si final se aplica a la primitiva, el compilador se comporta como se esperaba:
void func(final int value) {
Los enlaces de Java son muy similares a los enlaces de lenguaje C ++.
Primitivas de Java
Cada objeto Java, además de los campos de datos, contiene información de soporte. Si queremos operar, por ejemplo, en bytes separados y cada byte está representado por un objeto, entonces, en el caso de una matriz de bytes, la sobrecarga de memoria puede exceder muchas veces el tamaño utilizable.
Para que Java siga siendo lo suficientemente eficiente en los casos descritos anteriormente, se agregó al lenguaje soporte para tipos primitivos, primitivos.
Primitivo | Vista | Profundidad de bits | Posible análogo en C |
---|
byte | Entero | 8 | char |
corta | 16 | corta |
char | 16 | wchar_t |
int | 32 | int (largo) |
largo | 64 | largo |
flotar | Números de punto flotante | 32 | flotar |
doble | | 64 | doble |
booleano | Lógico | - | int (C89) / bool (C99) |
Todas las primitivas tienen sus análogos en el lenguaje C. Sin embargo, el estándar C no determina el tamaño exacto de los tipos enteros; en cambio, el rango de valores que este tipo puede almacenar es fijo. A menudo, el programador quiere garantizar la misma profundidad de bits para diferentes máquinas, lo que lleva a la aparición de tipos como uint32_t en el programa, aunque todas las funciones de la biblioteca requieren argumentos del tipo int.
Este hecho no puede atribuirse a las ventajas del lenguaje.
Las primitivas enteras de Java, a diferencia de C, tienen profundidades de bits fijas. Por lo tanto, no tiene que preocuparse por la profundidad de bits real de la máquina en la que se ejecuta el programa Java, así como por el orden de los bytes ("red" o "Intel"). Este hecho ayuda a comprender el principio "se escribe una vez, se ejecuta en todas partes".
Además, en Java todas las primitivas enteras están firmadas (el lenguaje carece de la palabra clave sin signo). Esto elimina la dificultad de usar variables con y sin signo en una sola expresión inherente a C.
En conclusión, el orden de bytes en primitivas de múltiples bytes en Java es fijo (byte bajo en dirección baja, Little-endian, orden inverso).
Las desventajas de la implementación de operaciones con primitivas en Java incluyen el hecho de que aquí, como en el programa C / C ++, puede ocurrir el desbordamiento de la cuadrícula de bits, sin que se produzcan excepciones:
int i1 = 2_147_483_640; int i2 = 2_147_483_640; int r = (i1 + i2);
Entonces, los datos en Java están representados por dos tipos de entidades: objetos y primitivas. Las primitivas violan el concepto de "todo es un objeto", pero en algunas situaciones son demasiado efectivas para no usarlas.
Herencia
La herencia es otra ballena OOP de la que probablemente hayas oído hablar. Si responde brevemente la pregunta "por qué la herencia es necesaria", la respuesta será "reutilización de código".
Suponga que programa en C y tiene una "clase" bien escrita y depurada: una estructura y funciones para procesarla. A continuación, surge la necesidad de crear una "clase" similar, pero con una funcionalidad mejorada, y aún se necesita la "clase" básica. En el caso del lenguaje C, solo tiene una forma de resolver este problema: la composición. Se trata de crear una nueva estructura extendida - "clase", que debe contener un puntero a la estructura base de "clase":
struct Base { int field1; char *field2; }; void baseMethod(struct Base *obj, int arg); struct Extended { struct Base *base; int auxField; }; void extendedMethod(struct Extended *obj, int arg) { baseMethod(obj->base, 123); }
Java como lenguaje orientado a objetos le permite ampliar la funcionalidad de las clases existentes utilizando el mecanismo de herencia:
Cabe señalar que Java de ninguna manera prohíbe el uso de la composición como una forma de extender la funcionalidad de las clases ya escritas. Además, en muchas situaciones, la composición es preferible a la herencia.
Gracias a la herencia, las clases en Java están organizadas en una estructura jerárquica, cada clase necesariamente tiene uno y solo un "padre" y puede tener cualquier número de "hijos". A diferencia de C ++, una clase en Java no puede heredar de más de un padre (esto resuelve el problema de la "herencia de diamantes").
Durante la herencia, la clase derivada llega a su ubicación todos los campos y métodos públicos y protegidos de su clase base, así como la clase base de su clase base, y así sucesivamente en la jerarquía de herencia.
En la parte superior de la jerarquía de herencia se encuentra el progenitor común de todas las clases Java: la clase Object, la única que no tiene un padre.
Identificación dinámica de tipo
Uno de los puntos clave del lenguaje Java es el soporte para la identificación dinámica de tipo (RTTI). En palabras simples, RTTI le permite sustituir un objeto de una clase derivada donde se requiere una referencia a la base:
Al tener un enlace en tiempo de ejecución, puede determinar el tipo verdadero del objeto al que se refiere el enlace, utilizando el operador instanceof:
if (link instanceof Base) {
Método de anulaciones
Redefinir un método o función significa reemplazar su cuerpo en la etapa de ejecución del programa. Los programadores de C son conscientes de la capacidad de un lenguaje para cambiar el comportamiento de una función durante la ejecución del programa. Se trata de usar punteros de función. Por ejemplo, puede incluir un puntero a una función en la estructura de la estructura y asignar varias funciones al puntero para cambiar el algoritmo de procesamiento de datos de esta estructura:
struct Object {
En Java, como en otros lenguajes OOP, los métodos de anulación están inextricablemente vinculados a la herencia. Una clase derivada obtiene acceso a los métodos públicos y protegidos de la clase base. Además del hecho de que puede llamarlos, puede cambiar el comportamiento de uno de los métodos de la clase base sin cambiar su firma. Para hacer esto, es suficiente definir un método con exactamente la misma firma en la clase derivada:
Es muy importante que la firma (nombre del método, valor de retorno, argumentos) coincida exactamente. Si el nombre del método coincide y los argumentos difieren, entonces el método se sobrecarga, más sobre lo que se muestra a continuación.
Polimorfismo
Al igual que la encapsulación y la herencia, la tercera ballena OOP, el polimorfismo, también tiene algún tipo de análogo en el lenguaje C orientado a los procedimientos.
Supongamos que tenemos varias "clases" de estructuras con las que desea realizar el mismo tipo de acción, y la función que realiza esta acción debe ser universal, debe "poder" trabajar con cualquier "clase" como argumento.
Una posible solución es la siguiente: enum Ids { ID_A, ID_B }; struct ClassA { int id; } void aInit(ClassA obj) { obj->id = ID_A; } struct ClassB { int id; } void bInit(ClassB obj) { obj->id = ID_B; } void commonFunc(void *klass) { int id = (int *)klass; switch (id) { case ID_A: ClassA *obj = (ClassA *) klass; break; case ID_B: ClassB *obj = (ClassB *) klass; break; } }
La solución parece engorrosa, pero el objetivo se logra: la función universal commonFunc () acepta el "objeto" de cualquier "clase" como argumento. Un requisito previo es una estructura de "clase" en el primer campo que debe contener un identificador por el cual se determina la "clase" real del objeto. Tal solución es posible debido al uso del argumento con el tipo "void *". Sin embargo, se puede pasar un puntero de cualquier tipo a dicha función, por ejemplo, "int *". Esto no causará errores de compilación, pero en tiempo de ejecución el programa se comportará de manera impredecible.Ahora veamos cómo se ve el polimorfismo en Java (sin embargo, como en cualquier otro lenguaje OOP). Supongamos que tenemos muchas clases que deberían procesarse de la misma manera por algún método. A diferencia de la solución para el lenguaje C presentada anteriormente, este método polimórfico DEBE incluirse en todas las clases del conjunto dado, y todas sus versiones DEBEN tener la misma firma. class A { public void method() {} } class B { public void method() {} } class C { public void method() {} }
A continuación, debe obligar al compilador a llamar exactamente la versión del método que pertenece a la clase correspondiente. void executor(_set_of_class_ klass) { klass.method(); }
Es decir, el método ejecutor (), que puede estar en cualquier parte del programa, debe poder trabajar con cualquier clase del conjunto (A, B o C). De alguna manera debemos "decirle" al compilador que _set_of_class_ denota nuestras muchas clases. Aquí la herencia es útil: es necesario hacer todas las clases a partir de los derivados del conjunto de alguna clase base, que contendrá un método polimórfico: abstract class Base { abstract public void method(); } class A extends Base { public void method() {} } class B extends Base { public void method() {} } class C extends Base { public void method() {} } executor() : void executor(Base klass) { klass.method(); }
Y ahora cualquier clase que sea heredera de Base (gracias a la identificación dinámica de tipo) se le puede pasar como argumento: executor(new A()); executor(new B()); executor(new C());
Dependiendo de qué objeto de clase se pase como argumento, se llamará a un método que pertenezca a esta clase.La palabra clave abstract le permite excluir el cuerpo del método (hacerlo abstracto, en términos de OOP). De hecho, le estamos diciendo al compilador que este método debe ser anulado en las clases derivadas de él. Si este no es el caso, se produce un error de compilación. Una clase que contiene al menos un método abstracto también se llama resumen. El compilador requiere marcar tales clases también con la palabra clave abstract.Estructura del proyecto Java
En Java, todos los archivos fuente tienen la extensión * .java. Faltan los archivos de encabezado * .h y los prototipos de funciones o clases. Cada archivo fuente Java debe contener al menos una clase. El nombre de la clase se acostumbra a escribir, comenzando con una letra mayúscula.Varios archivos con código fuente se pueden combinar en un paquete. Para hacer esto, se deben cumplir las siguientes condiciones:- Los archivos con código fuente deben estar en el mismo directorio en el sistema de archivos.
- El nombre de este directorio debe coincidir con el nombre del paquete.
- Al comienzo de cada archivo fuente, se debe indicar el paquete al que pertenece este archivo, por ejemplo:
package com.company.pkg;
Para garantizar la unicidad de los nombres de paquetes dentro del mundo, se propone utilizar el nombre de dominio de la empresa "invertido". Sin embargo, esto no es un requisito y cualquier nombre puede usarse en el proyecto local.También se recomienda que especifique nombres de paquete en minúsculas. Para que puedan distinguirse fácilmente de los nombres de clase.Ocultamiento de la implementación.
Otro aspecto de la encapsulación es la separación de la interfaz y la implementación. Si la interfaz es accesible para las partes externas del programa (externas al módulo o clase), entonces la implementación está oculta. En la literatura, a menudo se dibuja una analogía de caja negra cuando la implementación interna "no es visible" desde el exterior, pero lo que se introduce en la entrada de la caja y lo que da es "visible".En C, las implementaciones de ocultación se realizan dentro de un módulo, marcando funciones que no deberían ser visibles desde el exterior con la palabra clave estática. Los prototipos de las funciones que componen la interfaz del módulo se colocan en el archivo de encabezado. Un módulo en C significa un par: un archivo fuente con la extensión * .c y un encabezado con la extensión * .h.Java también tiene la palabra clave estática, pero no afecta la "visibilidad" del método o campo desde el exterior. Para controlar la "visibilidad" hay 3 modificadores de acceso: privado, protegido, público.Los campos y métodos de una clase marcada como privada solo están disponibles dentro de ella. Los campos y métodos protegidos también son accesibles para los descendientes de clase. El modificador público significa que el elemento marcado es accesible desde fuera de la clase, es decir, es parte de la interfaz. También es posible que no haya un modificador, en este caso el acceso al elemento de clase está limitado por el paquete en el que se encuentra la clase.Se recomienda que al escribir una clase, inicialmente marque todos los campos de la clase como privados y extienda los derechos de acceso según sea necesario.Método de sobrecarga
Una de las características molestas de la biblioteca estándar de C es la presencia de todo un zoológico de funciones que realizan esencialmente lo mismo, pero difieren en el tipo de argumento, por ejemplo: fabs (), fabsf (), fabsl () - funciones para obtener el valor absoluto para double, float y long tipos dobles respectivamente.Java (así como C ++) admite un mecanismo de sobrecarga de métodos: puede haber varios métodos dentro de una clase con un nombre completamente idéntico, pero que difieren en tipo y número de argumentos. Por el número de argumentos y su tipo, el compilador elegirá la versión necesaria del método en sí, es muy conveniente y mejora la legibilidad del programa.En Java, a diferencia de C ++, los operadores no pueden sobrecargarse. La excepción son los operadores "+" y "+ =", que inicialmente están sobrecargados para las cadenas de cadenas.Caracteres y cadenas en Java
En C, debe trabajar con cadenas de terminal nulo representadas por punteros al primer carácter: char *str;
Dichas líneas deben terminar con un carácter nulo. Si se "borra" accidentalmente, una cadena se considerará una secuencia de bytes en la memoria hasta el primer carácter nulo. Es decir, si otras variables del programa se colocan en la memoria después de la línea, luego de modificar una línea tan dañada, sus valores pueden (y muy probablemente se distorsionarán).Por supuesto, un programador en C no está obligado a usar cadenas clásicas de terminal nulo, sino que aplica una implementación de un tercero, pero aquí debe tenerse en cuenta que todas las funciones de la biblioteca estándar requieren cadenas de terminal nulo como argumentos. Además, el estándar C no define la codificación utilizada, este punto también debe ser controlado por el programador.En Java, el tipo de caracteres primitivo (así como el contenedor de caracteres, sobre los siguientes contenedores) representan un solo carácter de acuerdo con el estándar Unicode. Se utiliza la codificación UTF-16, respectivamente, un carácter ocupa 2 bytes en la memoria, lo que le permite codificar casi todos los caracteres de los idiomas utilizados actualmente.Los caracteres se pueden especificar por su Unicode: char ch1 = '\u20BD';
Si el Unicode de un personaje excede el máximo de 216 para char, entonces dicho personaje debe estar representado por int. En la cadena, ocupará 2 caracteres de 16 bits, pero de nuevo, los caracteres con un código superior a 216 se usan extremadamente raramente.Las cadenas Java son implementadas por la clase String incorporada y almacenan caracteres char de 16 bits. La clase String contiene todo o casi todo lo que se requiere para trabajar con cadenas. No hay necesidad de pensar en el hecho de que la línea necesariamente debe terminar con cero, aquí es imposible "borrar" imperceptiblemente este carácter de terminación cero o acceder a la memoria más allá de la línea. En general, cuando trabaja con cadenas en Java, el programador no piensa en cómo se almacena la cadena en la memoria.Como se mencionó anteriormente, Java no permite la sobrecarga del operador (como en C ++), sin embargo, la clase String es una excepción, solo que los operadores de combinación de líneas "+" y "+ =" se sobrecargan inicialmente. String str1 = "Hello, " + "World!"; String str2 = "Hello, "; str2 += "World!";
Cabe destacar que las cadenas en Java son inmutables: una vez creadas, no permiten su cambio. Cuando intentamos cambiar la línea, por ejemplo, así: String str = "Hello, World!"; str.toUpperCase(); System.out.println(str);
Entonces la cadena original en realidad no cambia. En cambio, se crea una copia modificada de la cadena original, que a su vez también es inmutable: String str = "Hello, World!"; String str2 = str.toUpperCase(); System.out.println(str2);
Por lo tanto, cada cambio de una cadena en realidad resulta en la creación de un nuevo objeto (de hecho, en casos de fusión de cadenas, el compilador puede optimizar el código y usar la clase StringBuilder, que se discutirá más adelante).Sucede que el programa a menudo necesita cambiar la misma línea. En tales casos, para optimizar la velocidad del programa y el consumo de memoria, puede evitar la creación de nuevos objetos de fila. Para estos fines, se debe usar la clase StringBuilder: String sourceString = "Hello, World!"; StringBuilder builder = new StringBuilder(sourceString); builder.setCharAt(4, '0'); builder.setCharAt(8, '0'); builder.append("!!"); String changedString = builder.toString(); System.out.println(changedString);
Por separado, vale la pena mencionar la comparación de cadenas. Un error típico de un programador Java novato es comparar cadenas usando el operador "==":
Dicho código no contiene formalmente errores en la etapa de compilación o errores de tiempo de ejecución, pero funciona de manera diferente de lo que cabría esperar. Dado que todos los objetos y cadenas, incluso en Java, están representados por enlaces, la comparación con el operador "==" proporciona una comparación de enlaces, no valores de objetos. Es decir, el resultado será verdadero solo si 2 enlaces realmente se refieren a la misma línea. Si las cadenas son objetos diferentes en la memoria, y necesita comparar sus contenidos, entonces necesita usar el método equals (): if (usersInput.equals("Yes")) {
Lo más sorprendente es que, en algunos casos, la comparación con el operador "==" funciona correctamente: String someString = "abc", anotherString = "abc";
Esto se debe a que, en realidad, someString y anotherString se refieren al mismo objeto en la memoria. El compilador coloca los mismos literales de cadena en el conjunto de cadenas: se produce el llamado internamiento. Luego, cada vez que aparece el mismo literal de cadena en el programa, se utiliza un enlace a la cadena desde el grupo. El internamiento de cadenas es precisamente posible debido a la propiedad de inmutabilidad de las cadenas.Si bien la comparación del contenido de las cadenas solo está permitida por el método equals (), en Java es posible usar cadenas correctamente en construcciones de mayúsculas y minúsculas (comenzando con Java 7): String str = new String();
Curiosamente, cualquier objeto Java se puede convertir en una cadena. El método toString () correspondiente se define en la clase base para todas las clases de la clase Object.Enfoque de manejo de errores
Cuando programe en C, puede encontrar el siguiente enfoque de manejo de errores. Cada función de una biblioteca devuelve un tipo int. Si la función es exitosa, entonces este resultado es 0. Si el resultado no es cero, esto indica un error. Muy a menudo, el código de error se pasa a través del valor devuelto por la función. Dado que la función solo puede devolver un valor y ya está ocupada por el código de error, el resultado real de la función debe devolverse a través del argumento como un puntero, por ejemplo, así: int function(struct Data **result, const char *arg) { int errorCode; return errorCode; }
Por cierto, este es uno de los casos cuando en un programa en C se hace necesario usar un puntero a un puntero.A veces usan un enfoque diferente. La función no devuelve un código de error, sino directamente el resultado de su ejecución, generalmente en forma de puntero. Una situación de error se indica con un puntero nulo. Luego, la biblioteca generalmente contiene una función separada que devuelve el código del último error: struct Data* function(const char *arg); int getLastError();
De una forma u otra, cuando se programa en C, el código que hace el trabajo "útil" y el código responsable de manejar los errores se entrelazan, lo que obviamente no hace que el programa sea fácil de leer.En Java, si lo desea, puede usar los enfoques descritos anteriormente, pero aquí puede aplicar una forma completamente diferente de manejar los errores: manejo de excepciones (sin embargo, como en C ++). La ventaja del manejo de excepciones es que, en este caso, el código "útil" y el código responsable del manejo de errores y contingencias están separados lógicamente entre sí.Esto se logra utilizando construcciones try-catch: el código "útil" se coloca en la sección try, y el código de manejo de errores se coloca en la sección catch.
Hay situaciones en las que no es posible procesar correctamente el error en el lugar de su aparición. En tales casos, se coloca una indicación en la firma del método de que el método puede causar este tipo de excepción: public void func() throws Exception {
Ahora, la llamada a este método debe estar necesariamente enmarcada en un bloque try-catch, o el método de llamada también debe estar marcado para que pueda lanzar esta excepción.Falta de preprocesador
No importa cuán conveniente sea el preprocesador familiar para los programadores de C / C ++, está ausente en el lenguaje Java. Los desarrolladores de Java probablemente decidieron que se usa solo para garantizar la portabilidad de los programas, y dado que Java se ejecuta en todas partes (casi), no se necesita un preprocesador.Puede compensar la falta de un preprocesador utilizando un campo de indicador estático y verificar su valor en el programa, cuando sea necesario.Si estamos hablando de la organización de las pruebas, entonces es posible usar anotaciones junto con la reflexión (reflexión).Una matriz también es un objeto.
Cuando se trabaja con matrices en C, la salida del índice más allá de los límites de la matriz es un error muy insidioso. El compilador no lo informará de ninguna manera, y durante la ejecución, el programa no se detendrá con el mensaje correspondiente: int array[5]; array[6] = 666;
Lo más probable es que el programa continúe la ejecución, pero el valor de la variable que se encuentra después de la matriz de matriz en el ejemplo anterior se distorsionará. La depuración de este tipo de error puede no ser fácil.En Java, el programador está protegido de este tipo de errores difíciles de diagnosticar. Cuando intenta ir más allá de los límites de la matriz, se genera una excepción ArrayIndexOutOfBoundsException. Si la captura de excepción no se programó utilizando la construcción try-catch, el programa se bloquea y se envía un mensaje correspondiente al flujo de error estándar que indica el archivo con el código fuente y el número de línea donde se superó la matriz. Es decir, el diagnóstico de tales errores se convierte en un asunto trivial.Este comportamiento del programa Java es posible porque la matriz en Java está representada por un objeto. No se puede cambiar el tamaño de la matriz Java; su tamaño está codificado en el momento en que se asigna la memoria. En tiempo de ejecución, obtener el tamaño de la matriz es tan fácil como desgranar peras: int[] array = new int[10]; int arraySize = array.length;
Si hablamos de matrices multidimensionales, entonces, en comparación con el lenguaje C, Java ofrece una oportunidad interesante para organizar matrices "en escalera". Para el caso de una matriz bidimensional, el tamaño de cada fila individual puede diferir del resto: int[][] array = new int[10][]; for (int i = 0; i < array.length; i++) { array[i] = new int[i + 1]; }
Como en C, los elementos de la matriz se encuentran en la memoria uno por uno, por lo que el acceso a la matriz se considera el más eficiente. Si necesita realizar operaciones de inserción / eliminación de elementos, o crear estructuras de datos más complejas, debe usar colecciones, como un conjunto (Conjunto), una lista (Lista), un mapa (Mapa).Debido a la falta de punteros y la incapacidad de incrementar los enlaces, el acceso a los elementos de la matriz es posible mediante índices.Colecciones
A menudo, la funcionalidad de las matrices no es suficiente, entonces debe usar estructuras de datos dinámicas. Dado que la biblioteca C estándar no contiene una implementación lista de estructuras de datos dinámicas, debe usar la implementación en códigos fuente o en forma de bibliotecas.A diferencia de C, la biblioteca estándar de Java contiene un amplio conjunto de implementaciones de estructuras de datos dinámicos o colecciones, expresadas en términos de Java. Todas las colecciones se dividen en 3 grandes clases: listas, conjuntos y mapas.Las listas (matrices dinámicas) le permiten agregar / eliminar elementos. Muchos no aseguran el orden de los elementos agregados, pero garantizan que no haya elementos duplicados. Las tarjetas o matrices asociativas operan con pares clave-valor, y el valor clave es único: no puede haber 2 pares con las mismas claves en la tarjeta.Para listas, conjuntos y mapas, hay muchas implementaciones, cada una de las cuales está optimizada para una operación específica. Por ejemplo, las listas son implementadas por las clases ArrayList y LinkedList, con ArrayList que proporciona un mejor rendimiento al acceder a un elemento arbitrario, y LinkedList es más eficiente al insertar / eliminar elementos en el medio de la lista.Solo los objetos Java completos pueden almacenarse en colecciones (de hecho, referencias a objetos), por lo tanto, es imposible crear una colección de primitivas directamente (int, char, byte, etc.). En este caso, se deben usar las clases de contenedor apropiadas:Primitivo | Clase de envoltura |
---|
byte | Byte |
corta | Corto |
char | Personaje |
int | Entero |
largo | Largo |
flotar | Flotador |
doble | Doble |
booleano | Booleano |
Afortunadamente, cuando se programa en Java, no es necesario seguir la coincidencia exacta del tipo primitivo y su "envoltorio". Si el método recibe un argumento, por ejemplo, de tipo Integer, se le puede pasar el tipo int. Y viceversa, donde se requiere el tipo int, puede usar Integer de forma segura. Esto fue posible gracias al mecanismo incorporado de Java para empacar / desempaquetar primitivas.De los momentos desagradables, debe mencionarse que la biblioteca estándar de Java contiene clases de colección antiguas que se implementaron sin éxito en las primeras versiones de Java y que no deberían usarse en nuevos programas. Estas son las clases Enumeración, Vector, Pila, Diccionario, Tabla hash, Propiedades.Generalizaciones
Las colecciones se usan comúnmente como tipos de datos genéricos. La esencia de las generalizaciones en este caso es que especificamos el tipo principal de la colección, por ejemplo, ArrayList, y entre paréntesis angulares especificamos el tipo de parámetro, que en este caso determina el tipo de elementos almacenados en la lista: List<Integer> list = new ArrayList<Integer>();
Esto permite que el compilador rastree el intento de agregar un objeto de un tipo que no sea el parámetro de tipo especificado: List<Integer> list = new ArrayList<Integer>();
Es muy importante que el parámetro tipo se borre durante la ejecución del programa, y no hay diferencia entre, por ejemplo, un objeto de la clase ArrayList <Integer>
y objeto de clase ArrayList <String>.
Como resultado, no hay forma de averiguar el tipo de elementos de colección durante la ejecución del programa: public boolean containsInteger(List list) {
Una solución parcial puede ser el siguiente enfoque: tome el primer elemento de la colección y determine su tipo: public boolean containsInteger(List list) { if (!list.isEmpty() && list.get(0) instanceof Integer) { return true; } return false; }
Pero este enfoque no funcionará si la lista está vacía.A este respecto, las generalizaciones de Java son significativamente inferiores a las generalizaciones de C ++. Las generalizaciones de Java en realidad sirven para "cortar" algunos de los posibles errores en la etapa de compilación.Iterar sobre todos los elementos de una matriz o colección
Al programar en C, a menudo debe iterar sobre todos los elementos de la matriz: for (int i = 0; i < SIZE; i++) { }
Cometer un error aquí es más simple, simplemente especifique el tamaño incorrecto de la matriz SIZE o coloque "<=" en lugar de "<".En Java, además de la forma "habitual" de la instrucción for, hay una forma de iterar sobre todos los elementos de una matriz o colección (a menudo llamada foreach en otros idiomas): List<Integer> list = new ArrayList<>();
Aquí se garantiza que clasificaremos todos los elementos de la lista, se eliminan los errores inherentes a la forma "habitual" de la declaración for.Colecciones varias
Dado que todos los objetos se heredan del Objeto raíz, Java tiene una oportunidad interesante para crear listas con varios tipos reales de elementos: List list = new ArrayList<>(); list.add(new String("First")); list.add(new Integer(2)); list.add(new Double(3.0)); instanceof: for (Object o : list) { if (o instanceof String) {
Traslados
Comparando C / C ++ y Java, es imposible no darse cuenta de la cantidad de enumeraciones funcionales que se implementan en Java. Aquí la enumeración es una clase completa, y los elementos de enumeración son objetos de esta clase. Esto permite que un elemento de enumeración establezca varios campos de cualquier tipo en correspondencia: enum Colors {
Como clase completa, una enumeración puede tener métodos y, utilizando un constructor privado, puede establecer los valores de campo de elementos de enumeración individuales.Hay una oportunidad regular para obtener una representación de cadena de un elemento de enumeración, un número de serie y también una matriz de todos los elementos: Colors color = Colors.BLACK; String str = color.toString();
Y viceversa: mediante la representación de cadena, puede obtener un elemento de enumeración y también llamar a sus métodos: Colors red = Colors.valueOf("RED");
Naturalmente, las enumeraciones se pueden usar en construcciones de mayúsculas y minúsculas.Conclusiones
Por supuesto, los lenguajes C y Java están diseñados para resolver problemas completamente diferentes. Pero, si comparamos el proceso de desarrollo de software en estos dos idiomas, entonces, de acuerdo con las impresiones subjetivas del autor, el lenguaje Java supera significativamente C en la conveniencia y velocidad de los programas de escritura. El entorno de desarrollo (IDE) desempeña un papel importante para proporcionar comodidad. El autor trabajó con IntelliJ IDEA IDE. Al programar en Java, no tiene que "temer" constantemente para cometer un error: a menudo, el entorno de desarrollo le dirá lo que debe arreglarse y, a veces, lo hará por usted. Si se produjo un error de tiempo de ejecución, el tipo de error y el lugar de su aparición en el código fuente siempre se indican en el registro; la lucha contra tales errores se convierte en un asunto trivial. Un programador en C no necesita hacer esfuerzos inhumanos para cambiar a Java, y todo porque la sintaxis del lenguaje ha cambiado ligeramente.Si esta experiencia será interesante para los lectores, en el próximo artículo hablaremos sobre la experiencia de usar el mecanismo JNI (ejecutar código nativo C / C ++ desde una aplicación Java). El mecanismo JNI es indispensable cuando desea controlar la resolución de la pantalla, el módulo Bluetooth y, en otros casos, cuando las capacidades de los servicios y administradores de Android no son suficientes.