抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

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厂商

上述基础类型对应的包装类型

  • ByteShortIntegerLongCharacterFloatDoubleBoolean
  • 将基本类型转换成包装类型即“装箱(boxing)”,反之“拆箱(unboxing)

基本类型与包装类型的区别

  • 基础类型不具备对象特性,所以:
    • 在需要使用Object的地方(比如泛型)必须是包装类型
    • 且作为类,包装类型可以为null==对于基本类型是比较值,对于包装类型则是比较内存地址,包装类型对象之间值的比较用equals()方法
  • 基本数据类型的局部变量放在Java虚拟机的局部变量表/栈中,未被static修饰的成员变量放在堆内存中;而对于包装类,实例放在堆,引用放在栈。一般基本类型更高效。
  • 基础类型相比包装类型占用空间小

自动拆装箱与包装类缓存机制

1
2
Integer num1 = 711;
int num2 = num1;

说明

这里前一条语句发生自动装箱,将int转换为Interger,实际上使用的是包装类的ValueOf()方法 --> Integer.ValueOf();后一条语句发生自动拆箱,调用intValue()方法 --> num1.intValue()

自动拆装箱发生

  • 将基本数据类型放入集合类(装)
  • 基本类型与包装类型大小比较(拆)
  • 包装类型的运算(拆)
  • 三目运算符中,第二、三位操作数有包装类时就拆
  • 方法参数/返回值类型不匹配,自动拆装

包装类缓存机制

大部分包装类(除了FloatDouble)使用缓存机制来提升性能:

在一定范围内利用自动装箱(ValueOf()方法)创建包装类对象时,会从缓存(常量池)中寻找指定数值(一般来说各个包装类默认创建了一个静态数组cache[],对应范围,所以只要在这个范围内理论上都有),未找到则新建对象并返回引用;找到则直接返回引用

  • 这个范围:
    • ByteShotrIntegerLong:-128 ~ 127
    • Character: 0 ~ 127
    • Boolean: True、False
  • 不在这个范围自动装箱或者直接new的包装类都会新建一个对象,且不会放入缓存,也不会复用(存储于堆空间)

由于包装类缓存机制,从缓存得来的对象引用比较时==(比地址)会为true,新建而来则为false,所以包装类对象实例之间值的比较最好用equals()方法(包装类默认重写了equals()方法,先看类型,再比较值)

另外,频繁拆装箱会影响系统性能,尽量避免不必要的拆装箱操作


高精度计算

虽说floatdouble可以满足大部分开发需要,但是精度在某些场景下仍然不够,这时候就需要BigDecimal来进行浮点数的创建运算:

构造

为防止精度丢失,推荐使用BigDecimal(Sting val)构造方法或者BigDecimal.valueOf(double val)静态方法(此方法内部使用DoubletoString()方法,采用双精度浮点数(double)截断策略)来创建对象(实际上BigDecimal支持使用intdoublelongString来作为构造方法的参数,但是使用double的构造方法结果有一定的不可预知性)

运算

运算方法:

  • 加 --> add()
  • 减 --> subtract()
  • 乘 --> multiply()
  • 除 --> divide(),且最好尽量使用3参数版本(num除数,scale保留小数位数,roundingMode保留规则),以针对出现无限循环小数的情况
  • 大小比较 --> compareTo()(忽略精度scale) ,-1即小于,0是等于,1为大于

注意点

  • 最好不使用equals()方法比较值(会比较精度,1.0和1就会不相等)
  • setScale()方法设置保留小数位、保留规则(建议为RoundingMode.HALF_EVEN,四舍六入五成双)
  • 格式化可结合NumberFormat类使用

长整数计算

既然有高精度浮点运算,那么在整数区域也有长整数BigInteger类型,来处理IntegerLong位数不够的情况:

构造

构造方法:BigInteger类支持的构造方法有不少,可以传入byte []char []Stringintint []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补齐

注:

  1. 移位操作实际上支持的类型只有intlong,编译器对shortbytechar类型位移前都会先转换为int再操作
  2. 移位位数超过数值所占位数时会先求余%再移,int对应32位,long对应64

方法重写

重写是子类对父类方法的重新改造,外部样子(原本的框架)不变,改变内部逻辑
重写规范:

  • 构造方法无法重写
  • 父子方法名、参数列表必须相同
  • 子类方法访问修饰符范围大于等于父类
  • 子类方法返回值类型应和父类方法相等或者更小
  • 子类方法抛出的异常范围小于等于父类
  • 如果父类方法访问修饰符为private/final/static,那么子类就无法重写此方法

方法的可变长参数机制

Java5提供了可变长参数,允许在调用方法时传入不定长度的参数:

语法

在参数类型后跟一个...即可:

1
2
3
4
5
   public void changeArgs(int ... args){
for(int arg : args){
System.out.println(arg);
}
}

注意

  1. 一个方法有且仅能有一个可变长参数
  2. 可变长参数只能作为此方法的最后一个参数
  3. 遇到方法重载时优先匹配固定参数方法,其次再是可变长参数
  4. 其本质上还是基于数组实现,可变长参数编译之后会转换成一个数组

拷贝

前置说明

值传递:接收实参的拷贝,即创建副本,对形参修改不影响实参
引用传递: 接收实参所引用的对象在堆中的地址,不会创建副本,对形参修改影响实参

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不可变的理解:

  1. 保存字符串的数组被final修饰且为私有,同时String类没有提供或暴露修改这个字符串的方法
  2. String类本身被final修饰无法被继承,避免子类破坏

StringBuilderStringBuffer

  • 都继承自AbstracStringBuilder,其没有使用finalprivate关键字,同时提供很多修改字符串的方法
  • StringBuffer对方法或者调用的方法加了同步锁,线程安全;而StringBuilder则没有,是非线程安全
  • 相同情况下StringBuilderStringBuffer性能稍高

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是在类中使用staticfinal关键字修饰一个long类型变量,变量名为serialVersionUID

某些字段不进行序列化

可以使用transient关键字修饰不想进行序列化的变量:

对象实例中被transient修饰的变量序列化会被阻止,反序列化时变量值不会持久化、恢复

  • transient只能修饰变量,不能修饰类和方法
  • transient修饰的变量反序列化后其值会被设置为该类型的默认值
  • static修饰的变量,存在于方法区,无论有无transient修饰都不会被序列化。(serialVersionUID比较特殊,作为一致性标识,在对象实例序列化时其也会被序列化到二进制字节流,反序列化时会一并解析并做一致性判断。)

评论