哈尔滨工业大学软件构造课程笔记第三章第一节

本文是关于软件构造课程的笔记,重点讨论Java中的数据类型、静态和动态类型检查、可变性和不变性。Java是一种静态类型语言,其类型在编译时已知,有助于在早期发现错误。同时,文章强调了不变性在编程中的重要性,特别是对于减少错误和提高代码可读性的价值。此外,还介绍了快照图作为理解和分析程序运行时状态的工具,以及复杂数据类型如数组、集合的使用。
摘要由CSDN通过智能技术生成

3.1 数据类型与类型检验

1.编程语言中的数据类型

类型和变量
类型是一组值,以及可以对这些值执行的操作。
例如:boolean,int,double,string…

变量:存储特定类型值的指定位置
形式:类型+名称

Java有几种基本类型
int(对于5和-200这样的整数,但限制在±2^31范围内,或大约±20亿)
long(适用于±2^63以下的较大整数)
boolean(true, false)
double(对于浮点数,它表示实数的子集)
char(单个字符如“A”和“$”)

Java也有对象数据类型

  • String表示一个字符序列。
  • BigInteger表示任意大小的整数

根据Java约定,基本类型是小写的,而对象类型以大写字母开头。

在这里插入图片描述对象类型形成层次结构
根是对象(所有非原语都是对象)
除Object外的所有类都有一个父类,用extends子句指定

class Guitar extends Instrument {
 ... 
 }

如果忽略了extends子句,则默认为Object

一个类是它所有超类的一个实例
-从超类中继承可见的字段和方法
-可以重写方法来改变它们的行为
在这里插入图片描述盒装原语
将基本类型包装为对象类型
– Boolean, Integer, Short, Long, Character, Float, Double

通常是在定义容器类型的时候使用它们(容器类型操作的元素要求是
对象类型,所以需要对基本数据类型进行包装,转换为对象类型)

一般情况下,尽量避免使用(会降低性能)

一般可以自动转换
在这里插入图片描述操作符:执行简单计算的符号
-赋值:=
-加法:+
-减法:-
-乘法:*
-除法:/

操作顺序:遵循标准的数学规则

  • 1.括号
  • 2.乘法和除法
  • 3.加法和减法

字符串连接(+)

– String text = "hello" + " world"; 
– text = text + " number " + 5; // text = "hello world number 5"

操作

操作函数把输入并生成输出(有时改变的值)
-作为中缀、前缀或后缀操作符。例如,a + b调用运算+:int×int→int。
-作为一个对象的方法。例如,bigint1.add(bigint2)调用操作add: BigInteger×BigInteger→BigInteger。
-作为函数。例如,Math.sin(theta)调用操作sin: double→double。在这里,数学不是一个对象。它是包含sin函数的类。

重载
同样的操作名可用于不同的数据类型
▪在Java中,算术运算符+、-、*和/对于数字基元类型来说是重载的。
▪方法也可以超载。大多数编程语言都有一定程度的重载。

2.静态和动态数据类型检查

类型转换

int a = 2; // a = 2 
double a = 2; // a = 2.0 (Implicit) 
int a = (int) 18.7; // a = 18 
double a = (double)2/3; // a = 0.6666…

int a = 18.7; // ERROR 
String a = 1; // ERROR
double a = 2/3; // a = 0.0

静态类型与动态类型
Java是一种静态类型语言。
所有变量的类型在编译时(程序运行之前)是已知的,因此编译器也可以推断出所有表达式的类型。
如果a和b被声明为整型,那么编译器就会得出a+b也是整型的结论。
-实际上,Eclipse环境在您编写代码时就会这样做,所以在您仍然在键入时就会发现许多错误
– 在编译阶段进行类型检查

在像Python这样的动态类型的语言中,这种类型的检查被推迟到运行时(当程序运行时)。
– 在运行阶段进行类型检查

静态检查和动态检查
一种语言可以提供的三种自动检查
静态检查:甚至在程序运行之前就会自动发现错误
动态检查:在执行代码时自动发现错误。
无检查:这种语言根本不能帮助您找到错误。你必须自己注意,否则就会得到错误的答案

静态检查 >> 动态动态 >> 无检查

不匹配的类型
Java验证类型总是匹配的

String five = 5; // ERROR!

静态检查
静态检查意味着在编译时检查bug。
静态类型可以防止大量的bug感染您的程序:确切地说,是由于将操作应用到错误类型的参数而导致的bug。

如果你写了一个断行代码如下:

5*6

试图将两个字符串相乘,那么静态类型将在您仍在编程时捕获此错误,而不是等待在执行期间到达该行。

▪语法错误,如额外的标点符号或虚假的话。
甚至像Python这样的动态类型的语言也做这种静态检查。
▪类名/函数名错误,像Math.sine (2)。(正确的名字是罪)
▪参数数目错误,像Math.sin(30, 20)。
▪参数类型错误,像 Math.sin(“30”)。
▪返回类型返回值类型错误,像return “30”;声明为返回整型数的函数。

动态检查
非法的参数值:例如,当y实际为0时,整数表达式x/y是错误的;否则它的工作原理。在这个表达式中,除0不是一个静态误差,而是一个动态误差

非法的返回值:例如:当特定的返回值不能在类型中表示时

越界:例如,在字符串上使用负号或太大的索引

空指针

静态与动态检查
▪静态检查往往是关于类型,错误是独立的具体价值的一个变量。
-静态类型保证了一个变量会有一些来自那个集合的值,但是我们直到运行时才知道它到底有哪些值。
-所以如果错误只是由某些值引起的,如除数为0或索引超出范围,那么编译器不会提出一个关于它的静态错误。
▪动态检查,相比之下,往往是关于特定的价值观造成的错误。

原始类型不是真数
一个陷阱在Java -和许多其他的编程语言是它的原始数值类型角落例不像我们用于整数和实数。

因此,实际上应该动态检查的一些错误根本没有检查。
整数除法:5/2不返回一个分数,它返回一个截断的整数。
——整数溢出。如果计算结果太正或太负,以至于不能在这个有限的范围内进行拟合,它就会悄悄地溢出并返回一个错误的答案。
(无静态/动态检查!)例如:int big = 200000*200000;
-浮点类型中的特殊值。NaN(“Not a Number”),
POSITIVE_INFINITY, NEGATIVE_INFINITY。

3.可变性和不变性

赋值
使用“=”给变量赋值

String foo; 
foo = “IAP 6.092;

赋值可以与变量声明相结合

double badPi = 3.14; 
boolean isJanuary = true;

改变一个变量或它的值
改变一个变量:将该变量指向另一个值的存储空间

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

变化是“罪恶”,但程序不能没有变化

尽可能避免变化,以避免副作用

不变性
不变性:重要设计原则
不变数据类型:一旦被创建,其值不能改变
如果是引用类型,也可以是不变的:一旦确定其指向的对象,不能再被改变指向其他对象
如果编译器无法确定final变量不会改变,就提示错误,这也是静态类型检查的一部分。

所以,尽量使用final变量作为方法的输入参数、作为局部变量。
final表明了程序员的一种“设计决策”

final类无法派生子类
final变量无法改变值/引用
final方法无法被子类重写

可变性和不变性
不变对象:一旦被创建,始终指向同一个值/引用
可变对象:拥有方法可以修改自己的值/引用

字符串作为不可变类型
String是不可变类型的一个例子。
▪字符串对象总是表示相同的字符串。
▪由于字符串是不可变的,一旦创建,字符串对象总是有相同的值。
▪要在字符串的末端添加一些东西,你必须创建一个新的字符串对象:

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

StringBuilder是一个可变类型
StringBuilder是可变类型的一个示例
它有删除部分字符串、插入或替换字符等方法。
这个类有改变对象值的方法,而不只是返回新值:

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

当只有一个引用指向该值,没有区别
有多个引用的时候,差异就出现了

当另一个变量t指向与s相同的字符串对象,另一个变量tb指向与sb相同的StringBuilder时,不可变对象与可变对象之间的差异就变得更加明显。

可变类型的优势
使用不可变类型,对其频繁修改会产生大量的临时拷贝(需要垃圾回收)
可变类型最少化拷贝以提高效率
使用可变数据类型,可获得更好的性能
也适合于在多个模块之间共享数据

突变的风险
不可变类型更“安全”,在其他质量指标上表现更好
可变性使得难以理解程序正在做什么,更难满足方法的规约
折中,看你看重哪个质量指标

风险
该函数超出了spec范畴,因为它改变了输入参数的值
传递可变对象是一个潜在的错误源泉,一旦被无意中改变,则这种错误非常难于跟踪和发现
对其他程序员来说,也难以理解

如何修改代码?
通过防御式拷贝,给客户端返回一个全新的Date对象
大部分时候该拷贝不会被客户端修改,可能造成大量的内存浪费
如果使用不可变类型,则节省了频繁复制的代价
不可变类型不需要防御式拷贝

别名使可变类型具有风险
安全的使用可变类型:局部变量,不会涉及共享;只有一个引用
如果有多个引用(别名),使用可变类型就非常不安全

4.快照图作为代码级、运行时和时刻视图

软件构造:转换btw视图

快照图
为了理解一些微妙的问题,画出运行时发生的事情对我们来说是很有用的
用于描述程序运行时的内部状态

为什么我们使用快照图?
便于程序员之间的交流
便于刻画各类变量随时间变化
便于解释设计思路
为后续课程中更丰富的设计符号铺路。

快照图中的原语和对象值
基本类型的值
基本类型的值由裸常数表示。传入箭头是对变量或对象字段的值的引用。

对象类型的值
对象值是一个按其类型标记的圆。
-当我们想要显示更多的细节时,我们在里面写字段名,用箭头指向它们的值。对于更详细的信息,字段可以包括它们声明的类型。

重新分配和不可变的值
例如,如果我们有一个字符串变量s,我们可以将它从值“a”重新赋值为“ab”

String s = "a";
s = s + "b";

String是不可变类型的一个例子,这种类型的值一旦创建就永远不会改变。
不可变的对象(它们的设计者希望它们总是表示相同的值)通过双边框在快照图中表示,就像图中的字符串对象一样。

可变值
相比之下,StringBuilder(一个内置的Java类)是一个可变的对象,它表示一个字符串,并且它有一些方法可以改变对象的值:

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

这两个快照图看起来非常不同,这是很好的:可变性和不可变性之间的区别将在使代码免于错误方面扮演重要角色。
在这里插入图片描述未赋值的/不变的引用
java也给我们不变的引用不可变的引用:变量分配一次,从来没有重新分配。要使引用不可变,请使用关键字final声明它:

final int n = 5;

如果Java编译器不相信您的最终变量只会在运行时分配一次,那么它将产生一个编译器错误。因此final对不可变引用进行了静态检查。
在快照关系图中,不可重分配的引用(final)用双箭头表示。

不可变的引用:用双线箭头
在这里插入图片描述引用是不可变的,但指向的值却可以是可变的
可变的引用,也可指向不可变的值

5.复杂数据类型:数组和集合

数组Array
数组是另一种类型t的定长序列。例如,如何声明一个数组变量,并构造一个数组值分配给它:

int[] a = new int[100];

定长数组不可改变长度

数组类型的操作包括:
索引: a[2]
赋值: a[2]=0
长度:a.length

列表List
列表是另一种类型T的可变长度序列。

List<Integer> list = new ArrayList<Integer>();

一些操作:
索引:list.get(2)
赋值:list.set(2, 0)
长度:list.size()

请使用列表而不是数组

List是一个接口。
列表中的成员必须是对象

在这里插入图片描述集合Set
集合是0个或多个惟一对象的无序集合

一个对象不能在一个集合中出现多次。不是进来就是出去

s1.contains(e) 测试集合是否包含元素
s1.containsAll(s2) 测试是否s1 ⊇ s2
s1.removeAll(s2) 从s1中删除s2

集合是一个抽象的区间
在这里插入图片描述映射Map
映射类似于字典(key-value)

map.put(key, val) 添加映射key→val
map.get(key) 获取key的value
map.containsKey(key) 测试map是否有key
map.remove(key) 删除一个映射

Map是一个抽象接口
在这里插入图片描述

声明List, Set和Map变量
使用Java集合,我们可以限制集合中包含的对象的类型

当添加一个item时,编译器执行静态检查,确保只添加合适类型的item。

因此,可确保取出的值是指定类型的。

List<Integer> list = new ArrayList<Integer>();
List<Integer> list = new ArrayList<>();

声明

List<String> cities; // a List of Strings
Set<Integer> numbers; // a Set of Integers
Map<String, Turtle> turtles; // a Map with String keys and Turtle values

我们不能创建原始类型的集合
例如,Set不起作用
但是,int有一个我们可以使用的整型包装器(例如Set numbers)。
当使用:

sequence.add(5); // add 5 to the sequence
int second = sequence.get(1); // get the second element

创建列表、设置和映射变量
Java有助于区分种类型的说明和实现

List、Set和Map都是接口
它们定义了这些不同类型的工作方式,但不提供实现代码。
优点:用户有权在不同的情况下选择不同的实现
List、Set和Map的实现:
– List: ArrayList和LinkedList
– Set: HashSet
– Map: HashMap

List<String> firstNames = new ArrayList<String>();
List<String> lastNames = new LinkedList<String>();

List<String> firstNames = new ArrayList<>();
List<String> lastNames = new LinkedList<>();

Set<Integer> numbers = new HashSet<>();

Map<String,Turtle> turtles = new HashMap<>();

迭代器Iteration
迭代器是一个对象,它遍历一组元素并逐个返回元素
for(…:…)形式的遍历,调用的是被遍历对象所实现的迭代

迭代器有两种方法:
next()返回集合中的下一个元素——这是一个mutator方法
hasNext()测试迭代器是否已经到达集合的末尾
在这里插入图片描述在这里插入图片描述

6.有用的不变类

有用的不变类
基本类型及其封装对象类型都是不可变的
-如果你需要计算大的数字,BigInteger和BigDecimal是不可变的。

不要使用可变日期,使用适当的java不可变类型。根据您需要的计时粒度确定时间

Java集合类型的通常实现——List, Set,
Map -都是可变的:ArrayList, HashMap等等。

集合Collections实用程序类有方法来获取这些可变集合的不可修改的视图

Collections.unmodifiableList
Collections.unmodifiableSet
Collections.unmodifiableMap

围绕可变数据类型的不可变包装器
这种包装器得到的结果是不可变的:只能看;但是这种“不可变”是在运行阶段获得的,编译阶段无法据此进行静态检查

不可变包装器
不可修改的包装器通过拦截将修改集合的所有操作并抛出UnsupportedOperationException来剥夺修改集合的能力。

不可更改的包装有两个主要用途,如下:
-使一个集合一旦建立就不可变。在这种情况下,最好不要维护对支持集合的引用。这绝对保证了不变性。
-允许某些客户只读访问您的数据结构。您保留了对支持集合的引用,但分发了对包装器的引用。通过这种方式,客户端可以查看但不能修改,而您可以维护完全访问。

7.空引用

空引用
▪在Java中,对对象和数组的引用也可以采用特殊的Null值,这意味着引用不指向对象。空值是Java类型系统中一个不幸的漏洞

原语不能为空,编译器将拒绝这种尝试与静态错误:

int size = null; //illegal

可以将null赋值给任何非基元变量,编译器在编译时欣然接受这段代码。但你会在运行时得到错误,因为你不能调用任何方法或使用这些引用中的任何字段(抛出nullpointerexception):

String name = null; name.length(); 
int[] points = null; points.length;

null不等于空字符串“”或空数组

非空的collection可以包含null值

一旦有人试图使用集合的内容,这些空值就可能导致错误。

String[] names = new String[] { null };

List<Double> sizes = new ArrayList<>();


sizes.add(null);

在这里插入图片描述
Null值是非常麻烦和不安全的,以至于建议您从设计词汇表中删除它们

Null值在参数和返回值中被隐式禁用。如果一个方法允许参数为空值,那么它应该显式地声明它,或者如果它可能返回一个空值作为结果,那么它应该显式地声明它。但这些通常都不是好主意。

8.总结

静态类型检查:
-远离bug。静态检查有助于在运行前捕获类型错误和其他bug,从而提高安全性。
-容易理解。它有助于理解,因为类型是在代码中显式声明的。
-做好改变的准备。静态检查通过标识需要同步更改的其他位置,使更改代码变得更容易。例如,当您更改变量的名称或类型时,编译器会立即在使用该变量的所有地方显示错误,并提醒您也要更新它们。

可变性对于性能和便利性非常有用,但它也会带来bug风险,因为它要求使用对象的代码在全局级别上表现良好,这极大地增加了我们必须进行的推理和测试的复杂性,以确保其正确性。

确保你理解了不可变对象(比如字符串)和不可变引用(比如最终变量)之间的区别

快照图可以帮助理解这一点。
对象是值,由快照图中的圆圈表示,不可变的对象有两个边界,表示它的值从不改变。
-引用是指向一个对象的指针,由快照图中的箭头表示,不可变引用是一个带有双线的箭头,表示该箭头不能移动到另一个对象上。

关键的设计原则是不变性:尽可能多地使用不可变的对象和不可变的引用。
-远离bug。不可变对象不容易受到别名造成的bug的影响。不可变引用总是指向同一个对象。
-容易理解。因为不可变的对象或引用总是意味着相同的东西,所以对于代码的读者来说,推理起来更简单
-他们不需要跟踪所有的代码找到所有的地方,对象或引用可能会被改变,因为它不能被改变。
-做好改变的准备。如果是一个对象或参考

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值