Java 8: mis primeros tropiezos

Pues ahora que ya salió oficialmente Java 8, lo instalé y empecé a hacer pruebas, con un proyecto grande que ya llevo desde hace varios años (empezó en Java 1.4 y lo he migrado a Java 5, 6, 7 y ahora seguirá la 8).

Cuando he migrado, las etapas suelen ser así:

Primero correr las aplicaciones tal cual están, sin modificar nada, sobre la nueva JVM. Eso parece que está funcionando bien (pero tendrán que estar en observación varios días obviamente).

Luego, compilar el código tal cual está, sin modificar nada, con el nuevo compilador. Aquí es donde me di de topes ya desde ahorita. Pasaron dos cosas:

Utilizo Lombok para reducir código en varios lugares y facilitarme la vida en general cuando programo en Java. Una de las cosas que trae es la anotación @SneakyThrows, que permite tratar una excepción verificada como si fuera RuntimeException al momento de compilar. Pero al compilar código que utiliza esta anotación en Java 8:

post-compiler 'lombok.bytecode.SneakyThrowsRemover' caused an exception: null

Y esto es usando la versión 1.12.6 que se supone ya trae soporte para Java 8.

Otra biblioteca que utilizo es slf4j, y este error sí me puso de mal humor. Resulta que en la versión 1.7 de slf4j metieron por fin parámetros variádicos para formatear logs, de modo que tenemos por ejemplo warn(String m, Object... args), pero conservaron el obsoleto warn(String m, Throwable t) y esto da problemas al compilar en Java 8, porque si tienes algo como log.warn("hola", metodoQueDevuelveObject()) resulta en un error:

error: reference to info is ambiguous

both method info(String,Object...) in Logger and method info(String,Throwable) in Logger match

Me parece una estupidez que el compilador no pueda distinguir entre ambos. Carajo, si el argumento no es un Throwable, ps entonces usas la otra variante, no? Pero bueno, es una de las broncas de tener sobrecarga de métodos, pero lo peor es que esto compila y corre perfectamente en Java 7, pero es un error en Java 8.

Así que por ahora hasta aquí llegué con las pruebas; voy a revisar si lo de slf4j es un problema en otro lado, pero lo de Lombok definitivamente es un showstopper.

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

slf4j

OK ya identifiqué el problema con slf4j. Realmente no es un problema con slf4j; no estoy seguro si esto era un defecto del compilador en Java 7, o simplemente hicieron más estricto el compilador en Java 8 o qué. Pero la causa del problema es que estoy pasando un solo argumento pero es genérico, es decir, tengo algo así:

public class Clase {
  public <T> T metodo() { ... }
}

Por qué lo tengo así? Pues porque antes de Java 5 lo tenía como public Object metodo() y tenía que hacer cast cada vez que lo llamaba de esta forma:

String x = (String)metodo();

Pero con la nueva variante ya no es necesario el cast:

String x = metodo();

Pero al llamarlo directamente como argumento en los métodos de slf4j, efectivamente es ambiguo; podría ser Object, o podría ser Throwable. Ahora lo que no entiendo es, por qué el compilador de Java 7 no se quejaba de esto...

Imagen de ezamudio

SneakyThrows

Mientras tanto ya registré el issue de Lombok.

Es la primera vez que liberar

Es la primera vez que liberan la versión para OSX al mismo tiempo.. super!

Workaround

Tssssss, eso del SLF4J si está de terror. Osea que por ahora no hay mas que un mal workaround, seria definir explicitamente el Object[]

log.warn("hola", new Object[] {metodoQueDevuelveObject() })

O un utilisimo Monkey Patch

Imagen de ezamudio

cast, null

Con hacer un simple cast funciona también: log.warn("hola {}", (Object)metodo()) o incluso puedes hacer algo que no está muy padre pero funciona: log.warn("hola {}", metodo(), null) (con eso ya el compilador sabrá que quieres usar warn(String,Object...) y pues como nada más hay que hacer un reemplazo en el string, el null será ignorado).

Pero como ya dije antes, ahora que entiendo el error, lo que no entiendo es por qué no salía en Java 7 (ni 6 ni 5).

De hecho ya para mi ahorita es más grave lo de lombok que lo de slf4j (que realmente se puede generalizar pero se vuelve algo complejo de describir).

Imagen de bferro

Varios bugs en Java 7

En Java 7 hay varios bugs relativos a la determinación del método más cercano (JLS sección 15.12). Seguramente los habrá en Java 8. Imagino que algunos se han arreglado. Pongo un ejemplo aun más sencillo que el de Enrique para ver que sucede lo contrario. Compílenlo con java 7 y con java 8, y ejecútenlo en el que compila

class Borrame {

    static void metodoSobrecargado(float i, Character... args) {
        System.out.println("Signature (float i, Character... args)");
    }
    static void metodoSobrecargado(Character... args) {
        System.out.println("(Character... args)");
    }
    static  void main(String[] args) {
        metodoSobrecargado(1, 'a');
        metodoSobrecargado('b', 'c');
    }
   
}

Imagen de julgo

segundo metodo

el problema en el segundo método (Character... args) para que llegue a compilar basta con pasarle una variable char o un Character

 
class Borrame {
    static void metodoSobrecargado(Character... args) {
         System.out.println("(Character... args)");
     }
     public static  void main(String[] args) {
char c=1;
Character ch=90;
         metodoSobrecargado(c,ch, 'a');
//o también metodoSobrecargado(lista) donde lista es Character [] lista={1,2,3};
      }
    }

- asi que me marcaria error cuando el paso es como secuencia de argumentos 1,'a' pero se supone que java hace la conversión a la matriz  Character [] lista={1,'a'};
algún problema en el cast ??

Imagen de bferro

Lo que comentamos es si compila o no en una versión u otra

Lo que se comenta son las diferencias en resolver el método más específico en las versiones diferentes de Java y los cambios que se realizan. para cada versión. ¿ya lo compilaste en Java 7 y en Java 8? Es bueno echarle un vistazo a 15.12.2.5. Choosing the Most Specific Method

Imagen de julgo

tengo java 7 todavía pero mi

tengo java 7 todavía pero mi comentario iva mas a los bugs de java 7 y si este problema llegue a catalogar como bug o es un comportamiento normal.
por ejemplo:

static void metodoSobrecargado(float args) {
          System.out.println("(recibe un float)");
      }
//funcione tanto metodoSobrecargado(1);
// y también  metodoSobrecargado('a');

pero esto no:

static void metodoSobrecargado(Character args) {
          System.out.println("(recibe un character)");
      }
//no puede un int convertirse a caracter en metodoSobrecargado(1);
//si se puede asignar char c=1; y Character ch=1;

Imagen de bferro

Eso no tiene que ver con sobrecarga de métodos

Lo que comentas julgo no tiene que ver con la sobrecarga de métodos, ni tampoco es un bug. Lo que aplica en ese caso es el mecanismo de inboxing, el tratamiento de char como tipo aritmético y la promoción de tipos aritméticos. Las últimas dos están presentes en casi todos los lenguajes de programación y el inboxing funciona como debe ser: un Character "encajona" un char.

Imagen de julgo

si ya se que no tiene que ver

si ya se que no tiene que ver con la sobrecarga de métodos @bferro solo tome tu ejemplo y no me preocupe en cambiarle de nombre, y si un Character encajona un char asi como un Integer a un int , mi pregunta iva a la asignación que es valida de
Character ch=1; pero al invocar un método no pueda hacerse con método_imprime(1); donde el método sea

static void método_imprime(Character argumento) {
           System.out.println("(recibe un ??)");
       }

Imagen de bferro

Claro que no puede hacerse

Por supuesto que no puede hacerse. El mecanismo de inboxing es crear un objeto al vuelo que son instancias de las clases wrappers de los tipos, y lo hace con el constructor de esa clase Character(char c). Es importante entender la diferencia en la comprobación que hace el compilador referente a la posible pérdida de precisión cuando declara e inicializa una variable y cuando cuando comprueba esa posible pérdida de precisión en el momento que invoca una función pasando su argumento correspondiente.
El tipo char primitivo en Java es un caracter Uincode de 16 bits (2 bytes), con valores estrictamente positivos en el rango de  \u0000 a \uFFFF . En el momento que se declara una variable de tipo char y se inicializa con un literal entero, el compilador comprueba si no hay pérdida de precisión en la inicialización. Si hay pérdida rechaza tal incialización. Así,

char car = 65535; // el valor máximo posible

es aceptado por el compilador, pero:

char c = 65536; // un valor fuera del rango permitido

no es aceptado y marca un error. Recordemos que el tipo int ocupa 32 bits y es un tipo con signo.
La comprobación de posible pérdida de precisión es más estricta cuando se trata de pasar un argumento a una función. En ese caso el compilador atiende exclusivamente al tamaño del tipo de dato y no a su valor. Supongamos la siguiente función:

 
static void funcion(char c) {}

Al llamar a funcion(1), el compilador solamente comprueba que un valor de tipo int de 4 bytes no puede ser usado para inicializar un valor de tipo char. En esta comprobación no tiene en cuenta el valor preciso de int.
Por supuesto que si indicas una coerción (downcasting) explícita no habrá error:

funcion ( (char) 700000);

compilará sin problemas.

Imagen de julgo

buena explicacion

-muy bien explicado @bferro la duda estaba relacionada en la forma como trabaja el compilador ya que asumia que en esos casos hacer el cast era innecesario.

 static void metodo_para_shorts(short arg) {
          System.out.println("(un short)");
      }
//aquí se llama al método convendría que sea tratado como short implícitamente.
 metodo_para_shorts(1);

-pero ya veo que int es la elección predeterminada y no se pierde nada con hacer un metodo_para_shorts((short)1);
y sin ello no podrían hacerse métodos sobrecargados que reciban int y short por ejemplo ya que seria ambiguo invocándolos
metodoSobrecargado(1);

Imagen de bferro

Claro que se puede

Por supuesto que se puede:

     static void f1(short s){System.out.println("short");}
     static void f1(int i){System.out.println("int");};
     static void f1(long l){System.out.println("long");}

Java define notación para expresar el tipo de un literal entero (de tipo int), literales de tipo long, double, float, pero no define notación alguna para decir que un literal es de tipo short o tipo byte. La razón es que estos dos últimos tienen poco uso con respecto a los demás. Por esa razón, siempre será necesario realizar un casting cuando se desea una "democión" de un literal int, double, float o long a un literal de tipo byte, char o short.
Pero lo anterior no impide la sobrecarga de métodos.

f1(1);    //Imprime int
f1(1L) // Imprime long
f1((short) 1D); // Imprime short. Aquí es necesaria la coerción.
Imagen de julgo

si se puede

digo que analizandolo bien si tiene sentido hacer la coerción y un int sea el predeterminado ya que de no ser asi invocando f1(1) , en un método sobrecargado habría problemas de ambigüedad

  static void f1(short s){System.out.println("short");}
   static void f1(int i){System.out.println("int");};

con eso queda mas claro como java trabaja con los tipos primitivos, en cuanto al tipo short tiene prácticamente poco uso como bien dices, tendrá ventajas significativas en comparación a int o doublé en determinados casos??? is more efficient to use short??, tal ves en otro post para no desviar el tema

Imagen de ezamudio

autoboxing?

Será que Bárbaro ya usa demasiado facebook? jejej

Imagen de bferro

No entendí brother

No entendí.

Imagen de ezamudio

s/inboxing/autoboxing

s/inboxing/autoboxing

Imagen de bferro

Ya caí.

es que siempre cuando me refiero a autobox, si lo meto siempre digo in box y cuando lo saco digo out box. Me hiciste reir.