java: "final" System.out, System.in y System.err?

80

System.outse declara como public static final PrintStream out.

Pero puedes llamar System.setOut()para reasignarlo.

¿Eh? ¿Cómo es esto posible si lo es final?

(el mismo punto se aplica a System.iny System.err)

Y lo que es más importante, si puede mutar los campos finales estáticos públicos, ¿qué significa esto en cuanto a las garantías (si las hay) que finalle brinda? (Nunca me di cuenta ni esperaba que System.in/out/err se comportara como finalvariables)

Jason S
fuente
3
Los campos finales no disfrutan de muchos beneficios por parte de la propia JVM, aunque son estrictamente controlados por el verificador. También hay formas de modificar los campos finales, pero no a través del código estándar de Java (ya que es un tema del verificador). Se hace a través de Unsafe y se expone en java a través de Field.set (necesita ser accesible), que se compila con las cosas inseguras mencionadas. También JNI puede hacerlo, por lo tanto, la JVM no está tan interesada en intentar optimizar ... {quizás debería haber estructurado el comentario como una respuesta, pero meh}
bestsss

Respuestas:

57

Campos protegidos contra escritura de JLS 17.5.4 :

Normalmente, los campos estáticos finales no se pueden modificar. Sin embargo System.in, System.outy System.errson campos estáticos finales que, por razones heredadas, los métodos deben permitir que los modifiquen System.setIn, System.setOuty System.setErr. Nos referimos a estos campos como protegidos contra escritura para distinguirlos de los campos finales ordinarios.

El compilador debe tratar estos campos de manera diferente a otros campos finales. Por ejemplo, una lectura de un campo final ordinario es "inmune" a la sincronización: la barrera involucrada en un bloqueo o lectura volátil no tiene por qué afectar el valor que se lee de un campo final. Dado que el valor de los campos protegidos contra escritura puede cambiar, los eventos de sincronización deberían tener un efecto sobre ellos. Por lo tanto, la semántica dicta que estos campos se traten como campos normales que no se pueden cambiar por el código de usuario, a menos que ese código de usuario esté en la Systemclase.

Por cierto, en realidad puede mutar finalcampos a través de la reflexión llamándolos setAccessible(true)(o usando Unsafemétodos). Tales técnicas se utilizan durante la deserialización, por Hibernate y otros marcos, etc., pero tienen una limitación: el código que ha visto el valor del campo final antes de la modificación no está garantizado para ver el nuevo valor después de la modificación. Lo especial de los campos en cuestión es que están libres de esta limitación, ya que el compilador los trata de manera especial.

axtavt
fuente
4
¡Que el FSM bendiga el código heredado por la hermosa forma en que compromete el diseño futuro!
Trineo
1
>> Esta técnica se usa durante la deserialización << esto no es cierto ahora, la deserelización usa Inseguro (más rápido)
bestsss
1
Es importante comprender que setAccessible(true)solo funciona para los que no son staticcampos, lo que lo hace adecuado para la tarea de ayudar a la deserialización o clonación del código, pero no hay forma de cambiar los static finalcampos. Es por eso que el texto citado comienza con “ Normalmente, los campos estáticos finales no se pueden modificar ”, refiriéndose a la final staticnaturaleza de estos campos y las tres excepciones. El caso de los campos de instancia se analiza en otro lugar.
Holger
Me pregunto por qué no quitaron simplemente el finalmodificador; parece más simple que todas estas cosas "protegidas contra escritura". Estoy bastante seguro de que no es un cambio rotundo.
Mark VY
@Holger Existe una forma de cambiar los campos finales estáticos (hasta Java 8). clase pública OnePrinter {Entero final estático privado UNO = 1; public static void printOne () {System.out.println (ONE); }} y luego puede Field z = OnePrinter.class.getDeclaredField ("ONE"); z.setAccessible (verdadero); Field f = Field.class.getDeclaredField ("modificadores"); int modificadores = z.getModifiers (); f.setAccessible (verdadero); f.set (z, modificadores & ~ Modificador.FINAL); z.set (nulo, 2); OnePrinter.printOne ();
Peter Verhas
30

Java utiliza un método nativo para poner en práctica setIn(), setOut()y setErr().

En mi JDK1.6.0_20, se setOut()ve así:

public static void setOut(PrintStream out) {
    checkIO();
    setOut0(out);
}

...

private static native void setOut0(PrintStream out);

Aún no puede reasignar finalvariables "normalmente" , e incluso en este caso, no está reasignando directamente el campo (es decir, aún no puede compilar " System.out = myOut"). Los métodos nativos permiten algunas cosas que simplemente no puede hacer en Java normal, lo que explica por qué existen restricciones con los métodos nativos, como el requisito de que se firme un subprograma para poder usar bibliotecas nativas.

Adam Batkin
fuente
1
Bien, entonces es una puerta trasera alrededor de la semántica pura de Java ... ¿podría responder la parte de la pregunta que agregué, es decir, si puede reasignar transmisiones, en finalrealidad tiene algún significado aquí?
Jason S
1
Probablemente sea final para que uno no pueda hacer algo como System.out = new SomeOtherImp (). Pero aún puede usar los establecedores usando un enfoque nativo como ve arriba.
Amir Raminfar
Supongo que en este caso las llamadas a los métodos nativos setIn0 y setOut0 realmente modificarán el valor de una variable final, los métodos nativos probablemente pueden hacer eso ... Es como usar códigos de trampa en juegos: S
Danilo Tommasina
@Danilo, sí, modifica :)
bestsss
@Jason, la incapacidad de configurarlo directamente requiere controles de seguridad al llamar a setIn / setErr. todo justo y cuadrado. Ejemplo donde java modifica los campos finales: java.util.Random (semilla de campo)
bestsss
7

Para ampliar lo que dijo Adam, aquí está el impl:

public static void setOut(PrintStream out) {
    checkIO();
    setOut0(out);
}

y setOut0 se define como:

private static native void setOut0(PrintStream out);
Amir Raminfar
fuente
6

Depende de la implementación. Es posible que el último nunca cambie, pero podría ser un proxy / adaptador / decorador para el flujo de salida real, setOut podría, por ejemplo, establecer un miembro en el que el miembro de salida realmente escribe. En la práctica, sin embargo, se configura de forma nativa.

vickirk
fuente
1

el outque se declara como final en la clase System es una variable de nivel de clase. donde como el que está en el método siguiente es una variable local. no estamos pasando el nivel de la clase, que en realidad es el último en este método

public static void setOut(PrintStream out) {
  checkIO();
  setOut0(out);
    }

El uso del método anterior es el siguiente:

System.setOut(new PrintStream(new FileOutputStream("somefile.txt")));

ahora los datos se desviarán al archivo. Espero que esta explicación tenga sentido.

Entonces, no hay papel de los métodos nativos o reflexiones aquí para cambiar el propósito de la palabra clave final.

Krish Krishna
fuente
1
setOut0 está modificando la variable de clase, que es final.
fgb
0

En cuanto a cómo, podemos echar un vistazo al código fuente para java/lang/System.c:

/*
 * The following three functions implement setter methods for
 * java.lang.System.{in, out, err}. They are natively implemented
 * because they violate the semantics of the language (i.e. set final
 * variable).
 */
JNIEXPORT void JNICALL
Java_java_lang_System_setOut0(JNIEnv *env, jclass cla, jobject stream)
{
    jfieldID fid =
        (*env)->GetStaticFieldID(env,cla,"out","Ljava/io/PrintStream;");
    if (fid == 0)
        return;
    (*env)->SetStaticObjectField(env,cla,fid,stream);
}

...

En otras palabras, JNI puede "hacer trampa". ; )

Radiodef
fuente
-2

Creo que setout0está modificando la variable de nivel local out, no puede modificar la variable de nivel de clase out.

坂 田 鈴木
fuente