软件构造复习笔记(四)数据类型与类型检验
目录链接
- Part I Data type in programming languages
- 1.1Primitive types基本数据类型
- 1.2Object types对象数据类型
- 1.3一些注意事项
- Part II Static vs. dynamic data type checkingas
- 2.1静态类型语言和静态检查
- 2.2动态类型语言和动态检查
- 2.3一些检验经验
- 2.4检查不出来的错误
- Part III Mutability and Immutability
- 3.1Immutability不变性
- 3.2两种类型的优缺点
- 3.3一些风险的实例
- Part IV Snapshot diagram as a code-level, run-time, and moment view
- 4.1快照图的作用和内容
- 4.2一些小练习
- Part V Complex data types: Arrays and Collections
- 5.1Array
- 5.2List
- 5.3Iterator
- 5.4Set
- 5.5Map
- 5.6迭代器在使用时要注意的问题
文章的内容是我在复习这门课程时候看PPT时自己进行的翻译和一些总结,如果有错误或者出入希望大家能和我讨论!同时也希望给懒得翻译PPT而刷到这篇博客的你一些帮助!
Part I Data type in programming languages
我们学校软件构造这门课程选用的编程语言是Java语言,接下来这一部分会给大家介绍Java语言中的一些数据类型
先说一下数据类型的概念。
数据类型:一组值以及可以对其执行的操作。举个栗子说吧·,例如int类型,他是一个整型变量,可以存储整数数值int i = 10
,而这个i就叫做变量。
变量:用特定数据类型定义,可存储满足类型约束的值。
1.1Primitive types基本数据类型
基本数据类型和C语言中的一样,有int、char、boolean等等就不在这里一 一赘述了。
图1.1.1 基本数据类型
1.2Object types对象数据类型
对象数据类型是Java区别于C语言的超大不同之一,在定义时,基本数据类型是全小写字母进行定义,而对象数据类型定义时首字母大写。二者的不同是对象数据类型定义后是一个对象(哈哈看起来像是句废话),对象的最大特点就是可以在其中定义很多方法。例如定义一个非常常用的对象数据类型Map如下,它定义了一个存储键值对的哈希表,可以使用.put()
方法来添加键值对,也可以使用.containKey()
方法来查询是否存在相应的键。
import java.util.*Map<String, Integer> newmap = new HashMap<>(); //新建一个HashMap
newmap.put("Bob", 2022); //添加新的键值对
newmap.containKey("Bob"); //查询键是否存在 //return true
1.3一些注意事项
在Java语言中基本数据类型是不可变数据类型(Immutable),它们在使用时只有值没有ID,什么意思呢?就是说他们的值如果相同,那它们就是同一个数据。基本数据类型在占中分配空间存储,而且代价低。
对象数据类型中有一些是可变数据类型而有一些则是不可变的。他们既有ID也有值,在堆中分配给他们空间,代价昂贵。
对象数据类型在比较的时候一定不可以使用逻辑运算符“==”!这个比较结果是十分不稳定的,因为对象数据类型中有很多构造方法跟成员变量,其中有一个方法叫做.equals()
方法,对象数据类型在进行逻辑比较时默认使用的就是这个构造方法,因此比较的结果是基于这个方法的比较内容进行返回的。所以在进行对象数据类型的比较时,应该重写.equals()
方法,来实现你自己对于比较结果的要求。如果在一段代码中,不小心将两个对象数据类型进行了逻辑运算符的比较,程序不会报错也可以正常运行,但是就是得不到想要的结果(我曾经找bug找了五个多小时才发现,很大原因是我是一个编程小白,很多代码书写问题亟待规范)。
除此之外,对象数据类型在使用时比基本数据类型更耗时,降低程序性能,因此要尽量避免使用,切不可以因为对象数据类型可以定义很多方法就贪图省事全部使用对象数据类型。下面的代码是Java自带定义的String的.equals()
方法:
public boolean equals(Object anObject) {if (this == anObject) {return true;} else {if (anObject instanceof String) {String aString = (String)anObject;if (this.coder() == aString.coder()) {return this.isLatin1() ? StringLatin1.equals(this.value, aString.value) : StringUTF16.equals(this.value, aString.value);}}return false;}}
Part II Static vs. dynamic data type checkingas
类型转换:
在定义变量的时候可以发生类型转换,有两种形式:隐式类型转换和强制类型转换。
例如,我声明一个变量num为double
类型并给它赋一个初始值为2,那么我们知道2是一个int
型变量而num是一个double
类型的变量,那么就会发生一次隐式类型转换,将2转化成双精度浮点型并赋值给num。
再例如,我们还是声明一个double
类型变量为num,使用赋值语句double num = (double) 2 / 3;
这句话会将整形运算的2/3强制转换转化为浮点类型的计算,所以num的初始值被赋为0.6666…而不是0。
2.1静态类型语言和静态检查
我们在这门课中使用的Java语言就是一种静态类型语言,静态类型语言是在编译期间进行类型检查的语言。所有变量类型在编译阶段(也就是运行之前)就已经知道了,这个特性使得编译器可以推导表达式的类型。啥意思呢?比如说我定义两个int
类型的变量a和b,我写一个表达式计算a + b
那自然而然它的结果就是int
类型的。
如果有小伙伴很调皮想搞点坏事做一做,他可能会将一个int
和一个double
进行算术运算,这想也是不可能的。事实上,在你运行的Java的IDE环境中,静态检查发生在你写代码的每时每刻,也就是说当你试图这么操作的时候IDE就已经报错了,当然,编译自然也不会通过。
2.2动态类型语言和动态检查
热门编程语言Python就是一种动态类型语言,动态类型语言是在运行时才进行类型检查的语言。意思就是在使用这种语言编程时,不会记录每一个变量的变量类型,只有当运行到第一次给该变量赋值时,程序内部才会进行记录。这一部分详细内容可以参考博客强类型和弱类型的语言有什么区别。
静态检查在程序运行之前就检测出变量类型问题,动态检查在程序运行中检查,而无检查压根儿就不检查此类问题。显然,静态检查的效果比动态检查好,两者都比不检查放挺儿好得多。
2.3一些检验经验
静态检查:
如上文所说,静态检验发生在编译阶段,它阻止了程序中一大部分的错误类型。具体说来,如果你在编程过程中试图使用这样的操作"5" * "6"
,也就是尝试让两个字符串变量做数乘,那么静态检查就会在程序运行之前逮捕这个错误。除此之外,静态检查还会检查一些语法上的错误,例如方法名字错误(Math.sin(2) 正确为sin),参数数量错误(Math.sin(2, 3) 正确只有一个参数),参数类型错误(Math.sin(“哈哈”) 正确为int)等等。静态检查更倾向于检查变量的类型错误,即不基于什么特定的值而导致的错误。可以把一个变量类型想象成一个大集合,而静态检查的作用就是保证这个变量的值在这个大集合当中。显然在我们运行这个程序之前是不可能知道这个变量的具体的值的。所以如果一些由特定的变量的值触发的错误,例如除零错误或者数组越界,编译器不会检查出这个错误,自然也不会将其报告成静态错误。
动态检查:
如上文,动态检查发生在程序的运行阶段。与静态检查相反,动态检查发现的错误需要基于特定的参数值。举几个例子就能轻松的理解这个问题。检测非法参数值:假设有一个计算式x / y
,在运行程序之后检测到y = 0
才会报错,静态检查就不会发现这个问题。数组越界:当你试图使用一个比数组范围大的下标的时候就会引发数组越界错误。值得注意的是,动态类型语言也会进行静态检查,它会检查除了类型错误之外的其他静态错误。
2.4检查不出来的错误
这里要说的是一些大家耳熟能详的小陷阱。在Java和很多其他的编程语言中,一些基本数据中的整型数在计算时表现出来的一些边边角角的性质让它看起来不那么像一个正真的数字在进行运算,下述详说:
- 整数除法:
例如计算式3 / 2
得到的结果是1而不是1.5,这是因为在计算时,整型数的除法会向0舍入,导致结果得不到真实的分数值。 - 整型溢出:
int
类型数据是有一定范围的,它一般是4个字节长度的有符号数字,因此它能表示的值就是有上下限的,可以表示-2147483648 - 2147483647
之间的所有整数。当计算一个超大的数时,比如 2 33 2^{33} 233,就会发生算术溢出,得不到想要的结果。 - 浮点类型中的特殊值:
在单双精度浮点类型中有一些不是数的特殊值,例如NaN
( “Not a Number”),POSITIVE_INFINITY
(正无穷), 和NEGATIVE_INFINITY
(负无穷)。进行浮点数运算时会得到这些特殊值。举个例子吧:我定义一个整型变量a = -9
,使用代码float f = Math.sqrt(a)
得到的f的值就是NaN,因为负数不能开平方根嘛。在之后的运算中使用这个NaN就会产生很多意料之外的错误。
下面给出了一些练习供大家熟悉和参考:
图2.4.1 检验练习题
Part III Mutability and Immutability
这一部分介绍可变数据类型和不可变数据类型的用法区别以及各自的优缺点。
我们可以使用等于号“=”来对变量进行赋值操作,要举个例子String message = "Hello Worlds!"
,可以在声明变量的同时为变量赋值,这一点不加赘述了。重点还是放在可变类型和不可变类型上面。那么为什么要区分可变类型和不可变类型呢?因为变化是麻烦的源头,但是变化又是程序必须要留在内心的恶魔。所以要尽可能的避免变化,同时又要使程序灵活。
3.1Immutability不变性
不变性是一个很很重要的设计原则,而不可变数据类型的意思就是一个变量一旦被创建,它的值就不能会再改变了。而Java语言使用一种叫“引用”的方式来操作自己的变量,可以理解成类似指针的东西,将我们创建的一个变量指向一个创建的对象来对其进行操作。
对于引用类型,也可以使之变成不可变的数据,可以使用final
声明变量。举个例子,final String sentence = "Arrive at the highest city all around the world ------ LiTang!"
那么sentence这个变量就和这句话便成了一种“绑定”的关系,如果在后面的编程中你试图改变sentence指向的位置,类似"sentence = "Look at the snowy mountains in the distance guys."
编译器在进行静态检查的时候就会提示一个静态错误。
图3.1.1 编译器的错误提示
所以对于一些定义了之后不需要进行改动的值尽量使用不可变数据类型,尽量使用final变量作为方法的输入参数,正阳的稳定性科技避免出现不必要的bug。但对于fianl有几个问题需要注意:
- final类无法派生子类
- fianl变量无法改变其值或是引用位置
- fianl方法无法被子类重写
如果你还不是很明白,我们拿String作为例子来叙述一下不可变数据类型是怎么工作的。
我们使用一段很简单的Java代码:
String s = "a";
s = s.concat("b"); //也可以使用s += "b"或者类似的字符串连接方法
在这两行代码中,我们首先创建了一个字符串"a",随后另s指向这个字符串。接下来我们想在a的后面加一个b,于是我们又新建了一个字符串"ab",然后更改了s的引用,是之指向了"ab"。更直观的,我们看一眼程序快照图:
图3.1.2 不可变数据类型快照图
而StringBuilder
是一个可变数据类型的很好的例子。在这个类中,有一些方法能够对他指向的字符串进行整体或全部的删除、插入、替换等等操作,而不是简简单单返回一个新值。我么看如下代码和程序快照图便知:
StringBuilder sb = new StringBuilder("a");
sb.append("b");
图3.1.3 可变数据类型快照图
这两种操作乍一看上去没有区别,其实是这样的:如果只有一个引用指向该对象的时候是没有区别的;而当有多个引用的时候,差异就出现了。我们看如下代码和快照图:
String s = "ab"; //不可变数据类型
String t = s;
t = t + "c";StringBuilder sb = //可变数据类型new StringBuilder("ab");
StringBuilder tb = sb;
tb.append("c");
图3.1.4 对比快照图
我么可以看到,如果我们此时引用s,那么它的值还是"ab";而如果我们引用sb,它的值则会变为"abc",这便是二者区别。
3.2两种类型的优缺点
- 不可变数据类型:
使用不可变数据类型显然可以减少变化的发生,以避免副作用。它更加安全,在一些其他指标上表现更好。而其缺点也很明显,对不可变数据类型的频繁修改会产生大量的临时拷贝,也就是需要进行垃圾回收。 - 可变数据类型:
可变数据类型可以最少化拷贝,可以提高效率,可以获得更好的性能,也更加适合于在多个模块之间进行数据共享。而它的缺点就是让程序的易读性变差,也更难满足方法的规约。
所以在使用这两种数据类型的时候需要进行折中,看看你更需要哪个方面的优势。
3.3一些风险的实例
- Risky example #1: passing mutable values:
先放代码再分析:
/**@return the sum of the numbers in the list */
public static int sum(List<Integer> list){int sum = 0;for (int x : list)sum += x;return sum;
}
/**@return the sum of the absolute numbers in the list */
public static int sumAbsolute(Lish<Integer> list){for (int i = 0; i < list.size(); i ++)list.set(i, Math.abs(list.get(i)));return sum(list);
}public static void main(String[] args){//..List<Integer> mtData = Array.asList(-5, -3, -2);System.out.println(sumAbsolute(myData));System.out.println(sum(myData));
}
这段代码有着安全性问题,它破坏了程序的规约。具体是在哪里破坏的呢?在sumAbsolute
方法中,它将输入的可变类型参数list中的值全部取了绝对值,也就是说它改变了输入参数的值!这显然是扯淡的,也是不合规矩的。因此,传递可变数据类型是一个潜在的错误源泉,一旦无意间将其值改变,这种错误非常难于跟踪和发现。
- Risky example #2: returning mutable values:
原始代码如下:
/**@return the first day of spring this year */
public static Date startOfSpring(){return askGroundhog();
}
public static void partyPlanning(){Date partyDate = startOfSping();
}
之后我们将代码修改为以下,使用全局变量来存储日期,让他能够不重复的存储:
/**@return the first day of spring this year */
public static Date startOfSpring(){if (groundAnswer == null)groundhogAnswer = askGroundhog();return askGroundhog();
}private static Date grounghogAnswer = null;public static void partyPlanning(){Date partyDate = startOfSping();partyDate.setMonth(partyDate.getMonth() + 1);
}
这里的问题是partyPlanning
在不知不觉中修改了春天的起始位置,因为partyDate
和groundhogAnswer
指向了同一个可变Date对象。在别人想要调用startOfSpring
这个方法的时候,会得到一个错误的值并继续计算。
如果想要避免这些风险的产生,可以使用一种叫做防御性拷贝的方法,通过防御性拷贝返回一个全新的类型的对象。然而考虑到大部分时候候该拷贝不会被客户端修改,可能造成大量的内存浪费,使用不可变类型就可以避免这些浪费。值得一提的是,不可变类型不需要防御式拷贝。
Part IV Snapshot diagram as a code-level, run-time, and moment view
4.1快照图的作用和内容
- 作用:
如果我们知道程序在运行的时候都发生了什么事儿,对我们理解一些微妙的问题是有很大用处的。快照图用于描述程序运行时的内部状态。它便于程序员之间的交流,便于客户啊各类变量随时间的变化,也便于理解设计思路。 - 内容:
快照图中可以提现基本类型的值、对象类型的值以及各种引用。有下面一些画法的规范:
内容 | 画法 | 图示 |
---|---|---|
基本类型的值 | 箭头指向值 | |
对象类型的值 | 箭头指向椭圆 | |
不可变对象 | 双线椭圆 | |
可变对象 | 单线椭圆 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/e29ce83ebabb4f82a8758ffccb7d2f32.png |
不可变引用 | 双线箭头 |
4.2一些小练习
- 针对可变值的不可变引用:
下面的代码在编译阶段会出错,编译器会警告final变量的值不能被分配。
final StringBuilder sb = new StringBuilder("abc");
sb.append("d");
sb = new StringBuilder("e"); //error
System.out.println(sb); //output: abcd
图4.2.1 可变值的不可变引用
- 针对不可变值的可变引用:
String s1 = new String("abc");
List<String> list = new ArrayList<>();
list.add(s1);s1 = s1.concat("d");
System.out.println(list.get(0)); //output: abcString s2 = s1.concat("e");
list.set(0, s2);
System.out.println(list.get(0)); //output: abcde
看一下快照图吧家人们
图4.2.1 不可变值的可变引用
Part V Complex data types: Arrays and Collections
这也部分介绍了复杂数据类型数组类和一些容器类。我认为主要是针对Java中有而C语言中没有的数据类型的一些使用规则和介绍。
5.1Array
数组是我们的老朋友了,在Java中也当然少不了他们的身影。这里的Array指的是定长数组,也就是不可以改变长度的数组。举个例子大家就认识了
int[] a = new int [100]; //声明一个长度为100个整型变量的数组
5.2List
这里的List就算是我们的新朋友了,它是长度可变的数组。List只是一个接口类,而且规定List中存储的变量必须是对象类型的变量。List在使用时是这样进行声明的:
List <Integer> list = new ArrayList<>();//声明一个ArrayList实现的list
这里的ArrayList
是List的实现方法之一,了不同的还有LinkedList
实现方法。他们的区别是ArrayList
是使用数组的方式进行实现,而LinkedList
是使用链表方式实现的。他们的却别在于使用一些方法使得时间复杂度不一样,就和链表和数组两种数据结构的差异是一样的。List的详细用法请戳这里。
图5.2.1 List类
5.3Iterator
这个东西是迭代器,我个人来讲不太乐意用这玩应,但也得说明一下。它的作用是顺序访问一个容器类所存储的元素,声明方式和两个方法如下:
List<Integer> mylist = new ArrayList<>();
Iterator<Integer> it = mtlist.iterator();it.hasnext(); //查询是否有下一个元素,返回真值
it.next(); //返回下一个元素并且移动迭代器指针
5.4Set
Set是一个列表,他是零个或者多个唯一对象的无序集合。也就是说要注意,它里面存储元素的顺序不一定是按照你放入的顺序存储的,它会检测放入对象的.hashcode()
返回值,如果添加的是重复元素,则不会加入到Set中去。它的声明方式如下:
Set<String> myset = new HashSet<String>();
更多的Set操作戳这里
图5.4.1 Set类
5.5Map
Map中存储的一组组键值对,他可以查询是否存在键并且可以根据提供的键找到对应的值。他的声明如下:
Map<String, String> mymap = new HashMap<String,String>();
更多Map操作戳这里
图5.5.1 Map类
5.6迭代器在使用时要注意的问题
图5.6.1 迭代器内部结构
我们运行下面一段代码:
public static void dropCourse6(ArrayList<String> subjects){Myiteratir iter = new Myiterator(subjects);while (itre.hasnext()){String subject = iter.next();if (ubject.startsWith("6."))subjects.remove(subject);}}
}
会得到如下的结果:
图5.6.2 运行结果
分析一下原因吧。我们在删除这个元素的时候使用的是.remove()
方法,它删除了List中的值之后导致Index发生了变化,但是我们的跌倒器却不知道,就导致了中间有一个元素被略过了。
图5.6.3 快照图解
为了解决这个问题我么可以使用迭代器自带的删除功能,这样再删除的时候迭代器就可以发现Index的变化,从而避免错误。
public static void dropCourse6(ArrayList<String> subjects){Myiteratir iter = new Myiterator(subjects);while (itre.hasnext()){String subject = iter.next();if (ubject.startsWith("6."))//subjects.remove(subject);iter.remoce();}}
}
这一部分的知识到此就结束了,感谢你能看到这里,说明你真的有好好学习。如果你觉得我写的还行,还请一键三连!
软件构造复习笔记(四)数据类型与类型检验相关推荐
- 【XJTUSE软件项目管理复习笔记】 第二章 软件项目整体管理
仅供学习参考,禁止商用与转载 文章目录 软件项目管理复习笔记 第二章 软件项目整体管理 什么是项目整体管理 战略计划和项目选择 项目选择 项目的财务分析 净现值分析(重点) 投资收益率(ROI)分析法 ...
- 软件构造学习笔记【三】(第4、5章)
目录链接 一.数据类型与类型检验 1.1 数据类型 1.1.1 基本数据类型 1.1.2 对象数据类型 1.2 类型检查 1.2.1可变性和不可变性 1.2.1.1 变与不变 1.2.1.2 fina ...
- 软件构造学习笔记(六)抽象数据类型
目录链接 Part I Abstraction and User-Defined Types Part II Classifying Types and Operations Part III Abs ...
- 软件构造学习笔记(九)面向复用的软件构造技术
目录链接 Part I What is Software Reuse? Part II How to measure "reusability"? Part III Levels ...
- 2022 - 软件构造复习
软件生命周期 一个软件产品或软件系统经历孕育.诞生.成长.成熟.衰亡等阶段,一般称为软件生存周期(软件生命周期). 根据软件所处的状态和特征,划分软件生存周期. 需求定义.软件设计.软件实现.软件维护 ...
- 从零开始的软件构造复习[上篇]
第一章 1.1 软件构造的多维度视图(Multi-dimensional software views) Build-time, moment, code-level 代码如何在逻辑上被组织为基本的程 ...
- 软件构造学习笔记-第九周、第十周
因为本周五开始五一假期,所以只有一节软件构造课.因为内容还属于创建模式.结构模式.行为模式.将该堂课的内容整合到本博客中.本周的重点是程序开发模式,在写代码之前首先充分考虑采用哪种模式更有利于开发.维 ...
- 软件构造学习笔记-第八周
本周重点是Liskov可替换原则.它要求父类和子类的行为一致性,子类要有更强的不变量.更弱的前置条件.更强的后置条件.在该原则的要求下,每个子类都可以对父类进行替换.这在开发过程中会带来极大的便利,在 ...
- 哈工大软件构造 复习
哈工大软件构造试题构成: 1.30分的选择题 2.70分的简答题 2019考试知识点(重要的,但不涵盖全部考试范围): 1.git工具的知识点(没有考察命令行,考察的主要是git的演变及各部分的作用) ...
最新文章
- 欧洲最大云服务公司火灾!数百万网站出现故障企业网络推广大型瘫痪现场!...
- 用C#抓取AJAX页面的内容
- js svg语音波动动画_11 个非常受欢迎的 JavaScript 动画库,值得学习!
- $@ $# $2 $0 $* Linux 参数使用
- Tails 3.0 正式发布,不再支持 32 位计算机
- css根据当前宽度设置css,JS和CSS实现自动根据分辨率设置页面宽度
- 【写作技巧】毕业论文格式要求
- poj1005——I Think I Need a Houseboat
- Thinking in Java 11.10 Map
- 判断素数的程序代码c语言,C语言中判断素数的程序代码是什么?
- xshell删除文件夹命令_xshell 常用命令整理
- 操作系统学习笔记:操作系统基础知识
- 树莓派控制火焰传感器
- Asset Pricing:Asset Pricing Formula
- Activiti工作流会签一 部署流程
- 金蝶KIS旗舰版V5.0.0研究学习
- The error may involve defaultParameterMap ### The error occurred while setting paramete
- 用C语言设计简易银行系统
- 连锁多门店收银系统源码之新增采购进货单功能逻辑
- Kettle(14):Linux安装Kettle