重新学习Java——Java基本的程序设计结构(二)
上一节简单回顾了Java基本的一些程序设计的知识,这一节将继续根据《Java核心技术》这本书,进行这方面知识的复习与探索。
1. 字符串
Java字符串实际上就是Unicode字符序列。例如,串“Java\u2122”由5个Unicode字符J、a、v、a和™。Java没有内置的字符串类型,而是在标准的Java类库中提供了一个预定义类String。每一个用双引号括起来的字符串都是String类的一个实例。对于String字符串的操作,也是Java中最重要的几个基础部分之一。
1.1 子串
String类的substring方法可以从一个较大的字符串提取出一个子串。
String greeting = "Hello"; String s = greeting.substring(0, 3); // 创建了一个由字符“Hel”组成的字符串,此时greeting变量的值仍不变
substring方法的第一个参数是想复制的第一个位置,第二个参数是不想复制的第一个位置。substring的工作方式有一个优点,容易计算子串的长度,s.substring(a, b)的长度为b - a。例如,子串“Hel”的长度为3 - 0 = 3。
1.2 拼接
与大多数的程序设计语言一样,Java语言允许使用+号连接两个字符串。单词之间没有空格。当一个字符串与一个非字符串的值进行拼接的时候,后者被转换成字符串。
int age = 13; String rating = "PG" + age; // 此时rating="PG13"
1.3 不可变字符串
String类没有提供用于修改字符串的方法。如果希望将greeting("Hello")的内容修改为"Help!",不能直接的将最后两个位置修改,在Java中须先提取出需要的字符,再进行拼接。
greeting = greeting.substring(0, 3) + "p!";
由于不能够修改Java字符串中的字符,所以在Java文档中,将String类对象成为不可变字符串。“Hello”永远包含字符H,e,l,l和o的代码单元序列,而不能修改其中的任何一个字符。当然,可以修改字符串变量greeting,让它引用另外一个字符串。
Java编译器在处理字符串时,是通过共享的方式来操作的。可以想象,将各种字符串存放在公共的存储池中,字符串变量指向存储池中相应的位置。如果复制一个字符串变量,原始字符串与复制的字符串共享相同的字符。
Java的设计者认为共享带来的高效率胜过提取、拼接字符串带来的低效率。在程序中,很少有需要修改字符串,而是往往需要对字符串进行比较(有一种例外情况,将源自于文件或者键盘的单个字符或较短的字符串汇集成字符串,为此Java提供了一个单独的类)。
1.4 检测字符串是否相等
可以使用equals方法检测两个字符串(字符串变量或字符串常量)是否相等,相等则返回true,否则返回false;使用equalsIgnoreCase方法不区分大小写来检测字符串是否相等。一定不能使用==检测两个字符串是否相等,因为这只能确定两个字符串是否放置在同一位置。如果JVM始终将相同的字符串共享,就可以使用==检测是否相等。但实际上只有字符串常量是共享的,而+或substring的结果不共享,因此不要使用==比较。
1.5 代码点和代码单元(没有弄明白什么意思)
1.6 构建字符串
如果需要用许多小段的字符串构建一个字符串,那么应该按照下面的步骤进行。
// 构造一个空的字符串构造器 StringBuilder builder = new StringBuilder(); // 当每次需要添加一部分内容时,调用append方法 builder.append(ch); builder.append(str); // 在需要构建字符串时就调用toString方法,可以得到一个String对象 String completedString = builder.toString();
在JDK5.0中引入了StringBuilder类。这个类的前身是StringBuffer,效率有些低,但允许多线程执行。如果在单线程中操作字符串,应该使用StringBuilder。
2. 输入输出
2.1 读取输入
我们已经知道,打印到“标准输出流”(即控制台窗口)是一个很简单的事情,只需调用System.out.println即可。然而读取System.in就不简单了。
// 首先要构造一个Scanner对象,并与System.in关联。 Scanner in = new Scannar(System.in); System.out.println("What is your name?"); String name = in.nextLine(); // 输入一行 String firstName = in.next(); // 读一个单词,以空白符分隔 int age = in.nextInt(); // 读取一个整数 double money = in.nextDouble(); // 读取一个浮点数
由于输入是可见的,所以Scanner类不适合用于从控制台读取密码。Java SE 6特别引入了Console类实现这个功能。
Console cons = System.console(); String username = cons.readLine("User name: "); char[] passwd = cons.readPassword("Password: "); // 为了安全,返回的密码放在一维字符串数组中,对密码进行处理后,应该马上用一个填充值覆盖数组元素 // 采用Console对象处理不如Scanner方便,每次只能读取一行
2.2 格式化输出
使用System.out.print(x)将x打印到控制台,将以x对应的数据类型所允许的最大非0数字为数打印x。Java沿用了C语言库函数中的printf方法尽心改格式化输出。
double x = 10000.0 / 3.0; System.out.printf("%8.2f", x); // 输出: 3333.33(数字前一个空格) String name = "Jack"; int age = 18; System.out.printf("Hello, %s. Next year, you'll be %d", name, age); // 输出:Hello, Jack. Next year, you'll be 18
上面的代码中,第一个程序,打印了一个8个字符宽度和小数点后两位字符的浮点型x;第二个程序的第一个s代表字符串,d表示十进制整数,在printf句型中,%号后的类型将依次替换成之后的类型。所以第一个%s被name变量替换,%d被age替换。
转 换 符 | 类 型 | 举 例 | 转 换 符 | 类 型 | 举 例 | |
d | 十进制整数 | 159 | s | 字符串 | Hello | |
x | 十六进制整数 | 9f | c | 字符 | H | |
o | 八进制整数 | 237 | b | 布尔 | True | |
f | 定点浮点数 | 15.9 | h | 散列码 | 42628b2 | |
e | 指数浮点数 | 1.59e+01 | tx | 日期时间 | 见后表 | |
g | 通用浮点数 | — | % | 百分号 | % | |
a | 十六进制浮点数 | 0x1.fccdp3 | n | 与平台有关的行分隔符 | — |
还可以给出控制格式化输出的各种标志,例如,逗号表示增加了分组的分隔符。可以使用多个标志,例如,“%,(.2f”使用分组的分隔符并将负数括在括号内。
System.out.printf("%,.2f", 10000.0 / 3.0); // 输出:3,333.33
标 志 | 目 的 | 举 例 |
+ | 打印正数和负数的符号 | +3333.33 |
空格 | 在正数之前添加空格 | | 3333.33| |
0 | 数字前面补0 | 003333.33 |
- | 左对齐 | |3333.33 | |
( | 将负数括在括号内 | (3333.33) |
, | 添加分组分隔符 | 3,333.33 |
#(对于f格式) | 包含小数点 | 3,333 |
#(对于x或0格式) | 添加前缀0x或0 | 0xcafe |
$ | 给定被格式化的参数索引。例如%1$d,%1$x将以十进制和十六进制格式打印第1个参数 | 159 9F |
< | 格式化前面说明的数值。例如,%d%<x以十进制和十六进制打印同一个数值 | 159 9F |
可以使用s转换符格式化任意的对象。对于任意实现了Formattable接口的对象都将调用fotmatTo方法;否则将调用toString方法,它可以将对象转换为字符串。可以使用静态的String.format方法传建一个格式化的字符串,而不打印输出。
printf方法可以打印时间格式,以t开始,以表中人以字母结束的两个字母格式。
System.out.printf("%tc", new Date()); // 星期日 九月 22 13:22:35 CST 2013
转 换 符 | 类 型 | 举 例 |
c | 完整的日期和时间 | 星期日 九月 22 13:31:11 CST 2013 |
F | IOS 8601 日期 | 2013-09-22 |
D | 美国格式的日期(月/日/年) | 09/22/13 |
T | 24小时时间 | 13:31:46 |
r | 12小时时间 | 01:31:56 下午 |
R | 24小时时间没有秒 | 13:33 |
Y | 4位数字的年(前面补0) | 2013 |
y | 年的后两位数字(前面补0) | 13 |
C | 年的前两年数字(前面补0) | 20 |
B | 月的完整拼写 | 九月(September) |
b或h | 月的缩写 | 九月(Sept) |
m | 两位数字的月(前面补0) | 09 |
d | 两位数字的日(前面补0) | 22 |
e | 两位数字的日(前面不补0) | 22 |
A | 星期几的完整拼写 | 星期日(Sunday) |
a | 星期几的缩写 | 星期日(Sun) |
j | 三位数的年中的日子(前面补0),在001到366之间 | 265 |
H | 两位数字的小时(前面补0),在0到23之间 | 13 |
k | 两位数字的小时(前面不补0),在0到23之间 | 13 |
I | 两位数字的小时(前面补0),在0到12之间 | 01 |
l | 两位数字的小时(前面不补0),在0到12之间 | 1 |
M | 两位数字的分钟(前面补0) | 37 |
S | 两位数字的秒(前面补0) | 50 |
L | 三位数字的毫秒(前面补0) | 346 |
N | 九位数字的毫微秒(前面补0) | 032000000 |
P | 上午或下午的大写标志 | PM |
p | 上午或下午的小写标志 | pm |
z | 从GMT起,RFC822数字位移 | +0800 |
Z | 时区 | CST |
s | 从格林威治时间1970-01-01 00:00:00起的秒数 | 1379828369 |
Q | 从格林威治时间1970-01-01 00:00:00起的毫秒数 | 1379828377563 |
从上表可以看出,某些个事只给出指定日期的部分信息,如果需要多次对日期操作才能实现一部分操作的目的就太笨拙了,为此可以采用一个格式化字符串之处被格式化的参数索引。索引必须紧跟在%后面,以$终止。索引从1开始,而非0。
System.out.printf("%1$s %2$tB %2$te, %2$tY", "Due date:", new Date()); // Due date: 九月 22, 2013
也可以选择使用<标志,它指示前面格式说明中的参数将被再次使用。
System.out.printf("%s %tB %<te, %<tY", "Due date:", new Date()); // Due date: 九月 22, 2013
2.3 文件输入与输出
Scanner in = new Scanner(new File("myfile.txt")); // 要想对文件进行读取,就需要用一个File对象构造一个Scanner对象,如果含有反斜杠文件名应该再加一个反斜杠 PrintWriter out = new PrintWriter("myfile.txt"): // 进行文件的写入
3. 控制流程
3.1 块作用域
块是指由一对花括号括起来的若干Java语句,块确定了变量的作用域。一个块可以嵌套在另一个块里面。但是不能再嵌套的两个块中声明相同的变量,无法通过编译。但是C++中可以,后声明的变量会覆盖之前的,但是Java不允许,有可能导致程序设计错误。
3.2 条件语句
在Java中,条件语句的格式为if (condition) statement1 else statement2。其中else是可选的,else与最近邻的if构成一组。
3.3 循环
当条件为true时,while循环执行。如果开始条件为false,则一次也不执行,结构为:while (condition) statement。如果希望循环至少执行一次,使用do/while语句可以实现这种操作,结构为:do statement while (condition)。
3.4 确定循环
就是传统的for循环结构,尽管Java允许for循环内部放置任何表达式,但有一个不成文的规定:for语句的3个部分应该对同一个计数器变量进行初始化,检测和更新,否则会让人觉得晦涩难懂。检测浮点数时有可能因为二进制的问题导致永远不能准确的达到停止条件因而失败。
3.5 多重选择:switch语句
在处理多个选项时,使用多重if/else结构有点笨拙且不清晰。可以使用switch语句建立一个这样的结构。
switch (choice) { case 1: ... break; case 2: ... break; case 3: ... break; default: ... break; }
没有相应的case语句,就执行default,如果case语句没有break,那么就会直接执行下一个case语句。
3.6 中断控制流程语句
Java中尽管goto语句是保留字,但没有功能。Java中使用带标签的break语句来跳出循环。
read_data: int i = 1; while (true) { if (i == 100) break read_data; // 当i等于100则跳出到指定位置的语句块末端 i++; }
单独使用break,可以跳出当前的块,还有一个continue,中断了正常的本次循环,并转移到最内层循环的首部,还有一种带标签的continue,调到与标签匹配的循环首部。
4. 大数值
如果基本的整数和浮点数精度不能满足要求,可以使用java.math包中两个很有用的类:BigInteger和BigDecimal。可以处理任意长度的数字序列的数值。
BigInteger a = BigInteger.valueOf(100); // 可以将数值转换为大数值
但不能使用熟悉的算术运算符处理大数值,需要使用对象自己的方法,add和multiply等。因为Java没有提供运算符重载的功能,但是Java设计者为字符串的连接重载了+运算符。
// 需要计算的语句 lotteryOdds = lotteryOdds * (n - i + 1) / i; // 如果使用大数值 lotteryOdds = lotteryOdds.multiply(BigInteger.valueOf(n - i + 1).divide(BigInteger.valueOf(i));
5. 数组
数组是一种数据结构,用来存储同一类型值的集合,通过下标可以访问数组中的每一个值。
int[] a; // 声明数组是,要指出数组类型和变量的名字,也可以使用int a[];不过大部分人喜欢前一种风格 int[] a = new int[100]; // 声明并且初始化了一个数组,该语句创造了一个可以存储100个整数的数组
数组下标从0开始,如果创建了100个元素,并访问a[100],则会抛出异常(正常只能访问0-99)。可以通过arratName.length获得数组长度。
5.1 For each循环
Java SE 5.0增加了这种循环,依次处理数组中每个元素(其他类型的元素集合亦可)而不担心下标。定义为格式:for (variable : collection) statement。定义一个变量用于暂存集合中的每一个元素,操作的对象必须是一个数组或实现了Iterable接口的对象。
如果只是想打印数组中的每个值,可以使用Arrays.toString(s)方法,打印。
5.2 数组初始化以及匿名数组
int[] smallPrimes = {1, 2, 3}; // 提供了简单的创建数组并同时赋值的方法 new int[] {1, 2, 3, 4}; // 匿名数组的声明
在Java中允许数组长度为0。
5.3 数组拷贝
Java中,允许将一个数组变量拷贝给另一个数组变量。这是,两个变量将引用同一个数组。如果希望将一个数组的所有值拷贝到一个新的数组中去,就要使用Arrays的copyOf方法。如果数组是数值类型,多余的元素被赋值为0,布尔型赋值为false,如果长度小于原长度,则只拷贝最前面的元素。
5.4 命令行参数
每一个Java都有自带的String[] args参数的main方法,表示main接收字符串数组,也就是命令行参数。
如果使用java Message -g cruel world; args数组将包含{"-g", "cruel", "world"}。
5.5 数组排序
相对数组排序,可以使用Arrays类中的sort方法,这个方法使用了快速排序算法。
5.6 多维数组
多维数组将使用多个下表访问数组。如double[][] balances = new double[a][b];for each循环不能自动处理二维数组,必须使用两个嵌套的循环。想要快速打印二维数组,可以使用:System.out.println(Arrays.deepToString(a));
5.7 不规则数组
Java实际上没有多维数组,只有一维数组,多维数组被解释为“数组的数组”;因此每一行都是一个单独的数组,可以构造不规则数组。