martes, 6 de noviembre de 2012

Tan recursivos pues!

El tema que me lleva hoy a escribir es la idea de recursión. Bueno, es uno de esos temas difíciles de explicar la primera vez y requiere algunas claridades frente a como funcionan las invocaciones a métodos.

¿Cómo funciona un método?

Cuando uno invoca un método, se "abre" espacio en la computadora para ejecutar un cierto conjunto de instrucciones (el contenido del método). Cuando se realizan muchas invocaciones, mucha memoria RAM del computador está siendo usada para ello. En una versión gráfica, es como si creáramos una caja que nos resuelve un problema dadas unas entradas y asegurando unas salidas.

              +-----------------------+
              |     Metodo!           |
         .    |                       |      .
   ........   |                       |   .....
   Entradas   |                       |  Salidas
              |                       |
              |                       |
              +-----------------------+


De esta manera, un método que tiene la responsabilidad de sumar dos números recibiría (entrada) como entrada dos elementos del conjunto de número enteros y devolvería (salida) un elemento del conjunto de números enteros. Para resolver dicha operación se crearía una "caja" simple que resolvería esa operación.

Cuando un método llama otro método, se abren varias "cajas" que esperan resultados unas de las otras, recordemos que un método tiene una responsabilidad definida. Cuando la resolución de un problema es compleja, se debe romper el problema en partes más sencillas, así mismo cuando un método se está complicando, es importante partirlo en métodos más simples. Cuando un método llama a otro, se crea algo llamado "pila de llamados".

  
  +---------+
  | Jugar   |
  +---------+
  | Menu    |
  +---------+
  | Main    |
  +---------+

La anterior pila de llamados representa un programa que inició con un llamado al método Main (como todo programa en Java), a su vez este llamó a un método que se llama Menú y a su vez este llamó a un método que se llama Jugar. En este momento la pila de llamados tiene en su punta el método jugar, que posiblemente es el que está ejecutando. Cuando este método termine, el método menú recibirá su respuesta y así pasará cuando termine el método Menú. Cada método tiene su espacio para guardar cosas, entre ellas las variables que se declaran dentro de él, sus parámetros y demás cosas.

Ahora sí, a lo que vinimos

Resulta que una recursión es un llamado de un método a sí mismo para la resolución sucesiva de un problema. En términos de la pila de llamados, se generarían espacios del mismo método. Supongamos una función que simplemente se llama a sí misma:

public static int metodo(int parametro)
{
   return metodo(parametro+1);
}

Resulta que el método de nuestro ejemplo crearía un número infinitos de entradas en la pila de llamados (teóricamente, porque en la práctica esa pila tiene límites y al sobrepasarlos se genera un error llamado StackOverflow o volcado de pila). Esto se vería más o menos así: (si el primer llamado fúe metodo(1))

     ...
 +---------+
 |metodo(3)|
 +---------+
 |metodo(2)|
 +---------+
 |metodo(1)|
 +---------+


Resulta que el llamado metodo(1) está esperando que el llamado metodo(2) termine, al igual que este último espera por metodo(3) y metodo(3) por metodo(4) y así sucesivamente. Respecto al ejemplo de Main->Menú->Jugar no cambia mucho, excepto por el hecho de que todas las componentes de la pila tienen el mismo nombre. En el código de ejemplo, estos llamados nunca terminarán porque siempre se genera un llamado nuevo. Esto resultará en un StackOverflow... Para detenerlo necesitamos un caso donde no se hagan llamados recursivos, a esto lo llamaremos posteriormente un caso base. Modifiquemos el código del anterior ejemplo para que eso pase cuando el parámetro llegue a un cierto valor, por ejemplo M.

public static int metodo(int parametro)
{
    if(parametro == M)  
    {
        return 0;
    } 
    return metodo(parametro+1);
}

En ese caso los llamados recursivos se expanderían hasta que el valor de la variable parametro llegue a M, cuando llegue, se retornará cero a todos los métodos anteriores. Aunque este método ya no tiene un StackOverflow, es un método sin mayor utilidad, pero revisemos el siguiente:

public static int factorial(int parametro)
{
   if(n==1)
    {
        return 1;
    }
    else
    {
        return parametro* factorial(parametro-1);
    }
}


En este ultimo caso, tenemos un caso en que se detiene (cuando n sea 1) y uno donde expande el llamado (n diferente de 1). De esta manera tenemos un caso base y un caso inductivo. Espero les sirva!

jueves, 1 de noviembre de 2012

Lectura...pero de archivos

En este post me voy a dedicar a revisar una forma sencilla de leer y escribir sobre un archivo de texto, pero primero lo primero....

Archivos... Para quien no sabe qué es uno

Bueno, pues en general los archivos son bloques de datos ordenados que se pueden almacenar de manera secuencial en un dispositivo magnético, óptico o similar. Son los que nos llenan el disco duro del computador!. Básicamente hay dos tipos de archivos, los archivos texto y los archivos binarios.

Los archivos de texto esencialmente guardan eso: texto. Se podría pensar que guardan una colección de Strings separados por el carácter de cambio de linea y para leerlos literalmente recorreremos el archivo como si de una colección de cadenas de caracteres se tratara.

Los archivos binarios permiten guardar algunas otras cosas, normalmente representaciones de los objetos que trabajamos en un programa "como los trajeron al mundo", osea en binario. En este post me dedicaré a los primeros...

Nada mejor que un ejemplo

Lo primero que vamos a hacer es leer un archivo sencillo con personas y números de teléfono. Suponga que el archivo contactos.txt tiene el siguiente contenido:

Pepito Perez;4243430
Fulano Rodríguez;6508765

Para leerlo vamos a necesitar varias cosas...
1. La clase File: La clase File de java es una representación de un archivo en un sistema de archivos (¿De verdad?). Las carpetas (directorios) tambien pueden ser representados con esta clase. Entre otras uno puede instanciarla con un archivo para crearlo, saber su carpeta, listar los archivos que están en ella, etc. La usaremos para representar el archivo especifico que vamos a leer.
2. La clase FileReader: Esta clase permite abrir el archivo, es la clase que sabe acceder al disco para abrir un archivo.
3. La clase BufferedReader: Esta clase nos genera un lector con el que podamos leer "pedazo a pedazo" el archivo. Nos provee métodos para leer las líneas del archivo y saber si se acabó.

Miremos el siguiente código fuente:

public ArrayList<Persona> cargarArchivo ()  
      {  
           try   
           {  
                ArrayList<Persona> resultado = new ArrayList<Persona>();  
                File archivo = new File("../data/contactos.txt");  
                BufferedReader lector = new BufferedReader(new FileReader(archivo));  
                String linea = "";  
                int i=0;  
                while (( linea = lector.readLine()) != null )  
                {  
                     String[] partes = linea.split(";");  
                     Persona p = new Persona(partes[0], new Integer(partes[1]));  
                     resultado.add(p);  
                }
                lector.close();
                return resultado;  
           }   
           catch (FileNotFoundException e)   
           {  
                System.out.println("Archivo no encontrado");  
                return null;  
           }   
           catch (IOException e)   
           {  
                System.out.println("Error de lectura");  
                return null;  
           }  
           System.out.println("Carga del archivo exitosa!");  
           return new ArrayList<Persona>();  
      }  

Vamos por partes, dijo Jack el destripador....

El método que está en el ejemplo anterior carga las personas que están en el archivo en un ArrayList y los retorna. Algunas cosas que ver en este código.

1. las clausulas try, catch: En principio estas clausulas no tienen nada que ver con el tema de este post, de hecho son mecanismos que tiene java para manejo de Excepciones, en resumen una excepción es un error y estas clausulas sirven para saber que hacer con él. El bloque de código que se escribe dentro del try simplemente se realizará (aquí va lo que nos interesa), la clausula catch sirve para atrapar ciertos tipos de error, como pueden ver hay dos de ellas, una para error "no encontré el archivo" y otra para "error de lectura". El contenido de esos bloques es código que se ejecutará en caso que esos errores se presenten.

2. File: En el ejemplo se crea un nuevo archivo, el parámetro que se le envía podría ser solamente el nombre del archivo, con lo que buscaría en archivo en la ubicación donde la clase está corriendo (en la misma carpeta). En el ejemplo se usa el comodín ".." que significa "una carpeta arriba". después la carpeta data y el nombre del archivo. 

3. Los lectores. En el ejemplo se instancia un BufferedReader con un FileReader con un File adentro. De esta manera tendremos acceso por fin a lo que necesitamos.

4. El método readLine: Hace lo que dice, lee una linea y luego la próxima en cada llamado. retornará null si ya se acabó el archivo, por eso está en esos términos la condición del ciclo while.

5. Split: Es una utilidad que nos da la clase String, sirve para romper un String en partes dada una cadena, en este caso la usaremos para romper un "renglón" del archivo en dos partes, la que tiene el nombre y la que tiene el número de teléfono. El resultado del método es un arreglo de String con cada parte.

6. Close: Al final sería decente cerrar el archivo que se está usando... por programas mal hechos que no usan esa instrucción es que a veces no podemos sacar nuestras memorias USB de un computador, porque un programa se quedo con un archivo abierto!

En una próxima ocasión escribiré acerca de como crear y escribir en archivos de texto!