软件构造(4-8讲,前半)
1. 基本数据类型、对象数据类型
Java中,数据类型分为基本数据类型(int、boolean、char等)和对象数据类型(String、Integer等)。所有的基本数据类型都是Immutable的,而且在栈中分配内存,代价也比较低。而对象数据类型有的是Immutable的,有的是Mutable的,分配的内存都在堆中,代价相对昂贵。
2. 静态/动态类型检查
编程语言按照类型检查可以分为两大类:静态类型 (Static Typing) 和 动态类型 (Dynamic Typing)。
静态类型语言中,变量具有类型,而且在编译期确定,具有某一类型的变量只能持有相同类型的数据。
动态类型语言中,变量没有类型,只有数据有类型,变量可以持有任意类型的数据。
例如,C是静态类型语言,一个int型变量只能作为int来处理。Python是动态语言,变量可以持有整数、字符串、列表、闭包等任何数据。java通常被认为是静态语言,然而准确来说并非如此。java的变量有类型,但是变量可以持有子类型的数据,具体是什么类型,是由运行时的数据决定的。这显然是动态语言的特性。譬如:Java中Object类型的变量,可以持有任意数据,因为任意类型都是Object的子类。如果所有的变量和函数参数都声明为Object类型,恐怕Java就可以作为动态语言使用了。静态类型检查,在编译阶段进行类型检查,这意味着避免了将错误带入到运行阶段,可以提高程序的正确性/健壮性,例如语法错误、类名/函数名错误,参数类型或数目错误、返回值类型错误都可以在静态类型检查时发现;动态类型检查,在运行阶段才会进行类型检查,例如非法的参数值 (最典型的NULL引用)、非法的返回值、越界等等。
静态类型检查是关于数据类型的检查,它不会关心具体的值,而动态类型检查是关于值的检查。
例如int n=1.1在静态类型检查的时候就会报错,但double a=0; double b=2/a;只有在运行之后,执行动态类型检查的时候才会报告除零错。
3. Mutable/Immutable(对象的可变性和不可变性)
不可变对象:一旦该类被创建,其值不能被改变。
可变对象:创建后,其值可以被改变(拥有方法可以修改自己的值/引用)。
例:String和StringBuilder。前者是不可变的,对于每次String的修改,并不是修改内在成员的值,而是产生了一个新的String对象,且其值为修改后的值,同时存储空间的位置发生了变化,然后引用之。后者是可变的,对于每次StringBuilder的修改,就是直接将该对象值进行修改,存储空间位置不变。
后续的显示泄露有有关该方面的应用。
4. 防御式拷贝
public final class Period { private final Date start; private final Date end; public Period(Date start, Date end) { if (start.compareTo(end) > 0) throw new IllegalArgumentException( start + " after " + end); this.start = start; this.end = end; } public Date start() { return start; } public Date end() { return end; } ... // Remainder omitted }
这个类看上去没有什么问题,时间是不可改变的,无显示泄露。然而Date类本身是可变的。
Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); end.setYear(78); // 通过改变date类改变了period的内容
因此,为了保护Period实例的内部信息避免受到修改,导致问题,对于构造器的每个可变参数进行防御性拷贝(defensive copy)是必要的。操作如下:
public Period(Date start, Date end) { this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if (this.start.compareTo(this.end) > 0) throw new IllegalArgumentException( this.start + " after " + this.end); }
5. Snapshot diagram
1.使用单线箭头指向实际值,不需要表明数据类型。
2.如果是可变对象,使用单线椭圆,椭圆内写明对象的类型及对象内的值。
3.如果是不可变对象,使用双线椭圆,椭圆内写明对象的类型及对象内的值。
4.如果是对象的不可变引用(final标记),使用双线箭头。eg:id
如果是对象的可变引用,使用单线箭头。eg:age
Eg:
String s1 = new String("abc"); List<String> list = new ArrayList<String>(); list.add(s1); s1 = s1.concat("d"); System.out.println(list.get(0));//输出abc String s2 = s1.concat("e"); list.set(0,s2); System.out.println(list.get(0));//输出abcde
第一次输出时的snapshot图:
第二次输出时的snapshot图:
6.Specification、前置/后置条件
spec是程序员自己对所写的方法的规约,它规定了方法应该做什么,不应该做什么,有了spec就可以编写测试用例了,因为程序员所编写的代码必定是符合spec的,否则就是不合格的,符合spec的代码也必然能通过根据spec所设计出的测试。
spec的结构:
precondition(前置条件):对客户端的约束,在使用方法时必须满足的条件。(用于限制输入)
- 使用@param annotation说明每个参数的前置条件
postcondition(后置条件):对开发者的约束,方法结束时必须满足的条件。(用于限制输出)
- 使用@return annotation说明后置条件
- 使用@throws annotation说明出现异常的时候会发生什么
在方法声明中使用static等关键字声明,可据此进行静态类型检查
spec不能暴露实现细节,不应该暴露局部变量,也不应该暴露私有的数据域,这些东西一旦暴露,就有可能给被非法的程序员利用,发现漏洞并实施攻击。
spec的评判标准
by——规约的确定性、规约的陈述性、规约的强度。
而其中重点是规约的强度的判断,spec变强的要求是更宽松的前置条件+更严格的后置条件,在这种情况下,就可以用变强了的spec去替换原来的spec。越强的规约,意味着implementor的自由度和责任越重,而client(调用者)的责任越轻。
测试用例的设计
测试用例可以分为两类:black box、glass box。
black box是针对preconditon与postcondition,充分考虑各种边界条件来设计的测试用例。
glass box目前存疑。
5. ADT(抽象数据型)操作
ADT的操作
Creators构造器:用于使用 new 关键字创建一个新的对象。还有一种方法是静态方法,如Arrays.asList()、String.valueOf(Object Obj)等。
Producers生产器:用于使用一个存在的对象产生一个新的对象,例如String.concat()就是使用已存在的字符串构造出一个新的对象,而且不会改动原先存在的对象。
Observers观察器:不对数据做任何改动,只是查看一个已经存在的对象的各个值,如List.size()、所有的getter方法等。
Mutators变值器:用于改变对象属性的方法,如List.add()。mutator通常返回void,因为它不需要对外界做出反应,只是对ADT的数据域做了更改;mutator也可能返回非空,比如返回boolean表示修改成败等。
设计ADT
设计一个好的ADT需要靠开发者的经验来设计它的操作的spec,设计一个ADT要遵循下面三个原则:
- 设计简洁一致的操作
- 要足以支持client所需要的对数据的所有操作,且用操作的难度要低
- 要么抽象要么具体,不要混合——要么针对抽象设计,要么针对具体应用的设计。
ps: 实现一个ADT的三个部分:specification、representation、implementation
Representation Independence 表示独立性
client不应该知道内部的数据域是怎么实现的,最好client只能通过ADT提供的getter方法获得ADT存储的数据。
即client使用ADT时无需考虑其内部如何实现,ADT内部表示的变化不应影响外部spec和客户端。其目的一个是为了便于未来的升级和维护,当内部发生变化的时候不会影响到client。
测试ADT
因为测试相当于client使用ADT,所以它也不能直接访问ADT内部的数据域,所以只能调用其他方法去测试被测试的方法。
针对creators, producers, and mutators:构造对象之后,用observer去观察是否正确
针对observer:用其他三类方法构造对象,然后调用被测observer,判断观察结果是否正确
不变性:
不变量:任何时候都是true,由ADT负责,与client的任何行为无关
- immutability就是一个典型的不变量
RI & AF:
两个空间 R 和 A:R空间是ADT的内部表示的空间,A空间是ADT能够表示的存在于实际当中的对象。ADT开发者关注表示空间R,client关注抽象空间A
- 抽象函数(AF):从R空间到A空间存在一个映射,这个映射是一个满射,这个映射将R中的每一个值解释为A中的一个值。这个解释函数就是AF。(AF:R→A)
- 表示不变性(RI):是一个集合,是R空间所有值的子集,它包含了所有合法的表示值,而只有满足RI的值,才是合法值,才会在A空间内有值与其对应。(RI:R→boolean)
相同的R空间有肯能会有不同的RI。即使是同样的R、同样的RI,也可能有不同的AF,即“解释不同”。
checkRep()
:用于随时检查RI是否满足。使用assert检查RI,在所有的方法最好都加入调用这个检查方法。checkRep()在检查时有可能耗费大量的时间影响性能,所以只需要在开发阶段保留这部分。用assert进行合法性检测
Eg:
public static void main(String[] args) { int x = Math.abs(-987); assert x >= 0; System.out.println(x); }
assert是boolean,assert x >=0 可视做为:
if(x>=0){System.out.println(x);} else{return AssertionError}//伪代码
但是,assert不能等同于if语句:assert语句仅仅在debug(调试)版本中才有效,而if正式版本中仍旧有效。
assert中的布尔表达式只要false便会终止程序抛出异常,并且说这个地方不符合规矩,但if在执行完之后,可能并不会发生报错,而是程序继续运行下去,导致某个非法数据被隐藏起来,从而导致了更大的错误却不能准确定位到错误位置。
assert x >= 0 : "x must > 0 "
当assert的Boolean为false时,
AssertionError
会带上消息x must >= 0
,更加便于调试。assert特点:
- 便于在程序调试时发现错误
- 不会影响程序执行效率
- 断言只能用于调试,不能作为程序的功能。
- 断言可以帮助我们定位错误,而不是排除错误()
- 断言不是用来检查程序的错误的,断言为假只会中断程序的执行,报告程序是错误的,而不会对错误进行相应的处理。
- 断言不能用来判断有可能发生的情况是否会发生,不能对函数的入口参数进行合法性检查,不能用断言代替条件语句,不能用断言判断有可能发生的错误,只能用于检查程序中不能发生的错误确实不会发生
- 断言一般用与检查函数参数的合法性(有效性)而不是正确性,但是合法的程序并不见得就是正确的程序。
摘抄自:(https://blog.csdn.net/qq_45774552/article/details/107029783?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162557579216780262568101%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=162557579216780262568101&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_v2~rank_v29-2-107029783.pc_search_result_before_js&utm_term=java%E7%94%A8assert%E8%BF%9B%E8%A1%8C%E5%90%88%E6%B3%95%E6%80%A7%E6%A3%80%E6%B5%8B&spm=1018.2226.3001.4187)表示泄露
java以类为单位,而每一个类中都定义了许多的变量、函数。
这些变量分为immutability、mutability两种。在java内部,immutability回的是内容,例如int、float、String。mutability返回的是指针,例如map、list、set等。
所谓表示泄露,即:程序可以在外部对内部的类属性进行修改的不安全状况。(s我们期望的是:内部的属性,应该用内部的方法来修改,而不应该外部引用就可以修改。)
eg:
class Person{ public String name; public int num; public List<String> post = new ArrayList<>(); public Person(String name,int num,List<String> post) { this.name=name; this.num=num; this.post=post; } } public class IOtest3 { public static void main(String[] args) { List<String> post = new ArrayList<>(); post.add("野王"); Person me = new Person("我",30,post); System.out.println(me.name+"是"+me.num+"段"+me.post); } }
输出为: 我是30段[野王]
看起来还不错,但是我们在外部对其修改:
class Person{ public String name; public int num; public List<String> post = new ArrayList<>(); public Person(String name,int num,List<String> post) { this.name=name; this.num=num; this.post=post; } } public class IOtest3 { public static void main(String[] args) { List<String> post = new ArrayList<>(); post.add("野王"); Person me = new Person("我",30,post); me.name=me.name.concat("真的"); me.num=100; me.post.remove(0); me.post.add("戛然小姐的狗"); System.out.println(me.name+"是"+me.num+"段"+me.post); } }
输出为: 我真的是100段[戛然小姐的狗]
可以发现,控制台的输出使类的内部属性都被修改了。这就导致我们编写的类是不安全的,内部的属性,应该用内部的方法来修改,而不应该外部引用就可以修改。编写immutable类会使程序健壮性更强。
解决方法:
对类的域由public改为private,这样你会发现我们无法直接通过me.name和me.num来直接获得内部属性。当然,要加上部分方法:
class Person{ private String name;//将域改为private private int num; private List<String> post = new ArrayList<>(); public Person(String name,int num,List<String> post) { this.name=name; this.num=num; this.post=post; } public String getName(){//因为main中无法通过me.name得到name值,故添此 return name; //方法 } public int getNum(){ return num; } public List<String> getPost(){//见下文↓ List<String> post2 = new ArrayList<>(); for(int i=0;i<post.size();i++){ post2.add(post.get(i)); } return post2; } } public class IOtest3 { public static void main(String[] args) { List<String> post = new ArrayList<>(); post.add("野王"); Person me = new Person("我",30,post); System.out.println(me.getName()+"是"+me.getNum()+"段"+me.getPost()); } }
注意方法:public List getPost(),书写方式和getName有所不同:开篇中提到,List返回的是指针,在本例中,若写为
public List<String> getPost(){ return post; }
则此方法返回的是指向post的指针,此时,在main中依旧可以通过譬如me.getPost.remove(0)进行外部修改,显然,他并没有完成我们消除表示泄露的要求。
注意方法:public List getPost(),书写方式和getName有所不同:开篇中提到,List返回的是指针,在本例中,若写为
public List<String> getPost(){ return post; }
则此方法返回的是指向post的指针,此时,在main中依旧可以通过譬如me.getPost.remove(0)进行外部修改,显然,他并没有完成我们消除表示泄露的要求。