Skip to content

数据类型与包装类

字数
5355 字
阅读时间
24 分钟

数据类型

Java 作为强类型语言,能够承载多种数据类型。这些属性是构建代码的基本单位,决定了变量能够存储何种类型的数据。Java 的数据类型分为两类——基本数据类型与引用数据类型:

基本数据类型

基本数据类型是 Java 中最基础的数据存储单位,它们直接存储数据值,而不是引用。Java 提供了 8 种基本数据类型,分为四类:

  1. 整数类型
    • byte
      • 大小:1 字节(8 位)
      • 范围:-128 到 127
      • 用途:节省内存时使用,适合存储小范围的整数。
    • short
      • 大小:2 字节(16 位)
      • 范围:-32,768 到 32,767
      • 用途:比 byte 范围更大,但仍较为节省内存。
    • int
      • 大小:4 字节(32 位)
      • 范围:-2^31 到 2^31-1
      • 用途:最常用的整数类型,适合大多数场景。
    • long
      • 大小:8 字节(64 位)
      • 范围:-2^63 到 2^63-1
      • 用途:需要存储更大范围的整数时使用,需在数值后加 L 或 l 标记。
  2. 浮点类型
    • float
      • 大小:4 字节(32 位)
      • 范围:约 ±3.40282347E+38F
      • 用途:节省内存时使用,适合存储小范围的浮点数,需在数值后加 F 或 f 标记。
    • double
      • 大小:8 字节(64 位)
      • 范围:约 ±1.79769313486231570E+308
      • 用途:默认的浮点类型,适合大多数需要小数的场景。
  3. 字符类型
    • char
      • 大小:2 字节(16 位)
      • 范围:0 到 65,535(Unicode 字符)
      • 用途:存储单个字符,用单引号 '' 包裹。
  4. 布尔类型
    • boolean
      • 大小:未明确定义(通常为 1 位)
      • 范围:true 或 false
      • 用途:表示逻辑真伪,用于条件判断。

引用数据类型

引用数据类型不直接存储数据值,而是存储数据的引用(即内存地址)。这些类型适合存储复杂的数据结构,如对象、数组、接口等。

包装类

包装类是什么

在 Java 中,每个基本数据类型都有一个对应的包装类。包装类是一个类,内部包含一个实例变量,用于存储对应的基本数据类型的值。此外,包装类还提供了一些静态方法、静态变量和实例方法,以便更方便地操作数据。

以下是基本数据类型及其对应的包装类:

基本类型包装类
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
booleanBoolean
charCharacter

为什么需要包装类

  1. 对象操作的需求
    Java 中有很多代码(如容器类 ArrayListHashMap 等)只能操作对象,而不能操作基本数据类型。为了能够使用这些代码,需要将基本数据类型转换为对应的包装类对象。

  2. 提供更多功能
    包装类提供了许多有用的方法,例如将字符串转换为数值、数值比较、进制转换等。这些方法可以方便地对数据进行操作。

  3. 支持泛型
    Java 的泛型(如 List<T>Map<K, V>)只能使用引用类型,不能使用基本数据类型。包装类使得基本数据类型可以用于泛型。

装箱与拆箱

为了在基本数据类型和包装类之间灵活转换,Java 引入了**装箱(Boxing)拆箱(Unboxing)**的概念。

  • 装箱:将基本类型转换为包装类的过程。

  • 拆箱:将包装类型转换为基本类型的过程。

Java 5 以后引入了自动装箱和拆箱技术,可以直接将基本类型赋值给引用类型,反之亦可。

各个包装类都可以与其对应的基本类型相互转换,每种包装类都有一个静态方法 valueOf(),接受基本类型,返回引用类型,也都有一个实例方法 xxxValue(),返回对应的基本类型。

java
Boolean bObj = Boolean.valueOf(true);  // 装箱
boolean b = bObj.booleanValue();       // 拆箱

Integer iObj = Integer.valueOf(10);    // 装箱
int i = iObj.intValue();               // 拆箱

Double dObj = Double.valueOf(10.0);    // 装箱
double d = dObj.doubleValue();         // 拆箱

Character cObj = Character.valueOf('a');  // 装箱
char c = cObj.charValue();                // 拆箱

Byte byObj = Byte.valueOf((byte) 10);  // 装箱
byte by = byObj.byteValue();           // 拆箱

Float fObj = Float.valueOf(10.0f);     // 装箱
float f = fObj.floatValue();           // 拆箱

Short sObj = Short.valueOf((short) 10);  // 装箱
short s = sObj.shortValue();             // 拆箱

Long lObj = Long.valueOf(10L);         // 装箱
long l = lObj.longValue();             // 拆箱

装箱和拆箱是如何实现的

Java 的自动装箱和拆箱是通过编译器在编译时插入相应的字节码指令来实现的。以下是一个简单的 Java 代码示例及其对应的字节码:

java
public class Main {
    public static void main(String[] args) {
        Integer i = 10;  // 自动装箱
        int n = i;       // 自动拆箱
    }
}

以Java 8为例,main方法对应的字节码如下:

java
 0 bipush 10 // bipush 是 push byte 的缩写,用于将一个字节(-128 到 127)的常量值压入操作数栈中。
 2 invokestatic #2 <java/lang/Integer.valueOf> // invokestatic 用于调用静态方法。
 5 astore_1 // astore_1 是 store reference into local variable 1 的缩写, 用于将栈顶的 Integer 对象存储到局部变量表(Local Variable Table)的第 1 个位置(索引为 1)
 6 aload_1 // aload_1 是 load reference from local variable 1 的缩写,从局部变量表的第 1 个位置加载 Integer 对象到操作数栈。
 7 invokevirtual #3 <java/lang/Integer.intValue> 
10 istore_2 // istore_2 是 store int into local variable 2 的缩写,将栈顶的 int 值存储到局部变量表的第 2 个位置(索引为 2)
11 return

可以看到,java的自动装箱与拆箱本质上正是使用了静态方法valueOf()与实例方法xxxValue()来实现的。

注意事项

  1. 空指针异常(NullPointerException)
    自动拆箱时,如果包装类对象为 null,会抛出 NullPointerException
java
    Integer num = null;
    int value = num;  // 抛出 NullPointerException
  1. 性能开销
    装箱和拆箱会引入额外的性能开销,尤其是在大量操作时(大量的创建包装类与调用方法)。应尽量避免在性能敏感的代码中频繁使用。

  2. 缓存机制 (jvm篇会详解) Java 对部分包装类(如 IntegerLong)的常用值(-128 到 127)进行了缓存,以提高性能。

java
    Integer a = 100;
    Integer b = 100;
    System.out.println(a == b);  // true(缓存范围内的值)
    
    Integer c = 200;
    Integer d = 200;
    System.out.println(c == d);  // false(缓存范围外的值)

包装类的共同特点

各个包装类的共同点包括都重写了Object类中的方法,都实现了Comparable接口,都继承了Number抽象类,都是不可变的等。

所有的包装类都重写了 Object 类中的以下方法:

  • boolean equals(Object obj)
    Object 类的默认实现是比较对象的地址(即引用是否相同),equals应该反映的是对象间的逻辑相等关系,所以所有包装类都重写了该实现,实际比较用的是其包装的基本类型值

    Java 8 中 Integer 类的 equals 方法实现:

    java
        public boolean equals(Object obj) {
            if (obj instanceof Integer) {
                return value == ((Integer)obj).intValue();
            }
            return false;
        }

    Float 类的 equals 方法实现:

    java
        public boolean equals(Object obj) {
            return (obj instanceof Float)
                && (floatToIntBits(((Float)obj).value) == floatToIntBits(value));
        }

    作用:比较两个 Float 对象的二进制表示是否完全相同。 原因:由于浮点数计算存在精度问题,数学上相等的两个浮点数在计算机中的二进制表示可能不同。因此,Float 类的 equals 方法通过 floatToIntBits() 将 float 的二进制表示转换为 int,再进行比较。

            Float f1 = 0.01f;
            Float f2 = 0.1f * 0.1f;
            System.out.println(f1.equals(f2));  // false
            System.out.println(Float.floatToIntBits(f1));  // 1008981770
            System.out.println(Float.floatToIntBits(f2));  // 1008981771

  • int hashCode()
    hashCode 方法返回一个对象的哈希值。哈希值是一个 int 类型的数,由对象中一般不变的属性映射得来,用于快速对对象进行区分、分组等。哈希值的特点包括:

    • 一个对象的哈希值不能改变。
    • 相同对象的哈希值必须一样。
    • 不同对象的哈希值一般应不同,但这不是必需的,可以有对象不同但哈希值相同的情况。

    hashCode 和 equals 的关系

    • 规则 1:如果两个对象的 equals 方法返回 true,则它们的 hashCode 必须相同。
    • 规则 2:如果两个对象的 equals 方法返回 false,则它们的 hashCode 可以相同,也可以不同,但应尽量不同。
    • 默认实现Object 类的 hashCode 默认实现是将对象的内存地址转换为整数。
    • 重写要求:如果子类重写了 equals 方法,则必须同时重写 hashCode 方法。这是因为 Java API 中的许多类(尤其是容器类)依赖于这一行为。

    包装类都重写了 hashCode 方法,根据其包装的基本类型值计算哈希值。对于 ByteShortInteger 和 Character哈希值就是其内部值

    以Byte类型为例,其代码为:

    @Override
    public int hashCode() {
        return Byte.hashCode(value);
    }
    
    public static int hashCode(byte value) {
        return (int)value;
    }

    对于Boolean类型,其hashCode代码为:

    @Override
    public int hashCode() {
        return Boolean.hashCode(value);
    }
    
    /**
         * Returns a hash code for a {@code boolean} value; compatible with
         * {@code Boolean.hashCode()}.
         *
         * @param value the value to hash
         * @return a hash code value for a {@code boolean} value.
         * @since 1.8
         */
    public static int hashCode(boolean value) {
        return value ? 1231 : 1237;
    }

    根据基类类型返回了两个不同的质数(即只能被1和自己整除的数),质数用于哈希时不易冲突。(质数在计算机中的作用可以看这篇质数

    对于Long类型,hashCode代码为:

    @Override
    public int hashCode() {
        return Long.hashCode(value);
    }
    
    /**
         * Returns a hash code for a {@code long} value; compatible with
         * {@code Long.hashCode()}.
         *
         * @param value the value to hash
         * @return a hash code value for a {@code long} value.
         * @since 1.8
         */
    public static int hashCode(long value) {
        return (int)(value ^ (value >>> 32));
    }

    Long类型的哈希值是高32位和低32位进行异或操作后的结果。 通过将高 32 位和低 32 位进行异或操作,可以将 long值的所有 64 位信息压缩到 32 位的哈希值中,使得哈希值的分布更加均匀,减少哈希冲突的概率。

    对于Float类型,hashCode代码为:

    @Override
    public int hashCode() {
        return Float.hashCode(value);
    }
    
    /**
         * Returns a hash code for a {@code float} value; compatible with
         * {@code Float.hashCode()}.
         *
         * @param value the value to hash
         * @return a hash code value for a {@code float} value.
         * @since 1.8
         */
    public static int hashCode(float value) {
        return floatToIntBits(value);
    }

    Float是将float的二进制表示看作int作为哈希值。

    对于Double类型,hashCode代码为:

    @Override
    public int hashCode() {
        return Double.hashCode(value);
    }
    
    /**
         * Returns a hash code for a {@code double} value; compatible with
         * {@code Double.hashCode()}.
         *
         * @param value the value to hash
         * @return a hash code value for a {@code double} value.
         * @since 1.8
         */
    public static int hashCode(double value) {
        long bits = doubleToLongBits(value);
        return (int)(bits ^ (bits >>> 32));
    }

    Double类型首相将double的二进制表示看作long,然后再按long计算hashCode.


  • String toString()
    返回对象的字符串表示形式。

        Integer a = 10;
        System.out.println(a.toString());  // "10"

  • 实现了 Comparable 接口

    所有的包装类都实现了 Comparable 接口,这意味着它们可以进行比较和排序。Comparable 接口定义了一个方法:

    • int compareTo(T o)
      用于比较当前对象与另一个对象的大小。

    接口只有一个方法CompareTo,当前对象与参数对象进行比较,在小于、等于、大于参数时,应分别返回-1、0、1.

    各个包装类的实现基本都是根据其基本类型值进行比较,对于Boolean,false小于true。对于Float和Double,存在和equals方法一样的问题,0.010.010.01和0.1∗0.10.1*0.10.1∗0.1相比的结果并不为0.

    通过实现 Comparable 接口,包装类可以直接用于排序(如 Collections.sort())和比较操作。


  • 继承了 Number 抽象类

    除了 Boolean 和 Character,其他包装类(如 IntegerDoubleFloat 等)都继承了 Number 抽象类。Number 类定义了一些用于将数值转换为基本数据类型的方法:

    java
    public abstract class Number implements java.io.Serializable {
    
        public abstract int intValue();
        public abstract long longValue();
        public abstract float floatValue();
        public abstract double doubleValue();
        public byte byteValue() {
            return (byte)intValue();
        }
        public short shortValue() {
            return (short)intValue();
        }
        /** use serialVersionUID from JDK 1.0.2 for interoperability */
        private static final long serialVersionUID = -8742448824652078965L;
    }

    通过继承 Number 类,包装类可以方便地将数值转换为不同的基本数据类型。


  • 不可变性(Immutable)

    所有的包装类都是不可变的(Immutable),这意味着一旦创建了一个包装类对象,它的值就不能被修改。如果需要修改值,必须创建一个新的对象。

    • 所有包装类都声明为了final,不能被继承
    • 内部基本类型值是私有的,且声明为了final
    • 没有定义setter方法
    Integer a = 10;
    a = a + 5;  // 创建了一个新的 Integer 对象,值为 15

    不可变性的优点

    • 线程安全:不可变对象可以在多线程环境中安全地共享,无需额外的同步机制。
    • 简化设计:不可变对象的行为更可预测,减少了出错的可能性。
    • 缓存支持:由于不可变性,Java 可以对包装类的常用值(如 Integer 的 -128 到 127)进行缓存,提高性能。

  • 静态工厂方法 valueOf()
    所有包装类都提供了静态方法 valueOf(),用于将基本类型或字符串转换为包装类对象。

    java
        Integer a = Integer.valueOf(10);
        Integer b = Integer.valueOf("10");

  • 常量池缓存
    部分包装类(如 IntegerLong)对常用值(-128 到 127)进行了缓存,以减少对象的创建开销。

    java
        Integer a = 100;
        Integer b = 100;
        System.out.println(a == b);  // true(缓存范围内的值)

    在Java 8中IntegervalueOf()方法的具体实现如下:

    java
    // IntegerCache.low = -128
    // IntegerCache.high 可通过JVM参数 -XX:AutoBoxCacheMax=<size> 设置,必须大于等于127,默认为127
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

    其中IntegerCache是一个私有静态内部类,其实现如下:

    java
    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];
    
        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;
    
            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);
    
            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }
    
        private IntegerCache() {}
    }

    IntegerCache表示Integer缓存,其中的cache变量是一个静态Integer数组,在静态初始化块中被初始化,默认情况下,保存了-128~127共256个整数对应的Integer对象。

    valueOf代码中,如果数值位于被缓存的范围,则直接从IntegerCache中获取已预先创建的Integer对象,只有不在缓存范围时,才通过new创建对象。

    通过共享常用对象,可以节省内存空间,由于Integer是不可变的,所以缓存的对象可以安全的被共享。Boolean、Byte、Short、Long、Character都有类似的实现。这种共享常用对象的思路,叫享元模式,英文叫Flyweight,即共享的轻量级元素。


  • 类型转换方法
    包装类提供了将字符串转换为基本类型的方法(如 Integer.parseInt()Double.parseDouble())。

    java
        int i = Integer.parseInt("10");
        double d = Double.parseDouble("10.5");

自动拆装箱的触发

示例:

java
public class Main {
    public static void main(String[] args) {         
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;
        Integer d = 3;
        Integer e = 321;
        Integer f = 321;
        Long g = 3L;
        Long h = 2L;         
        System.out.println(c==d);
        System.out.println(e==f);
        System.out.println(c==(a+b));
        System.out.println(c.equals(a+b));
        System.out.println(g==(a+b));
        System.out.println(g.equals(a+b));
        System.out.println(g.equals(a+h));
    }
}

output:
System.out.println(c==d);               // true
System.out.println(e==f);               // false
System.out.println(c==(a+b));           // true
System.out.println(c.equals(a+b));      // true
System.out.println(g==(a+b));           // true
System.out.println(g.equals(a+b));      // false
System.out.println(g.equals(a+h));      // true

大家一定对System.out.println(c==(a+b))这段代码的结果有很大疑惑,System.out.println(c==(a+b))对应的字节码如下:

java
 91 aload_3         // 将c压入操作数栈
 92 invokevirtual #10 <java/lang/Integer.intValue>    // 自动拆箱
 95 aload_1         // 将a压入操作数栈
 96 invokevirtual #10 <java/lang/Integer.intValue>    // 自动拆箱
 99 aload_2         // 将b压入操作数栈
100 invokevirtual #10 <java/lang/Integer.intValue>    // 自动拆箱
103 iadd            // 计算a+b的值并将结果压入操作数栈
104 if_icmpne 111 (+7)   // 比较 c 和 a+b 的值
107 iconst_1
108 goto 112 (+4)
111 iconst_0
112 invokevirtual #9 <java/io/PrintStream.println>
115 getstatic #8 <java/lang/System.out>

==运算符的两个操作数都是包装器类型的引用时,则比较的是引用地址,而如果其中有一个操作数是表达式(即包含算术运算符)则比较的是数值(即会触发自动拆箱过程),从对应的字节码也可以看出调用了intValue()方法,触发了自动拆箱,比较它们的数值是否相等,因此c==(a+b)的结果为true

System.out.println(c.equals(a+b))对应的字节码如下:

java
118 aload_3     // 将c压入操作数栈
119 aload_1     // 将a压入操作数栈
120 invokevirtual #10 <java/lang/Integer.intValue> // 自动拆箱
123 aload_2     // 将b压入操作数栈
124 invokevirtual #10 <java/lang/Integer.intValue> // 自动拆箱 
127 iadd        // 计算a+b的值并将结果压入操作数栈
128 invokestatic #2 <java/lang/Integer.valueOf>    // 将a+b的值的自动装箱
131 invokevirtual #11 <java/lang/Integer.equals>   // 调用equals方法
134 invokevirtual #9 <java/io/PrintStream.println>
137 getstatic #8 <java/lang/System.out>

从字节码可以看出,c.equals(a+b)首先触发自动拆箱,又触发了自动装箱,再调用equals方法,而所有的包装类都重写了Object类中的equals方法,equals用于判断当前对象和参数传入的对象是否相同,Object类的默认实现是比较地址,它和比较运算符(==)的结果是一样的。

equals应该反映的是对象间的逻辑相等关系,所以包装类都重写了该实现,实际比较用的是其包装的基本类型值,对于Integer类,其equals方法代码如下(Java 8):

java
public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

因此,c.equals(a+b)的结果为true.

System.out.println(g==(a+b))对应的字节码如下:

java
140 aload 7       // 将g压入操作数栈
142 invokevirtual #12 <java/lang/Long.longValue>    // 自动拆箱
145 aload_1       // 将a压入操作数栈 
146 invokevirtual #10 <java/lang/Integer.intValue>  // 自动拆箱
149 aload_2       // 将b压入操作数栈
150 invokevirtual #10 <java/lang/Integer.intValue>  // 自动拆箱
153 iadd          // 计算a+b的值并将结果压入操作数栈
154 i2l           // 将 a+b 的结果从 int 转换为 long
155 lcmp          // 比较 g 和 a+b 的值
156 ifne 163 (+7) 
159 iconst_1
160 goto 164 (+4)
163 iconst_0
164 invokevirtual #9 <java/io/PrintStream.println>
167 getstatic #8 <java/lang/System.out>

g==(a+b)的比较过程和c==(a+b)类似,多了一个类型转换,都是比较数值,因此结果为true.

System.out.println(g.equals(a+b))对应的字节码如下:

java
170 aload 7        // 将g压入操作数栈
172 aload_1        // 将a压入操作数栈
173 invokevirtual #10 <java/lang/Integer.intValue>  // 自动拆箱
176 aload_2        // 将b压入操作数栈
177 invokevirtual #10 <java/lang/Integer.intValue>  // 自动拆箱
180 iadd           // 计算a+b的值并将结果压入操作数栈
181 invokestatic #2 <java/lang/Integer.valueOf>     // 自动装箱
184 invokevirtual #13 <java/lang/Long.equals>       // 调用equals比较
187 invokevirtual #9 <java/io/PrintStream.println>
190 getstatic #8 <java/lang/System.out>

对于Long类,其equals方法代码如下(Java 8):

java
public boolean equals(Object obj) {
    if (obj instanceof Long) {
    	return value == ((Long)obj).longValue();
    }
    return false;
}

从字节码可以看出,g.equals(a+b)也进行了自动拆箱、装箱过程,但是a+b装箱后为Integer类型,而gLong类型,因此调用equasl会输出false.

System.out.println(g.equals(a+h))对应的字节码如下:

java
193 aload 7           // 将g压入操作数栈
195 aload_1           // 将a压入操作数栈
196 invokevirtual #10 <java/lang/Integer.intValue> // 自动拆箱
199 i2l               // 从a的数值从 int 转换成 long
200 aload 8           // 将h压入操作数栈
202 invokevirtual #12 <java/lang/Long.longValue>  // 自动拆箱
205 ladd              // 计算a+h的值并将结果压入操作数栈
206 invokestatic #5 <java/lang/Long.valueOf>       // 自动装箱
209 invokevirtual #13 <java/lang/Long.equals>      // 调用equals比较
212 invokevirtual #9 <java/io/PrintStream.println>

g.equals(a+h)相比于g.equals(a+b)多了一步类型转换,a+h装箱后的类型为Long,因此结果为true.

贡献者

文件历史