style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">

Diablos! ¿que podemos hacer con los generics?!

Este más que una entrada para compartir algo es un post para quejarme.

Me gusta muchísimo la forma en la que usar generics me evita tener que usar el cast al sacar datos de las colecciones y con el autoboxing a veces creo que es demasiado bueno.

Pero hay escenarios en los que es demasiado malo, especialmente cuando el tipo de dato generico es uno que a su vez es generico.

Por ejemplo el siguiente código:

    /**
     * Transforma la lista de objetos de tipo Disgnostic en una mapa listas de diagnostic
     * que tiene como llave el código del diagnostic.
     */
     
    private Map<String, List<Diagnostic<? extends JavaFileObject>>> toMap( List<Diagnostic<? extends JavaFileObject>> diagnostics ) {
        Map<String, List<Diagnostic<? extends JavaFileObject>>> result = new HashMap<String, List<Diagnostic<? extends JavaFileObject>>>();
        for ( Diagnostic<? extends JavaFileObject> d : diagnostics ) {
            List<Diagnostic<? extends JavaFileObject>> list = null;
            if ( !result.containsKey( d.getCode() ) ) {
                list = new ArrayList<Diagnostic<? extends JavaFileObject>>();
                result.put( d.getCode(), list );
            } else {
                list = result.get( d.getCode() );
            }
            assert list != null;
            list.add( d );
        }
        return result;
    }

el que el formateador no lo muestre bien, prueba mi punto

Lo que hace el código no es importante en si mismo, simplemente transforma una lista en un mapa, el problema es que la declaración del método es ridículamente larga incluso para Java,

Antes del generics sería como:

private Map toMap( List diagnostics ) {
    Map result = new HashMap();
    for( Object o  : diagnostics ) {
        Diagnostic d = ( Diagnostic ) o;
        List list = null;
        if( !result.containsKey( d.getCode() ) ) {
            list = new ArrayList();
            result.put( d.getCode() , list );
         } else {
            list = result.get( d.getCode() );
         }
         assert list != null;
         list.add( d );
     }
     return result;
}

Claro, el problema se mueve cuando se intenta usar el contenido porque hay que andar tirando cast por todos lados, eso ya lo vivimos en la era pre-1.5 y no queremos volver ahí, pero no habría otra manera?

En Java 1.7 ya será posible usar el "operador" diamante, pero no parece ser suficiente:

// pre 1.7
Map<String, List<Diagnostic<? extends JavaFileObject>>> result = new HashMap<String, List<Diagnostic<? extends JavaFileObject>>>();
// con 1.7
Map<String, List<Diagnostic<? extends JavaFileObject>>> result = new HashMap<>();

Como lo hacen otros lenguajes? C# por ejemplo? , C++? Scala? alguién sabe?

Desde que encontré SML me parece genial su inferencia, pero a veces pienso que demasiada magia hace daño.

Bueno, ya me quejé...

:)

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de ezamudio

usuarios, no programadores

El ejemplo que estás poniendo al principio es como de código que haces en una biblioteca o algo así, para implementar una API que otros van a usar. Cuando haces eso eres como un programador que hace software para programadores; otros programadores son tus usuarios.

Lo de generics está orientado a facilitarle la vida a los programadores usuarios, los que usan APIs, no los que las hacen.

Pero... estoy muy de acuerdo es que puede ser absurdamente largo. No entendí bien por qué decidieron implementar los generics como lo hicieron; eso de tener que poner el maldito <? extends Padre> como que no viene al caso; la neta es que deberías poder poner simplemente <Padre> y con eso deberías esperar instancias de Padre o de cualquier clase que extienda Padre. Digo, la herencia sigue aplicando, no? Pues no, en generics no... si pones <Padre> solamente puedes tener instancias de Padre, y resulta que no puedes meter instancias de clases que extienden Padre. No tiene ningún sentido.

Imagen de rugi

Je.... seguramente quièn lo

Je.... seguramente quièn lo implementò tenía algún complejo de "quedar bien con todos".

Justo describes el escenario de "utilidad", que ocurre si (si y solo si) quieres esperar únicamente instancias de la clase Padre y no de las que lo extienden (o a la inversa), pues, se define justamente un mecanismo de discriminación.

Lo interesante (y lo que le daría cierta validez a esta teoría) es encontrar escenarios de este tipo.

Saludos!!

Muy cierto lo de Generics

Muy cierto lo que menciona Oscar. La verdad es que si se puede hacer algo como agregar un dato a un generico, osea esto:

Map<String, Padre> m = new HashMap<String, Padre>();
               
m.put("A", new Hijo());
m.put("B", new Padre());
m.put("C", new Hijo());
m.put("D", new Padre());

siempe y cuando :

class Padre{}
 
class Hijo extends Padre{}

   

Recuerdan cuando postearon el lanzamiento de Ceylon donde @bferro nos mencionaba lo de "covariante, contravariante" que yo mencionaba que lo conocia como como UpCasting y DownCasting que tambien Enrique nos anticipaba que en java no se podia hacer justamente esto... pues la excepcion no es para los generics donde no puedes hacer por ejemplo:

aunque si puedas hacer:

Number x = new Integer(2);

   
Sea como sea, no se que tanta lógica para java sea el no soportar esto (como dice Enrique "No entendí bien por qué decidieron implementar los generics como lo hicieron")
  
Por cierto tuve que leer como 5 veces tu codigo para entenderle, si que es poco comun ver algo asi

Imagen de ezamudio

La diferencia

La cosa con el extends es que permite más flexibilidad en asignaciones cuando hay generics más específicos... es más fácil explicarlo con código:

List<Integer> ints = new ArrayList<Integer>();
List<Number> nums = new ArrayList<Number>();
//Eso no se puede
ints = nums; //no compila, tiene sentido
nums = ints; //tampoco compila, esto es lo que me molesta, si finalmente Integer es subclase de Number...
//Pero esto no tiene bronca
List<? extends Number> both = nums;
both = ints;

Wildcard

Me queda una duda, tengo entendido que no se puede utilizar el wildcard al momento de hacer la instanciación del objeto al cual apuntará la referencia, ejemplo:

List<?> lista = new ArrayList<? extends Number>(); //No compila

Solo se puede utilizar el wildcard al declarar la referencia.

La duda me surgió por que Oscar lo usa en su ejemplo

Map<String, List<Diagnostic<? extends JavaFileObject>>> result = new HashMap<String, List<Diagnostic<? extends JavaFileObject>>>();
Imagen de ezamudio

indirección

El código de Oscar compila porque el wildcard se lo aplica al Diagnostic, no al mapa externo ni a la lista que se define como valor en el mapa.

Imagen de bferro

Las listas y las otras colecciones en Java son invariantes

Lo que molesta a Enrique (@ezamudio). Repito aquí el código que escribe Enrique:

List<Integer> ints = new ArrayList<Integer>();
List<Number> nums = new ArrayList<Number>();
//Eso no se puede
ints = nums; //no compila, tiene sentido
nums = ints; //tampoco compila, esto es lo que me molesta, si finalmente Integer es subclase de Number...
//Pero esto no tiene bronca
List<? extends Number> both = nums;
both = ints;

Tendrás que aceptar esa molestia. Java no permite especificar la covarianza ni la contravarianza en el momento de la definición. Solo lo puede hacer de forma limitada en el momento de la llamada con el uso de comodines como lo escribes en tu código.
Las listas genéricas son invariantes con respecto al argumento tipo con el que las parametrizas.
 List<Integer> y List<Number> están en el mismo nivel de la jerarquía, aunque Integer sea un subtipo de Number.

Por herencia...

Creo que la forma correcta de leer <? extends Number> es que estas declarando explicitamente que aceptas CUALQUIER tipo de objeto que herede de Number... asi ya cobra mucho sentido usar el wildcard PERO NO DEBERIA SER NECESARIO como anteriormente se comentaba.

estas declaraciones que tienen que ser explicitas me imagino que es para que semanticamente solo uedan hacer downcast y no upcast porque al hacer List<? extends Integer> both = nums; ya no serviria este codigo

Imagen de bferro

El hubiera no existe

La covarianza, la contravarianza pueden ser tan útiles como la invarianza.
Java decide no aplicar la covarianza en el sitio de definición de una colección genérica. Scala permite que en el sitio de definición puedas determinar que la colección es invariante Coleccion[T], que es covariante Coleccion[+T] o que es contravariante Coleccion[-T].

Un ejemplo mundano de uso de una colección invariante. Vas a un restaurant argentino y pides una parrillada (una colección de cárnicos). El camarero ama la covarianza y te sirve en la mesa una parrillada de puro chorizo. El tipo es programador y entre su reglas está que un chorizo es un tipo de cárnico y por consiguiente aplica la herencia siguiendo su pensamiento covariante.
Seguramente te vas a enojar.

Imagen de ezamudio

tupla

Una parrillada es más como una tupla que una colección simple con Generics, porque está muy claramente especificado lo que incluye. Ningún menu la define como "carnes varias", siempre dice cuáles trae (arrachera, pollo, chorizo, costilla, etc).

Imagen de bferro

Totalmente de acuerdo y esa es la razón de uso de la invarianza

Esperaba esa respuesta, porque precisamente esa es la razón de uso de una colección genérica invariante, cuando impones esa restricción en el diseño donde no permites la covarianza.
Otro ejemplo mundano es el de una cesta de frutas y lo que te sirven es un cesta de puras naranjas.

Eso no quiere decir que la covarianza se necesita para otros escenarios. Lo que falta en Java es lo que añaden otros lenguajes como Scala, de darle al programador la posibilidad de decidir eso en el momento de la definición de la colección, incluyendo el caso invariante.
C#, así como otros lenguajes para el CLR tampoco soportan o soportaban la covarianza. En el caso de C# lo soporta a partir de la versión 4.0 para interfaces y delegados.

Imagen de bferro

Curiosamente las tuplas en Scala sí son covariantes

Las tuplas en Scala sí son covariantes, de forma que el siguiente código es válido:

class A {}
class B extends A {}
class C extends A{}
var tuplaABC =new Tuple3[A, A, A](new A(), new A(), new A())
// y ahora uso la propiedad covariante de Tuple3

tuplaABC = (new C(), new C(), new C())

Si no se parametriza correctamente la tupla, pueden servirte una parrillada de puro chorizo. El lenguaje requiere que las restricciones de diseño estén claras.

Imagen de Nopalin

Diseño

Pues igual tienen razón en lo que todos comentan de los generics en java, pero en lo personal creo que lo que comenta oscar no es el problema correcto para la realización de una app, si no el quie diseño un metodo con tantos generics?

Aunque ya si se esta tratando de testear el lenguage ps si, entonces si hay bronca.

Por otro lado, java no te forza a que la implementación tenga generics, tu puedes hacer esto:

List<Integer, Map<String,List<? extends Sillon>>> list = new ArrayList();

a final de cuentas lo que vas a utilizar es la variable con la declaración de su tipo, no la implementación, ya que como muchos han dicho, los generics no se checan en tiempo de ejecución si no de compilación.

sobres

@nopalin Hice lo que

@nopalin Hice lo que sugieres pero sale esto mira:

$ cat Sillon.java
import java.util.*;
class Sillon {
        List<Integer, Map<String,List<? extends Sillon>>> list = new ArrayList();
}
$ javac Sillon.java
Sillon.java:3: wrong number of type arguments; required 1
        List<Integer, Map<String,List<? extends Sillon>>> list = new ArrayList();
            ^
1 error

También es posible usar colecciones no generificadas (¿?) pero habría que volver a cast-landia y mejor no.

El ejemplo que puse es un caso de la vida real donde estoy usando un API de Java ( Diagnostic )

Al final lo terminé "resolviendo" ( aunque no había nada que resolver ) creando un tipo de dato que se pudiera usar en vez de toda la declaración esa.

Creé una clase privada:

static final class DiagnosticList extends ArrayList<Diagnostic<? extends JavaFileObject>

Y con eso sustituí:

private Map<String, List<Diagnostic<? extends JavaFileObject>>> toMap( List<Diagnostic<? extends JavaFileObject>> diagnostics ) {

Por:

private Map<String, DiagnosticList> toMap( DiagnosticList diagnostics ) {

Lo cual mejora la legibilidad y por otro lado refuerza la semántica del código ( es decir, dice mejor que quiero hacer )

Imagen de ezamudio

WTF?

A ver, me perdí de algo, ya están usando Java 8, o en Ryz se puede definir una List con 2 tipos genéricos?

Imagen de bferro

WTF?

Eso digo yo Enrique

Imagen de Nopalin

equivocacion

jajaja chale, en realidad si hize un errorsote por no probar el codigo, en ralidad fue algo que puse asi a la rápida para demostrar el punto de que la implementación no debe llevar generics obligadamente, solo la declaración de tipo de la variable, a ver ahora si un ejemplo que compila:

aunque como dije en el comentario anterior, quien haga una declaración de tipo con tantos generics tiene un mal diseño, lo mejor como hizo oscar es hacer un objeto que englobe los datos que quiere manipular, aunque bueno eso ya es cuestion de la forma de ver de cada uno.

saludos

Ahhhhh seeeeehh.... ¬¬ por

Ahhhhh seeeeehh.... ¬¬ por eso digo que NO compila :P

Ahora si aplica mi segundo comentario, se puede hacer pero hay que volver a castinglandia.

$ cat Sillon.java  
import java.util.*;
import java.math.*;
class Sillon {
    List<Map<String,List<Map<BigDecimal,Map<String,Comparator<Sillon>>>>>> var = new ArrayList();
}
$ javac -Xlint:unchecked Sillon.java
Sillon.java:4: warning: [unchecked] unchecked conversion
found   : java.util.ArrayList
required: java.util.List<java.util.Map<java.lang.String,java.util.List<java.util.Map<java.math.BigDecimal,java.util.Map<java.lang.String,java.util.Comparator<Sillon>>>>>>
    List<Map<String,List<Map<BigDecimal,Map<String,Comparator<Sillon>>>>>> var = new ArrayList();
                                                                                 ^
1 warning

Ese warning: [unchecked] unchecked conversion es como tirar a la basura el chequeo y se soportó para ser retro-compatibles.

Por cierto... si fueran a

Por cierto... si fueran a diseñar un nuevo lenguaje, como harían los generics :) :) :)

Lo que yo tengo pensado es soportar la covarianza (¿o era contravarianza?) por default, que sería también para mí un tanto más natural.

Así lo anterior sería:

demo.Num {
   test() {
       var :  List[Number] = ArrayList[Number]()
       var.add( Integer( 0 ) )
       var.add( Double( 0 ) )
       var.add( Byte( 0 ) )
    }

Aunque bueeeno eso lo tengo muy lejos en mi roadmap e incluso se podría considerar acaso un tema avanzado ( espero que todos lo consideren así y no lo sea solo porque yo no domino el tema )

La alternativa es no usar generics at all e inferir el tipo de dato cuando sea posible y cuando no decir: "El compilador no pudo inferir el tipo..." y forzar a usar casting.

...
    var : List = ArrayList()
    var.add( 1 ) // aha... list of integers uh?
    var.add( "2" ) // <-- compile time error ...  

Pero claro, hay problemas serios cuando se usa como parametro:

 ....
    useSort() {
          sortMe( Arrays.asList(1,2,3,4) )
          sortMe( Arrays.asList("a","b","c"))
    }
    sortMe(  aList :  List ) : List  {
    .... // list of what?
    }

El dilema estaría en saber como declarar "aList" si un List[Integer] , un List[String] o un List[Object]

En fin, aun falta mucho para eso, a ver si para entonces ya entendí bien los generics.

Imagen de rojovizcaino

+1

Si la pregunta es ¿qué podemos hacer? pues como dice Nopalin mejora el diseño encapsulando las estructuras de datos en objetos creo es la mejor respuesta. Sobre todo cuando se trata de parametros o retornos de métodos públicos. ¿Cuántas veces no han visto repetido un ciclo for sobre una lista donde se busca el elemento con la propiedad tal que cumpla la condición cual regado por todas partes? Si la estructura se encuentra encapsulada ese código queda en un sólo lugar y todo mundo es más feliz.

Objetos

Yo pienso igual que java: si no defines e tipo que sea de objetos, por que? Pues porque todos vienen de Object y blah blah blah cosa que por default puedes convertir lo que sea a Object... de otra manera si quieres hacerlo mas inteligente tendrias que introspeccionar (uuuf que palabra!) tu objeto lista para ver si puedes llegar a una clase padre que pueda ser el tipo de la lista

lista : List = ArrayList() // es List<Object>

lista.add 1
lista.add 25.4 * 2.0
lista.add 5f

cuando quieras hacer:

otraLista : List<Object>  = lista // deberia de poder hacerlo

...

otraLista : List<Number>  = lista // deberia de poder hacerlo

...

otraLista : List<Integer>  = lista // aqui habria un conflicto

Pero pienso yo: ¿Por qe en la ultima asignacion hay conflicto?

Ah pues porque la clase padre (que tienen en comun) no es de tipo Integer. No se si meexplico... a lo que voy es que seria bueno tener un autoanalisis en tiempo de ejecucion para que se pueda determinar si es factble la asignacion o no lo es

@rojovizcaino Yeap, de

@rojovizcaino

Yeap, de acuerdo, eso es precisamente lo que terminé haciendo; creando un tipo de dato que exprese mejor mis intenciones.

@java.daba.doo

El análisis en runtime ya está, se llama "class cast exception" el que se necesita es en tiempo de compilación. Interesante tema quizá luego podamos profundizar al respecto.

+1 por acordarte de :

list .add 1

se podría crear un método de extensión

...
+=( aList : List,  o : Object  ) : Boolean {  
    aList.add( o )
 }
 

para poder escribir:

lista .+= 1

:)

Existe validacion, pero a

Existe validacion, pero a poco (por ejemplo en la lista) te examina los objetos que contiene "en tiempo de ejecucion" y te devuelve una coleccion de la clase padre de todos los objetos?

A eso me queria yo referir

style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">