sábado, 31 de diciembre de 2011

Colecciones

votar
   Cuando en un lenguaje de programación es necesario tratar con un conjunto de datos, como por ejemplo el número de ventas realizadas, o el stock de un almacén, se emplean colecciones. En Java, las colecciones utilizan el marco de trabajo Java Collections Framework. Dentro de este marco, existen una serie de interfaces y clases diseñadas para tratar específicamente con conjuntos de datos. Si se te escapa el concepto de interfaz, puedes consultar la entrada correspondiente.

   Este marco esta ordenado de forma jerárquica. Primero está la interfaz Collection (no confundir con la clase Collections, en plural) a la que extienden las interfaces Set, List y Queue. A su vez, de estas interfaces parten otras interfaces y clases que tratan los datos de forma diferente según las necesidades de los mismos.
   Por otro lado tenemos la interfaz Map. Map forma parte del Java Collection Framework pero no tiene nada que ver con Collection. Más tarde volveremos con esta interfaz.

   Vayamos por ahora con Collection. Una colección es simplemente un grupo de objetos a los que llamamos elementos. Hay que tener en cuenta que las colecciones usan Genéricos, es decir, hay que declarar qué tipo de elementos los componen. Así, para declarar la interfaz Collection haríamos lo siguiente:
public interface Collection<E>
siendo E el nombre de los objetos de que está formada la colección. Por ejemplo, Collection<String>, o Collection<Integer>.

   Collection se usa cuando no hay que ser muy específico sobre cómo tratar los datos, y nos llega con los métodos que tiene esta interfaz. Cuando esto no es suficiente, usamos las interfaces que extienden Collection:
  • Set: es una colección que no puede contener elementos duplicados, como entradas de cine o nombres de clientes. Estos elementos pueden estar ordenados o no. La extiende la interfaz SortedSet, que tiene sus elementos ordenados, y la implementan tres clases
                 -HashSet: los elementos no están ordenados.
                 -TreeSet: los elementos están ordenados según un orden natural (de menor a mayor, lexicográficamente...) o según las reglas de comparación que provea el programador.
                  -LinkedHashSet: los elementos están colocados por el orden en que son insertados.
  • List: es una colección ordenada y que permite duplicados, por ejemplo, los libros vendidos en una semana. ArrayList, Vector y LinkedList son las tres clases que implementan List. Las tres tienen sus elementos ordenados según su índice.
  • Queue: es una colección que sirve para mantener los elementos antes de procesarlos. Normalmente, pero no siempre, los elementos están ordenados según el sistema FIFO (first-in-first-out), es decir, el primero en entrar es el primero en salir. PriorityQueue es una clase que implementa Queue y que ordena los elementos según su orden de prioridad.
   Volvamos ahora con la interfaz Map. Map es utilizado cuando tenemos un conjunto de valores y a cada uno de ellos le asignamos una clave. Por ejemplo, el nombre y el DNI, o el nombre de usuario y la contraseña. Un Map no puede contener claves duplicadas y cada clave referencia a un sólo valor (existen los multimaps, cuyas claves pueden referenciar a varios, pero ése es otro tema). Hashtable, TreeMap y HashMap son tres clases que implementan Map. De ellas, sólo TreeMap está ordenada según un orden natural o por las reglas de comparación dadas. Map también tiene una interfaz que la extiende: SortedMap (en realidad, TreeMap implementa NavigableMap, que extiende SortedMap, que extiende Map).

   Hay por último dos clases, Arrays y Collections, que extienden la clase Object, y que contienen un grupo de métodos útiles para usar con las colecciones. Puedes encontrar todos los métodos usados en estas clases e interfaces en los docs de Oracle.

  


sábado, 24 de diciembre de 2011

Novedades en Java 7

votar
   Acabo de leer un panfleto escrito por Madhusudhan Konda y publicado por O´Reilly sobre algunas de las novedades en Java 7 y he pensado que estaría bien hacer un resumen para aquellos que no lo conozcáis. Todos los ejemplos que aparecen a continuación son parte del escrito de Konda. Obviamente, no se tratan aquí todas las novedades que hay en Java 7, pero sí algunas de las que serán más útiles a los desarrolladores.

   Konda empieza tratando de las mejoras del lenguaje que se solicitaron a través del Project Coin. Una de las mejoras que se reclamaban en este proyecto era sobre el uso de Genéricos. Un ejemplo del uso de Genéricos hasta Java 7 sería como sigue:
   Map<String, List<Trade>>  trades = new TreeMap<String, List<Trade>>( );
   Esto resulta un poco lioso, porque hay que declarar los tipos a ambos lados. Observad ahora como quedaría usando Java 7:
   Map<String, List<Trade>> trades = new TreeMap<>( );
   Mucho mejor, ¿verdad?. Ya no hay que declarar los tipos a la derecha, porque el compilador infiere de qué tipos se trata viendo los que hay a la izquierda. Incluso sería legal omitir el operador de Genéricos (diamond operator), así:
   trades = new TreeMap( );
pero el compilador lanzaría advertencias de seguridad.

   Otra de las peticiones que se hicieron en el proyecto Coin fue el poder usar strings en sentencias Switch. Hasta ahora, las sentencias Switch usaban o bien tipos primitivos o bien tipos enumerados. Con Java 7 se pueden usar también strings, que antes tendrían que ir en una serie de if-else como estos:
   private void processTrade(Trade t){
         String status = t.getStatus( );
            if(status.equalsIgnoreCase(NEW)){
              newTrade(t);
             }else if(status.equalsIgnoreCase(EXECUTE)){
              executeTrade(t);
             }else if(status.equalsIgnoreCase(PENDING)){
              pendingTrade(t);
             }
           }
   Comparadlo ahora en como queda al poder usar una sentencia Switch:
      public void processTrade(Trade t){
         String status = t.getStatus( );
         switch (status){
            case NEW : newTrade(t);
                  break;
             case EXECUTE : executeTrade(t);
                  break;
             case PENDING : pendingTrade(t);
                  break; 
             default : break;
           }
         }

   Konda habla a continuación del manejo automático de recursos tales como Files, Input/OutputStreams, etc., que los desarrolladores tienen que cerrar manualmente, pero que ahora Java 7 puede realizar de forma automática. Hasta ahora el proceso era el siguiente:
   public void oldTry( ){
        try{
               fos = new FileOutputStream("movies.txt");
              dos = new DataOutputStream(fos);
              dos.writeUTF("Java 7 Block Buster");
       }catch (IOException e){
               e.printStackTrace( );
       }finally{
            try{
                   fos.close( );
                  dos.close( );
            }catch (IOException e){
                     //maneja la excepción
           }
        }
     }
   Y así es como se haría con Java 7:
   public void newTry( ){
            try(FileOutputStream fos = new FileOutputStream("movies.txt");
                 DataOutputStream dos = new DataOuputStream(fos))
                {
                    dos.writeUTF("Java 7 Block Buster");
                 }catch (IOException e){
                        //maneja la excepción
                }
       }
   Eso sí, para que esto funcione, los recursos que queremos que se cierren automáticamente tienen que implementar la interfaz java.lang.AutoCloseable. 

   Otra de las novedades introducidas en Java 7 es el uso de la barra baja para separar números literales y que sea más fácil leerlos. Por ejemplo, si hasta ahora un millón se escribía así:
   1000000
ahora lo escribiremos de esta forma:
   1_000_000
   También se introducen los literales binarios, con lo que ya no hay que convertirlos a hexadecimales y nos ahorramos un trabajo.

   Se ha mejorado también el manejo de excepciones múltiples, que ahora se pueden atrapar usando un único bloque. Hasta ahora, si tenías un método con, por ejemplo, tres excepciones, tenías que atraparlas de forma individual. Con Java 7 se puede hacer lo siguiente:
   public void newMultiCatch( ){
      try{
         methodThatThrowsThreeExceptions( );
      }catch(ExceptionOne | ExceptionTwo | ExceptionThree e){
             //maneja las excepciones
      }
    }
   Sin embargo, si tienes excepciones que pertenecen a distintos tipos que deben manejarse de distinta manera, se puede hacer lo siguiente: 
   public void newMultiMultiCatch( ){
       try{
          methodThatThrowsThreeExceptions( );
       }catch (ExceptionOne e){
              //maneja ExceptionOne
       }catch (ExceptionTwo | ExceptionThree e){
             //maneja ExceptionTwo y ExceptionThree
       }
   }

   Java 7 tiene también una nueva API, NIO 2.0, que proporciona mejoras a la hora de trabajar con Java IO. El nuevo paquete java.nio.file viene con nuevas clases e interfaces como Path, Paths, FileSystem, FileSystems y otras. Path, por ejemplo, es el equivalente mejorado de java.io.File. Lo mejor para comprobar como funciona NIO 2.0 es ir a los docs de Oracle y ver allí cómo funcionan estas nuevas clases y métodos.

   Otras de las mejoras está relacionada con la multitarea, para lo que se han creado las nuevas clases ForkJoinPool y ForkJoinTask.

   Por último, hay que mencionar la creación de otro nuevo paquete, java.lang.invoke, que permite que  la JVM soporte mucho mejor la utilización de lenguajes dinámicos tales como Phyton, Ruby y Clojure. Algunas de las nuevas clases que incorpora este paquete son MethodHandle y CallSite.

   Os recuerdo que este trabajo y los ejemplos aquí expuestos son obra de Madhusudhan Konda, que lo ha publicado en O´Reilly. Si queréis leerlo completo, yo me he descargado una versión gratuita a través de Amazon, y supongo que O´Reilly también ofrecerá esta posibilidad. ; )
   





   
   

sábado, 10 de diciembre de 2011

Interfaces

votar
   Cuando se comienza a estudiar Java, se aprenden normalmente primero los conceptos de Clase, Objeto, Métodos, Tipos, Variables, etc. Y de repente un día comienzan a hablarte de interfaces. ¿Interfaces?, pero, ¿qué es eso?, se pregunta el novato nervioso. ¿Es una clase?, ¿es un método?, ¿cómo se usa?, ¿es lo mismo que la interfaz de usuario? (vale, este último no estuvo muy atento en ninguna de las clases). Tranquilos todos, que aquí os vamos a explicar las interfaces como si fueseis niños.

   Una interfaz en Java, en realidad no es más que un cajón donde guardar uno o varios métodos de forma que todas las clases que lo necesiten puedan cogerlos y emplearlos. La cuestión es que esos métodos de las interfaces no tienen cuerpo, están vacíos; vamos, que no son más que el nombre, como si fuesen el método de una clase abstracta, así que cuando una clase va a utilizarlos tiene que implementarlos o desarrollarlos.

   Y por qué, te preguntarás, no puede una clase escribir directamente sus propios métodos, sin necesidad de implementar los de una interfaz. Bueno, aquí es donde viene el ejemplo explicativo.

   Supongamos que tienes una clase llamada Vehículos que tiene varias subclases: Coche, Moto, Camión, Autobús y Bicicleta. Esta clase Vehículos tiene una serie de métodos que por supuesto todas sus subclases heredan, como arrancar( ), frenar( ) o velocidad( ). Pero resulta que Coche, Camión y Autobús necesitan un método extra abrirPuertas( ). ¿Dónde lo pondrías?. Si lo colocas en Vehículos lo heredarían también Moto y Bicicleta, que no tienen ninguna puerta que abrir. Si estuviésemos en otro lenguaje, como C++ que permite la multiherencia podríamos hacer que las clases con puerta fuesen además de subclases de Vehículos, subclases de otra clase distinta con el método abrirPuertas( ), pero esto es Java. Claro, podemos escribir tres métodos distintos para cada una de las clases que lo necesiten, pero hay una forma mucho más económica de hacerlo: ir al cajón-interfaz que tenga este método y cogerlo.

   Así, no sólo estas clases pueden utilizar este método y adaptarlo a sus necesidades, sino que en realidad, cualquier otra clase no relacionada con ellas puede hacerlo también. Por ejemplo, si tenemos la clase Vivienda, también ella y todas sus subclases pueden utilizar el método abrirPuertas( ) sin tener ninguna relación con Vehículos ni sus subclases.

   La palabra mágica a utilizar es implements:
      class Coche extends Vehículos implements Abrible //Abrible es el nombre de la interfaz

   Una clase sólo puede extender otra (es una familia monoparental) pero puede implementar todas las interfaces que necesite. Es más, una interfaz también puede extender (que no implementar) otras interfaces. Esto es útil sobre todo cuando tú ya has escrito una interfaz y más adelante quieres añadirle otro método. Si lo haces en la vieja interfaz, todos los programas que estén usándola dejarán de funcionar porque ellos no implementan ese nuevo método. Así que puedes crear una nueva interfaz que extienda la antigua y que utilicen sólo los que necesiten ese nuevo método.

  ¿Cómo se escribe una interfaz?. Es muy similar a una clase:
   public interface Interface extends Interface2, Interface3{
             void abrirPuertas( );
   }

   Por supuesto, extends sólo se usa si la interfaz extiende alguna otra. El modificador public es para que cualquier clase pueda utilizar la interfaz. Si omites este modificador sólo las clases dentro del mismo paquete de la interfaz podrán utilizarla.
   Todos los métodos de una interfaz son implícitamente públicos y abstractos, así que no hay necesidad de añadirles esos modificadores.
   Las interfaces, además de métodos pueden tener constantes, que son siempre públicas, estáticas y finales, por lo que tampoco hay que añadirles esos modificadores. Por ejemplo:
      double Pi = 3.14;

   Cuando una clase implementa una interfaz, debe implementar todos los métodos de esa interfaz, y seguir todas las reglas de sobrescritura (override) de esos métodos.

   Además, una interfaz puede ser usada como un tipo de referencia en cualquier clase que la implemente, igual que si fuese un tipo primitivo o un objeto:
      Abrible ob1 = (Abrible)ob1;
   De esta forma, ob1 puede utilizar el método abrirPuertas( ) y es considerado no sólo como tipo de su clase, sino también como tipo de la interfaz.

   Si a pesar de la sencillez que hemos intentado usar en la explicación tienes alguna dificultad en seguirla, mándanos un mensaje o entra en la web JavaParaNulos donde encontrarás un Tutorial para principiantes.
   

sábado, 3 de diciembre de 2011

HashCode( ) y equals( )

votar
   Hoy vamos a hablar de dos métodos que están muy relacionados: equals( ) y hashCode( ). Estos dos métodos están en la clase Object, y como esta clase es la madre de todas clases, todas ellas heredan esto métodos. Por supuesto, como ya vimos en otro post, estos métodos pueden ser overriden (sobreescritos o sobrepasados).

   Estos métodos son muy utilizados al usar colecciones (ya hablaremos de ellas en otra entrada) pero hay que utilizarlos bien y saber cómo sobreescribirlos para no meter la pata.

   Expliquemos en qué consisten. El método equals( ) compara dos objetos y devuelve true si son iguales. Utiliza el operador (= =). ¡No confundir nunca con (=) que asigna un valor!. Cuando compara dos tipos primitivos es sencillo: 

   2300 = = 2300;
   2´575 = = 2´575;

   pero cuando comparamos objetos la cosa cambia, porque lo que equals( ) compara son las referencias a los objetos y no los mismos objetos, y si las referencias no son iguales considera que los objetos tampoco, aunque sean significativamente equivalentes. Lo entenderemos mejor explicando este ejemplo que podéis encontrar en el tutorial de Oracle.

   Supongamos que hay dos ejemplares de un libro:

   Libro primerEjemplar = new Libro( );
   Libro segundoEjemplar = new Libro( );

   Aunque sean dos ejemplares del mismo libro, y, por lo tanto, equivalentes para nosotros, el método equals( ) los tratará como distitntos a no ser que lo sobreescribamos o sobrepasemos. Ahora bien, los libros tienen un número de identificación, ISBN, que es igual para todos los ejemplares del mismo libro. Usando este número como referencia, podemos sobreescribir equals( )

   public class Libro{
      public boolean equals(Object o){
         if(o instanceof Libro) //nos aseguramos de que el objeto en cuestión es un libro
            return ISBN.equals((Libro)o.getISBN( )); //hacemos un casting para poder usar el nuevo método     equals()
              else
                return false;
      }
   }

   Veamos ahora cómo quedaría:
   
   Libro primerEjemplar = new Libro("0201914670"); //el mismo número que el ejemplo del tutorial
   Libro segundoEjemplar = new Libro("0201914670");
      if(primerEjemplar.equals(segundoEjemplar)){
         System.out.println("Los dos objetos son iguales");
      }else{
         System.out.println("Los objetos no son iguales");
      }
   
   Ahora, los dos ejemplares del mismo libro son considerados iguales porque lo que se compara es su ISBN.

   El método equals( ) tiene un contrato o normas que hay que cumplir al utilizarlo. Son las siguientes, y no, no estás en clase de matemáticas:

  • Es reflexivo. Para cualquier valor x, x.equals(x) debe devolver true.
  • Es simétrico. Para cualesquiera valores x e y, x.equals(y) es cierto si y sólo si y.equals(x) devuelve true.
  • Es transitivo. Para cualesquiera valores x, y y z, si x.equals(y) devuelve true e y.equals(z) devuelve true, entonces x.equals(z) debe devolver true.
  • Es consistente. Para cualesquiera valores x e y, x.equals(y) debe devolver consistentemente true o false mientras no se modifique ningún dato utilizado en la comparación.
  • Para cualquier valor x no vacío, x.equals(null) debe devolver falso.


   Además, si sobreescribes el método equals( ), también debes sobreescribir el método hashCode( ) ¿Por qué?.
   
   El valor que devuelve el método hashCode( ) es la alocación en la memoria del objeto, expresada en un número hexadecimal. (Excepto los supergeekies, este método normalmente se sobreescribe para que devuelva algo más comprensible al común de los mortales). Esto significa que, antes de sobreescribir el método equals( ), dos referencias a un objeto son iguales cuando el objeto es el mismo y por tanto ocupa el mismo lugar en la memoria. Pero al sobreescribir el método equals( ), vimos antes que estamos asignando la misma referencia a dos ejemplares distintos de un mismo libro. Así que el método hashCode( ) ya no es válido a no ser que lo sobreescribamos también, porque la regla básica es que si dos objetos son iguales según equals( ), su hashCode debe ser también igual.

   El hashCode viene siendo, pues, como el número de identidad de un objeto. Pero varios objetos pueden tener el mismo número de identidad. Incluso objetos distintos según equals( ).

   Imaginemos una cajonera donde en un cajón guardamos sólo bolígrafos, en otro sólo lápices, en otro sólo papel, etc. Cuando queremos un bolígrafo, sabemos perfectamente a cuál cajón ir. Ése es su hashCode. Pero resulta que no queremos un bolígrafo cualquiera, si no uno con tinta verde. Ahí es donde usamos el método equals( ). Vemos que todos los bolígrafos tienen el mismo hashCode, pero no todos son iguales. Eso sí, si dos objetos son bolígrafos, se guardan en el mismo cajón.

   HashCode( ) también tiene un contrato, que es como sigue:
  • Cuando durante la ejecución de una aplicación se invoque más de una vez el método hashCode( ), éste debe devolver consistentemente el mismo número, si no hay ningún cambio en equals( ). En distintas ejecuciones de la misma aplicación, el número puede variar.
  • Si dos objetos son iguales según equals( ), su hashCode es el mismo.
  • Si dos objetos son distintos según equals( ), su hashCode puede ser distinto o no.

   Claro, es mucho más eficiente a la hora de encontrar el bolígrafo deseado si estuvieran clasificados en distintos cajones según el color, pero a veces no tenemos tantos cajones, y otras veces nos da igual el color...como ya veremos al hablar de colecciones.

   Los que os sintáis un poco superados por este tema, podéis leer el libro de Kathy Sierra y Bert Bates "SCJP Sun Certified Programmer for Java 6". Los que queráis ampliarlo tenéis "Effective Java", de J. Bloch.
Y por supuesto, no olvidéis visitar Java Para Nulos ; )

   

sábado, 26 de noviembre de 2011

Manejo de Excepciones en Java

votar
Una excepción se produce cuando una condición excepcional interrumpe o altera el curso normal de un programa. Por ejemplo, un método que debe abrir un archivo se encuentra con que tal archivo no existe, o se intenta realizar una división siendo el divisor cero.

El manejo de excepciones consiste en escribir el código necesario para capturar una excepción y resolverla o al menos informar al usuario de lo que ocurre y que no se asuste. No es lo mismo ver una ventanita que nos informa amablemente de que no se encuentra el archivo o que inserte un disco a que la aplicación se quede muerta o ver un galimatías de números y letras (recordemos que el usuario final no suele ser un programador).

Como en Java todo se reduce a objetos, cuando ocurre una excepción el método que la provoca crea un objeto con información sobre el error y el sistema busca algo en el programa para manejarlo. Estos objetos, por supuesto, tienen que pertenecer a una clase. En Java, las excepciones derivan todas de la superclase Throwable, que está en el paquete java.lang. Throwable significa "arrojable" o "lanzable" en inglés, porque en Java las excepciones se "lanzan" o "arrojan" y hay que "pillarlas" o manejarlas con un gestor de excepciones (exception handler). De Throwable derivan las clases Error y Exception, y de esta última deriva la clase RuntimeException, cuyos objetos son errores que se producen en tiempo de ejecución.

Las excepciones pueden producirse por fallos de programación o por fallos imprevisibles del sistema. Estos últimos son los que forman la clase Error, de la que el sistema no se espera que se recupere y por eso no se requiere que se manejen. Vamos, que si te has quedado sin memoria, se va a ir todo a la porra hagas lo que hagas.

Entre las subclases de Error tenemos IOError, AWTError, CoderMalfunctioError, AssertionError, VirtualMachineError o mi favorita: ThreadDeath, que parece el título de una película. Pero hay muchas más.

Una de las grandes controversias en Java es que obligatoriamente hay una serie de excepciones que hay que manejar, quieras o no. Unos lo ven innecesario, otros imprescindible. Nosotros no vamos a mojarnos, sólo a explicar en qué consiste eso.

Las checked exceptions, o excepciones revisadas son las que derivan de la clase Exception, menos RuntimeException. Son las que se supone que hay que prever y de las que la aplicación se puede recuperar. Si no las manejas o las declaras, no podrás compilar tu programa.

Esto se hace de la siguiente manera: un método que pueda lanzar una excepción, bien lo hayas escrito tú, bien esté en el API, debe declararlo así
      public void miMétodoArriesgado() throws NombreDeLaExcepción;

Después, al utilizar ese método en el programa, hay que hacerlo dentro de un bloque try-catch (prueba y captura), que básicamente viene a decir: prueba a usar este método, y si produce una excepción, captúrala y haz lo que debas con ella. 
      try{
         public void miMétodoArriesgado(); //método que puede producir una excepción
      }catch (NombreDeLaExcepción e){
            //código que indica qué hacer si se produce
      }

Por supuesto, si la excepción no se produce, el código que escribimos dentro de catch no se ejecutará, si no que el programa seguirá normalmente. Además, si queremos que algo se ejecute independientemente de que surja o no una excepción, escribimos finally después de catch. Todo lo que va dentro de finally se ejecuta siempre, así que es muy útil para atar cabos sueltos, como cerrar archivos, etc.

Si no queremos usar un bloque try-catch para manejar la excepción, siempre nos queda la opción de esquivarla. Esto es como si alguien te lanza una pelota y tú te agachas con la esperanza de que otra persona la coja. Porque en alguna parte del programa, "alguien", léase un método, tiene necesariamente que pillar la excepción. El sistema va de método en método, hasta llegar a main() si hace falta, buscando un bloque de código que se ocupe de manejarla. Personalmente, esquivar la excepción no me gusta demasiado. Creo que es mejor lidiar con ella cuanto antes y listo. Pero a veces, por necesidades de programación, es mejor dejarla pasar y que otro método se ocupe. En ese caso hay que declarar el método como vimos, especificando la excepción que lanza.

Como sublcases de Exception tenemos IOExcepton, SQLException, ApplicationException, AWTException, BadLocationException, unas cuantas (muchas) más que hay en la librería Java y todas las que se te puedan ocurrir para tus métodos arriesgados. Porque por supuesto tú puedes crear tus propias excepciones.

Por último, están las RuntimeException y sus subclases, que como ya comentamos, no son excepciones revisadas, sino unchecked, es decir, no se revisan en el momento de la compilación. Estas excepciones tienen en común con las de clase Error que no se pueden anticipar ni normalmente recuperar de ellas, pero al contrario que estas, no se producen por fallos de sistema, sino casi siempre por fallos de programación, como errores de lógica. No es necesario declarar ni manejar estas excepciones, pero puedes hacerlo si quieres.

Dentro de RuntimeException podemos encontrar ArithmeticException,CannotRedoException, CannotUndoException,NullPointerException,NumberFormatException,ClassCastException,IndexOutOfBoundsException o BufferOverflowException. Pero claro, hay muchísimas más.

Si quieres dejar algún comentario o pregunta, ya sabes como ;)

  

   

sábado, 19 de noviembre de 2011

Overload vs. Override en Java

votar
   Overload y override, al ser dos términos que empiezan igual y al ser ambos utilizados con métodos, son muy dados a confusiones.

   He visto traducidos estos verbos al castellano como sobrecargar y sobrepasar, que utilizaré también aquí, pero a los que estéis pensando estudiar programación os recomiendo encarecidamente que, si no aprendéis a hablar inglés, al menos aprendáis a leerlo. Pensad que la mayoría de las novedades y libros de consulta sobre programación se escriben en ese idioma, y que, o bien no se traducen nunca al castellano, o cuando lo hacen, a veces ya han quedado desfasados.

   Volviendo con el tema, un método overloaded o sobrecargado no es más que la reutilización del nombre de un método. En la entrada anterior veíamos que esto también lo hacían los constructores.

   Supongamos que tienes el método suma( ) que tiene como argumentos a y b de tipo int y que utilizas ese método para sumar los argumentos

      int suma(int a, int b){
         //código que sume a+b
      }


pero después te encuentras con un par de decimales que también quieres sumar. Bastará con que hagas esto:

      double suma(double c, double d){
         //código que sume c+d
     }

   Como habéis visto, nos hemos limitado a cambiar los argumentos. El tipo de retorno es también diferente, pero eso no es necesario para sobrecargar un método. Podríamos haber escrito en los dos métodos void suma() y seguiría siendo overload.

   Lo importante es que los argumentos sean distintos, para que el compilador sepa qué método usar en cada caso. Eso sí, es mucho más lógico usar métodos con el mismo nombre que hagan lo mismo, porque sería un poco tonto llamar suma a un método que suma enteros y a otro que reste, aunque sea a decimales.

   Otros puntos a tener en cuenta son que al ser diferentes métodos pueden tener distintos modificadores de acceso (uno puede ser public y otro private, por ejemplo) y pueden lanzar diferentes excepciones. Los métodos de las subclases pueden sobrecargar a los métodos de las superclases, y aquí es donde a mayor confusión se presta con override.

   Override o sobrepasar un método es implementar eses método de una manera diferente. Por supuesto, eso es lo que hace una subclase con los métodos abstractos de su superclase, pero no es necesario que un método sea abstracto para que sea sobrepasado.

   Imaginemos un juego que tiene una clase Vehículos que a su vez tiene un método correr(). Esta clase tiene dos subclases, Coches y Motos que heredan el método correr() pero cada una lo implementa de forma distinta dependiendo de las diferentes características de las dos clases de vehículos. Eso es sobrepasar un método:

      public class TestVehículos{
         public static void main(String[] args){
            Vehículos v = new Vehículos();
            Coches c = new Coches();
            Motos m = new Motos();
            v.correr(); //ejecuta la versión correr() de Vehículos
            c.correr(); //ejecuta la versión correr() de Coches
            m.correr(); //ejecuta la versión correr() de Motos
         }
      }

   Ahora bien, sabemos que, aplicando el polimorfismo (otro día hablaremos de esto), podemos hacer algo así:

      Vehículos c = new Coches();
      Vehículos m = new Motos();
      c.correr();
      m.correr();

   ¿Qué versión de correr() se ejecutaría en estos casos? En tiempo de ejecución se aplica el tipo del Objeto (Coches y Motos) no el tipo de la variable de referencia (Vehículos), así que se ejecutará el método correr() de Coches y Motos.
 
   Pero cuidado, supongamos que la clase Coches tiene el método abrirPuertas(). Lo siguiente no sería legal:

      Vehículos c = new Coches();
      c.abrirPuertas();

   Porque el compilador no nos dejará que apliquemos un método de la clase Coches a una referencia de la clase Vehículos que no tiene ese método.

   Un método que sobrepase a otro no puede tener un modificador de acceso más restrictivo que el método sobrepasado, pero si más amplio. Por ejemplo, si correr() en Vehículos es protected, en Motos podría ser public pero no private.


   Tampoco puede lanzar excepciones nuevas o más amplias. El tipo de retorno tiene que ser el mismo o bien el de una subclase (esto último desde Java 5).

   Pero sobre todo, lo que nos ayuda a distinguir un overriding de un overloading es que si en este último la lista de argumentos tenía que ser distinta, en el primero debe ser exactamente igual.

   Espero que esta entrada os haya ayudado a aclarar conceptos, si no, ya sabéis que podéis enviar vuestras preguntas y comentarios.

sábado, 12 de noviembre de 2011

Constructores en Java

votar
   Parece una tontería y sin embargo, los constructores son de las cosas que más problemas causan a los programadores noveles.
   Los constructores sirven para crear nuevos objetos. Por ejemplo:
class Gata{
     public Gata( ){}
    Gata g = new Gata( );
}
   Bueno, en este ejemplo, no parece que un constructor parezca ser algo muy útil, y en muchas ocasiones, el programador no se molesta en escribirlo. Pero eso no significa que no esté ahí, porque si tú no lo escribes, el compilador siempre, siempre, siempre, pondrá el constructor por defecto, ya que si no, no sería posible crear ningún objeto. Así que, si escribimos esto:
class Gata{
    Gata g = new Gata( );
 }
el compilador interpretará esto:
class Gata{
    public Gata( ){
           super( );
    }
    Gata g = new Gata( );
}
   ¿Qué es eso de super( ); te estarás preguntando?. Bueno, todos los objetos en Java son parte de un enorme árbol genealógico. Todas las clases son descendientes, o subclases, de otra, aunque sólo sea de una, la madre de todas las clases: la clase Objeto. Cuando se ejecuta el constructor de una clase, ese constructor llama al constructor de su superclase, que también se ejecuta y a su vez llama al constructor de su superclase...y así hasta llegar a la clase Objeto. Ese super( ); es la llamada explícita al constructor de la superclase.
   ¿Y por qué hacer una llamada explícita si el compilador la hace por ti?. Supongamos este ejemplo:
class Mascota{
    private String nombre;
    public Mascota(String n){
        nombre = n;
    }
    public String getNonbre( ){
        return nombre;
    }
class Gata extends Mascota{
    public Gata( ){ }//escribes esto
       // super( ); pero es esto otro lo que hay en realidad    
}//Oh, oh, error de compilación.
   Efectivamente, el compilador hace una llamada al constructor por defecto de la superclase. El problema, es que la superclase y la clase no tienen el mismo constructor. El constructor por defecto no tiene argumentos, pero en este caso vemos que el constructor de Mascota tiene uno: String n.
   ¿Pero Mascota no tiene un constructor por defecto?. No, en el momento en que se escribe un constructor, el compilador no ve necesario añadir el constructor por defecto.
   Por supuesto, pueden coexistir varios constructores a la vez, así que si el programador escribe:
class Mascota{
    private String nombre;
    public Mascota(String n){
        nombre = n;
    }
    public Mascota( ){ }
    }
class Gata extends Mascota{
    public Gata( ){ }
}
el compilador no protestará porque puede llamar al constructor sin argumentos de la superclase. Veamos con   un ejemplo cómo funciona esta llamada a super( );
class Mascota{
    private String nombre;
    public String getNombre( ){
        return nombre;
    }
    public Mascota(String n){
        nombre = n;
        System.out.println("Esta es mi mascota");
}
class Gata extends Mascota{
    public Gata(String n){
        super(n);
    }
    public static void main(String[ ] args){
        Gata g = new Gata("Linda");
        System.out.println("El nombre de mi gata es " + g.getNombre();
    }
}
El resultado al ejecutar la clase Gata sería:
Esta es mi mascota
El nombre de mi gata es Linda
   Vemos que primero se ejecuta el constructor de la superclase y luego el de la clase.

   Además un constructor puede llamar a otro constructor de la misma clase usando this( ) Por ejemplo:
class Gata{
    String nombre;
    Gata(String n){
        nombre = n;
    }
    Gata( ){
        this("Bonita");
    }
    public static void main(String[ ] args){
        Gata g1 = new Gata("Linda");
        Gata g2 = new Gata( );
        System.out.println(g1);
        System.out.println(g2);
    }
}
   El constructor sin argumentos hace una llamada al primer constructor, que tiene un argumento de tipo String, y el resultado sería:
Linda
Bonita
   Tanto super( ) como this( ) deben usarse en la primera línea del constructor, pero no pueden usarse a la vez.

   Por último, no hay que confundir los constructores con los métodos. Los constructores siempre tienen el mismo nombre que la clase (y ningún programador que se precie escribiría un método con el nombre de la clase, aunque es factible), pero además, los constructores no tienen tipo de retorno, ni siquiera void, y los únicos modificadores que pueden usar son los de acceso.

   En fin, espero que este post os haya servido para clarificar algunas cosas respecto a los constructores.