Igualdad de cadenas de caracteres en Java, usando intern()
Y como no podía ser de otra manera, encontré algo para contradecir (en parte) mi nota anterior sobre Igualdad de cadenas de caracteres en Java. Y digo "en parte" porque salvo utliizando este pequeño truco, no debería utliizarse el == como comparador de igualdad entre String.
Veamos código:
public class StringTest {
@Test
public void testString(){
String uno = "uno21";
String dos = new String("uno21");
Assert.assertTrue(uno == dos.intern()); //nótese la llamada a intern()
}
}
Por lo tanto, podemos agregar una cuarta moraleja:
- Los String pueden compararse con == sólamente si se les invoca el método
intern, preferentemente a las dos cadenas. De todas maneras sigue siendo recomendable no utilizar el ==
Enlaces:
Igualdad de cadenas de caracteres en Java
Inauguramos esta sección, en la que trataremos de expresar ideas en pocas palabras (ciento cuarenta, o menos).
Esta vez les traemos un detalle de la clase String. Veamos código:
public class StringTest{
@Test
public void testIgualdad(){
String uno = "uno21";
String dos = "uno21";
String tres = new String("uno21");
Assert.assertEquals(uno,dos);
Assert.assertTrue(uno==dos);
Assert.assertEquals(uno,tres);
Assert.assertFalse(uno==tres); //nótese el assertFalse
}
}
Tenemos varias moralejas que aprender:
- Los String no deben compararse nunca por igualdad utilizando el operador ==
- Los String instanciados implícitamente y que contienen el mismo texto, apuntan a la misma variable. De hecho, hay un pool con ellos.
- Si bien los String parecen tipos primitivos, no lo son. Son más complejos que los objetos, de hecho.
Evaluando numeros pares/impares en Java
Hace unos días estábamos dando una clase básica de la sintaxis de Java, y se liberó un ejercicio en el cual había que calcular la sumatoria de números enteros impares, dado un valor tope. El 98% de los alumnos (es decir, todos menos uno) modificaron un código que ya tenían para evaluar números pares, dejando la evaluación if (numero % 2 == 1) o alguna variación muy similar. Sin embargo un alumno hizo una implementación diferente, cuyo código es el siguiente:
public int sumatoriaImpares(int limite){
int resultado = 0;
for (int numero = 1; numero <= limite; numero ++){
if ((numero & 1) == 1){
resultado += numero;
}
}
return resultado;
}
Se imaginarán que si bien me tomó por sorpresa en un primer momento, la implementación me terminó simpatizando. Me pregunté por qué utilizaría ese operador, y estimé que era para mejorar la performance. Por supuesto, me quedó la duda planteada y me propuse investigarla un poco.
Para muchos, no debería haber alternativa: la implementación con el resto de la división es la que corresponde. La discusión terminaría así: debemos hacer el código tan legible como sea posible, porque es menos costoso de entender para quien viene detrás nuestro a hacer arreglos o evoluciones sobre el software. Sin embargo, pretendo demostrar algo más.
Hecho este descargo, comento un poco qué hice para estudiar el caso: Por empezar, preparé dos métodos que evaluan si un número es impar, devolviendo un booleano. Un método lo hace mediante el resto de la división, y el otro mediante el and a nivel de bits. Aquí el código:
package ar.com.uno21.drafts.language;
public class EvaluadorImpares {
public static boolean esImparA(int unNumero) {
return ((unNumero % 2) == 1);
}
public static boolean esImparB(int unNumero) {
return ((unNumero & 1) == 1);
}
}
Luego preparé un código que medía los tiempos de una y otra comparación. Por supuesto, como los resultados serían cero, hice pruebas para una cantidad de comparaciones cada vez mayor, hasta llegar a aproximadamente mil millones de comparaciones. Y ejecuté varias veces las pruebas, promediando los resultados individuales. No agregaré el código para no abundar en detalles.
Corrí el código en dos plataformas: Linux y Windows. El detalle no es menor, ya que si bien el bytecode generado es el mismo, la JVM sobre la cual corre tiene implementaciones diferentes.
Y los resultados son bastante diferentes, como veremos a continuación:
- en Windows, es menos costoso evaluar un and a nivel de bits que el resto de una división. Sin embargo... ¿qué tan costoso es? ¿qué tanto puede beneficiarnos optar por el código más críptico? Los números obtenidos los pueden visualizar en esta planilla de resultados (en Windows), pero en promedio es un 83% más costosa la evaluación del resto frente a la otra implementación.
- en Linux, sin embargo, es igualmente costoso hacer una u otra implementación. Los resultados, también pueden verlos en esta otra planilla de resultados (en Linux).
No cabe duda de que en este caso la elección entre legibilidad y performance es bastante sencilla: elegiremos la legibilidad.
De hecho esta elección será, en la inmensa mayoría de los casos, la que tomaremos como regla general: Siempre deberíamos codificar de la manera más legible, antes que priorizar el desempeño. Los compiladores se encargan de una manera mucho más eficiente de optimizar el código, por lo que esa tarea ya no está vinculada al programador (que puede concentrarse en tareas de más alto nivel).
Nota: Si les interesa evaluar características de bajo nivel del lenguaje Java, pueden empezar por la definición del and a nivel de bits entre enteros y del operador resto entre enteros, de la especificación de la JVM.
Igualdad de objetos en Java (II)
Siguiendo con la nota de unos días atrás, Igualdad de objetos en Java (I), vamos a mostrar cómo debería codificarse un método equals para que funcione de la manera apropiada. En un futuro artículo veremos los fundamentos matemáticos de la igualdad, pero por el momento nos interesa ensuciarnos las manos en el código.
Recordemos la firma del método:
public boolean equals(Object obj);
Por lo tanto, la llamada al mismo tendrá la forma:
unObjeto.equals(otroObjeto);
Lo cual devolverá un valor booleano que deberá ser verdadero ante la igualdad de los objetos o falso en el caso contrario. Para cumplir con esa premisa, tenemos que considerar ciertos pasos.
Por empezar, tenemos que evitar que el código avance cuando no sea necesario. Considerando esto, podríamos iniciar el cuerpo del método mediante una simple pregunta que nos ahorrará muchos pasos: ¿es acaso el objeto que recibo por parámetro el mismo objeto que recibe la llamada al método? En otras palabras... ¿me estoy comparando conmigo mismo? De ser así, simplemente se devolverá un valor verdadero y el asunto quedará resuelto:
public boolean equals(Object obj) {
if (this == obj){
return true;
}
}
Esto no es todo, por supuesto. El código aún no compila (ya que no podemos evitar la sentencia del return), pero no deja de ser un muy buen primer paso: Ahora estamos tranquilos, sabiendo que si el código se invoca de una manera reflexiva, el resultado es correcto.
Siguiendo con el análisis, consideramos la opción de otro valor especial en el parámetro: el null. Imaginemos que se nos pide comparar un objeto contra un valor nulo: por supuesto será diferente al objeto en cuestión.
En este caso, adicionalmente, estaríamos salvando un error a futuro si preguntamos por la existencia de null, dado que si el argumento tuviese ese valor se podría llegar a librar una NullPointerException cuando intentemos comparar los atributos miembro del objeto. En código, sería así:
public boolean equals(Object obj) {
if (this == obj){
return true;
}
if (obj == null){
return false;
}
}
Una vez más, el código no compila, pero nos comenzamos a asegurar de que caminamos sobre terreno firme. Un último error que deberíamos contemplar es si realmente estamos recibiendo un objeto del tipo que esperamos (i.e. si estamos comparando Personas, no podemos recibir Animales). Esto se logra verificando la clase del objeto parámetro, y comparándola con la clase del objeto que recibe el mensaje. El código es más claro que la explicación:
public boolean equals(Object obj) {
if (this == obj){
return true;
}
if (obj == null){
return false;
}
if (getClass() != obj.getClass()){
return false;
}
}
Por supuesto, si las clases son diferentes los objetos serán distintos. Ahora bien, pasaremos a dar todos los demás pasos juntos para terminar con el código funcional de un equals que se comporta como queremos.
Necesitaremos convertir el objeto a la clase correspondiente (en este caso, Persona). Es importante destacar que la conversión de tipos podría haber generado una ClassCastException, pero está protegido por las sentencias predecesoras que comprueban el tipo del objeto parámetro.
Una vez convertido el parámetro al tipo necesario, deberemos comparar todos o algunos de sus atributos (próximamente escribiremos sobre esta decisión). Si todos los atributos comparados se corresponden, la igualdad es inmediata. Si no, los objetos serán diferentes. Como es más fácil comprobar la diferencia y cortar por el error, que cortar por la "no igualdad de todos los atributos" (como lo enuncia la Ley de De Morgan), el código se escribe por la negación. Vemos el equals terminado:
public boolean equals(Object obj) {
if (this == obj){
return true;
}
if (obj == null){
return false;
}
if (getClass() != obj.getClass()){
return false;
}
Persona other = (Persona) obj;
if (documento != other.documento){
return false;
}
if (nombre == null) {
if (other.nombre != null){
return false;
}
} else if (!nombre.equals(other.nombre)){
return false;
}
return true;
}
Nótese cómo cuando comparamos atributos que son referencias y no tipos primitivos, primero chequeamos las condiciones de nulidad de la variable local, luego la de la variable propia del objeto parámetro, y en caso de que no sea nula la local, se reutiliza el método equals del atributo dentro del objeto (tómense un minuto para releer esta frase y ver si la comprendieron bien).
La magia del equals se ve potenciada cuando podemos reutilizar el código de otro equals previamente codificado. En este caso, el equals de la clase String provista por Java.
Creo que con esta nota ya los mareamos lo suficiente, pero seguramente habrá muchas más sobre temas afines a la identidad e igualdad en Java. Y ahora, les pregunto a ustedes: ¿utilizan otra implementación de equals?
El porque de un archivo de propiedades
Los archivos de propiedades (o .properties a partir de ahora) estan compuestos por pares de la forma clave=valor (como si fuera un Map), donde clave y valor pueden ser cualquier cadena de caracteres.
Veamos unejemplo
defaulDateFormat=dd/mm/yyy language=español
(la extensión del archivo tiene que ser .properties)
En este tipo de archivos podemos definir constantes, variables de configuración, nombre de servidores, etc.
El motivo por el cual es ventajoso utilizar este tipo de archivos es que desde distintas partes de la aplicación (front end, back end) podemos acceder al valor a partir de la clave.
Con esto, por ejemplo, podemos evitar tener hardcodeado en el código los mensajes que se imprimen en la aplicación. O podemos tener un .properties para cada idioma y con sólo cambiar el archivo la tendriamos lista para poder ser usada en cualquier lugar del mundo.
En conclusión, un archivo .properties nos permite desacoplar varias capas de nuestra aplicación y concentrarlas en un solo lugar (como por ejemplo el idioma). Esto trae enormes ventajas: mayor escalabilidad, facilidad de mantenimiento y muchas otras que escapan el alcance de esta nota.
Ahora veamos algunos ejemplos de cómo levantar un archivo .properties
Declarando la siguiente linea de codigo en nuetra clase
private static ResourceBundle properties = ResourceBundle.getBundle("template");
Y de la siguiente forma accedemos a una propiedad
properties.getString("language");
De esta forma podríamos concentrar todos los properties en una sola clase, o mantenerlos separados (eso va a depender de lo que quiera hacer en cada caso).
Ademas, si utilizamos algun framework como Struts la cosa es mas fácil aún: agragando la siguiente linea al struts-config.xml
(en este caso particular este .properties solo sirve para los mensajes que se pueden imprimir o para variables que se usen en los jsp)
Como comentario final me gustaria agregar que esta nota no tiene como objetivo que el lector aprenda cómo mapear un archivo properties a una clase en java (de eso hay mucho escrito en muchos lugares) sino el despertar su curiosidad y mostrarle algo que puede mejorar su trabajo.
Igualdad de objetos en Java (I)
Al poco tiempo de empezar a programar en Java, nos encontramos con que debemos evaluar la igualdad de los objetos, para hacer cosas en función de eso. Entonces hacemos:
Persona personaUno = new Persona("Lucas Videla", 23456789);
Persona personaDos = new Persona("Matías González", 12345678);
if (personaUno == personaDos){
// hacer algo
}
Probamos el código, y quizás durante un tiempo no notemos que en realidad no funciona como corresponde: siempre arroja falso. Nos quedamos perplejos, dado que cuando hacíamos esto, el código funcionaba perfectamente:
int unEntero = 5;
int otroEntero = 6;
if (unEntero == otroEntero){
// hacer algo
}
Luego de un tiempo caemos en cuenta de que lo que está sucediendo es que la comparación no está haciendo lo que queremos: está comparando otra cosa.
En Java, el operador == compara el contenido de dos variables dadas, sean éstas del tipo que sean.
Para los enteros funciona, por ejemplo, porque la variable almacena el valor efectivo del entero. Para los números de punto flotante también, y así sucesivamente en los tipos primitivos.
Pero cuando nos encontramos con los objetos vemos que el criterio no funciona, ya que la variable almacena una referencia a otro espacio de memoria donde realmente está el objeto. Por lo tanto, comparará si las referencias son iguales, es decir, si apuntan al mismo lugar. En ese caso, sólo obtendremos un valor verdadero en el caso de que realmente sea el mismo objeto. Y esto casi nunca es admisible para los criterios de negocio.
Para solventar esto Java nos proporciona un mecanismo para la comparación por igualdad entre dos objetos, implementada a nivel de Object (y por lo tanto heredada por toda clase que desarrollemos). Estamos hablando del método equals, el cual tiene la siguiente firma:
public boolean equals(Object obj);
Recapitulando: el operador de comparación por igualdad (==) no funciona para los objetos. O mejor dicho, sí funciona, pero no de la forma en la que nos es útil. Por lo tanto necesitábamos un nuevo mecanismo. Y allí entra en juego la clase Object con su método al rescate: equals.
La implementación por defecto (es decir, la que tendremos a menos que hagamos algo para evitarlo) es ésta:
public boolean equals(Object obj) {
return (this == obj);
}
Por lo tanto, vemos que se comporta de la misma manera que el operador. En principio, la comparación por igualdad arrojará el mismo resultado que la utilización del método equals.
Entonces... ¿qué debemos hacer? Debemos sobreescribir el método, para nuestra clase en particular.
Aquí vemos cómo sobreescribir el método equals, para la clase Persona:
public class Persona{
private String nombre;
private int documento;
public Persona(String nombre, int documento){
this.nombre = nombre;
this.documento = documento;
}
public boolean equals(Object obj){
boolean retorno;
// código para definir el criterio de igualdad
// y establecer el valor de retorno
return retorno;
}
}
Ahora bien, tenemos la estructura donde escribir el código que evaluará la igualdad de los objetos. Pero... ¿qué escribir en dicho método?
Ese asunto es un tanto complejo, y lo voy adejar para una segunda parte de este artículo.
