java.io是新手学习Java的第一个难点。因为这个package中的东西比较多,也比较复杂,另外加上一些接口太过于面向对象了,更加增大了学习的难度。这一期,我针对这个问题专门探讨一下,通过三篇文章,大家就可以完全地掌握java.io这个包了。
要掌握java.io,必须要掌握的一个概念就是输入输出流。 数据流是一串连续不断的数据的集合,就象水管里的水流,在水管的一端一点一点地供水,而在水管的另一端看到的是一股连续不断的水流。数据写入程序可以是一段、一段地向数据流管道中写入数据,这些数据段会按先后顺序形成一个长的数据流。对数据读取程序来说,看不到数据流在写入时的分段情况,每次可以读取其中的任意长度的数据,但只能先读取前面的数据后,再读取后面的数据。不管写入时是将数据分多次写入,还是作为一个整体一次写入,读取时的效果都是完全一样的。 为什么要有这种抽象呢?我们知道,数据的来源是多种多样的,可能来自文件,也可能来自网络,或者内存,数据可能是有结构的(比如xml),也可能是无结构的,比如简单的文本。所以,如何在语言的层面进行统一的抽象就显得至关重要了。Java中使用了输入输出流这个概念来对所有的数据进行抽象。 根据数据流向的不同,又分为输入流和输出流。输入流是指数据从外部流入当前Java程序,而输出流是指数据从当前的Java程序流出到外部。 在Java中,代表输入流的interface是InputStream,代表输出流的interface是OutputStream。
从键盘上读入数据,最简捷的方式就是通过命令行参数。可能很多同学在第一次写Java程序的时候,对main方法的参数就会有疑问,不知道它是干啥的。其实这个参数主要就是为了处理命令行参数的,例如:
public class TestIO {
public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
}
}
我们可以通过
java TestIO apple banana pear
来观察一下参数是如何输入到Java程序中的。
大家知道,在控制台程序中,有三个可以进行输入输出的通道,我们通常称之为标准输入,标准输出和标准错误。在C语言中,我们会以stdin, stdout, stderr来代指。在Java中,也有这样的东西,分别是:
java.lang.System
public final class System extends Object{
static PrintStream err;//标准错误流(输出)
static InputStream in;//标准输入(键盘输入流)
static PrintStream out;//标准输出流(显示器输出流)
}
通过查看JDK源代码,可以看到,System.java 里,out是这么定义的:
public final static PrintStream out = null;
可见,out 是一个 static 变量,所以我们才可以使用类名直接引用它。
在Java语言中,所有的输入都被抽象成了输入流(InputStream),所有的输出都被抽象成了输出流(OutputStream)。以OutputStream为例,它的几个子类,PrintStream可以向控制台上输出字节,FileOutputStream可以向文件中写入字节,SocketOutputStream可以向网络连接上写入字节,等等,它们都是OutputStream的子类。
与之相对应的InputStream也有各种子类分别负责不同的功能。只是Output负责向外写,而Input负责向程序里读。
把字节的输入输出抽象成一个连续的流,确实形象了很多。有了IO,我们的程序终于可以与外界进行交互了。例如:
byte[] buf = new byte[512];
System.out.println("hey, may I have your name, please? ");
int n = 0;
try {
n = System.in.read(buf);
} catch (IOException e) {
e.printStackTrace();
}
System.out.print("hello, ");
System.out.write(buf, 0, n);
前三个字节是一组,通过UTF-8(我们会在后续的课程中陆续介绍编码的知识)的解码,可以得到前三个字节代表的十进制数是28023,这刚好就是中文字符“海”字的 unicode 码。可见,直接的字节操作对非ascii的字符会比较麻烦。例如,程序读入一个名字,想判断这个名字的姓氏是否为李,如果是字节的操作,我们就得先把读到的这些字节,解码到 unicode,或者反过来,把“李”编码为UTF-8再进行比较。这显然太麻烦了,编解码这么机械的工作,干嘛不让机器替我们做呢?
基于这个想法,Java引入了一个可以把字节流转成字符流的适配器——InputStreamReader。请继续观看下一篇文章,适配器模式。
Scanner
文章的最后,我还想额外提一下Scanner类。这是一个用于输入的辅助类,是从Java1.5开始引入的。在那之前,如果我想从标准输入里读两个数,并把它们的和打出来。用Java就得这么写:
public class TestIO {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String s = br.readLine();
String[] ss = s.split(" ");
int a = Integer.parseInt(ss[0]);
int b = Integer.parseInt(ss[1]);
System.out.print(a + b);
}
}
输入
1 2
输入
3
相比起来,C语言的写法就比较简捷:
int main() {
int a, b;
scanf("%d %d", &a, &b);
printf("%d\n", a + b);
}
Java为了解决这种数字的输入,就引入了一个叫做Scanner的类,但这个类被视为一个工具类,因为它不是一种流式处理,所以在JDK中,它被放到了java.util包下了。使用Scanner,代码可以化简如下:
public class TestIO {
public static void main(String[] args) throws IOException {
Scanner cin = new Scanner(System.in);
int a = cin.nextInt();
int b = cin.nextInt();
System.out.println(a + b);
}
}