-
简要
写demo的时候意外发现print()和printnln()的一些特别的地方,是有关于Java输入输出流的知识,都是些基础知识,但这个偶然的发现加深了一些理解,特别记录一下。
fun main(){ GlobalScope.launch { delay(3000L) println("World!") } print("Hello,") // println("Hello,") //Jvm保活 Thread.sleep(5000L) }
这段代码是关于kotlin协程的,程序的流程应该是先输出"Hello,",然后等待三秒输出"World!"。但是当我使用print()的时候是输出台等待三秒之后一次性输出字符串"Hello,World!",使用println的时候才是期望中的先输出"Hello,",再输出"World!",当然换行的不同我们不用说了,但是为什么print()会有种delay()方法没有发挥期望中的作用呢?处于求知欲我点开了源码。
-
源码分析
首先,kotlin中print()方法就是Java中的System.out.print()方法,同样,println()就是System.out.println()。
out是System中的一个静态常量:
public final static PrintStream out;
它的初始化在同类的static块中:
static { unchangeableProps = initUnchangeableSystemProperties(); props = initProperties(); addLegacyLocaleSystemProperties(); sun.misc.Version.initSystemProperties(); // TODO: Confirm that this isn't something super important. // sun.misc.VM.saveAndRemoveProperties(props); lineSeparator = props.getProperty("line.separator"); FileInputStream fdIn = new FileInputStream(FileDescriptor.in); FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out); FileOutputStream fdErr = new FileOutputStream(FileDescriptor.err); // BEGIN Android-changed: lower buffer size. // in = new BufferedInputStream(fdIn); in = new BufferedInputStream(fdIn, 128); // END Android-changed: lower buffer size. out = newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")); err = newPrintStream(fdErr, props.getProperty("sun.stderr.encoding")); ...... }
通过newPrintStream方法创建:
/** * Create PrintStream for stdout/err based on encoding. */ private static PrintStream newPrintStream(FileOutputStream fos, String enc) { if (enc != null) { try { return new PrintStream(new BufferedOutputStream(fos, 128), true, enc); } catch (UnsupportedEncodingException uee) {} } return new PrintStream(new BufferedOutputStream(fos, 128), true); }
现在我们的重心来到PrintStream中:
private PrintStream(boolean autoFlush, OutputStream out, Charset charset) { super(out); this.autoFlush = autoFlush; // Android-changed: Lazy initialization of charOut and textOut. // this.charOut = new OutputStreamWriter(this, charset); // this.textOut = new BufferedWriter(charOut); this.charset = charset; }
注意这里的autoFlush传进来的是true。
接下来看一下他们的print方法和println方法:
public void print(Object obj) { write(String.valueOf(obj)); }
public void println(Object x) { String s = String.valueOf(x); synchronized (this) { print(s); newLine(); } }
这两个方法有很多重构方法,所以传参不限于Object。可以看到,println()内部也是调用了print()方法,只不过最后多调用了一个newLine(),我们看一下newLine方法里面:
private void newLine() { try { synchronized (this) { ensureOpen(); // Android-added: Lazy initialization of charOut and textOut. BufferedWriter textOut = getTextOut(); textOut.newLine(); textOut.flushBuffer(); charOut.flushBuffer(); if (autoFlush) out.flush(); } } catch (InterruptedIOException x) { Thread.currentThread().interrupt(); } catch (IOException x) { trouble = true; } }
作为对比,我们看一下单纯的print有什么不一样,也就是write方法:
private void write(String s) { try { synchronized (this) { ensureOpen(); // Android-added: Lazy initialization of charOut and textOut. BufferedWriter textOut = getTextOut(); textOut.write(s); textOut.flushBuffer(); charOut.flushBuffer(); if (autoFlush && (s.indexOf('\n') >= 0)) out.flush(); } } catch (InterruptedIOException x) { Thread.currentThread().interrupt(); } catch (IOException x) { trouble = true; } }
看出不同了吧,单纯的调用write方法只有在输出的字符中含有换行符'\n'才会执行flush方法,而newLine方法则会保证只要autoFlush是true就会执行flush方法,前面说到autoFlush这种情况下传入的都是true,所以区别就在于字符串中是否含有\n,flush方法会打印缓冲区中的信息到控制台,我们就会看到。
PrintStream中的out就是传入的BufferedOutputStream,所以flush就是调用它的方法:
public synchronized void flush() throws IOException { flushBuffer(); out.flush(); }
它的out就是构造时的FileOutputStream,它的flush方法就是父类OutputStream的flush方法:
/** * Flushes this output stream and forces any buffered output bytes * to be written out. The general contract of <code>flush</code> is * that calling it is an indication that, if any bytes previously * written have been buffered by the implementation of the output * stream, such bytes should immediately be written to their * intended destination. * <p> * If the intended destination of this stream is an abstraction provided by * the underlying operating system, for example a file, then flushing the * stream guarantees only that bytes previously written to the stream are * passed to the operating system for writing; it does not guarantee that * they are actually written to a physical device such as a disk drive. * <p> * The <code>flush</code> method of <code>OutputStream</code> does nothing. * * @exception IOException if an I/O error occurs. */ public void flush() throws IOException { }
根据注释,这个方法会立即把数据写入到流中,但是不保证会写入到硬件存储设备,因为这是操作系统的职责,它只需要把数据交给和操作系统交互的流中,这部分实现是JVM实现的。这就是为什么正常情况下flush调用之后我们才能看到控制台输出信息的原因。
好了,那我们知道了这个之后,回过头来看,flush前面的代码做了什么。
ensureOpen()确保输入流不能为空,无需赘言。
getTextOut中做了什么:
// BEGIN Android-added: Lazy initialization of charOut and textOut. private BufferedWriter getTextOut() { if (textOut == null) { charOut = charset != null ? new OutputStreamWriter(this, charset) : new OutputStreamWriter(this); textOut = new BufferedWriter(charOut); } return textOut; } // END Android-added: Lazy initialization of charOut and textOut.
charOut把PrintStream封装进OutputStreamWriter中,然后textOut又把charOut封装进BufferedWriter中,然后调用write方法把数据写入,其实就是移交给charOut去做,OutputStreamWriter内部又是通过一个叫StreamEncoder的类去做的,经查询,StreamEncoder的处理过程和我们分析flush的调用过程一样,最终也是调用了JVM的底层方法,把数据压入缓冲区。
-
总结
经过以上的分析,我们知道,print()方法和println()方法的不同就是后者在把数据压入缓冲区之后还调用flush方法把缓冲区中的数据推到和操作系统交互的流中,然后操作系统和硬件交互从而显示出控制台信息,print()若是输出的字符串中含有'\n',则换行符前面的内容会直接显示出来,而换行符之后的内容则会暂存到缓冲区,直到调用下一次的flush方法或者遇到下一个换行符。
网友评论