在Java中,基本数据类型、包装类型和引用数据类型是三种不同的数据类型,它们在创建方式、存储方式、默认值以及使用场景等方面存在显著差异。
一、基本数据类型(Primitive Types)
定义:
基本数据类型是指Java语言中内置的数据类型,这些类型不是对象,而是直接存储数据的值。Java中一共有8种基本类型,包括:
- 整数类型:byte、short、int、long
- 浮点类型:float、double
- 字符类型:char
- 布尔类型:boolean
#注意:String
是一个独立的类,存在于 java.lang
包中,它用于表示和处理字符串。虽然 String
类与基本数据类型的包装类一样,都是引用类型,但它并不对应于任何基本数据类型。String
类实际上是 Object
类的直接子类,因此它继承了 Object
类的所有方法,并添加了字符串操作特有的方法。
String
是一个类,因此 String
类型的变量实际上是存储了一个引用,指向在堆(Heap)内存中的 String
对象。除了 String
,其他引用数据类型还包括类(Class)、接口(Interface)和数组(Array)等。
特点:
- 不可变性:基本类型的值是不可变的,即一旦赋值,其值就不能被改变(虽然可以通过重新赋值来改变变量所持有的值,但原值本身是不可变的)。
- 直接存储:基本类型的值直接存储在栈内存中,访问速度较快。
- 默认值:在声明时未显式初始化的基本类型变量会自动被赋予默认值(如int的默认值为0,boolean的默认值为false)。
- 基本数据类型的比较是值的比较
- 值传递:在方法调用时,基本类型是按值传递的,即传递的是变量值的副本。
举例:
int a = 10; // 声明并初始化一个基本类型的int变量
int b = a; // b是a的一个副本,改变b的值不会影响a
尽管它们都存储了相同的值 10
。重要的是要注意,b = a;
是一个值的复制操作,而不是引用或指针的赋值。这意味着如果你随后修改了 a
或 b
的值,另一个变量不会受到影响,因为它们在栈上有独立的存储空间。
注意区分:当基本数据类型作为类的字段(属性)时,它们是作为对象的一部分存储在堆上的。以下是为什么基本数据类型的类属性存储在堆上的原因:
-
对象封装: 在 Java 中,类的实例是一个对象,它封装了一系列的状态和行为。一个对象的所有字段(无论是基本数据类型还是引用类型)都必须在对象的内存空间内,这样对象才能作为一个整体被处理。因此,当创建一个对象时,对象的内存空间是在堆上分配的,包括它所有的字段。
-
对象的生命周期: 对象的生命周期不由方法调用的栈帧决定,而是由垃圾回收器管理的。如果基本数据类型的字段存储在栈上,它们的生命周期将与栈帧的生命周期绑定,这会导致对象的状态和行为不一致,因为对象的某些部分可能会在方法调用结束后消失。
-
引用的一致性: 如果一个对象的字段存储在栈上,那么每次方法调用时都可能创建字段的不同副本,这会导致引用同一个对象的不同线程看到不同的状态,从而破坏线程间的数据一致性。
-
内存管理: 堆内存是 Java 虚拟机(JVM)管理对象实例的主要方式。JVM 有一个垃圾回收器(Garbage Collector, GC),它负责清理不再使用的对象实例。如果基本数据类型的字段存储在栈上,那么 JVM 将需要额外的机制来管理这些字段的内存,这将增加复杂性。
总结来说,基本数据类型的类属性存储在堆上是为了保持对象的完整性、一致性以及由 JVM 统一管理内存。当基本数据类型作为局部变量时,它们存储在栈上,因为它们是方法调用的一部分,并且它们的生命周期仅限于当前栈帧。这是两种不同场景下的内存管理策略。
例子:
public class Example {
public int number; // 基本数据类型属性
public String text; // 对象引用类型属性
public Example(int number, String text) {
this.number = number; // 存储基本数据类型值
this.text = text; // 存储对象引用
}
}
在这个例子中,当创建 Example
类的实例时:
Example example = new Example(10, "Hello");
-
example.number
的值10
是一个int
类型的基本数据类型,它的值会直接存储在Example
类实例的堆内存空间中。 -
example.text
的值"Hello"
是一个String
对象的引用(引用地址),它存储在堆内存中的另一个位置(字符串常量池中)。ex.text
实际上存储的是指向该String
对象的内存地址(即引用)。
基本数据类型和对象引用类型的主要区别在于存储方式:
- 基本数据类型:值直接存储在对象实例的内存空间中。
- 对象引用类型:存储的是指向实际对象的引用(内存地址),实际对象存储在堆内存的其他位置(字符串常量池中)
在 Java 中,基本数据类型的属性总是直接存储在它们所属的对象实例的堆内存空间中。
二、包装类型(Wrapper Classes)
定义:
包装类是将基本数据类型封装成对象的形式,以便能够使用对象的方法。Java为每一种基本类型都提供了对应的包装类,如Integer对应int,Double对应double等。
Java中的包装类型包括:
byte
-Byte
short
-Short
int
-Integer
long
-Long
float
-Float
double
-Double
char
-Character
boolean
-Boolean
#首字母都是大写
特点:
- 对象性:包装类是对象,拥有方法和字段,通过引用对象的地址来调用。
- 引用传递:包装类型是按引用传递的,传递的是对象的引用而非对象本身。(重点!!!)
- 存储位置:包装类的对象存储在堆内存中,通过栈上的引用来访问。
- 默认值:包装类型在声明时未显式初始化的变量默认值为null。
- 自动装箱与拆箱:Java提供了自动装箱(将基本类型自动转换为包装类型)和自动拆箱(将包装类型自动转换为基本类型)的机制,简化了编码工作。
- 使用场景:包装类型主要用于需要将基本数据类型作为对象处理的场景,如将基本数据类型存储在集合中。
自动装箱和自动拆箱是什么?:
自动装箱(Autoboxing)和自动拆箱(Unboxing)是Java语言提供的特性,它们允许基本数据类型(Primitive Types)和它们对应的包装类(Wrapper Classes)之间进行隐式的转换。装箱其实就是调⽤了 包装类的 valueOf() ⽅法,拆箱其实就是调⽤了 xxxValue() ⽅法
public class Test {
public static void main(String[] args) {
Integer a = new Integer(10); // 使用new关键字显式创建Integer对象
Integer b = 10; // 自动装箱,等同于Integer b = Integer.valueOf(10);
int c = b; // 自动拆箱,将Integer对象转换为int值
}
}
//注意代码的注释部分!!!
例题:
public class Test {
public static void main(String[] args) {
Integer a = 1000;
int b = 1000;
System.out.println(a == b);
System.out.println(a.equals(b));
}
}
答案:true true
补充:
-
==
运算符:- 当用于基本数据类型(如
int
)时,==
比较的是两个值的相等性。 - 当用于对象引用时,
==
比较的是两个引用是否指向内存中的同一个对象,即它们的地址是否相同。
- 当用于基本数据类型(如
-
.equals()
方法:.equals()
是Object
类中的一个方法,用于比较两个对象的值。在Integer
类中,.equals()
方法被重写,用于比较两个Integer
对象的值(而不是它们的地址)。
解释:
-
自动装箱:
Integer a = 1000;
这行代码中,1000
是一个基本类型int
的值,但是它被自动装箱成一个Integer
对象。这是Java编译器自动完成的。 -
基本类型:
int b = 1000;
这行代码定义了一个基本类型int
的变量b
,并直接赋值为1000
。
现在来解释两个System.out.println
调用:
-
System.out.println(a == b);
:- 在这里,
==
运算符用于比较两个操作数。由于a
是一个对象,而b
是一个基本类型,所以这里发生了隐式的拆箱,将Integer
对象a
转换成基本类型int
,然后比较它们的值。因此,这个表达式的结果是true
,因为a
和b
的值都是1000
。
- 在这里,
-
System.out.println(a.equals(b));
:equals
方法是Object
类中的一个方法,被Integer
类重写以比较两个Integer
对象的值。在这个例子中,b
是一个基本类型int
的值,所以在调用equals
方法时,b
被装箱成一个Integer
对象,然后比较这个对象与a
对象的值。由于a
和b
的值都是1000
,所以这个表达式的结果也是true
。
为了更好地理解上面的补充部分,我将再出一道例题:
public class Test {
public static void main(String[] args) {
Integer a = 1000;
Integer b = 1000;
System.out.println(a == b);
System.out.println(a.equals(b));
}
}
答案:false true
解释:==
比较的是两个对象的地址,因为它们是不同的对象,地址不同,所以输出 false
.equals()
方法用于比较两个 Integer
对象的值(而不是它们的地址)所以为true
图解:
注意:
在Java中,为了节省内存和提高性能,Integer
类的部分值确实使用了常量池技术。这种技术称为整数缓存或整数装箱。具体来说,Integer
类维护了一个缓存,用于存储从-128
到127
(含)的整数。这个范围可以通过JVM的系统属性java.lang.Integer.IntegerCache.high
来调整。
当您执行以下代码时:
Integer i = 10;
Integer j = 10;
由于10
在这个范围内,所以i
和j
实际上都指向常量池中的同一个Integer
对象。因此,i == j
的结果是true
。
下面是完整示例:
public class Test {
public static void main(String[] args) {
Integer i = 10;
Integer j = 10;
System.out.println(i == j); // 输出 true
}
}
当您执行这段代码时,控制台会输出true
,因为i
和j
引用了常量池中的同一个Integer
对象。
但是,如果整数值超出了这个范围,情况就不同了:
Integer i = 128;
Integer j = 128;
System.out.println(i == j); // 可能输出 false
在这种情况下,i
和j
将分别指向堆上不同的Integer
对象,因此i == j
的结果是false
。要正确比较两个Integer
对象的值,您应该使用equals
方法,而不是==
:
Integer i = 128;
Integer j = 128;
System.out.println(i.equals(j)); // 输出 true
equals
方法比较的是对象的值,而不是它们的引用。因此,即使i
和j
引用不同的对象,只要它们的值相同,equals
方法也会返回true
。
以下总结的概念来加强理解:
-
自动装箱:当您使用
Integer i = 10;
时,Java 会自动将基本数据类型int
的值装箱为Integer
对象。如果这个值在-128
到127
范围内,并且 JVM 已经创建了这个值的Integer
对象,那么它会重用这个对象。 -
常量池:Java 为
Integer
类预先分配了一个内部缓存(常量池),用于存储频繁使用的值。这个缓存是在Integer
类被加载时创建的,并且对于-128
到127
范围内的每个值,它只包含一个唯一的Integer
对象。 -
比较引用:使用
==
比较两个引用类型变量时,它检查的是这两个变量是否引用内存中的同一个对象。如果两个Integer
变量都在常量池范围内,并且值相同,它们将引用同一个对象,因此==
比较的结果为true
。 -
超出范围:如果
Integer
的值超出了-128
到127
的范围,每次装箱时都会在堆上创建一个新的Integer
对象,因此即使值相同,比较的结果也可能是false
。
总结来说,当您使用 Integer i = 10;
和 Integer j = 10;
时,因为 10
在缓存范围内,所以 i
和 j
引用了同一个 Integer
对象,因此 i == j
为 true
。但是,如果值超出了这个范围,或者您使用 new Integer(10);
明确地创建一个新的 Integer
对象,那么 i
和 j
将引用不同的对象,此时 i == j
将为 false
。在这种情况下,应该使用 i.equals(j)
来比较它们的值。
除了 Integer
类之外,以下包装类也使用了类似的常量池技术:
-
Boolean
:由于Boolean
的值只有两个(true
和false
),所以Boolean
类为这两个值提供了缓存。所有的Boolean
实例true
和false
都是通过缓存获得的。 -
Byte
:与Integer
类似,Byte
类也使用了常量池技术,因为它封装的是byte
值,范围从-128
到127
。在这个范围内的Byte
对象是通过缓存创建的。 -
Short
:Short
类同样使用了常量池技术,尽管它封装的是short
值,其范围从-128
到127
(这个范围与Integer
和Byte
的缓存范围相同)。 -
Character
:对于Character
类,它的缓存范围是从0
到127
。这是因为这个范围的字符对应于基本的ASCII字符集。
这些包装类的常量池技术主要是为了节省内存和提高性能,因为对于这些范围内的值,没有必要每次都创建新的对象实例。当自动装箱发生时,Java虚拟机会尝试从相应的常量池中获取对象的引用。
例如,以下代码:
public class Test {
public static void main(String[] args) {
Boolean b1 = true;
Boolean b2 = true;
System.out.println(b1 == b2); // 输出 true,因为它们引用同一个实例
Byte by1 = 127;
Byte by2 = 127;
System.out.println(by1 == by2); // 输出 true,因为它们引用同一个实例
Short sh1 = 123;
Short sh2 = 123;
System.out.println(sh1 == sh2); // 输出 true,因为它们引用同一个实例
Character ch1 = 'A';
Character ch2 = 'A';
System.out.println(ch1 == ch2); // 输出 true,因为它们引用同一个实例
}
}
在上面的代码中,由于值在缓存范围内,所以 ==
比较的结果为 true
。然而,如果值超出了缓存范围,结果可能就会是 false
。
三、引用数据类型(Reference Types)
定义:
除了基本类型和包装类型以外的所有类型都被称为引用数据类型,如类(Class)、接口(Interface)、数组(Array)等。引用数据类型存储的是对象在堆内存中的地址,通过这个地址来访问对象。
特点:
- 对象性:引用数据类型是对象,可以添加属性和方法。它包括类(Class)、接口(Interface)、数组(Array)等,用于表示复杂的数据结构或行为。
- 引用传递:在方法调用时,引用数据类型是按引用传递的,即传递的是对象的引用(即对象的地址)。通过栈上的引用来访问堆上的对象。
- 存储位置:引用类型的对象存储在堆内存中,而引用(即对象的地址)存储在栈内存中。
- 默认值:引用类型的默认值也是null,表示不指向任何对象。
-
使用场景:引用数据类型通常用于表示复杂的数据结构或行为,如自定义的类、接口实现等。在集合框架、文件I/O、网络通信等场景中广泛使用。
解释第2点:
测试1:
public class Test {
public static void main(String[] args) {
int[] arr1={1,2,3};
int[] arr2=arr1;
arr1=null;
System.out.println(arr2[0]);
System.out.println(arr1[0]);
}
}
答案:1,报错NullPointerException
解释:
需要注意的是,数组变量之间的复制是浅复制(shallow copy),这意味着如果数组包含引用类型的元素,那么这些引用(地址)会被复制,但它们所引用的对象本身(对象存储在堆中)不会被复制。在上面的代码中,arr2
和arr1
指向同一个数组,所以当arr1
被设置为null
时,arr2
仍然指向同一个数组,因为它并没有被设置为null
。
测试2:
public class Test {
public static void main(String[] args) {
int[] arr1={1,2,3};
int[] arr2=arr1;
arr2=new int[5];
System.out.println(arr2[0]);
System.out.println(arr1[0]);
}
}
答案:0,1
由于arr2
之前已经指向了arr1
数组,这里实际上是在创建一个新的数组并让arr2
指向它。因此,arr2
不再指向原来的arr1
数组,而是指向了这个新的数组。arr1
和arr2
指向的是不同的数组,所以它们数组里的元素不同。
总结:引用数据类型赋值传递的是对象的引用(即对象的地址),(int[ ] arr2 = arr1;是将arr1的地址复制给arr2)除了改变对象arr2的数据值(即存储到堆里面的元素)会影响arr1的值,但是改变对象arr2的引用(设置为空null或者创建一个新的对象new int[5])只要不是改变堆里面的值,都不会影响arr1,因为他们是独立的。
总结:
基本数据类型 | 包装类型 | 引用数据类型 | |
定义 | Java内置的数据类型 | 将基本数据类型封装成对象 | 除了基本类型和包装类型以外的所有类型 |
对象性 | 不是对象 | 是对象 | 是对象 |
存储位置 | 栈内存 | 堆内存(对象),栈内存(引用) | 堆内存(对象),栈内存(引用) |
默认值 | 有默认值(如0、false) | null | null |
传递方式 | 值传递 | 引用传递 | 引用传递 |
示例 | int a = 10; | Integer a = 10; (自动装箱) | String a = new String("Hello"); |
好啦,今天的干货就到这啦~ 有什么建议或疑问的小伙伴欢迎到评论区留言!