本文作为“Java数据类型”一文的补充https://blog.csdn.net/cnds123/article/details/110517272
Java的数据类型可以分为基本类型(primitive types)和引用类型(reference types)两大类。在实际编程中,要根据需求选择合适的数据类型,并注意数据类型的转换和运算规则。
基本类型包括八种:byte, short, int, long, float, double, char, boolean。
这些类型的数据直接存储在内存中,它们的值是实际的数据。【https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html】
数据类型 |
大小/位 |
可表示数据范围 |
默认值 |
byte (字节型) |
8 |
-128~127 |
0 |
short (短整型) |
16 |
-32768~32767 |
0 |
int (整型) |
32 |
-2147483648~2147483647 |
0 |
long (长整型) |
64 |
-9223372036854775808~9223372036854775807 |
0 |
float (单精度) |
32 |
-3.4E38~3.4E38 |
0.0 |
double (双精度) |
64 |
-1.7E308~1.7E308 |
0.0 |
char (字符) |
16 |
0~255 |
'\u0000' |
boolean (布尔) |
- |
true或false |
false |
引用类型则包括类(class)、接口(interface)、数组(array)等。Java的引用数据类型(Reference Types)包括以下几种:
类(Class):这是最基本的引用类型,例如用户自定义的类,以及Java库中的类,如String、Scanner、ArrayList等。
【说明:
String:是一类,用于创建和操作字符串。
Scanner:这是Java的一个工具类,它用于获取用户的输入,或者从文件和字符串中读取数据。
ArrayList:这是Java的一个实现了List接口的类,是一个动态数组,可以自动增长和缩小。它可以存储任何类型的对象,包括null。】
接口(Interface):接口是一种引用类型,它是方法的集合。一个类可以实现(implement)一个或多个接口。
数组(Array):数组是一种引用类型,它可以存储固定数量的同一类型的值。
枚举(Enum):枚举是一种特殊的类,它有一组预定义的常量。
引用类型的变量存储的是一个地址,这个地址指向内存中的一个对象。这个对象可以是类的实例,也可以是数组。
Java基本类型(primitive types)和引用类型(reference types)的特点差别:
☆ 存储位置:基本类型的数据直接存储在栈内存中,存储的是实际的值。而引用类型的数据存储在堆内存中,变量实际上存储的是一个指向对象的地址或者引用,而不是对象本身。
计算机内存中的栈(Stack)【栈内存(Stack Memory)】和堆(Heap)【堆内存(Heap Memory)】主要是根据内存分配和管理方式来进行区分的。栈和堆的管理是由操作系统和编程语言的运行时(Runtime)共同控制的。它们各自有不同的用途和特点,栈内存是事先分配好的,遵循后进先出原则;而堆内存是动态分配的,可以根据程序需求进行调整。两者都是由操作系统和编程语言的运行时共同管理的。
☆ 变量赋值:对于基本类型,变量赋值是值的复制,而对于引用类型,变量赋值是引用的复制。
对于基本类型,变量赋值是直接将一个值赋给另一个变量。例如:
public class Main {
public static void main(String[] args) {
int a = 10;
int b = a;
System.out.println("Initial value of b: " + b);
a = 20;
System.out.println("Value of b after changing a: " + b);
}
}
输出:
b的初值: 10
改变a后b的值: 10
解析如下:
在这个例子中,
int a = 10;
int b = a;
我们首先声明了一个变量a并赋值为10,然后我们声明了一个变量b并将a的值赋给b。这时,变量赋值是值的复制,b的值也是10。然后,如果我们改变a的值,例如:
a = 20;
这时,b的值仍然是10,因为b是在赋值时获取的a的值,而不是a本身。示意图示如下:
对于引用类型,变量赋值是将一个引用赋给另一个变量。StringBuilder是Java中的一个可变字符串类,它允许你在不创建新的字符串对象的情况下修改字符串内容。
例如:
public class Main {
public static void main(String[] args) {
StringBuilder a = new StringBuilder("Hello");
StringBuilder b = a;
System.out.println("b的初值:" + b);
a.append(" world");
System.out.println("改变a后b的值: " + b);
a = new StringBuilder("Hi");
System.out.println("改变a的引用后b的值:" + b);
}
}
输出:
b的初值: Hello
改变a后b的值: Hello world
改变a的引用后b的值: Hello world
解析如下:
在这个例子中,
StringBuilder a = new StringBuilder("Hello");
StringBuilder b = a;
我们首先声明了一个StringBuilder对象a,然后我们声明了一个StringBuilder对象b并将a的引用赋给b。这时,a和b指向的是同一个对象。然后,如果我们通过a来修改这个对象的状态,例如:
a.append(" world");
这时,b的状态也会被改变,因为b和a指向的是同一个对象。
然而,如果我们改变a的引用,例如:
a = new StringBuilder("Hello world");
这时,b的状态并没有被改变,因为b和a现在指向的是两个不同的对象。示意图示如下:
需要注意的是,StringBuilder是可变的,所以它的内容可以被修改。与之相对的是String类,它是不可变的,一旦创建就不能被修改。
【StringBuilder和String都是Java中的字符串类,都是引用类型。
StringBuilder是可变的字符串类,它允许你在不创建新的字符串对象的情况下修改字符串内容。你可以通过调用StringBuilder的方法来追加、插入、删除和修改字符串内容。StringBuilder是一个可变的字符序列,它的长度和内容都可以被修改。
String是不可变的字符串类,一旦创建就不能被修改。当你对一个String对象进行修改时,实际上是创建了一个新的String对象。这是因为String类的设计是为了保证字符串的不可变性,这样可以提高字符串的安全性和性能。】
例子:
public class Main {
public static void main(String[] args) {
String a = "Hello World";
String b = a;
System.out.println("b的值: " + b);
a = "Hi";
System.out.println("a的值: " + a);
System.out.println("b的值: " + b);
}
}
输出:
b的值: Hello
a的值: Hi
b的值: Hello
在Java中字符串(String)是引用类型,为何改变了a的值b 没变?解析:
在Java中,字符串是不可变的。当你创建一个字符串对象时,它的值不能被修改。当你对字符串进行修改时,实际上是创建了一个新的字符串对象,而原始的字符串对象保持不变。
在你的代码中,当你将字符串"a"赋值给变量"b"时,实际上是将"b"指向了同一个字符串对象"Hello"。然后,当你将字符串"a"修改为"Hi"时,实际上是创建了一个新的字符串对象"Hi",并将变量"a"指向了这个新的字符串对象。但是,变量"b"仍然指向原始的字符串对象"Hello",所以它的值没有改变。
这是因为字符串在Java中被设计为不可变的,这样可以提高字符串的安全性和性能。示意图示如下:
☆ 参数传递:Java中的参数传递方式确实只有按值传递。无论是基本类型还是引用类型,都是将实际值或引用值复制一份传递给方法。
当说基本类型是按值传递时,意思是将实际的值复制一份传递给方法。如果方法中修改了这个复制的值,原始的值是不会被改变的。
当说引用类型是按值传递时,实际上是将引用的值(也就是对象在内存中的地址)复制一份传递给方法——形参实参指向同一个对象。这意味着方法中可以通过这个复制的引用来修改原始对象的状态,但是如果方法中改变了这个复制的引用(例如指向一个新的对象),原始的引用是不会被改变的。
让我们通过一些例子来理解Java中的参数传递方式。
首先,我们来看一个基本类型的例子:
//基本数据类型作为方法参数被调用
public class PassByValue {
public static void main(String[] args){
int msg = 100;
System.out.println("调用方法前msg的值:"+ msg); //100
fun(msg);
System.out.println("调用方法后msg的值:"+ msg); //100
}
public static void fun(int temp){
temp = 0;
}
}
输出:
调用方法前msg的值:100
调用方法后msg的值:100
解释:基本数据类型变量,调用方法时作为参数是按数值传递的,temp方法接收的是mag的副本。temp 是 fun 方法的参数,它是一个基本类型的变量,存储在栈上,当方法结束时,栈帧被自动移除,相关的内存空间也就被释放了。
示意图如下:
接下来,我们来看一个引用类型的例子:
当在Java中传递引用类型时,传递的是引用的值,也就是对象在内存中的地址的副本。这意味着形参和实参指向的是同一个对象,所以在方法内部可以通过这个副本引用来修改原始对象的状态。但是,如果在方法内部改变了这个副本引用的指向,比如将其指向一个新的对象,那么原始的引用并不会改变。
//引用数据类型作为方法参数被调用
class Book{
String name;
double price;
public Book(String name,double price){
this.name = name;
this.price = price;
}
public void getInfo(){
System.out.println("图书名称:"+ name + ",价格:" + price);
}
public void setPrice(double price){
this.price = price;
}
}
public class PassByReference{
public static void main(String[] args){
Book book = new Book("Java开发指南",66.6);
book.getInfo(); //第一次getInfo(), 图书名称:Java开发指南,价格:66.6
fun(book); //调用了fun()方法,设置新价格
book.getInfo(); //第二次getInfo(),图书名称:Java开发指南,价格:99.9
}
public static void fun(Book temp){
temp.setPrice(99.9); //设置新价格
}
}
输出:
图书名称:Java开发指南,价格:66.6
图书名称:Java开发指南,价格:99.9
解释:
段代码定义了一个Book类和一个PassByReference类。Book类有两个属性:name和price,分别表示书的名称和价格。Book类还有一个构造函数,用于创建对象时初始化这些属性,以及两个方法:getInfo()用于打印书的信息,setPrice(double price)用于设置书的价格。
PassByReference类包含main方法,这是Java程序的入口点。在main方法中,首先创建了一个Book对象book,初始化时书名为"Java开发指南",价格为66.6。然后调用book.getInfo()方法打印出书的信息。
接下来,main方法调用了fun(Book temp)方法,并将book对象作为参数传递给它。在fun方法内部,调用了temp.setPrice(99.9),这个方法调用实际上改变了传入的Book对象的price属性,将其设置为99.9。
由于Java中的对象引用是按值传递的,所以temp是book的一个副本,但它们都指向同一个Book对象。因此,当temp.setPrice(99.9)被调用时,它实际上改变了book对象的状态。
最后,当控制返回到main方法并再次调用book.getInfo()时,打印出的信息显示书的价格已经被改变为99.9。
temp 是 fun 方法的参数,它是一个引用类型的变量。当 fun 方法执行完毕,栈帧被移除,temp 变量的生命周期结束。在这个例子中,即使 temp 的生命周期结束了,Book 对象仍然通过 main 方法中的 book 变量被引用,因此它不会被垃圾回收。【只有当程序结束或者没有任何引用指向 Book 对象时,垃圾回收器才可能回收这个对象的内存。】
示意图如下:
无论变量是基本类型还是引用类型,作为参数传递给方法的值都会被复制以供被调用的方法使用。对于基本变量,变量的值被传递给方法。对于引用变量,它是一个引用。
为加深认识,下面再补充两个参数传递例子
一个基本类型的例子:
public class Test {
public static void change(int value) {
value = 55;
}
public static void main(String[] args) {
int value = 22;
System.out.println("Before: " + value);
change(value);
System.out.println("After: " + value);
}
}
在这个例子中,我们在main方法中定义了一个变量value,并将其传递给change方法。在change方法中,我们试图修改value的值。然而,当我们运行这个程序时,会发现value的值并没有被改变。这是因为value是按值传递的,change方法接收的是value的一个副本,对这个副本的修改不会影响到原始的value。
一个引用类型的例子:
public class Test {
public static void change(StringBuilder builder) {
builder.append(" world");
}
public static void main(String[] args) {
StringBuilder builder = new StringBuilder("Hello");
System.out.println("Before: " + builder);
change(builder);
System.out.println("After: " + builder);
}
}
在这个例子中,StringBuilder是可变的字符串类。我们在main方法中定义了一个StringBuilder对象,并将其传递给change方法。在change方法中,我们通过这个对象的引用来修改它的状态。当我们运行这个程序时,会发现builder的状态确实被改变了。这是因为builder是按值传递的,change方法接收的是builder的一个副本,这个副本和原始的builder指向的是同一个对象,所以通过这个副本可以修改原始对象的状态。
然而,如果我们试图在change方法中改变builder的引用,例如:
public static void change(StringBuilder builder) {
builder = new StringBuilder("Hello world");
}
这时,当我们运行程序时,会发现builder的状态并没有被改变。这是因为change方法接收的是builder的一个副本,这个副本和原始的builder指向的是同一个对象,但是当我们在change方法中改变这个副本的引用时,原始的builder的引用并没有被改变,它们现在指向的是两个不同的对象。
总之,无论是基本类型还是引用类型,Java中的参数传递方式都是按值传递。但是由于基本类型和引用类型的特性不同,它们在方法参数传递时的行为看起来是不同的。
☆ 生命周期:基本类型的生命周期随着它所在的函数或者对象的生命周期,当函数返回或者对象被销毁时,基本类型的变量也会被销毁。而引用类型的对象,即使没有任何引用指向它,也不会立即被销毁,需要等待垃圾回收器的回收(时间点是不确定的,依赖于垃圾回收器的实现)。
基本类型(Primitive Types)
基本类型包括int、long、short、byte、float、double、boolean和char。这些类型的变量直接存储实际的值,并且通常位于栈(Stack)内存上。栈内存主要用于存储方法调用的上下文和局部变量。
基本类型的生命周期如下:
当基本类型的变量在方法中被声明时,它的生命周期开始。
变量的值存在于方法的栈帧中,这个栈帧对应于调用该方法的线程。
当方法执行完毕,栈帧被移除,所有在该栈帧中的局部基本类型变量也随之被销毁。
对于类的成员变量(字段)来说,基本类型的生命周期与其所属的对象相同。
基本类型的内存空间回收情况:
基本类型的内存空间不需要显式回收,因为它们存储在栈上,当方法结束时,栈帧被自动移除,相关的内存空间也就被释放了。
引用类型(Reference Types)
引用类型包括类(Class)、接口(Interface)、数组(Array)等。引用类型的变量存储的是对象的引用(地址),而对象本身存储在堆(Heap)内存上。
引用类型的生命周期如下:
当创建一个引用类型的对象时,它的生命周期开始。
对象存储在堆内存中,而对象的引用(变量)可以存储在栈上(作为局部变量)或者堆上(作为另一个对象的成员变量)。
如果对象没有任何引用指向它(即不可达),那么它就成为垃圾回收(Garbage Collection, GC)的候选对象。
引用类型的内存空间回收情况:
Java有一个垃圾回收机制来自动管理堆内存的回收。当对象不再被引用时,垃圾回收器可以决定回收这些对象的内存空间。
垃圾回收器的运行通常是不可预测的,它决定何时执行回收操作,这取决于多种因素,如可用内存、GC算法等。
开发者可以通过调用System.gc()来建议虚拟机执行垃圾回收,但是这个调用并不保证垃圾回收器立即执行。
总结来说,基本类型的内存管理是自动的,随着方法的结束而结束。而引用类型的内存管理则依赖于垃圾回收器。
小结
这种分类的意义在于,基本类型和引用类型在内存管理、参数传递等方面有着不同的行为。基本类型的变量在栈内存中分配,而引用类型的对象在堆内存中分配。在参数传递时,基本类型是传值,也就是将实际的值传递过去;而引用类型是传地址,也就是将对象在内存中的地址传递过去。这种区别决定了它们在编程中的使用方式和效率。
附录
Java中的基本数据类型和引用数据类型的区别https://developer.aliyun.com/article/1123194
Java 到底是值传递还是引用传递?https://www.zhihu.com/question/31203609/answer/50992895
(Java)基本与引用数据类型(Primitive vs. Reference Data Types)https://blog.csdn.net/cnds123/article/details/134266737