A propósito de la declaración explícita de tipos y su comprobación en tiempo de compilación

En discusiones recientes en este sitio se ha hablado mucho de los lenguajes tipados y no tipados y la mezcla de ambas técnicas en algunos lenguajes, y me acordé de un ejemplo de Joshua Bloch en su libro Effective Java que, aunque simple es muy ilustrativo.

Java, como sabemos, es un lenguaje "estricto" en tipos. Escribo entre comillas la palabra estricto para no meterme en broncas con la teoría de tipos. Lo que pretende Java y otros lenguajes es evitar al "máximo" (otra vez las comillas por la misma razón) los errores en ejecución por la incompatibilidad de tipos.

Joshua Bloch se pregunta entonces

por qué definir

  1.  java.io.ObjectOutput.writeObject(Object obj)
  2.  

cuando realmente debe ser

  1.  java.io.ObjectOutput.writeObject(Serializable obj)
  2.  

Errores sencillos que ilustran conceptos importantes.

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 luxspes

Serializable Interface es un ejemplo de "Fuga en la Abstraccion"

Bueno, en mi opinión, la interfaz Serializable ya es en si misma un error/limitacion (una Fuga o Falla en la abstracción representada por el lenguaje), por que en realidad no define método alguno, es lo que se conoce como una "marker interface" y la unica razon por la que todavía existe, es por que cuando se invento Java, no pensaron en ponerle @Annotations desde un principio (como en C# ) y Sun (y ahora Oracle) no se ha atrevido a marcarla como deprecated y utilizar una Annotation.

Al final, este tipo de errores son un reflejo de las limitaciones intrínsecas en los lenguajes de moda en este momento en donde "torcemos" a los lenguajes, usando características con propósitos que no son para los que fueron diseñados (como por ejemplo los prefijos "get" y "set" en Java, en vez de tener verdadero soporte para properties) de ingeniosas maneras que a veces ayudan a escribir código mas legible... y veces simplemente reflejan problemas que revelan las limitaciones del lenguaje

Imagen de bferro

Poco tiene que ver el ejemplo con si es "marker" o no

El ejemplo poco tiene que ver con el asunto de que la interfaz sea una interfaz marcadora como es el caso de la interfaz Serializable.

Es un ejemplo sencillo de no utilizar correctamente el sistema de tipos del lenguaje para evitar el disparo de una excepción en ejecución que se evita escribiendo, en este caso, el tipo correcto del argumento de la función writeObject.

Que las interfaces marcadoras sean cosas como las que dices es otra historia, pero poco tiene que ver de que la interfaz esté o no vacía con lo que Joel Spolsky escribió de que todas las abstracciones "no triviales" son leaky, que por cierto ha sido tremendamente "balconeado" por muchos.

Que las anotaciones sustituyen totalmente a las interfaces marcadoras es un criterio de cada cual. Yo en lo personal considero que ambas son útiles y distingo su uso dependiendo de lo que quiero representar en la solución.

Imagen de benek

Serializable

Es cierto, hay un hueco, writeObject() terminará en IOException si el objeto pasado no implementa Serializable, pero el método realmente no lo está pidiendo, es un hueco de definición.

Algo también interesante sería saber por qué sigue así, backwards compatibility? No lo creo. Podría ser quizá porque exista una remota manera de utilizarlo sobre un objeto no serializable o también quizá para dejar abierta la posibilidad de que esto se pueda realizar sobreescribiendo el método.

Interesante tema.

Saludos.

Imagen de Nopalin

La forma de programar

Así como el nuevo paradigma de programación funcional tiende a atacar desperfectos en la OOP, se debería analizar por que suceden esos desperfectos. Me he topado con muchos programadores no que conocen realmente como fucniona un lenguaje, esperan a que el IDE les haga la gran mayoria y ellos codifiquen lo menos posible. Tal y como lo muestra el ejemplo, ¿por que definir el argumento como object y no serializable si a final de cuentas la implementacion solo soporta tipo serializable?, una razon puede ser por lo que dijo @benek pero entonces se tendria que analizar si en realidad es útil que se permitan serializar todos los objetos, o mejor solo aislar a los objetos definidos para ese propósito. A lo que voy es que no solo se debe analizar las peticiones de los clientes para que un sistema haga algo, si no que tambien se debe analizar la implementación, por que se hace de una manera en especial y por que nó, y como dije hace rato, para esto es necesario que el programador entienda bien los conceptos del lenguaje que utiliza y cuales son sus capacidades, no solo utilizar lo que el IDE le provea.

Las anotaciones son útiles, pero igual pienso que se estan abusando de ellas (acuerdense del nada con exceso, todo con medida). Con ellas se obliga al que sea el programador que cheque por los tipos (o las annotaciones utilizadas) para hacer una u otra cosa, cuando tal vez ese problema se lo deberiamos dejar al compilador.

Pero bueno, aqui ya entra el gusto por la forma de codificar de cada quien, y no se puede decir que algo sea mejor que lo otro ni viceversa.

sobres

Imagen de ezamudio

Razón

ObjectOutput es una interfaz y la documentación del método writeObject dice claramente "The class that implements this interface defines how the object is written".

En la práctica, el problema es con la implementación más popular de esa interfaz, que es ObjectOutputStream; esta clase es la que arroja NotSerializableException si se le pasa un objeto que no es serializable. Pero... eso fue decisión tomada al implementar ObjectOutputStream. Se deja Object en el método ObjectOutput.writeObject() porque así puede haber implementaciones distintas, que por ejemplo busquen una anotación o verifiquen cierta herencia, que serialicen de otra forma o incluso que escriban otra cosa al stream para ciertas clases y entonces no se requiere que sean Serializable.

Imagen de luxspes

No se, creo que el ejemplo forma parte de una tendencia

El ejemplo poco tiene que ver con el asunto de que la interfaz sea una interfaz marcadora como es el caso de la interfaz Serializable.

Tu crees? Revisemos el concepto de interfaz: "Implementing an interface allows a class to become more formal about the behavior it promises to provide. Interfaces form a contract between the class and the outside world, and this contract is enforced at build time by the compiler. If your class claims to implement an interface, all methods defined by that interface must appear in its source code before the class will successfully compile"

Pero esta interfaz ni siquiera tiene metodos, la usaron, como te explico, como una marker interface por que Java todavia no tenia anotaciones... este blog post dice tratarse del manejo de tipos... bueno, pues este es un ejemplo en el que se esta usando una interfaz para un proposito para que no fue diseñada, yo pienso que si esta relacionado,normalmente uno usa una interfaz para garantizar que el objeto que llega de parametro implementa algun metodo que sera necesario para la ejecucion del metodo que recibe al objeto implementador, pero en este caso no existe metodo alguno (ya desde ahi esta mal la cuestion), en mi opinion, ya desde ahi, al usar interfaces para lo que no son, el modelo mental del programador que escribio esto esta mal y no le importa usar incorrectamente a la interfaz, por que ni siquiera la esta usando para lo que fue diseñada.

Es un ejemplo sencillo de no utilizar correctamente el sistema de tipos del lenguaje para evitar el disparo de una excepción en ejecución que se evita escribiendo, en este caso, el tipo correcto del argumento de la función writeObject.

Si, tambien muestra eso, pero considero interesante hacer notar que se cometio el error con una marker interface... conoces otro caso similar en el API de Java en donde se haya cometido el mismo error, pero sin usar una marker interface? Tal parece que este "error" se ha convertido ahora en toda una "tendencia", en donde se recibe "Object" de parametro, y se lidia con el problema de forma interna... tal vez el error fue abandonar las marker interface?

Que las interfaces marcadoras sean cosas como las que dices es otra historia, pero poco tiene que ver de que la interfaz esté o no vacía

Por el contrario, tiene mucho que ver, las interfaces se crearon para definir contratos entre clases de objetos, y esos contratos se definen mediante metodos, una interfaz vaciano tiene sentido alguno dentro de los parametros definidos por la abstraccion "Interfaz", pero como Java originalmente no tenia mecanismos para definir metadatos, decidieron que era facil pervertir la abstraccionde interfaz, y usarla mas alla de sus parametros. (Por otro lado, tal vez no es tan mala idea?)

con lo que Joel Spolsky escribió de que todas las abstracciones "no triviales" son leaky, que por cierto ha sido tremendamente "balconeado" por muchos.

No entendi... balconeado como? liga por favor?

Que las anotaciones sustituyen totalmente a las interfaces marcadoras es un criterio de cada cual.

Si? Yo no creo que se a criterio, creo que mas bien depende de la situacion (no de la opinon de cada quien)

Yo en lo personal considero que ambas son útiles y distingo su uso dependiendo de lo que quiero representar en la solución.

Si, es posible, aunque tambien es curioso que tras aparecer las anotaciones, el uso de las marker interface bajo drasticamente (por que, por ejemplo @Entity es una annotation y no una marker interface? Pensandolo con calma, hasta podria considerse, dada la incapacidad de Java para verificar las Annotations en tiempo de compilacion, como una mala decision (si fuera una inteface, los metodos en el EntityManager, como por ejemplo, persist, recibirian de parametro "Entity", en vez de recibir Object... no? Por que habran decido no ir por ese camino? Sera que hay alguna otra limitacion en Java que lo hace una mala decision?

Imagen de bferro

No es una tendencia, el ejemplo tiene ~15 años

No soy partidario de mezclar en un mismo hilo de discusión varios temas. Prefiero, que si el aporte que alguien hace da pie para otro tema, discutirlo en otro thread. Así la discusión no se convierte en una madeja.

Comento entonces solamente las cosas que tienen que ver con el ejemplo original.

Sobre la interfaz marcadora y las anotaciones:

En el ejemplo es irrelevante que Serializable sea un marker interface. El ejemplo sigue siendo válido, aun en el caso que Java hubiera diseñado su mecanismo de Serialización con una interfaz con métodos (eso sería otro tema de discusión) que deberían ser implementados por las clases de objetos serializables.

Las interfaces marcadoras y las anotaciones son cosas similares y a la vez diferentes. Uno de los usos de las interfaces marcadoras es establecer convenciones y restricciones en el diseño en lugar de hacerlo de forma programática.

Los que diseñaron Java y los que lo usamos, conocemos bien el concepto de interfaz que escribes en negrita. También sabemos que los que diseñaron Java conocen muy bien las diferencias entre una interfaz marcadora y una interfaz. La ausencia de una nueva palabra reservada para denotar esa diferencia se debió mayormente a mantener el lenguaje con un vocabulario pequeño. El concepto de anotaciones como un mecanismo de introducir información de metadatos es conocido desde hace mucho tiempo. No es algo que se inventó en estos años.

Siempre en la evolución de los lenguajes llega el momento de introducir técnicas. Depende del contexto actual, de la tecnología disponible, y también de la expertisidad de los programadores.

Hay diferencias entre las interfaces vacías y las anotaciones. Si es de interés puedes comenzar un hilo de discusión al respecto y con gusto doy mis humildes opiniones.

Con respecto a los "leaky abstractions" (abstracciones con fuga y no fuga de abstracciones) de las que escribe Spolsky, es otro tema que como comentaba tiene que ver poco con que la interfaz esté vacía o llena. Las interfaces llenas también pueden representar abstracciones con fuga. No es nada nuevo: las abstracciones por naturaleza propia tienen lo que Spolsky llama en su artículo "fugas". Es algo inherente a una abstracción. No hace falta enunciar una ley para eso. Donde sí hay mucha "carnita" sobre las abstracciones es en el artículo de G. Kiczales al que se hace referencia en el sitio de Wikepedia que mencionas.

Opiniones sobre lo que Spolsky dice sobre leaky abstractions hay muchas en la red. El sitio de Joel on Software es muy leído y de ahí la cantidad de opiniones a lo que se discute en ese sitio.
Saludos,
Ferro

Imagen de luxspes

Que tiene que ver la antiguedad con si es o no una tendencia?

No soy partidario de mezclar en un mismo hilo de discusión varios temas. Prefiero, que si el aporte que alguien hace da pie para otro tema, discutirlo en otro thread. Así la discusión no se convierte en una madeja.

Comento entonces solamente las cosas que tienen que ver con el ejemplo original.

Ta bueno...

En el ejemplo es irrelevante que Serializable sea un marker interface. El ejemplo sigue siendo válido, aun en el caso que Java hubiera diseñado su mecanismo de Serialización con una interfaz con métodos (eso sería otro tema de discusión) que deberían ser implementados por las clases de objetos serializables.

Dudo que si va hubiera diseñado su mecanismo de Serialización con una interfaz con métodos ese ejemplo existiera, el esfuerzo de llamar a los metodos requeridos mediante reflection hubiera hecho que el autor del metodo inmediatamente se diera cuenta de su error y corrigiera. No estas de acuerdo?

Por otro lado.... por que no comentaste nada con respecto a @Entity? Sientes que en ese caso no es un error recibir Object? no crees que seria mejor utilizar una marker interface? O sientes que tiene poco que ver? (al final es el mismo caso, solo que al reves, el compilador no me avisara que estoy intentando persistir una clase a la base de datos que no es "persistible" por que le falta el @Entity, pero me lo avisaria si fuera una marker interface... que habra llevado a los creadores de JPA a "olvidarse" de las marker interface? Y mas en algo tan parecido (la persistencia a bases de datos y la serializacion son casi tecnologias hermanas, algunos hasta creen que podrian fusionarse... yo no estoy de acuerdo, pero esa es otra historia)

Imagen de ezamudio

Métodos de Serializable

El problema es que Serializable sí define unos métodos pero de manera informal. Solamente se tienen que implementar si se necesita serializar alguna cosa de manera especial.

Esto es de las pocas cosas (tal vez la única) que recuerdo que me gustaron cuando pasé de ObjC a Java, porque en ObjC había un protocolo para serializar y tenías que implementar los métodos a fuerzas, codificando cada campo y decodificándolo en el mismo orden; se recomendaba una versión al principio para poder codificar o decodificar más o menos campos según la versión, etc. En Java solamente tienes que implementar los métodos para codificar/decodificar tu objeto si es que piensas serializar a un medio más permanente pero si es sólo para transmitirlos por red por ejemplo, no es tan necesario y con marcar tu objeto como Serializable es suficiente.

Que estrictamente hablando,

Que estrictamente hablando, no son metodos de Serializable, pero sí se hace una validacion para verificar que lo que se intente serializar implemente esa interfaz.

Los métodos, "mágicos" son public void writeObject( ObjectOutputStream ) y public void readObject( ObjectOutputStream

El propósito de la interrfaz marcadora, es forzar de alguna manera al desarrollador a ser consciente de que el objeto va viajar de alguna forma, ya sea a un arreglo de bytes, o a disco duro o por la red.

Cuando no se quiere que algún atributo del objeto viaje, se debe de usar la palabra reservada "transient"

public class SomeObject implements Serializable {
     private  String data;
     private transient List<SomeObject> others; // este no se serializa
}

Así, el mecanismo por omisión de Java sabrá que cosas tiene que escribir y que cosas no.

Cuando se usa el mecanismo "personalizado", la clase misma dice que cosas va a escribir y leer y como va a hacerlo. Puede resultar muy benefico, por ejemplo, cuando se quiere escribir estrictamente lo mínimo por la red o a disco.

Por ejemplo, si un objeto tiene una lista de "otros" objetos, ( que pueden ser representados en una base de datos relacional ) se podría hacer que se serializaran solamente los id's de los objetos relacionados, en vez de todo el objeto.

public class Departamento {
    private int id;
     private String name;
    private List<Empleado>  empleados;
    private void writeObject( ObjectOutputStream out ) {
           out.wirteInt( id );
           out.writeUTF( name );
           out.writeInt( empleados.size() );
           for( Empleado empleado: empleados ) {
                 out.writeInt( empleado.getId() ) ;
            }
    }
    private void readObject( ObjectOutputStream in ) {
         this.id = in.readInt();
         this.name = in.readUTF();
         this.empleados = new ArrayList<Empleado>();
         int howMany = out.readInt();// cuantos vamos a leer
         for( int i = 0 ; i < howMany ; i++ ) {
             empleados.add( Empleado.readFromDbById(  in.readInt() ) );
         }
    }
       
}

De esta forma se evita que cada elemento de la lista viaje por la red, si de todas formas ya esta en la base de datos ( por ejemplo ) y solo hacemos que viaje el id.

Acá hay un ejemplo completo para probar en casa.

import java.io.*;

// ejemplo usando serializacion personalizada, con los metodos: writeObject / readObject
public class SerializeMe implements Serializable {
        private int i;
        private String s;

        public SerializeMe( int n, String m ) {
                this.i = n;
                this.s = m;
        }

        // privado, pero aun así puede ser invocado
        private void writeObject( ObjectOutputStream out ) throws IOException {
                out.writeInt( i );
                out.writeUTF( s );
        }

        // privado, pero aun así puede ser invocado
        private void readObject( ObjectInputStream in ) throws IOException {
                i = in.readInt();
                s = in.readUTF();
        }
        public String toString() {
                return "i = " + i + ", s = " + s;
        }
        public boolean equals( Object o ) {
                return ( ( SerializeMe ) o ).i == this.i &&
                       ( ( SerializeMe ) o ).s .equals( this.s );
        }

        public static void main( String [] args ) throws IOException, ClassNotFoundException  {
                Object object = new SerializeMe( 0x2A, "Magic" );
                ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream("x.ser"));
                out.writeObject( object );
        // revisar el file system...
                Object otro = new ObjectInputStream( new FileInputStream("x.ser")).readObject();
                System.out.println(" Object = " + object );
                System.out.println(" Otro   = " + otro );
                 
               
                System.out.println( "Verdad que son los mismos " + ( object.equals( otro )) );
        }
}  

Lo mágico de esto, es que aunque los métodos sean privados, el mecanismo de serialización puede invocarlos. Son especiales.