原创/朱季谦

灵魂拷问,这位独秀同学,你会这道题吗?

 请说说,“System.out.println()”原理......


这应该是刚开始学习Java时用到最多一段代码,迄今为止,与它算是老朋友了。既然是老朋友,就应该多去深入了解下其“内心”深处的“真正想法”。

在深入了解之前,先给自己提几个问题:

System是什么?out是什么?println又是什么?三个代码组成为何能实现打印信息的功能?

接下来,我们就带着问题,去熟悉我们这位相处已久的老伙计。

先从System开始一步一步探究。

在百度百科上,有对System做了这样的说明:System类代表系统,其中系统级的很多属性和控制方法都放置在该类的内部。

简而意之,该类与系统有关,可获取系统内部的众多属性以及方法,其部分源码如下:

 1 public final class System {
2 private static native void registerNatives();
3 static {
4 registerNatives();
5 }
6 private System() {
7 }
8 public final static InputStream in = null;
9 public final static PrintStream out = null;
10 public final static PrintStream err = null;
11 private static volatile SecurityManager security = null;
12 public static void setIn(InputStream in) {
13 checkIO();
14 setIn0(in);
15 }
16 public static void setOut(PrintStream out) {
17 checkIO();
18 setOut0(out);
19 }
20 ......
21 }

打开源码,发现这是一个final定义的类,其次,该类的构造器是以private权限进行定义的。根据这两情况可以说明,该类即不能被继承也无法实例化成对象,同时需注意一点,就是这个类里定义的很多变量和方法都是static来定义的,即这些类成员都是属于类而非对象。

因此,若需调用类中的这些带static定义的属性或者方法,无需创建对象就能直接通过“类名.成员名”来调用。

在System源码中,需要留意的是in,out,or三者,它们分别代表标准输入流,标准输出流,标准错误输出流。

到这一步,便可以逐渐看到System.out.println中的影子,没错,这行代码里的System.out,即为引用System类里静态成员out,它是PrintStream类型的引用变量,称为"字节输出流"。作为static定义的out引用变量,它在类加载时就被初始化了,初始化后,会创建PrintStream对象对out赋值,之后便能调用PrintStream类中定义的方法。

具体怎么创建PrintStream并赋值给静态成员out,我放在本文后面讲解。

接着,进入到PrintStream类当中——

 1 public class PrintStream extends FilterOutputStream
2 implements Appendable, Closeable
3 {
4 ......
5 public void println() {
6 newLine();
7 }
8
9 public void println(boolean x) {
10 synchronized (this) {
11 print(x);
12 newLine();
13 }
14 }
15
16 public void println(char x) {
17 synchronized (this) {
18 print(x);
19 newLine();
20 }
21 }
22
23 public void println(int x) {
24 synchronized (this) {
25 print(x);
26 newLine();
27 }
28 }
29
30 public void println(long x) {
31 synchronized (this) {
32 print(x);
33 newLine();
34 }
35 }
36
37 public void println(float x) {
38 synchronized (this) {
39 print(x);
40 newLine();
41 }
42 }
43
44 public void println(double x) {
45 synchronized (this) {
46 print(x);
47 newLine();
48 }
49 }
50
51 public void println(char x[]) {
52 synchronized (this) {
53 print(x);
54 newLine();
55 }
56 }
57
58 public void println(String x) {
59 synchronized (this) {
60 print(x);
61 newLine();
62 }
63 }
64
65 ......
66 }

发现这PrintStream里边存在诸多以println名字命名的重载方法。

这个,就是我们本文中最后需要回答的问题,即println是什么?

它其实是PrintStream打印输出流类里的方法。

每个有传参的println方法里,其最后调用的方法都是print()与newLine()。

值得注意一点,这些带有传参的println方法当中,里面都是通过同步synchronized来修饰,这说明System.out.println其实是线程安全的。同时还有一点需注意,在多线程情况下,当大量方法执行同一个println打印时,其synchronized同步性能效率都可能出现严重性能问题。因此,在实际生产上,普遍是用log.info()类似方式来打印日志而不会用到System.out.println。

在以上代码里,其中 newLine()是代表打印换行的意思。

众所周知,以System.out.println()来打印信息时,每条打印信息都会换行的,之所以会出现换行,其原理就是println()内部通过newLine()方法实现的。

若换成System.out.print()来打印,则不会出现换行情况。

为什么print()不会出现换行呢?

分析一下print()里代码便可得知,是因为其方法里并没有调用newLine()方法来实现换行的——

 1 public void print(boolean b) {
2 write(b ? "true" : "false");
3 }
4
5 public void print(char c) {
6 write(String.valueOf(c));
7 }
8
9 public void print(int i) {
10 write(String.valueOf(i));
11 }
12
13 public void print(long l) {
14 write(String.valueOf(l));
15 }
16
17 public void print(float f) {
18 write(String.valueOf(f));
19 }
20
21 public void print(double d) {
22 write(String.valueOf(d));
23 }
24
25 public void print(char s[]) {
26 write(s);
27 }
28
29
30 public void print(String s) {
31 if (s == null) {
32 s = "null";
33 }
34 write(s);
35 }

这些重载方法里面都调用相同的write()方法,值得注意的是,在调用write()时,部分方法的实现是都把参数转换成了String字符串类型,之后进入到write()方法详情里——

 1 private void write(String s) {
2 try {
3 synchronized (this) {
4 ensureOpen();
5 textOut.write(s);
6 textOut.flushBuffer();
7 charOut.flushBuffer();
8 if (autoFlush && (s.indexOf('\n') >= 0))
9 out.flush();
10 }
11 }
12 catch (InterruptedIOException x) {
13 Thread.currentThread().interrupt();
14 }
15 catch (IOException x) {
16 trouble = true;
17 }
18 }

其中,ensureOpen()的方法是判断out流是否已经开启,其详细方法如下:

1 private void ensureOpen() throws IOException {
2 if (out == null)
3 throw new IOException("Stream closed");
4 }

由方法可得知,在进行写入打印信息时,需判断PrintStream流是否已经开启,若没有开启,则无法将打印信息写入计算机,故而抛出说明流是关闭状态的异常提示:“Stream closed”

若流是开启的,即可执行 textOut.write(s);

根据个人理解,这里的textOut是BufferedWriter引用变量,即为常说的IO流里写入流,最终会将信息写入到控制台上,即我们平常说的控制台打印。可以理解成,控制台就是一个文件,但是能被我们实时看到里面是什么的文件,这样当每次写入东西时,就会实时呈现在文件里,也就是能被我们看到的控制台打印信息。

那么,问题来了,哪行代码是表示写入到控制台文件的呢?System、out、println又是如何组成到一起来起作用的?

让我们回到System类最开始的地方——

 1 public final class System {
2
3 /* register the natives via the static initializer.
4 *
5 * VM will invoke the initializeSystemClass method to complete
6 * the initialization for this class separated from clinit.
7 * Note that to use properties set by the VM, see the constraints
8 * described in the initializeSystemClass method.
9 */
10 private static native void registerNatives();
11 static {
12 registerNatives();
13 }
14
15 }

以上的静态代码会在类的初始化阶段被初始化,其会调用一个native方法registerNatives()。根据该方法的英文注释“VM will invoke the initializeSystemClass method to complete”,可知,VM将调用initializeSystemClass方法来完成该类初始化。

我们找到该initializeSystemClass方法,下面只列出本文需要用到的核心代码,稍微做了一下注释:

 1 private static void initializeSystemClass() {
2 //被vm执行系统属性初始化
3 props = new Properties();
4 initProperties(props);
5 sun.misc.VM.saveAndRemoveProperties(props);
6
7 //从系统属性中获取系统相关的换行符,赋值给变量lineSeparator
8 lineSeparator = props.getProperty("line.separator");
9 sun.misc.Version.init();
10 //分别创建in、out、err的实例对象,并通过set()方法初始化
11 FileInputStream fdIn = new FileInputStream(FileDescriptor.in);
12 FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
13 FileOutputStream fdErr = new FileOutputStream(FileDescriptor.err);
14 setIn0(new BufferedInputStream(fdIn));
15 setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));
16 setErr0(newPrintStream(fdErr, props.getProperty("sun.stderr.encoding")));
17
18 ......
19 }

主要关注这两行代码:

1  FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
2 setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));

一.这里逐行进行分析,首先FileDescriptor是一个“文件描述符”,可以通俗地把它当成一个文件,它有以下三个属性:

  1. in:标准输入(键盘)的描述符

  2. out:标准输出(屏幕)的描述符

  3. err:标准错误输出(屏幕)的描述符

FileDescriptor.out代表为“标准输出(屏幕)”,可以通俗地理解成标准输出到控制台的文件,即表示控制台。

new FileOutputStream(FileDescriptor.out)该行代码即说明通过文件输出流将信息输出到屏幕即控制台上。

若还是不理解,可举一个比较常见的例子——

1 public static void main(String[] args) throws IOException {
2 FileOutputStream out=new FileOutputStream("C:\\file.txt");
3 out.write(66);
4 }

这是比较简单的通过FileOutputStream输出流写入文件的写法,这里的路径“C:\file.txt”就与FileDescriptor.out做法类似,都是描述一个可写入数据的文件,只不过FileDescriptor.out比较特殊,它描述的是屏幕,即常说的控制台。

二.接下来是newPrintStream(fdOut, props.getProperty("sun.stdout.encoding"))——

1 private static PrintStream newPrintStream(FileOutputStream fos, String enc) {
2 if (enc != null) {
3 try {
4 return new PrintStream(new BufferedOutputStream(fos, 128), true, enc);
5 } catch (UnsupportedEncodingException uee) {}
6 }
7 return new PrintStream(new BufferedOutputStream(fos, 128), true);
8 }

该方法是为输出流创建一个BufferedOutputStream缓冲输出流,起到流缓冲的作用,最后通过new PrintStream()创建一个打印输出流。

通过该流的打印接口,如print(), println(),可实现打印输出的作用。

三.最后就是执行 setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));

1 private static native void setOut0(PrintStream out);

可知,该方法是一个native方法,感兴趣的童鞋可继续深入研究,这里大概就是将生成的PrintStream对象赋值给System里的静态对象引用变量:out。

1 public final static PrintStream out = null;

到这里,就回到了我们最开始的地方:System.out.println,没错,这里面的out,就是通过setOut0来进行PrintStream对象赋值的,我们既然能拿到了PrintStream的对象引用out,自然就可以访问PrintStream类里的任何public方法里,包括println(),包括print(),等等。

可提取以上初始化out的源码重做一个手动打印的测试,如:

执行,发现可以控制台上打印出"测试打印"四字。

最后,总结一下,System.out.println的原理是在类加载System时,会初始化System的initializeSystemClass()方法,该方法中将创建一个打印输出流PrintStream对象,随后通过setOut0(PrintStream out)方法,会将初始化创建的PrintStream 对象赋值给System静态引用变量out。out被赋值对象地址后,就可以调用PrintStream中的各种public修饰的方法里,其中就包括println()、print()这类打印信息的方法,通过out.println(“xxxx”)即可将“xxxx”打印到控制台上,也就是等价于System.out.println("xxxx")。

1 System.out.println("打印数据");
2 等价于--->
3 PrintStream out=System.out;
4 out.println("打印数据");

以上,就是System.out.println的执行原理。

若有不足,还请指出改正。

灵魂拷问:你真的理解System.out.println()执行原理吗?的更多相关文章

  1. 【Java】面试官灵魂拷问:if语句执行完else语句真的不会再执行吗?

    写在前面 最近跳槽找工作的朋友确实不少,遇到的面试题也是千奇百怪,这不,一名读者朋友面试时,被面试官问到了一个直击灵魂的问题:if 语句执行完else语句真的不会再执行吗?这个奇葩的问题把这名读者问倒 ...

  2. 随机数、方法重载和System.out.println()的理解

    1.编写一个方法,使用以上算法生成指定数目(比如1000个)的随机数. package testradom; public class testradom { public static void m ...

  3. 工厂设计模式灵魂拷问-Java实现

    show me the code and take to me,做的出来更要说的明白 GitHub项目JavaHouse同步收录 喜欢就点个赞呗! 你的支持是我分享的动力! 引入 我们经常听到工厂模式 ...

  4. 灵魂拷问:如何检查Java数组中是否包含某个值 ?

    在逛 programcreek 的时候,我发现了一些专注细节但价值连城的主题.比如说:如何检查Java数组中是否包含某个值 ?像这类灵魂拷问的主题,非常值得深入地研究一下. 另外,我想要告诉大家的是, ...

  5. 【java基础】程序员你真的理解反射机制吗?

    目录 前言 1.反射的概述 2.正式使用反射之前很有必要了解的Class类 3.反射的使用 前言 很多讲解反射的博客文章并没有详细讲解Class类,~当然包括之前的我也一样~,这样的文章只会让反射徒有 ...

  6. 灵魂拷问:Java 的 substring() 是如何工作的?

    在逛 programcreek 的时候,我发现了一些小而精悍的主题.比如说:Java 的 substring() 方法是如何工作的?像这类灵魂拷问的主题,非常值得深入地研究一下. 另外,我想要告诉大家 ...

  7. 灵魂拷问:创建 Java 字符串,用""还是构造函数

    在逛 programcreek 的时候,我发现了一些小而精悍的主题.比如说:创建 Java 字符串,用 "" 还是构造函数?像这类灵魂拷问的主题,非常值得深入地研究一下. 01.& ...

  8. 你真的理解Java的按引用传递吗?

    首先我们来看下面这段代码: public class Test1 { String a = "123"; public static void change(Test1 test) ...

  9. Android菜鸟的成长笔记(4)——你真的理解了吗?

    原文:Android菜鸟的成长笔记(4)--你真的理解了吗? 在上一篇中我们查看了QQ的apk源文件中的布局结构,并仿照QQ完成了我们第一个应用的界面,详细请看<Android菜鸟的成长笔记&g ...

随机推荐

  1. Apache2.4 下载和安装 - Win10

    Apache安装包已放入百度网盘,链接地址在本文最后 1.下载Windows版本的Apahce安装包 a. 访问官网,进入下载页面 https://www.apachelounge.com (apac ...

  2. python基础一(安装、变量、循环、git)

    一.开发语言分类 系统的开发语言有java.c++.c#.python.ruby.php等等,开发语言可分为编译型语言和解释型语言. 编译型语言就是写好代码之后就把代码编译成二进制文件,运行的时候运行 ...

  3. PicGo软件搭配gitee实现图床

    1.安装PicGo软件,并配置gitee 1.1安装picGo picGo 安装gitee-uploader 插件 官网下载地址如下:最新版本 可以自行选择版本进行下载,这里我选择了最新的版本进行下载 ...

  4. 吴恩达《深度学习》-课后测验-第一门课 (Neural Networks and Deep Learning)-Week 4 - Key concepts on Deep Neural Networks(第四周 测验 – 深层神经网络)

    Week 4 Quiz - Key concepts on Deep Neural Networks(第四周 测验 – 深层神经网络) \1. What is the "cache" ...

  5. 软件测试----xml文件介绍

    软件测试 目录 软件测试 一.什么是XML?: 二.XML和HTML的差异: 三.XML的特点 1.XML可以自定义标签 2.XML必须包含根元素 如上所示, 3.XML标签对大小写敏感 4.XML ...

  6. Java审计之文件操作漏洞

    Java审计之文件操作漏洞篇 0x00 前言 本篇内容打算把Java审计中会遇到的一些文件操作的漏洞,都给叙述一遍.比如一些任意文件上传,文件下载,文件读取,文件删除,这些操作文件的漏洞. 0x01 ...

  7. JVM的整体结构

    整个jvm的运行流程图如上所示,首先需要进行加载class文件,然后使用类加载子系统将class翻译解析导入内存,在内存中分别导入到对应的运行时数据区,然后执行引擎开始执行,对于需要的数据在对应的区域 ...

  8. Spring Boot学习(一)初识Spring Boot

    Spring Boot 概述 Spring Boot 是所有基于 Spring 开发的项目的起点.Spring Boot 的设计是为了让你尽可能快的跑起来 Spring 应用程序并且尽可能减少你的配置 ...

  9. HTTP协议学习之Request学习

    在开始前,我们首先对HTTP协议做个简单的了解 HTTP协议(Hyper Text Transfer Protocol) 超文本传输协议 名词非常的高大上,如果学过计算机网络这门课(再如果还有一点印象 ...

  10. leetcode1558题解【贪心】

    leetcode1558.得到目标数组的最少函数调用次数 题目链接 算法 贪心 时间复杂度O(nlogN),N为数组中最大的那个数. 1.题意就是给定一个函数,该函数有两种功能,一种就是将数组中的所有 ...