《Java 核心技术卷1 基础知识》第三章 Java 的基本程序设计结构 笔记(上):https://developer.aliyun.com/article/1391462
3.6 字符串
从概念上讲,Java 字符串就是 Unicode 字符序列
Java 没有内置的字符串类型,而是在标准 Java 类库中提供了一个预定义类,很自然地叫做 String
3.6.1 子串
String 类的 substring 方法可以从一个较大的字符串提取出一个子串
String greeting = "Hello";
String s = greeting.substring(0, 3);
在 substring 中从 0 开始计数,直到 3 为止,但不包含 3
3.6.2 拼接
Java 语言允许使用 + 号连接(拼接)两个字符串
当一个字符串与一个非字符串的值进行拼接时,后者会转换成字符串
任何一个 Java 对象都可以转换成字符串
把多个字符串放在一起,用一个界定符分隔,可以使用静态 join 方法
String all = String.join(" / ", "s", "m", "l", "xl");
在 Java 11 中,还提供了一个 repeat 方法
String repeated = "Java".repeat(3);
3.6.3 不可变字符串
String 类没有提供修改字符串中某个字符的方法
如何修改这个字符串呢?可以提取想要保留的子串,再与希望替换的字符拼接
由于不能修改 Java 字符串中的单个字符,所以在 Java 文档中将 String 类对象称为不可变的
不过,可以修改字符串变量,让它引用另外一个字符串
好像修改一个代码单元要比从头创建一个新字符串更加简洁
通过拼接来创建一个新字符串的效率确实不高
不可变字符串却有一个优点:编译器可以让字符串共享
可以想象将各种字符串放在公共的存储池中。字符串变量指向存储池中相应的位置。如果复制一个字符串变量,原始字符串与复制的字符串共享相同的字符。
Java 的设计者认为共享带来的高效率远远胜过于提取子串、拼接字符串所带来的低效率。可以看看你自己的程序,我们发现:大多数情况下都不会修改字符串,而只是需要对字符串进行比较
Java 字符串大致类似于 char* 指针:
char* greeting = "Hello";
当把 greeting 替换为另一个字符串的时候,Java 代码大致进行下列操作:
char* temp = malloc(6);
strncpy(temp, greeting, 3);
strncpy(temp + 3, "p!", 3);
greeting = temp;
3.6.4 检测字符串是否相等
使用 equals 方法检测两个字符串是否相等
检测两个字符串是否相等,而不区分大小写,可以使用 equalsIgnoreCase 方法
一定不要使用 == 运算符检测两个字符串是否相等!这个运算符只能够确定两个字符串是否存放在同一个位置上。当然,如果字符串在同一个位置上,它们必然相等。但是,完全有可能将内容相同的多个字符串副本放置在不同的位置上
如果虚拟机始终将相同的字符串共享,就可以使用 == 运算符检测是否相等。但实际上只有字符串字面量是共享的,而 + 和 substring 等操作得到的字符串并不共享。因此,千万不要使用 == 运算符测试字符串的相等性,以免在程序中出现这种最糟糕的 bug,看起来这种 bug 就像随机产生的间歇性错误
3.6.5 空串与 Null 串
空串 “” 是长度为 0 的字符串,检查一个字符串是否为空
if (str.length() == 0)
if (str.equals(""))
空串是一个 Java 对象,有自己的串长度(0)和内容(空)
String 变量还可以存放一个特殊的值,名为 null,表示目前没有任何对象与该变量关联
要检查一个字符串是否为 null,要使用以下条件:
if (str == null)
有时要检查一个字符串既不是 null 也不是空串
if (str != null && str.length() != 0)
首先要检查 str 不为 null
如果在一个 null 值傻姑娘调用方法,会出现错误
3.6.6 码点与代码单元
Java 字符串是由 char 值序列组成
char 数据类型是一个采用 UTF-16 编码表示 Unicode 码点的代码单元
最常用的 Unicode 字符使用一个代码单元就可以表示,而辅助字符需要一对代码单元表示
length 方法将返回采用 UTF-16 编码表示给定字符串所需要的代码单元数量
String greeting = "Hello";
int n = greeting.length(); // is 5
得到实际的数量,即码点数量
int cpCount = greeting.codePointCount(0, greeting.length()); // is 5
调用 s.charAt(n) 将返回位置 n 的代码单元, n 介于 0 ~ s.length() - 1 之间
char first = greeting.charAt(0); // first is 'H'
char last = greeting.charAt(4); // last is 'o'
遍历一个字符串,并且依次查看每一个码点,更容易的办法是使用 codePoints 方法,它会生成一个 int 值的“流”,每个 int 值对应一个码点
int[] codePoints = str.codePoints().toArray();
要把一个码点数组转换为一个字符串,可以使用构造器
String str = new String(codePoints, 0, codePoints.length);
3.6.7 String API
Java 中的 String 类包含了 50 多个方法。令人惊讶的是它们绝大多数都很有用,可以想见使用的频率非常高
java.lang.String
在 API 注释中,有一些 CharSequence 类型的参数。这是一种接口类型,所有字符串都属于这个接口。
当看到一个 CharSequence 形参时,完全可以传入 String 类型的实参
3.6.8 阅读联机 API 文档
可以在浏览器中访问 http://docs.oracle.com/javase/9/docs/api
3.6.9 构造字符串
有些时候,需要由较短的字符串构建字符串。如果采用字符串拼接的方式来达到这个目的,效率会比较低。每次拼接字符串时,都会构建一个新的 String 对象,既耗时,又浪费空间。使用 StringBuilder 类就可以避免这个问题的发生。
如果需要用许多小段的字符串构建一个字符串,那么应该按照下列步骤进行。
首先构建一个空的字符串构建器:
StringBuilder builder = new StringBuilder();
当每次需要添加一部分内容时,就调用 append 方法。
builder.append(ch);
builder.append(str);
在字符串构建完成时就调用 toString 方法,将可以得到一个 String 对象,其中包含了构建器中的字符序列。
String copletedString = builder.toString();
StringBuilder 类在 Java 5 中引入。这个类的前身是 StringBuffer,它的效率稍有些低,但允许采用多线程的方式添加或删除字符。如果所有字符串编辑操作都在单个线程中执行,则应该使用 StringBuilder。
3.7 输入与输出
3.7.1 读取输入
要想通过控制台进行输入,首先需要构造一个与“标准输入流” System.in 关联的 Scanner 对象
Scanner in = new Scanner(System.in);
使用 Scanner 类的各种方法读取输入
nextLine 方法将读取一行输入
String name = in.nextLine();
使用 nextLine 是因为在输入航中有可能包含空格。要想读取一个单词,可以调用
String firstName = in.next();
要想读取一个整数,就调用 nextInt 方法
int age = in.nextInt();
要想读取一个浮点数,就调用 nextDouble 方法
Scanner 类定义在 java.util 包中。当使用的类不是定义在基本 java.lang 包中时,一定要使用 import 指令倒入相应的包
程序清单 3-2
package chapter3.InputTest; import java.io.Console; import java.util.Scanner; public class InputTest { public static void main(String[] args) { Scanner in = new Scanner(System.in); System.out.println("What is your name?"); String name = in.nextLine(); System.out.println("How old are you?"); int age = in.nextInt(); System.out.println("Hell, " + name + ". Next year, you'll be " + (age + 1)); } }
因为输入是可见的,所以 Scanner 类不适用于从控制台读取密码。Java 6 特别引入了 Console 类来实现这个目的。要想读取一个密码,可以使用下列代码:
Console cons = System.console(); String username = cons.readLine("User name:"); char[] passwd = cons.readPassword("Password:");
采用 Console 对象处理输入不如采用 Scanner 方便。必须每次读取一行输入,而没有能够读取单个单词或数值的方法
java.util.Scanner
boolean hasNext()
boolean hasNextInt()
boolean hasNextDouble()
3.7.2 格式化输出
Java 5 沿用了 C 语言函数库中的 printf 方法
System.out.printf("%8.2f", x);
会以一个字段宽度打印 x:这包括 8 个字符,另外精度为小数点后 2 个字符
可以为 printf 提供多个参数
System.out.printf("Hello, %s. Next year, you'll be %d", name, age)
每一个以 % 字符开始的格式说明符都用相应都参数替换。格式说明符尾部都转换符指示要格式化的数值的类型:f 表示浮点数,s 表示字符串,d 表示十进制整数。
可以使用静态的 String.format 方法创建一个格式化的字符串
String message = String.format("Hello, %s. Next year, you'll be %d", name, age);
3.7.3 文件输入与输出
要想读取一个文件,需要构造一个 Scanner 对象
Scanner in = new Scanner(Path.of("myfile.txt"), StandardCharsets.UTF_8);
读取一个文件时,要知道它的字符编码
要想写入文件,就需要构造一个 PrintWriter 对象。在构造器中,需要提供文件名和字符编码
PrintWriter out = new PrintWriter("myfile.txt", StandardCharset.UTF_8);
如果文件不存在,创建该文件。可以像输出到 System.out 一样使用 print、println 以及 printf 命令
当指定一个相对文件时,文件位于相对于 Java 虚拟机启动目录的位置
java MyProg
启动目录就是命令解释器的当前目录
使用集成开发环境,那么启动目录将由 IDE 控制。可以使用下面的调用找到这个目录的位置:
String dir = System.getProperty("user.dir");
如果觉得定位文件太麻烦,可以考虑使用绝对路径名
如果用一个不存在的文件构造一个 Scanner,或者用一个无法创建的文件名构造一个 PrintWriter,就会产生异常
java.nio.file.Path
static Path of(String pathname) 根据给定的路径名构造一个 Path
3.8 控制流程
Java 中没有 goto 语句,但 break 语句可以带标签,可以利用它从内层循环跳出
3.8.1 块作用域
块(block)
块(即复合语句)是指由若干条 Java 语句组成的语句,并用一对大括号括起来。块确定了变量的作用域。一个块可以嵌套在另一个块中。
不能在嵌套的块中声明同名的变量
3.8.2 条件语句
在 Java 中,条件语句的形式为
if (condition) statement
条件必须用小括号括起来
块语句(block statement)
更一般的条件语句如下所示
if (condition) statement1 else statement2
else 部分总是可选的。else 子句与最邻近的 if 构成一组
3.8.3 循环
当条件为 true 时,while 循环执行一条语句(也可以是一个块语句)。一般形式如下:
while (condition) statement
如果开始时循环条件的值就为 false,那么 while 循环一次也不执行
如果希望循环体至少执行一次,需要使用 do/while 循环将检测放在最后。它的语法如下:
do statement while (condition)
这种循环语句先执行语句(通常是一个语句块),然欧再检测循环条件。如果为 true,就重复执行语句,然后再次检测循环条件,以此类推。
3.8.4 确定循环
for 循环语句是支持迭代的一种通用结构,由一个计数器或类似的变量控制地带次数,每次迭代后这个变量将会更新
for 语句的第 1 部分通常是对计数器初始化;第 2 部分给出每次新一轮循环执行前要检测的循环条件;第 3 部分指定如何更新计数器
尽管 Java 允许在 for 循环的各个部分放置任何表达式,但有一条不成文但规则:for 语句的 3 个部分应该对同一个计数器变量进行初始化、检测和更新。若不遵守这一规则,编写的循环常常晦涩难懂。
当在 for 语句的第 1 部分中声明一个变量之后,这个变量的作用域就扩展到这个 for 循环体的末尾
如果在 for 语句内部定义一个变量,这个变量就不能在循环体之外使用。因此,如果希望在 for 循环体之外使用循环计数器的最终值,就要确保这个变量在循环之外声明。
可以在不同的 for 循环中定义同名的变量
for 循环语句只不过是 while 循环的一种简化形式
3.8.5 多重选择:switch 语句
Java 有一个与 C/C++ 完全一样的 switch 语句
switch 语句将从与选项值相匹配的 case 标签开始执行,直到遇到 break 语句,或者执行到 switch 语句的结束处为止。如果没有相匹配的 case 标签,而有 default 子句,就执行这个子句。
有可能触发多个 case 分支。如果在 case 分支语句的末尾没有 break 语句,那么就会接着执行下一个 case 分支语句。
编译代码时可以考虑加上 +Xlint:fallthrough 选项,如下:
javac -Xlint:fallthrough Test.java
这样一来,如果某个分支最后缺少一个 break 语句,编译器就会给出一个警告消息
如果你确实正是想使用这种“直通式”(fallthrough)行为,可以为其外围方法加一个注解 @SuppressWarnings("fallthrough")。这样就不会对这个方法生成警告了。
注解是为编译器或处理 Java 源文件或类文件的工具提供信息的一种机制
case 标签可以是:
类型为 char、byte、short 或 int 的常量表达式
枚举常量
从 Java 7 开始,case 标签还可以是字符串字面量
当在 switch 语句中使用枚举常量时,不必在每个标签中指明枚举名,可以由 switch 的表达式值推倒得出。
Size sz = ...; switch (sz) { case SMALL: // no need to use Size.SMALL ... break; ... }
3.8.6 中断控制流程的语句
Java 的设计者将 goto 作为保留字
通常,使用 goto 语句被认为是一种拙劣的程序设计风格
偶尔使用 goto 跳出循环还是有益处的。Java 设计者同意这种看法,甚至在 Java 语言中增加了一条新的语句:带标签的 break,以此来支持这种程序设计风格。
Java 还提供了一种带标签的 break 语句,用于跳出多重嵌套的循环语句
在嵌套很深的循环语句中会发生一些不可预料的事情。此时可能更加希望完全跳出所有嵌套循环之外
标签必须放在希望跳出的最外层循环之前,并且必须紧跟一个冒号
执行带标签的 break 会跳转到带标签的语句块末尾
continue 语句将控制转移到最内层循环的首部
如果将 continue 语句用于 for 循环中,就可以跳到 for 循环的“更新”部分
还有一种带标签的 continue 语句,将跳到与标签匹配的循环的首部
3.9 大数
java.math 包中两个很有用的类:BigInteger 和 BigDecimal。这两个类可以处理包含任意长度数字序列的数值。BigInteger 类实现任意精度的整数运算,BigDecimal 实现任意精度的浮点数运算。
使用静态的 valueOf 方法可以将普通的数值转换为大数:
BigInteger a = BigInteger.valueOf(100);
对于更大的数,可以使用一个带字符串参数的构造器:
BigInteger reallyBig = new BigInteger("1234567890987654321");
另外还有一些常量:BigInteger.ZERO、BigInteger.ONE 和 BigInteger.TEN,Java 9 之后还增加了 BigInteger.TWO
不能使用人们熟悉的算术运算符处理大数,而需要使用大数类中的方法
BigInteger c = a.add(b); // c = a + b
BigInteger d = c.multiply(b.add(BigInteger.valueOf(2))); // d = c * (b + 2)
Java 没有提供运算符重载功能
3.10 数组
数组存储相同类型的序列
3.10.1 声明数组
数组是一种数据结构,用来存储同一类型值的集合。通过一个整型下表可以访问数组中的每一个值。
在声明数组变量时,需要指出数组类型和数组变量的名字。
int[] a;
这条语句只声明类变量 a,并没有将 a 初始化为一个真正的数组。应该使用 new 操作符创建数组。
int[] a = new int[100];
这条语句声明并初始化类一个可以存储 100 个整数的数组。
数组长度不要求是常量:new int[n] 会创建一个长度为 n 的数组
一旦创建了数组,就不能再改变它的长度
如果程序运行中需要经常扩展数组大的大小,就应该使用另一种数据结构 —— 数组列表(array list)
一种创建数组对象并同时提供初始化值的简写形式
int[] smallPrimes = {2, 3, 4, 5, 11, 13};
这个语法不需要使用 new,甚至不用指定长度
声明一个匿名数组:
new int[] {17, 19, 23, 29, 31, 37};
这会分配一个新数组并填入大括号中提供的值。它会统计初始化个数,并相应地设置数组大小。可以使用这种语法重新初始化一个数组而无须创建新变量。
int[] anonymous = {17, 19, 23, 29, 31, 37};
smallPrimes = anonymous;
在 Java 中,允许有长度为 0 的数组
3.10.2 访问数组元素
一旦创建了数组,就可以在数组中填入元素
创建一个数字数组时,所有元素都初始化为 0。boolean 数组的元素会初始化为 false。对象数组都元素则初始化为一个特殊值 null,表示这些元素未存放任何对象。
String[] names = new String[10];
会创建一个包含 10 个字符串的数组,所有字符串都为 null。如果希望这个数组包含空串,必须为元素指定空串:
for (int i = 0; i < 10; i ++) names[i] = "";
如果创建了一个 100 个元素的数组,并且试图访问元素 a[101],就会引发“array index out of bounds”异常
要想获得数组中的元素个数,可以使用 array.length
3.10.3 for each 循环
增强的 for 循环的语句格式为:
for (variable : collection) statement
它定义一个变量用于暂存集合中的每一个元素,并执行相应的语句。collection 这一集合表达式必须是一个数组或者一个实现了 Iterable 接口的类对象。
for each 循环语句显得更加简洁、更不易出错,因为你不必为下标的起始值和终值而操心
在很多情况下还是需要使用传统的 for 循环。例如,如果不希望遍历整个集合,或者在循环内部需要使用下标值。
调用 Arrays.toString(a),返回一个包含数组元素的字符串,这些元素包围在中括号内,并用逗号分隔。
System.out.println(Arrays.toString(a));
3.10.4 数组拷贝
在 Java 中,允许将一个数组变量拷贝到另一个数组变量。这时,两个变量将引用同一个数组
int[] luckyNumbers = smallPrimes;
luckyNumber2[5] = 12; // now smallPrimes[5] is also 12
如果希望将一个数组到所有值拷贝到一个新的数组中去,就要使用 Arrays 类的 copyOf 方法:
int[] coiedLucyNumbers = Arrays.copyOf(luckyNumbers, luckyNumbers.length);
第 2 个参数是新数组的长度。这个方法通常用来增加数组的大小:
luckyNumbers = Arrays.copyOf(luckyNumbers, 2 * luckyNumbers.length);
如果长度小于原始数组的长度,则只拷贝前面的值
C++ 注释:基本上与在堆(heap)上分配的数组指针一样。也就是说,
int[] a = new int[100];
不用于
int a[100];
而等同于
int *a = new int[100];
Java 中的 [] 运算符被预定义为会完成越界检查,而没有指针运算,即不能通过 a 加 1 得到数组中的下一个元素
3.10.5 命令行参数
每一个 Java 应用程序都有一个带 String args[] 参数的 main 方法。这个参数表明 main 方法将接收一个字符串数组,也就是命令行上指定的参数。
C++ 注释:程序名并没有存储在 args 数组中
3.10.6 数组排序
要想对数值型数组进行排序,可以使用 Arrays 类中的 sort 方法
int[] a = new int[100];
...
Arrays.sort(a);
这个方法使用了优化的快速排序算法。快速排序算法对于大多数数据集合来说都是效率比较高的。
Math.random 方法将返回一个 0 到 1 之前(包含 0、不包含 1)的随机浮点数。用 n 乘以这个浮点数,就可以得到从 0 到 n - 1 之间的一个随机数
sort
copyOf
copyOfRange
binarySearch
fill
equals
3.10.7 多维数组
多维数组将使用多个下标访问数组元素,它适用于表示表格或更加复杂的排列形式
声明一个二维数组相当简单
double[][] balance;
对数组进行初始化之前是不能使用的。在这里可以如下初始化:
balance = new double[NYEAR][NRATE];
如果直到数组元素,就可以不调用 new ,而直接使用简写形式对多维数组进行初始化。
int[][] magicSquare = { {1, 2, 3, 4}, {2, 3, 4, 5}, {3, 4, 5, 6} };
一旦数组初始化,就可以利用两个中括号访问各个元素
for each 循环语句不能自动处理二维数组对每一个元素。它会循环处理行,而这些行本身就是一维数组。要想访问二维数组 a 的所有元素,需要使用两个嵌套的循环,循环如下:
for (double[] row: a) for (double value : row) do someting with value
要想快速地打印一个二维数组的数据元素列表,可以调用
System.out.println(Arrays.deepToString(a));
3.10.8 不规则数组
Java 实际上没有多维数组,只有一维数组。多维数组被解释为“数组的数组”
由于可以单独地访问数组的某一行,所以可以让两行交换
double[] temp = balances[i]; balance[i] = balances[i + 1]; balance[i + 1] = temp;
还可以方便地构造一个“不规则”数组,即数组的每一行有不同的长度
int[][] odds = new int[NMAX][]; for (int n = 0; n odds[n] = new int[n + 1];