Java数据的基本(原始)类型和引用类型的特点差别

本文作为“Java数据类型”一文的补充https://blog.csdn.net/cnds123/article/details/110517272

Java的数据类型可以分为基本类型(primitive types)和引用类型(reference types)两大类。在实际编程中,要根据需求选择合适的数据类型,并注意数据类型的转换和运算规则。

Java数据的基本(原始)类型和引用类型的特点差别_第1张图片

基本类型包括八种: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本身。示意图示如下:

Java数据的基本(原始)类型和引用类型的特点差别_第2张图片

对于引用类型,变量赋值是将一个引用赋给另一个变量。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现在指向的是两个不同的对象。示意图示如下:

Java数据的基本(原始)类型和引用类型的特点差别_第3张图片

需要注意的是,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数据的基本(原始)类型和引用类型的特点差别_第4张图片

参数传递: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数据的基本(原始)类型和引用类型的特点差别_第5张图片

接下来,我们来看一个引用类型的例子:

当在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 对象时,垃圾回收器才可能回收这个对象的内存。】

示意图如下:

Java数据的基本(原始)类型和引用类型的特点差别_第6张图片

无论变量是基本类型还是引用类型,作为参数传递给方法的值都会被复制以供被调用的方法使用。对于基本变量,变量的值被传递给方法。对于引用变量,它是一个引用。

为加深认识,下面再补充两个参数传递例子

一个基本类型的例子:

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

你可能感兴趣的:(Java学习,java,开发语言)