灵魂拷问:你真的理解System.out.println()打印原理吗?

简介: 灵魂拷问:你真的理解System.out.println()打印原理吗?

原创/朱季谦

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

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


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

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

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

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

 

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

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

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

publicfinalclassSystem {
privatestaticnativevoidregisterNatives();
static {
registerNatives();
      }
privateSystem() {
      }
publicfinalstaticInputStreamin=null;
publicfinalstaticPrintStreamout=null;
publicfinalstaticPrintStreamerr=null;
privatestaticvolatileSecurityManagersecurity=null;
publicstaticvoidsetIn(InputStreamin) {
checkIO();
setIn0(in);
     }
publicstaticvoidsetOut(PrintStreamout) {
checkIO();
setOut0(out);
      }
      ......
  }

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

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

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


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

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

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

publicclassPrintStreamextendsFilterOutputStreamimplementsAppendable, Closeable  {
  ......
publicvoidprintln() {
newLine();
      }
publicvoidprintln(booleanx) {
synchronized (this) {
print(x);
newLine();
         }
     }
publicvoidprintln(charx) {
synchronized (this) {
print(x);
newLine();
         }
     }
publicvoidprintln(intx) {
synchronized (this) {
print(x);
newLine();
         }
     }
publicvoidprintln(longx) {
synchronized (this) {
print(x);
newLine();
         }
     }
publicvoidprintln(floatx) {
synchronized (this) {
print(x);
newLine();
         }
     }
publicvoidprintln(doublex) {
synchronized (this) {
print(x);
newLine();
         }
     }
publicvoidprintln(charx[]) {
synchronized (this) {
print(x);
newLine();
         }
     }
publicvoidprintln(Stringx) {
synchronized (this) {
print(x);
newLine();
         }
     }
   ......
 }

发现这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()方法来实现换行的——

publicvoidprint(booleanb) {
write(b?"true" : "false");
 }
publicvoidprint(charc) {
write(String.valueOf(c));
  }
publicvoidprint(inti) {
write(String.valueOf(i));
 }
publicvoidprint(longl) {
write(String.valueOf(l));
 }
publicvoidprint(floatf) {
write(String.valueOf(f));
 }
publicvoidprint(doubled) {
write(String.valueOf(d));
 }
publicvoidprint(chars[]) {
write(s);
 }
publicvoidprint(Strings) {
if (s==null) {
s="null";
     }
write(s);
 }

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

privatevoidwrite(Strings) {
try {
synchronized (this) {
ensureOpen();
textOut.write(s);
textOut.flushBuffer();
charOut.flushBuffer();
if (autoFlush&& (s.indexOf('\n') >=0))
out.flush();
         }
     }
catch (InterruptedIOExceptionx) {
Thread.currentThread().interrupt();
     }
catch (IOExceptionx) {
trouble=true;
     }
 }

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

privatevoidensureOpen() throwsIOException {
if (out==null)
thrownewIOException("Stream closed");
 }

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

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

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

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

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

publicfinalclassSystem {
/* register the natives via the static initializer.** VM will invoke the initializeSystemClass method to complete* the initialization for this class separated from clinit.* Note that to use properties set by the VM, see the constraints* described in the initializeSystemClass method.*/privatestaticnativevoidregisterNatives();
static {
registerNatives();
     }
 }

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

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

privatestaticvoidinitializeSystemClass() {
//被vm执行系统属性初始化props=newProperties();
initProperties(props); 
sun.misc.VM.saveAndRemoveProperties(props);
//从系统属性中获取系统相关的换行符,赋值给变量lineSeparatorlineSeparator=props.getProperty("line.separator");
sun.misc.Version.init();
//分别创建in、out、err的实例对象,并通过set()方法初始化FileInputStreamfdIn=newFileInputStream(FileDescriptor.in);
FileOutputStreamfdOut=newFileOutputStream(FileDescriptor.out);
FileOutputStreamfdErr=newFileOutputStream(FileDescriptor.err);
setIn0(newBufferedInputStream(fdIn));
setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));
setErr0(newPrintStream(fdErr, props.getProperty("sun.stderr.encoding")));
     ......
 }

主要关注这两行代码:

FileOutputStreamfdOut=newFileOutputStream(FileDescriptor.out);
setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));

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

  1. in:标准输入(键盘)的描述符
  2. out:标准输出(屏幕)的描述符
  3. err:标准错误输出(屏幕)的描述符

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

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

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

publicstaticvoidmain(String[] args) throwsIOException {
FileOutputStreamout=newFileOutputStream("C:\\file.txt");
out.write(66);
 }

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

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

privatestaticPrintStreamnewPrintStream(FileOutputStreamfos, Stringenc) {
if (enc!=null) {
try {
returnnewPrintStream(newBufferedOutputStream(fos, 128), true, enc);
         } catch (UnsupportedEncodingExceptionuee) {}
     }
returnnewPrintStream(newBufferedOutputStream(fos, 128), true);
 }

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

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

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

1privatestaticnativevoid setOut0(PrintStream out);

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

1publicfinalstatic 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的执行原理。

 

 

 

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

 

目录
相关文章
|
11天前
|
算法 程序员
老程序员分享:nextInt和nextLine以及next方法的区别
老程序员分享:nextInt和nextLine以及next方法的区别
|
10月前
print与println的区别
print与println的区别
48 0
|
12月前
|
Java
28个案例问题分析---09---equals问题--equals问题,java基本类型
28个案例问题分析---09---equals问题--equals问题,java基本类型
78 0
out.println()爆红解决方法
out.println()爆红解决方法
204 0
println输入和toString方法的重写
println输入和toString方法的重写
100 0
|
监控 安全 Java
JDK源码(18)-System
JDK源码(18)-System
101 0
JDK源码(18)-System
|
物联网 Shell Linux
System 函数|学习笔记
快速学习 System 函数
95 0
System 函数|学习笔记
|
物联网 Shell Linux
System 函数的实现|学习笔记
快速学习 System 函数的实现
462 0
System 函数的实现|学习笔记
|
缓存 Java Linux
System.currentTimeMillis() 和 System.nanoTime() 哪个更快?别用错了!
System.currentTimeMillis() 和 System.nanoTime() 哪个更快?别用错了!
176 0
学了这么久的Java,你确定真正知道System.out.println();吗?
大家学了这么久Java了,确定真的掌握了System.out.println(); 吗?确定了解了Java面向对象编程的含义了吗?今天,我就深层刨析一下这串源代码!
学了这么久的Java,你确定真正知道System.out.println();吗?