软件构造(4-8讲,前半)

软件构造(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、前置/后置条件
  1. spec是程序员自己对所写的方法的规约,它规定了方法应该做什么,不应该做什么,有了spec就可以编写测试用例了,因为程序员所编写的代码必定是符合spec的,否则就是不合格的,符合spec的代码也必然能通过根据spec所设计出的测试。

  2. spec的结构:

    1. precondition(前置条件):对客户端的约束,在使用方法时必须满足的条件。(用于限制输入)

      1. 使用@param annotation说明每个参数的前置条件
    2. postcondition(后置条件):对开发者的约束,方法结束时必须满足的条件。(用于限制输出)

      1. 使用@return annotation说明后置条件
      2. 使用@throws annotation说明出现异常的时候会发生什么
    3. 在方法声明中使用static等关键字声明,可据此进行静态类型检查

  3. spec不能暴露实现细节,不应该暴露局部变量,也不应该暴露私有的数据域,这些东西一旦暴露,就有可能给被非法的程序员利用,发现漏洞并实施攻击。

  4. 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类会使程序健壮性更强。

解决方法:

  1. 对类的域由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)进行外部修改,显然,他并没有完成我们消除表示泄露的要求。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值