Data Type and Type Checking 笔记
Data Type and Type Checking 笔记
这是复习软件构造第四章时做的笔记
3可变性与不可变性
1 赋值
-
用‘=’号赋值
-
赋值时前面可以有变量声明
double a = 3.14
2改变一个变量与改变其值的区别
-
改变一个变量:将其指向另一个存储空间
-
改变一个变量的值:向该变量目前指向的存储空间中写入新的值
-
应尽量避免变化
3不变性
-
不可变数据类型:一旦被创建其值不可被改变(注意对其的引用可能改变但是在其存储空间内对应的值却无法改变)
-
不可变引用类型:一旦指向特定的对象不可被改变指向其他对象
-
用final声明
final int n = 5 final Person a = new Person("Ross")
编辑器在进行静态检察时发现final变量在首次赋值后发生了变化会提示错误
-
尽量使用final变量作为函数参数或者是局部变量
-
注意以下几点
final 类无法派生子类 final 变量无法改变值或者引用 //final 方法无法被子类重写
-
不可变对象:创建后始终指向同一个值/引用
可变对象:拥有方法可以改变值或者引用
4例子
String就是不可变类型的例子,String对象在创建后其值一直不变一旦改变就需要创建一个新的对象
String s = "a";
s = s.concat("b");
StringBuilder 是可变类型的例子,一个StringBuilder对象中有可以改变对象自生值的方法如下的代码不会使sb的引用指向一个新的值
StringBuilder sb = new StringBuilder("a");
a.append("b");
5两者的差异
首先当只有一个引用指向该对象是二者的行为是相同的,但当有两个引用指向该对象时差异就出现了,看下图
我们看到当两个引用指向StringBuilder对时其中一个改变也会影响另一个
其次当使用不可变类型时对其频繁的修改将会产生大量的临时拷贝一方面需要回收垃圾另一方面也会造成时间的延长而可变类型因为可以最少化拷贝可以提高效率。总结以下就是可变数据类型可以有更好的性能也适用于在各个模块间拷贝数据,类似“Global Variable”,但是这样也会造成对应的错误。
参考MIT的阅读材料有以下的例子
-
当可变类型作为函数参数时
/** @return the sum of the absolute values of the numbers in the list */ public static int sumAbsolute (List<Integer> list) { // let's reuse sum(), because DRY, so first we take absolute values for (int i = 0; i < list.size(); ++i) list.set(i, Math.abs(list.get(i))); return sum(list); }
这个计算绝对值之和的函数会把传入的参数的值改变,并且函数的调用者可能不知道会有这种情况
-
当其作为函数参数返回时同理因为返回任何直接对传入参数的操作都会导致其变化。为了防止这些可能的bug应该在传出时进行防御性拷贝:
return new Date(groundhogAnswer .getTime());
但是这样会很浪费空间
-
这样比较下来当我们合理的使用不可变类型时其效率回比一直使用防御性拷贝的程序好,因为这样不同的地方用不同的对象来表示,相同的地方都索引到内存中同⼀ 个对象,这样会让程序节省空间和复制的时间。同时当在局部使用可变类型时由于不会涉及到共享,只有一个引用是安全的。
Snapshot diagram
关键词:as a code-level runtime moment view
Snapshot diagrams
- 用于描述运行时程序的内部状态包括其heap and stack
- 便于程序员之间的交流,以及其他各类变量与其随时间的变化
Values in Snapshot
-
基本类型的值
-
对象类型的值
- 我们可以看到对象类型可以包含基本类型的值
不可变对象
不可变对象用双线椭圆表示,例如String类型我们运行以下语句
String s = "a"
S = s + "b"
改变时对其引用打x,接着画一个新的对象并改变其引用
可变对象
用单线椭圆表示,对其内部元素的改变可以在其元素上打叉并写上新元素
引用
-
用final声明的不可变引用用双箭头指向对应对象,注意虽然引用不可边但是其指向的值确实可变的,同理可变的引用也可以指向不可变的值。针对不可变对象的赋值会在编译阶段报错。
-
可变引用用单箭头表示
可以举个例子
String s1 = new String("abc"); List<String> list = new ArryList(); list.add(s1); s1.concat("d"); System.out.println(list.get(0)) String s2 = s1.concat("e"); list.set(0,s2); System.out.println(list.get(0))
先给出对应的Snapshot diagram
首先我们发现List中的每一个元素起始是一个引用类型对其的赋值都会指向一个对象,其次让一个引用类型的变量等于另一个引用变量只会让他们指向相同的对象,并不会产生箭头的连锁反应
复杂数据类型:Array与Collection
数据类型的介绍
-
Array数组:固定长度的数据类型为T的一系列值
int [] a = new int [100]//其值可以是基本数据类型也可以是对象类型
int[] 类型包含了所有可能的数组值,但是一个特定的数组一旦被创建其值就无法改变
operations
取值 a[2] 赋值 a[2] = 0 取长度 a.length
-
List 列表是类型T的一个变长序列
List <Integer> list = new ArrayList<Integer>(); /* *Note 1 List是一个接口 *Note2 List的成员只能是对象类型的 */
operations
取值 list.get(2) 赋值 list.set(2,0) 取长度 list.size()
遍历以上两者数据结构对于array来说可以采用与C语言相似的方法对于list来说可以用迭代器来访问
-
Set:由零及以上的独特的对象构成的无序集合
一个对象不能出现在一个集合中两次
//同样的Set也是一个接口,与List相同其元素也只能为对象类型的 operations: s1.contains(e) s1.containsAll(s2) s1.removeall(s2)
-
Map:由键对值组成的集合
其也是一个接口
operations
map.put(key,val) map.get(key) map.containsKey(key)//返回boolean值 map.remove(key)
声明与创建collections对象:Set ,Map,List
当添加容器中的item时编辑器会执行静态检查,确保只添加合适类型的item。因此我们可以保证取出的值是指定类型的。
List<Integer> list = new ArrayList<>();
java将对一种类型的specification与implementation分开
声明:
List<String>List
Set<Integer>numbers
Map<String,Turtle>turtles
//注意无法创建包含基础类型的collection但是我们可以用包装类入将int->Integer
实现:
List : ArraayList and LinkedLisst
Set :HashSet
Map: HashMap
遍历
迭代器(Iterator)是一个可变对象,它遍历一组元素并逐个返回元素,例如for( : )形式的遍历调用的是遍历对象实现的迭代器。
Iterator有两个方法,一个是next返回collections中的下一个元素,一个是hasNext来测试迭代器是否到达了collection的末尾。
MIT材料给了Iterator的一个简易实现方法。其保存一个指向原collection的不可变指针list以及一个标志现在遍历到何处的可变标识index。
而开始index为0,Next()方法回先返回目前index处的元素值,然后让index值加1。而hasNex方法是判断目前index是否小于其长度。也即开始时调用hasNext方法时回返回第一个元素。
然后我们分析一个例子:
初始输入为:
其原因是由于List在remove对应的元素后会把对应的元素移到前面但是index对应的值却没有变化因此产生了错位,对应的diagram如下:
同样的对于以下代码会出现类似的错误
原因是:迭代器内部的每次遍历都会记录List内部的modcount(记录修改次数)当做预期值,然后在每次循环中用预期值与List的成员变量modCount作比较,但是普通list.remove调用的是List的remove,这时modcount++,但是iterator内记录的预期值并没有变化,所以会报错。但是如果在Iterator中调用remove,这时会同步List的modCount到Iterator中,故不再报错
而以下这种用实际Iterator的形式会合适的调整其index,及调用iter自己的remove方法
Iterator iter = subjects.iterator();
while(iter.hasNext()){
String subject = iter.next();
if(subject.startWith("6.")){
iter.remove();
}
}
Useful Immutable types
注意:基本类型及其封装对象都是不可变的
标准Collections的Java类中提供了获取入list,set,map等的不可变封装
Collections.unmodifiableList,Collections.unmodifiableSet,Collections.unmodifiableMap
这种包装器得到的结果是不可变的,mutators被禁用。如果尝试改变会抛出异常
但是注意这种”不可变“是在运行阶段取得的,意思是我在静态检查时不会发现对其进行更改的错误
List<String> list = new ArrayList<>();
list.add("ab");
List<String> listCopy = Collections.unmodifiableList(list);
listCopy.add("c");
list.add("c");
System.out.println(listCopy.size());
注意这里第三行的构造方法,以及第四行的更改操作在运行时才会保存、
Wrappers通过拦截所有更改collections的操作并抛出UnsupportedOperationException。其好处:保证了不变性同时可以保证cilent不会更改结果(自己保留对原collection的引用然后把对wrapper包装之后的结果返回给cilent)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理