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 方法
double 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.33333333Java 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)
如果文件不存在,创建该文件。可以像输出到 System.out 一样使用 print 、 println 以及 printf 命令。
当指定一个相对文件名时,文件位于相对于 Java 虚拟机启动目录的位置。如果在命令行方式下执行以下启动程序:
java MyProg
启动目录就是命令解释器的当前目录。然而,如果使用集成开发环境,name 启动目录将有 IDE 控制。可以使用下面的调用找到这个目录的位置:
String dir = System.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 实际上没有多维数组,只有一维数组