常用类

Posted 乌龟王八蛋

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了常用类相关的知识,希望对你有一定的参考价值。

包装类

什么是包装类?它们设计的意义是什么?

Java 是较为纯粹的面向对象设计语言,而其中存在八个原始类型,不归于类的范畴。为了践行 无物不可引用,万类皆是对象,Java 为原始类型提供了对应的引用类型(基本类型又可以称为原始类型),称其为原始类型的包装类

在写法上,除了 int、char,其它六种包装类均为首字母大写,例如 byteByte。而 int、char 是缩写,其包装类分别是 IntegerCharacter

相较于基本类型,包装类作为引用类型,可以存在 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 不可变,赋值与声明同步,此后不可修改

IntegerString 这些不可变类,是借由 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 中提供了两个可变长的字符串类,StringBufferStringBuilder

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 在则通过 扩容,避免了频繁的内存操纵

以上是关于常用类的主要内容,如果未能解决你的问题,请参考以下文章

swift常用代码片段

# Java 常用代码片段

# Java 常用代码片段

IOS开发-OC学习-常用功能代码片段整理

21个常用代码片段

js常用代码片段(更新中)