JDC Tech Tips (9 de Octubre del 2001)

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

  • Cómo se pasan los Argumentos a un Método Java.
  • Convertir Programas escritos en C a Lenguaje Java.

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/tt1009.html

Cómo se pasan los Argumentos a los Métodos Java

Supongamos que estamos haciendo alguna programación en Java, y tenemos un sencillo programa como este:

    public class CallDemo1 {
        static void f(int arg1) {
            arg1 = 10;
        }
    
        public static void main(String args[]) {
            int arg1;
    
            arg1 = 5;
    
            f(arg1);
    
            System.out.println("arg1 = " + arg1);
        }
    }

En el método main, a la variable arg1 se le ha dado el valor 5, y luego se ha pasado como argumento al método f. Este método declara un parámetro con el mismo nombre, arg1, usado para acceder al argumento.

¿Qué sucede cuando ejecutamos este sencillo programa? ¿El método f modifica el calor de arg1, qué valor se imprime, 5 ó 10? 5 es la respuesta correcta. Esto implica que seleccionar arg1 a 10 en el método f no tiene efecto fuera del método.

¿Por qué funciona de esta forma? La respuesta tiene que ver con las aproximaciones de paso por valor y paso por referencia para pasar argumentos a un método. El lenguaje Java usa exclusivamente paso por valor. Antes de explicar lo que esto significa, veamos un ejemplo de paso por referencia, usando código C++:


    // CallDemo1.cpp
    
    #include <iostream>
    
    using namespace std;
    
    void f(int arg1, int& arg2)
    {
        arg1 = 10;
        arg2 = 10;
    }
    
    int main()
    {
        int arg1, arg2;
    
        arg1 = 5;
        arg2 = 5;
    
        f(arg1, arg2);
    
        cout << "arg1 = " << arg1 << " arg2 = " 
                                       << arg2 << endl;
    }

La función f tiene dos parámetros. El primero es un parámetro pasado por valor y el segundo por referencia. Cuando este programa se ejecute, la primera asignación en la función f no tiene efecto en el llamador (la función main). Pero la segunda asignación si, de hecho, cambia el valor de arg2 en main. El & de int& dice que arg2 es un parámetro pasado por referencia. Este ejemplo particular no tiene equivalencia en el lenguaje Java.

¿Entonces qué significa realmente el paso por valor? Para responder a esto, es instructivo ver algunos de los bytecodes de la JVM1 que resultan de los dos siquientes comandos:

    javac CallDemo1.java

    javap -c -classpath . CallDemo1

Aquí tenemos un extracto de la salida:


    Method void f(int)
        0 bipush 10
        2 istore_0
        3 return

    Method void main(java.lang.String[])
        0 iconst_5
        1 istore_1
        2 iload_1
        3 invokestatic #2 <Method void f(int)>

En el método main, la instrucción iconst_5 empuja el valor 5 sobre la pila de operandos de la máquina virtual Java. Este valor es almacenado en la segunda variable local (arg1, con args como la primera variable local). iload_1 empuja el valor de la segunda variable local sobre la pila de operandos, donde será servida como argumento al método llamado.

Luego se llama al método f usando la instrucción invokestatic. El valor del argumento es extraído de la pila, y se usa para crear un marco de pila para el método llamado. Este marco de pila representa las variables locales en f, con el parámetro del método (arg1) siendo la primera de las variables locales.

Lo que esto significa es que los parámetros de un método son copias de los valores de los argumentos pasados al método. Si modificamos un parámetro, no tiene efecto en el llamador. Simplemente cambiamos el valor de la copia en el marco de pila que se está usando para contener las variables locales. No hay forma de "obtener de vuelta" los argumentos en el método llamante. Por eso asignar arg1 en f no cambia el valor de arg1 en el método main. Las variables arg1 en f y en main no están relacionadas, excepto en que el arg1 de f empieza con un copia del valor del arg1 de main. La variables ocupan diferentes posiciones de memoria, y el hecho de que tengan el mismo nombre es irrelavante.

Por contraste, un parámetro pasado por referencia se implementa pasando la dirección de memoria del argumento del llamador a la función llamada. La dirección del argumento es copiada dentro del parámetro. El parámetro contiene una dirección que referencia la posición de memoria del argumento para que los cambios en el parámetro realmente cambie en el valor del argumento en el llamador. En términos de bajo nivel, si tenemos la dirección de memoria de una variable, podemos cambiar el valor de la variable.

El discusión del paso de argumentos es complicada por el hecho de que el término "referencia" en paso por referencia significa algo ligeramente diferente al uso típico en términos de programación Java. En Java, el término referencia se usa en el contexto de objetos referencia. Cuando pasamos un objeto referencia a un método, no estámos usando el paso por referencia sino el paso por valor. En particular, se hace una copia del valor del objeto referencia, y cambiar la copia (a través del parámetro) no tiene efecto en el llamador. Echemos un vistazo a un par de ejemplos para clarificar esta idea:

    class A {
        public int x;
    
        A(int x) {
            this.x = x;
        }
    
        public String toString() {
            return Integer.toString(x);
        }
    }
    
    public class CallDemo2 {
        static void f(A arg1) {
            arg1 = null;
        }
    
        public static void main(String args[]) {
            A arg1 = new A(5);
    
            f(arg1);
    
            System.out.println("arg1 = " + arg1);
        }
    }

En este ejemplo, una referencia a un objeto A es pasada a f. Seleccionar arg1 a null no tiene efecto en el llamador, igual que en el ejemplo anterior. Se imprime el valor 5. El llamador pasa una copia del valor del objeto referencia (arg1), no la dirección de memoria de arg1. Por eso el método llamado no puede devolver arg1 y modificarlo.

Aquí hay otro ejemplo:

    class A {
        public int x;
    
        public A(int x) {
            this.x = x;
        }
    
        public String toString() {
            return Integer.toString(x);
        }
    }
    
    public class CallDemo3 {
        static void f(A arg1) {
            arg1.x = 10;
        }
    
        public static void main(String args[]) {
            A arg1 = new A(5);
    
            f(arg1);
    
            System.out.println("arg1 = " + arg1);
        }
    }

Aquí obtenemos el valor 10. ¿Cómo puede ser? Ya hemos visto que no hay forma de cambiar la versión del llamador de arg1 en el método llamado. Pero este código muestra que el objeto referenciado por arg1 se ha modificado. Aquí, el método llamante y el método llamado tienen un objeto en común, y ámbos métodos pueden modificar el objeto. En este ejemplo, el objeto referencia (arg1) es pasado por valor. Luego se hace una copia en el marco de la pila de f. Pero tanto el original como la copia son objetos referencia, y apuntan a un objeto común en memoria que puede ser modificado.

En programación Java, es común decir algo como "un objeto String se pasa al metodo f" o "un array se pasa al método g". Técnicamente hablando, los objetos y arrays no son pasados. En su lugar, se pasan sus referencias o sus direcciones. Por ejemplo, si tenemos un objeto Java que contiene 25 campos enteros, y cada campo tiene 4 bytes, el objeto será de aproximadamente 100 bytes de longitud. Pero cuando pasamos este objeto como un argumento a un métdo, no hay un copia real de los 100 bytes. En su lugar se pasa un puntero, referencia o dirección del objeto. El mismo objeto es referenciado en el llamante y en el método llamador. Por el contrario, en un lenguaje como C++, es posible pasar un objeto real o un puntero al objeto.

¿Cuáles son las implicaciones del paso por valor? Una es que cuando pasamos objetos o arrays, el método llamante y el método llamdo comparten los objetos, y ambos pueden cambiar el objeto. Por eso podríamos querer emplear técnicas de copia defensiva de objetos como se explica en Hacer copias Defensivas de Objetos.

Podemos evitar el caso de arriba, cuando el método llamado modifica un objeto, haciendo la clase inmutable. Una clase inmutable es una cuyos ejemplares no pueden ser modificados. Aquí está cómo hacer esto:

    final class A {
        private final int x;
    
        public A(int x) {
            this.x = x;
        }
    
        public String toString() {
            return Integer.toString(x);
        }
    }
    
    public class CallDemo4 {
        static void f(A arg1) {
            //arg1.x = 10;
        }
    
        public static void main(String args[]) {
            A arg1 = new A(5);
    
            f(arg1);
    
            System.out.println("arg1 = " + arg1);
        }
    }

El restulado es 5. Ahora quita el comentario de la modificación de A en f y recompila el programa. Observa que resulta un error de compilación. Hemos hecho A inmutable, por eso no puede ser legal modificarlo en f.

Otra implicación del paso por valor es que no podemos usar los parámetros de método para devolver varios valores desde un método, a menos que pasemos una referencia a un objeto mutable o a un array, y permitamos que el método modifique el objeto. Hay otra forma de devolver múltiples valores, como devolver un array desde un método, o crear una clase especializada y devolver un ejemplar de ella.

Para más información sobre cómo se pasan los argumentos a los métodos Java, puedes ver la sección 1.8.1 de The JavaTM Programming Language Third Edition de Arnold, Gosling, y Holmes. También puedes ver los ítems 13 y 24 (inmutabilidad y hacer copias defensivas cuando sea necesario) en Effective Java Programming Language Guide de Joshua Bloch

Como Convertir Programas Escritos en C a Lenguaje Java

Imagina que tenemos un gran programa C, y que estámos pensando en recodificarlo usando las características Java. ¿Cuáles son algunos de los problemas que necesitamos tener en cuenta para hacer esto? En este truco presentamos una serie de ejemplos, y cubre algunos de los problemas de convertir código C a lenguaje Java. En partícular, está enfocado, en la estructura del programa y de los datos.

Una nota preliminar sobre esta clase de conversiones. Los lenguajes Java y C son muy diferentes. C está enfocado hacia la programación de sistemas y la habilidad de realizar manipulaciones de bajo nivel. En comparación, el lenguaje Java toma una vista de alto nivel y tiene mejores facilidades para escribir grandes aplicaciones, bien estructuradas e independientes del hardware. Por eso hay que entender que algunas de las características de cada lenguaje no existen o sean muy diferentes en el otro.

Empecemos viendo un sencillo programa C, uno que contiene una variable global cuyo valor es seleccionado, recuperado e imprimido:


    /* ConvDemo1.c */
    
    #include <stdio.h>
    
    int current_month;  
    /* current month as a value 1-12 */
    
    int main()
    {
        current_month = 9;
    
        printf("current month = %d
", current_month);
    
        return 0;
    }

Cuando ejecutemos el programa, el resultado será:

 
current month = 9

No hay un equivalente Java para una variable Global, ¿Por eso cómo podríamos escribir un programa equivalente? Hay una forma de hacerlo:

    class Globals {
        private Globals() {}
    
        public static int current_month;
    }
    
    public class ConvDemo1 {
        public static void main(String args[]) {
            Globals.current_month = 9;
    
            System.out.println("current month = " +
                Globals.current_month);
        }
    }

La clase Globals tiene un constructor privado (por eso no se pueden crear ejemplares de la clase), y define un campo public static. Una vez hecho esto, se puede acceder a Globals.current_month a través de la aplicación, como si fuera una variable global C. Como en el caso de otros nombres, podríamos necesitar cualificar Globals con un nombre de paquete, dependiendo de cómo estructuremos nuestro programa. Y podríamos reemplazar el 9 con un nombre de constante como SEPTEMBER, o usar un tipo enumerado para month.

ConvDemo1 es un programa muy sencillo, que funciona, pero tiene un problema inmediato. El campo current_month es público, y es posible decir algo como:

Globals.current_month = -37;

y causar un bloqueo por eso. Normalmente, cuando programamos en Java, hacemos los campos como current_month privados. Luego controlamos el acceso a estos campos privados mediante los constructores y los métodos accesores o mutadores. Aquí hay un programa que toma esta aproximación -- hace el campo current_month privado, y define dos método para seleccionar y obtener el valor del campo:


    class Globals {
        private Globals() {}
    
        private static int current_month;
    
        public static void setCurrentMonth(int m) {
            if (m < 1 || m > 12) {
                throw new IllegalArgumentException();
            }
            current_month = m;
        }
    
        public static int getCurrentMonth() {
            return current_month;
        }
    }
    
    public class ConvDemo2 {
        public static void main(String args[]) {
            Globals.setCurrentMonth(9);
    
            System.out.println("current month = " +
                Globals.getCurrentMonth());
        }
    }

La aproximación ilustrada en ConvDemo2 lanza una cuestión: ¿Cuánto queremos que difieran la estructura original de nuestro programa C cuando lo convertamos a Java? Hay más de una respuesta correcta.

Todavía hay otra forma de escribir este programa. El programa ConvDemo2 realmente no usa objetos Java. No se crean ejemplares de la clase Globals. Aquí hay una alternativa que define una clase Globals y crea un objeto Globals:


    class Globals {
        private int current_month;
    
        private static final Globals globlist = 
                                         new Globals();
    
        private Globals() {
            current_month = 1;
        }
    
        public static Globals getInstance() {
            return globlist;
        }
    
        public void setCurrentMonth(int m) {
            if (m < 1 || m > 12) {
                throw new IllegalArgumentException();
            }
            current_month = m;
        }
  
        public int getCurrentMonth() {
            return current_month;
        }
    }
    
    public class ConvDemo3 {
        public static void main(String args[]) {
            Globals.getInstance().setCurrentMonth(9);
    
            System.out.println("current month = " +
              Globals.getInstance().getCurrentMonth());
        }
    }

En el ejemplo ConvDemo3, Globals es una clase singleton, es decir, es ejemplarizada sólo una vez. El método getInstance es llamado un "static factory method." El programa podría tener en lugar de hacer globlist un campo public static final, pero haciendo esto permitiría que la propiedad singleton se rompiera si fuera necesario más adelante.

Esta aproximación es quizás la más natural en términos Java. Aquí un conjunto de globlas es representado como un objeto que podemos manipular en la forma normal orientada a objetos, pero está más alejada del código C original.

Podemos usar una clase Java para estructurar funciones globales de una forma similar. La clases se pueden usar simplemente como vehículos empaquetadores. Observa que cuando usamos campos singletons y static, existen algunos problemas con la serizalización que podríamos tener que considerar.

Veamos un área totalmente diferente: el equivalente Java de las uniones C. Una unión es algo como un estructura de clase, pero los campos se solapan en memoria, y sólo uno de un conjunto de campos está activo en un momento dado. Usualmente hay una etiqueta de campo que dice qué campo está activo. Aquí hay un programa C que usa una unión:


    /* ConvDemo4.c */

    #include <stdio.h>
    
    enum Type {DAYS, HOURSMINUTES};
    
    struct Days {
        int num_days;
    };
    
    struct HoursMinutes {
        int num_hours;
        int num_minutes;
    };
    
    struct Time {
        enum Type tag;
        union {
            struct Days days;
            struct HoursMinutes hm;
        } u;
    };
    
    void print(struct Time ti)
    {
        int minutes;
    
        if (ti.tag == DAYS) {
            minutes = ti.u.days.num_days * 24 * 60;
        }
        else {
            minutes = ti.u.hm.num_hours * 
                              60 + ti.u.hm.num_minutes;
        }
    
        printf("number of minutes = %d
", minutes);
    }
     
    
    int main()
    {
        struct Time ti;
    
        ti.tag = DAYS;
        ti.u.days.num_days = 10;
        print(ti);
    
        ti.tag = HOURSMINUTES;
        ti.u.hm.num_hours = 15;
        ti.u.hm.num_minutes = 59;
        print(ti);
    }

En este programa, un intervalo de tiempo es representado por un número de días o por un objeto hours/minutes. La unión dentro de la estructura Time contiene un ejemplar de cada uno de estos objetos. Sin embargo, los objetos se solapan, y uno de ello es válido en cualquier momento.

Cando ejecutemos el programa, la salida será:

    number of minutes = 14400

    number of minutes = 959

¿Cómo crearíamos un programa equivalente en Java? Desde el estudio del programa C, está claro que hay múltiples representaciones de datos, y que por cada representación, queremos obtener el número de minutos pasados desde el objeto correspondiente.

Una aproximación Java es definir una clase abstracta llamada Time que especifique un método getMinutes. Luego podemos definir subclases concretas de Time llamadas Days y HoursMinutes. Cada subclase define una representación de datos específica, e implementa getMinutes de la forma apropiada:

Aquí podemos ver el código:

 
    abstract class Time {
        abstract int getMinutes();
    }
    
    class Days extends Time {
        private final int num_days;
    
        public Days(int num_days) {
            this.num_days = num_days;
        }
    
        public int getMinutes() {
            return num_days * 24 * 60;
        }
    }
    
    class HoursMinutes extends Time {
        private final int num_hours;
        private final int num_minutes;
    
        public HoursMinutes(int num_hours, int num_minutes) {
            this.num_hours = num_hours;
            this.num_minutes = num_minutes;
        }
    
        public int getMinutes() {
            return num_hours * 60 + num_minutes;
        }
    }
    
    public class ConvDemo4 {
        public static void main(String args[]) {
            Time t = new Days(10);
            System.out.println("minutes = " + t.getMinutes());
    
            t = new HoursMinutes(15, 59);
            System.out.println("minutes = " + t.getMinutes());
        }
    }

El programa ConvDemo4 usa una variable de la clase abstracta del tipo (Time) para contener una referencia a un objeto Days o a un HoursMinutes. No hay un campo etiqueta usado con el código Java. El tipo de información está disponible en el tiempo de ejecución, por es eso si tenemos una variable del tipo Time, podemos encontrar cual es el tipo real, por ejemplo, usando el operador instanceof.

Hasta ahora los ejemplos han usado variables y funciones globales, y los equivalentes Java de las uniones. Consideremos un ejemplo final, donde tenemos algún tipo de objeto de datos global, usando estructuras y arrays. Aquí está el código C:


    /* ConvDemo5.c */

    #include <stdio.h>
    
    struct A {
        int x;
        int y;
    };
    
    struct B {
        struct A a1;
        int b[5];
        struct A a2;
    };
    
    struct B b = {
        {37, 47},
        {1, 2, 3, 4, 5},
        {57, 67}
    };
    
    int main()
    {
        printf("%d %d
", b.a2.y, b.b[2]);
    }

El programa ConvDemo5.c define una estructura A, con dos campos enteros. Luego define una estructura B que contiene un objeto A, un array y otro objeto A. Un ejemplar de la estructura B es inicializado como dato glogal. Cuando ejecutemos este programa, la salida será:

 
    67 3

¿Cuál es el equivalente Java del programa ConvDemo5.c? El lenguaje Java no tiene variables globales, como se mencionó anteriormente. Además los objetos y los arrays siempre son asignados dinámicamente en Java. Por eso el programa correspondiente se parecería a esto:

 
    class A {
        public int x;
        public int y;
        A(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
    
    class B {
        public A a1;
        public int b[];
        public A a2;
        B(A a1, int b[], A a2) {
            this.a1 = a1;
            this.b = b;
            this.a2 = a2;
        }
    }
    
    class Globals {
        private Globals() {}
        public static B b = new B(
            new A(37, 47),
            new int[]{1, 2, 3, 4, 5},
            new A(57, 67)
        );
    }
    
    public class ConvDemo5 {
        public static void main(String args[]) {
            System.out.println(Globals.b.a2.y + " " +
                Globals.b.b[2]);
        }
    }

Este código Java es una simple tradución del código C. Podríamos querer modificarlo como explicamos antes, por ejemplo, haciendo algunos de los campos privados en vez de públicos:

Con el programa C, deciamos:

    struct B {
        struct A a1;
        int b[5];
        struct A a2;
    };

    struct B b;

luego el objeto b realmente contiene dos objetos A y un array distribuidos juntos en memoria. Pero el objeto equivalente Java contiene tres objetos, donde cada campo es una referencia; los objetos A reales están almacenados en algún lugar de la memoria. Un corolario a este punto es que no hay equivalente Java para algunas manipulaciones de bajo nivel que podemos hacer con objetos C. Por ejemplo, no hay equivalentes Java para obtener el tamaño de un objeto, para descubrir la posición en memoria de un campo, ni para hacer asumpciones sobre los campos que están juntos en meoria, etc.

Los ejemplos mostrados arriba ilustran varios problemas que necesitamos considerar cuando convirtamos código C a lenguaje Java. Hay algunos más. Realmente necesitamos decidir cuánto queremos que se parezca el código Java al C correspondiente, y cómo queremos emplear las características Java para mejorar la robustez de la aplicación.

Para más información sobre cómo convertir programas C al Lenguaje Java, puedes ver la sección 2.2 en The JavaTM Programming Language Third Edition de Arnold, Gosling, y Holmes. También puedes ver el ítem 20, "Replace unions with class hierarchies",en 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 puede dar, p terceras 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 October 09, 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