3.7 输入和输出
为了使示例程序更有趣,我们希望接受输入并正确格式化程序输出。当然,现代程序使用GUI来收集用户输入。然而,对这样一个接口进行编程需要的工具和技术比我们目前所能使用的要多。我们的第一个任务是对Java编程语言更熟悉,因此我们使用谦逊的控制台进行输入和输出。
3.7.1 读取输入
您看到,只需调用System.out.println
,就可以轻松地将输出打印到“标准输出流”(即控制台窗口)。从“Standard input stream”系统中读取并不是那么简单。要读取控制台输入,首先构造一个连接到System.in的Scanner:
Scanner in = new Scanner(System.in);
(我们将在第4章中详细讨论构造函数和新的操作符。)现在您可以使用Scanner类的各种方法来读取输入。例如,nextLine方法读取一行输入。
System.out.print("What is your name? ");
String name = in.nextLine();
这里,我们使用nextLine方法,因为输入可能包含空格。要读取单个单词(用空格分隔),请调用
String firstName = in.next();
要读取整数,请使用nextInt方法。
System.out.print("How old are you? ");
int age = in.nextInt();
类似地,nextDouble方法读取下一个浮点数。
清单3.2中的程序询问用户的姓名和年龄,然后打印一条类似
Hello, Cay. Next year, you'll be 57
最后注意行
import java.util.*;
在程序开始的地方。Scanner类在java.util包中定义。每当使用基本java.lang包中未定义的类时,都需要使用import指令。我们将在第4章中更详细地研究包和导入指令。
清单3.2 InputTest/InputTest.java
import java.util.*;
/**
* This program demonstrates console input.
*
* @version 1.10 2004-02-10
* @author Cay Horstmann
*/
public class InputTest {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// get first input
System.out.print("What is your name? ");
String name = in.nextLine();
// get second input
System.out.print("How old are you? ");
int age = in.nextInt();
// display output on console
System.out.println("Hello, " + name + ". Next year, you'll be " + age);
}
}
注意
Scanner类不适合从控制台读取密码,因为任何人都可以清楚地看到输入。Java 6为这个目的专门介绍了一个Console类。要读取密码,请使用以下代码:
Console cons = System.console(); String username = cons.readLine("User name: "); char[] passwd = cons.readPassword("Password: ");
出于安全原因,密码以字符数组而不是字符串的形式返回。处理完密码后,应立即用填充值覆盖数组元素。(第108页第3.10节“阵列”中讨论了阵列处理。)
Console对象的输入处理不如Scanner方便。您必须一次读取一行输入。没有阅读单个单词或数字的方法。
java.util.Scanner 5
- Scanner(InputStream in)
从被给的输入流中构造一个Scanner对象 - String nextLine()
读取输入流的下一行 - String next()
读取下一个输入的单词(用空格分隔) - int nextInt()
- double nextDouble()
读取并且转化下一个用来表示一个整形或者浮点型数字的字符序列 - boolean hasNext()
测试在输入中是否有另一个单词 - boolean hasNextInt()
- boolean hasNextDouble
测试是否下一个字符序列是整形或者浮点型
java.lang.System 1.0
- static Console console() 6
返回一个Console对象,用于通过控制台窗口与用户交互(如果可能的话),否则为空。控制台对象可用于在控制台窗口中启动的任何程序。否则,可用性取决于系统。
java.io.Console 6
- static char[] readPassword(String prompt, Object… args)
- static String readLine(String prompt, Object… args)
显示提示并读取用户输入,直到输入行结束。args参数可用于提供格式化参数,如下一节所述。
3.7.2 格式化输出
您可以使用语句System.out.print(x)将数字x打印到控制台。该命令将以该类型的最大非零位数打印X。例如,
double x = 10000.0 / 3.0;
System.out.print(x);
打印出
3333.3333333333335
这是一个问题,如果你想显示,例如,美元和美分。
在早期版本的Java中,格式化数字有点麻烦。幸运的是,Java 5从C库中带回了可尊敬的printf
方法。例如,调用
System.out.printf("%8.2f", x);
打印x,字段宽度为8个字符,精度为2个字符。也就是说,打印输出包含一个前导空格和七个字符
3333.33
可以向printf提供多个参数。例如:
System.out.printf("Hello, %s. Next year, you'll be %d", name, age);
以%字符开头的每个格式说明符都将替换为相应的参数。格式说明符结尾的转换字符指示要格式化的值的类型:f是浮点数字,s是字符串,d是十进制整数。表3.5显示了所有转换字符。
表3.5 printf转换
转换字符 | 类型 | 例子 |
---|---|---|
d | 十进制整数 | 159 |
x | 十六进制整数 | 9f |
o | 八进制整数 | 237 |
f | 固定浮点的浮点数 | 15.9 |
e | 科学计数法浮点数 | 1.59e+01 |
g | 一般的浮点数(比e和f更短) | - |
a | 十六进制浮点数 | 0x1.fccdp3 |
s | 字符串 | Hello |
c | 字符 | H |
b | 布尔 | true |
h | 哈希代码 | 42628b2 |
tx或Tx | 日期和时间(T强制大写) | 废除,使用java.time类代替,参见卷二第六章 |
% | 百分号标记 | % |
n | 平台相关的行分隔符 | - |
此外,还可以指定控制格式化输出外观的标志。表3.6显示了所有标志。例如,逗号标志添加了组分隔符。也就是说,
表3.6 printf的标志
标志 | 目的 | 例子 |
---|---|---|
+ | 打印正数和负数的符号 | +3333.33 |
空格 | 在正数前面添加一个空格 | | 3333.33| |
0 | 添加前导0 | 003333.33 |
- | 左调整域 | |3333.33| |
( | 将负数括在括号中 | (3333.33) |
, | 添加组分隔符 | 3,333.33 |
#(和f一起使用,包含一个浮点格式) | 3,333. | |
#(和x或o一起使用,添加0x或者0前缀) | 0xcafe | |
$ | 指定要格式化的参数的索引;例如,%1$d %1$x 以十进制和十六进制打印第一个参数。 | 159 9F |
< | 格式化与上一个规范相同的值;例如,%d %<x 以十进制和十六进制打印相同的数字。 | 159 9F |
System.out.printf("%,.2f", 10000.0 / 3.0);
打印出
3,333.33
可以使用多个标志,例如“%,(.2f”使用组分隔符并将负数括在括号中。
注意
您可以使用s转换来格式化任意对象。如果任意对象实现了Formattable接口,则调用对象的formatTo方法。否则,将调用ToString方法将对象转换为字符串。我们讨论了第5章中的toString方法和第6章中的接口。
可以使用静态的String.format方法创建格式化字符串而不打印:
String message = String.format("Hello, %s. Next year, you'll be %d", name, age);
出于完整性的考虑,我们简要讨论了printf方法的日期和时间格式选项。对于新代码,应该使用第二卷第6章中描述的java.time包的方法。但在旧代码中可能会遇到日期类和相关的格式选项。格式由两个字母组成,以t开头,以表3.7中的一个字母结尾;例如,
表3.7 日期和时间转化字符
转化字符 | 类型 | 例子 |
---|---|---|
c | 完整的日期和时间 | Mon Feb 09 18:05:19 PST |
2015 | ||
F | ISO 8601日期 | 2015-02-09 |
D | 美国格式日期 | 02/09/2015 |
T | 24小时时间 | 18:05:19 |
r | 12小时时间 | 06:05:19 pm |
R | 24小时时间,没有秒 | 18:05 |
Y | 四位数年份(有前导0) | 2015 |
y | 年份的最后两位(有前导0) | 15 |
C | 年的前两位(有前导0) | 20 |
B | 完整的月份名字 | February |
b或者h | 月份名字的简写 | Feb |
m | 两位数月份(有前导0) | 02 |
d | 两位数日期(有前导0) | 09 |
e | 两位数日期(没有前导0) | 9 |
A | 完整星期名 | Monday |
a | 星期名的简写 | Mon |
j | 三位数表示这一年的第几天(有前导0),从001到366 | 069 |
H | 两位数小时(有前导0),从00到23 | 18 |
k | 两位数小时(没有前导0),从0到23 | 18 |
I(大写i) | 两位数小时(有前导0),从01到12 | 06 |
l(小写L) | 两位数小时(没有前导0),从1到12 | 6 |
M | 两位数分钟(有前导0) | 05 |
S | 两位数秒(有前导0) | 19 |
L | 三位数毫秒(有前导0) | 047 |
N | 九位数纳秒(有前导0) | 047000000 |
p | 上午或下午标志 | pm |
z | RFC 822,距离GMT的数字偏离 | -0800 |
Z | 时区 | PST |
s | 从1970-01-01 00:00:00开始的秒数 | 1078884319 |
Q | 从1970-01-01 00:00:00开始的毫秒数 | 1078884319047 |
System.out.printf("%tc", new Date());
以以下格式打印出当前日期和时间
Mon Feb 09 18:05:19 PST 2015
如表3.7所示,某些格式只生成给定日期的一部分,例如,仅生成日期或月份。如果你必须多次提供日期来格式化每个部分,那就有点傻了。因此,格式字符串可以指示要格式化的参数的索引。索引必须紧跟在%之后,并且必须以$终止。例如,
System.out.printf("%1$s %2$tB %2$te, %2$tY", "Due date:", new Date());
打印出
Due date: February 9, 2015
或者,可以使用<标志。它指示应再次使用与前面格式规范中相同的参数。也就是说,声明
System.out.printf("%s %tB %<te, %<tY", "Due date:", new Date());
生成与前面语句相同的输出。
小心
参数序号值从1开始,而不是0:%1$…格式化第一个参数。这与0标志不同,容易引起混乱
您现在已经看到了printf方法的所有特性。图3.6显示了格式说明符的语法图。
图3.6 格式化特定语法
注意
数字和日期的格式是特定于区域设置的。例如,在德国,组分隔符是句点,而不是逗号,星期一的格式是蒙太格。第二卷第7章介绍了如何控制您的申请的国际行为。
3.7.3 文件输入和输出
为了从文件读取,像如下构造一个Scanner
对象:
Scanner in = new Scanner(Path.of("myfile.txt"), StandardCharsets.UTF_8);
如果文件名包含反斜杠,请记住用附加的反斜杠转义每个文件名:“c:\\mydirectory\\myfile.txt”。
注意
这里,我们指定UTF-8字符编码,这是Internet上文件的通用(但不是通用)编码。阅读文本文件时,您需要知道字符编码(有关详细信息,请参阅第二卷第2章)。如果省略字符编码,则使用运行Java程序的计算机的“默认编码”。这不是一个好主意,程序可能会根据运行位置的不同而采取不同的行动。
现在,您可以使用我们已经描述过的任何Scanner方法来读取文件。
要写入文件,请构造一个PrintWriter对象。在构造函数中,提供文件名和字符编码:
PrintWriter out = new PrintWriter("myfile.txt", StandardCharsets.UTF_8);
如果文件不存在,则创建该文件。可以像打印到System.out时那样使用print、println和printf命令。
小心
您可以用字符串参数构造一个Scanner,但scanner将字符串解释为数据,而不是文件名。例如,如果你调用
Scanner in = new Scanner("myfile.txt"); // ERROR?
然后扫描器将看到10个字符的数据:“m”、“y”、“f”等。这可能不是本例的初衷。
注意
当指定一个相对的文件名时,例如“myfile.txt”、“mydirectory/myfile.txt”或“…/myfile.txt”,文件相对于Java虚拟机启动的目录而定位。如果从命令shell启动程序,通过执行
java MyProg
那么起始目录就是命令shell的当前目录。但是,如果使用集成开发环境,它将控制起始目录。您可以使用此调用找到目录位置:
String dir = System.getProperty("user.dir");
如果您在查找文件时遇到困难,请考虑使用绝对路径名,如“c:\\mydirectory\\myfile.txt”或“/home/me/mydirectory/myfile.txt”。
如您所见,您可以像使用System.in和System.out一样轻松地访问文件。只有一个陷阱:如果使用不存在的文件构建Scanner,或用无法创建的文件名构造PrintWriter,则会发生异常。Java编译器认为这些异常比“除零”异常更严重。在第7章中,您将学习处理异常的各种方法。现在,您应该简单地告诉编译器您知道“输入/输出”异常的可能性。为此,您可以使用throws子句标记主方法,如下所示:
public static void main(String[] args) throws IOException
{
Scanner in = new Scanner(Path.of("myfile.txt"), StandardCharsets.UTF_8);
. . .
}
现在您已经了解了如何读写包含文本数据的文件。对于更高级的主题,如处理不同的字符编码、处理二进制数据、读取目录和写入zip文件,请参阅第二卷第2章。
注意
从命令shell启动程序时,可以使用shell的重定向语法并将任何文件附加到System.in和System.out:
java MyProg < myfile.txt > output.txt
那么,您不必担心如何处理IOException。
java.util.Scanner 5
- Scanner(Path p, String encoding)
构造一个Scanner,它从给定的路径中读取数据,使用给定的字符编码 - Scanner(String data)
构造一个Scanner,它从给定的字符串读取数据
java.io.PrintWriter 1.1
- PrintWriter(String fileName)
构造一个PrintWriter,它将数据写到给定的文件名
java.nio.file.Path
- static Path of(String pathname) 11
通过给定的路径名构造一个Path