(一)JAVA入门基础学习教程1

流程控制

在Java程序中,JVM默认总是顺序执行以分号;结束的语句。但是,在实际的代码中,程序经常需要做条件判断、循环,因此,需要有多种流程控制语句,来实现程序的跳转和循环等功能。

将介绍if条件判断、switch多重选择和各种循环语句。

输入和输出

输出

在前面的代码中,我们总是使用System.out.println()来向屏幕输出一些内容。 printlnprint line的缩写,表示输出并换行。因此,如果输出后不想换行,可以用print()

  1. public class Main {
  2. public static void main(String[] args) {
  3. System.out.print("A,");
  4. System.out.print("B,");
  5. System.out.print("C.");
  6. System.out.println();
  7. System.out.println("END");
  8. }
  9. }
  1. 输出结果如下:

:::success A,B,C.
END ::: 注意观察上述代码的执行效果。

格式化输出

Java还提供了格式化输出的功能。为什么要格式化输出?因为计算机表示的数据不一定适合人来阅读:

  1. public class Main {
  2. public static void main(String[] args) {
  3. double d = 12900000;
  4. System.out.println(d); // 1.29E7
  5. }
  6. }
  1. 输出结果如下:

:::success 1.29E7 ::: 如果要把数据显示成我们期望的格式,就需要使用格式化输出的功能。格式化输出使用System.out.printf(),通过使用占位符%?printf()可以把后面的参数格式化成指定格式:

  1. public class Main {
  2. public static void main(String[] args) {
  3. double d = 3.1415926;
  4. System.out.printf("%.2f\n", d); // 显示两位小数3.14
  5. System.out.printf("%.4f\n", d); // 显示4位小数3.1416
  6. }
  7. }
  1. 输出结果如下:

:::success 3.14
3,1416 ::: Java的格式化功能提供了多种占位符,可以把各种数据类型“格式化”成指定的字符串:

占位符 说明
%d 格式化输出整数
%x 格式化输出十六进制整数
%f 格式化输出浮点数
%e 格式化输出科学计数法表示的浮点数
%s 格式化字符串

注意,由于%表示占位符,因此,连续两个%%表示一个%字符本身。
占位符本身还可以有更详细的格式化参数。下面的例子把一个整数格式化成十六进制,并用0补足8位:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int n = 12345000;
  4. System.out.printf("n=%d, hex=%08x", n, n); // 注意,两个%占位符必须传入两个数
  5. }
  6. }
  1. 输出结果如下:

:::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.Scannerimport是导入某个类的语句,必须放到Java源代码的开头,后面我们在Java的package中会详细讲解如何使用import

然后,创建Scanner对象并传入System.inSystem.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语句块(即花括号{}包含的所有语句)。

让我们来看一个例子:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int n = 70;
  4. if (n >= 60) {
  5. System.out.println("及格了");
  6. }
  7. System.out.println("END");
  8. }
  9. }
  1. 输出结果如下:

:::success 及格了
END :::

当条件n >= 60计算结果为true时,if语句块被执行,将打印”及格了”,否则,if语句块将被跳过。修改n的值可以看到执行效果。

注意到if语句包含的块可以包含多条语句:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int n = 70;
  4. if (n >= 60) {
  5. System.out.println("及格了");
  6. System.out.println("恭喜你");
  7. }
  8. System.out.println("END");
  9. }
  10. }
  1. 输出结果如下:

:::success 及格了
恭喜你
END :::

当if语句块只有一行语句时,可以省略花括号{}:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int n = 70;
  4. if (n >= 60)
  5. System.out.println("及格了");
  6. System.out.println("END");
  7. }
  8. }
  1. 输出结果如下:

:::success 及格了
END :::

但是,省略花括号并不总是一个好主意。 假设某个时候,突然想给if语句块增加一条语句时:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int n = 50;
  4. if (n >= 60)
  5. System.out.println("及格了");
  6. System.out.println("恭喜你"); // 注意这条语句不是if语句块的一部分
  7. System.out.println("END");
  8. }
  9. }
  1. 输出结果如下:

:::success 恭喜你
END :::

由于使用缩进格式,很容易把两行语句都看成if语句的执行块,但实际上只有第一行语句是if的执行块。在使用git这些版本控制系统自动合并时更容易出问题,所以不推荐忽略花括号的写法。

else

if语句还可以编写一个else { ... },当条件判断为false时,将执行else的语句块:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int n = 70;
  4. if (n >= 60) {
  5. System.out.println("及格了");
  6. } else {
  7. System.out.println("挂科了");
  8. }
  9. System.out.println("END");
  10. }
  11. }
  1. 输出结果如下:

:::success 及格了
END :::

修改上述代码n的值,观察if条件为truefalse时,程序执行的语句块。 注意else不是必须的。 还可以用多个if ... else if ...串联。

例如:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int n = 70;
  4. if (n >= 90) {
  5. System.out.println("优秀");
  6. } else if (n >= 60) {
  7. System.out.println("及格了");
  8. } else {
  9. System.out.println("挂科了");
  10. }
  11. System.out.println("END");
  12. }
  13. }
  1. 输出结果如下:

:::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时,要特别注意判断顺序。观察下面的代码:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int n = 100;
  4. if (n >= 60) {
  5. System.out.println("及格了");
  6. } else if (n >= 90) {
  7. System.out.println("优秀");
  8. } else {
  9. System.out.println("挂科了");
  10. }
  11. }
  12. }
  1. 输出结果如下:

:::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时,还要特别注意边界条件。

例如:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int n = 90;
  4. if (n > 90) {
  5. System.out.println("优秀");
  6. } else if (n >= 60) {
  7. System.out.println("及格了");
  8. } else {
  9. System.out.println("挂科了");
  10. }
  11. }
  12. }
  1. 输出结果如下:

:::success 及格了 :::

假设我们期望90分或更高为“优秀”,上述代码输出的却是“及格”,原因是>>=效果是不同的。 前面讲过了浮点数在计算机中常常无法精确表示,并且计算可能出现误差,因此,判断浮点数相等用==判断不靠谱:

  1. public class Main {
  2. public static void main(String[] args) {
  3. double x = 1 - 9.0 / 10;
  4. if (x == 0.1) {
  5. System.out.println("x is 0.1");
  6. } else {
  7. System.out.println("x is NOT 0.1");
  8. }
  9. }
  10. }
  1. 输出结果如下

:::success

:::

正确的方法是利用差值小于某个临界值来判断:

  1. public class Main {
  2. public static void main(String[] args) {
  3. double x = 1 - 9.0 / 10;
  4. if (Math.abs(x - 0.1) < 0.00001) {
  5. System.out.println("x is 0.1");
  6. } else {
  7. System.out.println("x is NOT 0.1");
  8. }
  9. }
  10. }
  1. 输出结果如下:

:::success x is 0.1 :::

判断引用类型相等

在Java中,判断值类型的变量是否相等,可以使用==运算符。但是,判断引用类型的变量是否相等,==表示“引用是否相等”,或者说,是否指向同一个对象。例如,下面的两个String类型,它们的内容是相同的,但是,分别指向不同的对象,用==判断,结果为false

  1. public class Main {
  2. public static void main(String[] args) {
  3. String s1 = "hello";
  4. String s2 = "HELLO".toLowerCase();
  5. System.out.println(s1);
  6. System.out.println(s2);
  7. if (s1 == s2) {
  8. System.out.println("s1 == s2");
  9. } else {
  10. System.out.println("s1 != s2");
  11. }
  12. }
  13. }
  1. 输出结果如下:

:::success hello
hello
s1 != s2 :::

要判断引用类型的变量内容是否相等,必须使用equals()方法:

  1. public class Main {
  2. public static void main(String[] args) {
  3. String s1 = "hello";
  4. String s2 = "HELLO".toLowerCase();
  5. System.out.println(s1);
  6. System.out.println(s2);
  7. if (s1.equals(s2)) {
  8. System.out.println("s1 equals s2");
  9. } else {
  10. System.out.println("s1 not equals s2");
  11. }
  12. }
  13. }
  1. 输出结果如下:

:::success hello
hello
s1 equals s2 :::

注意:执行语句s1.equals(s2)时,如果变量s1null,会报NullPointerException

  1. public class Main {
  2. public static void main(String[] args) {
  3. String s1 = null;
  4. if (s1.equals("hello")) {
  5. System.out.println("hello");
  6. }
  7. }
  8. }
  1. 输出结果如下:

:::success

:::

要避免NullPointerException错误,可以利用短路运算符&&

  1. public class Main {
  2. public static void main(String[] args) {
  3. String s1 = null;
  4. if (s1 != null && s1.equals("hello")) {
  5. System.out.println("hello");
  6. }
  7. }
  8. }
  1. 输出结果如下:

:::success

:::

还可以把一定不是null的对象"hello"放到前面: 例如:if ("hello".equals(s)) { ... }

switch多重选择

除了if语句外,还有一种条件判断,是根据某个表达式的结果,分别去执行不同的分支。 例如,在游戏中,让用户选择选项:

  1. 单人模式
  2. 多人模式
  3. 退出游戏

这时,_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**结束执行。

我们看一个例子:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int option = 1;
  4. switch (option) {
  5. case 1:
  6. System.out.println("Selected 1");
  7. break;
  8. case 2:
  9. System.out.println("Selected 2");
  10. break;
  11. case 3:
  12. System.out.println("Selected 3");
  13. break;
  14. }
  15. }
  16. }
  1. 输出结果如下:

:::success Selected 1 :::

修改option的值分别为123,观察执行结果。

如果option的值没有匹配到任何case,例如option = 99,那么,switch语句不会执行任何语句。这时,可以给switch语句加一个default,当没有匹配到任何case时,执行default

  1. public class Main {
  2. public static void main(String[] args) {
  3. int option = 99;
  4. switch (option) {
  5. case 1:
  6. System.out.println("Selected 1");
  7. break;
  8. case 2:
  9. System.out.println("Selected 2");
  10. break;
  11. case 3:
  12. System.out.println("Selected 3");
  13. break;
  14. default:
  15. System.out.println("Not selected");
  16. break;
  17. }
  18. }
  19. }
  1. 输出结果如下:

:::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将导致意想不到的结果:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int option = 2;
  4. switch (option) {
  5. case 1:
  6. System.out.println("Selected 1");
  7. case 2:
  8. System.out.println("Selected 2");
  9. case 3:
  10. System.out.println("Selected 3");
  11. default:
  12. System.out.println("Not selected");
  13. }
  14. }
  15. }
  1. 输出结果如下:

:::success Selected 2
Selected 3
Not selected :::

option = 2时,将依次输出"Selected 2""Selected 3“、"Not selected",原因是从匹配到case 2开始,后续语句将全部执行,直到遇到break语句。因此,任何时候都不要忘记写break

如果有几个case语句执行的是同一组语句块,可以这么写:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int option = 2;
  4. switch (option) {
  5. case 1:
  6. System.out.println("Selected 1");
  7. break;
  8. case 2:
  9. case 3:
  10. System.out.println("Selected 2, 3");
  11. break;
  12. default:
  13. System.out.println("Not selected");
  14. break;
  15. }
  16. }
  17. }
  1. 输出结果如下:

:::success Selected 2, 3 :::

使用switch语句时,只要保证有breakcase的顺序不影响程序逻辑:

:::info switch (option) {
case 3:

break;
case 2:

break;
case 1:

break;
} :::

但是仍然建议按照自然顺序排列,便于阅读。

switch语句还可以匹配字符串。字符串匹配时,是比较“内容相等”。

例如:

  1. public class Main {
  2. public static void main(String[] args) {
  3. String fruit = "apple";
  4. switch (fruit) {
  5. case "apple":
  6. System.out.println("Selected apple");
  7. break;
  8. case "pear":
  9. System.out.println("Selected pear");
  10. break;
  11. case "mango":
  12. System.out.println("Selected mango");
  13. break;
  14. default:
  15. System.out.println("No fruit selected");
  16. break;
  17. }
  18. }
  19. }
  1. 输出结果如下:

:::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语句:

  1. public class Main {
  2. public static void main(String[] args) {
  3. String fruit = "apple";
  4. switch (fruit) {
  5. case "apple" -> System.out.println("Selected apple");
  6. case "pear" -> System.out.println("Selected pear");
  7. case "mango" -> {
  8. System.out.println("Selected mango");
  9. System.out.println("Good choice!");
  10. }
  11. default -> System.out.println("No fruit selected");
  12. }
  13. }
  14. }
  1. 输出结果如下:

:::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,还可以直接返回值。

把上面的代码改写如下:

  1. public class Main {
  2. public static void main(String[] args) {
  3. String fruit = "apple";
  4. int opt = switch (fruit) {
  5. case "apple" -> 1;
  6. case "pear", "mango" -> 2;
  7. default -> 0;
  8. }; // 注意赋值语句要以;结束
  9. System.out.println("opt = " + opt);
  10. }
  11. }
  1. 输出结果如下:

:::success opt = 1 ::: 这样可以获得更简洁的代码。

yield

大多数时候,在switch表达式内部,我们会返回简单的值。 但是,如果需要复杂的语句,我们也可以写很多语句,放到{...}里,然后,用yield返回一个值作为switch语句的返回值:

  1. public class Main {
  2. public static void main(String[] args) {
  3. String fruit = "orange";
  4. int opt = switch (fruit) {
  5. case "apple" -> 1;
  6. case "pear", "mango" -> 2;
  7. default -> {
  8. int code = fruit.hashCode();
  9. yield code; // switch语句返回值
  10. }
  11. };
  12. System.out.println("opt = " + opt);
  13. }
  14. }
  1. 输出结果如下:

:::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,可以这么写:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int sum = 0; // 累加的和,初始化为0
  4. int n = 1;
  5. while (n <= 100) { // 循环条件是n <= 100
  6. sum = sum + n; // 把n累加到sum中
  7. n ++; // n自身加1
  8. }
  9. System.out.println(sum); // 5050
  10. }
  11. }

输出结果如下: :::success 5050 :::

注意到while循环是先判断循环条件,再循环,因此,有可能一次循环都不做。 对于循环条件判断,以及自增变量的处理,要特别注意边界条件。

思考一下下面的代码为何没有获得正确结果:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int sum = 0;
  4. int n = 0;
  5. while (n <= 100) {
  6. n ++;
  7. sum = sum + n;
  8. }
  9. System.out.println(sum);
  10. }
  11. }

输出结果如下: :::success 5151 :::

如果循环条件永远满足,那这个循环就变成了死循环。死循环将导致100%的CPU占用,用户会感觉电脑运行缓慢,所以要避免编写死循环代码。

如果循环条件的逻辑写得有问题,也会造成意料之外的结果:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int sum = 0;
  4. int n = 1;
  5. while (n > 0) {
  6. sum = sum + n;
  7. n ++;
  8. }
  9. System.out.println(n); // -2147483648
  10. System.out.println(sum);
  11. }
  12. }
  1. 输出结果如下:

:::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循环改写一下:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int sum = 0;
  4. int n = 1;
  5. do {
  6. sum = sum + n;
  7. n ++;
  8. } while (n <= 100);
  9. System.out.println(sum);
  10. }
  11. }

输出结果如下: :::success 5050 :::

使用do while循环时,同样要注意循环条件的判断。

本章小结

do while循环先执行循环,再判断条件; do while循环会至少执行一次。

for循环

除了whiledo while循环,Java使用最广泛的是for循环。 for循环的功能非常强大,它使用计数器实现循环。for循环会先初始化计数器,然后,在每次循环前检测循环条件,在每次循环后更新计数器。

计数器变量通常命名为i

我们把1到100求和用for循环改写一下:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int sum = 0;
  4. for (int i=1; i<=100; i++) {
  5. sum = sum + i;
  6. }
  7. System.out.println(sum);
  8. }
  9. }
  1. 输出结果如下:

:::success 5050 :::

for循环执行前,会先执行初始化语句int i=1,它定义了计数器变量i并赋初始值为1,然后,循环前先检查循环条件i<=100,循环后自动执行i++,因此,和while循环相比,for循环把更新计数器的代码统一放到了一起。在for循环的循环体内部,不需要去更新变量i

因此,for循环的用法是:

:::info for (初始条件; 循环检测条件; 循环后更新计数器) {
// 执行语句
} :::

如果我们要对一个整型数组的所有元素求和,可以用for循环实现:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int[] ns = { 1, 4, 9, 16, 25 };
  4. int sum = 0;
  5. for (int i=0; i<ns.length; i++) {
  6. System.out.println("i = " + i + ", ns[i] = " + ns[i]);
  7. sum = sum + ns[i];
  8. }
  9. System.out.println("sum = " + sum);
  10. }
  11. }
  1. 输出结果如下:

:::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循环时,千万不要在循环体内修改计数器!在循环体中修改计数器常常导致莫名其妙的逻辑错误。

对于下面的代码:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int[] ns = { 1, 4, 9, 16, 25 };
  4. for (int i=0; i<ns.length; i++) {
  5. System.out.println(ns[i]);
  6. i = i + 1;
  7. }
  8. }
  9. }
  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 System.out.println(ns[i]);
} ::: 通过更新计数器的语句i=i+2就达到了这个效果,从而避免了在循环体内去修改变量i
使用for循环时,计数器变量i要尽量定义在for循环中: :::info int[] ns = { 1, 4, 9, 16, 25 };
for (int i=0; i System.out.println(ns[i]);
} // 无法访问i
int n = i; // compile error! ::: 如果变量i定义在for循环外: :::info int[] ns = { 1, 4, 9, 16, 25 };
int i;
for (i=0; i System.out.println(ns[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 System.out.println(ns[i]);
} :::

但是,很多时候,我们实际上真正想要访问的是数组每个元素的值。Java还提供了另一种for each循环,它可以更简单地遍历数组:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int[] ns = { 1, 4, 9, 16, 25 };
  4. for (int n : ns) {
  5. System.out.println(n);
  6. }
  7. }
  8. }
  1. 输出结果如下:

:::success 1
4
9
16
25 :::

for循环相比,for each循环的变量n不再是计数器,而是直接对应到数组的每个元素。for each循环的写法也更简洁。但是,for each循环无法指定遍历顺序,也无法获取数组的索引。 除了数组外,for each循环能够遍历所有“可迭代”的数据类型,包括后面会介绍的ListMap等。

本章小结

for循环通过计数器可以实现复杂循环; for each循环可以直接遍历数组的每个元素;

最佳实践:计数器变量定义在for循环内部,循环体内部不修改计数器;

break和continue

无论是**while**循环还是**for**循环,有两个特别的语句可以使用,就是**break**语句和**continue**语句。

break

在循环过程中,可以使用break语句跳出当前循环。

我们来看一个例子:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int sum = 0;
  4. for (int i=1; ; i++) {
  5. sum = sum + i;
  6. if (i == 100) {
  7. break;
  8. }
  9. }
  10. System.out.println(sum);
  11. }
  12. }
  1. 输出结果如下:

:::success 5050 :::

使用for循环计算从1到100时,我们并没有在for()中设置循环退出的检测条件。但是,在循环内部,我们用if判断,如果i==100,就通过break退出循环。 因此,break语句通常都是配合if语句使用。要特别注意,break语句总是跳出自己所在的那一层循环。

例如:

  1. public class Main {
  2. public static void main(String[] args) {
  3. for (int i=1; i<=10; i++) {
  4. System.out.println("i = " + i);
  5. for (int j=1; j<=10; j++) {
  6. System.out.println("j = " + j);
  7. if (j >= i) {
  8. break;
  9. }
  10. }
  11. // break跳到这里
  12. System.out.println("breaked");
  13. }
  14. }
  15. }
  1. 输出结果如下:

:::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则是提前结束本次循环,直接继续执行下次循环。

我们看一个例子:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int sum = 0;
  4. for (int i=1; i<=10; i++) {
  5. System.out.println("begin i = " + i);
  6. if (i % 2 == 0) {
  7. continue; // continue语句会结束本次循环
  8. }
  9. sum = sum + i;
  10. System.out.println("end i = " + i);
  11. }
  12. System.out.println(sum); // 25
  13. }
  14. }
  1. 输出结果如下:

:::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=1end i=1。在i为偶数时,continue语句会提前结束本次循环,因此,会打印begin i=2但不会打印end i = 2

在多层嵌套的循环中,continue语句同样是结束本次自己所在的循环。

本章小结

break语句可以跳出当前循环;

break语句通常配合if,在满足条件时提前结束整个循环;

break语句总是跳出最近的一层循环;

continue语句可以提前结束本次循环;

continue语句通常配合if,在满足条件时提前结束本次循环。

数组操作

本节将讲解对数组的操作,包括:

  • 遍历;
  • 排序。

以及多维数组的概念。

:::danger sorf({ }) :::

遍历数组

在Java程序基础里介绍了数组这种数据类型。 有了数组,我们还需要来操作它。 而数组最常见的一个操作就是遍历。

通过for循环就可以遍历数组。 因为数组的每个元素都可以通过索引来访问,因此,使用标准的for循环可以完成一个数组的遍历:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int[] ns = { 1, 4, 9, 16, 25 };
  4. for (int i=0; i<ns.length; i++) {
  5. int n = ns[i];
  6. System.out.println(n);
  7. }
  8. }
  9. }

输出结果如下: :::success 1
4
9
16
25 :::

为了实现for循环遍历,初始条件为i=0,因为索引总是从0开始,继续循环的条件为i<ns.length,因为当i=ns.length时,i已经超出了索引范围(索引范围是0 ~ ns.length-1),每次循环后,i++

第二种方式是使用for each循环,直接迭代数组的每个元素:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int[] ns = { 1, 4, 9, 16, 25 };
  4. for (int n : ns) {
  5. System.out.println(n);
  6. }
  7. }
  8. }
  1. 输出结果如下:

:::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(),可以快速打印数组内容:

  1. import java.util.Arrays;
  2. public class Main {
  3. public static void main(String[] args) {
  4. int[] ns = { 1, 1, 2, 3, 5, 8 };
  5. System.out.println(Arrays.toString(ns));
  6. }
  7. }

输出结果如下: :::success [1, 1, 2, 3, 5, 8] :::

本章小结

遍历数组可以使用for循环,for循环可以访问数组索引,for each循环直接迭代每个数组元素,但无法获取索引;

使用Arrays.toString()可以快速获取数组内容。

数组排序

对数组进行排序是程序中非常基本的需求。常用的排序算法有冒泡排序、插入排序和快速排序等。
我们来看一下如何使用冒泡排序算法对一个整型数组从小到大进行排序:

  1. import java.util.Arrays;
  2. public class Main {
  3. public static void main(String[] args) {
  4. int[] ns = { 28, 12, 89, 73, 65, 18, 96, 50, 8, 36 };
  5. // 排序前:
  6. System.out.println(Arrays.toString(ns));
  7. for (int i = 0; i < ns.length - 1; i++) {
  8. for (int j = 0; j < ns.length - i - 1; j++) {
  9. if (ns[j] > ns[j+1]) {
  10. // 交换ns[j]和ns[j+1]:
  11. int tmp = ns[j];
  12. ns[j] = ns[j+1];
  13. ns[j+1] = tmp;
  14. }
  15. }
  16. }
  17. // 排序后:
  18. System.out.println(Arrays.toString(ns));
  19. }
  20. }
  1. 输出结果如下:

:::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()就可以排序:

  1. import java.util.Arrays;
  2. public class Main {
  3. public static void main(String[] args) {
  4. int[] ns = { 28, 12, 89, 73, 65, 18, 96, 50, 8, 36 };
  5. Arrays.sort(ns);
  6. System.out.println(Arrays.toString(ns));
  7. }
  8. }
  1. 输出结果如下:

:::success [8, 12, 18, 28, 36, 50, 65, 73, 89, 96] ::: 必须注意,对数组排序实际上修改了数组本身。例如,排序前的数组是: :::info int[] ns = { 9, 3, 6, 5 }; ::: 在内存中,这个整型数组表示如下:

  1. ┌───┬───┬───┬───┐
  2. ns───>│ 9 3 6 5
  3. └───┴───┴───┴───┘

当我们调用Arrays.sort(ns);后,这个整型数组在内存中变为:

  1. ┌───┬───┬───┬───┐
  2. ns───>│ 3 5 6 9
  3. └───┴───┴───┴───┘

即变量ns指向的数组内容已经被改变了。
如果对一个字符串数组进行排序,例如: :::info String[] ns = { “banana”, “apple”, “pear” }; ::: 排序前,这个数组在内存中表示如下:

  1. ┌──────────────────────────────────┐
  2. ┌───┼──────────────────────┐
  3. ┌───┬─┴─┬─┴─┬───┬────────┬───┬───────┬───┬──────┬───┐
  4. ns ─────>│░░░│░░░│░░░│ "banana" "apple" "pear"
  5. └─┬─┴───┴───┴───┴────────┴───┴───────┴───┴──────┴───┘
  6. └─────────────────┘

调用Arrays.sort(ns);排序后,这个数组在内存中表示如下:

  1. ┌──────────────────────────────────┐
  2. ┌───┼──────────┐
  3. ┌───┬─┴─┬─┴─┬───┬────────┬───┬───────┬───┬──────┬───┐
  4. ns ─────>│░░░│░░░│░░░│ "banana" "apple" "pear"
  5. └─┬─┴───┴───┴───┴────────┴───┴───────┴───┴──────┴───┘
  6. └──────────────────────────────┘

原来的3个字符串在内存中均没有任何变化,但是ns数组的每个元素指向变化了。

本章小结

常用的排序算法有冒泡排序、插入排序和快速排序等; 冒泡排序使用两层for循环实现排序; 交换两个变量的值需要借助一个临时变量。 可以直接使用Java标准库提供的Arrays.sort()进行排序; 对数组排序会直接修改数组本身。

多维数组

二维数组

二维数组就是数组的数组。定义一个二维数组如下:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int[][] ns = {
  4. { 1, 2, 3, 4 },
  5. { 5, 6, 7, 8 },
  6. { 9, 10, 11, 12 }
  7. };
  8. System.out.println(ns.length); // 3
  9. }
  10. }
  1. 输出结果如下:

:::success 3 ::: 因为ns包含3个数组,因此,ns.length为3。实际上ns在内存中的结构如下:

  1. ┌───┬───┬───┬───┐
  2. ┌───┐ ┌──>│ 1 2 3 4
  3. ns ─────>│░░░│──┘ └───┴───┴───┴───┘
  4. ├───┤ ┌───┬───┬───┬───┐
  5. │░░░│─────>│ 5 6 7 8
  6. ├───┤ └───┴───┴───┴───┘
  7. │░░░│──┐ ┌───┬───┬───┬───┐
  8. └───┘ └──>│ 9 10 11 12
  9. └───┴───┴───┴───┘

如果我们定义一个普通数组arr0,然后把ns[0]赋值给它:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int[][] ns = {
  4. { 1, 2, 3, 4 },
  5. { 5, 6, 7, 8 },
  6. { 9, 10, 11, 12 }
  7. };
  8. int[] arr0 = ns[0];
  9. System.out.println(arr0.length); // 4
  10. }
  11. }
  1. 输出结果如下:

:::success 4 ::: 实际上arr0就获取了ns数组的第0个元素。因为ns数组的每个元素也是一个数组,因此,arr0指向的数组就是{1, 2, 3, 4 }。在内存中,结构如下:

  1. arr0 ─────┐
  2. ┌───┬───┬───┬───┐
  3. ┌───┐ ┌──>│ 1 2 3 4
  4. ns ─────>│░░░│──┘ └───┴───┴───┴───┘
  5. ├───┤ ┌───┬───┬───┬───┐
  6. │░░░│─────>│ 5 6 7 8
  7. ├───┤ └───┴───┴───┴───┘
  8. │░░░│──┐ ┌───┬───┬───┬───┐
  9. └───┘ └──>│ 9 10 11 12
  10. └───┴───┴───┴───┘

访问二维数组的某个元素需要使用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. ┌───┐ ┌──>│ 1 2 3 4
  3. ns ─────>│░░░│──┘ └───┴───┴───┴───┘
  4. ├───┤ ┌───┬───┐
  5. │░░░│─────>│ 5 6
  6. ├───┤ └───┴───┘
  7. │░░░│──┐ ┌───┬───┬───┐
  8. └───┘ └──>│ 7 8 9
  9. └───┴───┴───┘

要打印一个二维数组,可以使用两层嵌套的for循环: :::info for (int[] arr : ns) {
for (int n : arr) {
System.out.print(n);
System.out.print(‘, ‘);
}
System.out.println();
} ::: 或者使用Java标准库的Arrays.deepToString()

  1. import java.util.Arrays;
  2. public class Main {
  3. public static void main(String[] args) {
  4. int[][] ns = {
  5. { 1, 2, 3, 4 },
  6. { 5, 6, 7, 8 },
  7. { 9, 10, 11, 12 }
  8. };
  9. System.out.println(Arrays.deepToString(ns));
  10. }
  11. }
  1. 输出结果如下:

:::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. ┌───┐ ┌──>│ 1 2 3
  3. ┌──>│░░░│──┘ └───┴───┴───┘
  4. ├───┤ ┌───┬───┬───┐
  5. │░░░│─────>│ 4 5 6
  6. ├───┤ └───┴───┴───┘
  7. │░░░│──┐ ┌───┬───┬───┐
  8. ┌───┐ └───┘ └──>│ 7 8 9
  9. ns ────>│░░░│──┘ └───┴───┴───┘
  10. ├───┤ ┌───┐ ┌───┬───┐
  11. │░░░│─────>│░░░│─────>│10 11
  12. ├───┤ ├───┤ └───┴───┘
  13. │░░░│──┐ │░░░│──┐ ┌───┬───┐
  14. └───┘ └───┘ └──>│12 13
  15. └───┴───┘
  16. ┌───┐ ┌───┬───┬───┐
  17. └──>│░░░│─────>│14 15 16
  18. ├───┤ └───┴───┴───┘
  19. │░░░│──┐ ┌───┬───┐
  20. └───┘ └──>│17 18
  21. └───┴───┘

如果我们要访问三维数组的某个元素,例如,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方法; 如何解析命令行参数需要由程序自己实现。

(二)Java的面向对象编程

(二)Java的面向对象编程