java:“最终” System.out,System.in和System.err?


80

System.out声明为public static final PrintStream out

但是您可以致电System.setOut()重新分配它。

??如果是这样怎么可能final

System.in和适用于System.err

更重要的是,如果您可以对public static final字段进行突变,那么就可以为final您提供的保证(如果有)意味着什么?(我从未意识到,也没想到System.in/out/err表现为final变量)


3
尽管最终字段由验证程序严格检查,但JVM本身并没有带来很多好处。还有一些方法可以修改最终字段,但不能通过标准的Java代码来修改(因为这是验证程序的主题)。它是通过Unsafe完成的,并通过Field.set(需要可访问的true)在Java中公开,并编译为上述Unsafe内容。JNI也可以做到这一点,因此JVM并不热衷于尝试优化……{也许我应该将注释构造为答案,但是
……

Answers:


57

JLS 17.5.4写保护字段

通常,最终的静态字段可能不会被修改。然而System.inSystem.outSystem.err是,由于遗留原因,必须允许通过方法来改变最终的静态字段System.setInSystem.setOutSystem.setErr。我们称这些字段为写保护的,以区别于普通的最终字段。

编译器需要将这些字段与其他最终字段区别对待。例如,读取普通的最终字段对同步是“免疫”的:锁定或易失性读取中涉及的屏障不必影响从最终字段读取的值。由于可以看到写保护字段的值发生了变化,因此同步事件应该对其产生影响。因此,语义要求将这些字段视为不能由用户代码更改的普通字段,除非该用户代码在System类中。

顺便说一句,实际上,您可以final通过调用setAccessible(true)(或使用Unsafe方法)通过反射来使字段发生变化。Hibernate和其他框架等在反序列化过程中使用了此类技术,但它们有一个局限性:修改前已看到final字段值的代码不能保证在修改后会看到新值。有问题的字段的特殊之处在于它们不受此限制,因为编译器以特殊方式对待它们。


4
愿FSM能够以一种巧妙的方式来保护旧版代码,从而危及未来的设计!
雪橇

1
>>此技术用于反序列化<<现在不正确,反序列化使用不安全(更快)
最好的做法2011年

1
重要的是要了解它setAccessible(true)仅适用于非static字段,这使其适合于帮助反序列化或克隆代码的任务,但是无法更改static final字段。这就是为什么引用的文本以“通常,不得修改最终静态字段”开头的原因,指的final static是这些字段的性质和三个例外。实例字段的情况在另一个地方讨论。
Holger

我想知道为什么他们不简单地final摘除修饰符。似乎比所有这些“写保护”的东西都简单。我很确定这不是一个重大变化。
Mark VY

@Holger有一种方法可以更改静态的final字段(最多Java 8)。公共类OnePrinter {私有静态最终整数ONE = 1;公共静态无效printOne(){System.out.println(ONE); 然后,您可以将字段z = OnePrinter.class.getDeclaredField(“ ONE”); z.setAccessible(true); 字段f = Field.class.getDeclaredField(“ modifiers”); int修饰符= z.getModifiers(); f.setAccessible(true); f.set(z,modifiers〜〜Modifier.FINAL); z.set(null,2); OnePrinter.printOne();
彼得·韦尔哈斯

30

Java使用一个本地方法来实现setIn()setOut()setErr()

在我的JDK1.6.0_20上,setOut()如下所示:

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

...

private static native void setOut0(PrintStream out);

您仍然不能“正常”重新分配final变量,即使在这种情况下,也不能直接重新分配字段(即,您仍然无法编译“ System.out = myOut”)。本机方法允许执行某些您在常规Java中根本无法做到的事情,这说明了本机方法为何存在限制,例如要求使用Applet签名才能使用本机库。


1
好的,这是纯Java语义的后门...您能回答我添加的部分问题吗,即如果您可以重新分配流,那么final这里实际上有什么意义吗?
詹森·S

1
这可能是最终决定,因此无法执行类似System.out = new SomeOtherImp()的操作。但是您仍然可以像上面看到的那样使用本机方法使用setter。
2011年

我猜在这种情况下,对本机setIn0和setOut0方法的调用实际上会修改最终变量的值,本机方法可能可以做到这一点...就像在游戏中使用作弊代码:S
Danilo Tommasina

@Danilo,是的,它确实会进行修改:)
bestsss 2011年

@Jason,无法直接设置它需要在调用setIn / setErr时进行安全检查。一切公平而公正。Java确实修改了最终字段的示例:java.util.Random(字段种子)
bestsss 2011年

7

继续亚当所说的,这是暗示:

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

setOut0定义为:

private static native void setOut0(PrintStream out);

6

取决于实现。最后一个可能永远不会改变,但是它可能是实际输出流的代理/适配器/装饰器,例如setOut可以设置out成员实际写入的成员。但是实际上,它是本地设置的。


1

out其被声明为最终在系统类是一类电平变量中。下面的方法中的哪一个是局部变量。我们没有地方将类级别传递出去,这实际上是该方法的最后一个级别

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

以上方法的用法如下:

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

现在,数据将被转移到文件中。希望这种解释是有道理的。

因此,在改变final关键字的目的方面,本机方法或反射不起作用。


1
setOut0正在修改类变量,这是最终的。
fgb 2014年

0

至于如何,我们可以看一下以下源代码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);
}

...

换句话说,JNI可以“作弊”。; )


-2

我认为setout0是在修改局部级别变量out,它不能修改类级别变量out

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.