¿Los desarrolladores de Java abandonaron conscientemente RAII?

77

Como programador de C # desde hace mucho tiempo, recientemente he aprendido más sobre las ventajas de La adquisición de recursos es la inicialización (RAII). En particular, he descubierto que el lenguaje C #:

using (var dbConn = new DbConnection(connStr)) {
    // do stuff with dbConn
}

tiene el equivalente de C ++:

{
    DbConnection dbConn(connStr);
    // do stuff with dbConn
}

significa que recordar en el cierre de la utilización de recursos como DbConnection en un bloque using no es necesario en C ++. Esto parece ser una gran ventaja de C ++. Esto es aún más convincente cuando se considera una clase que tiene un miembro de instancia de tipo DbConnection , por ejemplo,

class Foo {
    DbConnection dbConn;

    // ...
}

En C # necesitaría que Foo implementara IDisposable como tal:

class Foo : IDisposable {
    DbConnection dbConn;

    public void Dispose()
    {       
        dbConn.Dispose();
    }
}

y lo que es peor, todos los usuarios de Foo deberían recordar incluir Foo en un bloque using , como:

   using (var foo = new Foo()) {
       // do stuff with "foo"
   }

Ahora, mirando C # y sus raíces de Java, me pregunto ... ¿los desarrolladores de Java apreciaron completamente lo que estaban abandonando cuando abandonaron la pila en favor del montón, abandonando así RAII?

(De manera similar, ¿ Stroustrup aprecia completamente la importancia de RAII?)

    
pregunta JoelFan 07.11.2011 - 15:45

11 respuestas

35
  

Ahora, mirando C # y sus raíces de Java, me pregunto ... ¿los desarrolladores de Java apreciaron completamente lo que estaban abandonando cuando abandonaron la pila en favor del montón, abandonando así RAII?

     

(Del mismo modo, ¿Stroustrup apreciaba completamente el significado de RAII?)

Estoy bastante seguro de que Gosling no entendió el significado de RAII en el momento en que diseñó Java. En sus entrevistas, a menudo habló sobre las razones para dejar de lado los genéricos y la sobrecarga de operadores, pero nunca mencionó los destructores deterministas y RAII.

Bastante divertido, incluso Stroustrup no era consciente de la importancia de los destructores deterministas en el momento en que los diseñó. No puedo encontrar la cita, pero si está realmente interesado, puede encontrarla en sus entrevistas aquí: enlace

    
respondido por el Nemanja Trifunovic 07.11.2011 - 16:41
56

Sí, los diseñadores de C # (y, estoy seguro, Java) decidieron específicamente contra la finalización determinista. Le pregunté a Anders Hejlsberg acerca de esto varias veces alrededor de 1999-2002.

Primero, la idea de diferentes semánticas para un objeto en función de si su pila o pila se basa en el objetivo de diseño unificador de ambos lenguajes, que fue para liberar a los programadores de tales problemas.

En segundo lugar, incluso si reconoce que hay ventajas, existen importantes complejidades de implementación e ineficiencias en el mantenimiento de la contabilidad. No puede realmente colocar objetos apilados en la pila en un idioma administrado. Se queda con decir "semántica similar a una pila" y comprometerse con un trabajo significativo (los tipos de valor ya son bastante difíciles, piense en un objeto que es una instancia de una clase compleja, con referencias que entran y regresan a la memoria administrada).

Por eso, no quieres una finalización determinista en cada objeto en un sistema de programación donde "(casi) todo es un objeto". Entonces, do tiene que introducir algún tipo de sintaxis controlada por programador para separar un objeto normalmente rastreado de uno que tenga finalización determinista.

En C #, tiene la palabra clave using , que llegó bastante tarde en el diseño de lo que se convirtió en C # 1.0. La cosa IDisposable está bastante mal, y uno se pregunta si sería más elegante que using funcione con la sintaxis del destructor de C ++ ~ marcando aquellas clases a las que se podría aplicar automáticamente el patrón boiler-plate IDisposable ?

    
respondido por el Larry OBrien 07.11.2011 - 19:43
35

Tenga en cuenta que Java se desarrolló en 1991-1995 cuando C ++ era un lenguaje muy diferente. Las excepciones (que hicieron que RAII fuera necesario ) y las plantillas (que facilitaron la implementación de punteros inteligentes) eran características "nuevas". La mayoría de los programadores de C ++ provenían de C y estaban acostumbrados a hacer la administración manual de la memoria.

Entonces, dudo que los desarrolladores de Java deliberadamente decidieran abandonar RAII. Sin embargo, fue una decisión deliberada para que Java prefiriera la semántica de referencia en lugar de la semántica de valor. La destrucción determinista es difícil de implementar en un lenguaje de referencia semántica.

Entonces, ¿por qué usar semántica de referencia en lugar de semántica de valor?

Porque hace que el lenguaje sea mucho más simple

  • No hay necesidad de una distinción sintáctica entre Foo y Foo* o entre foo.bar y foo->bar .
  • No hay necesidad de una asignación sobrecargada, cuando toda la asignación es copiar un puntero.
  • No hay necesidad de copiar constructores. (En ocasiones, es necesaria una función de copia explícita como clone() . Muchos objetos simplemente no necesitan ser copiados. Por ejemplo, los inmutables no lo hacen).
  • No es necesario declarar private copy constructors y operator= para hacer que una clase no pueda copiarse. Si no desea que se copien los objetos de una clase, simplemente no escribe una función para copiarlos.
  • No hay necesidad de funciones swap . (A menos que estés escribiendo una rutina de clasificación).
  • No hay necesidad de referencias de valores de estilo C ++ 0x.
  • No hay necesidad de (N) RVO.
  • No hay problema de corte.
  • Es más fácil para el compilador determinar los diseños de objetos, porque las referencias tienen un tamaño fijo.

La desventaja principal de la semántica de referencia es que cuando cada objeto tiene potencialmente múltiples referencias, es difícil saber cuándo eliminarlo. Casi tienes para tener administración automática de memoria.

Java eligió usar un recolector de basura no determinista.

¿No puede GC ser determinista?

Sí, puede. Por ejemplo, la implementación en C de Python utiliza el conteo de referencias. Y luego se agregó el rastreo GC para manejar la basura cíclica donde fallan las refcounts.

Pero el re-conteo es horriblemente ineficiente. Muchos ciclos de CPU pasaron actualizando los conteos. Peor aún en un entorno de subprocesos múltiples (como el tipo para el que se diseñó Java) donde esas actualizaciones necesitan sincronizarse. Mucho mejor usar el recolector de basura nulo hasta que necesite cambiar a otro.

Se podría decir que Java eligió optimizar el caso común (memoria) a expensas de recursos no fungibles como archivos y sockets. Hoy, a la luz de la adopción de RAII en C ++, esto puede parecer la elección equivocada. Pero recuerde que gran parte de la audiencia objetivo para Java eran programadores C (o "C con clases") que estaban acostumbrados a cerrar estas cosas explícitamente.

Pero ¿qué pasa con los "objetos de pila" de C ++ / CLI?

Son solo azúcar sintáctica para Dispose , al igual que C # using . Sin embargo, no resuelve el problema general de la destrucción determinista, ya que puede crear un gcnew FileStream("filename.ext") anónimo y C ++ / CLI no lo eliminará automáticamente.

    
respondido por el dan04 08.11.2011 - 06:51
19

Java7 introdujo algo similar al C # using : La declaración try-with-resources

  

una declaración try que declara uno o más recursos. Un recurso es como un objeto que debe cerrarse una vez que el programa haya terminado con él. La declaración try -with-resources asegura que cada recurso se cierre al final de la declaración. Cualquier objeto que implemente java.lang.AutoCloseable , que incluye todos los objetos que implementan java.io.Closeable , puede usarse como recurso ...

Entonces, supongo que o bien no eligieron conscientemente no implementar RAII o cambiaron de opinión mientras tanto.

    
respondido por el Patrick 07.11.2011 - 16:52
17

Su descripción de los orificios de using está incompleta. Considere el siguiente problema:

interface Bar {
    ...
}
class Foo : Bar, IDisposable {
    ...
}

Bar b = new Foo();

// Where's the Dispose?

En mi opinión, no tener tanto RAII como GC fue una mala idea. Cuando se trata de cerrar archivos en Java, es malloc() y free() allí.

    
respondido por el DeadMG 07.11.2011 - 16:43
17

Java intencionalmente no tiene objetos basados en la pila (también conocidos como objetos de valor). Estos son necesarios para que el objeto se destruya automáticamente al final del método.

Debido a esto y al hecho de que Java se recolecta como basura, la finalización determinista es más o menos imposible (ej. ¿Qué pasaría si mi objeto "local" fuera referenciado en otra parte? Entonces, cuando el método termina, no quiero que sea destruido) .

Sin embargo, esto está bien para la mayoría de nosotros, porque casi nunca hay necesidad para una finalización determinista, ¡excepto cuando se interactúa con recursos nativos (C ++)!

¿Por qué Java no tiene objetos basados en la pila?

(Aparte de los primitivos ..)

Porque los objetos basados en la pila tienen una semántica diferente a las referencias basadas en el montón. Imagina el siguiente código en C ++; ¿Qué hace?

return myObject;
  • Si myObject es un objeto basado en la pila local, se llama al constructor de copia (si el resultado está asignado a algo).
  • Si myObject es un objeto basado en la pila local y estamos devolviendo una referencia, el resultado no está definido.
  • Si myObject es un miembro / objeto global, se llama al constructor de copia (si el resultado está asignado a algo).
  • Si myObject es un miembro / objeto global y estamos devolviendo una referencia, se devuelve la referencia.
  • Si myObject es un puntero a un objeto basado en la pila local, el resultado no está definido.
  • Si myObject es un puntero a un miembro / objeto global, se devuelve ese puntero.
  • Si myObject es un puntero a un objeto basado en el montón, se devuelve ese puntero.

Ahora, ¿qué hace el mismo código en Java?

return myObject;
  • Se devuelve la referencia a myObject . No importa si la variable es local, miembro o global; y no hay objetos basados en pila o casos de punteros de los que preocuparse.

Lo anterior muestra por qué los objetos basados en pilas son una causa muy común de errores de programación en C ++. Por eso, los diseñadores de Java los sacaron; y sin ellos, no tiene sentido usar RAII en Java.

    
respondido por el BlueRaja - Danny Pflughoeft 07.11.2011 - 21:50
14

Soy bastante viejo. He estado allí y lo he visto y me golpeé la cabeza muchas veces.

Estuve en una conferencia en Hursley Park donde los chicos de IBM nos contaron lo maravilloso que era este nuevo lenguaje Java, solo alguien preguntó ... ¿por qué no hay un destructor para estos objetos? No quiso decir lo que conocemos como destructor en C ++, pero tampoco había finalista (o tenía finalizadores pero básicamente no funcionaron). Esto fue hace mucho, y decidimos que Java era un poco como un lenguaje de juguete en ese momento.

ahora agregaron Finalizadores a la especificación de lenguaje y Java vio cierta adopción.

Por supuesto, luego se les dijo a todos que no pusieran finalizadores en sus objetos porque esto ralentizó tremendamente el GC. (ya que no solo tenía que bloquear el montón, sino también mover los objetos que se iban a finalizar a un área temporal, ya que estos métodos no podían llamarse, ya que el GC había detenido la ejecución de la aplicación. En cambio, serían llamados inmediatamente antes del siguiente Ciclo GC) (y, lo que es peor, a veces el finalizador nunca se llamaría cuando la aplicación se estaba cerrando. Imagínate que nunca se haya cerrado el identificador de archivo)

Luego tuvimos C #, y recuerdo el foro de discusión en MSDN donde se nos dijo lo maravilloso que era este nuevo lenguaje de C #. Alguien preguntó por qué no hubo una finalización determinista y los chicos de MS nos dijeron que no necesitábamos tales cosas, luego nos dijeron que necesitábamos cambiar nuestra forma de diseñar aplicaciones, luego nos contaron lo increíble que era GC y cómo eran todas nuestras aplicaciones antiguas. Basura y nunca funcionó por todas las referencias circulares. Luego cedieron a la presión y nos dijeron que habían agregado este patrón de propósito de uso a la especificación que podríamos usar. Pensé que en ese momento ya habíamos vuelto a la gestión manual de la memoria en las aplicaciones de C #.

Por supuesto, los chicos de MS más tarde descubrieron que todo lo que nos habían dicho era ... bueno, hicieron que el Propósito fuera un poco más que una simple interfaz estándar, y luego agregaron la declaración de uso. W00t! Se dieron cuenta de que la finalización determinista era algo que faltaba en el lenguaje, después de todo. Por supuesto, aún debes recordar ponerlo en todas partes, por lo que es un poco manual, pero es mejor.

Entonces, ¿por qué lo hicieron cuando pudieron haber colocado semánticas de estilo de uso automáticamente en cada bloque de alcance desde el principio? Probablemente eficiencia, pero me gusta pensar que simplemente no se dieron cuenta. Al igual que al final se dieron cuenta de que todavía necesita punteros inteligentes en .NET (google SafeHandle) pensaron que el GC realmente resolvería todos los problemas. Olvidaron que un objeto es más que solo memoria y que GC está diseñado principalmente para manejar la administración de memoria. quedaron atrapados en la idea de que el GC manejaría esto, y olvidaron que pusiste otras cosas allí, un objeto no es solo un bulto de memoria que no importa si no lo eliminas por un tiempo.

Pero también creo que la falta de un método de finalización en el Java original tenía algo más que eso: que los objetos que creaste se referían a la memoria, y si querías eliminar otra cosa (como un controlador de base de datos o un socket o lo que sea), entonces se esperaba que lo hiciera manualmente .

Recuerde que Java se diseñó para entornos integrados donde las personas estaban acostumbradas a escribir código C con muchas asignaciones manuales, por lo que no tener libre automático no era un gran problema, nunca lo hizo antes, ¿por qué lo necesitaría? ¿Java? El problema no tenía nada que ver con subprocesos, o pila / montón, probablemente solo estaba allí para hacer que la asignación de memoria (y por lo tanto la desasignación) fuera un poco más fácil. En general, la declaración try / finally es probablemente un mejor lugar para manejar recursos sin memoria.

Así que, en mi humilde opinión, la forma en que .NET simplemente copió el mayor defecto de Java es su mayor debilidad. .NET debería haber sido un mejor C ++, no un mejor Java.

    
respondido por el gbjbaanb 20.03.2012 - 17:36
11

Bruce Eckel, autor de "Thinking in Java" y "Thinking in C ++" y miembro del Comité de Normas de C ++, opina que, en muchas áreas (no solo RAII), Gosling y el equipo de Java no hizo su tarea.

  

... Para comprender cómo el lenguaje puede ser tanto desagradable como complicado y, al mismo tiempo, bien diseñado, debe tener en cuenta la decisión de diseño principal de la que se basó todo en C ++: la compatibilidad con C. Stroustrup decidió: y de manera correcta, parecería que la manera de hacer que las masas de programadores de C se muevan a los objetos es hacer que el movimiento sea transparente: permitirles compilar su código de C sin cambios en C ++. Esta fue una gran limitación, y siempre ha sido la mayor fortaleza de C ++ ... y su perdición. Es lo que hizo a C ++ tan exitoso como fue, y tan complejo como es.

     

También engañó a los diseñadores de Java que no entendían C ++ lo suficientemente bien. Por ejemplo, pensaron que la sobrecarga del operador era demasiado difícil de usar para los programadores. Esto es básicamente cierto en C ++, ya que C ++ tiene asignación de pila y asignación de pila y debe sobrecargar a sus operadores para manejar todas las situaciones y no causar pérdidas de memoria. Difícil por cierto. Java, sin embargo, tiene un único mecanismo de asignación de almacenamiento y un recolector de basura, lo que hace que la sobrecarga del operador sea trivial, como se mostró en C # (pero ya se había mostrado en Python, que es anterior a Java). Pero durante muchos años, la línea en parte del equipo de Java fue "La sobrecarga del operador es demasiado complicada". Esta y muchas otras decisiones en las que alguien claramente no hizo su tarea es la razón por la que tengo una reputación de despreciar muchas de las elecciones hechas por Gosling y el equipo de Java.

     

Hay muchos otros ejemplos. Los primitivos "tenían que ser incluidos para la eficiencia". La respuesta correcta es mantenerse fiel a "todo es un objeto" y proporcionar una trampilla para realizar actividades de nivel inferior cuando se requería eficiencia (esto también habría permitido que las tecnologías de punto de acceso hicieran las cosas de manera transparente más eficientes, como eventualmente lo harían). tener). Ah, y el hecho de que no se puede usar el procesador de punto flotante directamente para calcular funciones trascendentales (en su lugar, se hace con software). He escrito sobre temas como este tanto como puedo soportar, y la respuesta que escucho siempre ha sido una respuesta tautológica que dice que "esta es la forma de Java".

     

Cuando escribí sobre lo mal que se diseñaron los genéricos, obtuve la misma respuesta, junto con "debemos ser compatibles con las decisiones anteriores (malas) tomadas en Java". Últimamente, más y más personas han adquirido suficiente experiencia con Genéricos para ver que realmente son muy difíciles de usar; de hecho, las plantillas de C ++ son mucho más potentes y consistentes (y mucho más fáciles de usar ahora que los mensajes de error del compilador son tolerables). Las personas incluso han estado tomando la reificación en serio, algo que sería útil pero que no hará mella en un diseño que está paralizado por restricciones autoimpuestas.

     

La lista continúa hasta el punto en que es tedioso ...

    
respondido por el Gnawme 07.11.2011 - 20:00
10

La mejor razón es mucho más simple que la mayoría de las respuestas aquí.

No puedes pasar objetos asignados a otras hebras.

Detente y piensa en eso. Sigue pensando ... Ahora C ++ no tenía hilos cuando todos estaban tan interesados en RAII. Incluso Erlang (montones separados por hilo) se complica cuando pasas demasiados objetos. C ++ solo obtuvo un modelo de memoria en C ++ 2011; ahora casi puede razonar sobre la concurrencia en C ++ sin tener que consultar la "documentación" de su compilador.

Java se diseñó desde (casi) el primer día para varios subprocesos.

Todavía tengo mi copia antigua de "El lenguaje de programación C ++" donde Stroustrup me asegura que no necesitaré hilos.

La segunda razón dolorosa es evitar el corte.

    
respondido por el Tim Williscroft 07.11.2011 - 22:25
5

En C ++, se usan funciones de lenguaje de nivel inferior y de propósito más general (los destructores se invocan automáticamente en objetos basados en la pila) para implementar uno de mayor nivel (RAII), y este enfoque es algo que la gente de C # / Java parece No ser demasiado aficionado. Prefieren diseñar herramientas específicas de alto nivel para necesidades específicas y proporcionarlas a los programadores listos para usar, integrados en el lenguaje. El problema con estas herramientas específicas es que a menudo son imposibles de personalizar (en parte, eso es lo que las hace tan fáciles de aprender). Cuando se construye a partir de bloques más pequeños, una mejor solución puede venir con el tiempo, mientras que si solo tiene construcciones integradas de alto nivel, esto es menos probable.

Así que sí, creo que (no estaba realmente allí ...) fue una decisión conciente, con el objetivo de facilitar el aprendizaje de los idiomas, pero en mi opinión, fue una mala decisión. Por otra parte, en general prefiero la filosofía de C ++ para dar a los programadores la oportunidad de rodar, así que estoy un poco sesgado.

    
respondido por el imre 07.11.2011 - 17:25
-1

Ya mencionaste el equivalente aproximado a esto en C # con el método Dispose . Java también tiene finalize . NOTA: Me doy cuenta de que la finalización de Java no es determinista y es diferente de Dispose . Solo estoy señalando que ambos tienen un método de limpieza de recursos junto con el GC.

Si algo C ++ se vuelve más doloroso porque un objeto tiene que ser destruido físicamente. En lenguajes de nivel superior como C # y Java, dependemos de un recolector de basura para limpiarlo cuando ya no hay referencias a él. No existe tal garantía de que el objeto DBConnection en C ++ no tenga referencias o indicadores no autorizados.

Sí, el código C ++ puede ser más intuitivo de leer pero puede ser una pesadilla para depurar porque los límites y limitaciones que los lenguajes como Java implementan descartan algunos de los errores más agravantes y difíciles, además de proteger a otros desarrolladores de los comunes. Errores de novato.

Tal vez se trate de preferencias, algunas como el poder, el control y la pureza de bajo nivel de C ++, donde otras personas como yo prefieren un lenguaje más aislado que mucho más explícito.

    
respondido por el maple_shaft 07.11.2011 - 16:06

Lea otras preguntas en las etiquetas