IO第十一回:处理流之四:标准输入流 & 标准输出流
标签: IO流
在IO中,标准输入流是System.in,标准输出流是System.out。我们经常使用它们,我们今天要学一学他们的原理
System.out
我们初学java的第一个程序是”hello world”
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
上面程序到底是怎么在屏幕上输出“hello world”的呢?
这就是下面要讲解的内容,即System.out.println(“hello world”)的原理。
out的定义
我们先看看System.java中out的定义,源码如下:
public final class System {
...
public final static PrintStream out = null;
...
}
从中,我们发现out是一个PrintStream类型的静态成员变量:
out是System类的静态变量,因此我们在使用时,可以以
类名.静态变量名
的形式来使用。out是PrintStream对象,PrintStream打印类中有许多重载的println()方法。
接下来,看它是如何被初始化的,它是怎么和屏幕输出关联的?
我们还是一步步来分析,首先看看System类中的initializeSystemClass()方法。
initializeSystemClass()的源码如下:
private static void initializeSystemClass() {
......
FileInputStream fdIn = new FileInputStream(FileDescriptor.in);
FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
FileOutputStream fdErr = new FileOutputStream(FileDescriptor.err);
setIn0(new BufferedInputStream(fdIn));
setOut0(new PrintStream(new BufferedOutputStream(fdOut, 128), true));
setErr0(new PrintStream(new BufferedOutputStream(fdErr, 128), true));
......
}
我们只关注out相关的代码,可以细分为一下几步:
第1步
FileDescriptor fd = FileDescriptor.out;
获取FileDescriptor.java中的静态成员out,out是一个FileDescriptor对象,它实际上是“标准输出(屏幕)”的标识符。第2步
FileOutputStream fdOut = new FileOutputStream(fd);
创建“标准输出(屏幕)”对应的“文件输出流”。第3步
BufferedOutputStream bufOut = new BufferedOutputStream(fdOut, 128);
创建“文件输出流”对应的“缓冲输出流”。目的是为“文件输出流”添加“缓冲”功能。第4步
PrintStream ps = new PrintStream(bufout, true);
创建“缓冲输出流”对应的“打印输出流”。目的是为“缓冲输出流”提供方便的打印接口,如print(), println(), printf();使其能方便快捷的进行打印输出。第5步
setOut0(ps);
执行setOut0(ps);
其中,第5步的setOut0(ps)的声明,如下:
private static native void setOut0(PrintStream out);
从中,我们发现setOut0()是一个native本地方法。其作用就是将ps设置为System.java的out静态变量。
FileDescriptor.out就是机器的“标准输出(屏幕)”的文件标识符。我们可以通俗的将文件标识符就理解为,FileDescriptor.out就是代表的“标准输出”。
因此,在initializeSystemClass()中,上面的5步就是将“FileDescriptor.out”封装了起来。封装后的System.out既有缓冲功能;又有便利的操作接口,如print(), println(), printf()。
记录日志文件
标准输出流对象会将数据打印到控制台上。查阅API可知有如下方法,
static void setOut(PrintStream out) //重新分配“标准”输出流。
可以重新指定输出流对象,即将System.out.println();的输出内容打印到我们想打印到的地方。
File file = new File("F:\\a.txt");
PrintStream printStream = new PrintStream(file);
System.setOut(printStream);
System.out.println("打印到F:\\a.txt中");
这时候内容回写入到文件a.txt中去,而不是打印在控制台中。
我们可以利用这个方法来记录日志文件。
假设有代码:
try{
int n = 5/0;
}catch(Exception e){
e.printStackTrace();
}
执行结果会抛出我们想要的错误日志信息。
java.lang.ArithmeticException: / by zero
at log.DemoLog.main(DemoLog.java:26)
这时候想将日志信息保存起来怎么办呢?
void printStackTrace()
将此throwable和其追溯打印到标准错误流。void printStackTrace(PrintStream s)
将此throwable和其追溯打印到指定的打印流。void printStackTrace(PrintWriter s)
将此throwable和其追溯打印到指定的打印作者。
看到Exception类中的这3个重载方法,我们不难得知,只要给他指定一个打印输出流对象当中,即可将日志信息保存到我们想要的地方。
File file = new File("F:\\a.log");
PrintStream printStream = new PrintStream(file);
try{
int n = 5/0;//除数为零异常
}catch(Exception e){
e.printStackTrace(printStream);
}
但是我们这有一个缺陷,每次出现异常,新的异常的打印都会替换掉原来的异常。
打印流PringStream接收的参数,可以是File类型的,可以是OutputStream类型的,也可以是String类型的文件名。
当接受OutputStream类型的对象时,我们可以在构造方法时添加append参数:
FileOutputStream(File file, boolean append)
创建一个向指定 File 对象表示的文件中写入数据的文件输出流。如果第二个参数append为 true,则将字节写入文件末尾处,而不是写入文件开始处。
因此
File file = new File("F:\\a.log");
PrintStream printStream = new PrintStream(new FileOutputStream(file,true),true);
try{
int n = 5/0;//除数为零异常
}catch(Exception e){
e.printStackTrace(printStream);
}
实现了日志的打印保存。
System.in
public static final InputStream in
“标准”输入流。 该流已经打开,准备提供输入数据。通常,该流对应于键盘输入或由主机环境或用户指定的另一个输入源。
System.in原理
public final static InputStream in = null;
在initializeSystemClass()
中
FileInputStream fdIn = new FileInputStream(FileDescriptor.in);
FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
FileOutputStream fdErr = new FileOutputStream(FileDescriptor.err);
setIn0(new BufferedInputStream(fdIn));
首先System.in指的是一个输入流(InputStream),它的功能是创建一个由键盘输入的流。
再说说System.in的工作原理:
我们知道一个输入流一定要有一个输入源为其提供数据,那System.in这个输入流的源头在哪里呢?
System.in比较特殊,在使用这个输入流的时候,数据源会更新,也就是说它的源头是新开辟出来的空间,用来储存键盘上输入的内容。
当我们调用System.in语句的时候实际上我们做了两个动作:
1. 创建了一个字节流;
2. 创建了一个类似于文件的储存空间(用完即被消除)。
试分析下面语句:
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while((s=in.readLine)!=null){
//循环主体
}
这种从键盘上向一个流中输入数据的方法称为阻塞式方法,当你不在键盘上输入数据或者没有按enter键输入的时候,循环是不会进行的,流会暂停流动。
简单标准输入
System.in作为字节输入流类InputStream的对象实现标准输入,通过read()方法从键盘接受数据。
int read()
// 返回输入数值的ASCII码,,该值为0到255范围内的int字节值。若返回值为-1,说明没有读取到任何字节读取工作结束。
int read(byte b[])
// 读入多个字节到缓冲区b中,返回值是读入的字节数
int read(byte b[],int offset,int len)
//读入多个字节到缓冲区b中,指定起始位置和长度
我们针对三种方法进行实践:
/**
* System.in.read()返回值为输入数值的ASCII码,该值为0到 255范围内的int字节值
* 如果因为已经到达流末尾而没有可用的字节,则返回值 -1。
*/
public static void testRead1() throws IOException {
int a = 0;
System.out.println("请输入a:");
a = System.in.read(); //返回int类型的ASCII值
System.out.println("a=" + a);
System.out.println("(char)a=" + (char)a);
}
上面的代码只能返回一个字符
请输入a:
a
a=97
(char)a=a
/**
* 将要输入的数据存进字节数组中
*
*/
public static void testRead2() throws IOException {
byte[] bytes = new byte[10];
System.in.read(bytes);
for (int i = 0; i < bytes.length; i++) {
if (bytes[i] != 0) {
System.out.print((char)bytes[i] );
}
}
下面的程序通过while循环,可以输入一行数组,直到输入停止
public static void testRead3() throws IOException {
int b;
System.out.println("请输入:");
while ((b = System.in.read()) != -1) {
System.out.print((char) b);
}
}
Scanner类对象
public final class Scanner extends Object implements Iterator<String> Closeable
一个简单的文本扫描器,可以使用正则表达式解析原始类型和字符串。
构造方法
我们可以从Scanner类的构造方法中发现其用法:
//文本扫描器的数据源是一个File类型的文件对象
Scanner(File source)
Scanner(File source, String charsetName)
//文本扫描器的数据源是一个InputStream输入流的对象
Scanner(InputStream source)
Scanner(InputStream source, String charsetName)
//文本扫描器直接将String字符串当做数据源
Scanner(String source)
//文本扫描器的数据源是一个路径
Scanner(Path source)
Scanner(Path source, String charsetName)
//文本扫描器的数据源是一个Readable字符源
Scanner(Readable source)
构造一个新的 Scanner ,产生从指定源扫描的值。
/文本扫描器的数据源是一字符通道
Scanner(ReadableByteChannel source)
构造一个新的 Scanner ,产生从指定通道扫描的值。
Scanner(ReadableByteChannel source, String charsetName)
构造一个新的 Scanner ,产生从指定通道扫描的值。
重要方法
void close()
关闭此扫描仪。Pattern delimiter()
返回 Pattern这个 Scanner正在使用来匹配分隔符。boolean hasNext()
如果此扫描仪在其输入中有另一个令牌,则返回true。boolean hasNextLine()
如果扫描仪的输入中有另一行,则返回true。String next()
查找并返回此扫描仪的下一个完整令牌。int nextInt()
将输入的下一个标记扫描为 int 。String nextLine()
将此扫描仪推进到当前行并返回跳过的输入。
从hasNext(),next()繁衍了大量的同名不同参方法,这里不一一列出,感兴趣的,可以查看API
代码演示
当通过new Scanner(System.in)创建一个Scanner,控制台会一直等待输入,直到敲回车键结束,把所输入的内容传给Scanner,作为扫描对象。如果要获取输入的内容,则只需要调用Scanner的nextLine()方法即可。
//逐行扫描文件,并逐行输出
public static void fileScanner() throws IOException{
File file = new File("PrintStream.txt");
FileInputStream fis = new FileInputStream(file);
Scanner scanner = new Scanner(fis);
//如果在此扫描器的输入中存在另一行,则返回 true
while (scanner.hasNextLine()) {
//此扫描器执行当前行,并返回跳过的输入信息。
System.out.println(scanner.nextLine());
}
fis.close();
scanner.close();
}
//从键盘输入,以空格为分隔,以回车为结束
public static void keyboardScanner1() {
System.out.println("请输入:");
Scanner scanner = new Scanner(System.in);
//判断扫描器中当前扫描位置后是否还存在下一段
while (scanner.hasNext()) {
//查找并返回来自此扫描器的下一个完整标记,返回的String字符
System.out.println(scanner.next());
}
}
public static void keyboardScanner2() {
System.out.println("请输入:");
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
if (line.equals("exit")) break;
System.out.println(">>>" + line);
}
}
//hasNext和next方法的衍生方法
public static void keyboardScanner2() {
Scanner in = new Scanner(System.in);
System.out.println("请输入一个整数");
while(in.hasNextInt()){
int num = in.nextInt();
System.out.println("数字"+num);//输入999 32 只读到999
System.out.println("请输入一个字符串");
String str = in.next();//输入 adcd ef 只读adcd
System.out.println("字符串"+str);
}
}
//Scanner useDelimiter(String pattern)
//将此扫描器的分隔模式设置为从指定的构造的模式 String 。
public static void testUseDelimiter() {
Scanner s = new Scanner("123 asda bf 12 123 nh l,sf.fl ...adafafa lda");
s.useDelimiter(" |, |\\."); //用“ ”或“,”或“.”来分隔
while (s.hasNext()) {
System.out.println(s.next());
}
}