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 类型了。 byteshort 类型主要用于特定的场合,比如底层的文件处理或存储空间空间宝贵时的大数组

在 Java 中,整型的范围和运行 Java 代码的机器无关。而 C/C++ 会在针对不同处理器时选择最为高效的整型,所以可能会发生整型溢出。长整型数值有一个后缀 Ll0b 开头表示二进制数, 0x0X 前缀表示十六进制, 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 类型的数值有一个后缀 Ff 。没有后缀 F 的浮点数值总是默认为 double 类型,当然也可以添加后缀 Dd

可以使用十六进制表示浮点数值,例如 0.123 = 2^-3 可以表示为 0x0p-3,十六进制中以 p 表示指数

所有浮点数值计算都遵循 IEEE 754 规范,下面是用于表示溢出和出错情况下的三个特殊的浮点数值

  • 正无穷大Double.POSITIVE_INFINITY
  • 负无穷大Double.NEGATIVE_INFINITY
  • NaNDouble.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 类型

有两个值 falsetrue ,整型值和布尔值之间不能互相进行转换

变量与常量

声明变量

在 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 :求余数,如果被除数是负数,它所得到的余数也是正数

sincostanatanatan2exploglog10

PIE 分别用于表示 πe 常量最接近的近似值

如果得到可预测的结果比运行速度更重要的话,就应该使用 StrictMath 类

如果一个计算溢出,数学运算符会返回错误的结果,而 Math 方法却会抛出异常

数值类型之间的转换

byte => short => int => longchar => intint => longint => doublefloat => double 都为无信息丢失的转换

long --> doublelong --> doubleint --> 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) 中如果字符串 st 相等,则返回 true ,否则返回 false ,另外, st 可以是字符串字面量,也可以是字符串变量。

一定不要使用 == 检测两个字符串是否相等,这个运算符只能确定两个字符串是否存放在同一个位置上,因为,完全可能将内容相同的多个字符串副本放置在不同的位置上

空串与 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 一样使用 printprintln 以及 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 标签可以是

  • 类型为charbyteshortint的常量表达式
  • 枚举常量
  • 从 Java 7 开始,case标签还可以是字符串字面量

switch 语句中使用枚举常量时,不必在每个标签总知名枚举名,可以由 switch 的表达式值推导得出

中断控制流程的语句

Java 通过带标签的 break 来支持跳出循环

我们还可以在循环上加一个标签并紧跟一个冒号,之后通过 break 对应标签跳出对应循环

事实上,可以将标签应用到任何语句,甚至是 if 语句或块语句

continue 语句在 while 循环中将中断正常的控制流程,之后将控制转移到最内层循环的首部。而在 for 循环中将跳到 for 循环的更新部分。

大数

如果基本的整数和浮点数精度不能满足需求,可以使用 java.math 包中的两个很有用的类: BigIntegerBigDecimal ,这两个类可以处理包含任意长度数字序列的数值。 BigInteger 类可以实现任意精度的整数运算, BigDecimal 实现任意精度的浮点数运算。

使用静态的 valueOf 方法可以将普通的数值转换为大数

BigInteger a = BigInteger.valueOf(100);

对于更大的数,可以使用一个带字符串参数的构造器

BigInteger reallyBig = new BigInteger("0293490273948729873490237498723984729834790283749023749238")

还有一些常量 BigInteger.ZERTBigInteger.ONEBigInteger.TEN ,Java 9 之后还有 BigInteger.TWO

但是不能使用人们熟悉的算术运算符处理大数,需要使用大数类中的 addmultiply 方法。

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 实际上没有多维数组,只有一维数组