Data Type And Type Checking
1.编程语言中的数据类型
类型和变量
-
一个类型是一系列值的集合,这些集合可以抽象出一个相同的特点,并且可以相互实现计算
- 例如:
- 布尔类型:true or false
- 整形:1,2,3…
- 浮点数类型:1.2,2.4,5.55…
- 字符串类型:“hello”,“world”
- 例如:
-
变量:一个被命名的地址,存储了某种类型的值
JAVA中变量指向的地址不一定存储的就是我们想要的值,也有可能是值的“遥控器”
Java中的数据类型
Java是一个纯粹的面向对象的编程语言,因此在Java中几乎所有变量都是一个对象,但是为了高效性,可移植性和轻便性,Java也有一些基本数据类型
- 基本数据类型:int,double,boolean,char…
- 基本数据类型存储在栈上
- 每个基本数据类型都有对应的包装类
- 包装类和基本数据类型直接可以自动装包和拆包
- 对象数据类型:String,BigInteger,BigDecimal…
- 对象数据类型存储在堆上
- 定义的对象变量应该是一个引用,也就是上文说到的"遥控板"
- 引用是存储在栈上的
- 对象数据类型在Java中是单继承的,并且有一个最终父类:Object
- 继承关系的层次结构如图1
虽然在栈上开辟空间存储数据以及销毁数据要快于在堆上,不过在现代结构的优化下,在堆上和栈上存储的代价几乎相同
2.动态 Vs. 静态数据类型检测
类型转换
int a = 2;
double a = 2;
int a = (int)18.7
double a = (double)2/3;int a = 18.7;
String a = 1;
结果:以上代码中1-4显示编译通过,6-7编译错误
向大的类型中存储小的值,会发生自动类型转换,不会发生问题;但是向小的类型中存储大的值,就会发生精度丢失,因此会报错,需要显示的指定,是否要丢失这部分精度
静态检测和动态检测
- 静态检测:在代码运行之前发现bug
- 动态检测:在代码运行时发现bug
- 无检测:语言不帮助你检测,自己找bug
三种检测方式的效率:静态>>动态>>无检查
静态检测
在编译阶段发现错误,避免将错误带入到运行阶段,可提高程序正确性与健壮性
运行阶段发现bug会带来更多的时间代价
检测内容:
- 语法错误,例如:多了一个逗号或奇怪的单词
- 类名/方法名错误,例如:
Math.abd(-1)
- 参数数目错误,例如:
Math.abs(20, -10)
- 参数类型错误,例如:
Math.abs("hello")
- 返回类型错误,实际的返回类型和方法要求的不能隐式转换
动态检测
在运行阶段发现错误
检测内容:
- 非法的参数值,实参类型不能隐式转换为形参类型
- 非法的返回值,返回类型和变量类型不能隐式转换
- 越界:遍历数组、集合的时候索引超出了它们的容量
- 空指针:对象的引用指向null
动态与静态的区别:静态检查只考虑编译阶段能确定的“值”;动态检查关心运行阶段能确定的“值”
3.可变性与不可变性
什么是赋值?
赋值是指,在内存空间中开辟一个空间,写入特定值,并且把变量和这块空间相关联
基本数据类型 Vs. 对象数据类型
- 基本数据类型:在栈上开辟空间,只与栈相关联
- 对象数据类型:在栈上开辟空间存储引用,在堆上开辟空间存储对象
引用就像一个遥控板,而对象则是电视机,可以在任何地方使用遥控板控制电视机,而不需要背着电视到处移动
变与不变
int a = 10;
a = 20;String s = "hello";
s = "world";
改变的方式有两种:改变变量和改变值
- 改变变量:改变变量的指向,让它指向另一块空间
- 改变值:改变变量指向的内存空间中的值,而不改变其指向
在编程中应该更多的使用immutable的数据,这样能大大提高程序的安全性
什么时候是哪种改变?
- 改变值:基本数据类型都是改变值,可变对象数据类型访问其堆上的空间改变其中的值
- 改变变量:对象数据类型改变其引用的值
还可以使用
final
关键字修饰变量,被修饰的变量其本身不可变。对于基本数据类型,其本身就是值,因此值不可变
对于对象数据类型,其本身实际上是"遥控板"即引用,因此引用本身不可变
不变对象 Vs. 可变对象
- 不变对象:引用指向的值不可变
- 可变对象:拥有方法可以修改堆上数据
String类型就是一个不可变类型,不能对字符串本身进行增删的操作
StringBuilder则是一个可变类型,可以对其指向的数据进行增加删除的操作
可变类型的优势
- 可变类型可以最少化拷贝以提高效率
- 可以获得更好的性能
- 适用于多个模块之间共享数据
不可变类型的优势
- 不可变类型更"安全"
4. 防御式拷贝
-
试想以下场景:向一个方法中传入一个可变对象,这是一件相当危险的事。不同于基本数据类型和不可变数据类型,传入一个可变对象,相当于把堆上数据的权柄全部交给了这个方法,客户端的数据可以随意被远端程序员更改,虽然社会是充满真善美的,但是我们还是喜欢由自己掌握自己的命运。
-
再试想以下场景:我重生成为了一个后端程序员,每天勤勤恳恳写代码,有一天我一如既往的打开电脑准备开启码代码的一天,发现我的数据被改了,在小小的电脑里挖呀挖呀挖,焦头烂额找不出错误,最后把代码发给了gpt,少不了一番自嘲。原来是你的一个方法里,向客户端返回了一个可变对象,客户端拿着这个对象到处霍霍,把值给霍霍乱了。
以上两个场景都是可变对象可能带来的安全隐患,贸然将数据权柄交给它人是一件相当莽撞的事情。为了解决这个问题,最好的方式就是使用不可变对象。还有一种方式就是采用所谓的防御式拷贝,简单来说就是创建一个新的可变类型对象,并且把原始对象的数据拷贝到新的堆内存上,然后将新的对象返回或传入。
- 大部分时候这个拷贝不会被客户端修改,可能造成大量的内存浪费
- 直接使用不可变对象,节省了频繁拷贝的代价
- 使用可变类型最好的方法:一个堆内存只有与一个引用相关联。
5.复杂数据类型:数组和集合
数组
-
数组是一个连续的组,其中存储相同数据类型的变量
-
Java中数组和c++中的不一样,Java中数组的数据也存储在堆上
-
数组名也是一个引用
-
数组中的元素通过索引访问
集合
List
List和数组很像,不同的是List的容量可以变化
List还可以对对其中的数据进行增删查改
Set
无索引,无法通过索引访问元素
Set集合中不存在相同值的元素
Map
Map中存放的是一对又一对的键值对,通过key寻找value
Map中的元素也不能通过索引访问
Map中只能每个键值只能存在一个
迭代器
迭代器是一个可以逐步遍历结合的对象
通过迭代器不能改变集合中对象的值
如果集合发生了增加或删除元素,那么之前的迭代器会失效
6.总结
- 类型检查
- 类型检查可以帮助提升程序的健壮性,可以帮助找出程序的错误
- 类型检查是简单易懂的
- 类型检查帮助你更好的维护代码
- 可变性与不可变性
- 可变数据具有高效性
- 不可变数据具有安全性
- 如何在效率和安全中选择一个合适的尺度,需要通过具体的要求进行分析
- 关键的设计准则在于,使用尽量多的不可变对象和不可变引用