4 数据类型和类型检验
4.1数据类型基本知识,静态/动态类型检查
Java中的数据类型:
分为基本数据类型和对象数据类型两种,根据Java的约定,基本类型是小写开头,而对象类型以大写字母开头
Java中的内存管理机制:
- 栈:栈是一片内存区域,存储的是局部变量,凡是定义在方法中的都是局部变量(方法外的是全局变量);for循环内部定义的也是局部变量;只有先加载函数才能进行局部变量的定义,所以方法先进栈,然后再定义变量,变量有自己的作用域,一旦离开作用域,变量就会被释放。
- 堆:存储的是数组和对象(数组也是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象),实体用于封装数据,而且是封装多个(实体的多个属性),如果一个数据消失,这个实体也没有消失,还可以用,所以堆是不会随时释放的。虽然不会被释放,但是如果没有指针指向某个对象,它会被当成垃圾,由垃圾回收机制不定时的收取。
- 垃圾回收机制:Java中有独有的垃圾回收机制,可以回收不再被引用的对象。
对象类型形成层次结构:
子父类的继承关系,最大的父类是Object类
Boxed primitives:
-
将基本数据类型包装成对象数据类型,: Boolean, Integer, Short, Long, Character, Float, Double
-
通常是在定义容器类型的时候使用它们(容器类型操作的元素要求是对象类型,所以需要对基本数据类型进行包装,转换为对象类型)如List,Map,Set等
-
一般情况下可以和对应的基本数据类型自动转换,但会降低性能,尽量避免使用
运算符
- +,-,*,/,()……
- 字符串可以通过+相连
- 基本数据类型直接使用运算符;对象可以通过调用相应方法来使用运算符;某些静态方法也可以执行运算(如Math库中的方法)
4.2静态和动态检查
静态类型语言:所有变量的类型在编译时已知,编译器可以推导表达式类型,可以在编译阶段进行类型检查
动态类型语言:变量类型在编译时未知或不需要知道,在运行阶段进行类型检查
一种语言可以提供3种检查:
-
静态检查:在编译之前进行检查,包括:语法错误、类名/函数名错误、参数数目错误、参数类型错误、返回值类型错误等。针对动态类型的语言会检查除类型以外的其他语法错误,主要关于“类型”的检查(如a是int,b是int,则得出a/b也是int)
-
动态检查:在运行过程中进行检查,检查非法的参数值,非法的返回值、越界、空指针等。主要关于“值”的检查(接上例,如a = 1,b = 0,执行时发现除0错误)
-
无检查:不进行检查
静态检查>动态检查>无检查
有些问题静态检查和动态检查都无法检测出来,如整数除法(截断整数)、整数溢出、浮点数的特殊类型:NaN,POSITIVE_INFINITY,NEGATIVE_INFINITY
4.3易变性和不变性
改变一个变量:将变量指向另一个存储空间
改变一个变量的值:将该变量当前指向的存储空间写入一个新值
4.3.1不变性(重要设计原则)
数据类型一旦被创建,其值不能改变;引用类型一旦确定其指向的对象,不能再给变其指向其他对象
Java中使用关键字“final”来标记:
- final类无法派生自己的子类
- final变量无法改变值/引用
- final方法无法被子类重写
编译器进行静态类型检查时,如判断final变量首次赋值后发生了改变,会提示错误。
尽量使用final变量作为方法的输入参数,作为局部变量
4.3.2 可变性
不变对象:一旦被创建,始终指向同一个值/引用
可变对象:拥有方法可以修改自己的值/引用
eg: String是不可变类型,StringBuilder是可变数据类型
4.3.3 易变性与不变性
二者区别:
当只有一个引用指向该对象时,二者没有区别;当有多个引用的时候,有差异
优缺点:
-
使用不可变类型,对齐频繁修改会产生大量的临时拷贝,性能较差,但更“安全”,在其他质量指标上表现更好;
-
可变数据类型最小化拷贝,可以提高效率,性能更高,也适合在多个模块之间共享数据。但可变性使得难以理解程序正在做什么,更难满足方法的规约。
-
所以,折中,具体情况具体分析。
4.3.4 危险示例
-
传递可变对象:传递可变对象是一个潜在的错误源泉,一旦被无意中改变,则这种错误非常难于跟踪和发现
eg:在列表示例中,列表(sum和sumAbsolute)和myData(main)指向相同的列表。一位程序员(sumAbsolute的)认为可以修改列表;另一位程序员(main的)希望列表保持不变。因为调用顺序和别名使用,main的程序员会输。
-
返回可变值:由于值可变,则可以在外部对值进行修改,导致内部引用该值的指针的值也发生变化,这样就有可能在其他地方出错。
eg:在Date示例中,,groundhogAnswer和partyDate两个变量名都指向Date对象。这些别名位于代码的完全不同部分,由不同的程序员控制,他们可能不知道另一个在做什么。
4.3.5如何改进代码
- 进行防御式拷贝(但由于大部分时候该拷贝不会被修改,可能造成大量的内存浪费)
- 使用不可变类型,防止被修改,此时也不需要防御式拷贝
一个类可能包含一些方法,使得自己的内部参数被改变,这样调用者可以轻而易举的破坏掉封装,危害很大,这种时候就需要防御性拷贝。
防御性拷贝的关键就在于不把原本类中的对象提供给调用者,而是创建一个跟封装的类中相同的对象返回给调用者,这样,你对这个参数进行修改的时候跟封装类内部的相关参数无关,也就不会改变类中的参数。这就是防御性拷贝。
别名使可变类型具有风险,如果在一个方法中完全局部地使用可变对象,并且只对该对象进行一次引用,那么可以使用可变对象。如果有多个引用(别名),使用可变类型就会变得非常不安全
4.4代码级、运行时和时刻视图的快照图
我们使用快照图表示程序在运行时的内部状态——其堆栈(正在进行的方法及其局部变量)和堆(当前存在的对象),来理解程序现在的运行状况。
快找图的优点:
- 便于程序员之间的交流
- 便于刻画各类变量随时间变化
- 便于解释设计思路
- 便于为后续课程中更丰富的设计符号铺平道路
基本数据类型和对象数据类型在快照图中的表示
基本数据类型:
-
基本数据类型由裸常量表示,传入箭头是对变量或对象字段中的值的引用。
对象数据类型:
-
一个对象数据类型是按其类型标记的圆,在里面写下字段名,用箭头指向它们的值。要了解更多详细信息,字段可以包括其声明的类型。
-
不可变对象用双线椭圆表示,区别于可变对象
-
可变和不可变的引用:后者用双线箭头表示
对可变值(例如:final StringBuilder sb)的不可变引用,即使我们指向同一个对象,其值也可以更改。
对不可变值(如String s)的可变引用,其中变量的值也可以更改,因为它可以重新指向不同的对象。
4.5 复杂数据类型:数组和集合
数组和列表
数组是一种类型为T的固定长度序列。分为定长数组和可变数组两种。定长数组就是普通的数组,变长数组用List进行表示,list是一个接口,保存的是对象。
int [] a = new int [3];//长度为3的定长数组
List<Integer> list = new Arraylist<Integer>();//变长数组
数组的遍历:
int max = 0;
for(int i = 0;i < arrry.lenth;i++)
{
max = Math.max(array[i],max);
}
for(int x:list)
{
max = Math.max(x,max);
}
list有两个实现类:ArrayList和LinkedList,分别代表数组和链表
Set
集合:无序、不可重复,与list有区别
方法:
s.contains(e);
s.containsAll(s2);
s.removeAll(s2)
Map
字典数据类型,将两组对象匹配(映射)
方法:
map.put(key,a1);//建立映射key->a1
map.get(key);//得到key对应的值
map.containsKey(key)//判断是否含有key
map.remove(key)//删除某个匹配
容器
Java 中容器框架的内容可以分为三层: 接口(模型), 模板和具体实现。
在开发中使用容器正常的流程是,首先根据需求确定使用何种容器模型,然后选择一个符合性能要求的容器实现类或者自己实现一个容器类。
list,set,map都是容器中的接口,都需要具体的实现类,但需要指定类型,不可混用,如list的实现类有arraylist和linkedlist;set的实现类有hashset;map的实现类hashmap。
接口
- 它们定义了这些类型的工作方式,但不提供实现代码
- 优点:用户有权在不同情况下选择不同的实现
迭代器
迭代器是一个对象,遍历一组元素并逐个返回元素。
for(A:B)形式的遍历,调用的是被遍历对象所实现的迭代器。
一个迭代器有两个方法:next():返回集合中下一个元素 ,hasnext():判断迭代器是否到达集合末尾。
迭代器的例子
自己定义的一个迭代器
使用自己写的迭代器遍历数组,并删除以6开头的课程,结果出错
分析快照图可知,出错原因在于在删除第一个元素之后,index和迭代器同时+1,漏掉了中间的第二个元素。
如果使用java自带的迭代器删除会报错:
因此,推荐使用的方法是使用迭代器的删除操作。
在迭代器遍历集合过程中,不可以使用集合的方法来增删元素,这里使用的subjects.remove()就是集合的方法,会破坏迭代器的结构从而抛出ConcurrentModificationException异常,应该使用迭代器的remove()方法来删除next()获取的元素,即iter.remove()
4.6有用的不可变数据类型
基本类型及其封装类型都不可变,日期类型Date是可变的
Java集合类型(列表、集合、映射)的常见实现(arraylist、hashmap)都是可变的,也必须可变,因为需要进行元素的增删改等功能,但是如果需要在两种方法之间传递整个集合,这可以使用防御式拷贝(缺点是造成空间的浪费),或者下面的方法:
不可修改的包装
Collections实用程序类具有获取这些可变集合的不可修改视图的方法
这种包装器的结果是不可变的,不能修改,否则会抛出异常,但是这种“不可变”是在进行阶段获得的,编译阶段无法进行静态检查。
不可修改的包装器通过截获所有将修改集合的操作并抛出UnsupportedOperationException,剥夺了修改集合的能力
“2”的意思是指如果去掉那一句,则listCopy的大小会变为2。这是因为listCopy和list指向的是同一块内存空间。所以在使用不可变的包装时要防止用户绕过listcopy直接对list进行操作,同时要保证不可变的对象一旦被创建就不可修改。