JDC Tech Tips (11 de Noviembre del 2001)

Bienvenido a los Consejos Técnicos de la Conexión del Desarrollador Java (JDC), del 23 de Octubre de 2001. Esta edición cubre:

  • Usar Punteros a Métodos.
  • Clases Abstractas vs. Interfaces.

Estos consejos fueron desarrollados usando Java(tm) 2 SDK, Standard Edition v 1.3

Se puede ver esta edición (en su original en inglés) de los Consejos Técnicos en formato html en http://java.sun.com/jdc/JDCTechTips/2001/tt1106.html

Usar Punteros a Métodos

Supongamos que estamos usando el lenguaje Java para implementar algún tipo de algoritmo de búsqueda u ordenación. Sopongamos también que necesitamos pasar al algoritmo un método comparador, es decir, un método usado para comparar y ordenar dos elementos.

Un lenguaje de bajo nivel como C soporta punteros a funciones, que son las direcciones de memoria de las funciones. Podemos pasar esos punteros a funciones de librería como qsort(), y combinar qsort y una función comparadora que especifiquemos para realizar tipos de ordenación arbitrarios.

El lenguaje Java no tiene punteros que sean visibles para el usuario, y no hay funciones globales (métodos). Todo método forma parte de una clase. Por eso, ¿Cómo podemos designar un método comparador particular para usarlo cuando ordenamos, buscamos, o hacemos algún tipo de operación similar?. Aquí tenemos una aproximación:

    class Compare {
        public int compare(Integer a, Integer b) {
            int aval = a.intValue();
            int bval = b.intValue();
            return aval < bval ? -1 : 
                               (aval == bval ? 0 : 1);
        }
    }
    
    public class MethPtr1 {
        static int compare_ab(
                    Integer a, Integer b, Compare c) {
            return c.compare(a, b);
        }
    
        public static void main(String args[]) {
            Integer a = new Integer(47);
            Integer b = new Integer(37);
    
            int cmp = compare_ab(a, b, new Compare());
    
            if (cmp < 0) {
                System.out.println("a < b");
            }
            else if (cmp == 0) {
                System.out.println("a == b");
            }
            else {
                System.out.println("a > b");
            }
        }
    }

En este ejemplo, el método compare_ab es una versión muy simplificada de un método de ordenación. Se le pasan dos objetos Integer, junto con un comparador. El método ordena los objetos Integer, devolviendo -1 si el primer objeto es menor que el segundo, 0 si son iguales, y 1 si el primer objeto es mayor que el segundo.

El comparador es un ejemplar de la clase Compare que tiene un método comparador definido dentro. El ejemplar es llamado un "objeto función," dado que define un sólo método, y que el método realiza operaciones sobre otros objetos que son pasados a ese método.

Se crea un ejemplar de Compare cada vez que se llama a compare_ab. Esto podría optimizarse creando un ejemplar de Compare y usarlo a lo largo del programa, o usando una clase singleton.

La salida del programa es:

    a > b

La aproximación de arriba hace el trabajo, pero tiene algunos problemas. Uno es que hay una estrategia de ordenación fija dentro del método compare. Si queremos invertir el orden de la comparación, o tomar el valor absoluto de los números antes de comparlos, no estamos de suerte. También, un algoritmo de ordenación y búsqueda estándard no sabe nada sobre la clase Compare que hemos definido; el algoritmo tiene que ser implementado en términos de un mecanismo de estanadrización.

Para resolver estos problemas, podemos cambiar el programa de esta forma:

    import java.util.Comparator;
    
    class Compare implements Comparator {
        public int compare(Object a, Object b) {
            int aval = ((Integer)a).intValue();
            int bval = ((Integer)b).intValue();
            return aval < bval ? -1 : 
                                (aval == bval ? 0 : 1);
        }
    }
    
    public class MethPtr2 {
        static int compare_ab(
                  Integer a, Integer b, Comparator c) {
            return c.compare(a, b);
        }
    
        public static void main(String args[]) {
            Integer a = new Integer(47);
            Integer b = new Integer(37);
    
            Comparator c = new Compare();
            int cmp = compare_ab(a, b, c);
    /*
            int cmp = compare_ab(
                               a, b, new Comparator() {
                public int compare(
                                Object aa, Object bb) {
                    int aval = (
                               (Integer)aa).intValue();
                    int bval = (
                               (Integer)bb).intValue();
                    return aval < bval ? -1 :
                        (aval == bval ? 0 : 1);
                }
            });
    */
  
            if (cmp < 0) {
                System.out.println("a < b");
            }
            else if (cmp == 0) {
                System.out.println("a == b");
            }
            else {
                System.out.println("a > b");
            }
        }
    }

java.util.Comparator es un interface estándard que esplicita el método compare. El interface lo usan otras clases y métodos, por ejemplo, Collections.sort. Implementamos este interface, definiendo cualquier método de comparación que deseemos.

Observa que es posible usar una clase interna anonima para implementar el interface Comparator. El ejemplo de arrbia muestra una alternativa que ilustra el uso de una clase interna. Esta aproximación es útil en situaciones donde sólo necesitamos usar la implementación de la clase en un lugar.

El ejemplo es una demostración de programación usando tipos de interfaces. Cuando el programa MethPtr2 llama a compare_ab, le pasa al método un objeto Compare. Pero el correspondiente parámetro en compare_ab está definido como un Comparator. Es como si dijeramos:

Comparator x = new Compare();

Esto es válido porque la clase Compare implementa el interface Comparator. Otro ejemplo común del Collections Framework es:

List x = new ArrayList();

Pasar un método a otro método, significando un objeto función o interface, para el método pasado pueda ser llamado, es algunas veces referido como "callback.". Aquí tenemos un ejemplo más explícito de callback:

    import java.util.*;
    
    interface Visitor {
        void visit(Object o);
    }
    
    class Walker {
        public static void walk(Object o, Visitor v) {
            if (o instanceof Map) {
                o = ((Map)o).entrySet();
            }
            if (o instanceof Collection) {
                Collection c = (Collection)o;
                Iterator iter = c.iterator();
                while (iter.hasNext()) {
                    v.visit(iter.next());
                }
            }
            else {
                throw new IllegalArgumentException();
            }
        }
    }
    
    public class MethPtr3 implements Visitor {
        public void visit(Object o) {
            System.out.println(o);
        }
    
        void doit() {
            List data1 = new ArrayList();
            data1.add("test11");
            data1.add("test12");
            data1.add("test13");
            Walker.walk(data1, this);
    
            Set data2 = new TreeSet();
            data2.add("test21");
            data2.add("test22");
            data2.add("test23");
            Walker.walk(data2, this);
    
            Map data3 = new HashMap();
            data3.put("test31key", "test31value");
            data3.put("test32key", "test32value");
            data3.put("test33key", "test33value");
            Walker.walk(data3, this);
        }
 
        public static void main(String args[]) {
            new MethPtr3().doit();
        }
    }

Supongamos que tenemos una estructura de datos, es decir, una List, Set, o Map, y queremos escribir un metodo de utilidad que atraviese la estructura. Como todos los elementos son visitados, también podríamos llamar a un método que especifiquemos. El programa de arriba lo hace.

Walker.walk es un método estático que acepta una referencia a una estructura de datos, junto con un objeto de una clase que implementa el interface Visitor. El método usa iterators para atravesar la estructura, y llama al método definido en la visita de la clase MethPtr3. Cuando ejecutamos este programa, el resultado es:

    test11
    test12
    test13
    test21
    test22
    test23
    test32key=test32value
    test31key=test31value
    test33key=test33value

La mayoría de las veces, usar objetos función e interfaces es la aproximación correcta para implementar punteros a métodos. Pero hay otro mecanismo que es importante conocer. Supongamos que hemos escrito un depurador, un intérprete, o algún tipo de programa similar, y que queremos buscar y llamar a métodos por su nombre. En otras palabras, el usario especifica un nombre de método, y nuestro programa llama a ese método. ¿Cómo haríamos esto?

Esta tarea podría ser imposible en muchos otros lenguajes de programación, pero la característica de reflexión de Java lo hace sencillo. Aquí tenemos un ejemplo:

    import java.lang.reflect.*;
    
    class A {
        public void f1() {
            System.out.println("A.f1 called");
        }
        public void f2() {
            System.out.println("A.f2 called");
        }
    }
    
    class B {
        public void f1() {
            System.out.println("B.f1 called");
        }
        public void f2() {
            System.out.println("B.f2 called");
        }
    }
    
    public class MethPtr4 {
        static void callMethod(Object obj, Method meth)
        throws Exception {
            meth.invoke(obj, null);
        }
    
        static void findMethod(String cname, String mname)
        throws Exception {
            Class cls = Class.forName(cname);
            Method meth = cls.getMethod(mname,
              new Class[]{});
            callMethod(cls.newInstance(), meth);
        }
    
        public static void main(String args[])
          throws Exception {
            if (args.length != 2) {
                System.err.println("missing class/method
                  names");
                System.exit(1);
            }
    
            findMethod(args[0], args[1]);
        }
    }

Después de compilar este programa, lo ejecutamos de esta forma:

java MethPtr4 A f2

Aquí hemos especificado una clase (A) y un método en la clase (f2) para que sean llamados. El método findMethod carga una clase (Class.forName), y luego encuentra el método dentro de la clase. Tanto el nombre de la clase como el del método son especificados como strings. Después de encontrar el método, está representado por un objeto Method. El objeto es pasado al método callMethod, junto con un objeto de la clase apropiada.

Esta aproximación es poderosa, pero es mejor no utilizarla a menos que realmente la necesitemos. Por ejemplo, si decimos:

java MethPtr4 A f3

Obtenemos una excepción. Por el contrario, si no estamos usando reflexión, y llamamos a un método que no existe (f3) en nuestro prorama, obtenemos un error de compilación. En otras palabras, cuando llamamos a un método usando reflexión, es necesario diferir algún chequeo que hace el compilador.

Para más información sobre el uso de punteros a método, puedes ver la sección 11.2.6, "La clase Method" , en "The Java Programming Language Third Edition." de Arnold, Gosling, y Holmes.
También puedes ver el ítem 22, "Replace function pointers with classes and interfaces", en "Effective Java Programming Language Guide" de Joshua Bloch.

Clases Abstractas vs. Interfaces

En el JDC Tech Tips del 9 de Obtubre de 2001, había un ítem sobre el uso de un árbol de clases abstractas para implementar el equivalente Java a la unión C. El código se parecía algo a este:

    abstract class Time {
        public abstract int getMinutes();
    }
    
    class Days extends Time {
        private int days;
        public int getMinutes() {
            return days * 24 * 60;
        }
    }
    
    class HoursMinutes extends Time {
        private int hours;
        private int minutes;
        public int getMinutes() {
            return hours * 60 + minutes;
        }
    }

Algún lector se preguntó por qué no se puede usar un interface en lugar de una clase abstracta, con el código escrito así:

    interface Time {
        int getMinutes();
    }
    
    class Days implements Time {
        private final int days;
        public Days(int days) {
            this.days = days;
        }
        public int getMinutes() {
            return days * 24 * 60;
        }
    }
    
    class HoursMinutes implements Time {
        private final int hours;
        private final int minutes;
        public HoursMinutes(int hours, int minutes) {
            this.hours = hours;
            this.minutes = minutes;
        }
        public int getMinutes() {
            return hours * 60 + minutes;
        }
    }
    
    public class AIDemo1 {
        public static void main(String args[]) {
            Time t1 = new Days(10);
            Time t2 = new HoursMinutes(15, 59);
            System.out.println(t1.getMinutes());
            System.out.println(t2.getMinutes());
        }
    }

De hecho, la aproximación del interface funciona. Sin embargo, hay una serie de compensaciones (tradeoffs) entre el uso de clases abstractas e interfaces. Este truco examina alguna de ellas.

Los dos mecanismos definen un contrato, es decir, el comportamiento requerido que otra clase debe implementar. Si tenemos las siguientes definiciones:

    abstract class A {
        abstract void f();
    }

    interface B {
        void f();
    }

entonces una clase concreta que extienda A debe definir f. Una clase que implemente B debe definir f.

Detrás de esta característica común, los dos mecanismos son bastante diferentes. Los interfaces proporcionan una forma de herencia múltiple ("interface inheritance"), porque podemos implementar varios interfaces. Una clase, por el contrario, sólo puede extender ("implementation inheritance") a otra clase. Una clase abstracta tiene métodos estáticos, partes protegidas y una implementación parcial. Los interfaces están limitados a métodos públicos y constantes y no se les permite ninguna implementación.

Pero ¿cuál es la diferencia entre usar clases abstractas o interfaces en el ejemplo de arriba? Una difererencia es que una clase abstracta es más fácil de evolucionar. Supongamos que queremos añadir un método:

public int getSeconds();

al contrato Time. Si usamos una clase abstracta, podemos decir:

    public int getSeconds() {
        return getMinutes() * 60;
    }

En otras palabras, proporcionamos una implementación parcial de la clase abstracta. Hacer esto significa que las subclases de la clase abstracta no necesitan proporcionar su propia implementación de getSeconds a menos que quieran sobreescribir la versión por defecto.

Si Time es un interface, podemos decir:

    interface Time {
        int getMinutes();
        int getSeconds();
    }

Pero no está permitido implementar getSeconds dentro del interface. Esto significa que todas las clases que implementen el interface Time ahora están rotas, a menos que las corrijamos para definir el método getSeconds. Por eso, si queremos usar un interface en esta situación, necesitamos estar absolutamente seguros de que obtenemos el interface correcto a la primera. De esta forma, no tendremos que añadirle nada al interface posteriormente, y por lo tanto no invalidaremos las clases que usan el interface.

Otro problema con este ejemplo es que podríamos querer dividir los datos comunes en la clase abstracta. No hay una funcionalidad equivalente para los interfaces. Por ejemplo, si decimos:

    interface A {
        int x = 7;
    }
    
    class B implements A {
        void f() {
            int i = x; // OK
            x = 37; // error
        }
    }

todo está bien, si queremos declarar una constante en el interface, pero no es posile declarar un campo de datos variable de esta forma..

Veamos otro ejemplo:

    import java.io.*;
    
    interface Distance {
        double getDistance(Object o);
    }
    
    interface Composite extends Comparable,
        Distance, Serializable {}
    
    class MyPoint implements Comparable, Distance,
                                        Serializable {
    //class MyPoint implements Composite {
    
        private final int x;
        private final int y;
    
        public MyPoint(int x, int y) {
            this.x = x;
            this.y = y;
        }
    
        public int getX() {
            return x;
        }
        public int getY() {
            return y;
        }
    
        public int compareTo(Object o) {
            MyPoint obj = (MyPoint)o;
            if (x != obj.x) {
                return x < obj.x ? -1 : 1;
            }
            return y < obj.y ? -1 : (y == obj.y ? 0 : 1);
        }
    
        public double getDistance(Object o) {
            MyPoint obj = (MyPoint)o;
            int sum = (x - obj.x) * (x - obj.x) +
                (y - obj.y) * (y - obj.y);
            return Math.sqrt(sum);
        }
    }
    
    public class AIDemo2 {
        public static void main(String args[]) {
            MyPoint mp1 = new MyPoint(1, 1);
            MyPoint mp2 = new MyPoint(2, 2);
    
            double d = mp1.getDistance(mp2);
            System.out.println(d);
    
            int cmp = mp1.compareTo(mp2);
            if (cmp < 0) {
                System.out.println("mp1 < mp2");
            }
            else if (cmp == 0) {
                System.out.println("mp1 == mp2");
            }
            else {
                System.out.println("mp1 > mp2");
            }
        }
    }

MyPoint es una clase que representa puntos X,Y geométricos, con el constructor usual y sus métodos accesores. La clase implementa tres interfaces. Un interface se usa para comparar un punto con otro, otro se usa para calcular la distancia Euclidiana entre dos puntos, y el último declara que los objetos MyPoint son serializables.

Una aproximación alternativa sería definir un nuevo interface Composite (llamado un "subinterface") que extienda los tres interfaces, y luego implementar Composite en MyPoint.

La salida del programa es:

    1.4142135623730951
    mp1 < mp2

Es fácil corregir una clase existente para que implemente un nuevo interface. Hacer esto algunas veces se llama "mixin.". En un mixin, una clase declara que proprociona algun comportamiento lateral opcional además de su función primaria. Comparable es un ejemplo de mixin.

Observa que se puede implementar el ejemplo AIDemo2 usando clases abstractas. Implementar varios interfaces no relacionados en una clase es duro de duplicar usando clases abstractas.

Frecuentemente es deseable combinar interfaces y clases abstractas. Por ejemplo, parte del diseño del Collections Framework se parece a esto:

    interface List {
        int size();
        boolean isEmpty();
    }
    
    abstract class AbstractList implements List {
        public abstract int size();
        public boolean isEmpty() {
            return size() == 0;
        }
    }
    
    class ArrayList extends AbstractList {
        public int size() {
            return 0; // placeholder
        }
    }

La parte superior del árbol son interfaces, como Collection y List, que describen un contrato, es decir, una especificación del comportamiento requerido. En el siguiente nivel están las clases abstractas, como AbstractList, que proporcionan una implementación parcial. Observa que el tamaño no está definido en AbstractList, pero que isEmpty si está definido en términos de tamaño. Si una lista tiene tamaño cero, está vacía por definición. Una clase concreta, como ArrayList, define métodos abstractos no definidos todavía.

Si usamos este esquema, y programamos en términos de tipos interface (List en lugar de ArrayList), hay varios beneficios:

  • Mucho del trabajo de implementación ya está hecho en las clases abstractas.
  • Podemos fácilmente cambiar de una implementación a otra (LinkedList en lugar de ArrayList).
  • Si ArrayList o LinkedList no son satistactorios, podemos desarrollar nuestra propia clase que implemente List.
  • Si no podemos extender una clase dada, porque ya hemos extendido otra clase, en su lugar podemos implementar el interface para la clase deseada y luego reenviar llamadas a métodos a un ejemplar privado de la clase deseada.

Los interfaces tienden a ser una mejor elección que las clases abstractas en muchos casos, aunque necesitamos obtener el interface correcto a la primera. Cambiar el interface después hará que rompamos mucho código. Las clases abstractas son útiles cuando estámos proporcionando una implementación parcial. En este caso, también deberíamos definir un interface como se ilustra arriba, e implementar el interface en la clase abstracta.

Para más información sore clases abstractas contra interfaces puedes ver la sección 4.4 "Working with Interfaces", y al sección 4.6 "When to Use Interfaces", de "The Java Programming Language Third Edition" de Arnold, Gosling, y Holmes.
También puedes ver el ítem 14, "Favor composition over inheritance", y el ítem 16, "Prefer interfaces to abstract classes", de "Effective Java Programming Language Guide" de Joshua Bloch.

Copyright y notas de la traducción

Nota respecto a la traducción

El original en inglés de la presente edición de los JDC Tech Tips fue escrita por Glen McCluskey, la traducción no oficial fue hecha por Juan A. Palos (Ozito), cualquier sugerencia o corrección hágala al correo [email protected] , sugerencia respecto a la edición original a mailto:[email protected]

Nota (Respecto a la edición via email)

Sun respeta su tiempo y su privacidad. La lista de correo de la Conexión del desarrollador Java se usa sólo para propósitos internos de Sun Microsystems(tm). Usted ha recibido este email porque se ha suscrito a la lista. Para desuscribirse vaya a la página de suscripciones, desmarque casilla apropiada y haga clic en el botón Update.

Suscripciones

Para suscribirse a la lista de correo de noticias de la JDC vaya a la página de suscripciones, elija los boletines a los que quiera suscribirse, y haga clic en Update.

Realimentación

¿Comentarios?, envie su sugerencias a los Consejos Técnicos de la JDC a mailto:[email protected]

Archivos

Usted encontrará las ediciones de los Consejos Técnicos de la JDC (en su original en inglés) en http://java.sun.com/jdc/TechTips/index.html

Copyright

Copyright 2001 Sun Microsystems, Inc. All rights reserved. 901 San Antonio Road, Palo Alto, California 94303 USA.

Este documento esta protegido por las leyes de autor. Para mayor información vea http://java.sun.com/jdc/copyright.html

Enlaces a sitios fuera de Sun

Los Consejos Técnicos de la JDC pueden dar enlaces a otros sitios y recursos. Ya que Sun no tiene control sobre esos sitios o recursos usted reconoce y acepta que Sun no es responsable por la disponibilidad de tales sitios o recursos, y no se responsabiliza por cualquier contenido, anuncios , productos u otros materiales disponibles en tales sitios o recursos. Sun no será responsable, directa o indirectamente, por cualquier daño o pérdida causada o supuestamente causada por o en relación con el uso de o seguridad sobre cualquier tal contenido, bienes o servicios disponibles en o através de cualquier sitio o recurso.

El original en Ingles de esta edición de los Consejos técnicos fue escrita por Glen McCluskey.

JDC Tech Tips November 06, 2001

Sun, Sun Microsystems, Java y Java Developer Connection (JDC) son marcas registradas de Sun Microsystems Incs. en los Estados Unidos y cualquier otro país.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP
SIGUIENTE ARTÍCULO