软件构造复习笔记(四)数据类型与类型检验


文章的内容是我在复习这门课程时候看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在不知不觉中修改了春天的起始位置,因为partyDategroundhogAnswer指向了同一个可变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: abc

String 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();
				}
		}
}

这一部分的知识到此就结束了,感谢你能看到这里,说明你真的有好好学习。如果你觉得我写的还行,还请一键三连!

在这里插入图片描述

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值