Appearance
Java 基础:基本程序结构
1. 基本类型
| 序号 | 基本类型 | 大小 | 最小值 | 最大值 | 包装类型 |
|---|---|---|---|---|---|
| 1 | boolean | — | — | — | Boolean |
| 2 | byte | 8 bits | -128 | +127 | Byte |
| 3 | char | 16 bits | \u0000 | \uFFFF | Character |
| 4 | short | 16 bits | -2^15 | +2^15 - 1 | Short |
| 5 | int | 32 bits | -2^31 | +2^31 - 1 | Integer |
| 6 | long | 64 bits | -2^63 | +2^63 - 1 | Long |
| 7 | float | 32 bits | IEEE754 | IEEE754 | Float |
| 8 | double | 64 bits | IEEE754 | IEEE754 | Double |
| 9 | void | — | — | — | Void |
Warning:Java 没有任何无符号(unsigned)形式的
int、long、short或byte类型。
1.1. 整型
Java 中,整型的数值范围与运行 Java 代码的机器无关。
- 长整型:数值后有一个
l或L的后缀,例如:4000000000L; - 十六进制:数值前有一个
0x或0x的前缀,例如:0xCAFE; - 八进制:数值前有一个
0的前缀,例如:010对应十进制中的8; - 二进制(在 Java 7 及之后开始支持):数值前有一个
0b或0B的前缀,例如:0b1001;
从 Java 7 及之后可以在数字字面量当中添加下划线,以提供更好的可读性,例如:1_000_000、0b1111_0100_0010_0100_0000。
1.2. 浮点类型
float 类型的数值有一个后缀 F 或 f(例如,3.14F)。没有后缀 F 的浮点数值(如 3.14)总是默认为 double 类型。当然,也可以在浮点数值后面添加后缀 D 或 d(例如,3.14D)。
表示溢出和出错情况的三个特殊的浮点数值:
- 正无穷大:
Double.POSITIVE_INFINITY; - 负无穷大:
Double.NEGATIVE_INFINITY; - NaN(不是一个数字):
Double.NaN;
Warning:需要注意的是,不能使用
if (x == Double.NaN)来检测一个特定的值是否等于Double.NaN(该判断永远为false),正确的做法是:if (Double.isNaN(x))。
1.3. char 类型
有些 Unicode 字符可以用一个 char 值描述,另外一些 Unicode 字符则需要两个 char 值。
比较特别的是,\u 除了可以出现在加单/双引号的字符/字符串中,还可以出现在代码中的其它地方,例如:
Java
public static void main(String\u005b\u005d args)Note
再比如
"\u0022+\u0022"并不是一个由引号(U+0022)包围加号构成的字符串。实际上,\u0022会在解析之前转换为",也就是变成""+"",结果是一个空字符串。更隐秘地,一定要当心注释中的
\u:Java// look inside c:\users也会产生一个语法错误,因为
\u后面并没有跟着 4 个十六进制数。
1.4. boolean 类型
Java 中整型和布尔值之间不能进行相互转换。
Tip
在 Java 虚拟机规范中,JVM 没有提供
boolean类型专用的字节码指令,在通常情况下使用int相关指令来代替,但是对boolean数组的访问与修改,会共用byte数组的baload和bastore指令。也就是换句话说,在符合 JVM 规范的虚拟机中:
- 如果
boolean是单独使用的,boolean占 4 个字节;- 如果
boolean是以 “boolean数组” 形式使用的,boolean占 1 个字节;具体
boolean占几个字节取决于 JVM 的实现。可以参考知乎上的这篇文章《boolean 类型占几个字节?》
1.5. 高精度数值
在 Java 中有两种类型的数据可用于高精度的计算。它们是 BigInteger 和 BigDecimal。尽管它们大致可以划归为 “包装类型”,但是它们并没有对应的基本类型。
这两个类包含的方法提供的操作,与对基本类型执行的操作相似。也就是说,能对 int 或 float 做的运算,在 BigInteger 和 BigDecimal 这里也同样可以,只不过必须要通过调用它们的方法来实现而非运算符。此外,由于涉及到的计算量更多,所以运算速度会慢一些。诚然,我们牺牲了速度,但换来了精度。
BigInteger 支持任意精度的整数。可用于精确表示任意大小的整数值,同时在运算过程中不会丢失精度。BigDecimal 支持任意精度的定点数字。例如,可用它进行精确的货币计算。
2. 变量与常量
Note
如果想要知道哪些 Unicode 字符属于 Java 中合法的标识符字符,可以使用
Character.isJavaIdentifierStart和Character.isJavaIdentifierPart来进行确认。尽管
$是一个合法的 Java 字符,但不要在你自己的代码中使用这个字符。它只用在 Java 编译器或其他工具生成的名字中。
从 Java 10 开始,对于局部变量,如果可以从变量的初始值推断出它的类型,就不再需要声明类型,只需要使用关键字 var 而无须指定类型:
Java
var vacationDays = 12;
var greeting = "Hello";1
2
2
Java 当中使用 final 关键字来表示常量,const 是 Java 保留的关键字。
对于基本类型,final 使数值恒定不变,而对于对象引用,final 使引用恒定不变。一旦引用被初始化指向了某个对象,它就不能改为指向其它对象。
你必须在定义时或在每个构造器中执行 final 变量的赋值操作。这保证了 final 属性在使用前已经被初始化过。
final 还可以用于参数、方法、类。
final参数:这个特性主要用于传递数据给匿名内部类;final方法:防止子类通过覆写改变方法的行为;final类:意味着它不能被继承,final类中的所有方法自动成为final方法;
Note 1:类中所有的
private方法都隐式地指定为final。因为不能访问private方法,所以不能覆写它。可以给private方法添加final修饰,但是并不能给方法带来额外的含义。
Note 2
众所周知,被声明为
final的字段不允许再将其重新赋值为另一个值,例如:Javapublic class System { ... public static final PrintStream out = ...; ... }1
2
3
4
5JavaSystem.out = new PrintStream(...); // ERROR -- out is final但是如果查看
System类,就会发现有一个setOut方法可以将System.out设置为不同的流。你可能会感到奇怪,为什么这个方法可以修改final变量的值。原因在于,setOut方法是一个原生方法,而不是在 Java 语言中实现的。原生方法可以绕过 Java 语言的访问控制机制。这是一种特殊的解决方法,你自己编写程序时不要模仿这种做法。
3. 运算符
3.1. 算数运算符
需要注意,整数被 0 除将会产生一个异常,而浮点数被 0 除将会得到无穷大或 NaN 结果。
Note
可移植性是 Java 语言的设计目标之一。无论在哪个虚拟机上运行,同一运算应该得到同样的结果。对于浮点数的算术运算,实现这样的可移植性是相当困难的。
double类型使用 64 位存储一个数值,而有些处理器则使用 80 位浮点寄存器。这些寄存器增加了中间过程的计算精度。例如,以下运算:Javadouble w = x * y / z;很多 Intel 处理器计算
x * y,并且将结果存储在 80 位的寄存器中,再除以z并将结果截断为 64 位。这样可以得到一个更加精确的计算结果,并且还能够避免产生指数溢出。但是,这个结果可能与始终使用 64 位计算的结果不一样。因此,Java 虚拟机的最初规范规定所有的中间计算都必须进行截断。这种做法遭到了数字社区的反对。截断计算不仅可能导致溢出,而且由于截断操作需要消耗时间,所以在计算速度上实际上比精确计算慢。为此,Java 程序设计语言承认了最优性能与理想的可再生性之间存在的冲突,并给予了改进。在默认情况下,现在虚拟机设计者允许对中间计算结果采用扩展的精度。但是,对于使用strictfp关键字标记的地方必须使用严格的浮点计算来生成可再生的结果。例如,可以把
main方法标记为strictfp:Javapublic static strictfp void main(String[] args)那么,
main方法中的所有指令都将使用严格的浮点计算。如果将一个类标记为strictfp,这个类中的所有方法都要使用严格的浮点计算。
Tip:Java 不支持运算符重载,虽然字符串重载了
+运算符,但是没有重载其它的运算符,也不支持在其它类中重载运算符。
3.2. 数学函数与常量
在 Math 类中,所有的方法都使用计算机浮点单元中的例程。如果想无论在哪个虚拟机上运行,同一运算都得到同样的结果,则应该使用 StrictMath 类。
在使用普通数学运算符的式子中,如果产生了计算溢出,则它也只是悄悄地返回错误的结果而不做任何提醒。例如,10 亿乘以 3 的计算结果是 -1294967296,因为最大的 int 值也只是刚刚超过 20 亿。不过,如果调用 Math.multiplyExact(100000000, 3) 就会生成一个异常。
Note
下面考虑这样一个问题:计算一个时钟时针的位置。这里要做一个时间调整,而且要归一化为一个 0 ~ 11 之间的数。这很简单:
(position + adjustment) % 12。不过,如果这个调整为负会怎么样呢?你可能会得到一个负数。这是因为最早制定规则的人并没有翻开数学书好好研究(余数总是要 ≥ 0),而是提出了一些看似合理但实际上很不方便的规则。所以要引入一个分支,或者使用((position + adjustment) % 12 + 12) % 12,不管怎样都很麻烦。
Math.floorMod方法就让这个问题变得容易了:floorMod(position + adjustment, 12)总会得到一个 0 ~ 11 之间的数。(遗撼的是,对于负除数,floorMod会得到负数结果,不过这种情况在实际中很少出现。)
3.3. == 和 equals
==、!=:比较的是对象的引用。equals:默认比较的也是对象的引用,但是大多数 Java 库类通过覆写 equals() 方法比较对象的内容而不是其引用:
例 1:默认比较的是对象的引用:
Javapublic class Equivalence { public static void main(String[] args) { Integer n1 = 129; Integer n2 = 129; System.out.println(n1 == n2); System.out.println(n1 != n2); } }1
2
3
4
5
6
7
8所以输出结果是:
Textfalse true1
2例 2:但是也有特殊的,比如
Integer内部维护着一个IntegerCache的缓存,默认缓存范围是[-128, 127]。所以上面的例子中,如果把129改成[-128, 127]之间的值,如47,那么情况将会是:Javapublic class Equivalence { public static void main(String[] args) { Integer n1 = 47; Integer n2 = 47; System.out.println(n1 == n2); System.out.println(n1 != n2); } }1
2
3
4
5
6
7
8输出:
Texttrue false1
2例 3:如果是我们自己创建的类:
Java// 默认的 equals() 方法没有比较内容 class Value { int i; } public class EqualsMethod2 { public static void main(String[] args) { Value v1 = new Value(); Value v2 = new Value(); v1.i = v2.i = 100; System.out.println(v1.equals(v2)); } }1
2
3
4
5
6
7
8
9
10
11
12
13输出:
Textfalse
因为 equals() 的默认行为是比较对象的引用而非具体内容。因此,除非你在新类中覆写 equals() 方法,否则比较的仍将是对象的引用。
另外还需要注意的是字符串比较。
3.4. 位运算
位运算符包括:
&:与;|:或;^:异或;~:非;
位移预算符包括:
<<: 能将其左边的运算对象向左移动右侧指定的位数,在低位补 0;>>: 能将其左边的运算对象向右移动右侧指定的位数,若位移前最高位为 1 则在高位补 1,若位移前最高位为 0 则在高位补 0;>>>: 能将其左边的运算对象向右移动右侧指定的位数,在高位补 0;
Warning
在移动
char、byte或short时会在移动发生之前将其提升为int,位移后的结果为int。如果将位移后的值重新赋值回原类型,则会发生截断。所以当
>>>=与byte、short类型一起使用的话,很可能会产生错误的结果:Javapublic class main { public static void main(String[] args) { short ch = (short) 0b11011011_11111011; System.out.println(Integer.toBinaryString(ch)); ch >>>= 10; System.out.println(Integer.toBinaryString(ch)); } }1
2
3
4
5
6
7
8期望输出的结果:
Text11111111111111111101101111111011 1101101
2实际输出的结果:
Text11111111111111111101101111111011 111111111111111111111111111101101
2
Tip:移位运算符的右操作数要完成模 32 的运算(若非左操作数是
long类型则模 64)。例如,1 << 35的值等同于1 << 3。
3.5. 优先级
各运算符优先级从高到低展示如下:
| 运算符 | 结合性 |
|---|---|
[] . ()(方法调用) | 从左向右 |
! ~ ++ -- +(一元运算)-(一元运算)()(强制类型转换)new | 从右向左 |
* / % | 从左向右 |
+ - | 从左向右 |
<< >> >>> | 从左向右 |
< <= > >= instanceof | 从左向右 |
== != | 从左向右 |
& | 从左向右 |
^ | 从左向右 |
| | 从左向右 |
&& | 从左向右 |
|| | 从左向右 |
?: | 从右向左 |
= += -= *= /= %= &= |= ^= <<= >>= >>>= | 从右向左 |
4. 控制流程
4.1. 块作用域
不能在嵌套的两个块中声明同名的变量:
Java
public static void main(String[] args) {
int n;
...
{
int k;
int n; // ERROR--can't redefine n in inner block
...
}
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
4.2. 条件语句
条件语句的形式为:
ABNF
"if" "(" condition ")" statement ["else" statement] ; The statement could also be an if statement.例如:
Java
if (yourSales >= 2 * target) {
performance = "Excellent";
bonus = 1000;
} else if (yourSales >= 1.5 * target) {
performance = "Fine";
bonus = 500;
} else if (yourSales >= target) {
performance = "Satisfactory";
bonus = 100;
} else {
System.out.println("You're fired");
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
4.3. 循环
while 循环语句的形式为:
ABNF
"while" "(" condition ")" statementdo/while 循环语句的形式为:
ABNF
"do" statement "while" "(" condition ")"for 循环语句的形式为:
ABNF
"for" "(" [initializer] ";" [condition] ";" [update] ")" statementfor each 循环语句的形式为:
ABNF
"for" "(" variable ":" collection ")" statementcollection 这一集合表达式必须是一个数组或者是一个实现了 Iterable 接口的类对象。
4.4. switch 语句
switch 语句的形式为:
ABNF
switch-statement = "switch" "(" expression ")" "{" switch-cases "}"
switch-cases = *switch-case [default-case] *switch-case
switch-case = "case" constant-expression ":" [statements] ["break;"]
default-case = "default:" [statements] ["break;"]1
2
3
4
5
6
7
2
3
4
5
6
7
case 标签(constant-expression)可以是:
- 类型为
char、byte、short或int的常量表达式; - 枚举常量;
- 从 Java 7 开始,
case标签还可以是字符串字面量;
当在 switch 语句中使用枚举常量时,不必在每个标签中指明枚举名,可以由 switch 的表达式值推导得出。例如:
Java
Size sz = ...;
switch (sz) {
case SMALL: // no need to use Size.SMALL
...
break;
...
}1
2
3
4
5
6
7
2
3
4
5
6
7
Tip
如果在
case分支语句的末尾没有break语句,那么就会接着执行下一个case分支语句。这种情况相当危险,常常会引发错误。编译代码时可以考虑加上-Xlint:fallthrough选项:Bash$ javac -Xlint:fallthrough Test.java这样一来,如果某个分支最后缺少一个
break语句,编译器就会给出一个警告消息。如果你确实正是想使用这种 “直通式”(fallthrough)行为,可以为其外围方法加一个注解
@SuppressWarnings("fallthrough")。这样就不会对这个方法生成警告了。
4.5. 中断控制流程的语句
尽管 Java 的设计者将 goto 作为保留字,但实际上并没有打算在语言中使用它。Java 语言中增加了一条新的语句:带标签的 break, 以此来支持这种程序设计风格。请注意,标签必须放在希望跳出的最外层循环之前,并且必须紧跟一个冒号。
Java
Scanner in = new Scanner(System.in);
int n;
read_data:
while (...) { // this loop statement is tagged with the label
...
for (...) { // this inner loop is not labeled
System.out.print("Enter a number >= 0: ");
n = in.nextInt();
if (n < 0) // should never happen—can't go on
break read_data; // break out of read_data loop
...
}
}
// this statement is executed immediately after the labeled break
if (n < 0) { // check for bad situation
// deal with bad situation
} else {
// carry out normal processing
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Note
实际上可以将标签应用到任何语句,甚至可以将其应用到
if语句或者块语句,如下所示:Javalabel: { ... if (condition) break label; // exits block ... } // jumps here when the break statement executes1
2
3
4
5另外需要注意,只能跳出语句块,而不能跳入语句块。
最后,还有一个 continue 语句。与 break 语句一样,它将中断正常的控制流程。continue 语句将控制转移到最内层循环的首部。例如:
Java
Scanner in = new Scanner(System.in);
while (sum < goal) {
System.out.print("Enter a number: ");
n = in.nextInt();
if (n < 0) continue;
sum += n; // not executed if n < 0
}1
2
3
4
5
6
7
2
3
4
5
6
7
如果将 continue 语句用于 for 循环中,就可以跳到 for 循环的 “更新” 部分。例如:
Java
for (count = 1; count <= 100; count++) {
System.out.print("Enter a number, -1 to quit: ");
n = in.nextInt();
if (n < 0) continue;
sum += n; // not executed if n < 0
}1
2
3
4
5
6
2
3
4
5
6
还有一种带标签的 continue 语句,将跳到与标签匹配的循环的首部。
5. 数组
可以使用下面两种形式定义一个数组变量:
Java
int[] a;或:
Java
int a[];推荐使用第一种风格。
Java 还提供了一种创建数组对象并同时提供初始值的简写形式:
Java
int[] smallPrimes = { 2, 3, 5, 7, 11, 13 };Tip:这种简写形式还允许最后一个值后面有逗号,以便于后续不断向其添加新的元素。
6. 字符串
6.1. 字符串比较
不要使用 == 运算符检测两个字符串是否相等,因为虚拟机不会始终确保将相同的字符串共享。实际上只有字符串字面量是共享的,而 + 或 substring 之类的操作得到的字符串并不共享。
6.2. 码点与代码单元
char 数据类型是一个采用 UTF-16 编码表示 Unicode 码点的代码单元。最常用的 Unicode 字符使用一个代码单元就可以表示,而辅助字符需要一对代码单元表示。
length 方法将返回采用 UTF-16 编码表示给定字符串所需要的代码单元数量。例如:
Java
String greeting = "Hello";
int n = greeting.length(); // is 51
2
2
要想得到实际的长度,即码点数量,可以调用:
Java
int cpCount = greeting.codePointCount(0, greeting.length());s.charAt(n) 将返回位置 n 的代码单元,n 介于 0 ~ s.length() - 1 之间。例如:
Java
char first = greeting.charAt(0); // first is 'H'
char last = greeting.charAt(4); // last is 'o'1
2
2
要想得到第 i 个码点,应该使用下列语句:
Java
int index = greeting.offsetByCodePoints(0, i);
int cp = greeting.codePointAt(index);1
2
2
如果想要遍历一个字符串,并且依次查看每一个码点,可以使用下列语句:
Java
int cp = sentence.codePointAt(i);
if (Character.isSupplementaryCodePoint(cp)) i += 2;
else i++;1
2
3
2
3
或者更简单的方法是使用 codePoints 方法,它会生成一个 int 值的 “流”,每个 int 值对应一个码点:
Java
int[] codePoints = str.codePoints().toArray();将一个码点数组转换为一个字符串:
Java
String str = new String(codePoints, 0, codePoints.length);Tip:虚拟机不一定把字符串实现为代码单元序列。在 Java 9 中,只包含单字节代码单元的字符串使用
byte数组实现,所有其他字符串使用char数组。
6.3. 构建字符串
Java
StringBuilder builder = new StringBuilder();
builder.append(ch); // appends a single character
builder.append(str); // appends a string
String completedString = builder.toString();1
2
3
4
2
3
4
Note:
StringBuilder类在 Java 5 中引入。这个类的前身是StringBuffer,它的效率稍有些低,但允许采用多线程的方式添加或删除字苻。如果所有字符串编辑操作都在单个线程中执行(通常都是这样),则应该使用StringBuffer。这两个类的 API 是一样的。
7. 枚举类型
Java
public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE }枚举实际上也是一个类,在上面这个示例中,它刚好有 4 个实例,且不可能构造新的对象。因此,在比较两个枚举类型的值时,并不需要调用 equals,直接用 == 就可以了。
如果需要的话,可以为枚举类型增加构造器、方法和字段。当然,构造器只是在构造枚举常量的时候调用,并且枚举的构造器总是私有的。下面是一个示例:
Java
public enum Size {
SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
private final String abbreviation;
Size(String abbreviation) {
this.abbreviation = abbreviation;
}
public String getAbbreviation() {
return abbreviation;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
所有枚举类型都是 Enum 类的子类,它们继承了这个类的许多方法:
toString:返回枚举常量名。例如,Size.SMALL.toString()将返回字符串"SMALL";ordinal:返回enum声明中枚举常量的位置,位置从0开始计数。例如:Size.MEDIUM.ordinal()返回1;
Note:
toString的逆方法是Enum的静态方法valueOf。例如,Size s = Enum.valueOf(Size.class, "SMALL");将s设置成Size.SMALL。
Tip:实际上所有的枚举类型都扩展了泛型
Enum类。例如,Size扩展了Enum<Size>。
每个枚举类型都有一个静态的 values 方法,它返回一个包含全部枚举值的数组。例如,Size[] values = Size.values(); 返回一个包含 Size.SMALL、Size.MEDIUM、Size.LARGE、Size.EXTRA_LARGE 元素的数组。