<!-- # 《Java 核心技术 卷Ⅰ》 第3章 Java 的基本程序设计结构 --> <!-- core-java-volume-1-fundametals-ch-03 --> ## 3.1 一个简单的 Java 应用程序 - 借助于包(`package`)可以方便地组织自己的代码。 - 使用包的主要原因是确保类名的唯一性。 - **Sun 公司建议将公司的因特网域名以逆序的形式作为包名,并且对于不同的项目使用不同的子包。** ```java package me.liujiajia.java.sample; public class FirstSample { public static void main(String[] args) { System.out.println("Hello,World!"); } } ``` 如果不带包名,可以通过如下方式编译并运行。 ```bash javac FirstSample.java java FirstSample ``` **注意:**运行时不带 *.class* 后缀。 如果带包名,上面的方式会报 *错误: 找不到或无法加载主类 FirstSample* 的错误。 此时编译及运行时需带上包名(命令行需定位到代码的根目录)。 ```bash javac me/liujiajia/java/sample/FirstSample.java java me/liujiajia/java/sample/FirstSample ``` ## 3.2 注释 Java 中有三种注释方式: - `//` 注释内容从 `//` 开始到行尾 - `/* */` 注释内容为 `/* */` 之间的所有内容,支持多行。 输入 `/*` 后回车即可自动生成。 - `/** */` 用在方法和类上,用来自动生成文档。 输入 `/**` 后回车即可自动生成。 ```java public class FirstSample { /** * 应用入口方法 * * @param args */ public static void main(String[] args) { // 打印 Hello,World! System.out.println("Hello,World!"); /* 这是一段多行注释 当注释很多时可以使用这种方式 */ } } ``` ## 3.3 数据类型 Java 是一种强类型语言,必须为每一个变量声明一种类型。 在 Java 中一共有 8 种基本类型(*primitive type*): - 4 种整型(`int`、`short`、`long`、`byte`) - 2 种浮点类型(`float`、`double`) - 1 种用于表示 *Unicode* 编码的字符单元的字符类型 `char` - 1 种用于表示真值的 `boolean` (布尔)类型 ### 3.3.1 整型 - `int`(4字节)(*-2,147,483,648*(`-2^31`) ~ *2,147,483,647*(`2^31 - 1`)) - `short`(2字节)(*32,768*(`-2^15`) ~ *32,767*(`2^15 - 1`)) - `long`(8字节)(*-9,223,372,036,854,775,808*(`-2^63`) ~ *9,223,372,036,854,775,807*(`2^63 -1`)) - `byte`(1字节)(*-128*(`-2^7`)~*127*(`2^7-1`)) 在 Java 中,整型的范围与运行 Java 代码的机器无关。 (C 和 C++ 程序需要针对不同的处理器选择最为高效的整型) **数值字面量** 长整型数值有一个后缀 `L` 或 `l`(因为小写 `l` 和 数值 `1` 很容易混淆,大多数规约中强制使用大写 `L`)。 十六进制使用前缀 `0x` 或 `0X`(例:*0xCAFE*)。 八进制使用前缀 `0` (例:`010`)(因为前缀 `0` 比较容易混淆,一般不推荐使用八进制的常数)。 二进制使用前缀 `0b` 或 `0B`(例:*0b1001* 表示十进制的 *9*)。 数字字面量可以添加下划线(`_`)以提高代码的可读性(例:`100_0000` 或 `0b1111_0100_0010_0001`)。 ### 3.3.2 浮点类型 - `float`(4字节)(有效位数为 6 ~ 7 位) - `double`(8字节)(有效位数为 15 位) **数值字面量** - `float` 值有一个后缀 `F` 或 `f`(例:*3.14F*) - `double` 值有一个后缀 `D` 或 `d`(例:*3.14D*) 没有后缀的小数默认为 `double` 型。 **三个特殊的浮点数** - 正无穷大(`Double.POSITIVE_INFINITY`) - 负无穷大(`Double.NEGATIVE_INFINITY`) - *NAN*(不是一个数字)(`Double.NaN`) **浮点类型的误差** `System.out.println(0.1 + 0.2);` 打印的不是 *0.3* ,而是 *0.30000000000000004* 。 这种舍入误差的主要原因是浮点数采用二进制系统表示,而在二进制中无法精确的表示分数 `1/10` 。 就像十进制无法精确的表示 `1/3` 一样。 在金融计算等不允许舍入误差的场景,应该使用 `BigDecimal` 类。 ### 3.3.3 char 类型 `char` 类型原本用于表示单个字符。不过,现在一些 *Unicode* 字符需要两个 `char` 值。 `char` 类型的字面量值需要用单引号括起来。如 *'A'* 。 `char` 类型的值可以表示为十六进制值,其范围从 `\u0000` 到 `\uFFFF` 。如 `\u03C0` 表示 *π* 。 **特殊字符的转义** - `\b` 退格 - `\t` 制表 - `\n` 换行 - `\r` 回车 - `\”` 双引号 - `\'` 单引号 - `\\` 反斜杠 **字符串之外的 `\u` 转义序列** 如 `public static void main(String\u005B\u005D args)` 就完全符合语法规则,其中 `\u005B` 和 `\u005D` 是 *[* 和 *]* 的编码。 *Unicode* 转义序列会在解析代码之前得到处理。 - `“\u0022+\u0022”`\u0022 会在解析之前转换为 “ ,这会得到 “”+“”,也就是一个空串。 - `// \u00A0 is a newline` `\u00A0` 会被替换为一个换行符。这会产生一个语法错误。 - `// look inside C:\users` 也会产生一个语法错误,因为 `\u` 后面并未跟着 4 个十六进制数。 ### 3.3.4 Unicode 和 char 类型 1991 年发布 *Unicode 1.0* 时,当时仅占用了 65536 个代码值中不到一半的部分。 设计 Java 时决定采用 16 位的 *Unicode* 字符集。 随着大量汉语、日语和韩语中表意文字的增加,*Unicode* 字符集超过了 65536 个。 为了解决这个问题,先要介绍几个专用术语。 - **码点(code point)** 指与一个编码表中的某个字符对应的代码值。 在 *Unicode* 标准中,码点采用十六进制书写,并加上前缀 `U+` 。 例:`U+0041` 就是字母 *A* 的码点。 - **代码级别(code plane)** Unicode 的码点可以分成 17 个代码级别。 第一个代码级别别称为 基本的多语言级别(*basic multilingual plane*),码点从 `U+0000` 到 `U+FFFF`。 其余的 16 个级别码点从 `U+10000` 到 `U+10FFFF` ,其中包括一些 **辅助字符**。 - **UTF-16** *UTF-16* 编码采用不同长度的编码表示所有 *Unicode* 码点。 - **代码单元(code unit)** 在基本的多语言级别中,每个字符用 16 位表示,通常被称为代码单元。 辅助字符则采用一对连续的代码单元进行编码。 如何知道一个代码单元是一个字符的编码还是一个辅助字符的第一或第二部分呢? - **替代区域(surrogate area)** UTF-16 巧妙的利用了基本的多语言级别中空闲的 2048 个值(`U+D800` ~ `U+DFFF`),这部分值通常被称为替代区域。 `U+D800` ~ `U+DBFF` 用于第一个代码单元(1024个值),`U+DC00` ~ `U+DFFF` 用于第二个代码单元(1024个值)。组合起来共有 `2^20` (即*104 8576*)个码点,也正好是其它16个代码级别码点的总数量。 在 Java 中,`char` 型描述了 *UTF-16* 编码中的一个代码单元(*code unit*)。 ### 3.3.5 boolean 类型 `boolean` (布尔)类型有两个值: `false` 和 `true` ,用来判断逻辑条件。 整型值和布尔值之间不能进行互相转换。(有些语言中 *0* 相当于 `false` ,非 *0* 相当于 `true` ) ## 3.4 变量 - 在 Java 中,每个变量都有一个类型(*type*)。 - 声明变量时,变量类型位于变量名之前。 ```java double salary; int vacationDays; ``` - 变量名必须是一个以字母开头并由字母或数字构成的序列。 与大多数语言相比,Java 中“字母”和“数字”的范围更大,例如 *π* 。 - 变量名大小写敏感。 - 变量名的长度基本上没有限制。 ### 3.4.1 变量初始化 - 声明一个变量后,必须用赋值语句对变量进行显式初始化。 否则编译器会报错:*variable not initialized* - 要想对一个已经声明过的变量进行赋值,就需要将变量名放在等号(`=`)左侧,取值的 Java 表达式放在等号的右侧。 ```java int vacationDays; vacationDays = 12; ``` 也可以将变量的声明和初始化放在同一行。 ```java int vacationDays = 12; ``` - 在 Java 中,变量的声明尽可能地靠近变量第一次使用的地方,这是一种良好的编写风格。 ### 3.4.2 常量 在 Java 中,利用关键字 `final` 指示常量。 ```java final double CM_PER_INCH = 2.54; ``` - 关键字 `final` 表示这个变量只能被赋值一次。一旦被赋值之后,就不能再更改。 - 习惯上,常量名使用全大写。 在 Java 中,经常希望某个常量可以在一个类中的多个方法中使用,通常将这些常量成为 **类常量**。可以使用关键字 `static final` 设置一个类常量。 ```java public static final double CM_PER_INCH = 2.54; ``` 如果一个常量被声明为 `public` ,那么其它类的方法也可以使用这个常量。 `const` 虽然是 Java 保留的关键字,但目前并没有使用。 ## 3.5 运算符 - `+` 加 - `-` 减 - `*` 乘 - `/` 除 当参与 `/` 运算的两个操作数都是整数时,表示整数除法; 否则,表示浮点除法。 整数被 0 除会产生一个异常; 浮点数被 0 除将会得到无穷大或 *NAN* 结果。 - `%` 求余(又称取模) ### 3.5.1 数学函数与常量 在 `Math` 类中,包含了各种各样的数学函数。 - `Math.sqrt(x)` 平方根 - `Math.pow(x,a)` 幂运算 - `Math.floorMod` 为了解决负整数的余数可能小于 0 的问题 在[这篇博客][1]中就看到了如果被除数是负数,得到的余数也可能是负数。 而在数学上的最优规则是:余数总是 ≥ 0。 可以使用 `(x % a + a) % a` 来满足上面的规则,但是很麻烦。 需要注意的是,如果除数是负数,仍然可能得到负的余数。 - `Math.sin` 三角函数 - `Math.cos` 三角函数 - `Math.tan` 三角函数 - `Math.atan` 三角函数 - `Math.atan2` 三角函数 - `Math.exp` 指数函数 - `Math.log` 自然对数 - `Math.log10` 以 10 为底的对数 - `Math.PI` *π* 近似数 - `Math.E` *e* 近似数 可以通过静态导入的方式避免在每次调用的时候添加 `Math` 前缀。 ```java import static java.lang.Math.*; ``` `StrictMath` 类提供了严格浮点计算版本的数学函数。 ### 3.5.2 数值类型之间的转换 下图是类型之间的合法转换。 实线表示无信息丢失;虚线表示可能会有精度损失。  两个数值进行二元操作时,先将两个操作数转换为同一种类型,然后再进行计算。 - 如果两个操作数中有一个是 `double` 类型,另一个操作数就会转换为 `double` 类型。 - 否则,如果其中一个操作数是 `float` 类型,另一个操作数将会转换为 `float` 类型。 - 否则,如果其中一个操作数是 `long` 类型,另一个操作数将会转换为 `long` 类型。 - **否则,两个操作数都将被转换为 `int` 类型。** ### 3.5.3 强制类型转换 强制类型转换的语法格式是在圆括号中给出想要转换的目标类型,后面紧跟待转换的变量名。 ```java double x = 8.22; int nx = (int) x; // 8 ``` 强制类型转换通过截断小数部分将浮点数转换为整形。 需要舍入运算时,需使用 `Math.round` 方法。 ```java double x = 8.22; int nx = (int) Math.round(x); ``` ### 3.5.4 结合赋值和运算符 ```java x += 4; ``` 相当于 ```java x = x + 4; ``` ### 3.5.5 自增和自减运算符 ```java int m = 7; int n = 7; int a = 2 * ++m; // a = 16, m = 8 int b = 2 * n++; // b = 14, n = 8 ``` **建议不要在表达式中使用 `++` ,因为这样的代码很容易让人困惑,而且会带来烦人的 bug 。** ### 3.5.6 关系和 boolean 运算符 - `==`:监测相等性 - `!=`:监测不等性 - `<`:小于 - `>`:大于 - `<=`:小于等于 - `>=`:大于等于 - `&&`:逻辑与 - 按照“短路”方式求值 - `||`:逻辑或 - 按照“短路”方式求值 - `!`:逻辑非 - `?:`:三元操作符 ### 3.5.7 位运算符 - `&`:与 - `|`:或 - `^`:异或 - `~`:非 - `>>`:右移(用符号位填充高位) - `<<`:左移 - `>>>`:右移(用0填充高位) > **警告** > > 移位运算符的右操作数需要完成模32的运算(左操作数是 `long` 型时模64)。 ### 3.5.8 括号和运算符级别 | 运算符 | 结合性 | | --- | --- | | `[]` `.()(方法调用)` | 从左向右 | | `!` `~` `++` `--` `+(一元运算)` `-(一元运算)` `()(强制类型转换)` `new` | **从右向左** | | `*` `/` `%` | 从左向右 | | `+` `-` | 从左向右 | | `<<` `>>` `>>>` | 从左向右 | | `<` `<=` `>` `>=` `instanceof` | 从左向右 | | `==` `!=` | 从左向右 | | `&` | 从左向右 | | `^` | 从左向右 | | <code>|</code> | 从左向右 | | `&&` | 从左向右 | | <code>||</code> | 从左向右 | | `?:` | **从右向左** | | `=` `+=` `-=` `*=` `/=` `%=` `&=` <code>|=</code> `^=` `<<=` `>>=` `>>>=` | **从右向左** | ```java // && 的优先级高于 || a && b || c // 等价于 (a && b) || c ``` ```java // += 是右结合运算符 a += b += c // 等价于 a += (b += c) ``` ### 3.5.9 枚举类型 ```java enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE }; ``` ## 3.6 字符串 从概念上讲, Java 字符串就是 Unicode 字符序列。 Java 没有内置的字符串类型,而是在标准类库中提供了一个预定义类 `String` 。 ```java String e = ""; String greeting = "Hello"; ``` ### 3.6.1 子串 ```java String greeting = "Hello"; String s = greeting.substring(0, 3); ``` `substring` 的第二个参数是不相复制的第一个位置(即子串不包含第二个参数位置的字符)。 `substring` 方法很容易计算子串的长度。 `substing(a, b)` 的长度为 `b - a` 。 ### 3.6.2 拼接 使用 `+` 号拼接两个字符串。 当一个字符串和一个非字符串拼接时,后者被转换成字符串。 ```java int age = 13; String rating = "PG" + age; ``` 如需把多个字符串放在一起,用一个定界符分割,可以使用静态 *join* 方法。 ```java String all = String.join(" / ", "S", "M", "L", "XL"); // all = "S / M / L / XL" ``` ### 3.6.3 不可变字符串 `String` 类没有提供用于修改字符串的方法。 ```java greeting = greeting.substring(0, 3) + "p!" ``` 由于不能修改 Java 字符串中的字符,所以在 Java 文档中将 `String` 类对象称为 **不可变字符串** 。 **优点:编译器可以让字符串共享。** Java 的设计者认为共享带来的高效率远远胜过提取、拼接字符串所带来的低效率。 ### 3.6.4 检测字符串是否相等 ```java s.equals(t); "Hello".equals(greeting); "Hello".equalsIngnoreCase("hello"); ``` **一定不要使用 `==` 运算符检测两个字符串是否相等!** 这个运算符只能够确定两个字符串是否放在同一个位置上。 如果虚拟机始终将相同的字符串共享,就可以使用 `==` 运算符检测是否相等。但实际上**只有字符串常量是共享的**,而 `+` 或 `substring` 等操作产生的结果并不是共享的。 ### 3.6.5 空串与 Null 串 空串 `""` 是长度为 0 的字符串。 检查是否为空串: ```java if (str.length == 0) { } ``` 或 ```java if ("".equals(str)) { } ``` 空串是一个 Java 对象,有自己的串长度(0)和内容(空)。不过,`String`变量还可以存放一个特殊的值,名为 `null`,这表示目前没有任何对象与该变量关联。 检查字符串是否为 `null` : ```java if (str == null) { } ``` 检查字符串既不是 `null` 也不是空串: ```java if (str != null && str.length() != 0) { } ``` ### 3.6.6 码点和代码单元 Java 字符串由 `char` 值序列组成。 `char` 数据类型是一个采用 *UTF-16* 编码表示 *Unicode* 码点的代码单元。 大多数的常用 *Unicode* 字符使用一个代码单元就可以表示,而辅助字符需要一对代码单元表示。 `length()` 方法将返回采用 *UTF-16* 编码表示的给定字符串所需要的代码单元数量。 ```java String greeting = "Hello"; int n = greeting.length(); // is 5 ``` 获取码点数量: ```java int cpCount = greeting.codePointCount(0, greeting.length()); ``` 获取指定位置的代码单元: ```java char first = greeting.charAt(0); // first is 'H' char last = greeting.charAt(4); // last is 'o' ``` 获取指定位置的码点: ```java int index = greeting.offsetByCodePoints(0, i); int cp = greeting.codePointAt(index); ``` 字符 `𝕆` (*U+1D546*)需要两个代码单元。 ```java String sentence = "𝕆 is the set of octonions"; char ch = sentence.charAt(1); // is � ``` > 可以在浏览器中运行 js 脚本获取对应的字符。 > ```javascript > String.fromCodePoint(0x1D546) > ``` 返回不是一个空格,而是 `𝕆` 的第二个代码单元。 遍历所有码点: ```java int cp = sentence.codePointAt(i); if (Character.isSupplementaryCodePoint(cp)) i += 2; else i++; ``` 回退操作: ```java i--; if (Character.isSurrogate(sentence.charAt(i))) i--; int cp = sentence.codePointAt(i); ``` 使用 `codePoints` 方法遍历: ```java int[] codePoints = sentence.codePoints().toArray(); ``` 把码点转换为字符串: ```java String str = new String(codePoints, 0, codePoints.length); ``` ### 3.6.7 String API `String` 类常用的方法: - `char charAt(int index)` - `int codePointAt(int index)` 5.0 - `int offsetByCodePoints(int startIndex, int cpCount)` 5.0 - `int compareTo(String other)` - `IntStream codePoints()` 8 - `new String(int[] codePoints, int offset, int count)` 5.0 - `boolean equals(Object other)` - `boolean euqalsIngnoreCase(String other)` - `boolean startWith(String prefix)` - `boolean endWith(String suffix)` - `int indexOf(String str)` - `int indexOf(String str, int fromIndex)` - `int indexOf(int cp)` - `int indexOf(int cp, int fromIndex)` - `int lastIndexOf(String str)` - `int lastIndexOf(String str, int fromIndex)` - `int lastIndexOf(int cp)` - `int lastIndexOf(int cp, int fromIndex)` - `int length()` - `int codePointCount(int startIndex, int endIndex)` 5.0 - `String replace(CharSequence oldString, CharSequence newString)` - `String substring(int beginIndex)` - `String substring(int beginIndex, int endIndex)` - `String toLowerCase()` - `String toUpperCase()` - `String trim()` - `String join(CharSequence delimiter, CharSequence... elements)` 8 ### 3.6.8 阅读联机 API 文档 ==> [Java™ Platform, Standard Edition 8 API Specification][2] 左上角选择 *java.lang* ,之后在左下角选择 *String* ,就可以看到 `String` 类的所有方法了。点击方法名的链接,可以看到方法的描述。 ### 3.6.9 构建字符串 ```java char ch = 'H'; String str = "ello"; StringBuilder builder = new StringBuilder(); builder.append(ch); // appends a single character builder.append(str); // appends a string String completedString = builder.toString(); ``` **java.lang.StringBuilder 5.0** - `StringBuilder()` - `int length()` - `StringBuilder append(String str)` - `StringBuilder append(char c)` - `StringBuilder appendCodePoint(int cp)` - `void setCharAt(int i, char c)` - `StringBuilder insert(int offset, String str)` - `StringBuilder insert(int offset, char c)` - `StringBuilder delete(int startIndex, int endIndex)` - `String toString()` ## 3.7 输入输出 ### 3.7.1 读取输入 构建一个 `Scanner` 对象,并与“标准输入流” `System.in` 关联。 ```java import java.util.Scanner; public class ScannerSample { public static void main(String[] args) { Scanner in = new Scanner(System.in); System.out.print("What is your name? "); String name = in.nextLine(); System.out.print("How old are your? "); int age = in.nextInt(); System.out.printf("Hello, %s. Next year you'll be %d.", name, age + 1); in.close(); } } ``` **java.util.Scanner 5.0** - `Scanner(InputStream in)` - `String nextLine()` - `String next()` 读取输入的下一个单词(以空格作为分隔符) - `int nextInt()` - `double nextDouble()` - `boolean hasNext()` 检测输入中是否还有其它单词 - `boolean hasNextInt()` - `boolean hasNextDouble()` **java.lang.System 1.0** - `static Console console()` 6 如果有可能进行交互操作,就通过控制台窗口为交互的用户返回一个 `Console` 对象,否则返回 `null` 。 **java.io.Console** 6 - `static char[] readPassword(String prompt, Object... args)` - `static String readLine(String prompt, Object... args)` 显示字符串 *prompt* 并读取用户输入,直到输入行结束。 ### 3.7.2 格式化输出 ```java double x = 10000.0 / 3.0; System.out.print(x); // print "3333.3333333333335" System.out.printf("%8.2f", x); // print " 3333.33" System.out.printf("Hello, %s. Next year you'll be %d.", name, age + 1); ``` **表 3-5 用于 `printf` 的转换符** | No. | 转换符 | 类型 | 举例 | | :---: | :---: | --- | --- | | 1 | `d` | 十进制整数 | 159 | | 2 | `x` | 十六进制整数 | 9f | | 3 | `o` | 八进制整数 | 237 | | 4 | `f` | 定点浮点数 | 15.9 | | 5 | `e` | 指数浮点数 | 1.59e+01 | | 6 | `g` | 通用浮点数 | - | | 7 | `a` | 十六进制浮点数 | 0x1.fccdp3 | | 8 | `s` | 字符串 | Hello | | 9 | `c` | 字符 | H | | 10 | `b` | 布尔 | True | | 11 | `h` | 散列码 | 42528b2 | | 12 | `tx` 或 `TX` | 日期时间(`T`强制大写) | 已经过时,应当改为使用 *java.time* 类。 | | 13 | `%` | 百分号 | *%* | | 14 | `n` | 与平台有关的行分隔符 | - | 示例代码: ```java System.out.printf("%d\n", 159); // print "159" System.out.printf("%x\n", 9); // print "9" System.out.printf("%o\n", 159); // print "237" System.out.printf("%f\n", 15.9); // print "15.900000" System.out.printf("%e\n", 15.9); // print "1.590000e+01" System.out.printf("%g\n", 15.9); // print "15.9000" System.out.printf("%a\n", 15.9); // print "0x1.fcccccccccccdp3" System.out.printf("%s\n", "Hello"); // print "Hello" System.out.printf("%c\n", 'H'); // print "H" System.out.printf("%b\n", true); // print "true" System.out.printf("%h\n", new java.util.Date()); // print "81be3b58" // System.out.printf("%tx\n", new java.util.Date()); // java.util.UnknownFormatConversionException: Conversion = 'tx' System.out.printf("%% << print a percent sign\n"); // print "% << print a percent sign" System.out.printf("%n^^ print this on a new line.\n"); // print "\n^^ print this on a new line." ``` `%tx` 格式打印报错了,估计是这个转换符已经废弃了导致的。 另外 `%h` 打印的散列码应该是对象实例本身的散列码(即 `Object.hashCode()` 方法的返回值),显示为 16 进制的。 **表 3-6 用于 `printf` 的标志** | No. | 标志 | 目的 | 举例 | | :---: | :---: | --- | --- | | 1 | `+` | 打印正数和负数的符号 | +3333.33 | | 2 | 空格 | 在正数之前添加空格 | " 3333.33" | | 3 | `0` | 数字签名补 0 | 003333.33 | | 4 | `-` | 左对齐 | "3333.33 " | | 5 | `(` | 将负号括在括号内 | (3333.33) | | 6 | `,` | 添加分组分隔符 | 3,333.33 | | 7 | `#`(对于`f`格式) | 包含小数点 | 3,333. | | 8 | `#`(对于`x`或`o`格式) | 添加前缀 `0x` 或 `0` | 0xcafe | | 9 | `$` | 给定被格式化的参数索引。<br/>例如,`%1$d`、`%1$x` 将以十进制和十六进制格式打印第一个参数。 | 159 9F | | 10 | `<` | 格式化前面说明的数值。<br/>例如,`%d%<x` 将以十进制和十六进制打印同一个数值。 | 159 9F | ```java double x = 10000.0 / 3.0; System.out.printf("|%+8.2f|\n", x); // print "|+3333.33|" System.out.printf("|% 8.2f|\n", x); // print "| 3333.33|" System.out.printf("|%08.2f|\n", x); // print "|03333.33|" System.out.printf("|%-8.2f|\n", x); // print "|3333.33 |" System.out.printf("|%(8.2f|\n", x); // print "| 3333.33|" System.out.printf("|%,8.2f|\n", x); // print "|3,333.33|" System.out.printf("|%8.2f|\n", x); // print "| 3333.33|" System.out.printf("|%#8.2f|\n", x); // print "| 3333.33|" System.out.printf("%x\n", 159); // print "9f" System.out.printf("%#x\n", 159); // print "0x9f" System.out.printf("%o\n", 159); // print "237" System.out.printf("%#o\n", 159); // print "0237" System.out.printf("%1$d %1$x\n", 159); // print "159 9f" System.out.printf("%d %<x\n", 159); // print "159 9f" ``` 上表中的 No.7 的效果不明,打印结果和上面的示例不一致。 **表 3-7 日期和时间的转换符** | No. | 转换符 | 类型 | 举例 | | :---: | :---: | --- | --- | | 1 | `c` | 完整的日期和时间 | 周四 8月 26 17:45:30 CST 2021 | | 2 | `F` | ISO 8601 日期 | 2021-08-26 | | 3 | `D` | 美国格式的日期(月/日/年) | 08/26/21 | | 4 | `T` | 24小时时间 | 17:45:30 | | 5 | `r` | 12小时时间 | 05:45:30 下午 | | 6 | `R` | 24小时时间没有秒 | 17:45 | | 7 | `Y` | 4位数字的年(前补0) | 2021 | | 8 | `y` | 年的后2位数字(前补0) | 21 | | 9 | `C` | 年的前2位数字(前补0) | 20 | | 10 | `B` | 月的完整拼写 | 八月 | | 11 | `b` 或 `h` | 月的缩写 | 8月 | | 12 | `m` | 2位数字的月(前补0) | 08 | | 13 | `d` | 2位数字的日(前补0) | 26 | | 14 | `e` | 2位数字的日(前不补0) | 26 | | 15 | `A` | 星期几的完整拼写 | 星期四 | | 16 | `a` | 星期几的缩写 | 周四 | | 17 | `j` | 3位数字的年中的日期(前补0)<br/>在001到366之间 | 238 | | 18 | `H` | 2位数字的小时(前补0)<br/>在0到23之间 | 17 | | 19 | `k` | 2位数字的小时(前不补0)<br/>在0到23之间 | 17 | | 20 | `I` | 2位数字的小时(前补0)<br/>在0到12之间 | 05 | | 21 | `l` | 2位数字的小时(前不补0)<br/>在0到12之间 | 5 | | 22 | `M` | 2位数字的分钟(前补0) | 45 | | 23 | `S` | 2位数字的秒(前补0) | 30 | | 24 | `L` | 3位数字的毫秒(前补0) | 300 | | 25 | `N` | 9位数字的毫微秒(前补0) | 300000000 | | 26 | `p` | 上午或下午的标志 | 下午 | | 27 | `z` | 从GMT起,RFC822数字位移 | +0800 | | 28 | `Z` | 时区 | CST | | 29 | `s` | 从格林威治时间1970-01-01 00:00:00起的秒数 | 1629971130 | | 30 | `Q` | 从格林威治时间1970-01-01 00:00:00起的毫秒数 | 1629971130300 | ```java java.util.Date now = new java.util.Date(); System.out.printf("%tc\n", now); // print "周四 8月 26 17:45:30 CST 2021" System.out.printf("%tF\n", now); // print "2021-08-26" System.out.printf("%tD\n", now); // print "08/26/21" System.out.printf("%tT\n", now); // print "17:45:30" System.out.printf("%tr\n", now); // print "05:45:30 下午" System.out.printf("%tR\n", now); // print "17:45" System.out.printf("%tY\n", now); // print "2021" System.out.printf("%ty\n", now); // print "21" System.out.printf("%tC\n", now); // print "20" System.out.printf("%tB\n", now); // print "八月" System.out.printf("%tb\n", now); // print "8月" System.out.printf("%tm\n", now); // print "08" System.out.printf("%td\n", now); // print "26" System.out.printf("%te\n", now); // print "26" System.out.printf("%tA\n", now); // print "星期四" System.out.printf("%ta\n", now); // print "周四" System.out.printf("%tj\n", now); // print "238" System.out.printf("%tH\n", now); // print "17" System.out.printf("%tk\n", now); // print "17" System.out.printf("%tI\n", now); // print "05" System.out.printf("%tl\n", now); // print "5" System.out.printf("%tM\n", now); // print "45" System.out.printf("%tS\n", now); // print "30" System.out.printf("%tL\n", now); // print "300" System.out.printf("%tN\n", now); // print "300000000" System.out.printf("%tp\n", now); // print "下午" System.out.printf("%tz\n", now); // print "+0800" System.out.printf("%tZ\n", now); // print "CST" System.out.printf("%ts\n", now); // print "1629971130" System.out.printf("%tQ\n", now); // print "1629971130300" ``` **格式说明符的语法图** `%{{参数索引值}$}{标志}{宽度}{.{精度}{转换字符}}{t{转换字符}}` ### 3.7.3 文件输入与输出 读取文件需要构建一个 `Scanner` 对象;写入则需要构建一个 `PrintWriter` 对象。 如果文件名中包含反斜杠符号(`\`),则需要使用转义符 `\\` 。 ```java package me.liujiajia.java.sample; import java.io.IOException; import java.io.PrintWriter; import java.nio.file.Paths; import java.util.Scanner; public class FileSample1 { public static void main(String[] args) throws IOException { PrintWriter out = new PrintWriter("c:\\mydirectory\\myfile.txt", "UTF-8"); out.println("Hello, JiaJia."); out.flush(); out.close(); Scanner in = new Scanner(Paths.get("c:\\mydirectory\\myfile.txt"), "UTF-8"); String str = in.nextLine(); System.out.println(str); in.close(); } } ``` > **注意** > > 读取/写入文件时建议不要省略编码格式。 > 不指定时默认使用操作系统的默认编码,这可能会导致在不同的机器上运行会得到不同的结果。 **java.util.Scanner 5.0** - `Scanner(File f)` - `Scanner(String data)` 构造一个从给定字符串读取数据的 *Scanner* **Java.io.PrintWriter 1.1** - `PrintWriter(String fileName)` **java.nio.file.Paths 7** - `static Path get(String pathname)` 根据给定的路径名构造一个 *Path* 。 ## 3.8 控制流程 没有 `goto` 语句,但是 `break` 语句可以带标签。 还有一种变形的 `for` 循环,类似于 *C#* 中的 `foreach` 循环。 ### 3.8.1 块作用域 块(*block*)(即复合语句)是指由一对大括号括起来的若干条简单的 Java 语句。 块确定了变量的作用域。 一个块可以嵌套在另一个块中。 ```java public static void main(String[] args) { int n; { int k; } // k is only defined up to here } ``` 不能在嵌套的两个块中声明同名的变量。 ```java public static void main(String[] args) { int n; { int k; int n; // Error -- can't redefine n in inner block } } ``` ### 3.8.2 条件语句 ```java if (condition) statement if (condition) { statement1 statement2 } ``` `else` 部分是可选的。 ```java if (condition) statement1 else statement2 if (condition) { statement1 statement2 } else { statement3 statement4 } ``` 重复的出现 `if ... else if ...` : ```java if (condition1) statement1 else if (condition2) statement2 else statement3 if (condition1) { statement1 statement2 } else if (condition2) { statement3 statement4 } else { statement5 statement6 } ``` **示例:** ```java if (yourSales >= target) { performance = "Satisfactory"; bonus = 100; } else { performance = "Unsatisfactory"; bonus = 0; } ``` ### 3.8.3 循环 ```java while (condition) statement while (condition) { statement1 statement2 } ``` 如果开始循环条件为 `false` ,则 `while` 循环体一次也不执行。 如果循环体至少执行一次,则应将检测条件放在最后。 ```java do statement while (condition) do { statement1 statement2 } while (condition) ``` ### 3.8.4 确定循环 `for` 循环语句是支持迭代的一种通用结构,利用每次迭代之后更新的计数器或类似的变量来控制迭代次数。 ```java for (int i = 1; i<= 10; i++) System.out.println(i); ``` `for` 语句的第一部分通常用于对计数器初始化;第二部分给出每次新一轮循环执行前要检测的循环条件;第三部分指示如何更新计数器。 尽管 Java 允许在 `for` 循环的各个部分放置任何表达式,但有一天不成文的规则:**`for` 语句的 3 个部分应该对同一个计数器变量进行初始化、检测和更新**。 书上说这段倒计数的代码有问题,没看出来哪里有问题。 ```java for (int i = 10; i > 0; i--) System.out.println("Counting down ... " + i); System.out.println("Blastoff!"); ``` 在循环中,检测两个浮点数是否相等需要格外小心。 ```java for (double x = 0; x != 10; x += 0.1) System.out.println(x); ``` 上面的代码可能永远不会结束。因为 *0.1* 无法用二进制精确的表示。实际运行时从 *9.99999999999998* 直接跳跃到了 *10.09999999999998* 。 在 `for` 语句第一部分声明的变量的作用域就是整个循环体,在循环体外无法访问。 `for` 循环语句不过是 `while` 循环的一种简化形式。 ```java for (int i = 10; i > 0; i--) System.out.println("Counting down ... " + i); ``` 可以重写为: ```java while (i > 0) { System.out.println("Counting down ... " + i); i--; } ``` ### 3.8.5 多重选择:switch 语句 ```java switch (choice) { case 1: break; case 2: break; default: break; } ``` `switch` 语句将从与选项值相匹配的 `case` 标签处开始执行直到遇到 `break` 语句,或者执行到 `switch` 语句的结束处为止。 如果没有相匹配的 `case` 标签,而有 `default` 子句,就执行这个子句。 `case` 标签可以是: - 类型为 `char`、`byte`、`short` 或 `int` 的常量表达式 - 枚举常量 - 从 *Java SE 7* 开始,`case` 标签还可以是字符串字面量。 这里比对时使用的是 `equals` 方法。 ### 3.8.6 中断控制流程语句 `break` 语句可以用于退出循环语句。 ```java while (years <= 100) { balance += payment; double interest = balance * interestRate / 100; balance += interest; if (balance >= goal) break; years++; } ``` 带标签的 `break` 用于跳出多重嵌套的循环语句。 标签必须放在希望跳出的最外层循环之前,并且必须紧跟一个冒号。 跳出到带标签的语句块末尾。 ```java read_data: while (...) { for(...) { if(...) { break read_data; } } } // 跳出到这里 ``` 可以将标签应用到任何语句中,甚至可以应用到 `if` 语句或者块语句中。 只能跳出语句块,不能跳入语句块。 ```java label: { if (conditon) break label; } ``` `continue` 语句和 `break` 语句一样,将中断正常的控制流程。 `continue` 语句将控制转移到最内层循环的首部。 ```java while (...) { if(...) { continue; } } ``` ## 3.9 大数值 如果基本的整数和浮点数精度不能够满足需求,那么可以使用 `java.math` 包中的 `BigInteger` 和 `BigDecimal` 。这两个类可以处理包含任意长度数字序列的数值。`BigInteger` 实现了任意精度的整数运算,`BigDecimal` 实现了任意精度的浮点数运算。 使用静态的 `valueOf` 方法可以将普通的数值转换为大数值。 ```java BigInteger a = BigInteger.valueOf(100); ``` 不能使用算术运算符处理大数值,需要使用对应的 `add`、`multiply` 等方法。 ```java BigInteger a = BigInteger.valueOf(100); BigInteger b = BigInteger.valueOf(118); BigInteger c= a.add(b); // c = a + b BigInteger d= c.multiply(b.subtract(BigInteger.valueOf(-3))); // d = c * (b - 3) ``` **java.math.BigInteger 1.1** - `BigInteger add(BigInteger other)` - `BigInteger subtract(BigInteger other)` - `BigInteger multiply(BigInteger other)` - `BigInteger divide(BigInteger other)` - `BigInteger mod(BigInteger other)` - `int compareTo(BigInteger other)` - `static BigInteger valueOf(long x)` **java.math.BigDecimal 1.1** - `BigDecimal add(BigDecimal other)` - `BigDecimal subtract(BigDecimal other)` - `BigDecimal multiply(BigDecimal other)` - `BigDecimal divide(BigDecimal other, RoundingMode mode)` - `int compare(BigDecimal other)` - `static BigDecimal valueOf(long x)` - `static BigDecimal valueOf(long x, int scale)` 返回值为 `x/10^scale` 的一个大实数。 ## 3.10 数组 数组是一种数据结构,用来存储同一类型值的集合。 通过一个整型下标可以访问数组中的每一个值。 在声明数组变量时,需要指出数组类型和数组变量的名字。 ```java int[] a; int a[]; ``` 初始化数组: ```java int[] a = new int[100]; ``` 数组长度不要求是常量。 数组的下标从 0 开始。 ```java int[] a = new int[100]; for (int i = 0; i < 100; i++) { a[i] = i; } ``` 下标越界时会引发 *array index out of bounds* 异常。 一旦创建了数组,就不能再改变它的大小。如果经常需要再运行过程中扩展数组的大小,就应该使用另一种数据结构 **数组列表(*array list*)** 。 ### 3.10.1 for each 循环 用来依次处理数组中的每个元素。 ```java for (variable : collection) statement ``` *collection* 集合表达式必须是一个数组或者是一个实现了 `Iterable` 接口的类对象。 for each 语句更加简洁、更不易出错(不比为下标的起始值和终止值而操心)。 ### 3.10.2 数组初始化以及匿名数组 Java 提供了一种创建数组对象并同时赋予初始化的简化书写形式。 数组的大小就是初始值的个数。 ```java int[] smallPrimes = {2, 3, 5, 7, 11, 13}; ``` 初始化一个匿名数组: ```java new int[] { 17, 19, 23, 29, 31, 37}; ``` 数组的长度可以为 0 。 ### 3.10.3 数组拷贝 常用于增加数组的大小: ```java luckyNumbers = Arrays.copyOf(luckNumbers, 2 * luckNumbers.length); ``` 如果长度小于原始数组的长度,则只拷贝最前面的数据元素。 ### 3.10.4 命令行参数 每一个 Java 应用程序都有一个带 `String arg[]` 参数的 *main* 方法。这个参数表明 *main* 方法将接收一个字符串数组,也就是命令行参数。 ```java package me.liujiajia.java.sample; public class CmdLineSample1 { public static void main(String[] args) { if (args.length == 0 || args[0].equals("-h")) System.out.print("Hello,"); else if (args[0].equals("-g")) System.out.print("Goodbay,"); for (int i = 1; i < args.length; i++) System.out.print(" " + args[i]); System.out.println("!"); } } ``` ```bash javac me/liujiajia/java/sample/CmdLineSample1.java java me/liujiajia/java/sample/CmdLineSample1 -h Liu JiaJia java me/liujiajia/java/sample/CmdLineSample1 -g Liu JiaJia ``` ### 3.10.5 数组排序 可以使用 `Arrays` 类中的 `sort` 方法对数值型数组进行排序。 ```java int[] a = new int[10000] ... Arrasy.sort(a); ``` 这个方法使用了优化的快速排序算法。快速排序算法对于大多数数据集合来说都是效率比较高的。 **java.util.Arrays 1.2** - `static String toString(type[] a)` 5.0 - `static type copyOf(type[] a, int length)` 6 - `static type copyOfRange(type[] a, int start, int end)` 6 - `static void sort(type[] a)` - `static int binarySearch(type[] a, type v)` - `static int binarySearch(type[] a, int start, int end, type v)` 6 采用二分搜索法查找值 *v*。如果查找成功,则返回响应的下标值;否则返回一个负数值 *r*。`-r-1` 是为了保持 *a* 有序 *v* 应插入的位置。 - `static void fill(type[] a, type v)` 将数组的所有数据元素值设置为 *v*。 - `static boolean equals(type[] a, type[] b)` 如果两个数组大小相同,并且下标相同的元素都对应相等,返回 `true`。 ### 3.10.6 多维数组 多维数组将使用多个下标访问数组元素,它适用于表示表格或更加复杂的排列形式。 ```java double[][] balances; ``` 与一维数组一样,在调用 *new* 对多维数组进行初始化之前不能使用它。 ```java balances = new double[NYEARS][NRATES]; ``` 简化形式: ```java int[][] magicSquare = { {16, 3, 2, 13}, {5, 10, 11, 8}, {9, 6, 7, 12}, {4, 15, 14, 1} }; ``` 访问多维数组:`balances[i][j]` 使用 *for each* 循环处理二维数组: ```java for (int[] row : magicSquare) { for (int value : row) { System.out.println(value); } } ``` 快速打印二维数组的元素列表: ```java System.out.println(Arrays.deepToString(magicSquare)); ``` ### 3.10.7 不规则数组 Java 实际上没有多维数组,只有一维数组。多维数组被解释为 **数组的数组** 。 ```java int[][] odds = new int[NMAX + 1][]; for (int n = 0; n <= NMAX; n++) odds[n] = new int[n + 1]; for (int n = 0; n < odds.length; n++) { for (int k = 0; k < odds[n].length; k++) { // computer lotteryOdds int lotteryOdds = 1; for (int i = 1; i <= k; i++) lotteryOdds = lotteryOdds * (n - i + 1) / i; odds[n][k] = lotteryOdds; } } for (int[] row : odds) { for (int odd : row) System.out.printf("%4d", odd); System.out.println(); } ``` 打印结果: ```bash 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 1 5 10 10 5 1 1 6 15 20 15 6 1 ``` [1]:/2021/5/5/java-puzzlers-001-is-odd (Java解惑-01:奇数性) [2]:https://docs.oracle.com/javase/8/docs/api/ (Java™ Platform, Standard Edition 8 API Specification) Loading... 版权声明:本文为博主「佳佳」的原创文章,遵循 CC 4.0 BY-NC-SA 版权协议,转载请附上原文出处链接及本声明。 原文链接:https://www.liujiajia.me/2021/8/27/core-java-volume-1-fundametals-ch-03 提交