软件构造课程随笔——3-1【数据类型与类型检查】

本文详细介绍了Java中的数据类型,包括基本数据类型与对象数据类型,探讨了静态和动态类型检查的区别,以及可变性和不可变性的重要概念。同时,文章还深入讲解了数组和集合类如List、Set、Map的使用方法,强调了不可变类型在提高代码安全性方面的作用。

编程语言中的数据类型

类型与变量
类型是一组值,以及可以对这些值执行的操作。在Java中分为基本数据类型和对象数据,通常基本数据类型小写开始,对象数据类型大写开始。
对于基本数据类型,按照值来区分,而对象数据类型则不是,即使值相同也未必相等,就像String不能用双等号,而需要用.equal来判断是否相等。

对象类型的层级关系
基本数据类型的包装类
Boolean, Integer, Short, Long, Character, Float, Double
实际上就是把基本数据类型封装为对象,但尽可能少用。另外在使用一些像List这样类时,不允许List,需要List list = new ArrayList();
在这个过程,list.add(1)会被自动转换为list.add(Integer.valueOf(1)),会降低效率。

java中基本数据类型与引用数据类型
在这里插入图片描述

静态和动态数据类型检查

静态类型

Java是一种静态类型的语言。所有变量的类型在编译的时候就已经知道了(程序还没有运行),所以编译器也可以推测出每一个表达式的类型。例如,如果a和b是int类型的,那么编译器就可以知道a+b的结果也是int类型的。事实上,Eclipse在你写代码的时候就在做这些检查,所以你就能够在编辑的同时发现这些问题。
在动态类型语言中(例如Python),这种类型检查是发生在程序运行的时候。
静态类型是静态检查的一种——检查发生在编译的时候。本课程的一个重要目标就是教会你避免bug的产生,静态检查就是我们知道的第一种方法。其中静态类型就阻止了一大部分和类型相关的bug——确切点说,就是将操作符用到了不对应的类型对象上。例如,如果你进行下面这个操作,试图将两个字符串进行算术乘法:
“5” * “6”
那么静态类型检查就会在你编辑代码的时候发现这个bug,而不是等到你编译后运行程序的时候。

静态检查、动态检查、无检查

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

静态检查: bug在程序运行前发现
动态检查: bug在程序运行中发现
无检查: 编程语言本身不帮助你发现错误,你必须通过特定的条件(例如输出的结果)检查代码的正确性。
静态检查>动态检查>不检查。

静态检查 :
语法错误,例如多余的标点符号或者错误的关键词。即使在动态类型的语言例如Python中也会做这种检查:如果你有一个多余的缩进,在运行之前就能发现它。
错误的名字,例如Math.sine(2). (应该是 sin.)
参数的个数不对,例如 Math.sin(30, 20).
参数的类型不对 Math.sin(“30”).
错误的返回类型 ,例如一个声明返回int类型函数return “30”;

动态检查 :
非法的变量值。例如整型变量x、y,表达式x/y 只有在运行后y为0才会报错,否则就是正确的。
无法表示的返回值。例如最后得到的返回值无法用声明的类型来表示。
越界访问。例如在一个字符串中使用一个负数索引。
使用一个null对象解引用。(null相当于Python中的None)
静态检查倾向于类型错误 ,即与特定的值无关的错误。正如上面提到过的,一个类型是一系列值的集合,而静态类型就是保证变量的值在这个集合中,但是在运行前我们可能不会知道这个值的结果到底是多少。所以如果一个错误必须要特定的值来“触发”(例如除零错误和越界访问),编译器是不会在编译的时候报错的。
与此相对的,动态类型检查倾向于特定值才会触发的错误。

原始类型并不是真正的数字!
在Java和许多其他语言中存在一个“陷阱”——原始数据类型的对象在有些时候并不像真正的数字那样得到应有的输出。结果就是本来应该被动态检查发现的错误没有报错。例如:
整数的除法:5/2并不会返回一个小数,而是一个去掉小数部分的整数对象,因为除法操作符对两个整数对象运算后的结果还是整数,而整数对象是无法表示5/2的精确值的(而我们期望它会是一个动态检查能发现的错误)。
整形溢出: int 和 long类型的值的集合是一个有限集合——它们有最大的值和最小的值,当运算的结果过大或者过小的时候我们就很可能得到一个在合法范围内的错误值。
浮点类型中的特殊值:在浮点类型例如double中有一些不是数的特殊值:NaN ( “Not a Number”), POSITIVE_INFINITY (正无穷), and NEGATIVE_INFINITY (负无穷).当你对浮点数进行运算的时候可能就会得到这些特殊值(例如除零或者对一个负数开更号),如果你拿着这些特殊值继续做运算,那你可能就会得到一个意想不到结果(译者注:例如拿NaN和别的数进行比较操作永远是False)。

可变性与不可变性

可变性

回忆之前我们讨论过的“用快照图理解值与对象”(译者注:“Java基础”),有一些对象的内容是不变的(immutable):一旦它们被创建,它们总是表示相同的值。另一些对象是可变的(mutable):它们有改变内部值对应的方法。

String 就是不变对象的一个例子,一个String 对象总是表示相同的字符串。而StringBuilder 则是可变的,它有对应的方法来删除、插入、替换字符串内部的字符,等等。
在这里插入图片描述
因为 String 是不变的,一旦被创建,一个 String 对象总是有一样的值。为了在一个 String 对象字符串后加上另一个字符串,你必须创建一个新的 String 对象:

String s = “a”;
s = s.concat(“b”); // s+=“b” and s=s+“b” also mean the same thing
在这里插入图片描述
与此相对, StringBuilder 对象是可变的。这个类有对应的方法来改变对象,而不是返回一个新的对象:
StringBuilder sb = new StringBuilder(“a”);
sb.append(“b”);
所以这有什么关系呢?在上面这两个例子中,我们最终都让s和sb索引到了"ab" 。当对象的索引只有一个时,它们两确实没什么区别。但是当有别的索引指向同一个对象时,它们的行为会大不相同。例如,当另一个变量t指向s对应的对象,tb指向sb对应的对象,这个时候对t和tb做更改就会导致不同的结果:
在这里插入图片描述

 String t = s;
  t = t + "c";
 StringBuilder tb = sb;
 tb.append("c");

可以看到,改变t并没有对s产生影响,但是改变tb确实影响到了sb.

可变性带来的风险

可变的类型看起来比不可变类型强大的多。如果你在“数据类型商场”购物,为什么要选择“无聊的”不可变类型而放弃强大的可变类型呢?例如 StringBuilder 应该可以做任何 String 可以做的事情,加上 set() 和 append() 这些功能。

答案是使用不可变类型要比可变类型安全的多,同时也会让代码更易懂、更具备可改动性。
#1: 传入可变对象
下面这个方法将列表中的整数相加求和:

/** @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 numbers 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));
}

注意到这个方法直接改变了数组 —— 这对实现者来说很合理,因为利用一个已经存在的列表会更有效率。如果这个列表有几百万个元素,那么你节省内存的同时也节省了大量时间。所以实现者的理由很充分:DRY与性能。
但是使用者可能会对结果很惊奇,传入可变对象真的(可能)会导致隐秘的bug
在这里插入图片描述
分析可知,本来只是用startOfSpring做个参照,但是因为传递的引用,所以partyDate的修改也修改了groundhogAnswer,影响了成员变量的值。
问题的原因在于Date是可变类型,可以用java.time包里的其他不变类代替,如: LocalDateTime , Instant 等。
另外,采用不可变类型,可通 过内存共享相同的值,降低复制带来的内存空间占用,并且不可变类无需防御性拷贝。
只在局部使用或者只有一个引用时,可变类型的使用是安全的,对可变类型的多个引用(别名)是带来风险的原因

防御性拷贝

原因:
假定类的客户端,尽力摧毁类的不变量,破坏这个类的约束条件,因此你必须保护性的设计程序。

但更常见的是,你的类将不得不处理由于善意得程序员诚实错误而导致的意外行为。不管怎样,花时间编写在客户端行为不佳的情况下仍然保持健壮的类是值得的。
为了保护Period实例的内部信息避免受到修改,导致问题,对于构造器的每个可变参数进行保护性拷贝(defensive copy)是必要的:
在这里插入图片描述

关系快照图(Snapshot diagram )

关系快照图描述了程序运行过程中的内部状态,例如栈(包括上面的方法和变量)、堆(存在的对象)
通常对象被圈起来,而原始类型直接指过去,如图:
在这里插入图片描述
在这里插入图片描述
在关系快照图中,final修饰的变量用双箭头表示。如果final修饰可变类型,实际上是指向可变值的不可变引用( 引用不可变,值可变) 。

复杂数据类型:Arrays和Collections

Array
int[] a = new int[100];
操作:索引、赋值、求长度(.length)
List
List list = new ArrayList();
操作:get,set,size
注意:List是一个接口,并且其成员必须是对象数据类型。
迭代
Set
集合类似于数学里的集合,无序,不可重复。
常用方法有:contain,containsAll(判断是不是子集),removeAll(从一个集合中删除一个集合)
同样,Set也是一个抽象接口。
Map
常用方法:put,get,containsKey,remove
Map也是一个抽象接口,常用的有HashMap。
通过Array构建list
Arrays.asList(new String[] { “a”, “b”, “c” })
声明List,Set和Map变量
当添加一个item时 ,编译器执行静态检查,确保只添加合适类型的item。
创建List,Set和Map变量
List,Set和Map作为接口, 定义了类型的工作方式,但不提供实现代码。 所以用户有权在不同情况下选择不同的实现。
一些实现:
List: ArrayList and LinkedList
Set: HashSet
Map: HashMap

Java Collection
在这里插入图片描述
迭代

List<String> cities = new ArrayList<>(); 
Set<Integer> numbers = new HashSet<>(); 
Map<String,Turtle> turtles = new HashMap<>();

for (String city : cities) { 
	System.out.println(city); 
}
for (int num : numbers) {
	System.out.println(num); 
}
for (int ii = 0; ii < cities.size(); ii++) {
	System.out.println(cities.get(ii)); 
}
for (String key : turtles.keySet()) {
	System.out.println(key + ": " + turtles.get(key)); 
}

迭代器是一个对象,它遍历一组 元素并逐个返回元素。 遍历时默认调用迭代器。
迭代器有两个方法,next() 返回下一个元素,hasNext()测试是否迭代到了最后。
在这里插入图片描述
在这里插入图片描述

有用的不可变类型

既然不可变类型避开了许多危险,我们就列出几个Java API中常用的不可变类型:
所有的原始类型及其包装都是不可变的。例如使用BigInteger和 BigDecimal 进行大整数运算。
不要使用可变类型 Date ,而是使用 java.time 中的不可变类型。
Java中常见的聚合类 — List, Set, Map — 都是可变的:ArrayList, HashMap等等。但是 Collections 类中提供了可以获得不可修改版本(unmodifiable views)的方法:
Collections.unmodifiableList
Collections.unmodifiableSet
Collections.unmodifiableMap
你可以将这些不可修改版本当做是对list/set/map做了一下包装。如果一个使用者索引的是包装之后的对象,那么 add, remove, put这些修改就会触发 Unsupported­Operation­Exception异常。
当我们要向程序另一部分传入可变对象前,可以先用上述方法将其包装。要注意的是,这仅仅是一层包装,如果你不小心让别人或自己使用了底层可变对象的索引,这些看起来不可变对象还是会发生变化!
Collections 也提供了获取不可变空聚合类型对象的方法,例如Collections.emptyList

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值