Java I/O系统学习系列四:标准IO
几乎所有学习Java的同学写的第一个程序都是hello world,使用的也都是System.out.println()这条语句来输出"hello world",我也不例外,当初学的时候只是简单拿来用,平时学习的时候需要打印日志也会使用这条语句,并没有去探究这条语句背后的原理,本文就来研究一下其原理。
System.out.println()提供的能力属于标准I/O的范畴,标准I/O这个术语参考的是Unix中“程序所使用的单一信息流”这个概念,即程序的所有输入都可以来自标准输入,它的所有输出也可以发送到标准输出,以及所有的错误信息都可以发送到标准错误。标准I/O的意义在于:我们可以很容易地把程序串联起来,一个程序的标准输出可以成为另一程序的标准输入。
其实对于标准I/O,直观一点的理解可以是来自命令行的I/O,因为程序通常是在命令行下运行的,并且是在命令行环境下和用户交互的。所以Java平台提供了两种方式用于和程序进行交互:通过标准流的方式和通过控制台的方式。
标准流是很多操作系统都有的一个特性。默认情况下,标准流是从键盘读取输入并且输出到显示设备(显示器)。同时标准流也支持输入输出到文件或者程序之间的I/O,但是这两种特性是由命令行解释器来决定的,而不是程序。
Java平台支持三种标准流:
- 标准输入,通过System.in获取
- 标准输出,通过System.out获取
- 标准错误,通过System.err获取
这些对象是预定义好的并且不需要手动打开。标准输出(Standard Output)和标准错误(Standard Error)都是用于输出,提供错误输出的好处是使用者可以将正常输出指向一个文件,同时还能够读取错误信息。
下面就来详细介绍一下这些标准流。
1. 标准输出
1.1 类结构
说到标准输出就不得不说常见的System.out.println()了,这是一条Java语句,没错,它可以将程序传给System.out(标准输出)的参数打印出来。我们可以将其分成三部分来看:
System
这是java.lang包中的一个final类,主要的作用如下:
- 提供了如标准输入,标准输出和错误输出流等基础设施;
- 可以访问外部定义的属性和环境变量;
- 提供了一种加载文件和库的方法;
- 提供了可以快速复制数组的部分内容的方法;
out
这是System类的一个静态成员字段,类型是PrintStream。其访问标识符是public final,这意味着它在启动时就会被实例化,并与主机的标准输出控制台进行关联,并且该流在实例化之后会自动打开,并准备接受数据。
println
这是PrintStream的一个方法,可以输出内容到控制台。
这里直接盗用一张网上的类图,结合起来看会更清晰其结构:
说完类结构,我们再来看看System.out的一些其他操作。
1.2 将System.out转换成PrintWriter
System.out是一个PrintStream,而PrintStream是一个OutputStream。PrintWriter有一个可以接受OutputStream作为参数的构造器。所以,可以使用那个构造器将System.out转换成PrintWriter:
public class ChangeSystemOut{ public static void main(String[] args){ PrintWriter out = new PrintWriter(System.out, true); out.println("Hello, world"); } }
这样包装了之后就可使用PrintWriter的功能了,这里使用了有两个参数的PrintWriter构造器,并将第二个参数设为true,以便开启自动清空功能,不然的话可能看不到输出。
1.3 输出重定向
System.out中的out对象是可以手动指定的。默认会在Java运行环境启动时进行初始化,并且可以在运行时改变其实际对象。我们可以通过setOut方法来将输出重定向,比如下面的例子,将输出重定向到一个文件中:
public class ChangeOut { public static void main(String args[]) { try { System.setOut(new PrintStream(new FileOutputStream("log.txt"))); System.out.println("Now the output is redirected!"); } catch(Exception e) {} } }
在有大量输出显示在屏幕并且这些输出滚动得太快以至于无法阅读时,重定向输出就变得极为有用。
1.4 System.out.println的性能分析
众所周知,System.out.println的性能并不好,为什么呢?我们可以看一下其调用顺序:println - > print - > write()+ newLine(),这个是在Sun / Oracle JDK中的实现。其中write()和newLine()方法都包含了一个synchronized块,同步的方式会有一点开销,不过呢更影响性能的则是添加字符到缓冲区和打印。
有文献表明,运行多个System.out.println并记录时间,执行时间会按比例增加。当打印超过50个字符并打印超过50,000行时,性能下降明显。
当然虽然System.out.println()性能不好,但是还是取决我们的使用场景,如果是写写demo学习则直接使用好了,因为是Java原生支持的特性,所以不需要引入任何依赖,这是其最大的好处吧。当然,在我们工作中开发商用软件,那就最好不要用System.out.println了,这就不仅仅是因为性能问题了。
1.5 System.out.println和通用日志组件的对比
为了方便,我们可能常常会直接使用System.out.println()输出日志,但是既然用System.out输出日志这么方便,那又为什么还需要那些通用日志组件(如log4j)呢?System.out.println()又存在什么问题?如下是一些常见的总结:
- 灵活性:像log4j这一类的通用组件提供了多种日志级别,这样就可以通过不同级别相应地分隔日志信息。例如,X消息只能在PRODUCTION级别打印,Y消息应打印在ERROR级别打印等,详细的级别定义这里就不再总结了。
- 可重构性:log4j只需一个参数更改即可关闭所有日志记录。
- 可维护性:想象一下,如果我们有数百个System.out.println散落在应用程序的各个角落,那将会使程序变得难以维护。
- 粒度:在应用程序中,每个类都可以有不同的记录器并相应地进行控制。
- 实用性:在System.out中重定向消息的选项比较少(指向文件、指向程序),但是像log4j之类的组件,其提供了更多的重定向选择,我们甚至可以重定向到自定义的输出选项。
所以呢如果我们只是正在编写一个小demo,只是为了实验/学习目的那么使用System.out.println是很方便的。但是当我们要开发软件时,我们就应该使用通用的日志组件比如log4j等。
2. 标准输入、标准错误
前面我们着重学习了一下标准输出,这里再总结一下它的兄弟:标准输入和标准错误。
标准输入和标准输入刚好相反,是用来从标准输入(一般是键盘)设备获取输入的。而标准错误则是通过PrintStream将错误信息打印到标准错误输出流中,在我们使用比如eclipse这种IDE时就可以看出它和标准输出的区别。看一个简单例子:
public class InOutErr { public static void main(String args[]) { try { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); String filename = reader.readLine(); InputStream input = new FileInputStream(filename); System.out.println("File opened..."); } catch (IOException e){ System.err.println("Where is that file?"); } } }
启动程序之后会阻塞,等待输入文件名称,随意输入,如果找不到对应文件,就会输出错误日志,可以看一下结果,err的打印是红色的。
同样,标准输入和标准错误也可以进行重定向,可以通过System提供的一些静态方法完成重定向:
- setIn(InputStream)
- setOut(PrintStream)
- setErr(PrintStream)
这里是一个简单例子演示这些方法的使用:
public class Redirecting { public static void main(String[] args) throws IOException{ PrintStream console = System.out; BufferedInputStream in = new BufferedInputStream(new FileInputStream("pom.xml")); PrintStream out = new PrintStream(new BufferedOutputStream(new FileOutputStream("test.out"))); System.setIn(in); System.setOut(out); System.setErr(out); BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String s; while((s = br.readLine()) != null){ System.out.println(s); } out.close(); // Remember this! System.setOut(console); } }
这个例子将标准输入重定向到文件上,并将标准输出和标准错误重定向到另一个文件上。注意,它在程序开头处存储了对最初的System.out对象的引用,并且在结尾处将系统输出恢复到了该对象上。
I/O重定向操纵的是字节流,而不是字符流,所以这里使用的是InputStream和OutputStream,而不是Reader和Writer。
3. 标准输入和输出的区别
标准输入和输出除了一个是输入,一个是输出,还有使用上的一些区别。
由于某些历史原因,标准流属于字节流,System.out和System.err其实是PrintStream类型。虽然是字节流,但是PrintStream利用一个内部字符流对象来字符流的许多特性。
相对标准输出而言System.in就只是一个单纯的字节流了,没有包含内部的字符流对象。如果要像字符流一样使用标准输入则需要通过InputStreamReader将其包装一下了:
InputStreamReader cin = new InputStreamReader(System.in);
标准输出和标准输入在这一点上的区别也可从两者的使用上看出来,以我们最常用的在控制台打印一条语句和从控制台接收键盘输入为例:
// 打印日志
System.out.println("");
// 接收标准输入
Scanner scan = new Scanner(System.in);
前者直接使用的是字符流的特性,后者则通过了一个Scanner进行包装。
4. 总结
- Java平台提供了三种标准流,分别是System.in(标准输入)、System.out(标准输出)、System.err(标准错误),我们常用的System.out.println()就是属于标准输出。
- System是java.lang包中的一个final类,out则是System类的一个静态成员,其类型为PrintStream,println()则是PrintStream的一个方法,可以输出内容到控制台。
- 标准流属于字节流,System.out和System.err其实是PrintStream类型,但是具有许多字符流的特性,而System.in就只是一个单纯的字节流。
- 标准流都可以进行重定向。
- System.out.println()性能并不好,但是平时学习使用是不影响的。
- 在项目中尽量不要使用System.out.println()输出日志,而应该使用更通用的日志组件来完成日志打印的任务。