前端视角 Java Web 入门手册 2.1:Java Core ——基础类型与包装类型

71 阅读6分钟

很多同学会混淆强类型与静态语言的概念

  • 强类型 vs. 弱类型:区别是类型转换的严格性和一致性,强类型语言要求开发者在类型转换时显式说明,而弱类型语言可能允许隐式转换
  • 动态语言 vs. 静态语言:区别是类型绑定的时机;动态语言的类型在运行时决定,而静态语言在编译时确定

JavaScript 是弱类型的动态语言,Python 是强类型的动态语言,Java 是强类型的静态语言

基础类型

Java是一种强类型语言,拥有八种基础类型,每种类型在内存中的存储方式和用途各不相同。基础类型直接存储在栈内存中,具有高效的性能和固定的内存占用

类型描述位数默认值
整型byte``short``int``long8, 16,32, 640
浮点型float``double32, 640.0
字符型char16\u0000
布尔型boolean-false
public class PrimitiveTypesExample {
    public static void main(String[] args) {
        byte b = 100;
        short s = 10000;
        int i = 100000;
        long l = 10000000000L;
        
        float f = 3.14F;
        double d = 3.141592653589793;
        
        char c = 'J';
        boolean bool = true;
        
        System.out.println("Byte: " + b);
        System.out.println("Short: " + s);
        System.out.println("Int: " + i);
        System.out.println("Long: " + l);
        System.out.println("Float: " + f);
        System.out.println("Double: " + d);
        System.out.println("Char: " + c);
        System.out.println("Boolean: " + bool);
    }
}

包装类型

包装类型(Wrapper Types)是对基础类型的封装,提供了对象形式的基础类型。每种基础类型都有一个对应的包装类,如int对应Integerdouble对应Double

基础类型包装类型位数
byteByte8
shortShort16
intInteger32
longLong64
floatFloat32
doubleDouble64
charCharacter16
booleanBoolean-

Java的集合框架如ArrayListHashMap等设计基于泛型(Generics),而泛型类型参数必须是对象类型(Reference Types),无法使用基础类型(Primitive Types),这就需要使用对应的包装类型来存储和操作基础类型的数据

import java.util.ArrayList;
import java.util.List;

public class WrapperTypesExample {
    public static void main(String[] args) {
        // 使用包装类型存储基础类型数据
        List<Integer> integerList = new ArrayList<>();
        integerList.add(10); // 自动装箱:int -> Integer
        integerList.add(20);
        integerList.add(30);
        
        // 迭代输出
        for (Integer num : integerList) {
            System.out.println(num);
        }
    }
}

自动装箱与自动拆箱

Java 5 引入了自动装箱(Autoboxing)和自动拆箱(Unboxing)机制,使基础类型和包装类型之间的转换更加便捷,极大地提升了 Java 的可用性和代码的简洁性

自动装箱是指将基础类型自动转换为对应的包装类型的过程。Java编译器会在需要对象的场景下自动完成这种转换,无需手动调用构造器或静态方法。

public class AutoboxingExample {
    public static void main(String[] args) {
        int primitiveInt = 10;
        // 自动装箱:int 转换为 Integer
        Integer wrapperInteger = primitiveInt;
        System.out.println("Wrapper Integer: " + wrapperInteger);
    }
}

自动拆箱是指将包装类型自动转换为对应的基础类型的过程。当需要基础类型的场景时,Java会自动进行解包处理。

public class UnboxingExample {
    public static void main(String[] args) {
        Integer wrapperInteger = 20;
        // 自动拆箱:Integer 转换为 int
        int primitiveInt = wrapperInteger;
        System.out.println("Primitive Int: " + primitiveInt);
    }
}

Java 作为面向对象语言,只要没有基本数据类型就不会面对拆箱/装箱问题了,这可以说是 Java 延续 C++ 带来的一个遗留问题,Ruby、Scala 等语言中所有数据类型都是对象。在 Java 中有些场景不得不使用包装类对象

  • 包装类最常见的使用就是在集合中,因为集合不允许存储基本类型的数据,只能存储引用类型的数据
  • 不止集合,泛型整体不允许使用基本数据类型作为类型参数,只能使用包装类作为类型参数
  • 反射只能对类对象进行操作,而基本数据类型没有对应的类对象,只能使用包装类对象
  • 对基本数据类型进行 null 值处理,需要使用包装类对象

为了防范 NPE(NullPointerException)问题《阿里巴巴 Java 开发手册》建议

  • POJO 类属性必须使用包装数据类型
  • RPC 方法的返回值和参数必须使用包装数据类型

在大多数情况下,拆箱和装箱操作不会对程序的性能产生显著的影响,因为现代的JVM对这些操作进行了优化。但是,在某些特定的场景(高并发或大数据)下这些操作可能会导致程序的性能下降,尽可能地避免使用不必要的装箱和拆箱操作,而是直接使用基本类型

包装类型的缓存机制

为优化性能和内存使用,某些包装类型实现了缓存机制。当创建特定范围内的包装对象时,Java会返回相同的对象实例,避免重复创建相同值的对象。

Integer 缓存

Integer 类实现了对象缓存机制,对于数值在-128到127范围内的Integer实例,Java 会缓存这些对象,避免重复创建。

public class IntegerCacheExample {
    public static void main(String[] args) {
        Integer a = 100;
        Integer b = 100;
        System.out.println(a == b); // 输出 true,指向同一缓存对象

        Integer c = 200;
        Integer d = 200;
        System.out.println(c == d); // 输出 false,超出缓存范围,创建了不同的对象
    }
}

Character 缓存

Character 类也实现了类似的缓存机制,默认缓存范围为'\u0000''\u007F'(0 到 127 的Unicode值)。

  • 在高频使用的数值或字符范围内,依赖缓存机制可以减少内存开销和提升性能
  • 超出缓存范围时,Java会创建新的对象实例
public class CharacterCacheExample {
    public static void main(String[] args) {
        Character x = 'A';
        Character y = 'A';
        System.out.println(x == y); // 输出 true,指向同一缓存对象

        Character m = '中';
        Character n = '中';
        System.out.println(m == n); // 输出 false,超出缓存范围,创建了不同的对象
    }
}

Boolean 缓存

Boolean 类只缓存两个实例:Boolean.TRUEBoolean.FALSE

public class BooleanCacheExample {
    public static void main(String[] args) {
        Boolean bool1 = Boolean.TRUE;
        Boolean bool2 = Boolean.TRUE;
        Boolean bool3 = new Boolean(true);

        System.out.println(bool1 == bool2); // 输出 true,指向同一缓存对象
        System.out.println(bool1 == bool3); // 输出 false,新创建的对象与缓存对象不同
    }
}

比较基础类型与包装类型的相等性

在 Java 中基础类型和包装类型的比较需要谨慎处理,尤其是在使用==运算符和.equals()方法时。误用可能导致逻辑错误或意外的行为

使用 == 比较

  • 基础类型:使用==运算符比较的是数值本身,比较结果基于值的相等性
  • 包装类型:使用==运算符比较的是对象引用,只有在引用指向同一个对象时,才返回true
public class EqualityComparison {
    public static void main(String[] args) {
        // 基础类型比较
        int a = 100;
        int b = 100;
        System.out.println(a == b); // 输出 true

        // 包装类型比较
        Integer c = 100;
        Integer d = 100;
        System.out.println(c == d); // 输出 true (在缓存范围内)

        Integer e = 200;
        Integer f = 200;
        System.out.println(e == f); // 输出 false (超出缓存范围)
    }
}

使用 .equals() 比较

  • 基础类型:基础类型不支持.equals()方法,因为它们不是对象
  • 包装类型.equals()方法比较的是对象的值,适用于比较包装类型的数值相等性
public class EqualsComparison {
    public static void main(String[] args) {
        Integer a = 100;
        Integer b = 100;
        System.out.println(a.equals(b)); // 输出 true

        Integer c = 200;
        Integer d = 200;
        System.out.println(c.equals(d)); // 输出 true
    }
}