zl程序教程

您现在的位置是:首页 >  其它

当前栏目

CoreJava第三章要点速记

要点 第三章 速记
2023-06-13 09:14:14 时间

  由于时间原因,不能像讲课一样给大家一一列出所有的要点,故在此篇博客中,仅记录一些个人之前忽略的点和常见易错点,将不展开全面介绍,各位读者可以当做闲暇阅读,查漏补缺。

第三章 要点、易错点速记

第三章 Java的基本程序设计结构

3.4 变量

  1. Java变量名是字母、’_‘或’'开头的字母数字串,没有长度限制,大小写敏感。常用于编译器或其他工具生成的名字中,通常不使用。
  2. 变量名最好不要只存在大小写上的差异。
  3. 访问权限修饰符、static修饰符、final修饰符位置都可以随意交换,但通常为public static final DataType varName;

3.4.1 变量初始化

  1. Java中声明变量后必须进行确定的显式初始化,否则无法通过编译。
  2. Java可以将变量声明放在代码中任意位置,但变量的声明应尽可能靠近第一次使用的地方,这是一种良好的程序编写风格。
  3. Java不区分变量的声明和定义。所有的Java变量声明都会开辟空间,而C/C++只有定义时会开辟空间,声明不会。

3.4.2 常量

  1. 关键字final表示该变量只能赋值一次,一旦赋值便不可修改。习惯上,常量名使用全大写+下划线
  2. Java常常希望某个常量在一个类的多个方法使用,称之为类常量。
  3. const是Java保留的关键字,但目前并没有使用。

3.5 运算符

  1. 整数除零会产生java.lang.ArithmeticException: / by zero异常;浮点除零会产生无穷大或NaN结果。
  2. Java为了实现可移植性,需要定义统一的运算规范。但问题是,例如double计算,很多Intel处理器会将中间计算结果保存在80位的寄存器中,这无疑会导致精度的增加。Java中默认允许对中间结果采用扩展的精度,如果不允许,要使用strictfp关键字标记该方法。

3.5.1 数学函数与常量

  常用的数学函数都包含在Math类中,可以直接调用。

  1. 例如:Math.sqrt(x)求平方根;Math.pow(x,a)求xa;Math.floorMod(x, y)非负余数等。
  2. Math类也提供了常用的三角函数:sin/cos/tan;以及指数函数exp/log/log10等;还提供了π和e的近似值:Math.PIMath.E

注意:

  • Math类为了达到最快性能,所有方法都使用本地计算机浮点单元中的例程(机器有关)。如果要在所有平台上得到相同结果,要使用StrictMath类。

3.5.2 数值类型之间的转换

  当有类型转换不会造成数据溢出(Java允许丢失精度)时,或使用结合赋值运算符时,Java会根据计算需要自动进行类型转换。

  1. int -> float、long -> float、long -> double 可能丢失精度
  2. 二元操作需要将两个操作数转为同一类型。类型转换规则:
    1. 如果有一个数是double,则另一个转double。
    2. 如果有一个数是float,则另一个转float。
    3. 如果有一个数是long,则另一个转long。
    4. 如果有一个数是int,则另一个转int。

3.5.3 强制类型转换

  当类型转换可能造成数据溢出从而损坏数据时(结合赋值运算符除外),Java不会自动进行类型转换。这时,就需要用小括号进行强制类型转换,强制类型转换会直接截断,不会进行舍入运算,而且如果超过目标类型的表示范围,就可能产生一个完全不同的值。   可以调用Math.round(x)产生一个四舍五入的long类型的值。

注意:

  • 不要在boolean类型与任何其他类型之间进行强制类型转换!如有需要,可以使用表达式b? 1 : 0

3.5.4 结合赋值和运算符

注意:

  • 如果运算符得到的值与左侧操作数类型不同,会发生强制类型转换。例如x是一个int,而后调用x += 3.5;是合法的,这等价于x = (int) (x + 3.5);

3.5.5 自增与自减运算符

注意:

  • 自增自减运算符的操作数是变量不能是常量
  • 建议不要在表达式中使用++,这样的代码阅读起来比较困难,且易错。

3.5.6 关系和boolean运算符

  1. !、==、!=、>、<、=、>=、<=
  2. 常常利用&&和||运算符的短路特点来避免出错。例如if(obj != null && obj.xxx);
  3. Java支持三元操作符?:

3.5.7 位运算符

  处理整型类型时,可以直接进行位操作。位运算符包括:

  1. &,与
  2. |,或
  3. ^,异或
  4. ~,按位取反
  5. <<,算数(逻辑)左移(低位填零)
  6. >>,算数右移(高位填充符号位)
  7. >>>,算数右移(高位填充0)

  利用掩码技术可以得到整数中的各个位。例如:int forthBitFromRight = (n & 0b1000) / 0b1000int forthBitFromRight = (n & (1<<3)) >> 3。利用&并结合使用2的适当的幂,可以将其他位“掩盖掉”,从而只保留某一位。

注意:

  • &|应用在boolean上时,也会得到一个boolean值,但是不会采用短路的方式,左右的表达式都需要计算。
  • >>>会用0填充高位,>>用符号位填充高位。如果做除以2的操作,应该使用算数右移。
  • 在C/C++中,>>是进行逻辑右移(通常强转为unsigned类型然后右移从而保证是高位填0)还是算数右移(通常是算数右移)依赖具体实现,而Java则消除了这种不确定性。

3.5.8 括号与运算符级别

  1. 分不清时候可以使用()运算符
  2. Java不使用,运算符。不过可以在for语句的第1部分和第3部分使用逗号分隔表达式列表。

3.5.9 枚举类型

  • 枚举类型是一种自定义的类型,枚举类型的声明有点像类的声明。
  • 枚举类型只包括有限个命名的值。

3.6 字符串

  Java中,字符串的本质就是一串Unicode序列。Java没有内置的字符串基本类型,而是在Java类库中提供了一个预定义类java.lang.String

3.6.1 子串

String对象可以调用其substring(beginIndex, endIndex)方法获得子串(起始下标为0)。 注意:

  • substring的第二个参数是不想复制的第一个位置。
  • 这样做的优点是容易计算子串的长度。长度 = endIndex-beginIndex。

3.6.2 拼接

  1. String 可以使用+拼接字符串。当一个字符串与非字符串拼接时,后者将被转换成字符串。
  2. 如果要使用分隔符拼接一个字符串数组,可以使用String.join("/", “s”, “m”, “l”, “xl”)方法。

3.6.3 不可变字符串

  相比C/C++可以修改单个字符而言,String类没有提供用于修改字符串的方法。   由于不能修改Java字符串中的字符,所以Java文档中将String类对象称为“不可变字符串”。如果需要修改某个字符串变量,通常直接让它引用另一个字符串

  虽然重新建立一个字符串的效率并不高,但是不可变字符串带来一个优点:编译器可以让字符串共享。Java中有一个字符串池,每个字符串变量分别指向存储池中相应的位置。如果复制一个字符串变量,原始字符串与复制的字符串可以共享相同的字符串,而不必担心字符串被莫名其妙修改的问题。   Java设计者认为共享带来的高效远远胜于提取、拼接字符串带来的低效。因为更多时候我们做的不是修改而是比较。(有一种例外情况,将来自输入流的单个字符或较短的字符串拼接成长串,不过为此有专门的类StringBuilder来负责)

  此外,不必担心过多的字符串导致堆内存遗漏,Java有GC机制。

3.6.4 检测字符串是否相等

  可以使用equals方法检测两个字符串(变量或字面量)是否相等。(如果忽略大小写可以使用equalsIgnoreCase方法)。

注意:   一定不要使用 == 检查两个字符串是否相等!这只能判断两个字符串是否放在同一内存位置上。虽然放在同一位置上的字符串必然相等,但是完全有可能将内容相同的多个字符串拷贝防止在不同的位置上!“==”返回true或false与两个字符串相等与否并非等价。

  如果虚拟机始终将所有相同的字符串共享,那么是可以使用==来判断的。但实际上只有字符串常量是共享的,而 + 或substring等操作产生的结果是不共享的。   C++中的string类重载了==运算符,以便检测字符串内容的相等性。C语言中则通常使用strcmp()方法。这类似于Java中的compareTo()方法,但是Java中的compareTo()通常用于比较字典序,判断String相等还是使用equals()最为清晰。

3.6.5 空串与null

注意,如果要检查一个String既不是null也不是空串,要先判断是否为null,再调用它身上的方法判断是否为空串。

3.6.6 码点与代码单元

  Java字符串由char值序列组成。char类型是一个采用UTF-16编码表示的Unicode码点的代码单元。大多数Unicode使用一个代码单元,某些特殊符号则需要两个。

  1. 求长度:str.length()返回代码单元数量str.codePointCount(0, str.length())返回码点数量
  2. 根据下标找char / int码点:char ch = str.charAt(index);返回对应的char(太底层,不建议使用,可能返回一个无意义的代码单元);int cp = str.codePointAt(index);返回Unicode码点值。
  3. 根据字符偏移量找下标:int index = str.offsetByCodePoints(0, i);,即从下标0开始,向后偏移i个码点对应码点的下标。
  4. 如果需要遍历所有码点,可以使用int[] codePoints = str.codePoints().toArray()方法,得到一个int[]数组;反之使用new String(codePoints, 0, codePoints.length)得到原来的字符串。

3.6.7 String API(略)

3.6.8 阅读联机API文档(略)

3.6.9 构建字符串

  StringBuilder(JDK 5引入)和StringBuffer的区别:

  • StringBuilder效率高,但是只支持单线程;
  • StringBuffer效率较低,但是支持多线程添加和删除字符;

3.7 输入输出

3.7.1 读取输入(控制台输入)

  • 标准打印流:System.out
  • 标准字节输入流:System.in
  • 因为System.in是字节输入流,所以只能以字节为单位从控制台读取,所以如果要把一串字节(byte[])转换成字符串、数字等等,就非常非常麻烦,因此,需要有一个包装类Scanner帮我们处理这类繁琐无聊的事情。
  • 如果需要控制台输入,最好将一个Scanner对象与标准输入流关联:new Scanner(System.in);,直接调用它的next()、nextLine()、nextInt()、nextDouble()等方法,这个Scanner便会帮我们处理这些琐碎无聊且易错的事情。

3.7.2 格式化输出

  幸运的是,Java沿用了C/C++的标准输入输出printf(),在Java中是System.out.printf()方法。转换符、格式控制符基本沿用了C/C++的风格。   此外Java还给出了很多扩展功能的printf()标志,以及用于Date类对象的日期和时间的转换符。   可以使用静态的String.format(String… args)方法创建一个格式化的字符串。

3.7.3 文件的输入与输出

  • 文件输入:只需要在构造Scanner时,传输一个File对象作为输入即可(不能直接使用字符串),根据源码,Scanner会自动将File装入FileInputStream。
  • 文件输出:同理,构造一个打印流对象即可。

补充点:PrintStream和PrintWriter的区别

  • System.out使用的是PrintStream,其工作原理是将字符以系统默认编码转换成字节流送给控制台,不支持指定编码,这就导致在将数据传输给另一个平台时,解码出现错误。因为我们常用的控制台是在本地机器的,所以一般没有问题。PrintStream类出现较早,所以为了兼容性,System.out沿用了PrintStream。
  • 我们通常使用的是较晚出现的PrintWriter,其基本功能同PrintStream一样,主要区别在于PrintWriter支持指定的编码方式java.nio.charset.Charset,使得在跨平台时,兼容性和可控性会更好。

3.8 控制流程

3.8.1 块作用域

  • 块作用域:用一对大括号括起来的若干条Java语句。
  • Java中嵌套的块作用域不允许声明同名的变量(C++中是允许的,并且内层变量覆盖外层变量,但是容易出错)。
  • 使用块(复合语句)可以在Java程序结构中原本只能放置一条(简单)语句的地方放置多条语句。

3.8.2 条件语句(略)

  • if () {} else if () {} else {}

3.8.3 循环(略)

  • while(){}
  • do{}while();

3.8.4 确定循环(略)

  • for(int i = 0; i < n; ++i) {}

3.8.5 多重选择

  • 在程序中不建议使用switch,因为一旦忘记break就容易引发错误。
  • case标签可以是:
    • char、byte、short、int的常量表达式
    • 枚举常量。
    • 从JavaSE7开始可以是字符串字面量

3.8.6 中断控制流程语句

  • Java除了支持普通的break之外,还额外支持一种带标签的break,例如:
read_data:
while(...) {
	for(...;...;...) {
		......
		break read_data;
		......
	}
}

if(...) {

} else {

}
  • 注意,对于任何使用break语句的代码,都最好检测一下循环是否正常结束,还是break跳出。
  • 实际上,continue也有带标签的continue,因为不常用且导致代码难以阅读,故不再展开。

3.9 大数值

  如果基本的整数和浮点数运算无法满足精度需要,则可以使用BigInteger和BigDecimal类,它们分别实现了任意长度的整数运算和任意精度的浮点运算。

注意:   与C++不同,Java没有重载运算符的功能。虽然Java设计者为String重载了 + 运算符,但没有重载其他运算符,也不支持程序员重载运算符。

3.10 数组

  数组是一种顺序存储的数据结构。优点在于支持随机访问,缺点在于增删元素的时间复杂度较高。

有关数组初始化问题:

  • 在Java中,创建一个数组时,所有元素都初始化为0、false或null,表示没有存放任何对象。

有关数组长度问题:

  • 在Java中,数组的长度不要求是常量。假设n已经被显式初始化,则new int[n]是合法的语句。
  • 可以通过数组的public final int length属性可以获得数组的长度,一旦访问越界则会抛出异常。
  • 如果需要可扩展长度的数组,可以使用ArrayList类。

3.10.1 for each 循环

  Java有一种简洁不易错且功能很强的循环结构:for (variable : collection) statement,这样我们就无需担心集合长度以及下标问题辣~~~

注意

  1. collection必须是数组或实现了Iterable接口的类对象
  2. 其实每次迭代都是把一个collection中的变量赋值给了variable,所以如果要对基本数据类型进行遍历,则仅仅支持访问而不支持修改,因为修改的仅仅是临时变量,而非集合中的真正值。
  3. 如果需要在循环中使用下标值,或者仅仅访问集合中的个别元素,则需要使用传统的for循环。
  4. 如果仅仅需要打印所有值,可以使用Arrays.toString()方法。它会调用数组中每个对象的toString方法(基本数据类型是直接转换成字符串),然后加一个方括号,每个元素用逗号分隔,将集合中的元素全部打印出来。

3.10.2 数组初始化以及匿名数组

  • 初始化数组:int[] arr = {1, 2, 3} 。其效果等价于 int[] arr = new int[]{1, 2, 3}。
  • 创建匿名数组:new int[N]new int[] {1, 2, 3},其优点在于不创建新变量的情况下创建一个数组对象。

注意:Java中,允许数组的长度为零。长度为零的数组与null不同,是占用空间的。

3.10.3 数组拷贝

  • 如果直接使用“=”赋值,则仅仅拷贝数组的引用(浅拷贝)。
  • 如果需要“深拷贝”,将整个数组再拷贝一份副本,则需要使用Arrays.copyOf(arr, arr.length)方法,这将返回拷贝后新数组的引用。长度小于原数组则截断,大于原数组则补0/false。
  • Java中的数组变量没有重载+/-运算符,所以不能像C++的指针一样通过加减来得到下一个元素。
  • Java的数组是对象,因此是在堆内存中保存的(GC回收)。而C++中,int arr[100];是保存在栈内存中(随着代码块结束自动回收);int* a = new int[100];才是保存在堆中(且需要手动delete)。

3.10.4 命令行参数

  在Java中,main方法固定带有一个字符数组String[] args作为参数。   当在命令行键入如下字符串并会车时,会调用Message类的main入口方法,并将"-g"、“cruel”、"world"作为参数传入String[] args中,并可以在程序中使用。

java Message -g cruel world

3.10.5 数组排序

  可以直接调用Arrays.sort()方法,进行优化的快速排序,快排对于大多数数据集合来说效率还是比较高的。   程序清单3-7给出一个非常巧妙的不重复抽签办法。每次只随机产生下标,然后找出该元素后,用最后一个元素覆盖之,然后n–,使得下一次抽签的范围变成0 ~ (n-1),然后不断迭代这一过程即可。

3.10.6 多维数组

  Java中,N维数组的定义和初始化大体与之前的一维数组类似,只不过多了几个维度。 注意

  1. for each 不能直接遍历二维数组中的每一个元素,它是按照一位数组处理的。可以使用下面嵌套的foreach语句:
for(double[] row : arr)
	for(double value : row)
		do something with value......
  1. 如果要快速打印一个二维数组的数据元素列表,可以调用
System.out.println(Arrays.deepToString(arr));

3.10.7 不规则数组

  Java的多维数组与C/C++在使用上大同小异,但是在存储结构上存在着很大差别,这正是Java的优势所在。   C/C++中,多维数组中的所有数据通常也是连续摆放在内存的一片区域中的,而Java中的数组更像是“数组的数组”,例如二维数组a引用的内存中,其实保存的是row个一位数组的引用。

这样带来的好处是:   第一,我们可以很轻易的将两行进行交换:

double[] tmp = balances[i];
balances[i] = balances[j];
balances[j] = tmp;

  第二,我们可以创建一个每行长度不等的不规则数组(例如对角矩阵):

int[][] odds = new int[rowCount][]; // odds数组中均为null
// 依次为每一行创建一个新数组
for(int i = 0; i < rowCount; ++i) {
	odds[i] = new int[i+1];
}

注意

  1. 由于Java多维数组的内存分布与C和C++有显著差异,所以在Java的二维数组声明中,往往“行”数比“列”数重要。“列”数可以省略(因为列数的长度可以是任意的),而“行”数不能。这一点与C/C++恰好相反。
  2. 在C++中,Java声明double[][] balances = new double[10][6];等价于double **balances = new double*[10];,然后为指针数组中的每个元素申请堆中的空间:
for(int i = 0; i < 10; ++i) {
	balances[i] = new double[6];
}

庆幸的是在Java中会自动进行这样的空间开辟,除非建立的是不规则数组。

小博同学,2020年7月8日凌晨00:32,于小破邮。