第四章介绍了数据类型与类型检验。
数据类型:一组值以及可以对其执行的操作
变量:数据类型的的具体实现,用特定的数据类型定义,可以储存满足数据类型约束的值
数据类型可以分为基本数据类型和对象数据类型:
基本数据类型 | 对象数据类型 | |
只有数值,无ID | 有数值且有ID | |
不可变 | 有些可变有些不可变 | |
存放在栈中 | 存放在堆中 | |
无统一表达方式 | 可以有统一定义 | |
代价高 | 代价低 |
一般来说,定义基本数据类型变量时,命名以小写字母打头;而对象数据类型以大写字母打头。
对象数据类型可以形成层次结构,可以存在继承关系,可以有重写以形成独特的方法
在有需要时(通常是在定义容器(list、map、set等)的时候,因为容器要求操作对象为对象数据类型),我们可以将基本数据类型包装为对象数据类型,有时候它也能实现自动转换,但是会降低性能,因此我们应该尽可能避免使用/自动转换的发生。
例如:list.add(1);整数1一般是以int类型存在,但由于list方法的操作对象必须是对象数据类型,所以这里会将1转化为integer型,相当于list.add(Integer.valueOf(1));
**方法和函数都是某些功能的实现,但是前者用对象数据类型变量的名调用,后者是类调用
语言可以按照变量类型的检查阶段划分为静态类型语言和动态类型语言:
静态类型语言:所有变量类型在编译时已知,如Java
动态类型语言:在运行阶段才进行类型检查,如Python
类型检查显然也分为静态检查和动态检查:
静态检查:在编译阶段就进行的检查,包含类名/函数名检查、参数数目检查、参数类型见擦汗、返回值类型检查
动态检查:在运行阶段才进行的检查,包括检查参数的合法性、返回值的合法性、越界检查和空指针问题的检查
简单概括,静态检查是检查数据的类型,动态检查检查的是数据的值。
需要知道的是,动态类型语言也有静态检查,它在编译阶段会执行除了类型检查以外的静态检查,而静态语言也支持动态检查。
可变/不可变
改变变量:将该变量指向另一个储存空间
改变变量的值:对该变量指向的储存空间写入一个新的值
不变数据类型:被创建后值不可以改变
不变引用类型:被创建后不能改变指向的对象
不变类型的定义在变量类型前加final,编译器在静态检查时会对不变数据的对应改变进行报错
数据的不变性虽然有时候显得笨重,但是稳定,不容易产生未知的错误,在设计过程中应该优先考虑使用不变量
*final类无法派生子类
*final方法无法重写
可变对象:与上述的不变对象相反,可变对象有修改值/引用的方法
针对上述说的可变/不可变对象,课上给出一个用快照图体现字符串更改的例子,如图
String是不可变数据类型,而字符串构造器是可变数据类型,在客户端看来一样的更改内容表示的代码,实际操作起来完全不一样,不可变类型的改变是生成一块新的地址区域,然后将引用指向新的区域,而可变类型的改变则是我们意识当中应该是的对当前指向的区域直接修改值。
在考虑的对象较单一的时候并无多大差别,但是如果有多个对象且关系较复杂,若对于这些特性不够熟悉,误用可能会产生一些难以发现的错误
如上图所示,新建对象,赋初值让它与一个已存在的对象一致,之后进行的操作就是在字符串后面加一个字符,上面提到二者的差别,可以看到对String而言,操作对象发生了我们认为的值的改变(实际上是指向对象的改变),之前与它相等的字符串没有跟着发生变化,仍然指向原来的区域;字符串构造器这边,赋初值时把tb、sb指向同一区域,我们提到这种可变数据类型的改变可以直接发生为区域内的值的改变,因此变化完之后二者仍相等。基于C语言的基础,String之类的不可变数据类型与之前的理解是一样的,而可变数据类型观感上更像是指针操作。
可变数据类型显然更加灵活,更适合共享一些数据,处理一些较复杂问题时可能表现得比较优秀,而且性能也更好,因为不会因值的改变产生一些可能再也不需要用到的区域块。但是可变性让我们有的时候难以捕捉它的行为,有时候不够安全,同时使用较多时很明显会不容易管控。尤其是传递一个可变对象时,它很可能在之后的操作被修改,往往是不明所以的错误的源泉。
因此我们更倾向于使用不可变的数据类型,这种理念也贯彻到后面的ADT设计中
不过有的时候我们不得不使用可变数据类型,这个时候为了整个程序的安全,应该在恰当的地方进行防御式拷贝(通常时返回这个数据时,我们倾向于return new ...)这样返回一个新的区域,比较不容易产生上面说的错误。不可变数据类型一般不需要防御式拷贝的保护措施,当然保险点更好,只要不影响性能;也可以利用局部变量来实现我们想要的操作,因为局部变量只会在本地被使用,不会存在多个引用的问题,这个时候使用可变数据类型是比较安全的。
快照图可以很好地描述程序运行时的内部状态,一些规则之前已经提过,这里补充之前遗漏的:引用箭头直接指向数据表示引用基本数据类型,指向椭圆表示对象数据类型
还有关于可变引用不可变值和不可变引用可变值的问题实例:
上图情景,在声明sb对象时用了final,这就是一个不可变的引用,尽管字符串构造器本身是可变类型,出错发生在第三行,试图对sb引用的改变
上图是关于不可变值的可变引用,动态数组list中先将s1加入再修改和先修改s2再加入的结果是不一样的:s1加入数组后,list的0索引指向abc那块区域,之后对s1进行修改,由于是不变数据类型,所以生成了一个新的块,s1指向新的,list[0]仍然指向原来的那块;另一种情况比较简单,修改完得到新的块再建立数组的指向关系而已
数组和遍历器
数组的声明与C还是有些不同的:
int [] a = new int[N] //Java中的声明,其中N是一个具体数字
int a[100]; //C的声明
Java的声明需要我们马上为之new一块空间,而且喜欢将[]提前,使数组名分离开来。而且关于数字型的赋初值方面,Java默认为0,C语言中若没有赋初值的操作,值是不确定的。
上述是Java的定长数组的声明,Java动态数组的实现依赖于List类实现
List<type> list = new Listtype<type>();
然后就可以使用具体实现类的各种方法完成我们需要的操作,包括添加、修改等等
常用的容器还有集合Set和图Map,这些容器中只能存放指定类型的数据,功能十分丰富,各种方法此处不再赘述,这几种类型的数据的遍历这边可以谈一下:
List<String> cities = new ArrayList<>();
Set<Integer> numbers = new HashSet<>();
Map<String,Turtle> turtles = new HashMap<>();
for (String city : cities) {
System.out.println(city);
}
for (int num : numbers) {
System.out.println(num);
}
/*通过下标遍历*/
for (int ii = 0; ii < cities.size(); ii++) {
System.out.println(cities.get(ii));
}
/*通过键遍历*/
for (String key : turtles.keySet()) {
System.out.println(key + ": " + turtles.get(key));
}
Map每一块值包含一个键和一个数据(相当于编号和数值),我们可以通过键来遍历
除此之外也可以用迭代器来实现遍历:
List<Integer> list = new ArrayList<Integer>();
....
Iterator it = list.iterator();
while(it.hasNext()){
//操作
it.next();
}
一般来说,迭代过程对原容器进行删除操作时忌讳的,往往会导致遍历跳过部分对象,因此更倾向于对迭代器进行删除,之后再恢复到容器。
前面我们提到过可变数据类型的一些限制,对此的处理方法还有一种将之包装成不可变数据类型以进行安全的传递:
封装完之后,会自动屏蔽掉所有会破坏不变性的方法,此外还要避免对原空间进行访问和修改以免出错。
以下是一些常用的封装
public static <T> Collection<T>
unmodifiableCollection(Collection<? extends T> c);
public static <T> Set<T> unmodifiableSet(Set<? extends T> s);
public static <T> List<T> unmodifiableList(List<? extends T>
list);
public static <K,V> Map<K, V> unmodifiableMap(Map<? extends K,
? extends V> m);
public static <T> SortedSet<T>
unmodifiableSortedSet(SortedSet<? extends T> s);
public static <K,V> SortedMap<K, V>
unmodifiableSortedMap(SortedMap<K, ? extends V> m);