Java笔记语法篇
基本数据类型
类型 | 位数 | 字节数 | 默认初始值 | 取值范围 |
---|---|---|---|---|
byte | 8 | 1 | 0 | -128(27)~127 |
short | 16 | 2 | 0 | -32768(215)~32767 |
int | 32 | 4 | 0 | -2147483648(231)~2147483647 |
long | 64 | 8 | 0L | -9223372036854775808(263)~9223372036854775807 |
char | 16 | 2 | ‘u0000’ | 0~65535(216-1) |
float | 32 | 4 | 0f | 1.4E-45~3.4028235E38 |
double | 64 | 8 | 0d | 4.9E-324~1.7976931348623157E308 |
boolean | false | ture / false |
对于boolean
在Java
虚拟机中对其支持有限:
- 单个
boolean
类型的变量表达式在编译时会被转换成int
类型,用1
表示true
,用0
表示false
,即4个字节; - 而对
boolean
数组的操作则同byte
数组等价,即1个字节。另外,其具体实现也依赖于JVM
厂商
上述基础类型对应的包装类型
Byte
、Short
、Integer
、Long
、Character
、Float
、Double
、Boolean
;- 将基本类型转换成包装类型即“装箱
(boxing)
”,反之“拆箱(unboxing)
”
基本类型与包装类型的区别
- 基础类型不具备对象特性,所以:
- 在需要使用
Object
的地方(比如泛型)必须是包装类型 - 且作为类,包装类型可以为
null
;==
对于基本类型是比较值,对于包装类型则是比较内存地址,包装类型对象之间值的比较用equals()
方法
- 在需要使用
- 基本数据类型的局部变量放在
Java
虚拟机的局部变量表/栈中,未被static
修饰的成员变量放在堆内存中;而对于包装类,实例放在堆,引用放在栈。一般基本类型更高效。 - 基础类型相比包装类型占用空间小
自动拆装箱与包装类缓存机制
1 | Integer num1 = 711; |
说明
这里前一条语句发生自动装箱,将int
转换为Interger
,实际上使用的是包装类的ValueOf()
方法 --> Integer.ValueOf()
;后一条语句发生自动拆箱,调用intValue()
方法 --> num1.intValue()
自动拆装箱发生
- 将基本数据类型放入集合类(装)
- 基本类型与包装类型大小比较(拆)
- 包装类型的运算(拆)
- 三目运算符中,第二、三位操作数有包装类时就拆
- 方法参数/返回值类型不匹配,自动拆装
包装类缓存机制
大部分包装类(除了Float
、Double
)使用缓存机制来提升性能:
在一定范围内利用自动装箱(ValueOf()
方法)创建包装类对象时,会从缓存(常量池)中寻找指定数值(一般来说各个包装类默认创建了一个静态数组cache[]
,对应范围,所以只要在这个范围内理论上都有),未找到则新建对象并返回引用;找到则直接返回引用
- 这个范围:
Byte
、Shotr
、Integer
、Long
:-128 ~ 127Character
: 0 ~ 127Boolean
: True、False
- 不在这个范围自动装箱或者直接new的包装类都会新建一个对象,且不会放入缓存,也不会复用(存储于堆空间)
由于包装类缓存机制,从缓存得来的对象引用比较时==
(比地址)会为true
,新建而来则为false
,所以包装类对象实例之间值的比较最好用equals()
方法(包装类默认重写了equals()方法,先看类型,再比较值)
另外,频繁拆装箱会影响系统性能,尽量避免不必要的拆装箱操作
高精度计算
虽说float
和double
可以满足大部分开发需要,但是精度在某些场景下仍然不够,这时候就需要BigDecimal
来进行浮点数的创建运算:
构造
为防止精度丢失,推荐使用BigDecimal(Sting val)
构造方法或者BigDecimal.valueOf(double val)
静态方法(此方法内部使用Double
的toString()
方法,采用双精度浮点数(double)
截断策略)来创建对象(实际上BigDecimal
支持使用int
、double
、long
、String
来作为构造方法的参数,但是使用double
的构造方法结果有一定的不可预知性)
运算
运算方法:
- 加 -->
add()
- 减 -->
subtract()
- 乘 -->
multiply()
- 除 -->
divide()
,且最好尽量使用3
参数版本(num
除数,scale
保留小数位数,roundingMode
保留规则),以针对出现无限循环小数的情况 - 大小比较 -->
compareTo()
(忽略精度scale
) ,-1即小于,0是等于,1为大于
注意点
- 最好不使用
equals()
方法比较值(会比较精度,1.0和1就会不相等) setScale()
方法设置保留小数位、保留规则(建议为RoundingMode.HALF_EVEN
,四舍六入五成双)- 格式化可结合
NumberFormat
类使用
长整数计算
既然有高精度浮点运算,那么在整数区域也有长整数BigInteger
类型,来处理Integer
、Long
位数不够的情况:
构造
构造方法:BigInteger
类支持的构造方法有不少,可以传入byte []
、char []
、String
、int
、int []
、long
,同时还可以加入更多参数指定进制规则等等
运算
同理,因为位数过高,所以肯定没法用常规的运算符进行运算,只能使用其类方法:
add()
--> 加subtract()
--> 减multiply()
--> 乘divide()
--> 除mod()
--> 取模(参数需要>=0
)remainder()
--> 求余pow()
--> 平方(同理,参数需>=0
)abs()
--> 绝对值negate()
--> 相反值compareTo()
--> 比较大小,规则同BigDecimal
另外
还可以使用xxxValue()
方法,将BigInteger
转换成基本数据类型,亦或者使用toString()
方法或者toByteArray()
方法转换成某种进制的字符串/字节数组
求余和取模
这二者是有区别的(不光是Java
,其他高级语言也是):
- 对于整数基本类型,
%
就是求余,而Math.floorMod()
方法才是取模。 - 当参与运算的两个数符号不一致时,对于
a模b
,运算结果符号与b
一致;而a余b
结果符号则和a
相同。因为对于r=a - c*b
,取模时c
会向负无穷
方向舍入(floor()
方法),而求余则向0
方向舍入(fix()
方法)
移位运算符
<<
左移运算符,高位丢弃,低位取零。x << 1
在不溢出的情况下相当于x * 2
>>
右移运算符,高位补符号位(即正数高位补0,负数高位补1),低位丢弃。所以 x >> 2
相当于 x / 2
>>>
无符号右移,和>>
不同的是所有空位都以0补齐
注:
- 移位操作实际上支持的类型只有
int
和long
,编译器对short
、byte
、char
类型位移前都会先转换为int
再操作 - 移位位数超过数值所占位数时会先求余
%
再移,int
对应32
位,long
对应64
位
方法重写
重写是子类对父类方法的重新改造,外部样子(原本的框架)不变,改变内部逻辑
重写规范:
- 构造方法无法重写
- 父子方法名、参数列表必须相同
- 子类方法访问修饰符范围大于等于父类
- 子类方法返回值类型应和父类方法相等或者更小
- 子类方法抛出的异常范围小于等于父类
- 如果父类方法访问修饰符为
private
/final
/static
,那么子类就无法重写此方法
方法的可变长参数机制
Java
5提供了可变长参数,允许在调用方法时传入不定长度的参数:
语法
在参数类型后跟一个...
即可:
1 | public void changeArgs(int ... args){ |
注意
- 一个方法有且仅能有一个可变长参数
- 可变长参数只能作为此方法的最后一个参数
- 遇到方法重载时优先匹配固定参数方法,其次再是可变长参数
- 其本质上还是基于数组实现,可变长参数编译之后会转换成一个数组
拷贝
前置说明
值传递:接收实参的拷贝,即创建副本,对形参修改不影响实参
引用传递: 接收实参所引用的对象在堆中的地址,不会创建副本,对形参修改影响实参
在Java
中,是只有值传递的
- 引用拷贝: 就是把一个引用数据类型的值复制一下,给一个新的引用数据类型,它俩指向的对象的地址是一样的。
注:因为在Java
中引用数据类型存储的就是对象地址值,所以传值就把引用复制了一下,本质上就是值传递。这里区别于C++
中的&
,把一个变量的地址作为参数传过去,传的不是实参的值,是其地址
对象拷贝
创建一个新对象,地址不一样
浅拷贝:
针对原始对象中的成员变量进行值传递和引用拷贝,所以如果改变复制对象或者原始对象中的引用类型变量,会同时影响对方。
一般使用默认的clone()
方法即可
深拷贝:
对原始对象中的基本数据类型变量进行值传递,但是对引用数据类型变量进行空间申请和此变量指向的对象复制,即对整个原始对象进行拷贝,产生一个独立的新对象。
可以借助重写clone()
方法或者通过对象序列化(Serializable
)实现
另外,由深拷贝性质可知,如果引用数据类型成员变量用final
修饰深拷贝将失败,没法儿重新赋值
hashCode()和equals()
之前一直以为但凡重写了equals()
就必须重写hashCode()
,但是实际上不是的:
-
在不创建类对应的散列表(
Hashset``HashMap
等等)时,两者无关; -
反之则确实需要重写,散列表中高效查找及去重会用到(定位链表/数组索引位置)
String
字符存储
Java9
之前使用char[]
,从其开始使用byte[]
:
1 | private final byte[] value; |
新版的 String 其实支持两个编码方案:Latin-1 和 UTF-16。
如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。
Latin-1 编码方案下,byte 占 1 个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间。
JDK 官方就说了绝大部分字符串对象只包含 Latin-1 可表示的字符。
如果字符串中包含的汉字超过 Latin-1 可表示范围内的字符,byte 和 char 所占用的空间是一样的
Sting
不可变的理解:
- 保存字符串的数组被
final
修饰且为私有,同时String
类没有提供或暴露修改这个字符串的方法String
类本身被final
修饰无法被继承,避免子类破坏
StringBuilder
和StringBuffer
:
- 都继承自
AbstracStringBuilder
,其没有使用final
和private
关键字,同时提供很多修改字符串的方法 StringBuffer
对方法或者调用的方法加了同步锁,线程安全;而StringBuilder
则没有,是非线程安全- 相同情况下
StringBuilder
比StringBuffer
性能稍高
JDK9
之前String
字符串+
拼接借助StringBuilder
调用append()
,之后改为动态方法makeConcatWithConstants()
拼接优化可以研究一下
String.itern()
其作用是对字符串常量池进行数据修改或者查找
JDK7
之前字符串常量池放在永久代,其中存储对象。所以itern()
方法会先去看字符串常量池里有没有这个对象,没有就创建;有就返回
JDK7
开始字符串常量池迁移到堆中,存储引用。因此itern()
会在字符串常量池查找引用,没有的话就创建一个指向堆区对应的已有字符串对象地址的引用;有则返回引用
常量折叠(Constant Folding)
编译过程中,Javac编译器会进行常量折叠代码优化:
常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
对于 String str3 = “str” + “ing”; 编译器会给你优化成 String str3 = “string”;
并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:
1.基本数据类型( byte、boolean、short、char、int、float、long、double)以及字符串常量。
2.final 修饰的基本数据类型和字符串变量
3.字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )
泛型
注意: 静态方法中无法访问类的泛型成员变量(因为静态方法加载先于类的实例化),需要用到的话可以在静态方法上声明、使用自己的泛型参数,与类的泛型参数无关
序列化与反序列化
概念
序列化: 将数据结构或对象转换成二进制字节流的过程
反序列化: 将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
序列化协议对应TCP/IP模型
应用层的表示层
使用
对于Java
自带的序列化方式,只需实现java.io.Serializable
接口或者Externalizable
接口。这里主要记录/针对前者
serialVersionUID
:
序列化号
serialVersionUID
属于版本控制的作用。
反序列化时,会检查serialVersionUID
是否和当前类的serialVersionUID
一致。
如果serialVersionUID
不一致则会抛出InvalidClassException
异常。
强烈推荐每个序列化类都手动指定其serialVersionUID
,如果不手动指定,那么编译器会动态生成默认的serialVersionUID
显式指定serialVersionUID
是在类中使用static
和final
关键字修饰一个long
类型变量,变量名为serialVersionUID
某些字段不进行序列化
可以使用transient
关键字修饰不想进行序列化的变量:
对象实例中被
transient
修饰的变量序列化会被阻止,反序列化时变量值不会持久化、恢复
transient
只能修饰变量,不能修饰类和方法transient
修饰的变量反序列化后其值会被设置为该类型的默认值- 被
static
修饰的变量,存在于方法区,无论有无transient
修饰都不会被序列化。(serialVersionUID
比较特殊,作为一致性标识,在对象实例序列化时其也会被序列化到二进制字节流,反序列化时会一并解析并做一致性判断。)