20220424 Java核心技术 卷1 基础知识 1-3

Java 程序设计概述

Java 是一种功能齐全的出色语言,是一个高质量的执行环境,还提供了一个庞大的库。

Java“ 白皮书” 的关键术语:

  1. 简单性
  2. 面向对象
  3. 分布式
  4. 健壮性
  5. 安全性
  6. 体系结构中立
  7. 可移植性
  8. 解释型
  9. 高性能
  10. 多线程
  11. 动态性

关 于 Java 的常见误解

Java 将成为适用于所有平台的通用性编程语言?

从理论上讲, 这是完全有可能的。 但在实际中, 某些领域其他语言有更出色的表现, 比如, Objective C 和后来的 Swift 在 IOS 设备上就有着无可取代的地位, 浏览器中的处理几乎完全由 JavaScript 掌控。 Windows 程序通常都用 C++ 或 C# 编写。 Java 在服务器端编程和跨平台客户端应用领域则很有优势。

Java 只不过是另外一种程序设计语言?

Java 是一种很好的程序设计语言, 很多程序设计人员喜欢 Java 胜过 C、 C++ 或 C# 。有上百种好的程序设计语言没有广泛地流行,而带有明显缺陷的语言, 如:C++ 和 Visual Basic 却大行其道。

这是为什么呢? 程序设计语言的成功更多地取决于其支撑系统的能力, 而不是优美的语法。

人们主要关注: 是否提供了易于实现某些功能的易用、 便捷和标准的库? 是否有开发工具提供商能建立强大的编程和调试环境? 语言和工具集是否能够与其他计算基础架构整合在一起?

Java 的成功源于其类库能够让人们轻松地完成原本有一定难度的事情。 例如:联网 Web 应用和并发。Java 减少了指针错误, 这是一个额外的好处, 因此使用 Java 编程的效率更高。但这些并不是 Java 成功的全部原因。

Java 程序设计环境

Java 运行时环境( JRE ), 它包含虚拟机但不包含编译器。这并不是开发者想要的环境, 而是专门为不需要编译器的用户而提供。

当 Oracle 为解决一些紧急问题做出某些微小的版本改变时, 将其称为更新。 例如:Java SE 8u31 是 JavaSE 8 的第 31 次更新, 它的内部版本号是 1.8.0_31。 更新不需要安装在前一个版本上,它会包含整个 JDK 的最新版本。 另外, 并不是所有更新都公开发布, 所以如果 “ 更新 31” 之后没有“ 更新 32”,你也不用惊慌。

Java 的基本程序设计结构

Java 区分大小写

在 Java 中, 每个句子必须用分号结束。

点号( • )用于调用方法。Java 使用的通用语法是

这等价于函数调用。

object,method(parameters)

这等价于函数调用。

在 Java 中,有 3 种标记注释的方式:

  • 最常用的方式是使用 /,其注释内容从 // 开始到本行结尾
  • 当需要长篇的注释时, 既可以在每行的注释前面标记 /,也可以使用 /**/ 将一段比较长的注释括起来
  • 第 3 种注释可以用来自动地生成文档。这种注释以 /** 开始, 以 */ 结束

/* */ 注释不能嵌套。也就是说, 不能简单地把代码用 /**/ 括起来作为注释, 因为这段代码本身可能也包含一个 */

数 据 类 型

Java 是一种 强类型语言 。这就意味着必须为每一个变量声明一种类型: 在 Java 中,共有 8 种基本类型 ( primitive type ), 其中有 4 种整型、2 种浮点类型、 1 种用于表示 Unicode 编码的字符单元的字符类型 char 和 1 种用于表示真值的 boolean 类型。

Java 有一个能够表示任意精度的算术包,通常称为“ 大数值”(bignumber)。 虽然被称为大数值,但它并不是一种新的 Java 类型, 而是一个 Java 对象。

整型

整型用于表示没有小数部分的数值, 它允许是负数。Java 提供了 4 种整型:

类型 存储需求 取值范围
byte 1 字节 [ -128 , 127 ]
short 2 字节 [ -32768 , 32767 ]
int 4 字节 [ -2147483648 , 2147483647] (正好超过 20 亿)
long 8 字节 [ -9223372036854775808 , 9223372036854775807]

在通常情况下, int 类型最常用。 但如果表示星球上的居住人数, 就需要使用 long 类型了。byteshort 类型主要用于特定的应用场合, 例如, 底层的文件处理或者需要控制占用存储空间量的大数组。

在 Java 中, 整型的范围与运行 Java 代码的机器无关。 这就解决了软件从一个平台移植到另一个平台,或者在同一个平台中的不同操作系统之间进行移植给程序员带来的诸多问题。

长整型数值有一个后缀 Ll ( 如 4000000000L。) 十六进制数值有一个前缀 0x0X (如 0xCAFEL 八进制有一个前缀 0 , 例如, 010 对应八进制中的 8 。 很显然, 八进制表示法比较容易混淆, 所以建议最好不要使用八进制常数。

从 Java 7 开始, 加上前缀 0b0B 就可以写二进制数。 例如,0b1001 就是 9 。另外,同样是
从 Java 7 开始,还可以为数字字面量加下划线, 如用 1_000_000 (或 0b1111_0100_0010_1000_0000 )
表示一百万。这些下划线只是为了让人更易读。Java 编译器会去除这些下划线。

注意, Java 没有任何无符号(unsigned) 形式的 int、 long、 short 或 byte 类型。

int i = 32;
// long
long l = 32L;
// 16 进制
int i16 = 0X20;
// 8 进制
int i8 = 040;
// 2 进制
int i2 = 0B100000;
// 为数字字面量加下划线,方便查看
int i2_2 = 0B10_0000;

浮点型

浮点类型用于表示有小数部分的数值。在 Java 中有两种浮点类型

类型 存储需求 取值范围
float 4 字节 大约 ±3.40282347E+38F (有效位数为 6 ~ 7 位)
double 8 字节 大约 ±1.79769313486231570E+308 (有效位数为 15 位)

double 表示这种类型的数值精度是 float 类型的两倍(有人称之为双精度数值)。绝大部分应用程序都采用 double 类型。在很多情况下,float 类型的精度很难满足需求。实际上,只有很少的情况适合使用 float 类型,例如,需要单精度数据的库, 或者需要存储大量数据。

float 类型的数值有一个后缀 Ff (例如, 3.14F)。没有后缀 F 的浮点数值(如 3.14 ) 默认为 double 类型。当然,也可以在浮点数值后面添加后缀 Dd (例如,3.14D )。

可以使用十六进制表示浮点数值。例如,0.125=2^-3 可以表示成 0x1.0p-3。在十六进制表示法中, 使用 p 表示指数, 而不是 e。 注意, 尾数采用十六进制, 指数采用十进制。指数的基数是 2, 而不是 10

科学计数法的表示方法为: Mantissa x Base ^ Exponent

举个例子,123.45用科学计数法可以表示为: 12345 x 10^(-2)
其中12345就是尾数Mantissa,10是基Base,-2是指数Exponent;
还可以表示为: 1.2345 x 10^(2)
其中1.2345就是尾数Mantissa,10是基Base,2是指数Exponent

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

  • 正无穷大
  • 负无穷大
  • NaN (不是一个数字)

例如,一个正整数除以 0 的结果为正无穷大。计算 0/0 或者负数的平方根结果为 NaN

常量 Double.POSITIVE_INFINITYDouble.NEGATIVE_INFINITYDouble.NaN ( 以及相应的 Float 类型的常量)分别表示这三个特殊的值, 但在实际应用中很少遇到。

特别要说明的是, 不能这样检测一个特定值是否等于 Double.NaN :

if (x = Double.NaN) // is never true

所有“ 非数值” 的值都认为是不相同的。然而, 可以使用 Double.isNaN 方法:

if (Double.isNaN(x)) // check whether x is "not a number  

警告: 浮点数值不适用于无法接受舍入误差的金融计算中。 例如,命令 ``System.out.println( 2.0-1.1 )将打印出0.8999999999999999, 而不是人们想象的0.9。**这种舍入误差的主要原因是 浮点数值采用二进制系统表示, 而在二进制系统中无法精确地表示分数 1/10。 这就好像十进制无法精确地表示分数1/3一样**。如果在数值计算中不允许有任何舍入误差,就应该使用BigDecimal` 类。

char 类型

char 类型原本用于表示单个字符。不过,现在情况已经有所变化。 如今,有些 Unicode 字符可以用一个 char 值描述, 另外一些 Unicode 字符则需要两个 char 值。

char 类型的字面量值要用单引号括起来。 例如: 'A' 是编码值为 65 所对应的字符常量。它与 "A" 不同,"A" 是包含一个字符 A 的字符串, char 类型的值可以表示为十六进制值,其范围从 \u0000\uffff 。例如:\u2122 表示注册符号 ( ™ ), \u03C0 表示希腊字母 ( π )。

除了转义序列 \u 之外, 还有一些用于表示特殊字符的转义序列。所有这些转义序列都可以出现在加引号的字符字面量或字符串中。 例如,'\u02122'"Hello\n"。 转义序列 \u 还可以出现在加引号的字符常量或字符串之外(而其他所有转义序列不可以)。 例如:

就完全符合语法规则, \u005B\u005D[] 的编码。

public static void main(String \u005B\u005D args)

// javac 可以编译通过,IDEA 会提示错误

就完全符合语法规则, \u005B\u005D[] 的编码。

特 殊 字 符 的 转 义 序 列:

转义序列 名称 Unicode 值
\b 退格 \u0008
\t 制表 \u0009
\n 换行 \u000a
\r 回车 \u000d
\" 双引号 \u0022
\' 单引号 \u0027
\\ 反斜杠 \u005c

警告: Unicode 转义序列会在解析代码之前得到处理。(生成的 .class 文件中 \u 内容已经被解析) 例如,"\u0022+\u0022" 并不是一个由引号(\u0022 ) 包围加号构成的字符串。 实际上, \u0022 会在解析之前转换为 ", 这会得到 ""+"" (两个空字符串相加),也就 是一个空串。

更隐秘地, 一定要当心注释中的 \u 。注释

// \u00A0 is a newline

会产生一个语法错误, 因为读程序时 \u00A0 会替换为一个换行符类似地, 下面这个注释

也会产生一个语法错误, 因为 \u 后面并未跟着 4 个十六进制数, ,

// Look inside c:\users

也会产生一个语法错误, 因为 \u 后面并未跟着 4 个十六进制数。

Unicode 和 char 类型

在设计 Java 时决定采用 16 位的 Unicode 字符集

经过一段时间, 不可避免的事情发生了。Unicode 字符超过了 65536 个,其主要原因是增加了大量的汉语、 日语和韩语中的表意文字。现在, 16 位的 char 类型已经不能满足描述所有 Unicode 字符的需要了。

Java 语言解决这个问题的基本方法。从 Java SE 5.0 开始。 码点 ( code point ) 是指与一个编码表中的某个字符对应的代码值。 在 Unicode 标准中,码点采用十六进制书写,并加上前缀 U+ , 例如 U+0041 就是拉丁字母 A 的码点。

Unicode 的码点可以分成 17 个代码级别 (codeplane)。第一个代码级别称为 基本的多语言级别 (basic multilingual plane ), 码点从 U+0000U+FFFF , 其中包括经典的 Unicode 代码;其余的 16 个级别码点从 U+10000U+10FFFF , 其中包括一些辅助字符(supplementary character)。

UTF-16 编码采用不同长度的编码表示所有 Unicode 码点。在基本的多语言级别中,每个字符用 16 位表示,通常被称为 代码单元(code unit) ; 而辅助字符采用一对连续的代码单元进行编码。这样构成的编码值落入基本的多语言级别中空闲的 2048 字节内, 通常被称为 替代区域(surrogate area) [ U+D800 ~ U+DBFF 用于第一个代码单元,U+DC00 ~ U+DFFF 用于第二个代码单元]。这样设计十分巧妙, 我们可以从中迅速地知道一个代码单元是一个字符的编码,还是一个辅助字符的第一或第二部分。

在 Java 中,char 类型描述了 UTF-16 编码中的一个代码单元。

我们强烈建议不要在程序中使用 char 类型, 除非确实需要处理 UTF-16 代码单元。最好将字符串作为抽象数据类型处理。

boolean 类型

boolean (布尔)类型有两个值:falsetrue, 用来判定逻辑条件。整型值和布尔值之间不能进行相互转换。

变 量

变量名必须是一个以字母开头并由字母或数字构成的序列。需要注意,与大多数程序设计语言相比,Java 中“ 字母” 和“ 数字” 的范围更大。

变量名中所有的字符都是有意义的,并且 大小写敏感 。变量名的长度基本上没有限制。

如果想要知道哪些 Unicode 字符属于 Java 中的“ 字母”, 可以使用 Character 类的 isJavaldentifierStartisJavaldentifierPart 方法来检查。

变量初始化

在 Java 中, 变量的声明尽可能地靠近变量第一次使用的地方, 这是一种良好的程序编写风格。

常量

在 Java 中, 利用关键字 final 指示常量。

关键字 final 表示这个变量只能被赋值一次。一旦被赋值之后, 就不能够再更改了。习惯上,常量名使用全大写。

在 Java 中,经常希望某个常量可以在一个类中的多个方法中使用,通常将这些常量称为类常量。可以使用关键字 static final 设置一个类常量。

运 算 符

在 Java 中,使用算术运算符 +、-、 *、/ 表示加、减、 乘、 除运算。 当参与 / 运算的两个操作数都是整数时, 表示整数除法;否则, 表示浮点除法。 整数的求余操作(有时称为取模) 用 表示。 例如, 15/2 等于 7 ,15%2 等于 1, 15.0/2 等于 7.5 。

需要注意, 整数被 0 除将会产生一个异常, 而浮点数被 0 除将会得到无穷大或 NaN 结果。

在默认情况下, 虚拟机设计者允许对中间计算结果采用扩展的精度。但是, 对于使用 strictfj 关键字标记的方法必须使用严格的浮点计算来生成可再生的结果。例如, 可以把 main 方法标记为

于是
, 在 main 方法中的所有指令都将使用严格的浮点计算。如果将一个类标记为
strictfp, 这个类中的所有方法都要使用严格的浮点计算。

public static strictfp void main(String[] args)

于是, 在 main 方法中的所有指令都将使用严格的浮点计算。如果将一个类标记为 strictfp , 这个类中的所有方法都要使用严格的浮点计算。

数学函数与常量

Math 类中,包含了各种各样的数学函数。

Math 类中, 为了达到最快的性能, 所有的方法都使用计算机浮点单元中的例程.. 如果得到一个完全可预测的结果比运行速度更重要的话, 那么就应该使用 StrictMath 类,, 它使用“ 自由发布的 Math 库”(fdlibm) 实现算法, 以确保在所有平台上得到相同的
结果。

数值类型之间的转换

经常需要将一种数值类型转换为另一种数值类型。 下图给出了数值类型之间的合法转换。

在下图中有 6 个实心箭头,表示无信息丢失的转换; 有 3 个虚箭头, 表示可能有精度损失的转换。 例如,123456789 是一个大整数, 它所包含的位数比 float 类型所能够表达的位数多。 当将这个整型数值转换为 float 类型时, 将会得到同样大小的结果,但却失去了一定的精度。

int n = 123456789;
float f = n; // f is 1.23456792E8

当使用上面两个数值进行二元操作时(例如 n + f, n 是整数, f 是浮点数,) 先要将两个操作数转换为同一种类型,然后再进行计算。

  • 如果两个操作数中有一个是 double 类型, 另一个操作数就会转换为 double 类型。
  • 否则, 如果其中一个操作数是 float 类型, 另一个操作数将会转换为 float 类型。
  • 否则, 如果其中一个操作数是 long 类型, 另一个操作数将会转换为 long 类型。
  • 否则, 两个操作数都将被转换为 int 类型。

img

强制类型转换

在必要的时候, int 类型的值将会自动地转换为 double 类型。但另一方面,有时也需要将 double 转换成 int。在这种情况下,需要通过强制类型转换 ( cast )实现这个操作。强制类型转换的语法格式是在圆括号中给出想要转换的目标类型,后面紧跟待转换的变量名。例如:

double x * 9.997;
int nx = (int) x;   

这样, 变量 nx 的值为 9。强制类型转换通过截断小数部分将浮点值转换为整型。

如果想对浮点数进行舍人运算, 以便得到最接近的整数(在很多情况下, 这种操作更有用,) 那就需要使用 Math.round 方法:

double x = 9.997;
int nx = (int) Math.round(x);  

现在, 变量 nx 的值为 10。 当调用 round 的时候, 仍然需要使用强制类型转换( int )。其原因是 round 方法返回的结果为 long 类型,由于存在信息丢失的可能性,所以只有使用显式的强制类型转换才能够将 long 类型转换成 int 类型。

警告: 如果试图将一个数值从一种类型强制转换为另一种类型, 而又超出了目标类型的表示范围, 结果就会截断成一个完全不同的值。 例如,(byte) 300 的实际值为 44。

System.out.println((byte) 300);  // 44 ,byte 只有一个字节长度,8位
System.out.println(Integer.toBinaryString(300));    // 100101100
System.out.println(0b00101100); // 44

结合赋值和运算符

可以在赋值中使用二元运算符,这是一种很方便的简写形式。 例如,
x += 4; 等价于:x = x + 4;

(一般地, 要把运算符放在= 号左边, 如 *=%= )。

注释: 如果运算符得到一个值, 其类型与左侧操作数的类型不同, 就会发生强制类型转换。 例如, 如果 x 是一个 int, 则以下语句

x += 3.5;

是合法的, 将把 x 设置为 (int)(x + 3.5)

int x = 3;
System.out.println(x + 3.5);  // 6.5
x += 3.5;     // 等价于 (int)(x + 3.5)  
System.out.println(x);  // 6

自增与自减运算符

在 Java 中, 提供了自增、 自减运算符: n++ 将变量 n 的当前值加 1, n-- 则将 n 的值减 1。 例如, 以下代码:

int n = 12;
n++;

将 n 的值改为 13。 由于这些运算符会改变变量的值, 所以它们的操作数不能是数值。 例如,4++ 就不是一个合法的语句。

实际上, 这些运算符有两种形式;上面介绍的是运算符放在操作数后面的“ 后缀” 形式。
还有一种“ 前缀” 形式:++n 。后缀和前缀形式都会使变量值加 1 或减 1。但用在表达式中时,二者就有区别了。前缀形式会先完成加 1; 而后缀形式会使用变量原来的值

建议不要在表达式中使用 ++, 因为这样的代码很容易让人闲惑,而且会带来烦人的 bug。

int x = 0;
System.out.println(x++);    // 0
x = 0;
System.out.println(++x);    // 1


int m = 7;
int n = 7;
int a = 2 * ++m; // now a is 16, m is 8
int b = 2 * n++; // now b is 14, n is 8
System.out.println(a);  // 16
System.out.println(b);  // 14

关系和 boolean 运算符

==(等于)、<(小于)、>(大于)、<= (小于等于)和 >=(大于等于)

&& 表示逻辑 运算符,使用 || 表示逻辑 运算符, ! 就是逻辑 运算符

&&|| 运算符是按照 短路 方式来求值的: 如果第一个操作数已经能够确定表达式的值,第二个操作数就不必计算了。

  • 如果 && 的第一个表达式结果为 false ,那么不会计算第二个表达式
  • 如果 || 的第一个表达式结果为 true ,那么不会计算第二个表达式
x != 0 && 1 / x > x + y // && 短路,保证第二个表达式中 x 不为 0
x == 0 || 1 / x > x + y // || 短路,保证第二个表达式中 x 不为 0

Java 支持三元操作符 ?: ,只会根据 condition 计算两个表达式中的一个

condition ? expression1 : expression2
x == 0 ? 0 : 1 / x
int x = 0;
System.out.println((x != 0) && (1 / x > x));    // false
System.out.println((x == 0) || (1 / x > x));    // true
System.out.println((x == 0 ? 0 : 1 / x));   // 0

位运算符

处理整型类型时,可以直接对组成整型数值的各个位完成操作。这意味着可以使用掩码技术得到整数中的各个位。位运算符包括:

  • & ("and")
  • | ("or")
  • ^ ("xor")
  • ~ ("not")

这些运算符按位模式处理。 例如, 如果 n 是一个整数变量, 而且用二进制表示的 n 从右边数第 4 位为 1,则

int fourthBitFromRight = (n & 0b1000) / 0b1000;		// n & 0b1000 结果只有两个,0 或者 0b1000

会返回 1, 否则返回 0。利用 & 并结合使用适当的 2 的幂, 可以把其他位掩掉, 而只保留其中的某一位。

| 有相似的作用,参考方法:java.lang.Integer#highestOneBit

还有 >><< 运算符将位模式左移或右移。 需要建立位模式来完成位掩码时, 这两个运算符会很方便:

int fourthBitFromRight = (n & (1 << 3)) >> 3;	// 等价于 (n & 0b1000) / 0b1000

>>> 运算符会用 0 填充高位,这与 >> 不同,它会用符号位填充高位。不存在 <<< 运算符。

警告: 移位运算符的右操作数要完成模 32 的运算(除非左操作数是 long 类型, 在这种情况下需要对右操作数模 64 )。 例如, 1<<35 的值等同于 1<<3 或 8 。

括号与运算符级别

下表给出了运算符的优先级。 如果不使用圆括号, 就按照给出的运算符优先级次序进行计算。同一个级别的运算符按照从左到右的次序进行计算 (除了表中给出的右结合运算符外) 。

+= 是右结合运算符, 所以表达式 a += b += c 等价于 a += (b += c)

运 算 符 结 合 性
[ ] . ( ) (方法调用) 从左向右
! ~ ++ -- + (一元运算、正号) - (一元运算、负号) () (强制类型转换) new 从右向左
* / % 从左向右
+ - 从左向右
<< >> >>> 从左向右
< <= > >= instanceof 从左向右
== != 从左向右
& 从左向右
^ 从左向右
` `
&& 从左向右
`
?: 从右向左
= += - = *= /= %= &= ` = ^=` `<<=` `>>=` `>>>=`
序列号 符号 名称 结合性(与操作数) 目数 说明
1 . 从左到右 双目
( ) 圆括号 从左到右
[ ] 方括号 从左到右
2 + 正号 从右到左 单目
- 负号 从右到左 单目
++ 自增 从右到左 单目 前缀增,后缀增
-- 自减 从右到左 单目 前缀减,后缀减
~ 按位非/取补运算 从右到左 单目
逻辑非 从右到左 单目 ! 不可以与 = 联用
3 ***** 从左到右 双目
/ 从左到右 双目 整数除法:取商的整数部分,小数部分去掉,不四舍五入
% 取余 从左到右 双目
4 + 从左到右 双目
- 从左到右 双目
5 << 左移位运算符 从左到右 双目
>> 带符号右移位运算符 从左到右 双目
>>> 无符号右移 从左到右 双目
6 < 小于 从左到右 双目 关系运算符“大于”说明
<= 小于或等于 从左到右 双目
> 大于 从左到右 双目
>= 大于或等于 从左到右 双目
instanceof 确定某对象是否属于指定的类 从左到右 双目
7 == 等于 从左到右 双目 关系运算符“==”说明
!= 不等于 从左到右 双目
8 & 按位与 从左到右 双目
9 | 按位或 从左到右 双目
10 ^ 按位异或 从左到右 双目
11 && 短路与 从左到右 双目
12 || 短路或 从左到右 双目
13 ? : 条件运算符 从右到左 三目
14 = 赋值运算符 从右到左 双目
+= 混合赋值运算符 从右到左 双目
-= 混合赋值运算符 从右到左 双目
*= 混合赋值运算符 从右到左 双目
/= 混合赋值运算符 从右到左 双目
%= 混合赋值运算符 从右到左 双目
&= 混合赋值运算符 从右到左 双目
|= 混合赋值运算符 从右到左 双目
^= 混合赋值运算符 从右到左 双目
<<= 混合赋值运算符 从右到左 双目
>>= 混合赋值运算符 从右到左 双目
>>>= 混合赋值运算符 从右到左 双目

枚举类型

枚举类型包括有限个命名的值。

字 符 串

从概念上讲, Java 字符串就是 Unicode 字符序列。 例如, 串 Java\u2122 由 5 个 Unicode 字符 J、a、v、a 和 ™ 。Java 没有内置的字符串类型, 而是在标准 Java 类库中提供了一个预定义类, 很自然地叫做 String每个用双引号括起来的字符串都是 String 类的一个实例。

子串

String 类的 substring 方法可以从一个较大的字符串提取出一个子串。

String greeting = "Hello";
String s = greeting.substring(0, 3);    // Hel

substring 方法的第二个参数是不想复制的第一个位置。 这里要复制位置为 0、 1 和 2 (从 0 到 2, 包括 0 和 2 ) 的字符。在 substring 中从 0 开始计数,直到 3 为止, 但不包含 3。

substring 的工作方式有一个优点: 容易计算子串的长度。字符串 s.substring(a, b) 的长度为 b-a 。例如, 子串“ Hel ” 的长度为 3-0=3。

拼接

Java 语言允许使用 + 号连接(拼接)两个字符串。

当将一个字符串与一个非字符串的值进行拼接时,后者被转换成字符串。

如果需要把多个字符串放在一起, 用一个定界符分隔,可以使用静态 join 方法:

String join = String.join("", "S", "M", "L", "XL");     // SMLXL
String all = String.join(" / ", "S", "M", "L", "XL");   // S / M / L / XL

不可变字符串

String 类没有提供用于修改字符串的方法。

Java 文档中将 String 类对象称为 不可变字符串

不可变字符串有一个优点:编译器可以让字符串共享

检测字符串是否相等

可以使用 equals 方法检测两个字符串是否相等。

要想检测两个字符串是否相等,而不区分大小写, 可以使用 equalsIgnoreCase 方法。

一定不要使用 == 运算符检测两个字符串是否相等! 这个运算符只能够确定两个字串是否放置在同一个位置上。当然, 如果字符串放置在同一个位置上, 它们必然相等。但是,完全有可能将内容相同的多个字符串的拷贝放置在不同的位置上。

空 串 与 Null 串

空串 "" 是长度为 0 的字符串

null 表示目前没有任何对象与该变量关联

有时要检查一个字符串既不是 null 也不为空串,这种情况下就需要使用以下条件:

if (str != null && str.length() != 0) 

码点和代码单元

Java 字符串由 char 值序列组成。 char 数据类型是一个采用 UTF-16 编码表示 Unicode 码点的代码单元。大多数的常用 Unicode 字符使用一个代码单元就可以表示,而辅助字符需要一对代码单元表示。

length 方法将返回采用 UTF-16 编码表示的给定字符串所需要的代码单元数量。

要想得到实际的长度,即码点数量,可以调用:

int cpCount = s1.codePointCount(0, s1.length());  
System.out.println("\ud83d\ude00");     // 😀
System.out.println("\u4e2d");   // 中

System.out.println(Integer.toHexString('中'));   // 4e2d

String s1 = "😀";
s1.codePoints().forEach(codePoint -> System.out.println(codePoint + " :: " + Integer.toHexString(codePoint)));  // 128512 :: 1f600
System.out.println(s1.length());    // 2
System.out.println(s1.codePointCount(0, s1.length()));  // 1
s1.chars().forEach(charItr -> System.out.println(charItr + " :: " + Integer.toHexString(charItr)));
/*
        55357 :: d83d
        56832 :: de00
         */

System.out.println(StringUnicodeUtil.string2Unicode("😀"));  // \ud83d\ude00

char c1 = '\u4e2d';     // 中
// char c2 = '\ud83d\ude00';    // 编译报错:Too many characters in character literal
char c3 = '\ud83d'; //
System.out.println(c3);     // ?

String s2 = new String(s1.codePoints().toArray(), 0, s1.codePoints().toArray().length);

构 建 字 符 串

有些时候, 需要由较短的字符串构建字符串, 例如, 按键或来自文件中的单词。采用字符串连接的方式达到此目的效率比较低。 每次连接字符串, 都会构建一个新的 String 对象,既耗时, 又浪费空间。使用 StringBuilder 类就可以避免这个问题的发生。

在 JDK5.0 中引入 StringBuilder 类。 这个类的前身是 StringBuffer ,其效率稍有些低, 但允许采用多线程的方式执行添加或删除字符的操作。如果所有字符串在一个单线程中编辑 (通常都是这样,) , 则应该用 StringBuilder 替代它。 这两个类的 AP丨是相同的。

输入输出

要想通过控制台进行输人,首先需要构造一个 Scanner 对象,并与“ 标准输人流 System.in 关联。

Scanner scanner = new Scanner(System.in);
String s1 = scanner.nextLine();
System.out.println("s1 :: " + s1);
String s2 = scanner.next();
System.out.println("s2 :: " + s2);
while (!scanner.hasNext("#")) {
    System.out.println(scanner.next());
}
System.out.println(scanner.next());
System.out.println("s3 ::" + scanner.nextInt());
System.out.println("s4 ::" + scanner.nextDouble());

/*
abc
s1 :: abc
a b c #
s2 :: a
b
c
#
1
s3 ::1
1
s4 ::1.0

*/

因为输入是可见的, 所以 Scanner 类不适用于从控制台读取密码。Java SE 6 特别引入了 Console 类实现这个目的。

Console cons = System.console();
String username = cons.readLine("User name: ");
char[] passwd = cons.readPassword("Password: ");

System.out.println(username + " == " + new String(passwd));

直接在 IDE 的控制台执行会报空指针错误,因为获取步到 Console ,需要在命令行上执行 java 运行代码,此时输入密码时,控制台上不可见。

格式化输出

Java SE 5.0 沿用了 C 语言库函数中的 printf 方法。

每一个以 字符开始的格式说明符都用相应的参数替换。 格式说明符尾部的转换符将指示被格式化的数值类型:f 表示浮点数,s 表示字符串,d 表示十进制整数。下表列出了所有转换符:

转换符 类型 举例
d 十进制整数 159
x 十六进制整数 9f
o 八进制整数 237
f 定点浮点数 15.9
e 指数浮点数 1.59e+01
g 通用浮点数
a 十六进制浮点数 0x1.fccdp3
s 字符串 Hello
c 字符 H
b 布尔 True
h 散列码 42628b2
tx 或 Tx (T 强制大写) 日 期 时 间 已经过时, 应当改为使用 java.time 类
% 百分号 %
n 与平台有关的行分隔符

另外,还可以给出控制格式化输出的各种标志:

标志 目的 举例
+ 打印正数和负数的符号 +3333.33
空格 在正数之前添加空格 | 3333.33|
0 数字前面补 0 003333.33
- 左对齐 |3333.33 |
( 将负数括在括号内 ( 3333.33 )
, 添加分组分隔符 3,333.33
# ( 对于 f 格式) 包含小数点 3,333.
# (对于 x 或 0 格式) 添加前缀 0x 或 0 0xcafe
$ | 给定被格式化的参数索引。例如, %1\(d, %1\)x 将以十进制和和十六进制格式打印同第一个参数 159 9F
< 格式化前面说明的数值。 例如,%d%<x 以十进制和十六进制打印同一个数值 159 9F

可以使用 s 转换符格式化任意的对象,, 对于任意实现了 Formattable 接口的对象都将调用 formatTo 方法; 否则将调用 toString 方法, 它可以将对象转换为字符串

可以使用静态的 String.format 方法创建一个格式化的字符串, 而不打印输出:

String message = String.format("Hello, %s. Next year , you'll be %d", name, age) ;  

基于完整性的考虑, 下面简略地介绍 printf 方法中日期与时间的格式化选项。 在新代码中, 应当使用 java.time 包的方法。 不过你可能会在遗留代码中看到 Date 类和相关的格式化选项。 格式包括两个字母, 以 t 开始, 以下表中的任意字母结束。

System.out.printf("%tc", new Date());   //      1.23星期一 十月 25 15:56:15 CST 2021
转换符 类 型 举 例
C 完整的日期和时间 Mon Feb 09 18:05:19 PST 2015
F ISO 8601 日期 2015-02-09
D 美国格式的日期 (月 / 日 / 年) 02/09/2015
T 24 小时时间 18:05:19
r 12 小时时间 06:05:19 pm
R 24 小时时间没有秒 18:05
Y 4 位数字的年 (前面补 0 ) 2015
y 年的后两位数字(前面补 0 ) 15
C 年的前两位数字 (前面补0 ) 20
B 月的完整拼写 February
b 或 h 月的缩写 Feb
m 两位数字的月 (前面补 0 ) 02
d 两位数字的日 (前面补 0 ) 09
e 两位数字的日(前而不补 0 ) 9
A 星期几的完整拼写 Monday
a 星期几的缩写 Mon
j 三位数的年中的日子(前面补 0 ) ,在 001 到 366 之间 069
H 两位数字的小时 ( 前面补 0 ) ,在 0 到 23 之间 18
k 两位数字的小时( 前面不补 0 ) ,在 0 到 23 之间 18
I(大写 i ) 两位数字的小时(前面补 0 ) ,在 0 到 12 之间 06
l (小写 L) 两位数字的小时 (前面不补 0 ) ,在 0 到 12 之间 6
M 两位数字的分钟 (前面补 0 ) 05
S 两位数字的秒(前面补 0 ) 19
L 三位数字的毫秒(前面补 0 ) 047
N 九位数字的毫微秒 (前面补 0 ) 047000000
P 上午或下午的标志 pm
z 从 GMT 起, RFC822 数字位移 -0800
Z 时区 PST
s 从格林威治时间 1970-01-01 00:00:00 起的秒数 1078884319
Q 从格林威治时间 1970-01-01 00:00:00 起的毫秒数 1078884319047

如果需要多次对日期操作实现对每一部分进行格式化的目的, 可以采用一个格式化的字符串指出要被格式化的参数索引。 索引必须紧跟在 % 后面, 并以 $ 终止。

还可以选择使用 < 标志。它指示前而格式说明中的参数将被再次使用。

提示: 参教索引值从 1 开始,而不是从 0 开始,对第1个参数格式化。这就避免了与 0 标志混淆

现在, 已经了解了 printf 方法的所有特性。下图给出了格式说明符的语法图:

img

许多格式化规则是本地环境特有的。例如,在德国,组分隔符是句号而不是逗号,Monday 被格式化为 Montag 。

文件输入与输出

要想对文件进行读取, 就需要一个用 File 对象构造一个 Scanner 对象

Scanner in = new Scanner(Paths.get(path), "UTF-8");

在这里指定了 UTF-8 字符编码, 读取一个文本文件时,要知道它的字符编码,如果省略字符编码, 则会使用运行这个 Java 程序的机器的 “默认编码”。 这不是一个好主意,如果在不同的机器上运行这个程序, 可能会有不同的表现

要想写入文件, 就需要构造一个 PrintWriter 对象

PrintWriter out = new PrintWriter(path, "UTF-8") ;

以下方式容易错用,这不是指定一个文件路径,而是将字符串作为源数据:

Scanner in = new Scanner("myfile.txt");

当指定一个相对文件名时, 例如,“myfile.txt”,“mydirectory/myfile.txt” 或 “../myfile.txt”, 文件位于 Java 虚拟机启动路径的相对位置

启动路径就是命令解释器的当前路後。 然而, 如果使用集成开发环境, 那么启动路径将由 IDE 控制。 可以使用下面的调用方式找到路径的位置:

System.out.println(System.getProperty("user.dir"));

如果觉得定位文件比较烦恼, 则可以考虑使用绝对路径

当采用命令行方式启动一个程序时, 可以利用 Shell 的重定向语法将任意文件关联到 System.inSystem.out

java MyProg < myfile.txt > output.txt

控 制 流 程

与任何程序设计语言一样, Java 使用条件语句和循环结构确定控制流程。

没有 goto 语句, 但 break 语句可以带标签, 可以利用它实现从内层循环跳出的目的

尽管 Java 允许在 for 循环的各个部分放置任何表达式, 但有一条不成文的规则:for 语句的 3 个部分应该对同一个计数器变量进行初始化、 检测和更新。若不遵守这一规则,编写的循环常常晦涩难懂

switch 语句 有可能触发多个 case 分支。 如果在 case 分支语句的末尾没有 break 语句, 那么就会接着执行下一个 case 分支语句。这种情况相当危险, 常常会引发错误。 为此, 我们在程序中从不使用 switch 语句

case 标签可以是:

  • 类型为 char、byte、 short 或 int 的常量表达式
  • 枚举常量
  • 从 Java SE 7 开始, case 标签还可以是字符串字面量

通常,使用 goto 语句被认为是一种拙劣的程序设计风格。

无限制地使用 goto 语句确实是导致错误的根源,但在有些情况下,偶尔使用 goto 跳出循环还是有益处的。Java 设计者同意这种看法,甚至在 Java 语言中增加了一条带标签的 break, 以此来支持这种程序设计风格。

请注意,标签必须放在希望跳出的最外层循环之前, 并且必须紧跟一个冒号

事实上,可以将标签应用到任何语句中, 甚至可以应用到 if语句或者块语句中,
如下所示:

label:
{
    ...
    if (condition) break label; // exits block
    ...
}
// jumps here when the break statement executes  

还有一种带标签的 continue 语句,将跳到与标签匹配的循环首部。

label1:
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        if (i == 1 && j == 1) {
            break label1;
        }
        System.out.println(i + " :: " + j);
    }
}
System.out.println("over 1...");


label2:
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        if (i == 1 && j == 1) {
            continue label2;
        }
        System.out.println(i + " :: " + j);
    }
}
System.out.println("over 2...");


label3:
{
    System.out.println("111");
    if (true) {
        break label3;
    }
    System.out.println("222");
}

输出为:

0 :: 0
0 :: 1
0 :: 2
1 :: 0
over 1...
0 :: 0
0 :: 1
0 :: 2
1 :: 0
2 :: 0
2 :: 1
2 :: 2
over 2...
111

大 数 值

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

主要方法:

  • valueOf
  • add
  • subtract
  • multipiy
  • divide
  • mod
  • compareTo

数 组

数组是一种数据结构, 用来存储同一类型值的集合。

声明数组的两种方式:

  • int[] a (推荐,类型和变量名分开)
  • int a[]

创建数组时,会给数组元素赋值,默认初始值看类型,例如:

intp a = new int[100];	// 默认初始值是 0

一旦创建了数组, 就不能再改变它的大小。

for each 循环

增强的 for 循环的语句格式为:

for (variable : collection) statement  

collection 这一集合表达式必须是一个数组或者是一个实现了 Iterable 接口的类对象(例如 ArrayList )。

传统的 for 循环:

for (int i = 0; i < arr.length; i++)  

在很多场合下, 还是需要使用传统的 for 循环。例如, 如果不希望遍历集合中的每个元素, 或者在循环内部需要使用下标值等。

数组初始化以及匿名数组

创建数组对象并同时赋予默认初始值:

int[] arr = new int[5];

创建数组对象并同时赋予初始值的简化书写形式:

int[] small Primes = { 2, 3, 5, 7, 11, 13 };

初始化一个匿名的数组,创建一个新数组并利用括号中提供的值进行初始化, 数组的大小就是初始值的
个数:

new int[] { 17, 19, 23, 29, 31, 37 };

在 Java 中,允许数组长度为 0。在编写一个结果为数组的方法时, 如果碰巧结果为空, 则这种语法形式就显得非常有用。此时可以创建一个长度为 0 的数组:

new elementType[0]

注意, 数组长度为 0 与 null 不同。

数组拷贝

Arrays 类的 copyOf 方法:

int[] arrB = Arrays.copyOf(arrA, arrA.length);

第 2 个参数是新数组的长度。这个方法通常用来控制数组的大小:

  • 如果长度小于原始数组的长度,则只拷贝最前面的数据元素
  • 如果长度大于原始数组的长度,多余的元素将被赋默认初始值
int[] arr = {1,2,3,4,5};
System.out.println(Arrays.toString(Arrays.copyOf(arr, 8)));     // [1, 2, 3, 4, 5, 0, 0, 0]
System.out.println(Arrays.toString(Arrays.copyOf(arr, arr.length)));     // [1, 2, 3, 4, 5]
System.out.println(Arrays.toString(Arrays.copyOf(arr, 3)));     // [1, 2, 3]
System.out.println(Arrays.toString(Arrays.copyOf(arr, 0)));     // []

命令行参数

main 方法的 String[] args 参数,如果不传入参数,默认是长度为 0 的 String 数组,如果执行以下命令:

java Message -g cruel world

args 数组包含以下内容:

args[0]:"-g"
args[l]:"cruel"
args[2]:"world"

数组排序

要想对数值型数组进行排序, 可以使用 Arrays 类中的 sort 方法,这个方法使用了优化的快速排序算法。

产生一个抽彩游戏中的随机数值组合。 例如抽彩是从 49 个数值中抽取 6 个 :

public class LotteryDrawing {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);

        System.out.print("How many numbers do you need to draw? ");
        int k = in.nextInt();

        System.out.print("What is the highest number you can draw? ");
        int n = in.nextInt();

        // fill an array with numbers 1 2 3 . . . n
        int[] numbers = new int[n];
        for (int i = 0; i < numbers.length; i++)
            numbers[i] = i + 1;

        // draw k numbers and put them into a second array
        int[] result = new int[k];
        for (int i = 0; i < result.length; i++) {
            // make a random index between 0 and n - 1
            // int r = (int) (Math.random() * n);
            int r = 2;

            // pick the element at the random location
            result[i] = numbers[r];

            // move the last element into the random location
            numbers[r] = numbers[n - 1];
            n--;
        }

        // print the sorted array
        Arrays.sort(result);
        System.out.println("Bet the following combination. It'll make you rich!");
        for (int r : result)
            System.out.println(r);
    }
}

程序的目的是,产生多个不重复的随机值。

代码的思路是:

  1. 初始化值域数组 numbers ,值为下标,长度为 n
  2. 循环获取 k 个值
    1. 获取随机值 r ,随机值的范围在 [0, n-1]
    2. 将 numbers[r] 赋值给 result[i]
    3. numbers[r] 重新赋值为 numbers[n-1] ,并将 n 递减 1,这里很重要,相当于排除了已被选中的下标,并将之后可选的数组 numbers 的范围缩小 1

多维数组

多维数组将使用多个下标访问数组元素, 它适用于表示表格或更加复杂的排列形式。

int[][] magicSquare = {{16, 3, 2, 13}, {5, 10, 11, 8}, {9, 6, 7, 12}, {4, 15, 14, 1}};
System.out.println(Arrays.deepToString(magicSquare));
for (int[] ints : magicSquare) {
    for (int anInt : ints) {
        System.out.println(anInt);
    }
}

不规则数组

Java 实际上没有多维数组, 只有一维数组。 多维数组被解释为“ 数组的数组 ” 。

img

表达式 balances[i] 引用第 i 个子数组, 也就是二维表的第 i 行。它本身也是一个数组, balances[i][j] 引用这个数组的第 j 项。

由于可以单独地存取数组的某一行, 所以可以让两行交换。

double[] temp = balances[i];
balances[i] = balances[i + 1];
balances[i + 1] = temp;

还可以方便地构造一个 “ 不规则 ” 数组, 即数组的每一行有不同的长度。

int[][] arr = new int[3][];
for (int i = 0; i < arr.length; i++) {
    arr[i] = new int[i + 1];
}
for (int[] ints : arr) {
    for (int anInt : ints) {
        System.out.print(anInt+"\t");
    }
    System.out.println();
}

/*
	0	
	0	0	
	0	0	0	
*/
posted @ 2022-04-24 21:07  流星<。)#)))≦  阅读(58)  评论(0编辑  收藏  举报