软件构造 3-1 Data Type and Type Checking

3.1 数据类型与类型检验

一. Java 数据类型

   Java 数据类型是一些值的集合,以及这些值对应的操作

  以如下 5 种常用的原始类型为例 :

  • int
  • long
  • double
  • char
  • boolean

  对象类型有:

  • String
  • BigInteger 表示任意大小的整数

  从 Java 的传统来说,原始类型用小写字母,对象类型的起始字母用大写

  有一些操作符可以对不同类型的对象进行操作,这时我们就称之为可重载overloaded ),例如 Java 中的算术运算符 +, -, *, / 都是可重载的。一些函数也是可重载的。大多数编程语言都有不容程度的重载性。

  总之分类为:基本数据类型面向对象的数据类型

PrimitivesObject Reference Types
intlongbyteshortcharfloatdoublebooleanClassesinterfacesarraysenumsannotations
只有值没有 ID(与其他值无法区分)既有ID,也有值
不可变的一些可变一些不可变
中分配内存中分配内存
Can’t achieve unity of expressionUnity of expression with generics
代价低代价昂贵

  对象类型形成层次结构——继承关系:

在这里插入图片描述

1.1 静态、动态类型语言及检测

1.1.1 静态、动态类型语言

  Java 是一种静态类型的语言。所有变量的类型在编译的时候就已经知道了(程序还没有运行),所以编译器也可以推测出每一个表达式的类型。

  • 例如,如果 abint 类型的,那么编译器就可以知道 a + b 的结果也是 int 类型的。事实上,Eclipse 在你写代码的时候就在做这些检查,所以你就能够在编辑的同时发现这些问题。

  在动态类型语言中(例如 Python ),这种类型检查是发生在程序运行的时候。

  参考:弱类型、强类型、动态类型、静态类型语言的区别是什么?

  静态类型静态检查的一种——检查发生在编译的时候。

1.1.2 静态、动态类型检测,无检查

  编程语言通常能提供以下三种自动检查的方法:

  • 静态检查: bug 在程序运行前发现,一般是编译阶段。实质上是判断赋的值是否在相应集合内。
    int 在整数集内。其避免了将错误代入到运行阶段,可提高程序正确性 / 健壮性。
  • 动态检查: bug 在程序运行中发现 ,一般是运行阶段。检测具体某一个值是否出错。
  • 无检查: 编程语言本身不帮助你发现错误,你必须通过特定的条件(例如输出的结果)检查代码的正确性。

  静态类型检测有:

  1. 语法错误    需要注意的是,即使在动态类型的语言例如 Python会做这种检查:如果你有一个多余的缩进,在运行之前就能发现它。
  2. 类名 / 函数名错误
  3. 参数数目错误
  4. 参数类型错误
  5. 返回值类型错误

  动态类型检测有:

  1. 非法的参数值 例如除数为 0
  2. 非法的返回值
  3. 越界
  4. 空指针

  静态检查倾向于类型错误,即与特定的值无关的错误。正如上面提到过的,一个类型是一系列值的集合,而静态类型就是保证变量的值在这个集合中,但是在运行前我们可能不会知道这个值的结果到底是多少。所以如果一个错误必须要特定的值来“触发”(例如除零错误和越界访问),编译器是不会在编译的时候报错的。

  与此相对的,动态类型检查倾向于特定值才会触发的错误。

1.1.3 无检查——特定的错误类型

  一些错误是不会被检查出来的。换言之,原始数据类型的对象在有些时候并不像真正的数字那样得到应有的输出。例如:5/2 并不能得到 2.5 ;整数加减时可能溢出从而得到意想不到的值;浮点数运算得到 NaN

1.1.4 示例

  不要因为一些语言的习惯而搞错了其在 Java 中的正确性,看下面的代码:

int n = 5;
if (n) {
  n = n + 1;
}

  这是静态错误,错误出现在第二行, n 是 int 类型,不是 boolean 类型。

double sum = 7;
double n = 0;
double average = sum / n;

  不会报错,但是得到了错误的结果。7/0 结果是 infitity

二. 可变性与不可变性

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

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

1. Immutable 不变性

  一旦被创建,其值不能被改变。要改变新建存储空间改变指向


  使用不可变类型要比可变类型安全的多,同时也会让代码更易懂、更具备可改动性。可变性会使得别人很难知道你的代码在干吗,也更难制定开发规定(例如规格说明)。


  如果编译器无法确定 final 变量不会改变,就提示错误,这也是静态类型检查的一部分。

  所以,尽量使用 final 变量作为方法的输入参数、作为局部变量


  final

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

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

  StringBigIntegerBigDecimal原始类型包装类型就是不可变类型的对象。

2. Mutable 可变性

  优化性能是我们使用可变对象的原因之一。另一个原因是为了分享:程序中的多个对象可以通过共享一个数据结构共享信息的改变。


  StringBuilderListSetMap 等常见的聚合类、ArrayListHashMap 都是可变的, Date 就是可变类型的变量。


  Collections 类中提供了可以获得不可变方法

  这样的类,会使如 addremoveput 这样的修改触发异常

  但这实质上这仅仅是一层包装,如果不小心让别人别人或自己使用了底层可变对象的索引,这些看起来不可变对象还是会发生变化。


  当只有一个引用指向该值,没有区别。

  有多个引用时,差异就出现了。这种情况称为“别名”。


  防御式拷贝模式可以防止可变类型带来的隐藏 bug,但这会很浪费空间和时间

  相反,如果使用不可变类型,不同的地方用不同的对象来表示,相同的地方都索引到内存中同一个对象,这样会让程序节省空间复制的时间。所以说,合理利用不变性对象(大多是有多个变量索引的时候)的性能比使用可变性对象的性能更好。

3. 对比

  Immutable 包含任何改变属性的方法能有 public 属性以任何方法修改(返回)属性的。最主要的特点是方便安全

  但使用不可变类型,对其频繁修改会产生大量的临时拷贝,需要垃圾回收。而可变类型最少化拷贝提高效率

String s = "";
for (int i = 0; i < r; ++i) {
	s = s + n;
}

StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; ++i) {
	sb.append(String.valueOf(i));
}
String s = sb.toString();

  总之,使用可变数据类型,也可获得更好的性能,也适合于在多个模块之间共享数据(像全局变量)。


  如果在 effects 内没有显式强调输入参数会被更改,我们认为方法会修改输入参数


  不可变类型更“安全”,在其他质量指标上表现更好。


  样例:

String s = " Hello ";
s += " World ";
s.trim( );
System.out.println(s);

  需要注意的是:

See the specification of String.trim()
Returns:
A copy of this string with leading and trailing white space removed, or this string if it has no leading or trailing white space.//返回去除前后空格的拷贝而不是改变原有对象

  故返回的是

" Hello World "//打印的结果

在这里插入图片描述

3.

  函数调用时,如果调用了 List可变型的数据类型,可能改变其值。函数可能超出了 spec 范畴,改变了输入参数的,而这种错误非常难于跟踪和发现。

  如:

/** @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 values of the number in the list */
public static int sumAbsolute(List<Integer> list) {
	// let's reuse sum(), because DRY, so first we take absolute values
	for (int i = 0; i <list.size(); ++i)
		list.set(i, Math.abs.(list.get(i)));
	return sum(list);
}

// meanwhile, somewhere else in the code...
public static void main(String[] args) {
	// ...
	List<Integer> myData = Arrays.asList(-5, -3, -2);
	System.out.println(sumAbsolute(myData));
	System.out.println(sum(myData));
}

  而实际上输出的是:

10
10

  与预期的

10 
-10

  不相同,这是因为过程中 List 的值发生改变。


/** @return the first day of spring this year */
public static Date startOfSpring() {
	return askGroundhog();
}
/** @return the first day of spring this year */
public static Date startOfSpring() {
	if (groundhogAnswer == null) groundhogAnswer = askGroundhog();
	return groundhogAnswer;
}
private static Date groundhogAnswer = null;
//somewhere else in the code...
public static void partyPlanning() {
	//let's have a party one month after spring starts!
	Date partyDate = startOfSpring();
	partyDate.setMonth(partyDate.getMonth() + 1);
	// ... uh-oh. what just happened?
}

   Date 可变变量,由于 partyDategroundhogAnswer 指向同一空间,前者的改变使得后者也改变。

在这里插入图片描述

4. 修改方式

  防御式拷贝:给客户端返回一个全新的对象。但大部分时候该拷贝不会被客户端修改,可能造成大量的内存浪费。

  如果使用不可变类型,则节省了频繁复制的代价。


  安全的使用可变类型:局部变量会涉及共享只有一个引用

  如果有多个引用(别名),使用可变类型就非常不安全。

三. 代码快照图(code-levelrun-timemoment)

  代码快照图用于描述程序运行时的内部状态。

1. 原始值与对象值

  原始值(基本类型的值):

  原始值都是以常量来表达的。箭头的来源可以是一个变量或者一个对象的内部区域( field )。

  对象值(对象类型的值):

  一个对象一个圆表示。对象内部会有很多区域( field ),这些区域又指向它们对应的值。同时这些区域也是有它们的类型的,例如 int x

在这里插入图片描述

  对象属性对象一同放入中。

2. 可变对象与不可变对象

  不可更改的对象(设计者希望它们一直是这个值)在快照图中以双圆圈边框表示,例如字符串对象:

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

在这里插入图片描述

  与此相对应的, StringBuilder (Java的一个内置类) 是一个可更改的字符串对象,它内置了许多改变其内容的方法:

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

  可变对象:

在这里插入图片描述

   Java 也提供了不可更改的引用:

   final 声明,变量一旦被赋值不能再次改变它的引用指向的值或者对象,即限定了指向)。

  如果 Java 编译器发现 final 声明的变量在运行中被赋值多次,它就会报错。所以 final 就是为不可更改的引用提供了静态检查

  在快照图中,不可更改引用final)用双箭头表示。

在这里插入图片描述
  这里要特别注意一点, final 只是限定了引用不可变,我们可以将其引用到一个可更改的值 (例如 final StringBuilder sb ),虽然引用不变,但引用的对象本身的内容(指向的值)可以改变

  可变的引用,也可指向不可变的值。

final StringBuilder sb = new StringBuilder("abc");
sb.append ("d");
sb = new StringBuilder("e");
System.out.println(sb);

  此时编译阶段出错,但会有输出,输出为

abcd

在这里插入图片描述

四. Java 数组及聚合类型、类与方法、API 文档

1. 数组

  数组是一连串类型相同的元素组成的结构,而且它的长度是固定的(元素个数固定)。
  其中我们用到了 a.length 诸如此类的操作,不加括号。这是由于他不是一个类内的方法调用,不能在其后加上括号和参数。

2. 列表 List

   List 类型是一个长度可变的序列结构,并且是抽象接口(定义类型的工作,但是不提供具体的实现代码)。列表可以包含零个多个对象,而且对象可以出现多次。我们可以在列表中删除添加元素。我们可以这样声明列表:

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

  常用的操作符有下:

  • 索引一个元素: list.get(2)
  • 赋予一个元素特定的值: list.set(2, 0)
  • 求列表的长度list.size()
  • 在列表的末尾添加元素list.add(e)
  • 测试列表是否为list.isEmpty()

  List 是一个接口,无法进行实例化List 只能存对象不能存储基本类型,否则将在编译阶段出错。

  由于 List 是一个接口,这种类型的对象无法直接用 new 来构造,但是它指定了 List 必须提供的操作。 ArrayList 是一个实类型的类( concrete type ),它提供了 List 操作符的具体实现。当然, ArrayList 不是唯一的实现方法(还有 LinkedList 等),但是是最常用的一个。

  
  另外要注意的是,我们要写 List<Integer> 而不是 List<int> 。因为 List 只会处理对象类型而不是原始类型。在 Java 中,每一个原始类型都有其对应的对象类型原始类型使用小写字母名字,例如 int ,而对象类型开头字母大写,例如 Integer )。当我们使用尖括号参量化一个类型时, Java 要求我们使用对象类型而非原始类型。在其他的一些情况中, Java自动原始类型对等的对象类型之间相转换。

在这里插入图片描述

3. 映射 Map

  Map 是一个二元组,且为抽象接口。在 Python 中,字典的 keys 必须是可哈希的Java 也是类似。常见的操作有:

  • 添加映射 key → valmap.put(key, val)
  • 获取 key 映射的值 如 map.get(key)
  • 测试 key 是否存在 如 map.containsKey(key)
  • 删除 key 所在的映射 如 map.remove(key)

  在快照图中,我们将 Map 表示为包含 key/value 序对的对象。例如一个 Map<String, Turtle>

在这里插入图片描述

4. 集合 Set

  集合是一种含有零个多个不重复对象聚合类型,并且为抽象接口。和映射中的 key 相同, Python 中的集合的元素也要求是可哈希的Java 也是类似。常见操作有:

  • 测试集合中是否含有es1.contains(e)
  • 测试是否 s1 ⊇ s2s1.containsAll(s2)
  • s1 中去除 s2 的元素 如 s1.removeAll(s2)

  在快照图中,我们不用数字索引表示集合的元素(即元素没有顺序的概念),例如一个含有整数的集合:

在这里插入图片描述

5. 列表 List 、映射 Map 、集合 Set 的共性

  特别地,我们对于初始化,有如下规则:

   Python 提供了创建列表和字典的方便方法:

lst = [ "a", "b", "c" ]
dict = { "apple": 5, "banana": 7 }

   Java 不是这样 它只为数组提供了类似的创建方法:

String[] arr = { "a", "b", "c" };

  我们可以用 Arrays.asList 从数组创建列表:

Arrays.asList(new String[] { "a", "b", "c" })

  或者直接提供元素:

Arrays.asList("a", "b", "c")

  要注意的是,如果一个 List 是用 Arrays.asList 创建的,它的长度固定了。

  在 Python 中,聚合类中的元素的类型可以不同,但是在 Java 中,我们能够要求编译器对操作进行静态检查,确保聚合类中的元素类型相同

  由于 Java 要求元素的普遍性,我们不能直接使用原始类型作为元素的类型,例如Set<int> ,但是,正如前面所提到的, int 有一个对应的 Integer ”包装“对象类型,我们可以用

Set<Integer> numbers.

  为了使用方便, Java自动在原始类型和包装过的对象类型中做一些转换,所以如果我们声明一个 List<Integer> sequence ,下面的这个代码也能够正常运行:

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

   List , Set , 和 Map 都是接口 :他们定义了类型的工作,但是他们不提供具体的实现代码。这有很多优点,其中一个就是我们能根据具体的环境使用更适合的实现方式。

  例如 List 的创建:

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

  如果左右两边的类型参数都是一样的,Java 可以自动识别,这样可以少打一些字:

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

  对于 List ,当你不确定时,使用 ArrayList

  对于 Set ,默认使用 HashSetJava 还提供了 sorted sets ,它使用 Treeset 实现。

  对于 Map,默认使用 HashMap

6. 迭代器 Iterator

  一定要注意在循环的时候不要改变你的循环参量(它是可改变的值)。

   Iteration可变的迭代器。迭代器使用有两种方法:

  • next() 返回下一个元素,会修改迭代器的方法(mutator method);它不仅会返回一个元素,而且会改变内部状态,使得下一次使用它的时候会返回下一个元素。
  • hasNext() 测试是否到达末尾
List<String> lst = ...;
Iterator iter = lst.iterator();
while (iter.hasNext()) {
	String str = iter.next();
	System.out.println(str);
}

在这里插入图片描述

   Java 也提供了一种使用数字索引进行迭代的方法。除非你真的需要数字索引,否则我们不推荐这种写法,它可能会引来一些难以发现的 bug


  使用迭代器是因为不同的聚合类型其内部实现的数据结构不都相同(例如连接链表、哈希表、映射等等),而迭代器的思想就是提供一个访问元素通用中间件。通过使用迭代器,使用者只需要用一种通用的格式就可以遍历访问聚合类的元素,而实现者可以自由的更改内部实现方法。

7. 枚举类型 enum

  当不可变的值的集合满足“”和“有限”这两个条件时,将这个集合中的所有值统一定义为一个命名常量就是有意义的。
  例如,我们这样初始化与命名:

public enum Month { 
    JANUARY, FEBRUARY, MARCH, APRIL, 
    MAY, JUNE, JULY, AUGUST, 
    SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER;
}

public enum PenColor { 
    BLACK, GRAY, RED, PINK, ORANGE, 
    YELLOW, GREEN, CYAN, BLUE, MAGENTA;
}
PenColor drawingColor;

  像引用一个被命名的静态常量一样来引用枚举类型的值:

drawingColor = PenColor.RED;

  需要强调的是,枚举类型是一个独特的新类型。较老的语言,像 Python2Java 的早期版本,它们倾向于使用数字常量或者字符串来表示这样的值的有限集。但是一个枚举型变量更加“类型安全”,因为它可以发现一些类型错误,如类型不匹配

int month = TUESDAY;  //  如果month定义为整型值(TUESDAY也是一个整型值),那么这样写不会报错(但是从语义上看是错的,因为显然不能将“周四”赋值给一个“月份”,这可能不符合作者的本意)
Month month = DayOfWeek.TUESDAY; // 如果month被定义为枚举类型Month,那么这条语句将会触发静态错误 (static error)

  或者拼写错误:

String color = "REd";        // 不报错,拼写错误被忽略
PenColor drawingColor = PenColor.REd; // 当枚举类型的值被拼写错时,会触发静态错误

8. 类、方法

   public 意味着任何在你程序中的代码都可以访问这个类或者方法。其他的类型修饰符,例如 private ,是用来确保程序的安全性的——它保证了可变类型不会被别处的代码所修改。

  在 Python 中,类的方法与普通的函数有一个特别的区别——它们必须有一个额外的第一个参数名称,但是在调用这个方法的时候你不为这个参数赋值, Python 会提供这个值。这个特别的变量指对象本身,按照惯例它的名称是 self

  虽然你可以给这个参数任何名称,但是强烈建议你使用 self 这个名称——其他名称都是不赞成你使用的。

  使用一个标准的名称有很多优点——你的程序读者可以迅速识别它,如果使用 self 的话,还有些 IDE(集成开发环境)也可以帮助你。

   static 意味着这个方法没有 self 这个参数—— Java 会隐含的实现它,所以你不会看到这个参数。静态的方法不能通过对象来调用,这与 List add() 方法 或者 String length() 方法形成对比,它们要求先有一个对象。静态方法的正确调用应该使用索引,例如对以下程序:`

public class Hailstone {
    /**
     * Compute a hailstone sequence.
     * @param n  Starting number for sequence.  Assumes n > 0.
     * @return hailstone sequence starting with n and ending with 1.
     */
    public static List<Integer> hailstoneSequence(int n) {
        List<Integer> list = new ArrayList<Integer>();
        while (n != 1) {
            list.add(n);
            if (n % 2 == 0) {
                n = n / 2;
            } else {
                n = 3 * n + 1;
            }
        }
        list.add(n);
        return list;
    }
}

  静态方法的调用如下:

Hailstone.hailstoneSequence(100)

9. Java API 文档

   API应用编程接口application programming interface )的简称。比如 Facebook 开放了一个供你编程的 API(实际上不止一个,因为需要对不同的语言和架构开放不同的 API ),那么你就可以用它来写一个和 Facebook 交互的应用。

  • java.lang.StringString 类型的全称。我们仅仅使用 "双引号" 这样的方式就可以创建一个 String 类型的对象。
  • java.lang.Integer 和其他原始包装器类。在多数情况下,Java 都会自动地在原始类型(如 int )和它们被包装( wrapped ,或者称为“封装,boxed ”)之后的类型之间相互转换
  • java.util.List 类似 Python 中的列表,但是在 Python 中,列表是语言的一部分。在 Java 中, List 需要用 Java 来具体实现。
  • java.util.Map 类似 Python字典
  • java.io.File 用于表示硬盘上的文件。让我们看看 File 对象提供的方法:我们可以测试这个文件是否可读、删除这个文件、查看这个文件最近一次被修改是什么时候。
  • java.io.FileReader 使我们能够读取文本文件
  • java.io.BufferedReader 让我们高效地读取文本文件。它还提供一个很有用的特性:一次读取一整行

   更深入地看看 BufferedReader 的文档:
在这里插入图片描述
  在这一页的顶部是 BufferedReader继承关系和一系列已经实现接口。一个 BufferedReader 对象可以调用这些被列出的类型中定义的所有可用的方法(加上它自己定义的方法)。

  接下来会看到它的直接子类,对于一个接口来说就是一个实现类。这可以帮助我们获取诸如 HashMapMap 的直接子类这样的信息。

  再往下是对这个类的描述。有时候这些描述会有一些模棱两可,但是如果你要了解一个类,这里就是你的第一选择。

  如果你想创建一个 BufferedReader,那么 constructor summary 版块就是你要看的资料。构造函数并不是 Java 中唯一获取一个新对象的方法,但它却是最为普遍使用的:
在这里插入图片描述

  接下来是 BufferedReader 对象中所有我们可以调用的方法的列表:

在这里插入图片描述

  在综述下面是每个方法和构造函数的详细描述。点击一个构造函数或者方法即可看到详细的描述。如果你想弄明白一个方法有什么作用,那你应该查看这里。

  每一个详细描述包括以下内容:

  • 方法签名signature):我们能看到方法返回值类型方法名,以及参数。我们还可以看到一些异常,就目前而言,它们就是运行这个方法可能导致的错误
  • 完整的描述。
  • 参数:描述这个方法接收的参数。
  • 对方法返回值的描述。

  规格说明

  这些详细的描述称为规格说明。它们使得我们能够在不了解具体实现代码的情况下使用诸如 String, Map, 或 BufferedReader 这样的工具。

五. 使用不可变数据类型

  基本类型及其封装对象类型都是不可变的。

  大数中的 BigIntegerBigDecimal 也是不可变的。

   JavaSetMapList 包装:这种包装器得到的结果是不可变的:只能看,即不能使用 get()set()remove()

  • Collections.unmodifiableList
  • Collections.unmodifiableSet
  • Collections.unmodifiableMap

  但是这种“不可变”是在运行阶段获得的,编译阶段无法据此进行静态检查。例如排序时,编译阶段编译器并不会发出警告,只会在运行时发出警告。

六. 测试目标

  • safe from bugs (SFB) 远离 bug。需要满足:正确性 (目前看起来是正确的)、防御性 (将来也会是正确的);
  • easy to understand (ETU) 易读性。我们不得不和以后有可能需要理解和修改代码的程序员进行交流 (修改 BUG 或者添加新的功能),那个将来的程序员或许会是几个月或者几年以后的你,如果你不进行交流,那么到了那个时候,你将会惊讶于你居然忘记了这么多,并且这将会极大地帮助未来的你有一个更良好的设计;
  • ready for change (RFC) 可改动性。软件总是在更新迭代的,一些好的设计可以让这个过程变得非常容易,但是也有一些设计将会需要让开发者扔掉或者重构大量的代码。
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值