El Sistema de tipos de Java

Tipos Primitivos y de Referencia

Java distingue entre tipos primitivos int, double, char y byte, y los tipos de referencia (tales como Object y String). Los tipos primitivos son fijos y son los únicos que tienen semantica de valor. No es posible crear tipos-valor en Java. No es posible llamar a un método en un valor de un tipo primitivo. Las variables con tipos referencia contienen una referencia a un objeto.

Tipos Envoltorio

Para facilitar el paso de tipos primitivos a tipos referencia, Java provee un tipo wrapper para cada tipo primitivo. Por ejemplo, el wrapper para el tipo int es java.lang.Integer. Tales tipos de datos se denominan type objects. La wikipedia dice lo siguiente sobre los type objects:

In computer science, an object type (a.k.a. wrapping object) is a datatype which is used in object-oriented programming to wrap a non-object type to make it look like a dynamic object.

Some object-oriented programming languages make a distinction between reference and value types, often referred to as objects and non-objects on platforms where complex value types don't exist, for reasons such as runtime efficiency and syntax or semantic issues. For example, Java has primitive wrapper classes corresponding to each primitive type: Integer and int, Character and char, Float and float, etc. Languages like C++ have little or no notion of reference type; thus, the use of object type is of little interest.

El Problema

Por otro lado, los operadores sobre los tipos primitivos no se soportan sobre los correspondientes tipos referencia. Esto daba lugar en las versiones de Java anteriores a J2SE 5.0 a códigos innecesariamente complejos, como éste en que se suman dos listas en una tercera:

~/Lgroovy/simpledatatypes$ cat -n Sum.java
 1  // Java code
 2  import java.io.*;
 3  import java.util.*; 
 4  public class Sum {
 5      public static void main (String [] args) {
 6        ArrayList listOne = new ArrayList();
 7        ArrayList listTwo = new ArrayList();
 8        ArrayList results = new ArrayList();
 9  
10        int s = (args.length >  0)? Integer.parseInt(args[0]) : 5;
11        for (int i = 1; i < s; i++) {
12            System.out.println("  - Storing (" + i + ")");
13            listOne.add(new Integer(i)); 
14            // listTwo = 2*listOne
15            listTwo.add(new Integer(2 * ((Integer) listOne.get(i-1)).intValue()));
16        }
17  
18        for(int i = 0; i < listOne.size(); i++) {
19          Integer first = (Integer) listOne.get(i);
20          Integer second = (Integer) listTwo.get(i);
21  
22          int sum = first.intValue()+second.intValue();
23          results.add (new Integer(sum));
24        }
25  
26        for (int i = 0; i < results.size(); i++) {
27            System.out.println("  - results(" + i + ") = " + results.get(i));
28        }
29    }
30  }

Autoboxing en Groovy

Autoboxing is the term for treating a value type as a reference type without any extra source code. The compiler automatically supplies the extra code needed to perform the type conversion.

En Groovy, todos los tipos son objetos. Las operaciones de boxing y unboxing son realizadas automáticamente. Este es un programa Groovy equivalente al anterior:

generaciondecodigos@nereida:~/src/groovy/simpledatatypes$ cat Sum.groovy
#!/usr/bin/env groovy
a = (1..10).toList()                 # objeto IntRange -> ArrayList
b = a.collect { 2*it }               # objeto ArrayList
r = (0..9).collect { a[it]+b[it] }   # objeto IntRange -> ArrayList
println r
Damos permisos de ejecución al fichero y ejecutamos:
generaciondecodigos@nereida:~/src/groovy/simpledatatypes$ chmod a+x Sum.groovy
generaciondecodigos@nereida:~/src/groovy/simpledatatypes$ ./Sum.groovy
[3, 6, 9, 12, 15, 18, 21, 24, 27, 30]
O usando groovysh:
groovy:000> (c, d) = [(1..10).toList(), (1..10).collect {2*it }]
===> [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]]
groovy:000> println (0..9).collect { c[it]+d[it] }
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Véanse las entradas Multiple Assignment y Collections en groovy.codeHaus.org asi como la documentación de la clase Object del Groovy JDK.

Groovy siempre usa la clase wrapper en vez de la clase primitiva, pero nos permite utilizar las operaciones sobre los tipos básicos. La conversión de un tipo primitivo en un tipo wrapper se conoce como boxing. La acción inversa se denomina unboxing. Groovy realiza estas operaciones donde quiera que sea necesario.

Autoboxing en Java

En las versiones posteriores de Java a J2SE 5.0 es posible simplificar la suma, ya que se soporta autoboxing:

     for(int i = 0; i < listOne.size(); i++) {
        results.add ((Integer) listOne.get(i) + (Integer) listTwo.get(i));
      }

Tipos Genéricos en Java

La presencia de genericidad, bucles for-each y autoboxing hace que en las últimas versiones de Java se simplifiquen estos problemas. El siguiente programa calcula las frecuencias de las palabras que se pasan en la línea de comandos:

generaciondecodigos@nereida:~/src/groovy/simpledatatypes$ cat -n Frequency.java
   1  import java.util.*;
   2
   3  // Prints a frequency table of the words on the command line
   4  public class Frequency {
   5     public static void main(String[] args) {
   6        Map<String, Integer> m = new TreeMap<String, Integer>();
   7        for (String word : args) {
   8            Integer freq = m.get(word);
   9            m.put(word, (freq == null ? 1 : freq + 1));
  10        }
  11        System.out.println(m);
  12     }
  13  }
sigue un ejemplo de ejecución:
generaciondecodigos@nereida:~/src/groovy/simpledatatypes$ javac Frequency.java
generaciondecodigos@nereida:~/src/groovy/simpledatatypes$ java Frequency en todos los y son todos las operaciones de boxing y boxing son y
{boxing=2, de=1, en=1, las=1, los=1, operaciones=1, son=2, todos=2, y=3}

El programa declara primero un Map de String a Integer, implementado según un TreeMap, el cual asociará el número de veces que una palabra ocurre en la línea de comandos.

La declaración:

  Map<String, Integer> m = new TreeMap<String, Integer>()
es un ejemplo de uso de genericos. Los genéricos nos proveen con un mecanismo para comunicar el tipo de una colección al compilador, de manera que su correcto uso pueda ser comprobado en tiempo de compilación. Como el compilador conoce ahora el tipo de un elemento de una colección puede insertar los ahormados o casts y comprobar que el uso de la colección es consistente con la declaración.

El siguiente fragmento está tomado del libro The Java Language Specification, Third Edition:

A class is generic if it declares one or more type variables. These type variables are known as the type parameters of the class. The type parameter section follows the class name and is delimited by angle brackets. It defines one or more type variables that act as parameters. A generic class declaration defines a set of parameterized types, one for each possible invocation of the type parameter section. All of these parameterized types share the same class at runtime.

For instance, executing the code

    Vector<String> x = new Vector<String>(); 
    Vector<Integer> y = new Vector<Integer>(); 
    boolean b = x.getClass() == y.getClass();

will result in the variable b holding the value true.

Cuando veamos algo como <Type> deberemos leer "of Type”. La declaración anterior se lee un MAP of String to Integer. Esto es mejor que usar un ahormado o cast, porque un cast nos informa de algo que el programador cree que es cierto en cierto punto del código y que la JVM deberá comprobar en tiempo de ejecución.

Bucles for-each en Java

Después se itera sobre cada argumento que aparece en la línea de comandos usando el nuevo bucle for-each.

       for (String word : args) {
           Integer freq = m.get(word);
           m.put(word, (freq == null ? 1 : freq + 1));
       }
En la tabla puede verse las equivalencias del nuevo bucle, dependiendo que lo que se recorra sea un Arrays o un Iterator. En ambos casos se requiere una variable extra, un índice para el array y un iterador para la colección.

Uso Definición
for (type var : arr) {
    body-of-loop
}
for (int i = 0; i < arr.length; i++) { 
    type var = arr[i];
    body-of-loop
}
for (type var : coll) {
    body-of-loop
}
for (Iterator<type> iter = coll.iterator(); 
     iter.hasNext(); ) 
{
    type var = iter.next();
    body-of-loop
}

Autoboxing: valores de un hash

Para cada palabra word, obtenemos la correspondiente entrada en el Map (freq = m.get(word)) y actualizamos la entrada (m.put(word, (freq == null ? 1 : freq + 1))). Esta actualización es un caso de autoboxing. No es posible poner un int en el Map, ya que la signature de put es:

 Object     put(Object key, Object value)
          Associates the specified value with the specified key in this map (optional operation).
y tampoco es posible añadir uno a un Integer.

Contando la frecuencia de aparición de una palabra: Versión Groovy

Aún así el correspondiente programa Groovy es mas pequeño y legible:

generaciondecodigos@nereida:~/src/groovy/simpledatatypes$ cat Frequency.groovy
#!/usr/bin/env groovy
def m = [:]
args.each { v = m[it]; m[it] = (v ?  ++v : 1) }
println(m);
Sigue una ejecución:
generaciondecodigos@nereida:~/src/groovy/simpledatatypes$ ./Frequency.groovy en todos son y son en en
[en:3, todos:1, son:2, y:1]

Véase También



Subsecciones
Casiano Rodríguez León
2010-04-30