domingo, 10 de marzo de 2019

Ordenamiento mezcla natural: objetos

Como ejemplo final en esta serie, veremos cómo ordenar un objeto que nosotros definimos. Vamos usar un objeto Persona que tiene nombre y edad. El único requisito para poder ordenarlo es que podamos compararlo. Esto lo logramos implementando el método compareTo() de la interface Comparable, nuestra implementación ordenará primero por edad y luego por nombre.

Las implementaciones de Lector y Escritor son exactamente iguales a las del ejemplo anterior en donde ordenamos enteros grandes.

Para crear nuestro archivo de prueba, leemos nuestro archivo de texto con nombres y a cada nombre le asignamos una edad para crear un objeto Persona. Noten que la clase Persona también implementa la interface Serializable. Esta interface no requiere que implementemos ningún método y nos permite serializar el objeto para escribirlo y leerlo de archivos.

Espero que los ejemplos en esta serie sirvan como base por si desean ordenar otro tipo de datos.

$ javac MezclaNaturalGenerico.java MezclaNaturalEjemplo4.java
$ java MezclaNaturalEjemplo4
Error en el ordenamiento
Fusion 1
Fusion 2
...
EL ARCHIVO ESTA ORDENADO
MezclaNaturalEjemplo4.java

domingo, 3 de marzo de 2019

Ordenamiento mezcla natural: enteros grandes

Ahora veremos como ordenar enteros grandes con el algoritmo genérico de mezcla natural. Este ejemplo es muy parecido al primer ejemplo donde ordenamos cadenas binaria. Pero en lugar de usar Data[Input|Ouput]Stream ahora usamos Object[Input|Output]Stream. Esto nos permite leer y escribir objetos serializados en lugar de sólo datos primitivos.

La principal diferencia, que me tomó por sorpresa, es que el método available() no regresa 0 para indicar fin del archivo. En este caso, tenemos que capturar EOFException para detectar cuando acabamos de leer los datos.

El programa crea y ordena un archivo con 100,000 enteros.

$ javac MezclaNaturalGenerico.java MezclaNaturalEjemplo3.java
$ java MezclaNaturalEjemplo3
Error en el ordenamiento
Fusion 1
Fusion 2
...
EL ARCHIVO ESTA ORDENADO
MezclaNaturalEjemplo3.java

sábado, 2 de marzo de 2019

Ordenamiento mezcla natural: cadenas

La versión original de este algoritmo ordena cadena binarias porque fue un requerimiento del proyecto. Pero ahora que ya tenemos la versión genérica, podemos ordenar otros datos. Empecemos por ordenar cadena simples (no binarias). Nuestro archivo de entrada tiene un nombre por línea.

Noten que en el Lector tenemos que obtener la línea por adelantado para poder implementar adecuadamente hasNext().

Después de ejecutar el ejemplo, pueden corroborar el resultado abriendo el archivo nombres_copia.txt.

$ javac MezclaNaturalGenerico.java MezclaNaturalEjemplo2.java
$java MezclaNaturalEjemplo2
MezclaNaturalEjemplo2.java

domingo, 24 de febrero de 2019

Ordenamiento mezcla natural: cadenas binarias

Ahora veremos cómo usar el algoritmo genérico de mezcla natural con cadenas binarias. Tenemos que implementar las dos interfaces que permiten aislar los detalles de lectura y escritura de datos.

  public static class Lector implements MezclaNaturalGenerico.Lector<String> 
  public static class Escritor implements MezclaNaturalGenerico.Escritor<String>

La implementación de estas dos interfaces es bastante directa. Por ejemplo, el método next() ecapsula readUTF() y el método hasNext() encapsula dis.available() != 0. También comentaba que el algoritmo se inicializa con fábricas para generar Lectores y Escritores. Eso lo podemos ver en esta línea:

  new MezclaNaturalGenerico<>(Lector::new, Escritor::new);

Vale la pena explicar qué está pasando aquí. La forma extendida sería implementar la interface que está esperando el constructor.

  static class FabricaLectores implements
      Function<File, MezclaNaturalGenerico.Lector<String>> {
    @Override
    public MezclaNaturalGenerico.Lector apply(Object o) {
      return new Lector(o);
    }
  }

Y la usamos así:

  new MezclaNaturalGenerico<>(new FabricaLectores(), ...);

La parte principal de FabricaLectores es new Lector(o), lo demás sólo causa ruido. Dado que Function es una interface funcional, es decir, que sólo tiene un método abstracto, la podemos reemplazar con una lambda.

  new MezclaNaturalGenerico<>(o -> new Lector(o), ...)

Pero como la única instrucción de la lambda es un new, la podemos reemplazar por una referencia al constructor. Y así es cómo llegamos al resultado final.

new MezclaNaturalGenerico<>(Lector::new, ...);

Al ejecutar este ejemplo deben obtener lo siguiente:

$ javac MezclaNaturalGenerico.java MezclaNaturalEjemplo1.java
$ java MezclaNaturalEjemplo1
Error en el ordenamiento
Fusion 1
...
EL ARCHIVO ESTA ORDENADO
1) AARON
2) ABBEY
3) ABBIE
4) ABBY
5) ABDUL
...
MezclaNaturalEjemplo1.java

lunes, 18 de febrero de 2019

Ordenamiento mezcla natural: algoritmo genérico

Esta serie de posts está inspirado en uno de nuestros posts más populares: Ordenamiento externo: Mezcla Natural. En ese post recibimos algunas preguntas de cómo reusar el código para ordenar otro tipo de datos. El código original sólo maneja cadenas en formato binario. Por lo que decidí actualizarlo para manejar cualquier tipo de dato. En este post hablaré de los cambios necesarios y en los siguientes posts mostraré ejemplos de cómo usarlo. El algoritmo base no cambió, lo que tuve que aislar es el tipo de dato y la forma en la que lo leemos y escribimos.

Tipo de dato.

El tipo de dato lo podemos ver por las menciones del tipo String a lo largo del código original. Tenemos que reemplazar String con un tipo genérico. La única operación que necesitamos del tipo de dato es que lo podamos comparar. Eso lo podemos observar cuando invocamos el método compareTo. Esto nos lleva a cambiar la definición de la clase de:

public class MezclaNatural

a:

public class MezclaNaturalGenerico<T extends Comparable<T>>

Noten que exigimos que el tipo de datos genérico implemente la interface Comparable, la cual declara el método compareTo. Ahora podemos reemplazar String con el parámetro genérico. Por ejemplo:

    String actual = null;
    String anterior = null;

con:

    T actual = null;
    T anterior = null;

Lectura de datos.

Para leer datos estábamos usando los métodos available() y readUTF() de un DataInputStream. Además invocamos el método close() de este Stream cuando terminamos de usarlo. Vamos a reemplazar este DataInputStream con una interface que provea una funcionalidad similar. En lugar de crear una interface completamente nueva, decidí crear una interface que extiende dos interfaces de las librerías de Java:

  public interface Lector<T> extends Iterator<T>, Closeable { }

Un usuario tiene que implementar esta interface para que nuestro algoritmo no se preocupe de cómo leer datos. Observen cómo volvemos a usar el parámetro genérico en esta definición. Los métodos hasNext() y next() de la interface Iterator reemplazarán a los métodos available() y readUTF().

Un pequeño detalle de implementación es que nuestro algoritmo tiene que instanciar varios Lector(es) a lo largo de su ejecución. Por lo que un usuario debe proporcionar una fábrica de Lectores. Esto lo podemos ver en el siguiente parámetro de nuestro constructor:

    Function<File, Lector<T>> generaLector

La fábrica (Function) tomará como entrada un archivo abstracto e instanciará un Lector.

Escritura de datos.

Este caso es paralelo a la lectura de datos. Para escribir usamos el método writeUTF() de un DataOutputStream. Esto la abstraemos con la interface:

  public interface Escritor<T> extends Consumer<T>, Closeable { }

El método accept() de Consumer reemplazará al método writeUTF(). Y también necesitamos una fábrica de Escritores.

    Function<File, Escritor<T>> generaEscritor

Esto es todo por este post. Si lo comparan con el código original, verán que la lógica es la misma y la mayoría fueron reemplazos directos (por ejemplo: readUTF con next) En el siguiente post veremos cómo usarlo.

MezclaNaturalGenerico.java

miércoles, 30 de enero de 2019

Condiciones en Makefiles

De vez en cuando me toca lidiar con Makefiles. La mayoría de las veces, tengo que tomar tips de varias fuentes para poder completar un solo comando. En este post, me gustaría compartirles una de esas ocasiones. Mi objetivo era ejecutar condicionalmente comandos basado en la versión instalada de Java. Les muestro primero el resultado final.

prueba_java:
ifneq (,$(findstring build 1.8, $(shell java -version 2>&1)))
 @echo "Java 8"
else
 @echo "No Java 8"
endif

El comando java -version retorna algo similar a esto:

openjdk version "1.8.0_191"
OpenJDK Runtime Environment (build 1.8.0_191-8u191-b12-0ubuntu0.16.04.1-b12)
OpenJDK 64-Bit Server VM (build 25.191-b12, mixed mode)

Noten el uso de “2>&1”. El comando anterior no imprime la versión a stdout sino a stderr. Por lo que tengo que redireccionar stderr a stdout.

El siguiente paso es buscar la cadena build 1.8 en el resultado de java -version usando la función findstring. Noten que el primer argumento no lleva comillas. Aquí aprendí que las comillas son necesarias sólo si el comando que se ejecutará en la terminal las necesita. Como es el caso del comando echo. Pero en esta situación, findstring es un comando que make ejecutar directamente, y los comandos nativos de make no necesitan comillas.

Por último, comparamos el resultado de findstring con la cadena vacía. El comando findstring retorna build 1.8 si encuentra la cadena en el segundo argumento o una cadena vacía en caso contrario. Noten nuevamente la forma en la que representamos la cadena vacía al no dejar espacio entre el primer paréntesis y la coma.

Les dejo un enlace a un sitio con muy buenos ejemplos. Una vez estaba tratando de entender algo similar a las siguientes lineas:

foo := a.o b.o c.o
bar := $(foo:%.o=%)

Y no sabía ni cómo buscar. Navegando por este tutorial encontré este ejemplo que era muy parecido al código que estaba analizando, y eso me dio la pista de que esto es equivalente al comando patsubst

domingo, 27 de enero de 2019

Java classpath y jar

Recientemente me topé con la forma en la cual las opciones classpath y jar interactúan. Usemos un ejemplo muy sencillo para ilustarlo.

Supongamos que queremos usar una clase que viene en un jar. Por ejemplo:

public class MiClase {
  public static String saludo() {
    return "Saludos de MiClase";
  }
}

Podemos construir un jar de la siguiente manera.

javac MiClase.java
jar cf MiClase.jar MiClase.class

Ahora usemos este jar desde otra clase.

public class Inicio {
  public static void main(String[] args) {
    System.out.println(MiClase.saludo());
  }
}

Si sólo queremos ejecutar esta clase.

javac -cp MiClase.jar Inicio.java
java -cp .:MiClase.jar Inicio

Qué pasa si también queremos crear un jar de esta clase.

jar cfe Inicio.jar Inicio Inicio.class

Noten que el parámetro Inicio indica el punto de entrada a nuestra aplicación. Este fue mi primer intento para ejecutar el programa.

java -cp MiClase.jar -jar Inicio.jar

El cual me generó este error:

Exception in thread "main" java.lang.NoClassDefFoundError: MiClase

Resulta que la opción jar funciona con jars que contienen todas sus dependencias, también les llaman jar ejecutables. La solución es poner los jars en el classpath e indicar el punto de entrada.

java -cp MiClase.jar:Inicio.jar Inicio