最近阅读了梁勇,阮丽珍编著的《java深入解析:透析java本质的36个话题》一书,将其中一些话题记录下来。
目录
1. 关键字
java中的关键字如下表所示:
注意:
goto
与const
作为保留的关键字而存在,虽然未在程序中使用,但是也禁止程序员将其作为标识符来使用。true
、false
与null
是3个字面常量,并非Java中的关键字,在程序中也禁止作为标识符使用。
2. 标识符
标识符定义规则:
- 标识符的首字符所对应的代码点必须使得
Character
类的isJavaIdentifierStart
方法返回值为true
,后续字符所对应的代码点(如果存在后续字符的话)必须使得Character
类的isJavaIdentifierPart
方法返回值为true
。 - 标识符不能与Java中的关键字相同。
- 标识符不能和Java中预定义的字面常量名称相同(
true
、false
、null
)。 - 标识符的长度必须在系统所支持的范围内。
注意:
- 尽管标识符能够使用
$
,但是最好不要使用,因为$
被编译器所使用,在源文件(.java
文件)编译成字节码(.class
文件)后,会成为顶层类型与嵌套类型之间的连接符。
3. 整型数据类型之间的转换
注意:
- 因为
char
类型是无符号类型(0~65535
),因此char
与byte
(−128~127
),char
与short
(−32768~32767
)类型不存在子集关系,即,char
与其他两种类型之间的转换总是需要类型转换。 byte
、char
或者short
类型(或者为三者的混合)参与运算时,结果为int
类型。- 复合运算符(例如
+=
)在赋值时可以自动将运算结果转化为左侧的操作数类型。 - 从
byte
类型到char
类型的转换需要经过两个步骤:1.将byte
通过扩展转换,转换成int
类型;2.再将int
类型通过收缩转换,转换成char
类型。 - 如果变量的类型是
byte
、short
或char
类型,当对其赋予编译时期的常量,而该常量又没有超过变量的取值范围时,编译器可以进行隐式的收缩转换。但是只适用于变量的赋值,不适用于方法调用语句。
4. 浮点数
注意:
- 浮点数在计算机内部不能精确表示,大部分都是近似值,就跟十进制无法精确表示
1/3
一样。 - 二进制所能表示的两个相邻的浮点值间存在一定的间隙,浮点值越大,这个间隙也会越大。当浮点值大到一定程度时,如果对浮点值的改变很小(例如对
30000000000000.00
加0.001
),就不足以使浮点值发生改变。
5. 贪心规则
i+++j
如何计算?
编译器在分析字符时,会尽可能多的结合有效字符,而不管这种结合方式是否符合语法规则。具体来说,上面i+++j
相当于(1++) + j
。
6. ++i与i++
不管是前置++
,还是后置++
,都是先将变量的值加1
,然后才继续计算的。二者之间区别是:前置 ++
是将变量的值加1
后,使用增值后的变量进行运算的,而后置++
是首先将变量赋值给一个临时变量,接下来对变量的值加1
,然后使用那个临时变量进行运算。
7. 移位运算符
int
类型占用4字节,32位,long
类型占用8字节,64位。如果将int
类型(long
类型) 移动超过31位(63位),便失去了意义。但是,系统对此做了相关处理。
实际上,当左侧操作数为int
类型时,右侧操作数只有低5位是有效的(低5位范围为[0,31]),可以看做是右侧操作数会先与掩码0x1f
做与运算,然后左侧操作数再移动相应位数。而对于long
类型,右侧操作数只有低6位是有效的(低6位范围[0,63]),可以看做是右侧操作数先跟掩码0x3f
做与运算,然后再移动相应位数。例如:
int i = 5 << -10;
-10
的补码为:1111 1111 1111 1111 1111 1111 1111 0110
,取其低五位为10110
,这个值就是22
,所以上面例子相当于:
int i = 5 << 22;
由于计算机内部使用二进制表示数据,很多人会有这样的想法:左移一位,相当于乘以2,右移一位,相当于除以2。
实际上,这种想法是不准确的。因为java中:
- 相除运算的舎入模式是,向0舎入,即两个操作数都是整型的时候,相除的结果也是整型,如果不能整除,则结果向靠近
0
的方向取值。 - 位移运算的舎入模式是向下舎入,即结果向小的方向取值。
由于相乘运算与整除的时候不涉及到舎入模式的问题,所以值相等,但是一旦不能整除,就会涉及到舎入模式,结果就会存在差异。
乘除与移位运算结果统计如下:
所以:数值v
左移n
位与v
乘以2n
的值是相等的,如果v
能够整除2^n
,则v
右移n
位与v
除以2n
的值也是相等的。如果v
不能整除2^n
,当v
是正数时,v
右移n
位与v
除以2n
的值相等,当v
是负数时,则v
右移n
位与v
除以2n
的值是不相等的(右移指有符号右移)。
8. "+"如何连接字符串的
- 当使用“+”来连接字符串时,实际上是使用临时创建的
StringBuilder
对象的append()
方法来辅助完成的。 - 对于编译时常量,在编译后直接计算出字符串的值,不会在运行时创建临时的
StringBuilder
对象来完成字符串的连接。
9. equals方法与"=="的区别
对于这个问题,很多人都会很自信的回答:equals
方法比较的是对象内容,而==
比较的是对象的地址。但是很遗憾,这个是不完整的。
其实,equals
方法是在Object
类中声明的,而所有类都直接或间接继承了Object
类,也就是都继承了这个方法。equals
方法在Object
类中实现如下:
public boolean equals(Object obj) {
return (this == obj);
}
由上面equals
方法的实现可知,在Object
类中,equals
方法也是调用的==
进行判断,比较的也是地址。
我们经常用equals
方法比较两个字符串内容,那是因为String
类重写了equals
方法。
所以:从Object
类继承的equals
方法与“==
”运算符的比较方式是相同的。如果继承的equals
方法对我们自定义的类不适用,则可以重写equals
方法。
对于重写equals
方法,有以下几点要求:
- 自反性。对于任何非
null
的引用x
,x.equals(x)
应当返回true
。 - 对称性。对于任何非
null
的引用x
,y
,当且仅当:y.equals(x)
返回true
的时候,x.equals(y)
才会返回true
。 - 传递性。对于任何非
null
的引用值x
、y
与z
,如果x.equals(y)
返回true
,并且y.equals(z)
返回true
,那么x.equals(z)
也应返回true
。. - 一致性。对于任何非空引用值
x
与y
,假设对象上equals
比较中的信息没有被修改,则多次调用x.equals(y)
始终返回true
或始终返回false
。 - 对于任何非空引用值
x
,x.equals(null)
应返回false
。
注意:在java类库中,集合类的许多方法都是调用equals
作为内部判断条件,如果重写equals
方法没有遵守以上规则,那么可能会达不到期望的结果,甚至某些操作是反常的。
在重写equals
方法的时候,也必须重写hashCode
方法,否则该类与其他类交互时,可能产生不确定的运行后果。
重写hashCode
方法需要遵守以下规定:
- 如果两个对象通过调用
equals
方法是相等的,那么这两个对象调用hashCode
方法必须返回相同的整数。 - 如果两个对象调用
equals
方法不相等,不要求这两个对象调用hashCode
方法返回相同的整数。 - 在java应用执行期间,如果对象
equals
方法比较所用的信息没有更改,那么在同一个对象上面多次调用hashCode
方法,必须返回一致的整数。但是如果多次执行同一个应用,不要求该整数必须相同。
10. 方法重载
方法重载:调用哪个重载方法是根据实参的静态类型(编译时类型)决定的,与运行时实参具体的类型无关。例如:
public class OverLoadTest {
public static void main(String[] args) {
Object object = new String("test"); // object静态类型为Object
OverLoadTest overLoadTest = new OverLoadTest();
overLoadTest.m(object); // 会打印出Object
}
private void m(String s) {
System.out.println("String");
}
private void m(Object o) {
System.out.println("Object");
}
}
11. 方法重写
如果子类Son
的某个方法sm
重写了父类Father
的某个方法fm
,则需要满足以下条件:
sm
方法与fm
方法都为实例方法。sm
方法的签名是fm
方法的子签名。方法的签名,即方法的名称与方法的参数类型。sm
方法的返回类型是fm
方法的返回类型的可替换类型。sm
方法的访问权限不得低于fm
方法的访问权限。sm
方法不能抛出比fm`方法更多的受检异常。sm
方法继承了fm
方法。
12. 方法与成员变量的隐藏
方法的隐藏与方法的重写条件除了要求子父类的方法都是静态方法之外,其余的条件都是相同的。
成员变量的隐藏比较简单,只要求子类与父类的成员变量名字相同即可,与变量的类型,访问权限以及是实例变量还是静态变量无关。
重写与隐藏的本质区别是:重写是动态绑定的,根据运行时引用所指向对象的实际类型来决定调用相关类的成员。而隐藏是静态绑定的,根据编译时引用的静态类型来决定调用相关类的成员。换句话说,如果子类重写了父类的方法,当父类的引用指向子类对象时,通过父类的引用调用的是子类的方法。如果子类隐藏了父类的方法(成员变量),通过父类的引用调用的仍然是父类的方法(成员变量)。
13. 构造器
- 构造器不是方法,也不是类的成员,构造器不能被继承。
- 构造器是递归调用的,子类的构造器会调用父类的构造器,直到调用到
Object
类的构造器为止。 - 构造器没有创建对象。程序运行时,是
new
运算符在堆上开辟了一定的空间,然后执行对象的初始化,当对象创建成功后,也是new
运算符将对象的起始地址返回。实际上,构造器的作用是在对象创建时,进行类中实例成员的初始化。 - 当类中没有显示声明构造器时,编译器会自动添加一个默认的无参构造,该构造器的访问权限与类的访问权限一致。
- 在构造器或是实例方法调用的时候,会将其关联的对象作为第一个参数隠式传递,这个对象就是当前对象
this
。
14. 成员变量的初始化方式
成员变量也叫做域,或者字段,就是在类内部声明,表明类具有的某种特征。
成员变量可以分为静态成员变量(通常叫做类变量)和实例成员变量。
- 实例成员变量可以使用3种方式进行初始化:
- 在声明处初始化
- 在实例化块中初始化
- 在构造器中初始化
实例化初始化块中不能使用return
语句,因为实例化初始化块中的语句会赋值到构造器前面来执行,如果使用了return
语句,那么原来构造器中的语句就无法执行。
从编译器的角度来看,不管是在声明中初始化还是在实例块中初始化,最后都是在构造器中执行的。编译器会将每个构造器都生成一个<init>
方法,当然,这个构造器是经过处理后的构造器,也就是将实例变量声明处初始化与实例初始化块都复制到其最上方的构造器。这就是说,所有的实例初始化都是在<init>
方法中执行的。
- 类变量可以使用2种方式进行初始化:
- 在声明处初始化
- 在静态初始化块中初始化
静态初始化块中不能使用return
语句,因为系统会把所有的静态初始化(静态变量声明处初始化和静态初始化块中初始化)都整理到一块执行,相当于整理成为了一个大的“静态初始化块“,统一执行这个“静态初始化块”,如果在静态初始化块中使用了return
语句,就会导致后面的静态初始化语句不能执行。
静态初始化块中不能抛出任何检测异常,因为无法捕获这些异常。
当子类继承父类的实例变量x
,如果子类没有隐藏变量x
,则对于同一个对象,只存在一个变量x
,即通过this.x
与super.x
访问的是同一个变量。如果子类隐藏了变量x
,则通过this.x
与super.x
访问的不再是同一个变量。
当子类继承父类的静态变量s
,如果子类没有隐藏变量s
,则变量s
由父类及其所有子类共享,无论是通过类名(父类或子类)还是通过实例对象(父类或子类的实例)访问的s
都是同一个变量。
15. 初始化顺序
- 类的初始化也是按照顺序进行的,当初始化子类的时候,如果父类尚未初始化,就会首先初始化父类,这个过程也是递归进行的,直到递归到
Object
类为止。如果父类已经初始化,则初始化不再执行,因为类的初始化只会执行一次。 - 初始化的顺序可以简单总结为:先静态,后实例,先父类,后子类。对于静态初始化,按照静态变量声明处初始化和静态初始化块在类中出现的顺序执行。对于实例初始化,按照实例变量声明初始化和实例初始化块在类中出现的顺序执行,然后执行构造器。
16. 基本数据类和包装数据类型
从JDK1.5开始,编译器可以自动执行装箱与拆箱转换。
装箱:将基本数据类型转换为包装类引用数据类型(调用包装类的valueOf()
方法)。
拆箱:将包装类引用数据类型转换为基本数据类型(调用typeValue
方法,type
表示基本数据类型)
。
包装类在类中实现了缓存,当需要将基本数据类型转换为包装类型时,如果基本数据类型的值在包装类的缓存范围内中 ,则返回包装中的缓存对象,否则返回一个新建对象。
下面是各个包装类的缓存值范围:
17. 接口
- 接口中未声明任何方法,也默认存在9个方法,这9个方法与
Object
类中9个public
方法一一对应。 - 接口是完全抽象的设计,不能实例化。使用
new InterfaceName(){}
创建的接口类型,实际上是创建了一个匿名类,该匿名类实现了该接口。
18 枚举
- 枚举是一个特殊的类,枚举常量就是类的对象,并且在枚举类外无法创建枚举对象。
- 所有的枚举类都继承自
java.lang.Enum
接口,但是不能显示的继承Enum
类。 - 所有的枚举类都声明为
final
。所有的枚举常量都声明为public static final
。