Java17 零基础入门手册(四)
六、运算符
前面的章节已经涵盖了 Java 编程的基本概念。你学会了如何组织你的代码,你的文件应该如何命名,你可以使用哪些数据类型,这取决于你要解决的问题。您将学习如何声明字段、变量和方法,以及如何将它们存储在内存中,从而帮助您设计解决方案,使资源消耗达到最优。
在声明变量之后,在这一章中你将学会使用操作符来组合它们。大多数 Java 操作符都是您从数学中学到的,但是因为编程涉及到数字以外的其他类型,所以添加了具有特定用途的额外操作符。在表 6-1 中,列出了所有 Java 操作符及其类别和作用域。
表 6-1
Java 转义序列
|种类
|
操作员
|
范围
|
| --- | --- | --- |
| 铸造 | (类型) | 显式类型转换。 |
| 一元,后缀 | expr++,expr—— | 后期递增/递减。 |
| 一元,前缀 | ++exp,exp | 预递增/递减。 |
| 一元的,逻辑的 | ! | 否定。 |
| 一元、按位 | ~ | 按位补码对整数值执行逐位反转。 |
| 乘法、二进制 | *, /, % | 对于数值类型:乘、除、除并返回余数。 |
| 加法、二进制 | +, - | 对于数值类型:加法、减法。“+”也用于String连接。 |
| 二进制移位 | >>, >>, >>> | 对于数值类型:乘以和除以 2 的幂,有符号和无符号。 |
| 条件的、关系的 | instanceof | 测试对象是否是指定类型(类、子类或接口)的实例。 |
| 条件的、关系的 | ==, !=, <, >, <=, >= | 等于、不同于、小于、大于、小于或等于、大于或等于。 |
| 二进制 | & | 按位逻辑与。 |
| 二进制异或 | ^ | 双态逻辑异或。 |
| 包含或,二元 | | | Bitewise 逻辑 OR。 |
| 条件逻辑 AND | && | 逻辑与。 |
| 条件、逻辑或 | || | 逻辑或。 |
| 条件的、三元的 | ? : | 也被称为猫王操作员。 |
| 作业 | =, +=, -=, *=, /= %=, &=, ^=, <<=,>>=, >>>= ,|= | 简单作业,组合作业。 |
让我们从编程中最常见的操作符开始这一章:赋值操作符“=”。
赋值运算符
“=”赋值操作符显然是编程中使用最多的,因为没有它什么也做不了。你创建的任何变量,不管是什么类型,原语还是引用,都必须在程序中的某一点被赋予一个值。使用赋值操作符设置值非常简单:在“=”操作符的左边是变量名,右边是一个值。赋值生效的唯一条件是值与变量的类型匹配。
为了测试这个操作符,你可以使用jshell来玩一会儿:只要确保你在详细模式下启动它,这样你就可以看到你的赋值的效果。本章执行的语句如清单 6-1 所示。
jshell -v
| Welcome to JShell -- Version 17-ea
| For an introduction type: /help intro
jshell> int i = 0;
i ==> 0
| created variable i : int
jshell> i = -4;
i ==> -4
| assigned to i : int
jshell> String sample = "text";
sample ==> "text"
| created variable sample : String
jshell> List<String> list = new ArrayList<>();
list ==> []
| created variable list : List<String>
jshell> list = new LinkedList<>();
list ==> []
| assigned to list : List<String>
Listing 6-1jshell Play
在前面的例子中,我们声明了原始值和引用值,并给它们赋值和重新赋值。不允许对类型与初始类型不匹配的值进行赋值。在清单 6-2 中的代码示例中,我们试图将一个文本值赋给一个先前声明为int类型的变量。
jshell> i = -5;
i ==> -5
| assigned to i : int
jshell> i = "you are not allowed";
| Error:
| incompatible types: java.lang.String cannot be converted to int
| i = "you are not allowed";
| ^-------------------^
Listing 6-2More jshell Play
JDK 10 中引入的类型推断对此没有影响,变量的类型将根据第一个赋值的类型来推断。显然,这意味着您不能在没有指定初始值的情况下使用var关键字声明变量。这显然排除了null值,因为它没有类型。
这可以通过将null值强制转换成我们感兴趣的类型来实现,如清单 6-3 所示。
jshell> var j;
| Error:
| cannot infer type for local variable j
| (cannot use 'var' on variable without initializer)
| var j;
| ^----^
jshell> var j = 5;
j ==> 5
| created variable j : int
jshell> var sample2 = "bubulina";
sample2 ==> "bubulina"
| created variable sample2 : String
// this does not work, obviously
jshell> var funny = null;
| Error:
| cannot infer type for local variable funny
| (variable initializer is 'null')
| var funny = null;
| ^---------------^
// yes, this actually works !
jshell> var funny = (Integer) null;
funny ==> null
| created variable funny : Integer
Listing 6-3jshell Failed Variable Declaration
显式类型转换(type)和instanceof
这两个操作符放在一起讨论,因为提供与真实场景中可能需要编写的代码非常相似的代码样本更容易。
之前在书中提到过,最好尽可能保持引用类型的通用性,以便在不破坏代码的情况下改变具体的实现。这就是所谓的型多态性。类型多态性是为不同类型的实体提供一个单一的接口,或者使用一个单一的符号来表示多个不同的类型。
有时我们可能需要将对象组合在一起,但是根据它们的类型执行不同的代码。还记得上一章提到的Performer层级吗?我们将在这里利用这些类型来展示如何使用这些操作符。如果你不想回到上一章去记住层次结构,在图 6-1 中它又出现了,但是有了一点变化:一个名为Graphician的额外类被添加到层次结构中,它实现了接口Artist并扩展了类Human 1 。
图 6-1
人类等级制度
在下面的代码示例中,创建了一个类型为Musician的对象和一个类型为Graphician的对象,它们都被添加到包含类型为Artist的引用的列表中。我们可以这样做,因为两种类型都实现了接口Artist。清单 6-4 中的代码显示了这个层次结构中的几个类,它们被用来创建添加到同一个列表中的对象,然后从列表中提取,并测试它们的类型。
package com.apress.bgn.six;
import com.apress.bgn.four.classes.Gender;
import com.apress.bgn.four.hierarchy.*;
import java.util.ArrayList;
import java.util.List;
public class OperatorDemo {
public static void main(String... args) {
List<Artist> artists = new ArrayList<>();
Musician john = new Performer("John", 40, 1.91f, Gender.MALE);
List<String> songs = List.of("Gravity");
john.setSongs(songs);
artists.add(john);
Graphician diana = new Graphician("Diana", 23, 1.62f, Gender.FEMALE, "MacOs");
artists.add(diana);
for (Artist artist : artists) {
if (artist instanceof Musician) { // (*)
Musician musician = (Musician) artist; // (**)
System.out.println("Songs: " + musician.getSongs());
} else {
System.out.println("Other Type: " + artist.getClass());
}
}
}
}
Listing 6-4Code Sample Showing instanceof
and (type) Operators
标有(*)的行显示了如何使用instanceof操作符。该运算符用于测试对象是否是指定类型(类、超类或接口)的实例。它用于编写条件来决定应该执行哪个代码块。
标有(**)的行进行一个引用的显式转换,也称为转换操作。由于instanceof操作符有助于确定引用所指向的对象属于Musician类型,我们现在可以将引用转换为适当的类型,这样就可以调用类Musician的方法。
注意如何使用instanceof操作符来测试类型,然后,为了使用引用,需要编写一个显式转换。从 Java 14 开始,instanceof操作符被丰富成包含了转换,这使得语法更加清晰和简单,如清单 6-5 所示。
for (Artist artist : artists) {
if (artist instanceof Musician musician) {
System.out.println("Songs: " + musician.getSongs());
} else {
System.out.println("Other Type: " + artist.getClass());
}
}
Listing 6-5Java 14 New instanceof Syntax
但是如果显式转换失败了会发生什么呢?为此,我们将尝试将之前声明的Graphician引用转换为音乐家。下面一行可以添加到前面的代码清单中,它不会阻止代码编译。
Musician fake = (Musician) diana;
Graphician类与Musician类型没有关系,所以代码不会运行。控制台中会抛出一个特殊的异常,告诉您发生了什么问题。控制台中打印的错误消息将非常明确,并在下一个日志片段中描述。
Exception in thread "main" java.lang.ClassCastException: class com.apress.bgn.six.Graphician cannot be cast to class com.apress.bgn.four.hierarchy.Musician (com.apress.bgn.six.Graphician is in module chapter.six of loader 'app'; com.apress.bgn.four.hierarchy.Musician is in module chapter.four@1.0-SNAPSHOT of loader 'app')
at chapter.six/com.apress.bgn.six.OperatorDemo.main(OperatorDemo.java:25)
该消息明确指出这两种类型不兼容,并且包含了包和模块名称。
显式转换不限于引用类型;它也适用于原语。在前一章中提到,任何具有较小区间值的类型变量都可以转换为具有较大区间值的类型,而无需显式转换。通过使用显式转换,反过来也是可能的,但是如果值太大,位将丢失,并且值将是意外的。看看清单 6-6 中描述的 byte 和 int 之间转换的例子。
jshell> byte b = 2;
b ==> 2
| created variable b : byte
jshell> int i = 10;
i ==> 10
| modified variable i : int
| update overwrote variable i : int
jshell> i = b
i ==> 2
| assigned to i : int
jshell> b = i
| Error: \\
| incompatible types: possible lossy conversion from int to byte
| b = i
| ^
jshell> b = (byte) i
b ==> 2
| assigned to b : byte
jshell> i = 300_000
i ==> 300000
| assigned to i : int
jshell> b = (byte) i
b ==> -32 // oops! value outside of byte interval
| assigned to b : byte
Listing 6-6jshell Conversions Examples
一般来说,只需使用显式转换来扩大变量的范围,而不是缩小变量的范围,因为缩小变量的范围会导致异常或精度损失。
数值运算符
本节将数字类型上最常用的所有运算符组合在一起。你从 math 上知道的数值运算符:+, -, /, *和比较器在编程中也有,但是可以组合起来得到不同的效果。
一元运算符
一元运算符只需要一个操作数,它们影响应用它们的变量。
增量和减量
在 Java(和其他一些编程语言)中,有一个一元运算符,名为 incrementors( ++)和 decimator(--)。这些运算符放在变量的前面或后面,用于将变量的值增加或减少 1。它们通常在循环中用作计数器,以调节循环的终止。当它们放在变量之前时,叫做前缀,当它们放在变量之后时,叫做后缀。
当它们有前缀时,在下一条语句中使用变量之前,先对变量执行操作。这意味着在清单 6-7 中,i变量的值将递增,然后赋给j。
package com.apress.bgn.six;
public class UnaryOperatorsDemo {
public static void main(String... args) {
int i = 1;
int j = ++i;
System.out.println("j is " + j + ", i is " + i);
}
}
Listing 6-7Prefixed Incrementor Example
前面代码的预期结果是j=2,因为i变量的值在赋给j之前被修改为 2。因此,预期输出为j is 2, i is 2。
当它们是后缀时,在下一个语句中使用该变量之后,对该变量执行操作。这意味着在清单 6-8 中,i的值首先赋给j,之后递增。
package com.apress.bgn.six;
public class UnaryOperatorsDemo {
public static void main(String... args) {
int i = 1;
int j = i++;
System.out.println("j is " + j + ", i is " + i);
}
}
Listing 6-8Prefixed Incrementor Example
前面代码的预期结果是j=1,因为i变量的值在赋值给j后被修改为 2。因此,预期输出为j is 1, i is 2。
递减运算符也可以同样的方式使用;唯一的影响是变量减少了 1。
尝试修改UnaryOperatorsDemo以使用--操作符。
符号运算符
数学运算符+(plus)可以用在单个运算符上,表示一个数是正数(相当多余,大多数情况下从不使用)。所以基本上:
int i = 3;
与以下内容相同:
int i = +3;
数学运算符可用于声明负数。
jshell> int i = -3
i ==> -3
| created variable i : int
或者否定一个表达式:
[jshell> int i = -3
i ==> -3
| created variable i : int
[jshell> int j = - ( i + 4 )
j ==> -1
| created variable j : int
正如你在前面的例子中看到的,( i + 4 )的结果是 1,因为i = -3,但是因为圆括号前面的-,最终赋给j变量的结果是-1。
否定运算符
还有一个一元运算符,它的作用是对变量求反。运算符"!"适用于布尔变量,用于求反。所以true变成了false,而false变成了true,如清单 [6-9 所示。
jshell> boolean t = true
t ==> true
| created variable t : boolean
[jshell> boolean f = !t
f ==> false
| created variable f : boolean
[jshell> boolean t2 = !f
t2 ==> true
| created variable t2 : boolean
Listing 6-9Negating Boolean Values in jshell
二元运算符
二元运算符相当多,有些甚至可以组合起来执行新的运算。这部分从你可能从数学中知道的那些开始。
+(加/加/串联)运算符
"+"用于将两个数值变量相加,如清单 [6-10 中的语句所示。
jshell> int i = 4
i ==> 4
| created variable i : int
jshell> int j = 6
j ==> 6
| created variable j : int
jshell> int k = i + j
k ==> 10
| created variable k : int
jshell> int i = i + 2
i ==> 6
| modified variable i : int
| update overwrote variable i : int
Listing 6-10Adding Numeric Values in jshell
最后一个语句 int i = i + 2的作用是将i的值增加 2,正如你所看到的,这里有一点冗余。这个语句可以写成两次不提到i,因为它的作用是把 I 的值增加 2。这可以通过使用由赋值和加法操作符组成的+=操作符来完成。最佳说法是i += 2。
+操作符也可以用来连接String实例,或者将String实例与其他类型的实例连接起来。JVM 根据上下文决定如何使用+操作符。例如,试着猜测正在执行的清单 6-11 中代码的输出。
package com.apress.bgn.six;
public class ConcatenationDemo {
public static void main(String... args) {
int i1 = 0;
int i2 = 1;
int i3 = 2;
System.out.println(i1 + i2 + i3);
System.out.println("Result1 = " + (i1 + i2) + i3);
System.out.println("Result2 = " + i1 + i2 + i3);
System.out.println("Result3 = " + (i1 + i2 + i3));
}
}
Listing 6-11Concatenating String and int Values
猜得怎么样了?
如果代码被执行,下面的内容将显示在控制台中。
1\. 3
2\. Result1 = 12
3\. Result2 = 012
4\. Result3 = 3
此输出中每一行的解释如下所示:
-
第 1 行的结果可以解释如下:所有操作数的类型都是
int,所以 JVM 将这些项作为int值相加,System.out.println方法打印出这个结果。 -
第 2 行的结果可以解释如下:括号隔离了两个术语
(i1+i2)的相加。因此,JVM 执行圆括号之间的加法,就像对int值的普通加法一样。但是在那之后,我们剩下的是"Result1 = " + 1 + i3,并且这个操作包括一个String操作数,这意味着+操作符必须被用作连接操作符,因为添加一个带有文本值的数字没有其他作用。 -
此时,第 3 行解释中的结果应该是显而易见的:我们有三个
int操作数和一个String操作数,因此 JVM 决定操作的上下文不能是数字,所以需要连接。 -
第 4 行的结果可以用与第 2 行类似的方式来解释。括号确保运算的上下文是数字的,因此添加了三个操作数。
这是一个典型的例子,展示了 JVM 如何决定涉及到+操作符的操作的上下文,您也可以在其他 Java 教程中找到这个例子。但是 int 变量可以被替换为float或double,其行为是相似的。串联也适用于引用类型,因为 any Java 类型默认是Object的扩展,因此可以通过调用其toString()方法转换为String。清单 6-12 显示了一个String和一个Performer实例之间的连接。
package com.apress.bgn.six;
import com.apress.bgn.four.classes.Gender;
import com.apress.bgn.four.hierarchy.Musician;
import com.apress.bgn.four.hierarchy.Performer;
public class ReferenceConcatenationDemo {
public static void main(String... args) {
Musician john = new Performer("John", 43, 1.91f, Gender.MALE);
System.out.println("Singer: " + john);
// or convert explicitly
System.out.println("Singer: " + john.toString());
}
}
Listing 6-12Concatenating String and Performer Values
-(减)运算符
数学运算符-(minus)用于减去两个变量或从一个变量中减去一个值。在清单 6-13 中,您可以看到这个操作符和由赋值操作符组成的-=操作符以及减法操作符是如何使用的。
jshell> int i = 4
i ==> 4
| created variable i : int
jshell> int j = 2
j ==> 2
| created variable j : int
jshell> int k = i - j
k ==> 2
| created variable k : int
jshell> int i = 4
i ==> 4
| modified variable i : int
| update overwrote variable i : int
jshell> i = i - 3
i ==> 1
| assigned to i : int
jshell> int i = 4
i ==> 4
| modified variable i : int
| update overwrote variable i : int
jshell> i -=3
$7 ==> 1
| created scratch variable $7 : int
Listing 6-13Subtracting Numeric Values in jshell
*(乘)运算符
“*”(乘法)运算符用于将两个变量相乘或将一个值与一个变量相乘。可以用在类似于"+"和-的语句中,还有一个复合运算符“*=”,可以用来将一个变量的值相乘并就地赋值。在清单 6-14 中,您可以看到这个操作符在工作。
jshell> int i = 4
i ==> 4
| created variable i : int
jshell> int j = 2
j ==> 2
| created variable j : int
jshell> int k = i * j
k ==> 8
| created variable k : int
jshell> int i = 4
i ==> 4
| modified variable i : int
| update overwrote variable i : int
jshell> i = i * 3
i ==> 12
| assigned to i : int
jshell> int i = 4
i ==> 4
| modified variable i : int
| update overwrote variable i : int
jshell> i *= 3
$7 ==> 12
| created scratch variable $7 : int
Listing 6-14Multiplying Numeric Values in jshell
/(除法)运算符
The "/"(divide)运算符用于将两个变量相除或将一个值除以一个变量。可以在类似语句中使用为“+”、-,还有一个复合运算符“/=”,可以用来对一个变量的值进行除法运算,并当场赋值。
除法的结果被命名为商,,它被赋给赋值运算符(" = ")左侧的变量。当操作数是整数时,结果也是整数,余数被丢弃。在清单 6-15 中,您可以看到这个操作符在工作。
jshell> int i = 4
i ==> 4
| created variable i : int
jshell> int j = 2
j ==> 2
| created variable j : int
jshell> int k = i / j
k ==> 2
| created variable k : int
jshell> int i = 4
i ==> 4
| modified variable i : int
| update overwrote variable i : int
jshell> int i = i / 3
i ==> 1
| modified variable i : int
| update overwrote variable i : int
jshell> int i = 4
i ==> 4
| modified variable i : int
| update overwrote variable i : int
jshell> i /= 3
$7 ==> 1
| created scratch variable $7 : int
Listing 6-15Divide Numeric Values in jshell
%(模数)运算符
"%"也称为模数运算符,用于将两个变量相除,但结果是相除的余数。这个操作叫做模块化 、,还有一个复合操作符%=,可以用来除一个变量的值,并当场分配余数。在清单 6-16 中,您可以看到这个操作符在工作。
jshell> int i = 4
i ==> 4
| created variable i : int
jshell> int j = 3
j ==> 3
| created variable j : int
jshell> int k = i % j
k ==> 1
| created variable k : int
jshell> int i = 4
i ==> 4
| modified variable i : int
| update overwrote variable i : int
jshell> i = i % 3
i ==> 1
| assigned to i : int
jshell> int i = 4
i ==> 4
| modified variable i : int
| update overwrote variable i : int
jshell> i %= 3
$7 ==> 1
| created scratch variable $7 : int
Listing 6-16Modulus Numeric Values in jshell
模数运算符返回余数,但是当操作数是实数时会发生什么呢?
简而言之,浮点数的运算很复杂。它取决于小数点后的位数,以及用于除法的操作数。看一下清单 6-17 。
jshell> double d = 5.28d
d ==> 5.28
| created variable d : double
jshell> d / 2
$2 ==> 2.64
| created scratch variable $2 : double
jshell> d % 2
$4 ==> 1.2800000000000002
| created scratch variable $4 : double
Listing 6-17Modulus Numeric Operations with Floating Point Numbers in jshell
对上述结果的解释是由于浮点数的内部表示方式导致精度损失。
此外,如果余数是一个小数点后有无限小数位数的实数,表示它是不可能的,所以一些四舍五入是必要的。这显示在清单 6-18 中。
jshell> float f = 1.9f
f ==> 1.9
| created variable f : float
jshell> float g = 0.4f
g ==> 0.4
| created variable g : float
jshell> float h = f % g
h ==> 0.29999995 // remainder
| created variable h : float
Listing 6-18Loss of Precision in jshell for a Remainder with an Infinite Number of Decimals After the Decimal Point
jshell中返回的提醒是0.29999995,有些情况下可以四舍五入为0.3。但是,当数据用于敏感操作时,四舍五入可能是危险的,例如确定机器人手术的肿瘤体积或发射到火星的火箭的完美轨道。
浮点数的舍入是有问题的,因为它会导致精度的损失。
使用浮点数时精度的损失不是 Java 的问题,因为根据 IEEE754 2 算法的规则支持使用浮点数的运算。
如果一个项目需要更高精度的数学运算,java.lang.Math类提供了不同类型的舍入和其他类型的浮点数运算的方法。
关系运算
在某些情况下,当设计问题的解决方案时,您需要引入条件来驱动和控制执行流。条件要求使用比较运算符对两个项之间的比较进行评估。本节描述了 Java 中使用的所有比较运算符,并提供了代码示例。我们继续吧。
==等于运算符
测试术语的相等性。因为在 Java 中,单个等号(" = ")用于赋值,所以引入了双等号来测试相等性并避免混淆。该操作符经常用于控制执行流。控制执行流是下一章的主题,但是为了说明如何使用"=="操作符,本章将介绍几个简单的代码示例,包括if和for等控制语句。在清单 6-19 中,你可以看到一个测试"=="比较器在数组中搜索值 2 的例子。如果找到该值,则在控制台中打印找到该值的索引。
package com.apress.bgn.six;
public class ComparisonOperatorsDemo {
public static void main(String... args) {
int[] values = {1, 7, 9, 2, 6,};
for (int i = 0; i < values.length; ++i) {
if (values[i] == 2) {
System.out.println("Fount 2 at index: " + i);
}
}
}
}
Listing 6-19Example for Using the "==" Operator to Test a Value in an Array
对标记行中的条件进行评估,结果是一个布尔值。当结果为false时,什么都不做,但是如果结果为true则打印索引。因为结果是布尔类型的,如果您犯了一个错误,使用了=而不是==,代码将无法编译。
在比较布尔值时,你必须格外小心。清单 5-20 中的代码可以编译并运行,但是它不能像预期的那样工作。
package com.apress.bgn.six;
public class BadAssignementDemo {
public static void main(String... args) {
boolean testVal = false;
if(testVal = true) {
System.out.println("TestVal got initialized incorrectly!");
} else {
System.out.println("TestVal is false? " + (testVal == false));
}
}
}
Listing 6-20Example of an Unexpected Initialization of a Boolean Variable Instead of an Evaluation of Its Value
“==”符号对于原语来说很好。对于引用类型,在解释栈和堆内存的区别时,需要使用本书前面章节 开头的equals()方法。
其他比较运算符
其他比较运算符只作用于基本类型。由于没有太多关于它们的内容,本节将一一介绍。
- 测试术语的不相等性。它与
==运算符相反。这个操作符也作用于引用类型,但是它比较的是引用值而不是对象本身,就像==一样。
作为一个练习,修改清单 6-19 中的例子,当数组元素值不等于 2 时打印一条消息。
-
<和<=的用途和你可能在数学课上学过的一样。第一个(<)测试操作符左边的项目是否小于右边的项目。下一个(<=)测试运算符左边的项目是否小于或等于右边的项目。此运算符不能用于引用类型。 -
>和>=的用途和你可能在数学课上学过的一样。第一个(>)测试操作符左边的项目是否大于右边的项目。下一个(>=)测试运算符左边的项目是否大于或等于右边的项目。此运算符不能用于引用类型。
几乎所有数值运算符都可以用于不同类型的变量,因为它们会自动转换为具有更宽区间表示的类型。清单 6-21 中的代码反映了一些情况,但是在实践中,你可能需要做出更加极端的事情,这些事情并不总是遵守编程的常识规则,也没有遵循好的实践。不过,如果可以的话,尽量避免这样做!
package com.apress.bgn.six;
public class MixedOperationsDemo {
public static void main(String... args) {
byte b = 1;
short s = 2;
int i = 3;
long l = 4;
float f = 5;
double d = 6;
int ii = 6;
double resd = l + d;
long resl = s + 3;
//etc
if (b <= s) {
System.out.println("byte val < short val");
}
if (i >= b) {
System.out.println("int val >= byte val");
}
if (l > b) {
System.out.println("long val > byte val");
}
if(d > i) {
System.out.println("double val > byte val");
}
if(i == i) {
System.out.println("double val == int val");
}
}
}
Listing 6-21Different Primitive Types Comparison Examples
只要确保你曾经处于这样一种情况,你需要做一些可疑的事情*(非优化代码构造)*像这些来进行大量的测试,并且认为你的转换是好的,特别是当涉及到浮点类型的时候。这是因为(例如)清单 6-22 中的这段代码可能会产生意想不到的结果。
package com.apress.bgn.six;
public class BadDecimalPointDemo {
public static void main(String... args) {
float f1 = 2.2f;
float f2 = 2.0f;
float f3 = f1 * f2;
if (f3 == 4.4) {
System.out.println("expected float value of 4.4");
} else {
System.out.println("!! unexpected value of " + f3);
}
}
}
Listing 6-22Unexpected Comparison Results with Floating Numbers
如果您期望控制台中打印出消息预期浮点值为 4.4 ,您将会非常惊讶。
任何 IEEE 754 浮点数表示都会出现问题,因为一些数字在十进制中看起来有固定的小数位数,实际上在二进制中有无限多的小数位数。所以显然我们不能用==来比较浮点数和双精度数。最容易实现的解决方案之一是使用包装类提供的比较方法,在本例中是Float.compare,如清单 6-23 所示。
package com.apress.bgn.six;
public class GoodDecimalPointDemo {
public static void main(String... args) {
float f1 = 2.2f;
float f2 = 2.0f;
float f3 = f1 * f2;
if (Float.compare(f3,4.4f) == 0) {
System.out.println("expected float value of 4.4");
} else {
System.out.println("!!unexpected value of " + f3);
}
}
}
Listing 6-23Correct Comparison Results with Float.compare
使用前面的例子,预期的消息现在被打印在控制台中:预期的浮点值 4.4 。
按位运算符
在 Java 中,有几个操作符用于位级操作数值类型的变量。按位运算符用于改变操作数中的各个位。由于减少了资源的使用,位运算速度更快,通常使用的 CPU 处理能力也更少。它们在编程视觉应用(例如游戏)时最有用,在这些应用中,颜色、鼠标点击和移动应该被快速确定,以确保令人满意的体验。
按位非
运算符~有点像二元运算符的反运算符。Is 执行整数值的逐位反转。这会影响用于表示该值的所有位。所以如果我们宣布
byte b1 = 10;
二进制表示是00001010。Integer类提供了一个名为toBinaryString()的方法,可以用来打印之前定义的变量的二进制表示,但是它不会打印所有的位,因为这个方法不知道我们想要多少位的表示。所以我们需要使用一个特殊的String方法来格式化输出。清单 6-24 中描述的方法可以用来打印 8 位二进制的b1值,正如前面提到的。
public static void print8Bits(byte arg) {
System.out.println("decimal:" + arg);
String str =
String.format("%8s", Integer.toBinaryString(arg)).replace(' ', '0');
System.out.println("binary:" + str);
}
Listing 6-24Method Used to Print Each Bit of a byte Value
如果我们对b1值应用~操作符,得到的二进制值就是11110101。如果您没有注意到,该值超出了byte间隔范围,并自动转换为int。这就是负数在 Java 中的内部表示方式——根据 Java 语言规范,这种表示方式称为 2 的补码。(这一点将在本章末尾讨论。)
因此结果将是-11,如清单 6-25 中的代码所示:
package com.apress.bgn.six;
BitwiseDemo
public class BitwiseDemo {
public static void main(String... args) {
byte b1 = 10;
print8Bits(b1);
byte b2 = (byte) ~b1;
print8Bits(b2);
}
// print8Bits method omitted
}
// execution result
decimal:10
binary:00001010
decimal:-11
binary:11111111111111111111111111110101
Listing 6-25Testing the ~ Bitwise Negator Operator
在前面的代码清单中,您可能注意到了这个语句字节b2 = (byte) ~b1,并且希望得到解释。按位补码表达式运算符需要一个可转换为基元整数类型的操作数,否则会发生编译时错误。在内部,Java 使用一个或多个字节来表示值。~运算符将其操作数转换为int类型,因此在进行补码运算时可以使用 32 位;这是避免精度损失所必需的。这就是为什么在前面的例子中需要显式转换为byte。因为有了图像,一切都变得更加清晰,在图 6-2 中,你可以看到~对b1变量位的影响,与其值平行。
图 6-2
negator 运算符对字节值中每一位的影响
按位 AND
按位AND运算符由&表示,它逐位比较两个数字。如果相同位置上的位的值为1,则结果中的位将为1。清单 6-26 中的代码示例描述了&操作符的结果。
package com.apress.bgn.six;
public class BitwiseDemo {
public static void main(String... args) {
byte b1 = 117; // 01110101
print8Bits(b1);
byte b2 = 95; // 01011111
print8Bits(b2);
byte result = (byte) (b1 & b2); // 01010101
print8Bits(result);
}
// print8Bits method omitted
}
// execution result
decimal:117
binary:01110101
decimal:95
binary:01011111
decimal:85
binary:01010101
Listing 6-26Testing the & Bitwise AND Operator
在图 6-3 中可以更好地看到&操作符的效果。01010101值是十进制数 85 的二进制表示。
图 6-3
&运算符对每一位的影响
此外,出于实际原因,Java 中提供了组合操作符&=,以便可以对结果所赋给的同一个变量进行按位AND操作,如清单 6-27 所示。这样做的好处是结果会自动转换成byte,所以不需要显式转换。
jshell> byte b1 = 117
b1 ==> 117
| created variable b1 : byte
jshell> b1 &= 95
$2 ==> 85
| created scratch variable $2 : byte
Listing 6-27Testing the &= Bitwise AND Operator in jshell
按位异或
按位OR运算符(也称为包含 or)由|(管道)表示,它逐位比较两个数字,如果至少一个位为 1,则结果中的位被设置为 1。清单 6-28 中的代码描述了|操作符的结果。
package com.apress.bgn.six;
public class BitwiseDemo {
public static void main(String... args) {
byte b1 = 117; // 01110101
print8Bits(b1);
byte b2 = 95; // 01011111
print8Bits(b2);
byte result = (byte) (b1 | b2); // 01111111
print8Bits(result);
}
// print8Bits method omitted
}
// execution result
decimal:117
binary:01110101
decimal:95
binary:01011111
decimal:127
binary:01111111
Listing 6-28Testing the | Bitwise OR Operator
在图 6-4 中可以更好地看到|操作符的效果。01111111值是数字 127 的二进制表示。
图 6-4
|运算符对每一位的影响
此外,出于实际原因,Java 中提供了复合运算符|=,以便可以对结果所赋给的同一个变量进行按位异或运算,如清单 6-29 所示。这样做的好处是结果会自动转换成byte,所以不需要显式转换。
jshell> byte b1 = 117
b1 ==> 117
| created variable b1 : byte
jshell> b1 |= 95
$2 ==> 127
| created scratch variable $2 : byte
Listing 6-29Testing the |= Bitwise OR Operator in jshell
按位异或
按位异或或异或运算符由^表示,它逐位比较两个数,如果两位的值不同,则结果中的位被设置为 1。清单 6-30 中的代码示例描述了^操作符的结果。
package com.apress.bgn.six;
public class BitwiseDemo {
public static void main(String... args) {
byte b1 = 117; // 01110101
print8Bits(b1);
byte b2 = 95; // 01011111
print8Bits(b2);
byte result = (byte) (b1 ^ b2); // 00101010
print8Bits(result);
}
// print8Bits method omitted
}
// execution result
decimal:117
binary:01110101
decimal:95
binary:01011111
decimal:42
binary:00101010
Listing 6-30Testing the ^ Bitwise XOR Operator
在图 6-5 中可以更好地看到^操作符的效果。00101010值是数字 42 的二进制表示。
图 6-5
^运算符对每一位的影响
此外,出于实际原因,Java 中提供了复合运算符^=,以便可以对结果所赋给的同一个变量进行按位异或运算,如清单 6-31 所示。这样做的好处是结果会自动转换成byte,所以不需要显式转换。
jshell> byte b1 = 117
b1 ==> 117
| created variable b1 : byte
jshell> b1 ^= 95
$2 ==> 42
| created scratch variable $2 : byte
Listing 6-31Testing the ^= Bitwise OR Operator in jshell
逻辑运算符
当设计用于控制程序执行流程的条件时,有时需要编写复杂的条件:由多个表达式构造的复合条件。有四种运算符可用于构造复杂的条件。其中两个是可以重用的位运算&(AND)和|(OR),但是它们需要对条件的所有部分进行求值。其他操作符&&(AND)和||(OR)与前面提到的操作符具有完全相同的效果,但不同之处在于它们不需要对所有表达式求值,这就是为什么它们也被称为快捷操作符。为了解释这些操作符的行为,有一个典型的例子可以使用。
在清单 6-32 中,我们声明了一个包含 10 个术语的列表(其中一些是null)和一个生成随机索引的方法,该索引用于从列表中选择一个条目。然后我们测试从列表中选择的元素,看看它是否不是null并且等于一个期望值。如果两个条件都为真,则在控制台中打印一条消息。让我们看看第一个例子。
package com.apress.bgn.six;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class LogicalDemo {
static List<String> terms = new ArrayList<>() {{
add("Rose");
add(null);
add("River");
add("Clara");
add("Vastra");
add("Psi");
add("Cas");
add(null);
add("Nardhole");
add("Strax");
}};
public static void main(String... args) {
for (int i = 0; i < 20; ++i) {
int index = getRandomIndex(terms.size());
String term = terms.get(index);
System.out.println("Generated index: " + index);
if (term != null & term.equals("Rose")) {
System.out.println("Rose was found");
}
}
}
private static int getRandomIndex(int listSize) {
Random r = new Random();
return r.nextInt(listSize);
}
}
Listing 6-32Testing the & Operator to Control the Execution Flow
为了确保得到预期的结果,我们将从列表中选择一个随机元素的操作重复 20 次。正如您可能注意到的,在标记的行中,按位&用于组合两个表达式。只有当变量term的值不是null并且等于Rose时,您才会期望控制台中打印出文本“Rose found”。但是,当运行前面的代码时,会打印出以下内容:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "term" is null
at chapter.six/com.apress.bgn.six.LogicalDemo.main(LogicalDemo.java:56)
这是因为两个表达式都被求值。但是想想吧!如果term变量是null,我们甚至应该评估它与Rose的相等性吗,尤其是在调用null对象的方法会导致运行时错误的情况下?显然不是,这就是为什么&不适合这种情况。如果 term 是null,那么它不满足第一个条件,对第二个条件求值也没有意义,所以输入&&快捷操作符,它就能做到这一点。这是因为当使用逻辑AND操作符时,如果第一个表达式被求值为false,那么第二个表达式被求值为什么并不重要;结果永远是false。因此,我们可以将前面的代码示例更正为清单 6-33 中的代码示例。
package com.apress.bgn.six;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class LogicalDemo {
static List<String> terms = new ArrayList<>() {{
/* list elements omitted */}};
public static void main(String... args) {
for (int i = 0; i < 20; ++i) {
int index = getRandomIndex(terms.size());
String term = terms.get(index);
System.out.println("Generated index: " + index);
if (term != null && term.equals("Rose")) {
System.out.println("Rose was found");
}
}
}
// getRandomIndex method omitted
}
Listing 6-33Testing the && Operator to Control the Execution Flow
当执行代码时,不会抛出异常,因为如果term是null,则第二个表达式不会被求值。因此,这段代码在技术上更有效,因为它评估的条件更少,但它也设计得更好,因为它避免了失败。
现在,让我们修改前面的代码示例,这一次,如果我们找到了null或Rose,我们将打印一条消息。为此需要一个 or 运算符,所以我们将首先尝试使用按位版本(清单 6-34 ):
package com.apress.bgn.six;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class LogicalDemo {
static List<String> terms = new ArrayList<>() {{
/* list elements omitted */}};
public static void main(String... args) {
for (int i = 0; i < 20; ++i) {
int index = getRandomIndex(terms.size());
String term = terms.get(index);
System.out.println("Generated index: " + index);
if (term == null | term.equals("Rose")) {
System.out.println("null or Rose was found");
}
}
}
// getRandomIndex method omitted
}
Listing 6-34Testing the && Operator to Control the Execution Flow
如果我们运行前面的代码,当随机索引恰好匹配列表中的一个null元素的索引时,就会抛出一个NullPointerException。这是因为|操作符要求对两个表达式都求值,所以如果term为空,调用term.equals(..)将导致抛出异常。因此,为了确保代码按预期工作,必须用||替换|,这简化了条件,并且不计算其中的第二个表达式,除非第一个条件的计算结果是false。这是因为当使用逻辑 OR 操作符时,如果第一个表达式的计算结果是true,那么第二个表达式的计算结果是什么并不重要,结果总是true。我们将把它作为一个练习留给你。
条件可以由多个表达式和多个运算符组成,无论是&&还是||。清单 6-35 中的代码描述了一些复杂的情况。
package com.apress.bgn.six;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class ComplexConditionsDemo {
static List<String> terms = new ArrayList<>() {{
/* list elements omitted */}};
public static void main(String... args) {
for (int i = 0; i < 20; ++i) {
int rnd = getRandomIndex(terms.size());
if (rnd == 0 || rnd == 1 || rnd <= 3) {
System.out.println(rnd + ": this works...");
}
if (rnd > 3 && rnd <=6 || rnd < 3 && rnd > 0) {
System.out.println(rnd + ": this works too...");
}
}
}
private static int getRandomIndex(int listSize) {
Random r = new Random();
return r.nextInt(listSize);
}
}
Listing 6-35Complex Conditions Composed from Multiple Expressions
当心变得太复杂的情况;确保用大量测试覆盖这段代码。在编写复杂的条件时,一些表达式可能会变得多余,IntelliJ IDEA 和其他智能编辑器会在多余和未使用的表达式上显示死代码警告,以帮助开发人员改进代码设计。
移位运算符
移位操作符是在比特级工作的操作符。因为移动位是一个敏感的操作,所以这些操作数的要求是参数必须是整数。运算符左边的操作数是将要移位的数字,运算符右边的操作数是将要移位的位数。
Java 中有三个移位操作符,每一个都可以和赋值操作符组合起来进行移位,并将结果就地赋给原变量。本节通过简单的例子和图像来分析所有的移位操作符,使事情变得清晰。
<<左移运算符
顾名思义,给定一个用二进制表示的数,这个运算符用于向左移位。清单 6-36 中的代码显示了运行中的<<左移操作符。
package com.apress.bgn.six;
public class ShiftDemo {
public static void main(String... args) {
byte b1 = 12; // 00001100
print8Bits(b1);
byte b2 = (byte) (b1 << 3); // 01100000
print8Bits(b2);
}
// print8Bits method omitted
}
// execution result
decimal:12
binary:00001100
decimal:96
binary:01100000
Listing 6-36Testing the << Operator
当位向左移位时,剩余的位置用 0 填充。同样,数字变大了,新值是它的旧值乘以2^N,其中N是第二个操作数。
清单 6-36 中的代码可以像b1 <<= 3一样编写,使用复合操作符,不需要声明另一个变量。结果是12 * 2³。如图 6-6 所示移位。
图 6-6
`<
移位运算符将
byte值提升为int,以避免精度损失。在前面的代码示例中,要移位的位数很小,足以产生一个位于byte类型区间内的值。这就是为什么显式转换为byte有效,并且结果仍然有效。这并不总是可能的,正如您将在本节中进一步看到的那样。
>>符号右移运算符
顾名思义,给定一个用二进制表示的数,这个运算符用于向右移位。清单 6-37 中的代码显示了运行中的>>右移操作符。
package com.apress.bgn.six;
public class ShiftDemo {
public static void main(String... args) {
byte b1 = 96; // 01100000
print8Bits(b1);
byte b2 = (byte) (b1 >> 3); // 00001100
print8Bits(b2);
}
// print8Bits method omitted
}
// execution result
decimal:96
binary:01100000
decimal:12
binary:00001100
Listing 6-37Testing the >> Operator
当位向右移位时,如果数字为正数,则剩余的位置用 0 填充。如果数字为负数,则剩余的位置将被替换为 1。这样做是为了保留数字的符号。同样,数字变小,新值是它的旧值除以2^N,其中N是第二个操作数。
清单 6-37 中的代码可以写成b1 >>= 3,使用复合运算符,无需声明另一个变量。结果是12 * 2³。如图 6-7 所示移位。
图 6-7
>>运算符的作用
图 6-7 和清单 6-37 都显示了应用于正数的右移运算符。当涉及到负数时,事情就变得复杂了,因为负数在内部被表示为 2 的补码。这是什么意思?这意味着,为了得到负数的表示,我们得到正数的表示,我们翻转这些位,然后加 1。图 6-8 描绘了从7的表示开始,获取-7的内部表示的过程。
图 6-8
用二进制补码在内部表示负数
2 的补码表示中的-7值在byte范围之外,因此内部负数表示为整数。这意味着print8Bits(..)方法需要被替换为打印所有 32 位int值的版本。清单 6-38 显示了应用于负数的>>无符号右移运算符。
package com.apress.bgn.six;
public class ShiftDemo {
public static void main(String... args) {
System.out.println( " -- ");
int i1 = -96;
print32Bits(i1);
int i2 = i1 >> 3;
print32Bits(i2);
}
public static void print32Bits(int arg) {
System.out.println("decimal:" + arg);
String str = arg > 0 ?
String.format("%32s", Integer.toBinaryString(arg)).replace(' ', '0'):
String.format("%32s", Integer.toBinaryString(arg)).replace(' ', '1');
System.out.println("binary:" + str);
}
}
// execution result
decimal:-96
binary:11111111111111111111111110100000
decimal:-12
binary:11111111111111111111111111110100
Listing 6-38Testing the >> Operator with Negative Numbers
二进制补码表示的一个优点是,算术运算对于有符号和无符号运算符是相同的,这意味着 cpu 的算术逻辑单元需要一半的电路。
二进制补码表示的一个奇特之处在于
-Integer.MAX_VALUE和Integer.MIN_VALUE以相同的方式表示。
>>>无符号右移运算符
>>>无符号右移运算符也叫逻辑移位。给定一个用二进制表示的数,该运算符用于将位向右移位,剩余的位置用 0 替换,而不管该值是正还是负。这就是为什么结果总是正数。
清单 6-39 显示了>>>无符号右移操作符对负值的作用。
package com.apress.bgn.six;
public class ShiftDemo {
public static void main(String... args) {
int i1 = -16;
print32Bits(i1);
int i2 = i1 >>> 1;
print32Bits(i2);
}
// print32Bits method omitted
}
// execution result
decimal:-16
binary:11111111111111111111111111110000
decimal:2147483640
binary:01111111111111111111111111111000
Listing 6-39Testing the >>> Operator with Negative Values
清单 6-39 中的代码可以像i1 >>>= 1一样编写,使用组合操作符,不需要声明另一个变量。结果是一个非常大的正数。如图 6-9 所示移位。
图 6-9
>>>运算符对负值的影响
与所有按位运算符一样,移位运算符将char、byte或short类型变量提升为int,这就是为什么显式转换是必要的。你可能已经注意到,对负数进行移位是很棘手的;结果数字很容易超出类型允许值的区间,显式转换会导致精度损失,甚至严重异常。那么为什么要使用它们呢?因为他们速度很快。只要确保在使用移位运算符时进行密集测试即可。
猫王接线员
****猫王运算符是 Java 中唯一的三元运算符。它的功能相当于一个 Java 方法,该方法评估一个条件,并根据结果返回值。Elvis 操作员的模板如下所示:
variable = (condition) ? val1 : val2
清单 6-40 中描述了与该运算符等效的方法。
variable = methodName(..);
type methodName(..) {
if (condition) {
return val1;
} else {
return val2;
}
}
Listing 6-40The Elvis Operator Equivalent Method
这个运算符被命名为 Elvis 运算符的原因是,问号类似于 Elvis Presley 的头发,而冒号类似于眼睛。Elvis 操作员可以在jshell中轻松测试,如清单 6-41 所示。
jshell> int a = 4
a ==> 4
| created variable a : int
jshell> int result = a > 4 ? 3 : 1;
result ==> 1
| created variable result : int
jshell> String a2 = "test"
a2 ==> "test"
| created variable a2 : String
jshell> var a3 = a2.length() > 3 ? "hello" : "bye-bye"
a3 ==> "hello"
| created variable a3 : String
Listing 6-41The Elvis Operator Being Tested in jshell
当您有一个简单的if语句,每个分支只包含一个表达式时,这个操作符非常实用,因为使用这个操作符,您可以将所有内容压缩到一个表达式、一行代码中。只要确保在使用它时,代码的可读性得到了提高。从性能的角度来看,if语句和等效的 Elvis 操作符表达式没有区别。使用 Elvis 操作符的另一个优点是表达式可以用来初始化变量。
摘要
在本章中,我们了解到:
-
Java 有很多操作符,简单的,复合的。
-
按位运算速度很快,但是很危险。
-
负数在内部用二进制补码表示。
-
操作符在不同的上下文中做不同的事情。
-
Java 有一个三元运算符,它接受三个操作数:一个布尔表达式和两个相同类型的对象。布尔表达式的求值结果决定了哪个操作数是语句的结果。
本章的目的只是让你熟悉整本书中用到的所有操作符,帮助你理解提供的解决方案,甚至设计和编写你自己的解决方案。
Footnotes 1新类的实现与本章无关,所以这里不详细介绍,但是你可以在本书附带的项目中找到它。
2
关于浮点运算的 IEEE 标准的描述可以在维基百科上找到,“IEEE 754,” https://en.wikipedia.org/wiki/IEEE_754 ,,2021 年 10 月 15 日访问。
**
七、控制流程
前面几章已经介绍了创建语句的方法,以及根据操作数类型使用什么运算符。在前面的章节中,有时会添加一些逻辑元素来使代码可以运行,本章将详细解释如何使用基本的编程条件语句和重复语句来操作代码的执行。一个解决方案,一个算法可以用流程图来表示。
到本章为止,我们所做的大多数编程都包含声明和打印语句,简单的单步执行语句。看看清单 7-1 中的这段代码。
package com.apress.bgn.seven;
public class Main {
public static void main(String... args) {
String text = "sample";
System.out.println(text);
}
}
Listing 7-1Java Code Made of a Few Statements
如果我们要为它设计一个流程图,这个模式将是简单的和线性的,没有决策,也没有重复,如图 7-1 所示。
图 7-1
简单流程图示例
解决现实世界的问题通常需要比这更复杂的逻辑,因此更复杂的语句是必要的。在此之前,让我们描述一下流程图的组成部分,因为在本章中我们会用到很多。在表 7-1 中,列出了所有流程图元素,并解释了它们的用途。
表 7-1
流程图元素
|形状
|
名字
|
范围
|
| --- | --- | --- |
| | 末端的 | 指示程序的开始或结束,并包含与其范围相关的文本。 |
|
| 流线 | 表示程序的流程和操作的顺序。 |
|
| 输入/输出 | 指示变量声明和输出值。 |
|
| 过程 | 简单的流程语句:赋值、值的改变等等。 |
|
| 决定 | 显示了决定特定执行路径的条件操作。 |
|
| 预定义流程 | 此元素表示在别处定义的流程。 |
|
| 页面连接器 | 该元素通常带有标签,表示同一页面上的流的延续。 |
|
| 离页连接器 | 该元素通常带有标签,表示流在不同页面上的延续。 |
|
| 注释(或注解) | 当一个流或一个元素需要额外的解释时,就使用这种类型的元素来引入它。 |
上表中的流程图元素非常标准;您可能会在任何编程课程或教程中发现非常相似的元素。经过这种一致的介绍,才适合进入其中。
if-else声明
Java 中最简单的决策流语句是if-else语句(可能在其他语言中也是如此)。你可能已经在前面章节的代码示例中看到过if-else语句;这是无法避免的,因为提供可运行的代码来鼓励您编写自己的代码是非常重要的。在本节中,重点将严格放在这种类型的陈述上。
让我们想象这样一个场景:我们用用户提供的数字参数运行一个 Java 程序。如果数字是偶数,我们在控制台中打印偶数;否则,我们打印奇数。图 7-2 描述了与该场景相匹配的流程图。
图 7-2
if-else流程图示例
该条件被评估为一个boolean值:如果结果为true,则执行对应于if分支的语句,如果结果为false,则执行对应于 else 分支的语句。
清单 7-2 中描述了实现该流程图所描述的过程的 Java 代码。
package com.apress.bgn.seven;
public class IfFlowDemo {
public static void main(String... args) {
int a = Integer.parseInt(args[0]);
if (a % 2 == 0) { // is even
//Display EVEN
System.out.println("EVEN");
} else {
//Display ODD
System.out.println("ODD");
}
}
}
Listing 7-2Java Code with if-else Statement
要用不同的参数运行这个类,你必须创建一个 IntelliJ 启动器,并将你的参数添加到Program arguments文本字段,就像本书开头解释的那样。前面代码片段中的每个 Java 语句都配有一个与流程图元素匹配的注释,以使实现显而易见。有趣的是,并不是一条if语句的两个分支都是强制的,else分支并不总是必要的。
有时,如果一个值正好匹配某个条件,您只想打印一些内容,而对其他情况不感兴趣。例如,给定一个用户提供的参数,如果数字是负数,我们只想打印一条消息,但是如果数字是正数,我们对打印或做任何事情都不感兴趣。其流程图如图 7-3 所示。
图 7-3
if流程图示例,缺少 else 分支
清单 7-3 中描述了 Java 代码。
package com.apress.bgn.seven;
public class IfFlowDemo {
public static void main(String... args) {
int a = Integer.parseInt(args[0]);
if (a < 0) {
System.out.println("Negative");
}
}
}
Listing 7-3Java Code with if Statements
同样的,语句也可以变得简单,同样的,如果我们需要,我们可以将更多的if-else语句链接在一起。让我们考虑下面的例子:用户插入一个从 1 到 12 的数字,我们必须打印该数字对应的月份的季节。流程图会是什么样子?你认为图 7-4 符合这个场景吗?
图 7-4
复杂if-else流程图示例
此外,当
if或else的代码块包含一条语句时,花括号不是强制性的,但大多数开发人员保留它们是为了代码清晰,并帮助 ide 正确缩进代码。
看起来很复杂,对吧?等待直到您看到代码,如清单 7-4 所示。
package com.apress.bgn.seven;
public class SeasonDemo {
public static void main(String... args) {
int a = Integer.parseInt(args[0]);
if(a == 12 || (a>=1 && a<= 2)) {
System.out.println("Winter");
} else {
if (a>2 && a <= 5 ) {
System.out.println("Spring");
} else {
if (a>5 && a <= 8 ) {
System.out.println("Summer");
} else {
if (a>8 && a <= 11 ) {
System.out.println("Autumn");
} else {
System.out.println("Error");
}
}
}
}
}
}
Listing 7-4Java Code with a Lot of if-else Statements
看起来很丑吧?幸运的是,Java 提供了一种简化它的方法,特别是因为拥有这么多只包含另一个if语句的else块实在没有意义。简化的代码将else语句与包含的if(s)语句连接起来。代码最终看起来如清单 7-5 所示。
package com.apress.bgn.seven;
public class CompactedSeasonDemo {
public static void main(String... args) {
int a = Integer.parseInt(args[0]);
if (a == 12 || (a >= 1 && a <= 2)) {
System.out.println("Winter");
} else if (a > 2 && a <= 5) {
System.out.println("Spring");
} else if (a > 5 && a <= 8) {
System.out.println("Summer");
} else if (a > 8 && a <= 11) {
System.out.println("Autumn");
} else {
System.out.println("Error");
}
}
}
Listing 7-5Java Code with Compacted if-else Statements
用户提供的不是[1,12]的任何参数都会导致程序打印错误。您可以通过修改 IntelliJ Idea 启动器来亲自测试它。图 7-5 中强调了需要关注的要素。
图 7-5
IntelliJ IDEA 启动器和参数
switch声明
当一个值需要对一组固定的值进行不同的操作时,if可能会变得更复杂,这组值增加得越多。在这种情况下,更合适的语句是switch语句。让我们先看看清单 7-6 中的代码,然后看看还有哪些可以改进的地方。
package com.apress.bgn.seven.switchst;
public class SeasonSwitchDemo {
public static void main(String... args) {
int a = Integer.parseInt(args[0]);
var season = "";
switch (a) {
case 1:
season = "Winter";
break;
case 2:
season = "Winter";
break;
case 3:
season = "Spring";
break;
case 4:
season = "Spring";
break;
case 5:
season = "Spring";
break;
case 6:
season = "Summer";
break;
case 7:
season = "Summer";
break;
case 8:
season = "Summer";
break;
case 9:
season = "Autumn";
break;
case 10:
season = "Autumn";
break;
case 11:
season = "Autumn";
break;
case 12:
season = "winter";
break;
default:
System.out.println("Error");
}
System.out.println(season);
}
}
Listing 7-6Java Code with Detailed switch Statement
这看起来不太实际,至少对于这个场景来说是这样。在展示如何以不同的方式编写switch语句之前,让我们先解释一下它的结构和逻辑。清单 7-7 中描述了switch语句的通用模板:
switch ([onvar]) {
case [option]:
[statement;]
break;
...
default:
[statement;]
}
Listing 7-7General Template of the switch Statement
方括号中的术语在下面的列表中有详细说明:
-
[onvar]是根据 case 语句测试以选择语句的变量。它可以是任何原始类型、枚举,从 Java 7、String开始。显然,switch 语句不受评估为布尔结果的条件的限制,这允许很大的灵活性。 -
case [option]是前面提到的变量的一个值,根据它来决定要执行的语句。一个案例,如关键词所述。 -
[statement]是在[onvar] == [option]时执行的一条或一组语句。考虑到没有else分支,我们必须确保只执行与第一个匹配相对应的语句,这就是break;语句的用武之地。break语句停止当前的执行路径,并将执行点移到包含它的语句之外的下一条语句。没有break;语句,行为切换到fall through,这意味着匹配后的每个case语句都被执行,直到找到break;。我们将在这一章的后面详细介绍它。如果没有它,在第一次匹配后,将遍历所有后续事例,并执行与之对应的语句。 -
如果我们执行前面的程序并提供数字 7 作为参数,文本 Summer 将被打印出来。但是,如果对案例 7 和 8 的 break 语句进行注释,输出将变为秋季。
-
default [statement;]是当在case上没有找到匹配时执行的语句;default案不需要break声明。如果前一个程序以[1-12]间隔之外的任何数字运行,将打印错误,因为将执行默认语句。
既然你已经理解了switch是如何工作的,那么让我们来看看如何减少前面的语句。月份的例子在这里是合适的,因为它可以进一步修改,以显示如何简化 switch 语句,当一个语句应该执行多种情况时。在我们的代码中,每个赋值语句写三次有点多余。还有很多break;的说法。有两种方法可以改进前面的switch陈述。
简化清单 7-6 中 switch 语句的第一种方法是将返回值相同的情况组合在一起,如清单 7-8 所示。
package com.apress.bgn.seven.switchst;
public class SimplifiedSwitchDemo {
public static void main(String... args) {
int a = Integer.parseInt(args[0]);
var season = "";
switch (a) {
case 1:
case 2:
case 12:
season = "winter";
break;
case 3:
case 4:
case 5:
season = "Spring";
break;
case 6:
case 7:
case 8:
season = "Summer";
break;
case 9:
case 10:
case 11:
season = "Autumn";
break;
default:
System.out.println("Error");
}
System.out.println(season);
}
}
Listing 7-8Simplified switch Statement
这种情况下的分组表示需要执行相同语句的情况的对齐。这看起来仍然有点奇怪,但是它减少了语句的重复。前一种情况下的行为是可能的,因为每个没有break语句的case后面都跟着下一个case语句。
第二种方法是使用 Java 12 中引入的一个switch表达式。switch直接返回季节,而不是将它存储在变量中,这样可以简化语法,如清单 7-9 所示。
package com.apress.bgn.seven.switchst;
public class ExpessionSwitchDemo {
public static void main(String... args) {
int a = Integer.parseInt(args[0]);
String season = switch (a) {
case 1 -> "Winter";
case 2 -> "Winter";
case 3 -> "Spring";
case 4 -> "Spring";
case 5 -> "Spring";
case 6 -> "Summer";
case 7 -> "Summer";
case 8 -> "Summer";
case 9 -> "Autumn";
case 10 -> "Autumn";
case 11 -> "Autumn";
case 12 -> "winter";
default -> "Error";
};
System.out.println(season);
}
}
Listing 7-9switch Expression Example
switch表达式的引入是为了将 switch 语句视为一个表达式,对其求值并在语句中使用。switch表达式不需要break;语句来防止失败。当在与一个case值匹配后执行代码块时,使用 Java 13 中引入的yield语句返回该值。
清单 7-10 中的代码显示了之前switch表达式的不同版本,其中需要相同结果的case值被分组,并添加了额外的System.out.println(..)以显示yield的用法。返回值由System.out.println(..)括起开关表达式。
package com.apress.bgn.seven.switchst;
public class AnotherSwitchExpressionDemo {
public static void main(String... args) {
int a = Integer.parseInt(args[0]);
System.out.println( switch (a) {
case 1, 2, 12 -> {
System.out.println("One of 1,2,12 is tested.");
yield "Winter";
}
case 3,4,5 -> {
System.out.println("One of 3,4,5 is tested.");
yield "Spring";
}
case 6,7,8 -> {
System.out.println("One of 6,7,8 is tested.");
yield "Summer";
}
case 9,10,11 -> {
System.out.println("One of 9,10,11 is tested.");
yield "Autumn";
}
default ->
throw new IllegalStateException("Unexpected value");
});
}
}
Listing 7-10switch Expression Example Using yield Statements
在 Java 7 中,switch语句开始支持String值。支持String值的switch的主要问题是,总是有可能出现意外的行为,因为equals(..)方法用于查找匹配,显然,该方法是区分大小写的。前面的示例被修改为要求用户输入表示月份的文本。switch语句用于决定打印的季节,除非case选项中的文本与用户输入的文本完全匹配,否则打印的文本为错误。此外,由于提到了switch表达式,代码变为清单 7-11 中的代码。
package com.apress.bgn.seven.switchst;
public class StringSwitchSeasonDemo {
public static void main(String... args) {
//Read a
String a = args[0];
var season = "";
switch (a) {
case "january", "february", "december" -> season = "winter";
case "march", "april", "may" -> season = "Spring";
case "june", "july", "august" -> season = "Summer";
case "september", "october", "november" -> season = "Autumn";
default -> System.out.println("Error");
}
System.out.println(season);
}
}
Listing 7-11switch Statement Using String Values
如果我们用参数一月运行前面的程序,winter 将被打印在控制台中。如果我们用一月或null,错误将被打印在控制台上。
在支持String值之前,switch语句也支持枚举值。当值被分组到一个固定的集合中时,例如一年中月份的名称,这是很实用的。通过使用枚举,可以实现对String值的支持。用户以文本值的形式输入月份。该值被转换为大写,并用于提取相应的枚举值。这允许在switch语句中支持不区分大小写的String值。清单 7-12 中的代码展示了这样一个实现。
package com.apress.bgn.seven.switchst;
public class EnumSwitchDemo {
enum Month {
JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST,
SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER
}
public static void main(String... args) {
//Read a
String a = args[0];
try {
Month month = Month.valueOf(a.toUpperCase());
var season = "";
switch (month) {
case JANUARY:
case FEBRUARY:
case DECEMBER:
season = "Winter";
break;
case MARCH:
case APRIL:
case MAY:
season = "Spring";
break;
case JUNE:
case JULY:
case AUGUST:
season = "Summer";
break;
case SEPTEMBER:
case OCTOBER:
case NOVEMBER:
season = "Autumn";
break;
}
System.out.println(season);
} catch(IllegalArgumentException iae) {
System.out.println("Unrecognized enum value: " + a);
}
}
}
Listing 7-12switch Statement Using Enums Values
注意如何使用 enums,返回相同的季节为一月、一月、一月等等。此外,不需要default选项,因为如果找不到与用户提供的数据匹配的枚举值,就会抛出异常。
这就是关于 switch 语句的全部内容。在实践中,根据您试图开发的解决方案,您可能会决定结合使用if和switch语句。不幸的是,由于其特殊的逻辑和灵活的选项数量,很难为switch语句绘制流程图,但尽管如此,我还是尝试了,如图 7-6 所示。
图 7-6
switch语句流程图
循环语句
有时在编程中,我们需要涉及相同变量的重复步骤。为了完成工作而一遍又一遍地写同样的语句是荒谬的。让我们以对整数值数组进行排序为例。实现这一点的最著名的算法,也是编程课程中首先教授的算法,因为它很简单,被称为冒泡排序。该算法两个两个地比较数组的元素,如果它们的顺序不正确,它就交换它们。它会一次又一次地遍历数组,直到不再需要交换为止。该算法的效果如图 7-7 所示。
图 7-7
冒泡排序阶段和效果
该算法执行两种类型的循环:一种是使用索引迭代数组的每个元素。重复这种遍历,直到不需要交换为止。在 Java 中,这个算法可以用不同的循环语句以多种方式编写。但是我们会到达那里;让我们慢慢来。
Java 中有三种类型的循环语句:
-
for声明 -
while声明 -
do-while声明
for循环语句是最常用的,但是while和do-while也有它们的用途。
for声明
对于可计数的数组和集合等对象,建议使用 For 进行迭代。例如,遍历一个数组并打印它的每个值就像清单 7-13 中描述的那样简单。
package com.apress.bgn.seven.forloop;
public class ForLoopDemo {
public static void main(String... args) {
int arr[] = {5, 1, 4, 2, 3};
for (int i = 0; i < arr.length; ++i) {
System.out.println("arr[" + i + "] = " + arr[i]);
}
}
}
Listing 7-13Simple for Loop
基于前面的例子,可以画出for语句的流程图,如图 7-8 所示。
图 7-8
for语句流程图
清单 7-14 中的代码片段描述了for循环模板:
for ([int_expr]; [condition];[step]){
[code_block]
}
Listing 7-14The for Loop Template
方括号中的每个术语都有特定的用途,下面的列表对此进行了解释:
-
[init_expr]是初始化表达式,用于设置该循环使用的计数器的初始值。它以;结束,并且不是强制性的,因为声明初始化可以在语句之外完成,特别是如果我们想在代码的后面和语句之外使用 counter 变量。前面的代码可以写得很好,如清单 7-15 所示: -
[condition]是循环的终止条件;只要这个条件被评估为真,循环将继续执行。条件以;结束,有趣的是它也不是强制性的,因为终止条件可以放在循环重复执行的代码中。因此,前面的代码可以进一步修改,如清单 7-16 所示:
package com.apress.bgn.seven.forloop;
public class AnotherForLoopDemo {
public static void main(String... args) {
int arr[] = {5, 1, 4, 2, 3};
int i = 0;
for (; i < arr.length; ++i) {
System.out.println("arr[" + i + "] = " + arr[i]);
}
System.out.println("Loop exited with index: " + i);
}
}
Listing 7-15The for Loop with Termination Condition and Counter Modification Expression
[step]是步长表达式或增量,这是在循环的每一步增加计数器的表达式。作为最后一个学期,它没有结束;。正如您可能已经预料到的,它也不是强制性的,因为没有什么可以阻止开发人员操作代码块内部的计数器。所以前面的代码也可以写成清单 7-17 :
package com.apress.bgn.seven.forloop;
public class AndAnotherForLoopDemo {
public static void main(String... args) {
int arr[] = {5, 1, 4, 2, 3};
int i = 0;
for (; ; ++i) {
if (i >= arr.length) {
break;
}
System.out.println("arr[" + i + "] = " + arr[i]);
}
System.out.println("Loop exited with index: " + i);
}
}
Listing 7-16The for Loop with Only Counter Modification Statement
package com.apress.bgn.seven.forloop;
public class YeyAnotherForLoopDemo {
public static void main(String... args) {
int arr[] = {5, 1, 4, 2, 3};
int i = 0;
for (; ;) {
if (i >= arr.length) {
break;
}
System.out.println("arr[" + i + "] = " + arr[i]);
++i;
}
System.out.println("Loop exited with index: " + i);
}
}
Listing 7-17The for Loop with No Initialization, Condition, or Counter Modification Expression
计数器的修改甚至不必在步骤表达式内部完成;这可以在终止条件下完成。必须相应地修改初始化表达式和终止条件,以便仍然符合目的。清单 7-18 中描述的代码与之前的所有示例具有相同的效果。
package com.apress.bgn.seven.forloop;
public class LastForLoopDemo {
public static void main(String... args) {
int arr[] = {5, 1, 4, 2, 3};
int i;
for (i = -1; i++ < arr.length -1;) {
System.out.println("arr[" + i + "] = " + arr[i]);
}
System.out.println("Loop exited with index: " + i);
}
}
Listing 7-18The for Loop with Counter Modification in Termination Condition
你也应该知道步进表达式并不一定是递增的。它可以是修改计数器值的任何表达式。如果数组或集合从一个较大的索引开始向一个较低的索引遍历,可以使用i= i+1或i=i+3,甚至递减,而不是++i或i++。任何保持计数器在类型边界和集合边界内的数学运算都可以安全使用。
-
[code_block]是在循环的每一步重复执行的代码块。如果这段代码中没有退出条件,那么只要计数器通过终止条件,就会执行这段代码。当代码块包含一个语句时,花括号不是强制性的,但是大多数开发人员保留它们是为了代码清晰,并帮助 ide 正确缩进代码。
由于提到初始化表达式、终止条件和迭代表达式是可选的,这意味着下面是有效的
for语句:for ( ; ; ) {\\ statement(s) here}像这样使用 for 语句时要小心。代码块必须包含终止条件,以避免无限循环。
这是 for 循环语句的基本形式,但是在 Java 中还有其他方法来迭代一组值。比方说,我们必须遍历一个列表,而不是数组,如清单 7-19 所示。
package com.apress.bgn.seven.forloop;
import java.util.List;
public class ListLoopDemo {
public static void main(String... args) {
List<Integer> list = List.of(5, 1, 4, 2, 3);
for (int j = 0; j < list.size(); ++j) {
System.out.println("list[" + j + "] = " + list.get(j));
}
}
}
Listing 7-19The for Loop Over a List
代码看起来有些不切实际,这就是为什么可以用另一种类型的 for 语句遍历List<E>实例,这种语句在 Java 8 之前被称为forEach。你马上就会明白为什么,但首先让我们看看清单 7-20 中的forEach在起作用。
package com.apress.bgn.seven.forloop;
import java.util.List;
public class ForEachLoopDemo {
public static void main(String... args) {
List<Integer> list = List.of(5, 1, 4, 2, 3);
for (Integer item : list) {
System.out.println(item);
}
}
}
Listing 7-20The forEach Loop Over a List<E>
这种类型的for语句也被称为具有增强的语法,并为其表达式中使用的集合中的每个项目执行代码块。这意味着它可以在Collection<E>接口的任何实现上工作,也可以在数组上工作。因此,到目前为止作为示例给出的代码也可以如清单 7-21 所示编写。
package com.apress.bgn.seven.forloop;
import java.util.List;
public class ForLoopDemo {
public static void main(String... args) {
int arr[] = {5, 1, 4, 2, 3};
for (int item : arr) {
System.out.println(item);
}
}
}
Listing 7-21The forEach Loop
Over an Array
显然,这种情况下最好的部分是我们不再需要终止条件或计数器。从 Java 8 开始,名称forEach不能再用于具有增强语法的 for 语句,因为默认方法forEach被添加到所有的Collection<E>实现中。结合 lambda 表达式,打印列表元素的代码就变成了清单 7-22 中的代码。
package com.apress.bgn.seven.forloop;
import java.util.List;
public class ForLoopDemo {
public static void main(String... args) {
List<Integer> list = List.of(5, 1, 4, 2, 3);
list.forEach(item -> System.out.println(item));
//or
list.forEach(System.out::println);
}
}
Listing 7-22The forEach Method Used to Loop Over a List<E>
很漂亮,对吧?但是等等,还有更多:它也适用于数组,但是首先需要将它转换成合适的java.util.stream.BaseStream实现。这是由Arrays实用程序类提供的,它在 Java 8 中用支持 lambda 表达式的方法进行了丰富。所以是的,到目前为止编写的带有arr数组的代码可以从 Java 8 开始编写,如清单 7-23 所示。
package com.apress.bgn.seven.forloop;
import java.util.List;
public class ForLoopDemo {
public static void main(String... args) {
int arr[] = {5, 1, 4, 2, 3};
Arrays.stream(arr).forEach(System.out::println);
}
}
Listing 7-23The forEach Method Used to Loop Over an Array
在 Java 17 中,前面所有的例子都可以很好地编译和执行,所以在编写解决方案时,可以使用您最喜欢的语法。
while声明
while语句不同于for语句。不存在必须执行的固定数量的步骤,因此并不总是需要计数器。一个while语句执行的重复次数只取决于控制这个次数的延续条件被评估为真的次数。清单 7-24 中描述了该声明的通用模板。
while ([eval(condition)] == true) {
[code_block]
}
Listing 7-24The while Statement Template
一个while语句实际上也不需要初始化语句,但是如果需要的话,它可以在while代码块内部或者外部。while语句可以代替 for 语句,但是for语句的优点是它将初始化、终止条件和计数器的修改封装在一个块中,因此更加简洁。可以使用while语句重写数组遍历代码示例。代码如清单 7-25 所示:
package com.apress.bgn.seven.whileloop;
public class WhileLoopDemo {
public static void main(String... args) {
int arr[] = {5, 1, 4, 2, 3};
int i = 0;
while(i < arr.length) {
System.out.println("arr[" + i + "] = " + arr[i]);
++i;
}
}
}
Listing 7-25The while Statement Used to Loop Over an Array
如您所见,计数器变量int i = 0;的声明和初始化是在while代码块之外完成的。计数器的递增是在要重复的代码块内完成的。此时,如果我们为这个场景设计流程图,它将与图 7-8 中描述的for语句看起来一样。虽然听起来不可思议,但是[condition]也不是强制的,因为它可以直接用true替换,但是在这种情况下,您必须确保在一定会执行的代码块中有一个退出条件,否则执行很可能会以错误结束,因为 JVM 不允许无限循环。这个条件必须放在代码块的开头,以防止有用的逻辑在不应该执行的情况下执行。对于我们这个简单的例子,很明显我们不希望为一个索引在数组范围之外的元素调用System.out.println,如清单 7-26 所示。
package com.apress.bgn.seven.whileloop;
public class AnotherLoopDemo {
public static void main(String... args) {
int arr[] = {5, 1, 4, 2, 3};
int i=0;
while(true){
if (i >= arr.length) {
break;
}
System.out.println("arr[" + i + "] = " + arr[i]);
++i;
}
}
}
Listing 7-26The while Statement Used to Loop Over an Array, Without a Continuation Expression
当我们使用不总是在线的资源时,最好使用while语句。假设我们在一个不稳定的网络中为我们的应用使用一个远程数据库。第一次超时后,我们可以尝试直到成功,而不是放弃保存数据,对吗?这是通过使用一个while语句来完成的,该语句将不断尝试在其代码块中初始化一个连接对象。代码大致如清单 7-27 所示。
package com.apress.bgn.seven.whileloop;
import java.sql.*;
public class WhileConnectionTester {
public static void main(String... args) throws Exception {
Connection con = null;
while (con == null) {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
con = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mysql",
"root", "mypass");
} catch (Exception e) {
System.out.println("Connection refused. Retrying in 5 seconds ...");
Thread.sleep(5000);
}
}
// con != null, do something
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery("select * from user");
while (rs.next()) {
System.out.println(rs.getString(1) + " " + rs.getString(2));
}
con.close();
}
}
Listing 7-27The while Statement Used to Repeatedly Try to Obtain a Database Connection
这段代码的问题是它将永远运行。如果我们想在一段时间后放弃尝试,我们必须引入一个变量来计算尝试次数,并使用一个break;语句退出循环,如清单 7-28 所示。
package com.apress.bgn.seven.whileloop;
import java.sql.*;
public class AnotherWhileConnectionTester {
public static final int MAX_TRIES = 10;
public static void main(String... args) throws Exception {
int cntTries = 0;
Connection con = null;
while (con == null && cntTries < MAX_TRIES) {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
con = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mysql",
"root", "mypass");
} catch (Exception e) {
++cntTries;
System.out.println("Connection refused. Retrying in 5 seconds ...");
Thread.sleep(5000);
}
}
if (con != null) {
// con != null, do something
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery("select * from user");
while (rs.next()) {
System.out.println(rs.getString(1) + " " + rs.getString(2));
}
con.close();
} else {
System.out.println("Could not connect!");
}
}
}
Listing 7-28The while Statement Used to Repeatedly Try to Obtain a Database Connection Until the Number of Tries Expires
根据经验,在使用循环语句时,一定要确保存在退出条件。
既然我们现在已经涵盖了实现图 7-7 中描述的Bubble sort算法所需的所有语句,让我们看看代码是什么样子的。请注意,该算法可以用多种方式编写,但下面的代码最符合前面提供的解释。因此,当数组中的元素顺序不正确时,数组会被一次又一次地遍历,相邻的元素会被交换以符合所需的顺序,在本例中是升序。清单 7-29 中描述了最简单的Bubble sort算法。
package com.apress.bgn.seven;
import java.util.Arrays;
public class BubbleSortDemo {
public static final int arr[] = {5, 1, 4, 2, 3};
public static void main(String... args) {
boolean swapped = true;
while (swapped) {
swapped = false;
for (int i = 0; i < arr.length - 1; ++i) {
if (arr[i] > arr[i + 1]) {
int temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
swapped = true;
}
}
}
Arrays.stream(arr).forEach(System.out::println);
}
}
Listing 7-29The Simplest Version of the Bubble sort Algorithm
运行时,前面的代码交换了arr数组的元素,直到它们都按升序排列,因此前面代码的最后一行打印了修改后的arr:
1
2
3
4
5
do-while声明
do-while语句类似于while语句,有一点不同:在执行代码块后,继续条件被求值。这会导致代码块至少执行一次,这对于显示菜单很有用,例如,除非其中嵌入了阻止执行的条件。清单 7-30 中描述了该声明的通用模板。
do {
[code_block]
} while ([eval(condition)] == true)
Listing 7-30The do-while Statement Template
大多数情况下,语句while和do-while可以很容易地互换,并且代码块的逻辑变化很小或没有变化。例如,遍历一个数组并打印其元素的值也可以使用do-while来编写,根本不需要改变代码块。在图 7-9 中,你可以看到两个并行的实现,while 在左边,do-while 在右边。
图 7-9
用于打印数组元素的 while 和 do-while 实现
然而,这两个例子的流程图非常不同,并且揭示了两个语句的不同逻辑。您可以通过查看图 7-10 来比较它们。
图 7-10
while 和 do-while 语句流程图的比较
在图 7-9 的例子中,如果数组为空,do-while语句导致抛出ArrayIndexOutOfBoundsException异常,因为代码块的内容被执行,即使它们不应该被执行,因为索引值等于数组长度(零),但是没有索引等于 0 的元素,因为数组为空。但是,因为条件是在代码块之后计算的,所以没有办法知道。在图 7-11 中,你可以看到前面的代码样本被修改为用一个空数组运行,并且它们的输出是并排的。
图 7-11
用于打印空数组元素的 while 和 do-while 实现
为了使do-while实现具有与while实现相同的行为,代码块的执行必须受到至少有一个元素的数组的限制。清单 7-31 展示了一种方法。
package com.apress.bgn.seven.whileloop;
public class DoWhileLoopDemo {
public static void main(String... args) {
int arr[] = new int[0];
int i = 0;
do {
if(arr.length >=1) {
System.out.println("arr[" + i + "] = " + arr[i]);
++i;
}
} while (i < arr.length);
}
}
Listing 7-31do-while Statement Implementation That Works Correctly for an Empty Array Too
当代码块必须至少执行一次时,
do-while语句工作得最好,否则我们不必要地评估一次条件。
前面介绍的冒泡排序算法就是一个很好的例子,其中while和do-while语句可以互换使用,不需要额外的代码修改。
既然已经提到有不止一种方法来编写这个算法,清单 7-32 显示了一个改进的版本,它不仅使用了do-while,而且减少了每次遍历的数组的大小。这是可能的,因为根据图 7-7 ,在每次遍历之后,数组的最后一个索引保存了被遍历子集的最大数量。
package com.apress.bgn.seven;
import java.util.Arrays;
public class BubbleSortDemo {
public static final int arr[] = {5, 1, 4, 2, 3};
public static void main(String... args) {
boolean swapped = true;
do {
swapped = false;
for (int i = 0, n = arr.length -1; i < n - 1; ++i, --n) {
if (arr[i] > arr[i + 1]) {
int temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
swapped = true;
}
}
} while (swapped);
Arrays.stream(arr).forEach(System.out::println);
}
}
Listing 7-32Optimized Version of the Bubble Sort Algorithm Using do-while Statement
for语句中的初始化和步骤表达式允许用“,”分隔多个术语。所以下面的代码是有效的,并且运行良好。
for (int j = 0, k =2; j < 10; ++j, ++k) {
System.out.println("composed indexes: [" + j + ", " + k + "]");
}
还记得试图连接到不稳定网络中的数据库的代码示例吗(清单 7-27 )?当使用while时,执行开始于测试连接是否不为空,但是连接甚至还没有用有效值初始化。进行那个测试是不合逻辑的,对吧?参见清单 7-33 中所示的片段。
Connection con = null;
while (con == null) {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
con = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mysql", "root", "mypass");
// some code omitted
Listing 7-33while Implementation to Check Connection to a Database
这种实现虽然很实用,但有点多余,而且逻辑并没有真正遵循最佳编程实践。一个do-while实现是最合适的,因为它避免了初始测试,如果con实例是null,当没有其他方法时。清单 7-34 中描述了编写代码的一种变体。
package com.apress.bgn.seven.whileloop;
import java.sql.*;
public class DoWhileConnectionTester {
public static final int MAX_TRIES = 10;
public static void main(String... args) throws Exception {
int cntTries = 0;
Connection con = null;
do {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
con = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mysql",
"root", "mypass");
} catch (Exception e) {
++cntTries;
System.out.println("Connection refused. Retrying in 5 seconds ...");
Thread.sleep(5000);
}
} while (con == null && cntTries < MAX_TRIES);
if (con != null) {
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery("select * from user");
while (rs.next()) {
System.out.println(rs.getString(1) + " " + rs.getString(2));
}
con.close();
} else {
System.out.println("Could not connect!");
}
}
}
Listing 7-34do-while Implementation to Check Connection to a Database
当然,跳过一次条件的评估并不是一个大的优化,但是在一个大的应用中,每一个小的优化都很重要。
打破循环和跳过步骤
在前面的例子中,我们提到了使用break;语句退出循环,并承诺返回并添加更多细节。有三种方法可以操纵循环的行为:
break语句退出循环,如果带有标签,将会中断带有标签的循环;当我们有更多的嵌套循环时,这很有用,因为我们可以从任何嵌套循环中断开,而不仅仅是包含语句的循环。
*** continue语句跳过其后任何代码的执行,继续下一步。
*** **`return`**语句**用于退出一个方法,所以如果循环或者 if 或者`switch`语句在一个方法的主体内,它也可以用于退出循环。**
**至于最佳实践,不应该滥用`return`语句来退出方法,因为它们可能会使执行流程难以遵循。******
****### break声明
break语句只能在switch、for、while和do-while语句中使用。您已经看到了如何在switch语句中使用它,所以让我们向您展示如何在所有其他语句中使用它。使用break语句可以中断for、while或do-while循环,但必须由退出条件控制,否则不会执行任何步骤。在清单 7-35 中,我们只打印一个数组中的前三个元素,即使for循环被设计为遍历所有元素。如果我们得到的指数等于 3,我们退出循环。
package com.apress.bgn.seven.forloop;
public class BreakingForDemo {
public static final int arr[] = {5, 1, 4, 2, 3};
public static void main(String... args) {
for (int i = 0; i < arr.length ; ++i) {
if (i == 3) {
System.out.println("Bye bye!");
break;
}
System.out.println("arr[" + i + "] = " + arr[i]);
}
}
}
Listing 7-35Breaking Out of a for Loop
如果我们有一个嵌套循环,标签可以用来决定循环语句的中断。例如,在清单 7-36 中,我们有三个嵌套的 for 循环,当所有索引都相等时,我们退出中间的循环。
package com.apress.bgn.seven.forloop;
public class BreakingNestedForLoopDemo {
public static final int arr[] = {5, 1, 4, 2, 3};
public static void main(String... args) {
for (int i = 0; i < 2; ++i) {
HERE: for (int j = 0; j < 2; ++j) {
for (int k = 0; k < 2; ++k) {
if (i == j && j == k) {
break HERE;
}
System.out.println("(i, j, k) = (" + i + "," + j + "," + k + ")");
}
}
}
}
}
Listing 7-36Breaking Out of a Nested for Loop
前一个代码示例中使用的标签名为HERE,它在满足条件时退出的for语句之前声明。break 语句后面是相同的标签。在开发中,使用全大写字母编写标签名称被认为是一种最佳实践,因为这样可以避免在阅读代码时将标签与变量或类名混淆。
用标签来打破循环实际上是很不可取的,因为它会导致代码跳转,并使执行流程更难跟踪。因此,如果你必须这样做,确保你的标签是可见的。
为了确保这一点,你可以看看控制台。您应该会看到(I,j,k)的一些组合(包括带有i = j = k的组合)丢失了。这里列出了输出。
(i, j, k) = (1,0,0)
(i, j, k) = (1,0,1)
(i, j, k) = (1,1,0)
continue声明
continue语句不会中断循环,但可用于根据条件跳过某些步骤。实际上,continue语句停止了循环当前步骤的执行,并移动到下一步,所以你可以说这个语句继续了循环。让我们继续试验数组遍历的例子,这一次,让我们跳过使用continue语句打印奇数索引的元素。代码如清单 7-37 所示。
package com.apress.bgn.seven.forloop;
public class ContinueForDemo {
public static final int arr[] = {5, 1, 4, 2, 3};
public static void main(String... args) {
for (int i = 0; i < arr.length; ++i) {
if (i % 2 != 0) {
continue;
}
System.out.println("arr[" + i + "] = " + arr[i]);
}
}
}
Listing 7-37Skipping Printing Elements with Odd Indexes Using a for Loop and continue Statement
显然,这个语句必须是有条件的,否则,循环将只是无用地迭代。
continue语句也可以和标签一起使用。让我们举一个与前面使用的三个for嵌套循环类似的例子,但是这一次,当k索引等于 1 时,什么都不打印,我们跳到包含k循环的循环的下一步。代码如清单 7-38 所示。
package com.apress.bgn.seven.forloop;
public class ContinueNestedForLoopDemo {
public static final int arr[] = {5, 1, 4, 2, 3};
public static void main(String... args) {
for (int i = 0; i < 3; ++i) {
HERE: for (int j = 0; j < 3; ++j) {
for (int k = 0; k < 3; ++k) {
if (k == 1) {
continue HERE;
}
System.out.println("(i, j, k) = (" + i + "," + j + "," + k + ")");
}
}
}
}
}
Listing 7-38Continue a Nested for Loop
为了确保这一点,您可以在控制台中查看打印了哪些组合,我们清楚地注意到没有打印带有k=1或k=2的组合。这里列出了输出。
(i, j, k) = (0,0,0)
(i, j, k) = (0,1,0)
(i, j, k) = (0,2,0)
(i, j, k) = (1,0,0)
(i, j, k) = (1,1,0)
(i, j, k) = (1,2,0)
(i, j, k) = (2,0,0)
(i, j, k) = (2,1,0)
(i, j, k) = (2,2,0)
在 Java 社区中,使用标签来打破循环是不被允许的,因为跳转到标签类似于在某些老式编程语言中可以找到的goto语句。goto是 Java 保留的关键字,因为这个语句曾经存在于 JVM 的第一个版本中,但后来被删除了。使用跳转会降低代码的可读性和可测试性,并导致糟糕的设计。这就是为什么goto在以后的版本中被删除了,但是任何需要这种操作的都可以通过break和continue语句实现。
return声明
return语句很简单:如前所述,它可以用于退出方法体的执行。如果方法返回一个值,那么return语句会伴随着返回的值。return 语句可用于退出本节提到的任何语句。它可以代表一种快捷执行方法的非常聪明的方式,因为当前方法的执行停止,处理从调用该方法的代码点继续。
我们来看几个例子。清单 7-39 中的代码展示了一个寻找数组中第一个偶数元素的方法。如果找到,该方法返回其索引;否则,它返回-1。
package com.apress.bgn.seven;
public class ReturnDemo {
public static final int arr[] = {5, 1, 4, 2, 3};
public static void main(String... args) {
int foundIdx = findEvenUsingFor(arr);
if (foundIdx != -1) {
System.out.println("First even is at: " + foundIdx);
}
}
public static int findEvenUsingFor(int ... arr) {
for (int i = 0; i < arr.length; ++i) {
if (arr[i] %2 == 0) {
return i;
}
}
return -1;
}
}
Listing 7-39Finding an Even Number Using the do-while Statement
同样的方法可以用一个while语句编写,但是return语句的目的是一样的。代码如清单 7-40 所示。
// enclosing class omitted
public static int findEvenUsingWhile(int ... arr) {
int i = 0;
while (i < arr.length) {
if (arr[i] % 2 == 0) {
return i;
}
++i;
}
return -1;
}
Listing 7-40Finding an Even Number Using the while Statement
如你所见,return语句可以用在任何情况下,当我们想要在一个条件满足时终止一个方法的执行。
使用try-catch结构控制流程
本书之前提到过异常和try-catch语句,但不是作为控制流程执行的工具。在我们跳到解释和例子之前,让我们先讨论一下try-catch-finally语句的通用模板。该模板如清单 7-41 所示。
try {
[code_block]
} catch ([exception_block]} {
[handling_code_block]
} finally {
[cleanup_code_block]
}
Listing 7-41try-catch-finally Statement Template
下面的列表解释了该模板的组件:
-
[code_block]是要执行的代码块。 -
[exception_block]是一个或多个异常类型的声明,可以由[code_block]抛出。 -
被抛出的异常标志着必须处理的意外情况;一旦捕获到异常,就执行这段代码来处理它,要么尝试将系统恢复到正常状态,要么记录关于异常原因的详细信息。
-
[clean_up_code]用于释放资源或将对象设置为空,使其有资格被收集。如果存在,无论是否抛出异常,都会执行该代码块。
现在您已经知道了一个try-catch-finally是如何工作的,您大概可以想象如何使用它来控制执行流。在[code_block]中,您可以显式抛出异常,并决定如何处理它们。
考虑到我们一直使用的数组,我们将再次基于它来设计我们的代码。清单 7-42 显示了一段代码,当发现一个偶数值时,它抛出一个异常。
package com.apress.bgn.seven.ex;
public class ExceptionFlowDemo {
public static final int arr[] = {5, 1, 4, 2, 3};
public static void main(String... args) {
try {
checkNotEven(arr);
System.out.println("Not found, all good!");
} catch (EvenException e) {
System.out.println(e.getMessage());
} finally {
System.out.println("Cleaning up arr");
for (int i = 0; i < arr.length; ++i) {
arr[i] = 0;
}
}
}
public static int checkNotEven(int... arr) throws EvenException {
for (int i = 0; i < arr.length; ++i) {
if (arr[i] % 2 == 0) {
throw new EvenException("Did not expect an even number at " + i);
}
}
return -1;
}
}
Listing 7-42Controlling Flow Using Exceptions
EvenException类型是为这个特定示例编写的定制异常类型,它的实现在这里不相关。如果我们执行这段代码,将会打印以下内容:
Did not expect an even number at 2
Cleaning up arr
如你所见,通过抛出一个异常,我们将执行指向了处理代码,所以“没有找到,一切正常!”不打印,因为有一个finally块,它也被执行。是的,您可以混合使用:使用不同类型的异常,并且您可以拥有多个 catch 块来解决您的问题。在我之前工作的一家公司,我们有一段代码验证一个文档,并根据没有通过的验证检查抛出不同类型的异常,在finally块中,我们有一段代码将错误对象转换为 PDF。代码看起来类似于清单 7-43 中的代码。
ErrorContainter errorContainer = new ErrorContainter();
try {
validate(report);
} catch (FileNotFoundException | NotParsable e) {
errorContainer.addBadFileError(e);
} catch (InvestmentMaxException e) {
errorContainer.addInvestmentError(e);
} catch (CreditIncompatibilityException e) {
errorContainer.addIncompatibilityError(e);
} finally {
if (errorContainer.isEmpty()) {
printValidationPassedDocument();
} else {
printValidationFailedDocument(errorContainer);
}
}
Listing 7-43Code Sample Showing a try-multi-catch Statement
finally代码块中的代码很复杂,完全不建议放在那里。然而,有时在现实世界中,解决方案并不总是尊重最佳实践,甚至是常识性的实践。当处理遗留代码时,你可能会发现自己在编写蹩脚但功能强大的代码来解决客户的问题——因为当然,编程是令人敬畏的,但在一些经理眼中,结果更重要。如果你足够幸运地在一家公司找到一份工作,这家公司希望在未来构建代码或者将它交给其他团队成员,你可能最终会遇到一个喜欢最佳实践的经理。只要记得尽力而为,把一切都记录妥当,就没问题了。
try-catch-finally格挡相当厉害。它们是一种有用的结构,用于指导执行流和打印关于应用整体状态和最终问题来源的有用信息。如果设计得当,异常处理可以提高代码的质量和可读性。在设计它们时,有一些规则要遵循:
图 7-12
IntelliJ IDEA 编译错误和显示 try-catch 块中异常类型顺序错误的消息
-
尽量避免使用多个 catch 块,除非使用它们来区别对待不同类型的异常。
-
使用
|(pipe)符号将处理方式相同的相似类型的异常分组在一起。Java 7 中增加了对此的支持。 -
捕捉相关类型的异常时要小心。第一个匹配异常类型的 catch 处理异常,因此超类应该在 catch 列表中处于较低的位置。如果顺序不正确,编译器甚至会很不高兴,如图 7-12 所示。
当然,您还应该遵守本书前面提到的避免异常吞咽和捕获Throwable的基本规则。
摘要
这一章涵盖了开发中最重要的事情之一:如何设计你的解决方案,以及它的逻辑。还向您介绍了什么是流程图及其组件,它们是决定如何编写代码以及如何控制执行路径的工具。最后,您已经学习了在什么时候使用哪些语句,并且提到了一些 Java 最佳实践,这样您将能够设计出最适合您的问题的解决方案。Java 提供了以下功能:
-
编写
if语句的简单和更复杂的方法。 -
一个
switch语句,适用于任何原始类型、枚举,从 Java 7、String实例开始。 -
一个返回一个值的
switch表达式,可用于编写更复杂的语句。 -
写
for语句的几种方法。 -
如何使用
forEach方法和流来遍历一组值。 -
while语句,当必须重复一个步骤直到满足一个条件时使用。 -
do-while语句,当一个步骤必须重复直到满足一个条件,并且该步骤至少重复一次时使用,因为继续条件在它之后计算。 -
如何使用
break、continue、return等语句操纵循环行为。 -
如何使用
try-catch-finally结构控制执行流程?****