可变数据类型和不可变数据类型以及Snaphot图

序言

在上软件构造这门课时,在讲数据类型时引入了可变数据类型和不可变数据类型,这是以前未接触过的概念,以前自学Java时,仅有基本数据类型,和引用数据类型这两种概念;此外,老师还引入了Snaphot digrams,其用于描述程序运行时的内部状态,较为实用,今天“趁热打铁”,写博客记录一下。

先回顾一下Java的基本数据类型和引用数据类型。

基本数据类型

基本数据类型主要是我们最熟悉的几个变量类型:

byte、short、int、long,float,double,bool,char;

基本数据类型运算的规则:(不包含布尔类型)

  1. 自动类型提升:当容量小的数据类型的变量与大的数据类型做运算时,结果自动提升为容量大的数据类型。

    byte、char、short(这三者做运算均转为int)->int->long->->float->double。

    容量大小指的是表示属的范围的大和小,比如float的容量大于long

  2. 强制类型转换:需要使用强转符,可能导致精度损失

  3. 整数型定义时,常量存储的类型为int型,若定义变量类型为long,或容量大于int型,且常量超过int型可表示的最大范围数,编译不通过,需要加l

  4. 浮点型定义时,常量存储的类型为double类型,若定义变量类型为float,double类型向float转换,报错,常量后面需加f

整型常量:默认类型为int型; 浮点型常量:默认类型为double型。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test{
public static void main(String[] args){
long a = 2147483647; //编译可通过
long a = 2147483648; //编译不通过,显示数过大
long a = 2147483648l; //编译可通过
float f1 = 12.3; //编译不通过,因为12.3默认为double型,需要12.3加f
byte b = 12; //编译可通过
byte b = b + 1; //编译不通过,因为1被认为是int型
float f1 = b + 12.3; //编译不通过,因为12.3被认为是doble型
System.out.println(a);
}
}

引用数据类型

引用数据类型主要有:类(字符串)、接口、数组、枚举类等。其被称为引用数据类型的原因是:创建变量并赋值后,且不是直接存储的值,而是存储的是堆中创建值的地址,通过地址寻值,有点类似于C语言的指针。

举例String:

String不是基本数据类型,而是为引用数据类型,字符串。

String类型变量的使用:

String可以和8种基本数据类型做运算,只能做连接,用string类接收,运算结果仍然是String 。

二者区别对比:

类型 基本数据类型 引用数据类型
成员 byte、short、int、long,float,double,bool,char 类、接口、数组、枚举类
分配内存区域 在栈中分配内存 在堆中分配内存
是否与其他值可区分 只有值,没有ID,与其他值无法区分 既有ID,也有值
可变/不可变数据类型 全都为不可变数据类型 有些是可变的,有些是不可变的
赋值 传递的是值 传递的是地址

下面再谈谈上课所讲的可变和不可变数据类型。

不可变数据类型(Immutable types)

定义:一旦一个变量被创建且被赋初值,其值不能再改变。此外,如果如果是引用类型,也可以是不变的,即一旦确定其指向的对象,不能再被改变指向其他对象。

我们一般用final标识符,来说明这个变量的值不能再被改变。一旦一个变量被final修饰后并进行首次赋值后,编译器进行静态类型检查时,如判断 final 变量首次赋值后发生了改变,会提示错误。如图idea报错。

下面举一个不可变数据类型String的例子:

1
String str = "Hello";

此时创建了一个String类型的变量str,值为”Hello”,这里是一个局部变量,下图给出其在内存中的表示。

再执行了下面一条语句后,其变化:

1
str = str + "World";

image-20210706184856276

可以看到,其并没有改变堆里地址为0x2233存储的原来的字符串值,而是在堆里新建一个对象,其值为“HelloWorld”,然后改变栈里str的存储值。

用Snaphot图表示变化:

image-20210706190855510

不可变数据类型的优点:因为其值不能改变,不可变类型更“安全”,在其他质量指标上表现更好。其安全性会在下面与可变数据类型对比时体现出来。

缺点:使用不可变类型,对其频繁修改会产生大量的临时拷贝,需要垃圾回收。就正如上面所举的例子:改变了str的值,但“Hello”的值仍然在堆中,就需要垃圾回收。

可变数据类型

定义:可以改变值,且拥有方法可以修改自己的值。

这里举一个可变数据类型StringBuilder的例子:

1
2
StringBuilder str1 = new StringBuilder("Hello");
str1.append("World");

这里选择直接用Snaphot图表示变化:

image-20210706190817364

看到这里,可能就会产生疑惑:这里的StringBuilder和前面的String的结果不都一样吗?没有什么区别啊。但这个还需要我们深思一下。

如果是当只有一个引用指向该对象,二者没有区别。

但是注意:有多个引用的时候,差异就出现了!!!

给出上课老师举的例子:

image-20210706191223709

可以看到,变量t和s都最开始都指向同一个值“ab”,但当t修改值变为“abc”时,仅仅改变了t的指向,指向“abc”,而并未更改原来的值,s的值不变仍为“ab”;

再看变量sb和tb二者最开始都指向同一个值“ab”,但当tb改变值为“abc”时,是在原有的值上直接修改,不同于之前String类型变量t,这样一来sb也并未改变值,但其由于指向的值的改变,导致sb的值变为“abc”;这就相当危险了,看上去是对一个变量值的修改,却也同时改变了另一个变量的值,很难让人察觉出这一变化,从而产生一些副影响。这里就体现了不可变数据类型的安全性所在!

但可变数据类型还是有优点的:首先就是,我们的程序需要值的变化,其必不可少。

可变类型最少化拷贝以提高效率,减少垃圾;其次,也适合于在多个模块之间共享数据,例如全局变量。

但是我们上面提到了可变数据类型的一些“危险”,那就讲一讲如何安全的使用可变类型:

局部变量,只有一个引用,不会涉及共享,不会有危险。

但如果有多个引用(别名),使用可变类型就非常不安全。

主要办法就是:防御性拷贝,给客户端返回一个全新的对象,是要返回值的拷贝,但是新建的,地址不同,避免直接返回,传递地址,致使产生多个引用。

Snophot Diagram

Snophot图十分常用,其主要用于描述程序运行时的内部变化,如在栈中和堆中的对象、变量等等。

其优点是:直观、简洁,便于程序员之间交流,便于刻画各类变量随时间发生的变化,便于解释思路。

下面讲一讲其表示规范。

基本类型的值

其用箭头指向一个常量表示变量对这个值的引用。如图所示:

image-20210706195413812

对象类型的值

对象类型的值用一个按其类型标记的圆表示。

定义一个圆。

1
2
3
4
5
class Circle{
int x; //圆心横坐标
int y; //圆心纵坐标
int r; //圆的半径
}

如果要显示更多的细节时,可在这个圆里面写明成员变量名,用箭头指向它们的值。若想要更详细,可以标明其成员变量的类型。

image-20210706200318325

不可变对象

其在对象类型的基础上改为双线椭圆。

如之前前面所举的例子。

image-20210706190855510

可变对象

这个也较为简单,就是之前举的例子。

image-20210706190817364

不可变的引用

这里不可变的引用是指,变量指不可变。其用双线箭头表示。

1
final int y =3;

如图所示:

image-20210706200914860

同时我们还要注意:引用是不可变的,但指向的值却可以是可变的。比如说,定义了一个final StringBuilder sb,我们不能改变其指向,但其指向的值却是可改变的。

可变的引用,也可指向不可变的值,比如说,定义了一个String s,其指向一个存储在堆中不可变的值“Hello”,但我们可以改变s的指向,指向“Hello world”。

这就是关于Snophot的内容。

这些只是自己的一些浅薄的理解,若有问题,恳请批评指正。

参考资料

【1】软件构造课程PPT

【2】自学JAVA的笔记

  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2019-2022 1nvisble
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信