数据类型
Java中有八大数据类型:
- boolean,1bit(比特位)
- byte,1字节
- char,两字节
- short,两字节
- int,4字节
- float,4字节
- long,8字节
- double,8字节
以上称为基本数据类型,Java为这些基本类型都提供了对应的包装类型。基本类型与包装类型之间的赋值会进行自动拆箱与装箱:
public static void main(String[] args) {
Integer x = 2;
int y = x;
System.out.println(y);
}
包装类型的缓存池,
Integer:
// IntegerCache.low = -128
// IntegerCache.high = 127
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
如果调用Integer.valueOf方法,传入的值如果命中范围[-128,127],那么会直接返回缓冲区中对应的内容;反之,不在这个范围内,创建一个新的对象:
public static void main(String[] args) {
Integer num01 = Integer.valueOf(125);
Integer num02 = Integer.valueOf(125);
System.out.println(num01 == num02); // true
}
这里需要提一嘴,== 符号比较的是虚拟内存地址是否相同,即是否是同一块内存区域。
(虚拟内存地址这个概念,具体参见操作系统相关文章)
如果,你好奇缓冲区的实现的话,可以看一下源码:
private static class IntegerCache {
static final Integer[] cache;
static Integer[] archivedCache;
{
int size = (high - low) + 1; // size = 256
if (archivedCache == null || size > archivedCache.length) {
Integer[] c = new Integer[size];
int j = low;
for(int i = 0; i < c.length; i++) {
c[i] = new Integer(j++);
}
archivedCache = c;
}
cache = archivedCache;
}
}
静态初始化块,这个东西,都明白吧。。。
逻辑也很简单,无非就是从 -128开始,一直遍历到127,填满这个数组。。。
编译器会在缓冲区的范围内自动调用ValueOf这个静态方法:
public static void main(String[] args) {
Integer x = 10;
var task = new TimerTask() {
@Override
public void run() {
Integer y = 10;
System.out.println(x == y); // true
}
};
Timer timer = new Timer();
timer.schedule(task, 3000); // 3s后执行
}
并不是所有的包装类型都会有缓存池,Double和Float就没有,取范围值的话,小数不可穷尽。
其他包装类型缓冲池的范围可自行查看源码。
字符串(String)对象
public final class String {
private final byte coder; // 字符串编码类型
// java8及之前:private final char value[];
private final byte[] value;
}
String类被带有final修饰符,表示不可继承。内部维护一个不可变的内容数组,并且使用private修饰,这样可保证String对象不可变。
不可变的好处?
- 可以缓存hash值,相等的字符串对应的hash值一定是相同的。
- 可以建立字符串缓存池(String Pool),如果一个字符串对象已经被创建过,那么直接复用即可。
- 线程安全
为什么Java8之后的版本要使用byte[]?
节约内存空间,提高内存利用率。
char类型的数组中的元素占两字节,如果当前的字符串内的字符的范围在ASCII码表内,那么仅仅只用单字节即可搞定,使用char[]就造成了内存空间的浪费。
byte[]中的元素占一个字节,可以根据coder属性来确定单个字符所占的字节数,有效的利用内存空间。
String、StringBuilder、StringBuffer的区别?
String:不可变,线程安全,适用于字符串不会频繁变更的场景
StringBuilder:内部维护了一个字节数组,不是线程安全的,适用于大量字符串拼接的场景。
StringBuffer:内部和StringBuilder一样的,只不过内部的内部的方法加上了 synchronized,是线程安全的。
(对于字符串的操作场景大都是单线程的,所以StringBuffer基本用不上)
String.intern()?
这是一个被native修饰的方法,底层是C或者C++实现,所以,无法得知源码。
该方法的作用就是先将创建一个和自己相等的实例放入字符串缓冲池(String Pool),然后返回缓冲池中对应的引用。
public static void main(String[] args) {
// 创建字符串对象,假设它的内存地址是:123
String s1 = new String("sssssssss");
// 在缓冲池中创建与s1相等的实例,并返回实例的引用
String s2 = s1.intern();
// s1的引用地址和s2的引用地址不相同,所以是false
System.out.println(s1 == s2); // false
}
如果是显式的使用双引号声明的字符串对象,那么默认会执行添加缓存池的操作:
public static void main(String[] args) {
// 创建字符串对象,假设它的内存地址是:123
String s1 = new String("sssssssss");
// 在缓冲池中创建与s1相等的实例,并返回实例的引用
String s2 = s1.intern();
// s1的引用地址和s2的引用地址不相同,所以是false
System.out.println(s1 == s2); // false
String s3 = "sssssssss";
// 都引用的是缓冲池中的对象,所以会相等
System.out.println(s3 == s2); // true
}
一些概念
关于方法的参数传递
这里必须记住一个概念,无论参数是否是对象,传递的都是变量值,只不过非基本类型的变量传递的是一个引用值而已,但也是传值。
float与double,int与long
浮点数字面量默认是double,整数字面量默认的是int:
public static void main(String[] args) {
int a = 10;
long b = 9999999999L; // 和明显,不加L或l的话,9999999999超过除了int的范围
float c = 1.1F; // 不加F或f的话,编译器报错
double d = 1.1;
}
隐式类型转换
+= 、-=、/=、*= 这些运算符都会进行隐式的类型转换:
public static void main(String[] args) {
short a = 1;
a += 1; // 相当于 a = (short)(a + 1)
System.out.println(a);
}
访问权限修饰符
public:可以被任何类访问。
protected:可以被同一包下的其他类访问,也可以被子类访问(即使子类不在同一包中)。
默认:没有修饰符的话,默认为包级别,只能被同一包中的其他类访问。
private:只能在定义它的类的内部访问,外部无法直接访问。
接口和抽象类
使用频率上来说的话,接口的使用频率绝对是要大于抽象类的,这在我们平常的开发中也可以看到。
从设计角度出发,抽象类强调的是一种 IS-A 的关系,而接口的话敲掉的是一种 LIKE-A 的关系。
使用上来说,一个类可以实现多个接口,但不能继承多个抽象类。
接口的字段是 static + final 的,成员方法只能是 public ,而抽象类没有这个限制。
重写和重载
方法的重写存在于类的继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。
重写的注意事项:
- 子类方法的访问权限必须大于等于父类方法。
- 子类方法的返回类型必须是父类方法返回类型或者其子类型
重载针对是统一类中,方法名相同,参数类型、个数、位置至少有一者不同。(方法重载与返回值无关)
基类(Object)提供的一些通用方法
equals():
作用嘛,就是判断两个对象是否相等,更准确的说,是两个对象的字段值是否相等。当然,Object提供的默认实现,采用了 == ,如果我想判断两个对象是否相同,那大可不必使用equals,直接使用 == 不是更好嘛,所以我们得重写这个方法。关于这个方法的实现原则,有四点:自反性、对称性、传递性、一致性、与null的比较。这些网上都可以搜索得到,而且的话,编辑器可以自动化的生成,不需要我们操心。
hashCode():
该方法就是用于计算一个对象的hash值的。那这个hash值有什么用呢?可以用来判断两个对象是否相等。
等一下,那equals()也可以判断啦,为什么要多此一举?
主要是为了方便实现类似以HashSet,HashMap这样的集合实现类,只需要进行一次hash计算,就可以知道元素是否在集合中,而如果说使用equals,需要遍历集合中的每一个元素,时间复杂度从O(1)变成了O(N)。
这里还需要注意,两个对象的hash值相等,但是equals可能返回false;但是,如果两个对象的equals返回true,那么两个对象的hash值一定是一样的。
这就是为什么重写了hashCode和hahsCode的原因。
toString():
返回对象的字符串表达形式,编辑器也提供了重写的功能
clone():
该方法被native标记,不是用Java代码编写的。
当然,我们也可以实现自己的对象克隆逻辑,但是最好的是去调用父类的方法,毕竟性能高。
深拷贝和浅拷贝
深拷贝 => 完整的复制对象中的属性值,不存在相同指针的情况。
浅拷贝 => 复制对象中的值和引用,存在相同指针的情况。
基本上就这些了吧,剩下的都是一些比较高级的机制。。。
2024.10.19
writeBy kaiven