Java 基础知识 02
计算机基础
位运算符
- Java定义了位运算符,应用于整数类型(int),长整型(long),短整型(short),字符型(char),和字节型(byte)等类型。
- 位运算符作用在所有的位上,并且按位运算。假设
A = 60,B = 13;
它们的二进制格式表示将如下:
A = 0011 1100
B = 0000 1101
----------------------
A & B = 0000 1100
A | B = 0011 1101
A ^ B = 0011 0001
~A = 1100 0011
操作符 | 描述 |
| 如果相对应位都是1,则结果为1,否则为0 |
| 如果相对应位都是0,则结果为0,否则为1 |
^ | 如果相对应位值相同,则结果为0,否则为1 |
~ | 按位取反运算符翻转操作数的每一位,即0变成1,1变成0。 |
| 按位左移运算符。左操作数按位左移右操作数指定的位数。 |
| 按位右移运算符。左操作数按位右移右操作数指定的位数。 |
| 按位右移补零操作符。左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充。 |
注:
- 在实际编程中,位移运算符仅作用于整型
(32位)
和长整型(64位)
数上。 - 移动的位数是一个mod的结果
(32位:mod 32,64位:mod 64)
。即对应来说,35 >> 1
和35 >> 33
是一样的结果,35 << 1和35 << 65
是一个结果。 - 负数在无符号右移63位时,除最右边为1外,其余均为0,达到最小值1。如果
>>> 64
,则为其原数值本身。
|和||,&和&&区别
&和&&
,按位与和逻辑与,两者都可以作用于条件表达式,但是后者有短路的作用,表达式如下:
boolean a = true;
boolean b = true;
boolean c = (a=(1==2)) && (b=(1==2)); // 最后a的值为false, b的值为true
boolean c = (a=(1==2)) & (b=(1==2)); // 最后a的值为false, b的值为false
同样,|和||
,按位或与逻辑或,后者具有短路的功能,表达式如下:
boolean a = true;
boolean b = true;
boolean c = (a=(1==1)) || (b=(1==1)); // 最后a的值为true, b的值为false
boolean c = (a=(1==1)) | (b=(1==1)); // 最后a的值为true, b的值为true
浮点数
思考:精度丢失的原因
float a = 1F;
float b = 0.9F;
// 结果为:0.100000024
float c = a - b;
- 这是编程时经常会出现的问题,小数的精度丢失。首先要明确一点的是,在
Java
中,小数也就是浮点数采用的是科学计数法:a × 10n,其中1≤|a|<10
。 - 以单精度
float
为例,它占用4个字节,也就是32个bit
,分为三个部分
- 符号位: 在最高位处,0表示整数,1表示负数;
- 阶码位: 存储的其实是指数对应的移码,而不是指数的原码或补码。移码:将一个真值在数轴上正向平移一个偏移量之后得到的,所以移码大的真值也大。这样做的好处就是可以更直观地反映两个真值的大小。
假设指数的真值为e,阶码位E,则有:E = e + (2n-1 - 1),以单精度为例,这里的n = 8。 - 尾数位:最右侧分配连续的23位用来存储有效数字。这里为了节约存储空间,将符合规格化尾数的首个1省略,所以尾数表面上是23位,其实表示了24位二进制数。
- 在进行加减运算时会进行如下操作:
- 零值检测。检查参加运算的两个数中是否存在为0的数。(浮点数是无法表示0的,所以0在浮点数是一种规定,即阶码与尾数全为0)。所以,如果其中一个数为0,则直接得出结果。
- 对阶操作。判断阶码是否相等,跟数学中的同位相加减是一个道理。而计算机中移位会导致精度丢失,无论是左移还是右移,但是右移可能会将高位移除。所以,对阶时移动方向为右移,即选择阶码小的数进行操作。
- 尾数求和。对阶完成后,直接按位相加减即可。
- 结果规格化。要满足科学技术的规范,结果有可能需要进行阶码移位。
- 结果舍入。对阶和规格化时,会右移导致精度丢失,即被移出的位数会被丢弃,从而导致结果精度丢失。为了减少这种精度的损失,先将移出的这部分数据保存起来,称为保护位,等到规格化后再根据保护位进行舍入处理。
通过上文的叙述,下面说一下为何1.0f - 0.9f
会有精度丢失:
浮点数 | 符号 | 阶码 | 尾数 | 尾数补码 |
1.0 | 0 | 127 | 1000-0000-0000-0000-0000-0000 | 1000-0000-0000-0000-0000-0000 |
-0.9 | 1 | 126 | 1110-0110-0110-0110-0110-0110 | 0001-1001-1001-10001-1001-1010 |
- 首先两个数都不为0。但是,由于阶码不同,所以要进行对阶操作。根据规定将126移动为127,同时高位补1,则移动后-0.9的尾数补码为:
1000-1100-1100-1100-1100-1101
。 - 此时进行同位加减。尾数位计算结果为:
0000-1100-1100-1100-1100-1101
。 - 规格化。尾数的最高位必须为1,所以结果需要向左移4位,同时阶码需要减4,再隐藏最高位,而低位则用0进行补齐。最终尾数结果为:
100-1100-1100-1100-1101-0000
。 - 综上,最终结果的符号为
0
,阶码位1111011
,尾数为100-1100-1100-1100-1101-0000
。三部分组合起来就是最终结果,对应的十进制就是0.100000024
。
CPU与内存
CPU内部结构:由控制器和运算器组成,内部寄存器来使这两者的协同更加高效。
控制器
控制器有点像编程语言的编译器。由控制单元、指令编码器、指令寄存器组成。控制单元是CPU的大脑,由时序控制和指令控制组成;指令编码器是在控制单元的协调下完成指令读取、分析并交由运算器执行等操作;指令寄存器是存储指令集。
运算器
它的核心是算术逻辑运算元,即ALU
,能执行算术运算或逻辑运算等各种指令,运算单元会从寄存器中提取或存储数据。
寄存器
最著名的寄存器是CPU的高级缓存L1、L2
,CPU的运行速度远大于内存的读写速度。并且CPU会缓存部分指令和数据,以提升性能。
TCP/IP
-
Transmission Control Protocol/Internet Protocol
中文译为传输控制协议/因特尔互联网协议,我们熟知的协议有:HTTP、HTTPS、FTP、SMTP、UDP
等。详细的讲解这里就不叙述了,类似的文章也有狠多。这里详解一下三次握手和四次挥手。
TCP建立连接
- 首先TCP是一种面向连接,确保数据在端到端间可靠传输的协议。TCP报文格式如下图所示:
- 这里着重介绍TCP的FLAG位,由6个bit组成,分别代表
SYN、ACK、FIN、URG、PSH、RST
,都以置1表示有效。重点关注SYN、ACK和FIN
。
-
SYN(Synchronize Sequence Numbers)
用作建立连接时的同步信号; -
ACK(Acknowledgement)
用于对收到的数据进行确认,所确认的数据由确认序列号表示; -
FIN(Finish)
表示后面没有数据需要发送,通常意味着所建立的连接需要关闭了。
- 三次握手指的就是TCP建立连接的三个步骤:
- A机器(客户端)发出一个数据包并将SYN置为1,表示希望建立连接。包中携带的序列号假设为x。
- B机器(服务端)收到A机器发过来的数据包后,通过SYN得知这是一个建立连接的请求,于是发送一个响应包并将SYN和ACK标记都置为1。假设这个包中的序列号是y,而确认序列号必须是x+1,表示收到了A发送过来的SYN。在TCP中,SYN被当作数据部分一个字节。
- A机器收到B机器的响应后需要进行确认,确认包中将ACK置1,并将序列号设置为y+1,表示收到了来自B的SYN
- 三次握手有两个目的:信息对等和防止超时。三次握手,分别确认的信息如下图所示:
TCP断开连接
TCP是全双工通信,这与数据库建立的连接时不同的。断开连接,则需要进行四步操作:
- A机器想要关闭连接,则待本方数据发送完毕后,传递FIN信号给B机器;
- B机器应答ACK,告诉A机器可以断开,但是需要等B机器处理完数据,再主动给A机器发送FIN信号。这时,A机器处于半关闭状态
(FIN_WAIT_2)
,无法再发送新的数据。 - B机器做好连接关闭前的准备工作后,发送FIN给A机器,此时B机器也进入半关闭状态
(CLOSE_WAIT)
。 - A机器发送针对B机器FIN的ACK后,进入
TIME_WAIT
状态,经过2MSL(Maximum Segment Lifetime)
后,没有收到B
机器的报文,则确定B机器已经收到A机器最后发送的ACK指令,此时TCP连接正式释放。
HTTPS
-
HTTPS
的全称是HTTP over SSL
,简单理解就是在http传输的基础上增加了SSL协议的加密能力。 -
SSL(Secure Socket Layer, SSL)
安全套接字层,SSL协议工作于传输层与应用层之间,为应用提供数据的加密传输。 - 访问一个HTTPS的网站的大致流程如下:
- 浏览器向服务器发送请求,请求中包括浏览器支持的协议,并附带一个随机数。
- 服务器收到请求后,选择某种非对称加密算法,把数字证书签名公钥、身份信息发送给浏览器,同时也附带一个随机数。
- 浏览器收到后,验证证书的真实性,用服务器的公钥发送握手信息给服务器。
- 服务器解密后,使用之前的随机数计算出一个对称加密的密钥,以此作为加密信息并发送
- 后续所有的信息发送都是以对称加密方式进行的。
面向对象
- 面向对象的四大特征:抽象、封装、继承和多态。
- 抽象是程序员的核心要素之一,体现出程序员对业务的建模能力,以及对架构的宏观掌控力。
- 封装是一种对象功能内聚的表现形式,是模块之间的耦合度便利,更具有维护性。
- 继承使子类能够继承父类,获得父类的部分属性和行为,使模块更有复用性。
- 多态使模块在复用性的基础上更加有扩展性,使系统运行更有想象空间。
访问权限控制
- Java访问权限分为四个等级,权限控制及可见范围如下表所示:
访问权限控制符 | 任何地方 | 包外子类 | 包 内 | 类 内 |
| √ | √ | √ | √ |
| × | √ | √ | √ |
| × | × | √ | √ |
| × | × | × | √ |
- 在定义类时,推荐访问控制级别从严处理:
- 如果不允许外部直接通过
new
创建对象,构造方法必须是private
; - 工具类不允许有
public
或default
构造方法; - 类非
static
成员变量并且与子类共享,必须是protected
; - 类非
static
成员变量并且仅在本类中使用,必须是private
; - 类
static
成员变量如果仅在本类中使用,必须是private
; - 若是
static
成员变量,必须考虑是否为final
; - 类成员变量只供类内部调用,必须是
private
; - 类成员方法只对继承类公开,那么限制为
protected
;
序列化
- 将数据对象转换为二进制流的过程称为对象的序列化
(Serialization)
。反之,将二进制流恢复为数据对象的过程称之为反序列化(Deserialization)
。常见的序列化方式有三种:
- Java原生序列化。保留了对象的元数据信息,兼容性最好,但不支持跨语言,而且性能一般。
- Hessian序列化。一种支持动态类型、跨语言、基于对象传输的网络协议。具有如下特性
- 自描述序列化类型。极大缩短了二进制流。
- 语言无关。支持脚本语言;
- 协议简单,比Java原生序列化高效。
- 注意:Hessian序列化时,先序列化子类,然后序列化父类,因此反序列化结果会导致子类同名成员变量被父类的值覆盖。
- JSON序列化。一种轻量级的数据交换格式。JSON序列化就是将数据对象转换为JSON字符串。可读性比较好,方便调试。
覆写
- 概念就不多说了。简单的说就是子类重写父类中可以被重写的方法。通过父类引用执行子类方法是需要主要以下两点
- 无法调用到子类中存在而父类本身不存在的方法;
- 可以调用到子类覆写父类的方法,这是一种多态的实现。
- 覆写父类方法,需要满足以下几个条件:
- 访问权限不能变小。即方法的访问权限限制不能更小,但可以更大;
- 返回类型能够向上转型成为父类方法的返回类型。也就是说,返回类型是父类方法返回类型本身或其子类;
- 异常也要能向上转型成为父类的异常。也就是说,子类只能抛出父类方法的异常及其子类异常;
- 方法名、参数类型及个数必须严格一致。
- 覆写只能针对非静态、非final、非构造方法。如果想在子类覆写的方法中调用父类方法,则可以使用
super
关键字。
重载
- 先介绍一个概念:方法签名
-
方法名称+参数类型+参数个数
,组成一个唯一键,称为方法签名。注意:方法的返回值并不是方法签名的一员。 - JVM就是通过这个唯一键决定调用哪种重载的方法。
- JVM在重载方法时,选择合适的目标顺序方法如下:
- 精确匹配;
- 如果是基本数据类型,自动转换成更大表示范围的基本类型;
- 通过自动拆箱与装箱;
- 通过子类向上转型继承了路线依次匹配;
- 通过可变参数匹配。
-
2
和3
的区别,举例说:传入int值为1的参数,那么形参类型为long类型的方法优先于形参类型为Integer类型的方法。 - 可以看出,可变参数的方法是在匹配规则中优先级最低了,并且实际中也极不推荐这种方式。
泛型
- 泛型定义时,任何字母都可以作为泛型符号。也就说在泛型符号位中使用
String
,此时并不是表示String类型,而只是一个符号的作用。约定俗称的符号包括:
- E代表
Element
,用于集合中的元素; - T代表
the Type of Object
,表示某个类; - K代表
Key
、V代表Vlaue
,用于键值对元素。
- 泛型定义时的规范如下:
- 尖括号里的每一个元素都指代一种未知类型;
- 尖括号的未知非常讲究,必须在类名之后或方法返回值之前。
- 泛型在定义处只具备执行Object方法的能力。
- 对于编译之后的字节码指令,并没有这些花里胡哨的方法签名,这也就说明泛型只是一种编写代码时的语法检查
- 泛型的好处包括:
- 类型安全,将运行时的异常转到了编译时。
- 提升可读性;
- 代码重用。
JVM
字节码
- Java之所以可以一次编写,到处执行。便是运用了中间码,这种中间码就是字节码。JVM将字节码解释执行,屏蔽对底层操作系统的依赖。如果是热点代码,会通过JIT动态地编译为机器码,提高执行效率。
字节码主要指令如下
- 加载或存储指令
- 将局部变量加载到操作栈中。如
ILOAD、ALOAD
; - 从操作栈顶存储到局部变量表。如
ISTORE、ASTORE
; - 将常亮加载到操作栈顶。如
ICONST、BIPUSH、SIPUSH、LDC
;
-
ICONST
加载的是-1 ~ 5
的数; -
BIPUSH
,即Byte Immediate PUSH
,加载-128 ~ 127
之间的数; -
SIPUSH
,即Short Immediate PUSH
,加载-32768 ~ 32767
之间的数; -
LDC
,即Load Constant
,在-2147483648 ~ 2147483647
或者是字符串时,JVM采用LDC指令压入栈中。
- 运算指令
- 将两个操作栈帧的值进行计算,并把结果写入到操作栈顶,如
IADD、IMUL
等;
- 类型转换指令
- 显示转换两种不同的数值类型。
I2L、D2F
等;
- 对象创建与访问指令。
- 创建对象指令。如
NEW、NEWARRAY
等; - 访问属性指令。如
GETFIELD、PUTFIELD、GETSTATIC
等; - 检查实例类型指令。如
INSTANCEOF、CHECKCAST
等。
- 操作栈管理指令
- 出栈操作。如
POP
即一个元素,POP2
即两个元素。 - 复制栈顶元素并压入栈。如
DUP
。
- 方法调用与返回指令
-
INVOKEVIRTUAL
指令:调用对象的实例方法; -
INVOKESPECIAL
指令:调用实例初始化方法、私有方法、父类方法等。 -
INVOKESTATIC
指令:调用类静态方法; -
RETURN
指令:返回VOID
类型。
- 同步指令
- JVM使用方法结构中的
ACC_SYNCHRONIZED
标志同步方法,指令集中有MONITORENTER
和MONITOREXIT
支持synchroinze
语义。
类加载过程
- 具体过程,可以参见另外一篇博客 《JVM的内部结构与详述》
- 这里补充说一下,为什么需要在自定义类加载器,也就是为何要打破双亲委托机制。
- 主要是为了防止在实际工程中,引入多个jar包导致某些类存在包路径和类名相同的情况,就会引起冲突,导致应用程序出现异常。主流的容器类框架都会自定义类加载器,实现不同中间件之间的类隔离,有效避免了类冲突。
- 以下情况下需要自定义类加载器:
- 隔离加载类;
- 修改类加载方式;动态加载的情况下,会有这种场景。
- 扩展加载源。比如从数据库,网络中进行加载。
- 防止源码泄露。
内存布局和垃圾回收
详见 《JVM的内部结构与详述》