常用类
Posted 乌龟王八蛋
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了常用类相关的知识,希望对你有一定的参考价值。
包装类
什么是包装类?它们设计的意义是什么?
Java 是较为纯粹的面向对象设计语言,而其中存在八个原始类型,不归于类的范畴。为了践行 无物不可引用,万类皆是对象,Java 为原始类型提供了对应的引用类型(基本类型又可以称为原始类型),称其为原始类型的包装类
在写法上,除了 int、char,其它六种包装类均为首字母大写,例如 byte
与 Byte
。而 int、char 是缩写,其包装类分别是 Integer
、Character
相较于基本类型,包装类作为引用类型,可以存在 NULL
值。同时,提供了一些方法,例如
// 求最小求值
int minValue = Integer.MIN_VALUE;
// 求最大取值
int maxValue = Integer.MAX_VALUE;
// 二进制位数
int size = Integer.SIZE;
// 所占字节数
int bytes = Integer.BYTES;
需要注意,Byte、Short、Integer、Long、Float、Double 同属于 Number 数值类,Character、Boolean 不是
明确一点,所有的包装类,都是 final 不可变类
,这点与 String
是一致的,任何的修改都是新对象的创建
简单些说,引用类型的赋值,实际是获得堆内存的引用地址,而对于不可变类,任何的修改都会创建新的地址,原有的内存空间不再指向
而对于其它的可变引用类型,如数组(数组不可变的是类型、长度,而非内容),内容的修改不会导致新的数组(内存空间)创建
int[] ints1 = new int[3];
// ints 1 与 ints 2 使用了同一份内存空间
int[] ints2 = ints1;
ints1[2] = 3;
System.out.println(Arrays.toString(ints2));
/* [0, 0, 3] */
这些,需要格外注意,什么才是真正的 final
不可变,赋值与声明同步,此后不可修改
如 Integer
、String
这些不可变类,是借由 final
完成的修饰。所以,若需要修改其中的内容,必须开辟新的内存空间,这会造成不必要的浪费,也是各种缓存机制存在的必然
// Integer 类已经前缀了 final,意为不可变的类
public final class Integer extends Number implements Comparable<Integer> {}
拆箱、装箱
拆、装箱,是原始类型与引用类型之间的相互转换
装箱:基本类型转为其包装类 valueOf()
拆箱:包装类转为其基本类型 intValue()
目前,Java 已经支持了自动装箱、自动拆箱,无须再调用特定的方法进行手动拆、装箱
包装类的拆箱、拆箱 示例,仅作了解即可
/* 手动装箱 */
Integer integer1 = Integer.valueOf(12);
/* 手动拆箱 */
int intValue1 = integer1.intValue();
/* 自动装箱 */
Integer integer3 = 12;
/* 自动拆箱 */
int intValue2 = integer1;
缓存池
包装类中,存在缓存池的设置,避免对象的重复创建,以 Integer
为例
在 Integer 类中,存在一个私有静态内部类 private static class IntegerCache {}
简单的理解,Integer 类的装箱操作,会调用 valueOf()
,并开辟一块新的堆内存
无论是手动装箱,还是自动装箱,都会调用 valueOf()
,只是隐藏了这部分
若装箱后的 Integer 对象存在于 Integer 的缓存池中,则不会创建新对象,而是直接引用自缓存池 IntegerCache
Integer a1 = 120;
Integer a2 = 1200;
Integer b1 = 120;
Integer b2 = 1200;
/* == 对于引用类型,比较的是堆内存地址,Java 中也不支持运算符重载 */
System.out.println(a1 == b1);
System.out.println(a2 == b2);
/* true、false */
上述的包装类示例,违背了以往的认知,这就是缓存池在发挥作用!对于引用类型,== 是比较二者的堆内存地址,同样数值的 Integer 类型,为何会出现堆内存地址相同、相否的情况
当调用 valueOf()
时,会先判断当前创建的对象是否存在于缓存池中
public static Integer valueOf(int i) {
// 判断当前原始类型数值,是否存在于缓存池中
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
下述是 Integer 缓存池的具体实现源码
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
int h = 127;
String integerCacheHighPropValue = VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
通过一系列的示例、源码,可以很清楚的认识到缓存池。根据包装的原始类型大小,决定是否新建对象,还是从缓存池中取出相同的对象
在 Integer 中,缓存池是 -128~127,一个 byte 的取值范围。可以手动的扩充缓存池的大小,但并不推荐这样做
当然,若是直接使用 new Integer()
,可以创建一个新的 Integer 对象,但已经废弃。关键字 new,为强制创建,无视缓存池机制
值得注意的是,并非所有的包装类都存在缓存池,浮点型的 Float、Double 与 Boolean 不存在缓存池的概念。Byte、Short、Integer、Long、Character 存在缓存池,默认范围都是 -128~127
重写 equals()
同样的以 Integer 为例,探讨其中的方法重写。在 Integer
类、String
类中,对于 Object
类的 equals()
方法已经被重写
默认的 equals()
方法,对于引用类型,比较的是 变量是否引用自同一对象,判断的依据是堆内存地址。在重写之后,根据实际的数值判断
Integer a = 12;
Integer b = 12;
Integer c = new Integer(12);
/* == 不可重写,依旧是根据对象的引用地址判断 */
System.out.println(a == b);
System.out.println(a == c);
/* equals() 已被重写,根据对象的值进行判断 */
System.out.println(a.equals(b));
System.out.println(a.equals(c));
/*
true、false、true、true
*/
上述的示例可以看出
- a、b 由于
Integer
的缓存池机制,引用的是同一个对象,堆内存地址与实际数值皆一致 - c 通过关键字
new
,没有引用缓存池,而是全新的堆内存地址,但实际数值依旧保持一致 - 在
==
的判断中,不存在问题,a 与 b 相等,与 c 不相等
可是,equals()
的默认实现,是根据对象的引用地址进行的。而经过 Integer 的重写,根据实际的数值判断,以至于堆内存不同的变量在 equals() 的判断中为 true
所以,请看 Integer
类中的重写实现,与 Object
类中的默认实现,二者的区别
/* Object 类的默认 equals() 方法,其中 this 指代当前对象 */
public boolean equals(Object obj) {
return (this == obj);
}
/* Integer 重写后的 equals() 方法 */
public boolean equals(Object obj) {
if (obj instanceof Integer) {
/* Integer 类的 equals() 根据值判断二者的相等 */
return value == ((Integer)obj).intValue();
}
return false;
}
重写 hashCode()
Object 类中,也存在一个方法,hashCode(),负责返回对象的哈希值
共同的认知是,重写 equals() 的同时,必须重写 hashCode()
哈希值是根据对象的属性,在通过哈希算法生成的,在 Integer 重写之后,规则发生改变,Integer 的哈希值等于它的数值本身
@Override
public int hashCode() {
return Integer.hashCode(value);
}
值得注意的是
- 相同的对象,哈希值一定相同
- 而哈希值相同的对象,也可能不是同一个对象
// Arrays.hashCode() 也重写 hashCode(),暂时不用
int[] ints = new int[2];
System.out.println(ints.hashCode());
System.out.println(new int[2].hashCode());
/*
哈希值:2083562754、1239731077
*/
hashCode()
的默认实现,是将对象的引用地址转换为整数值。引用地址右虚拟机生成,可覆盖
可以参考 HashMap 中的键值存储形式,它允许存在重复的 hash 值,以单向链表的形式存储
对于 equals() 与 hashCode(),优先是通过对象的哈希值判断相等性。若哈希值不同,则并非同一个对象;若哈希值相同,则根据 equals() 二次判断。这样,可以达到性能与安全的平衡
若 equals()
重写,而 hashCode() 不重写,根据对象的值进行判断,则会出现,equals() 判断为 true,而 hashCode() 不相同,以至于操作失误
简单的理解为:equals()
判断为 true,则 hashCode()
必须为 true;而 hashCode()
为 true时,equals()
存在为 false 的可能
String 存储位置
String 类经常使用,所以 Java 也对它做了 “缓存” 处理
String 类是不可以变的字符数组,任何的修改都会生成新的 String 对象
在 JVM 虚拟机中,存在一个特殊的内存空间,常量池
对于普通方式创建的字符串,则放入常量池中,可以参考 Integer 类的缓存池
String a = "1";
String b = "1";
// == 用于比较对象的引用地址
System.out.println(a == b);
/* true */
当直接赋值为字符串字面量时,相同内容的新对象,也会被认为是同一个对象
String a = "1";
String c = new String("1");
System.out.println(a == c);
/* false */
关键字 new
,强制在堆内存中,创建一个新的 String 类对象,不存入常量池
值得注意的是,字符串的实际存储位置,或者说常量池位置,随着 JDK 版本迭代而变更
例如,在 JDK8.0 中,常量池已经由方法区移动至堆内存中
StringBuffer、StringBuilder
String 类是不可变的,每次修改都会创建新的对象,这样的效率并不是很高
Java 中提供了两个可变长的字符串类,StringBuffer
、StringBuilder
StringBuffer
:JDK1.0 提供,线程安全、运行效率慢
StringBuilder
:JDK5.0 提供,线程不安全、运行效率快
这两个方法中,常用的是 StringBuild,并且二者的实现是差不多的
核心在于线程的安全与否,而保证线程的同时,不可避免的降低执行效率
简单的说,StringBuffer、StringBuild 的本质也是字符数组
不同的是,这二者的字符数组,其每一个元素为一个字符串,而不是纯粹的单个字符
二者对于 String 类的改造,在于字符数组的可扩容
在对字符串做修改时,String 类是直接创建新的对象,原内存直接废弃
而 StringBuffer、StringBuilder 则是将该内存 扩容
以下是 StringBuilder 的 API
appeand(String str)
:添加 String 对象至字符数组中appeand(StringBuilder sb)
:添加 StringBuilder 对象值字符数组中delete(int start, int end)
:删除字符数组中的部分元素
可以看出,StringBuilder 使用方法 appeand()
替代了 String 类拼接字符的 +
现在,简单介绍一些,StringBuilder 对于字符数组扩容的操作
初始大小
:StringBuilder 继承抽象父类,默认字符数组长度为 16
public StringBuilder() {
super(16);
}
添加 String 对象
:添加 String 对象之前,先判断字符数组是否需要扩容
// StringBuilder 的对象添加,已经交由抽象父类完成
public AbstractStringBuilder append(String str) {
if (str == null) {
return appendNull();
}
int len = str.length();
ensureCapacityInternal(count + len);
putStringAt(count, str);
count += len;
return this;
}
字符数组扩容
:是否需要扩容,是根据添加当前 String 对象后的字符数组长度确定的
// 判断添加 String 对象是否超出长度,若超出,则扩容为之前的两倍
private void ensureCapacityInternal(int minimumCapacity) {
int oldCapacity = value.length >> coder;
if (minimumCapacity - oldCapacity > 0) {
value = Arrays.copyOf(value, newCapacity(minimumCapacity) << coder);
}
}
StringBuild 的扩容,使得无需再开辟新的内存空间
简单的理解,StringBuilder 相当于 String 类的容器
接下来,简单介绍 StringBuffer 的线程安全问题,当然,这不常用
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
上述为 StringBuffer 的字符串对象添加方法,这直接解释了线程安全的缘由
StringBuffer 在修饰符、返回值类型之间,加入了 关键字 synchronized
synchronized
:线程安全,该方法同一时间点,只允许一条线程进行操作
String 类的不可变,使得程序安全、简单且易于理解,但频繁的创建新对象,则使得效率较低
StringBuffer、StringBuilder 在则通过 扩容
,避免了频繁的内存操纵
以上是关于常用类的主要内容,如果未能解决你的问题,请参考以下文章