流程控制
在Java程序中,JVM默认总是顺序执行以分号;结束的语句。但是,在实际的代码中,程序经常需要做条件判断、循环,因此,需要有多种流程控制语句,来实现程序的跳转和循环等功能。
将介绍if条件判断、switch多重选择和各种循环语句。
输入和输出
输出
在前面的代码中,我们总是使用
System.out.println()
来向屏幕输出一些内容。println
是print line
的缩写,表示输出并换行。因此,如果输出后不想换行,可以用print()
:
public class Main {
public static void main(String[] args) {
System.out.print("A,");
System.out.print("B,");
System.out.print("C.");
System.out.println();
System.out.println("END");
}
}
输出结果如下:
:::success
A,B,C.
END
:::
注意观察上述代码的执行效果。
格式化输出
Java还提供了格式化输出的功能。为什么要格式化输出?因为计算机表示的数据不一定适合人来阅读:
public class Main {
public static void main(String[] args) {
double d = 12900000;
System.out.println(d); // 1.29E7
}
}
输出结果如下:
:::success
1.29E7
:::
如果要把数据显示成我们期望的格式,就需要使用格式化输出的功能。格式化输出使用System.out.printf()
,通过使用占位符%?
,printf()
可以把后面的参数格式化成指定格式:
public class Main {
public static void main(String[] args) {
double d = 3.1415926;
System.out.printf("%.2f\n", d); // 显示两位小数3.14
System.out.printf("%.4f\n", d); // 显示4位小数3.1416
}
}
输出结果如下:
:::success
3.14
3,1416
:::
Java的格式化功能提供了多种占位符,可以把各种数据类型“格式化”成指定的字符串:
占位符 | 说明 |
---|---|
%d | 格式化输出整数 |
%x | 格式化输出十六进制整数 |
%f | 格式化输出浮点数 |
%e | 格式化输出科学计数法表示的浮点数 |
%s | 格式化字符串 |
注意,由于%表示占位符,因此,连续两个%%表示一个%字符本身。
占位符本身还可以有更详细的格式化参数。下面的例子把一个整数格式化成十六进制,并用0补足8位:
public class Main {
public static void main(String[] args) {
int n = 12345000;
System.out.printf("n=%d, hex=%08x", n, n); // 注意,两个%占位符必须传入两个数
}
}
输出结果如下:
:::success n=12345000, hex=00bc53a8 ::: 详细的格式化参数请参考JDK文档java.util.Formatter
输入
和输出相比,Java的输入就要复杂得多。
我们先看一个从控制台读取一个字符串和一个整数的例子:
:::info
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in); // 创建Scanner对象
System.out.print(“Input your name: “); // 打印提示
String name = scanner.nextLine(); // 读取一行输入并获取字符串 _System.out.print(“Input your age: “); // 打印提示
int age = scanner.nextInt(); // 读取一行输入并获取整数
System.out.printf(“Hi, %s, you are %d\n”, name, age); // 格式化输出_
}
}
:::
首先,我们通过
import
语句导入java.util.Scanner
,import
是导入某个类的语句,必须放到Java源代码的开头,后面我们在Java的package
中会详细讲解如何使用import
。然后,创建
Scanner
对象并传入System.in
。System.out
代表标准输出流,而System.in
代表标准输入流。直接使用System.in
读取用户输入虽然是可以的,但需要更复杂的代码,而通过Scanner
就可以简化后续的代码。 有了Scanner
对象后,要读取用户输入的字符串,使用scanner.nextLine()
,要读取用户输入的整数,使用scanner.nextInt()
。Scanner
会自动转换数据类型,因此不必手动转换。要测试输入,我们不能在线运行它,因为输入必须从命令行读取,因此,需要走编译、执行的流程:
:::info $ javac Main.java :::
这个程序编译时如果有警告,可以暂时忽略它,在后面学习IO的时候再详细解释。
编译成功后,执行:
:::info
$ java Main
Input your name:
Bob Input your age: 12
Hi, Bob, you are 12
:::
根据提示分别输入一个字符串和整数后,我们得到了格式化的输出。
if判断
在Java程序中,如果要根据条件来决定是否执行某一段代码,就需要if语句。
if
语句的基本语法是:
:::info
if (条件) {
// 条件满足时执行
}
:::
根据if的计算结果(
true
还是false
),JVM决定是否执行if
语句块(即花括号{}
包含的所有语句)。
让我们来看一个例子:
public class Main {
public static void main(String[] args) {
int n = 70;
if (n >= 60) {
System.out.println("及格了");
}
System.out.println("END");
}
}
输出结果如下:
:::success
及格了
END
:::
当条件
n >= 60
计算结果为true
时,if
语句块被执行,将打印”及格了”,否则,if语句块将被跳过。修改n
的值可以看到执行效果。注意到if语句包含的块可以包含多条语句:
public class Main {
public static void main(String[] args) {
int n = 70;
if (n >= 60) {
System.out.println("及格了");
System.out.println("恭喜你");
}
System.out.println("END");
}
}
输出结果如下:
:::success
及格了
恭喜你
END
:::
当if语句块只有一行语句时,可以省略花括号{}:
public class Main {
public static void main(String[] args) {
int n = 70;
if (n >= 60)
System.out.println("及格了");
System.out.println("END");
}
}
输出结果如下:
:::success
及格了
END
:::
但是,省略花括号并不总是一个好主意。 假设某个时候,突然想给if语句块增加一条语句时:
public class Main {
public static void main(String[] args) {
int n = 50;
if (n >= 60)
System.out.println("及格了");
System.out.println("恭喜你"); // 注意这条语句不是if语句块的一部分
System.out.println("END");
}
}
输出结果如下:
:::success
恭喜你
END
:::
由于使用缩进格式,很容易把两行语句都看成
if
语句的执行块,但实际上只有第一行语句是if
的执行块。在使用git这些版本控制系统自动合并时更容易出问题,所以不推荐忽略花括号的写法。
else
if
语句还可以编写一个else { ... }
,当条件判断为false
时,将执行else
的语句块:
public class Main {
public static void main(String[] args) {
int n = 70;
if (n >= 60) {
System.out.println("及格了");
} else {
System.out.println("挂科了");
}
System.out.println("END");
}
}
输出结果如下:
:::success
及格了
END
:::
修改上述代码n的值,观察
if
条件为true
或false
时,程序执行的语句块。 注意,else
不是必须的。 还可以用多个if ... else if ...
串联。
例如:
public class Main {
public static void main(String[] args) {
int n = 70;
if (n >= 90) {
System.out.println("优秀");
} else if (n >= 60) {
System.out.println("及格了");
} else {
System.out.println("挂科了");
}
System.out.println("END");
}
}
输出结果如下:
:::success
及格了
END
:::
串联的效果其实相当于:
:::info
if (n >= 90) {
// n >= 90为true:
System.out.println(“优秀”);
} else { // n >= 90为false:
if (n >= 60) {
// n >= 60为true:
System.out.println(“及格了”);
} else {
// n >= 60为false:
System.out.println(“挂科了”);
}
}
:::
在串联使用多个
if
时,要特别注意判断顺序。观察下面的代码:
public class Main {
public static void main(String[] args) {
int n = 100;
if (n >= 60) {
System.out.println("及格了");
} else if (n >= 90) {
System.out.println("优秀");
} else {
System.out.println("挂科了");
}
}
}
输出结果如下:
:::success 及格了 :::
执行发现,
n = 100
时,满足条件n >= 90
,但输出的不是"优秀"
,而是"及格了"
,原因是if
语句从上到下执行时,先判断n >= 60
成功后,后续else
不再执行,因此,if (n >= 90)
没有机会执行了。
正确的方式是按照判断范围从大到小依次判断:
:::info
// 从大到小依次判断:
if (n >= 90) {
// …
} else if (n >= 60) {
// …
} else {
// …
}
:::
或者改写成从小到大依次判断:
:::info
// 从小到大依次判断:
if (n < 60) {
// …
} else if (n < 90) {
// …
} else {
// …
}
:::
使用
if
时,还要特别注意边界条件。
例如:
public class Main {
public static void main(String[] args) {
int n = 90;
if (n > 90) {
System.out.println("优秀");
} else if (n >= 60) {
System.out.println("及格了");
} else {
System.out.println("挂科了");
}
}
}
输出结果如下:
:::success 及格了 :::
假设我们期望90分或更高为
“优秀”
,上述代码输出的却是“及格”
,原因是>
和>=
效果是不同的。 前面讲过了浮点数在计算机中常常无法精确表示,并且计算可能出现误差,因此,判断浮点数相等用==
判断不靠谱:
public class Main {
public static void main(String[] args) {
double x = 1 - 9.0 / 10;
if (x == 0.1) {
System.out.println("x is 0.1");
} else {
System.out.println("x is NOT 0.1");
}
}
}
输出结果如下
:::success
:::
正确的方法是利用差值小于某个临界值来判断:
public class Main {
public static void main(String[] args) {
double x = 1 - 9.0 / 10;
if (Math.abs(x - 0.1) < 0.00001) {
System.out.println("x is 0.1");
} else {
System.out.println("x is NOT 0.1");
}
}
}
输出结果如下:
判断引用类型相等
在Java中,判断值类型的变量是否相等,可以使用
==
运算符。但是,判断引用类型的变量是否相等,==
表示“引用是否相等”,或者说,是否指向同一个对象。例如,下面的两个String
类型,它们的内容是相同的,但是,分别指向不同的对象,用==
判断,结果为false
:
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "HELLO".toLowerCase();
System.out.println(s1);
System.out.println(s2);
if (s1 == s2) {
System.out.println("s1 == s2");
} else {
System.out.println("s1 != s2");
}
}
}
输出结果如下:
:::success
hello
hello
s1 != s2
:::
要判断引用类型的变量内容是否相等,必须使用
equals()
方法:
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "HELLO".toLowerCase();
System.out.println(s1);
System.out.println(s2);
if (s1.equals(s2)) {
System.out.println("s1 equals s2");
} else {
System.out.println("s1 not equals s2");
}
}
}
输出结果如下:
:::success
hello
hello
s1 equals s2
:::
注意:执行语句
s1.equals(s2)
时,如果变量s1
为null
,会报NullPointerException
:
public class Main {
public static void main(String[] args) {
String s1 = null;
if (s1.equals("hello")) {
System.out.println("hello");
}
}
}
输出结果如下:
:::success
:::
要避免
NullPointerException
错误,可以利用短路运算符&&
:
public class Main {
public static void main(String[] args) {
String s1 = null;
if (s1 != null && s1.equals("hello")) {
System.out.println("hello");
}
}
}
输出结果如下:
:::success
:::
还可以把一定不是
null
的对象"hello"
放到前面: 例如:if ("hello".equals(s)) { ... }
。
switch多重选择
除了if语句外,还有一种条件判断,是根据某个表达式的结果,分别去执行不同的分支。 例如,在游戏中,让用户选择选项:
- 单人模式
- 多人模式
- 退出游戏
这时,
_switch_
语句就派上用场了。 switch case 语句有如下规则:
- switch 语句中的变量类型可以是: byte、short、int 或者 char。从 Java SE 7 开始,switch 支持字符串 String 类型了,同时 case 标签必须为字符串常量或字面量。
- switch 语句可以拥有多个 case 语句。每个 case 后面跟一个要比较的值和冒号。
- case 语句中的值的数据类型必须与变量的数据类型相同,而且只能是常量或者字面常量。
- 当变量的值与 case 语句的值相等时,那么 case 语句之后的语句开始执行,直到 break 语句出现才会跳出 switch 语句。
- 当遇到 break 语句时,switch 语句终止。程序跳转到 switch 语句后面的语句执行。case 语句不必须要包含 break 语句。如果没有 break 语句出现,程序会继续执行下一条 case 语句,直到出现 break 语句。
- switch 语句可以包含一个 default 分支,该分支一般是 switch 语句的最后一个分支(可以在任何位置,但建议在最后一个)。default 在没有 case 语句的值和变量值相等的时候执行。default 分支不需要 break 语句。
**switch**
语句根据**switch **
(表达式)计算的结果,跳转到匹配的**case**
结果,然后继续执行后续语句,直到遇到**break**
结束执行。
我们看一个例子:
public class Main {
public static void main(String[] args) {
int option = 1;
switch (option) {
case 1:
System.out.println("Selected 1");
break;
case 2:
System.out.println("Selected 2");
break;
case 3:
System.out.println("Selected 3");
break;
}
}
}
输出结果如下:
:::success Selected 1 :::
修改
option
的值分别为1
、2
、3
,观察执行结果。如果
option
的值没有匹配到任何case
,例如option = 99
,那么,switch
语句不会执行任何语句。这时,可以给switch
语句加一个default
,当没有匹配到任何case
时,执行default
:
public class Main {
public static void main(String[] args) {
int option = 99;
switch (option) {
case 1:
System.out.println("Selected 1");
break;
case 2:
System.out.println("Selected 2");
break;
case 3:
System.out.println("Selected 3");
break;
default:
System.out.println("Not selected");
break;
}
}
}
输出结果如下:
:::success Not selected :::
如果把
switch
语句翻译成if
语句,那么上述的代码相当于:
:::info
if (option == 1) {
System.out.println(“Selected 1”);
} else if (option == 2) {
System.out.println(“Selected 2”);
} else if (option == 3) {
System.out.println(“Selected 3”);
} else {
System.out.println(“Not selected”);
}
:::
对于多个
==
判断的情况,使用switch
结构更加清晰。 同时注意,上述“翻译”只有在switch
语句中对每个case
正确编写了break
语句才能对应得上。 使用switch
时,注意case
语句并没有花括号{}
,而且,case
语句具有“穿透性”,漏写break
将导致意想不到的结果:
public class Main {
public static void main(String[] args) {
int option = 2;
switch (option) {
case 1:
System.out.println("Selected 1");
case 2:
System.out.println("Selected 2");
case 3:
System.out.println("Selected 3");
default:
System.out.println("Not selected");
}
}
}
输出结果如下:
:::success
Selected 2
Selected 3
Not selected
:::
当
option = 2
时,将依次输出"Selected 2"
、"Selected 3
“、"Not selected"
,原因是从匹配到case 2
开始,后续语句将全部执行,直到遇到break
语句。因此,任何时候都不要忘记写break
。如果有几个
case
语句执行的是同一组语句块,可以这么写:
public class Main {
public static void main(String[] args) {
int option = 2;
switch (option) {
case 1:
System.out.println("Selected 1");
break;
case 2:
case 3:
System.out.println("Selected 2, 3");
break;
default:
System.out.println("Not selected");
break;
}
}
}
输出结果如下:
:::success Selected 2, 3 :::
使用
switch
语句时,只要保证有break
,case
的顺序不影响程序逻辑:
:::info
switch (option) {
case 3:
…
break;
case 2:
…
break;
case 1:
…
break;
}
:::
但是仍然建议按照自然顺序排列,便于阅读。
switch
语句还可以匹配字符串。字符串匹配时,是比较“内容相等”。
例如:
public class Main {
public static void main(String[] args) {
String fruit = "apple";
switch (fruit) {
case "apple":
System.out.println("Selected apple");
break;
case "pear":
System.out.println("Selected pear");
break;
case "mango":
System.out.println("Selected mango");
break;
default:
System.out.println("No fruit selected");
break;
}
}
}
输出结果如下:
:::success Selected apple :::
switch
语句还可以使用枚举类型,枚举类型我们在后面讲解。
编译检测
使用IDE时,可以自动检查是否漏写了
break
语句和default
语句,方法是打开IDE的编译检查。在Eclipse中,选择
Preferences
-Java
-Compiler
-Errors/Warnings
-Potential ``programming problems
,将以下检查标记为Warning:
- ‘switch’ is missing ‘default’ case
- ‘switch’ case fall-through
在Idea中,选择
Preferences
-Editor
-Inspections
-Java
-Control flow issues
,将以下检查标记为Warning:
- Fallthrough in ‘switch’ statement
- ‘switch’ statement without ‘default’ branch
当
**switch**
语句存在问题时,即可在IDE中获得警告提示。
:::danger
不要忘记**break**
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
不要忘记**default**
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
:::
switch表达式
使用
switch
时,如果遗漏了break
,就会造成严重的逻辑错误,而且不易在源代码中发现错误。从Java 12开始,switch
语句升级为更简洁的表达式语法,使用类似模式匹配(Pattern Matching)的方法,保证只有一种路径会被执行,并且不需要break
语句:
public class Main {
public static void main(String[] args) {
String fruit = "apple";
switch (fruit) {
case "apple" -> System.out.println("Selected apple");
case "pear" -> System.out.println("Selected pear");
case "mango" -> {
System.out.println("Selected mango");
System.out.println("Good choice!");
}
default -> System.out.println("No fruit selected");
}
}
}
输出结果如下:
:::success Selected apple :::
注意新语法使用
->
,如果有多条语句,需要用{}
括起来。不要写break
语句,因为新语法只会执行匹配的语句,没有穿透效应。很多时候,我们还可能用
switch
语句给某个变量赋值。
例如:
:::info
int opt;
switch (fruit) {
case “apple”:
opt = 1;
break; case “pear”:
case “mango”:
opt = 2;
break;
default:
opt = 0;
break;
}
:::
使用新的
switch
语法,不但不需要break
,还可以直接返回值。
把上面的代码改写如下:
public class Main {
public static void main(String[] args) {
String fruit = "apple";
int opt = switch (fruit) {
case "apple" -> 1;
case "pear", "mango" -> 2;
default -> 0;
}; // 注意赋值语句要以;结束
System.out.println("opt = " + opt);
}
}
输出结果如下:
:::success opt = 1 ::: 这样可以获得更简洁的代码。
yield
大多数时候,在
switch
表达式内部,我们会返回简单的值。 但是,如果需要复杂的语句,我们也可以写很多语句,放到{...}
里,然后,用yield
返回一个值作为switch
语句的返回值:
public class Main {
public static void main(String[] args) {
String fruit = "orange";
int opt = switch (fruit) {
case "apple" -> 1;
case "pear", "mango" -> 2;
default -> {
int code = fruit.hashCode();
yield code; // switch语句返回值
}
};
System.out.println("opt = " + opt);
}
}
输出结果如下:
:::success opt = -1008851410 :::
while循环
循环语句就是让计算机根据条件做循环计算,在条件满足时继续循环,条件不满足时退出循环。
例如,计算从1到100的和: :::info 1 + 2 + 3 + 4 + … + 100 = ? :::
除了用数列公式外,完全可以让计算机做100次循环累加。因为计算机的特点是计算速度非常快,我们让计算机循环一亿次也用不到1秒,所以很多计算的任务,人去算是算不了的,但是计算机算,使用循环这种简单粗暴的方法就可以快速得到结果。
我们先看Java提供的
while
条件循环。它的基本用法是:
:::info
while (条件表达式) {
循环语句
}
// 继续执行后续代码
:::
while
循环在每次循环开始前,首先判断条件是否成立。如果计算结果为true
,就把循环体内的语句执行一遍,如果计算结果为false
,那就直接跳到while
循环的末尾,继续往下执行。
我们用while
循环来累加1到100,可以这么写:
public class Main {
public static void main(String[] args) {
int sum = 0; // 累加的和,初始化为0
int n = 1;
while (n <= 100) { // 循环条件是n <= 100
sum = sum + n; // 把n累加到sum中
n ++; // n自身加1
}
System.out.println(sum); // 5050
}
}
输出结果如下: :::success 5050 :::
注意到
while
循环是先判断循环条件,再循环,因此,有可能一次循环都不做。 对于循环条件判断,以及自增变量的处理,要特别注意边界条件。
思考一下下面的代码为何没有获得正确结果:
public class Main {
public static void main(String[] args) {
int sum = 0;
int n = 0;
while (n <= 100) {
n ++;
sum = sum + n;
}
System.out.println(sum);
}
}
输出结果如下: :::success 5151 :::
如果循环条件永远满足,那这个循环就变成了死循环。死循环将导致100%的CPU占用,用户会感觉电脑运行缓慢,所以要避免编写死循环代码。
如果循环条件的逻辑写得有问题,也会造成意料之外的结果:
public class Main {
public static void main(String[] args) {
int sum = 0;
int n = 1;
while (n > 0) {
sum = sum + n;
n ++;
}
System.out.println(n); // -2147483648
System.out.println(sum);
}
}
输出结果如下:
:::success
-2147483648
-1073741824
:::
表面上看,上面的while
循环是一个死循环,但是,Java的int
类型有最大值,达到最大值后,再加1会变成负数,结果,意外退出了while
循环。
本章小结
while
循环先判断循环条件是否满足,再执行循环语句;while
循环可能一次都不执行;
do while循环
在Java中,
while
循环是先判断循环条件,再执行循环。而另一种do while
循环则是先执行循环,再判断条件,条件满足时继续循环,条件不满足时退出。 它的用法是:
:::info
do {
执行循环语句
} while (条件表达式);
:::
可见,
do while
循环会至少循环一次。
我们把对1到100的求和用do while
循环改写一下:
public class Main {
public static void main(String[] args) {
int sum = 0;
int n = 1;
do {
sum = sum + n;
n ++;
} while (n <= 100);
System.out.println(sum);
}
}
输出结果如下: :::success 5050 :::
使用
do while
循环时,同样要注意循环条件的判断。
本章小结
do while
循环先执行循环,再判断条件;do while
循环会至少执行一次。
for循环
除了
while
和do while
循环,Java使用最广泛的是for
循环。for
循环的功能非常强大,它使用计数器实现循环。for
循环会先初始化计数器,然后,在每次循环前检测循环条件,在每次循环后更新计数器。计数器变量通常命名为
i
。
我们把1到100求和用for循环改写一下:
public class Main {
public static void main(String[] args) {
int sum = 0;
for (int i=1; i<=100; i++) {
sum = sum + i;
}
System.out.println(sum);
}
}
输出结果如下:
:::success 5050 :::
在
for
循环执行前,会先执行初始化语句int i=1
,它定义了计数器变量i
并赋初始值为1
,然后,循环前先检查循环条件i<=100
,循环后自动执行i++
,因此,和while
循环相比,for
循环把更新计数器的代码统一放到了一起。在for
循环的循环体内部,不需要去更新变量i
。因此,
for
循环的用法是:
:::info
for (初始条件; 循环检测条件; 循环后更新计数器) {
// 执行语句
}
:::
如果我们要对一个整型数组的所有元素求和,可以用
for
循环实现:
public class Main {
public static void main(String[] args) {
int[] ns = { 1, 4, 9, 16, 25 };
int sum = 0;
for (int i=0; i<ns.length; i++) {
System.out.println("i = " + i + ", ns[i] = " + ns[i]);
sum = sum + ns[i];
}
System.out.println("sum = " + sum);
}
}
输出结果如下:
:::success
i = 0, ns[i] = 1
i = 1, ns[i] = 4
i = 2, ns[i] = 9
i = 3, ns[i] = 16
i = 4, ns[i] = 25
sum = 55
:::
上面代码的循环条件是i
<ns.length
。 因为ns
数组的长度是5
,因此,当循环5
次后,i
的值被更新为5
,就不满足循环条件,因此for
循环结束。
:::info 请思考:如果把循环条件改为i<=ns.length,会出现什么问题? :::
注意
for
循环的初始化计数器总是会被执行,并且for
循环也可能循环0
次。使用
for
循环时,千万不要在循环体内修改计数器!在循环体中修改计数器常常导致莫名其妙的逻辑错误。
对于下面的代码:
public class Main {
public static void main(String[] args) {
int[] ns = { 1, 4, 9, 16, 25 };
for (int i=0; i<ns.length; i++) {
System.out.println(ns[i]);
i = i + 1;
}
}
}
输出结果如下:
:::success
1
9
25
:::
虽然不会报错,但是,数组元素只打印了一半,原因是循环内部的
i = i + 1
导致了计数器变量每次循环实际上加了2
(因为for
循环还会自动执行i++
)。因此,在for
循环中,不要修改计数器的值。计数器的初始化、判断条件、每次循环后的更新条件统一放到for()
语句中可以一目了然。
如果希望只访问索引为奇数的数组元素,应该把for
循环改写为:
:::info
int[] ns = { 1, 4, 9, 16, 25 };
for (int i=0; i
}
:::
通过更新计数器的语句i=i+2
就达到了这个效果,从而避免了在循环体内去修改变量i
。
使用for
循环时,计数器变量i要尽量定义在for
循环中:
:::info
int[] ns = { 1, 4, 9, 16, 25 };
for (int i=0; i
} // 无法访问i
int n = i; // compile error!
:::
如果变量i
定义在for
循环外:
:::info
int[] ns = { 1, 4, 9, 16, 25 };
int i;
for (i=0; i
} // 仍然可以使用i
int n = i;
:::
那么,退出for
循环后,变量i
仍然可以被访问,这就破坏了变量应该把访问范围缩到最小的原则。
灵活使用for循环
for循环还可以缺少初始化语句、循环条件和每次循环更新语句
例如:
:::info
// 不设置结束条件:
for (int i=0; ; i++) {
…
}
:::
:::info
// 不设置结束条件和更新语句:
for (int i=0; ;) {
…
}
:::
:::info
// 什么都不设置:
for ( ; ; ) {
…
}
:::
通常不推荐这样写,但是,某些情况下,是可以省略for
循环的某些语句的。
for each循环
for
循环经常用来遍历数组,因为通过计数器可以根据索引来访问数组的每个元素:
:::info
int[] ns = { 1, 4, 9, 16, 25 };
for (int i=0; i
}
:::
但是,很多时候,我们实际上真正想要访问的是数组每个元素的值。Java还提供了另一种
for each
循环,它可以更简单地遍历数组:
public class Main {
public static void main(String[] args) {
int[] ns = { 1, 4, 9, 16, 25 };
for (int n : ns) {
System.out.println(n);
}
}
}
输出结果如下:
:::success
1
4
9
16
25
:::
和
for
循环相比,for each
循环的变量n
不再是计数器,而是直接对应到数组的每个元素。for each
循环的写法也更简洁。但是,for each
循环无法指定遍历顺序,也无法获取数组的索引。 除了数组外,for each
循环能够遍历所有“可迭代”的数据类型,包括后面会介绍的List
、Map
等。
本章小结
for
循环通过计数器可以实现复杂循环;for each
循环可以直接遍历数组的每个元素;最佳实践:计数器变量定义在
for
循环内部,循环体内部不修改计数器;
break和continue
无论是
**while**
循环还是**for**
循环,有两个特别的语句可以使用,就是**break**
语句和**continue**
语句。
break
在循环过程中,可以使用
break
语句跳出当前循环。
我们来看一个例子:
public class Main {
public static void main(String[] args) {
int sum = 0;
for (int i=1; ; i++) {
sum = sum + i;
if (i == 100) {
break;
}
}
System.out.println(sum);
}
}
输出结果如下:
:::success 5050 :::
使用
for
循环计算从1到100时,我们并没有在for()
中设置循环退出的检测条件。但是,在循环内部,我们用if判断,如果i==100
,就通过break
退出循环。 因此,break
语句通常都是配合if
语句使用。要特别注意,break
语句总是跳出自己所在的那一层循环。
例如:
public class Main {
public static void main(String[] args) {
for (int i=1; i<=10; i++) {
System.out.println("i = " + i);
for (int j=1; j<=10; j++) {
System.out.println("j = " + j);
if (j >= i) {
break;
}
}
// break跳到这里
System.out.println("breaked");
}
}
}
输出结果如下:
:::success
i = 1
j = 1
breaked
i = 2
j = 1
j = 2
breaked
i = 3
j = 1
j = 2
j = 3
breaked
i = 4
j = 1
j = 2
j = 3
j = 4
breaked
i = 5
j = 1
j = 2
j = 3
j = 4
j = 5
breaked
i = 6
j = 1
j = 2
j = 3
j = 4
j = 5
j = 6
breaked
i = 7
j = 1
j = 2
j = 3
j = 4
j = 5
j = 6
j = 7
breaked
i = 8
j = 1
j = 2
j = 3
j = 4
j = 5
j = 6
j = 7
j = 8
breaked
i = 9
j = 1
j = 2
j = 3
j = 4
j = 5
j = 6
j = 7
j = 8
j = 9
breaked
i = 10
j = 1
j = 2
j = 3
j = 4
j = 5
j = 6
j = 7
j = 8
j = 9
j = 10
breaked
:::
上面的代码是两个for
循环嵌套。
因为break
语句位于内层的for
循环,因此,它会跳出内层for
循环,但不会跳出外层for
循环。
continue
break
会跳出当前循环,也就是整个循环都不会执行了。 而continue
则是提前结束本次循环,直接继续执行下次循环。
我们看一个例子:
public class Main {
public static void main(String[] args) {
int sum = 0;
for (int i=1; i<=10; i++) {
System.out.println("begin i = " + i);
if (i % 2 == 0) {
continue; // continue语句会结束本次循环
}
sum = sum + i;
System.out.println("end i = " + i);
}
System.out.println(sum); // 25
}
}
输出结果如下:
:::success
begin i = 1
end i = 1
begin i = 2
begin i = 3
end i = 3
begin i = 4
begin i = 5
end i = 5
begin i = 6
begin i = 7
end i = 7
begin i = 8
begin i = 9
end i = 9
begin i = 10
25
:::
注意观察
continue
语句的效果。当i
为奇数时,完整地执行了整个循环,因此,会打印begin i=1
和end i=1
。在i为偶数时,continue
语句会提前结束本次循环,因此,会打印begin i=2
但不会打印end i = 2
。在多层嵌套的循环中,
continue
语句同样是结束本次自己所在的循环。
本章小结
break
语句可以跳出当前循环;
break
语句通常配合if
,在满足条件时提前结束整个循环;
break
语句总是跳出最近的一层循环;
continue
语句可以提前结束本次循环;
continue
语句通常配合if
,在满足条件时提前结束本次循环。
数组操作
本节将讲解对数组的操作,包括:
- 遍历;
- 排序。
以及多维数组的概念。
遍历数组
在Java程序基础里介绍了数组这种数据类型。 有了数组,我们还需要来操作它。 而数组最常见的一个操作就是遍历。
通过
for
循环就可以遍历数组。 因为数组的每个元素都可以通过索引来访问,因此,使用标准的for
循环可以完成一个数组的遍历:
public class Main {
public static void main(String[] args) {
int[] ns = { 1, 4, 9, 16, 25 };
for (int i=0; i<ns.length; i++) {
int n = ns[i];
System.out.println(n);
}
}
}
输出结果如下:
:::success
1
4
9
16
25
:::
为了实现
for
循环遍历,初始条件为i=0
,因为索引总是从0
开始,继续循环的条件为i<ns.length
,因为当i=ns.length
时,i
已经超出了索引范围(索引范围是0 ~ ns.length-1
),每次循环后,i++
。第二种方式是使用
for each
循环,直接迭代数组的每个元素:
public class Main {
public static void main(String[] args) {
int[] ns = { 1, 4, 9, 16, 25 };
for (int n : ns) {
System.out.println(n);
}
}
}
输出结果如下:
:::success
1
4
9
16
25
:::
注意:在for (int n : ns)
循环中,变量n
直接拿到ns
数组的元素,而不是索引。
显然
for each
循环更加简洁。但是,for each
循环无法拿到数组的索引,因此,到底用哪一种for
循环,取决于我们的需要。
打印数组内容
直接打印数组变量,得到的是数组在JVM中的引用地址:
:::info
int[] ns = { 1, 1, 2, 3, 5, 8 };
System.out.println(ns); // 类似 [I@7852e922
:::
这并没有什么意义,因为我们希望打印的数组的元素内容。因此,使用
for each
循环来打印它:
:::info
int[] ns = { 1, 1, 2, 3, 5, 8 };
for (int n : ns) {
System.out.print(n + “, “);
}
:::
使用
for each
循环打印也很麻烦。幸好Java标准库提供了Arrays.toString()
,可以快速打印数组内容:
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] ns = { 1, 1, 2, 3, 5, 8 };
System.out.println(Arrays.toString(ns));
}
}
输出结果如下: :::success [1, 1, 2, 3, 5, 8] :::
本章小结
遍历数组可以使用
for
循环,for
循环可以访问数组索引,for each
循环直接迭代每个数组元素,但无法获取索引;使用
Arrays.toString()
可以快速获取数组内容。
数组排序
对数组进行排序是程序中非常基本的需求。常用的排序算法有冒泡排序、插入排序和快速排序等。
我们来看一下如何使用冒泡排序算法对一个整型数组从小到大进行排序:
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] ns = { 28, 12, 89, 73, 65, 18, 96, 50, 8, 36 };
// 排序前:
System.out.println(Arrays.toString(ns));
for (int i = 0; i < ns.length - 1; i++) {
for (int j = 0; j < ns.length - i - 1; j++) {
if (ns[j] > ns[j+1]) {
// 交换ns[j]和ns[j+1]:
int tmp = ns[j];
ns[j] = ns[j+1];
ns[j+1] = tmp;
}
}
}
// 排序后:
System.out.println(Arrays.toString(ns));
}
}
输出结果如下:
:::success
[28, 12, 89, 73, 65, 18, 96, 50, 8, 36]
[8, 12, 18, 28, 36, 50, 65, 73, 89, 96]
:::
冒泡排序的特点是,每一轮循环后,最大的一个数被交换到末尾,因此,下一轮循环就可以“刨除”最后的数,每一轮循环都比上一轮循环的结束位置靠前一位。
另外,注意到交换两个变量的值必须借助一个临时变量。
像这么写是错误的:
:::info
int x = 1;
int y = 2;
x = y; // x现在是2
y = x; // y现在还是2
:::
正确的写法是:
:::info
int x = 1;
int y = 2;
int t = x; // 把x的值保存在临时变量t中, t现在是1
x = y; // x现在是2
y = t; // y现在是t的值1
:::
实际上,Java的标准库已经内置了排序功能,我们只需要调用JDK提供的
Arrays.sort()
就可以排序:
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] ns = { 28, 12, 89, 73, 65, 18, 96, 50, 8, 36 };
Arrays.sort(ns);
System.out.println(Arrays.toString(ns));
}
}
输出结果如下:
:::success [8, 12, 18, 28, 36, 50, 65, 73, 89, 96] ::: 必须注意,对数组排序实际上修改了数组本身。例如,排序前的数组是: :::info int[] ns = { 9, 3, 6, 5 }; ::: 在内存中,这个整型数组表示如下:
┌───┬───┬───┬───┐
ns───>│ 9 │ 3 │ 6 │ 5 │
└───┴───┴───┴───┘
当我们调用Arrays.sort(ns);
后,这个整型数组在内存中变为:
┌───┬───┬───┬───┐
ns───>│ 3 │ 5 │ 6 │ 9 │
└───┴───┴───┴───┘
即变量ns
指向的数组内容已经被改变了。
如果对一个字符串数组进行排序,例如:
:::info
String[] ns = { “banana”, “apple”, “pear” };
:::
排序前,这个数组在内存中表示如下:
┌──────────────────────────────────┐
┌───┼──────────────────────┐ │
│ │ ▼ ▼
┌───┬─┴─┬─┴─┬───┬────────┬───┬───────┬───┬──────┬───┐
ns ─────>│░░░│░░░│░░░│ │"banana"│ │"apple"│ │"pear"│ │
└─┬─┴───┴───┴───┴────────┴───┴───────┴───┴──────┴───┘
│ ▲
└─────────────────┘
调用Arrays.sort(ns);
排序后,这个数组在内存中表示如下:
┌──────────────────────────────────┐
┌───┼──────────┐ │
│ │ ▼ ▼
┌───┬─┴─┬─┴─┬───┬────────┬───┬───────┬───┬──────┬───┐
ns ─────>│░░░│░░░│░░░│ │"banana"│ │"apple"│ │"pear"│ │
└─┬─┴───┴───┴───┴────────┴───┴───────┴───┴──────┴───┘
│ ▲
└──────────────────────────────┘
原来的3个字符串在内存中均没有任何变化,但是
ns
数组的每个元素指向变化了。
本章小结
常用的排序算法有冒泡排序、插入排序和快速排序等; 冒泡排序使用两层
for
循环实现排序; 交换两个变量的值需要借助一个临时变量。 可以直接使用Java标准库提供的Arrays.sort()
进行排序; 对数组排序会直接修改数组本身。
多维数组
二维数组
二维数组就是数组的数组。定义一个二维数组如下:
public class Main {
public static void main(String[] args) {
int[][] ns = {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
};
System.out.println(ns.length); // 3
}
}
输出结果如下:
:::success
3
:::
因为ns
包含3个数组,因此,ns.length
为3。实际上ns
在内存中的结构如下:
┌───┬───┬───┬───┐
┌───┐ ┌──>│ 1 │ 2 │ 3 │ 4 │
ns ─────>│░░░│──┘ └───┴───┴───┴───┘
├───┤ ┌───┬───┬───┬───┐
│░░░│─────>│ 5 │ 6 │ 7 │ 8 │
├───┤ └───┴───┴───┴───┘
│░░░│──┐ ┌───┬───┬───┬───┐
└───┘ └──>│ 9 │10 │11 │12 │
└───┴───┴───┴───┘
如果我们定义一个普通数组arr0
,然后把ns[0]
赋值给它:
public class Main {
public static void main(String[] args) {
int[][] ns = {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
};
int[] arr0 = ns[0];
System.out.println(arr0.length); // 4
}
}
输出结果如下:
:::success
4
:::
实际上arr0
就获取了ns
数组的第0个元素。因为ns
数组的每个元素也是一个数组,因此,arr0
指向的数组就是{1, 2, 3, 4 }
。在内存中,结构如下:
arr0 ─────┐
▼
┌───┬───┬───┬───┐
┌───┐ ┌──>│ 1 │ 2 │ 3 │ 4 │
ns ─────>│░░░│──┘ └───┴───┴───┴───┘
├───┤ ┌───┬───┬───┬───┐
│░░░│─────>│ 5 │ 6 │ 7 │ 8 │
├───┤ └───┴───┴───┴───┘
│░░░│──┐ ┌───┬───┬───┬───┐
└───┘ └──>│ 9 │10 │11 │12 │
└───┴───┴───┴───┘
访问二维数组的某个元素需要使用array[row][col]
例如:
:::info
System.out.println(ns[1][2]); // 7
:::
二维数组的每个数组元素的长度并不要求相同
例如,可以这么定义ns
数组:
:::info
int[][] ns = {
{ 1, 2, 3, 4 },
{ 5, 6 },
{ 7, 8, 9 }
};
:::
这个二维数组在内存中的结构如下:
┌───┬───┬───┬───┐
┌───┐ ┌──>│ 1 │ 2 │ 3 │ 4 │
ns ─────>│░░░│──┘ └───┴───┴───┴───┘
├───┤ ┌───┬───┐
│░░░│─────>│ 5 │ 6 │
├───┤ └───┴───┘
│░░░│──┐ ┌───┬───┬───┐
└───┘ └──>│ 7 │ 8 │ 9 │
└───┴───┴───┘
要打印一个二维数组,可以使用两层嵌套的for
循环:
:::info
for (int[] arr : ns) {
for (int n : arr) {
System.out.print(n);
System.out.print(‘, ‘);
}
System.out.println();
}
:::
或者使用Java标准库的Arrays.deepToString()
:
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[][] ns = {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
};
System.out.println(Arrays.deepToString(ns));
}
}
输出结果如下:
:::success [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]] :::
三维数组
三维数组就是二维数组的数组。可以这么定义一个三维数组:
:::info
int[][][] ns = {
{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
},
{
{10, 11},
{12, 13}
},
{
{14, 15, 16},
{17, 18}
}
};
:::
它在内存中的结构如下:
┌───┬───┬───┐
┌───┐ ┌──>│ 1 │ 2 │ 3 │
┌──>│░░░│──┘ └───┴───┴───┘
│ ├───┤ ┌───┬───┬───┐
│ │░░░│─────>│ 4 │ 5 │ 6 │
│ ├───┤ └───┴───┴───┘
│ │░░░│──┐ ┌───┬───┬───┐
┌───┐ │ └───┘ └──>│ 7 │ 8 │ 9 │
ns ────>│░░░│──┘ └───┴───┴───┘
├───┤ ┌───┐ ┌───┬───┐
│░░░│─────>│░░░│─────>│10 │11 │
├───┤ ├───┤ └───┴───┘
│░░░│──┐ │░░░│──┐ ┌───┬───┐
└───┘ │ └───┘ └──>│12 │13 │
│ └───┴───┘
│ ┌───┐ ┌───┬───┬───┐
└──>│░░░│─────>│14 │15 │16 │
├───┤ └───┴───┴───┘
│░░░│──┐ ┌───┬───┐
└───┘ └──>│17 │18 │
└───┴───┘
如果我们要访问三维数组的某个元素,例如,ns[2][0][1]
,只需要顺着定位找到对应的最终元素15
即可。
理论上,我们可以定义任意的N维数组。 但在实际应用中,除了二维数组在某些时候还能用得上,更高维度的数组很少使用。
本章小结
二维数组就是数组的数组,三维数组就是二维数组的数组; 多维数组的每个数组元素长度都不要求相同; 打印多维数组可以使用
Arrays.deepToString()
; 最常见的多维数组是二维数组,访问二维数组的一个元素使用array[row][col]
。
命令行参数
Java程序的入口是
main
方法,而main
方法可以接受一个命令行参数,它是一个String[]
数组。这个命令行参数由JVM接收用户输入并传给
main
方法:
:::info
public class Main {
public static void main(String[] args) {
for (String arg : args) {
System.out.println(arg);
}
}
}
:::
我们可以利用接收到的命令行参数,根据不同的参数执行不同的代码。
例如,实现一个-version
参数,打印程序版本号:
:::info
public class Main {
public static void main(String[] args) {
for (String arg : args) {
if (“-version”.equals(arg)) {
System.out.println(“v 1.0”);
break;
}
}
}
}
:::
上面这个程序必须在命令行执行,我们先编译它:
:::info
$ javac Main.java
:::
然后,执行的时候,给它传递一个-version
参数:
:::info
$ java Main -version v 1.0
:::
这样,程序就可以根据传入的命令行参数,作出不同的响应。
本章小结
命令行参数类型是
String[]
数组; 命令行参数由JVM接收用户输入并传给main
方法; 如何解析命令行参数需要由程序自己实现。