Java中的“final” System.out,System.in和System.err是什么?

81

System.out 被声明为 public static final PrintStream out

但是你可以调用System.setOut() 来重新分配它。

如果它是 final,这怎么可能呢?

(同样适用于 System.inSystem.err)

更重要的是,如果你可以改变公共静态 final 字段,那么在关于 final 给你的保证方面会有什么含义呢? (我从没意识到 System.in/out/err 表现得像是 final 变量)


3
JVM本身并不会为final字段提供太多的好处,但它们会被验证器严格检查。虽然存在一些修改final字段的方法,但不能通过标准的Java代码来执行(因为这是验证器的主题)。它可以通过Unsafe进行操作,并通过Field.set(需要accessible true)在Java中公开,编译成上述的Unsafe内容。 JNI也可以实现此操作,因此JVM不太热衷于尝试优化... {也许我应该将注释结构化为答案,但是算了} - bestsss
7个回答

57

JLS 17.5.4 写保护字段

通常,final static字段不能被修改。但是,由于历史原因,System.inSystem.outSystem.err是final static字段,必须允许通过System.setInSystem.setOutSystem.setErr方法进行更改。我们将这些字段称为“写保护”,以区别于普通的final字段。

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

顺便说一下,实际上你可以通过反射调用setAccessible(true)(或使用Unsafe方法)来改变final字段。这些技术在反序列化、Hibernate和其他框架等过程中使用,但它们有一个限制:在修改前看到final字段的值的代码不一定会在修改后看到新的值。这些特定字段的特殊之处在于它们被编译器以特殊方式处理,没有这个限制。


4
愿飞天面条怪物保佑遗留代码,因为它妥协了未来设计,让它变得更加可爱! - Sled
1
这种技术在反序列化期间使用。但现在已经不是了,反序列化使用Unsafe(更快)。 - bestsss
1
重要的是要明白setAccessible(true)只适用于非静态字段,这使得它适用于帮助反序列化或克隆代码的任务,但是没有办法更改静态常量。这就是为什么引用文本以“通常情况下,不能修改final static字段”开头,指的是这些字段的final static特性和三个例外情况。实例字段的情况在另一个地方进行了讨论。 - Holger
@MarkVY 我认为当前的要求是将其保持为 final 并向新用户指示应该将其视为不可修改。只有“允许它们被修改”的部分是遗留的 - set 方法是遗留的。 - Teddy
遗留代码?它们提供了重要的功能。我认为这些字段上的 "final" 可以说是遗留的,虽然这样安全管理器至少可以拦截调用。也许它们应该是私有的,我们应该编写 System.out().println - Mark VY
显示剩余3条评论

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中无法实现的操作,这解释了为什么有关本地方法的限制,例如要求小程序签名才能使用本地库。


1
好的,这是对纯Java语义的一个后门...你能回答我添加的问题部分吗?也就是说,如果您可以重新分配流,那么final在这里实际上是否有任何意义? - Jason S
1
这可能是最终的,因此不能像System.out = new SomeOtherImp()那样做某些事情。但是您仍然可以使用本机方法使用上面所示的设置器。 - Amir Raminfar
我猜在这种情况下,对本地setIn0和setOut0方法的调用将真正修改最终变量的值,本地方法可能可以做到这一点...这就像在游戏中使用作弊码:S - Danilo Tommasina
@Danilo,是的,它确实进行了修改 :) - bestsss
@Jason,无法直接设置它需要在调用setIn/setErr时进行安全检查,这是公平合理的。Java修改final字段的示例:java.util.Random(字段seed)。 - bestsss

6
为了进一步解释Adam所说的内容,这里是实现代码:

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

setOut0被定义为:

private static native void setOut0(PrintStream out);

5

这取决于具体实现。最终的输出流可能永远不会改变,但它可以是实际输出流的代理/适配器/装饰器。例如,setOut可以设置一个成员变量,out成员实际上写入该变量。然而,在实践中,它通常是以本地方式设置。


2

System类中声明为finalout是一个类级别变量。 而下面方法中的out是一个局部变量。 我们没有将类级别的out(实际上是final的)传递到这个方法中。

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

以上方法的使用如下:

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

现在数据将被重定向到文件中。希望这个解释有意义。

因此,在更改final关键字的用途方面,原生方法或反射没有作用。


2
setOut0正在修改类变量,该变量是final的。 - fgb
根据答案中所述,@fgb在setOut0中不是一个类变量,而是setOut的输入参数,并且属于局部作用域。 - sankar
@fgb 正如答案所述,setOut0 中的 out 不是类变量,而是 setOut 的输入参数,因此属于局部范围。 - sankar
收到了,@fgb。System.setOut(PrintStream out)被从其他几个地方调用,传递的是System.out。 - sankar

1

关于如何实现,我们可以查看源代码中的 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可以“作弊”。 ; )

不确定为什么这个答案没有得到足够的投票。谢谢。 - sankar

-1

我认为setout0正在修改局部变量out,它无法修改类级别的变量out


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接