Data Type and Type Checking 笔记

这是复习软件构造第四章时做的笔记

3可变性与不可变性

1 赋值

  1. 用‘=’号赋值

  2. 赋值时前面可以有变量声明

    double a = 3.14
    

2改变一个变量与改变其值的区别

  1. 改变一个变量:将其指向另一个存储空间

  2. 改变一个变量的值:向该变量目前指向的存储空间中写入新的值

  3. 应尽量避免变化

3不变性

  1. 不可变数据类型:一旦被创建其值不可被改变(注意对其的引用可能改变但是在其存储空间内对应的值却无法改变)

  2. 不可变引用类型:一旦指向特定的对象不可被改变指向其他对象

  3. 用final声明

    final int n = 5
    final Person a = new Person("Ross")
    

    编辑器在进行静态检察时发现final变量在首次赋值后发生了变化会提示错误

  4. 尽量使用final变量作为函数参数或者是局部变量

  5. 注意以下几点

    final 类无法派生子类
    final 变量无法改变值或者引用
    //final 方法无法被子类重写
    
  6. 不可变对象:创建后始终指向同一个值/引用
    可变对象:拥有方法可以改变值或者引用

4例子

String就是不可变类型的例子,String对象在创建后其值一直不变一旦改变就需要创建一个新的对象

String s = "a";
s = s.concat("b");

StringBuilder 是可变类型的例子,一个StringBuilder对象中有可以改变对象自生值的方法如下的代码不会使sb的引用指向一个新的值

StringBuilder sb = new StringBuilder("a");
a.append("b");

5两者的差异

首先当只有一个引用指向该对象是二者的行为是相同的,但当有两个引用指向该对象时差异就出现了,看下图

image-20220604153322278

我们看到当两个引用指向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

  • 基本类型的值

    image-20220604204638191

  • 对象类型的值

    image-20220604204705978

    • 我们可以看到对象类型可以包含基本类型的值

不可变对象

不可变对象用双线椭圆表示,例如String类型我们运行以下语句

String s = "a"
S = s + "b" 

image-20220604205146432

改变时对其引用打x,接着画一个新的对象并改变其引用

可变对象

用单线椭圆表示,对其内部元素的改变可以在其元素上打叉并写上新元素

image-20220604205502801

引用

  • 用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

    image-20220604213427039

    首先我们发现List中的每一个元素起始是一个引用类型对其的赋值都会指向一个对象,其次让一个引用类型的变量等于另一个引用变量只会让他们指向相同的对象,并不会产生箭头的连锁反应

复杂数据类型:Array与Collection

数据类型的介绍

  1. Array数组:固定长度的数据类型为T的一系列值

    int [] a = new int [100]//其值可以是基本数据类型也可以是对象类型
    

    int[] 类型包含了所有可能的数组值,但是一个特定的数组一旦被创建其值就无法改变

    operations

    取值 a[2]
    赋值 a[2] = 0
    取长度 a.length
    
  2. 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来说可以用迭代器来访问

  1. Set:由零及以上的独特的对象构成的无序集合

    一个对象不能出现在一个集合中两次

    //同样的Set也是一个接口,与List相同其元素也只能为对象类型的
    operations:
    s1.contains(e)
    s1.containsAll(s2)
    s1.removeall(s2)
    
  2. 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方法时回返回第一个元素。

image-20220605200036173

然后我们分析一个例子:

image-20220605202636129

初始输入为:

image-20220605202730001

其原因是由于List在remove对应的元素后会把对应的元素移到前面但是index对应的值却没有变化因此产生了错位,对应的diagram如下:

image-20220605203215063

image-20220605203749761

同样的对于以下代码会出现类似的错误

image-20220605203905055

image-20220605210917613

原因是:迭代器内部的每次遍历都会记录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)

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kzer111

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值