Java 核心技术
Java 程序设计环境
Java Development Kit(Java 开发工具包)
简称JDK
,编写 Java 程序的程序员使用的软件Java Runtime Environment(Java 运行时环境)
简称JRE
,是运行 Java 程序用户使用的软件Server JRE(服务器JRE)
,在服务器上运行 Java 程序的软件Standard Edition(标准版)
,简称SE
,用于桌面或复杂服务器应用的平台Enterprise Edition(企业版)
,简称EE
,用于复杂服务器应用的 Java 平台OpenJDK
,Java SE 的一个免费开源实现
Java 的基本程序设计结构
语法
类
- Java 应用程序中的全部内容都必须放置在类中
- 类名都是以大写字母开头的名词。如果名字由多个单词组成,每个单词的第一个字母都应该大写,比如驼峰命名法
- 源代码的文件名必须与公共类的名字相同
- 运行已编译的程序时,Java 虚拟机总是从指定类中的
main
方法的代码开始执行,因此为了代码能够执行,在类的源文件中必须包含一个main
方法。当然,也可将用户自定义的方法添加到类中,并在main
方法中调用这些方法 - 如果
main
方法正常退出,那么 Java 应用程序的退出码为0
{}
:和 C/C++
一样,Java 中的任何方法的代码都用 {
开始,用 }
结束
;
:在 Java 中,每个橘子必须用分号结束,回车不是语句的结束标志
""
:与 C/C++
一样,采用双引号界定字符串
注释
Java 中的注释不会出现在可执行程序中。最常用的方式是使用 //
,从 //
到本行末尾,当需要长注释时以 /**
开始 */
结束
数据类型
Java 中一共有 8 种数据类型,其中 4 种整型、2 种浮点类型、1 种字符类型 char
(用于表示 Unicode 编码的代码单元)和 1 种用于表示真值的 boolean
类型
Java 有一个能够表示任意精度算术包,通常称为大数(big number)。但它不是基本类型,而是一个对象
整型
用于表示没有小数部分的数值,允许是负数,Java 提供了 4 种整型,具体内容如下
类型 | 存储需求 | 取值范围 |
---|---|---|
int | 4 字节 | -2, 147, 483, 648 ~ 2, 147, 483, 647 |
short | 2 字节 | -32, 768 ~ 32, 767 |
long | 8 字节 | -9, 223, 372, 036, 854, 775, 808 ~ 9, 223, 372, 036, 854, 775, 807 |
byte | 1 字节 | -128 ~ 127 |
通常情况下, int
类型最常用,但如果想要表示整个地球的居住人口,就需要使用 long
类型了。 byte
和 short
类型主要用于特定的场合,比如底层的文件处理或存储空间空间宝贵时的大数组
在 Java 中,整型的范围和运行 Java 代码的机器无关。而 C/C++
会在针对不同处理器时选择最为高效的整型,所以可能会发生整型溢出。长整型数值有一个后缀 L
或 l
。 0b
开头表示二进制数, 0x
或 0X
前缀表示十六进制, 0
前缀表示八进制。还可以为数字字面量添加下划线,比如 1_000_000
表示一百万,Java 编辑器会去除这些下划线
浮点类型
浮点类型用于表示有小数部分的数值,在 Java 中有两种浮点类型,如下所示
类型 | 存储需求 | 取值范围 |
---|---|---|
float | 4 字节 | 大约 ± 3.402, 823, 47E+38F(有效数为 6 ~ 7 位) |
double | 8 字节 | 大约 ± 1.797, 693, 134, 862, 315, 70E+308(有效数字为 15 位) |
double
表示这种类型的数值精度是 float
类型的两倍。只有很少的情况下适合使用 float
类型, float
类型的数值有一个后缀 F
或 f
。没有后缀 F
的浮点数值总是默认为 double
类型,当然也可以添加后缀 D
或 d
可以使用十六进制表示浮点数值,例如 0.123 = 2^-3 可以表示为 0x0p-3,十六进制中以 p 表示指数
所有浮点数值计算都遵循 IEEE 754
规范,下面是用于表示溢出和出错情况下的三个特殊的浮点数值
- 正无穷大
Double.POSITIVE_INFINITY
- 负无穷大
Double.NEGATIVE_INFINITY
- NaN
Double.NaN
浮点数值不适用于无法接受舍入误差的金融计算。可以使用 BigDecimal
类
char 类型
char
类型原本用于表示单个字符,现在有些 Unicode
字符可以用一个 char
值描述,另外一些 Unicode
则需要两个 char
值。它的值需要用单引号括起来,例如 'A'
是编码值为 65 的字符常量,它与 "A"
不同, "A"
是包含一个字符 A
的字符串。 char
类型的值可以表示为十六进制值,范围从 \u0000
到 \uFFFF
,除了 \u
之外,还有一些用于表示特殊字符的转义序列,如下
转义序列 | 名称 | Unicode 值 |
---|---|---|
\b | 退格 | \u0008 |
\t | \u0009 | |
\n | \u000a | |
\r | \u000d | |
\" | \u0022 | |
\' | \u0027 | |
\\ | \u005c |
注意,在注释中,不要出现 \u,在代码中最好不要出现 char 除非确实需要处理 UTF-16 代码单元
boolean 类型
有两个值 false
和 true
,整型值和布尔值之间不能互相进行转换
变量与常量
声明变量
在 Java 中,每个变量都有一个类型,在声明变量时,先指定变量的类型,然后是变量名。每个声明都以分号结束,而且是一条完整的 Java 语句。
变量名必须是一个以字母开头并由数字构成的序列,与其他语言相比,Java 中的范围更大,字母包括大小写, _
, $
,或在某种语言中表示字母的任何 Unicode
字符。变量名的长度基本没有限制
尽管 $ 是一个合法的 Java 字符,但不要在自己的代码中使用这个字符。它只作用在 Java 编译器或其他工具生成的名字中
单 _
不能作为变量名,可以在一行中声明多个变量
int i,j
变量初始化
声明一个变量之后,必须用赋值语句对变量进行显式初始化,千万不要使用未初始化的值。赋值需要将值放在 =
右侧,在 Java 中可以将声明放在任何地方
从 Java 10 开始,对于局部变量,如果可以从变量的初始值推断出它的类型,就不再需要声明类型
var vacationDays = 12 // int
var greeting = "Hello" // String
常量
在 Java 中,利用关键字 final
指示常量,它只能被赋值一次。一旦赋值,就不能被更改,习惯上,常量名使用大写。
枚举类型
有时,变量的取值只在一个有限的集合内,我们可以自定义枚举类型。枚举类型包括有限个命名的值,比如
enum Size { SMALL, MEDIUN, LARGE, EXTRA_LARGE }
现在可以声明这种类型的变量
Size s = Size.MEDIUM
运算符
运算符用于连接值
算术运算符
+
、 -
、 *
、 /
表示加减乘除运算,当参与 /
运算的两个操作数都是整数时,表示整数除法;否则,表示浮点除法。求余用 %
表示,整数被 0
除会产生一个异常,而浮点数被 0
除将会得到无穷大或 NaN
结果
数字函数与常量
在 Math
中,包含了各种各样的数学函数
sqrt
:平方根
pow
:幂
floorMod
:求余数,如果被除数是负数,它所得到的余数也是正数
sin
, cos
, tan
, atan
, atan2
, exp
, log
, log10
PI
和 E
分别用于表示 π
和 e
常量最接近的近似值
如果得到可预测的结果比运行速度更重要的话,就应该使用 StrictMath 类
如果一个计算溢出,数学运算符会返回错误的结果,而 Math 方法却会抛出异常
数值类型之间的转换
byte
=> short
=> int
=> long
, char
=> int
, int
=> long
, int
=> double
, float
=> double
都为无信息丢失的转换
long
--> double
, long
--> double
, int
--> float
都表示可能有精度损失的转换
当用一个二元运算符连接两个值时,需要将两个操作数转换为同一种类型,再进行计算
- 如果两个操作数有一个是
double
,另一个会转为double
- 否则,如果有一个是
float
,另一个操作数转换为float
- 否则,如果有一个是
long
,另一个操作数转换为long
- 否则,都转换为
int
强制类型转换
可能损失信息的转换要通过强制类型转换累完成,强制类型转换的语法格式是在圆括号中给出想要转换的目标类型,后面紧跟待转换的变量名
double x = 9.997;
int nx = (int) x; // nx 的值为 9
如果想要对浮点数进行舍入运算,则需要使用 Math.round
方法
doube x = 9.997;
int nx = (int) Math.round(x); // round 的值为 long 类型,所以还是需要使用强制类型转换
不要在 boolean 类型与任何数值之间进行强制类型转换,可以使用 b ? 1 : 0 代替
结合赋值和运算符
+=
, -=
等
如果运算符得到的值的类型与左侧操作数的类型不同,就可能会发生强制类型转换
自增与自减运算符
++
和 --
关系和 boolean 运算符
==
检测相等性
!=
检测不等
<
、 <=
、 >
、 >=
和 &&
、 ||
等
位运算符
处理整型类型时,可以直接对组成整数的各个位子完成操作。这意味着可以使用掩码技术得到证书中的各个位。位运算符奥阔
&
、 |
、 ^
、 ~
这些运算符按位模式处理。如,如果 n
是一个整数变量,而且用二进制表示的 n
从右到左边数第 4 位为 1,则
int fourthBitFromRight = (n & 0b100) / 0b1000
会返回 1
,否则返回 0
另外还有 >>
和 <<
运算符可以将位模式左移或右移。需要建立位模式来完成位掩码时,这两个运算符会很方便:
int forthBitFromRight = (n & (1 << 3)) >> 3;
最后, >>>
会用 0
填充高位,这与 >>
不同,它会用符号位填充高位。不存在 <<<
运算符
字符串
从概念上讲,Java 字符串就是 Unicode 字符序列。Java 没有内置的字符串类型,而是在标准 Java 类库中提供了一个预定义类,很自然地叫做 String。每个用双引号括起来的字符串都是 String 类的一个实例
子串
String
类的 substring
方法可以从一个较大的字符串中提取出一个子串,复制的子串不包含第二个参数
拼接
Java 语言允许使用 +
号连接两个字符串。当一个字符串与一个非字符串的值进行拼接时,后者会转换成字符串。如果需要把多个字符串拼接在一起,可以使用 join
方法
String.join("/", "S", "M", "L")
=> S/M/L
String.repeat`方法可以重复字符串
不可变字符串
String
类没有提供修改字符串中某个字符的方法。我们需要通过 substring
提取想要保留的子串,再与希望替换的字符拼接。
由于不能修改 Java 字符串中的单个字符,所以在 Java 文档中将 String
类对象称为是不可变的。不过,可以修改字符串变量,让它引用其他字符串
检测字符串是否相等
可以使用 equals
方法检测两个字符串是否相等,对于表达式 s.equals(t)
中如果字符串 s
与 t
相等,则返回 true
,否则返回 false
,另外, s
与 t
可以是字符串字面量,也可以是字符串变量。
一定不要使用 ==
检测两个字符串是否相等,这个运算符只能确定两个字符串是否存放在同一个位置上,因为,完全可能将内容相同的多个字符串副本放置在不同的位置上
空串与 Null 串
空串 ""
是长度为 0
的字符串。可以调用以下代码检查一个字符串是否为空:
if(str.length() == 0)
或
if(str.equals(""))
空串是一个 Java 对象,有自己的串长度和内容。不过, String
变量还可以存放一个特殊的值,名为 null
,表示目前没有任何变量与该变量关联。要检查一个字符串是否为 null
,要使用以下条件:
if (str == null)
码点与代码单元
Java 字符串由 char
值序列组成。 char
数据类型是一个采用 UTF-16
编码表示 Unicode
码点的代码单元。最常用的 Unicode
字符可以用一个代码单元表示,而辅助字符需要用一对代码单元表示
length
方法可以返回 UTF-16
编码给定字符串所需要的代码单元数量,想要获得实际的长度,即码点数量,需要调用:
s.codePointCount(0, s.length())
调用 s.charAt(n)
将返回位置 n
的代码单元, n
介于 0 ~ s.length() - 1
之间,想要得到第 n
个码点,应该使用
int index = greeting.offsetByCodePoints(0, i);
int last = greeting.codePointAt(index);
不能忽略 U+FFFF 以上的奇怪字符,有些 emoji 符号可能会在字符串中加入
如果想要遍历一个字符串,并以此查看每一个码点,可以使用下列语句
int cp = sentence.codePointAt(i);
if (Character.isSupplementaryCodePoint(cp)) i += 2;
else i++;
可以使用下列语句实现反向遍历
i--;
if (Character.isSurrogate(sentence.charAt(i))) i--;
int cp = sentence.codePointAt(i);
更容易的办法是使用 codePoints
方法,它会生成一个 int
值的流,每个 int
值对应一个码点。可以将它转换称为一个数组,再完成遍历
int[] codePoints = str.codePoints().toArray();
要把一个码点数组转换成为字符串,可以使用构造器
String str = new String(codePoints, 0, codePoints.length)
String API
见 P49
构建字符串
有时,需要由较短的字符串构建字符串,例如,按键或文件中的单词。如果采用字符串拼接的方式达到这个目的,效率会比较低。每次构建一个新的 String
对象时,耗时又费时间。使用 StringBuilder
类可避免这个问题
如果需要用到很多小段字符串来构建一个字符串,应按一下步骤进行
StringBuilder builder = new StringBuilder();
当每次需要添加一部分内容时,就调用 append
方法
builder.append(ch);
builder.append(str);
构建完成时调用 toString
方法,将可以得到一个 String
对象,其中包含了构建器中的字符序列
String completedString = builder.toString();
StringBuilder
类方法见 P54
输入与输出
我们使用基本的控制台来实现输入与输出
读取输入
想通过控制台进行输入,首先需要构建一个与“标准输入流” System.in
关联的 Scanner
对象
Scanner in = new Scanner(System.in)
现在,就可以使用 Scanner
中的各种方法读取输入了
System.out.print("What is your name?");
String name = in.nextLine();
想要读取一个单词,可以使用
String firstName = in.next()
想要读取一个整数,就调用 nextInt
方法
System.out.print("How old are you?");
int age = in.nextInt();
于此同时,想要读取下一个浮点数,就调用 nextDouble
方法
Scanner
类定义在 java.util
包中。当使用的类不是定义在基本 java.lang
包中的时候,一定要使用 import
指令导入相应的包
import java.util.*;
因为输入可见,所以不要使用 Scanner 类从控制台读取密码,应该使用 Console 类
api 见 P56
格式化输出
可以使用语句 System.out.print(x)
将数值 x
输出到控制台。这条命令将以 x
的类型所允许的最大非 0 数位个数打印输出 x
。如:
double x = 1000.0 / 3.0;
System.out.print(x);
// 打印 3333.33333333
Java 5 沿用了 C 语言函数库中的 printf
方法,例如,调用 System.out.printf("%8.2f", x)
会以一个字段宽度打印 x
:这包括 8 个字符,另外精度为小数点后两个字符,也就是说,这会打印一个前导的空格和 7 个字符,如下
3333.33
可以为 printf
提供多个参数,例如
System.out.printf("Hello, %s. Next year, you'll be %d", name, age);
每一个以 %
字符开始的格式说明符都用对应的参数替换。格式说明符尾部的转换符只是要格式化的数值的类型。具体见 P58
另外,还可以指定控制格式化输出外观的各种标志。见 P58。比如
System.out.printf("%,.2f", 10000.0 / 3.0);
=> 3,333.33
可以使用静态的 String.format
方法创建一个格式化的字符串,而不打印输出:
String message = String.format("Hello, %s. Next year, you'll be %d", name, age)
对于新的代码应该使用 java.time
包的方法。不过可能在遗留代码中看到 Date
类和相关的格式化选项。具体见 P59
文件输入与输出
想要读取一个文件,需要构造一个 Scanner
对象,如下:
Scanner in = new Scanner(Path.of("myfile.txt"), StandardCharsets.UTF_8);
如果文件名中包含反斜杠符号,就要记住在每个反斜杠之前再加一个额外的反斜杠定义: c:\\mydirectory\\myfile.txt
指定字符编码对于互联网上的文件很常见
现在可以利用前面介绍的任何一个 Scanner
方法对文件进行读取
想要写入文件,需要构造一个 PrimtWriter
对象。在构造器中,需要提供文件名和字符编码:
PrintWriter out = new PrintWriter("myfile.txt", StandardCharsets.UTF_8)
如果文件不存在,创建该文件。可以像输出到 Sysem.out
一样使用 print
、 println
以及 printf
命令。
当指定一个相对文件名时,文件位于相对于 Java 虚拟机启动目录的位置。如果在命令行方式下执行以下启动程序:
java MyProg
启动目录就是命令解释器的当前目录。然而,如果使用集成开发环境,name 启动目录将有 IDE 控制。可以使用下面的调用找到这个目录的位置:
String dir = Sysem.getProperty("user.dir");
控制流程
Java 使用条件语句和循环结构确定控制流程。当需要对某个表达式的多个值进行检测时,可以使用 switch
语句
块作用域
块(即复合语句)是由若干条 Java 语句组成的语句,并用一堆大括号括起来,块确定了变量的作用域。一个块可以嵌套在另一个块中。但是,不能在嵌套的两个块中声明同名的变量。
条件语句
在 Java 中,条件语句的形式为
if(condition) statement
这里的条件必须用小括号括起来
Java 常常希望在某个条件为真时执行多条语句。在这种情况下,就可以使用块语句。
在 Java 中,更一般的条件语句如下所示
if (condition) statement1 else statement2
也可以使用 if ... else if ...
循环
当条件为 true
时, while
循环执行一条语句,一般形式为
while (condition) statement
如果希望循环体至少执行一次,请使用
do statement whild (condition)
确定循环
for
循环是支持迭代的一种结构,如下所示,下面的循环将数字 1 ~ 10 输出到屏幕上
for (int i = 1; i <= 10; i++)
System.out.println(i);
在循环中,检测两个浮点数是否相等需要格外小心,应为可能永远不会结束
for
语句的第一部分声明了一个变量之后,这个变量的作用域就扩展到了这个 for
循环的末尾
多重选择:switch 语句
switch
语句将从与选项值相匹配的 case
标签开始执行,直到遇到 break
语句,或执行到 switch
语句的结束处为止。如果没有相匹配的 case
标签,就用那个 default
子句
case
标签可以是
- 类型为
char
、byte
、short
或int
的常量表达式 - 枚举常量
- 从 Java 7 开始,
case
标签还可以是字符串字面量
当 switch
语句中使用枚举常量时,不必在每个标签总知名枚举名,可以由 switch
的表达式值推导得出
中断控制流程的语句
Java 通过带标签的 break
来支持跳出循环
我们还可以在循环上加一个标签并紧跟一个冒号,之后通过 break
对应标签跳出对应循环
事实上,可以将标签应用到任何语句,甚至是 if 语句或块语句
continue
语句在 while
循环中将中断正常的控制流程,之后将控制转移到最内层循环的首部。而在 for
循环中将跳到 for
循环的更新部分。
大数
如果基本的整数和浮点数精度不能满足需求,可以使用 java.math
包中的两个很有用的类: BigInteger
和 BigDecimal
,这两个类可以处理包含任意长度数字序列的数值。 BigInteger
类可以实现任意精度的整数运算, BigDecimal
实现任意精度的浮点数运算。
使用静态的 valueOf
方法可以将普通的数值转换为大数
BigInteger a = BigInteger.valueOf(100);
对于更大的数,可以使用一个带字符串参数的构造器
BigInteger reallyBig = new BigInteger("0293490273948729873490237498723984729834790283749023749238")
还有一些常量 BigInteger.ZERT
、 BigInteger.ONE
和 BigInteger.TEN
,Java 9 之后还有 BigInteger.TWO
但是不能使用人们熟悉的算术运算符处理大数,需要使用大数类中的 add
和 multiply
方法。
api 见 P77
数组
数组存储相同类型值的序列
声明数组
数组是一种数据结构,用于存储相同类型值的集合。通过一个整型下标(index,或索引)可以访问数组中的每一个值。在声明数组变量时,需要指出数组类型(数组元素紧跟 [])和数组变量的名字。
int[] a;
不过这条语句只声明了变量 a
,没有将 a
初始化为一个真正的数组。应该使用 new
操作符创建数组
int[] a = new int[100]; // or var a = new int[100]
这条语句声明并初始化了一个可以存储 100 个整数的数组。
数组长度不要求是常量: new int[n]
会创建一个长度为 n
的数组。
一旦创建了数组,就不能改变它的长度,但是可以改变单个数组元素,如果程序运行中需要经常扩展数组的大小,就应该使用另一种数据结果——数组列表。
在 Java 中,提供了一种创建数组对象并同时提供初始值的间歇性是
int[] smallPrimes = { 2, 3, 5, 7, 11, 13 };
最后一个值允许有逗号,你可以不断为数组增加值
还可以声明一个匿名数组:
new int[] { 17, 19, 23,29,31,37 };
可以使用这种语法重新初始化一个数组而无须创建新变量,比如
smallPrimes = new int[] { 17, 19, 23, 29, 31 }
在 Java 中,允许长度为空的数组,这和 null 不同
访问数组元素
创建一个数字数组时,所有元素都初始化为 0。 boolean
数组的元素会初始化为 false
。对象数组的元素则初始化为一个特殊值 null
String[] names = new String[10];
会创建一个包含 10 个字符串的数组,所有字符串都为 null
。如果希望这个数组包含空串,必须指定空串
for (int i = 0; i < 10; names[i] = "");
for each 循环
Java 中有一种功能很强的循环结构,可以依次处理数组(或其他元素集合)中的每个元素,而不必考虑下标值
for(variable : collection) statement
它定义一个变量用于暂存集合中的每个元素,并执行相应的语句。 collection
这一集合表达式必须是一个数组或是实现了 Iterable
接口的类对象。
但是当需要使用下标值时还是要使用 for
循环
有一个简单的打印数组所有值的方法,即 Array.toString(a)
数组拷贝
在 java 中,允许将一个数组变量拷贝到另一个数组变量,这时,两个变量将引用同一个数组
int[] luckyNumbers = smallPrimes;
luckyNumbers[5] = 12; // 现在 smallPrimes[5] 也是 12
如果希望将一个数组的所有值拷贝到一个新数组去,要使用 Arrays
类的 copyOf
方法:
int[] copiedLuckyNumbers = Arrays.copyOf(luckyNumbers, luckyNumbers.length);
第二个参数通常是数组的长度,这个方法通常用来增加数组的大小:
luckyNumbers = Arrays.copyOf(luckyNumbers, 2 * luckyNumbers.length);
Java 中的 [] 运算符北与定位会完成越界检查,而且没有指针运算,即不通过 a 加 1 得到数组中的下一个元素
命令行参数
每一个 Java 应用程序都有一个带 String arg[]
参数的 main
方法。这个参数表明 main
方法将接受一个字符串数组
数组排序
要想对数值型数组进行排序,可以使用 Arrays
类中的 sort
方法,这个方法使用了快速排序算法
int[] a = new int[10000];
// ...
Array.sort(a)
Api 见 P85
多维数组
多维数组将使用多个下标访问数组元素,它适用于表示表格或更加复杂的排列形式。声明一个二维数组:
balances = new double[NYEARS][NRATES];
另外如果知道数组元素,可以不调用 new
,直接使用简写形式来对多维数组进行初始化
int[][] magicSquare =
{
{16, 3, 2, 13},
{5, 10, 11, 9},
{9, 1, 4, 12}
}
一旦数组初始化,就可以利用两个中括号访问各个元素,比如 balance[i][j]
可以使用两个 for each 循环嵌套遍历多维数组
打印二维数组的数据元素列表,可以使用 System.out.println(Arrays.deepToString(a))
不规则数组
Java 实际上没有多维数组,只有一维数组