3-1数据类型和类型检查
一、编程语言中的数据类型
类型与变量类
数据类型是一组值,以及可以在这些值上执行的操作
实例:
- boolean: Truth value (true or false).
- int: Integer (0, 1, -47).
- double: Real number (3.14, 1.0, -2.1).
- String: Text (“hello”, “example”).
变量:用特定数据类型定义,可存储满足类型约束的值
Java中的类型
Java有几个基本类型,例如:
- int(对于整数,如5和-200,但限于±2 ^ 31,或约±20亿)
- long (可达2±63)
- boolean (true or false)
- double (浮点数,表示实数子集)
- char(像A和‘$’这样的单个字符)
Java也有对象数据类型:
- String表示字符序列
- BigInteger 表示任意大小的整数
通过Java约定,基元类型为小写,而对象类型以大写字母开头
对象类型形成层次结构
根是对象(所有非原语都是对象)
-除了对象之外的所有类都有一个父类,用扩展子句指定
如果扩展子句省略,则默认为对象
类是其所有超类的实例
-从其超类继承可见字段和方法
-可以重写方法来改变他们的行为
装箱原语
将基本类型包装为对象类型
- Boolean, Integer, Short, Long, Character, Float, Double
通常是在定义集合类型的时候使用它们
一般情况下,尽量避免使用
一般可以自动转换
操作符
执行简单计算的符号
- Assignment: =
- Addition: +
- Subtraction: -
- Multiplication: *
- Division: /
操作顺序:遵循标准数学规则
- 1.圆括号
- 2.乘法和除法
- 3.加减法
字符串连接(+)
- String text = "hello" + " world";
- text = text + " number " + 5; // text = "hello world number 5"
操作
操作是输入和产生输出的函数(有时也会改变值本身)
- 作为中缀、前缀或后缀运算符。例如,A+B调用操作 + : int × int → int
- 作为对象的一种方法。例如,Bbigint1.add(bigint2) 调用操作添加 BigInteger × BigInteger → BigInteger
- 作为一个函数。例如,Math.sin(theta) 调用操作sin:double → double,在这里,数学不是一个对象。这是包含sin函数的类
重载
同样的操作名可用于不同的数据类型
Java中的数字基元类型的算术运算符 +, -, *, / 严重重载
方法也可以重载。大多数编程语言都有一定程度的重载
二、静态与动态数据类型
Java是一种静态类型的语言
- 所有变量的类型在编译时(程序运行之前)是已知的,因此编译器也可以推断出所有表达式的类型
- 如果a和b被声明为int,则编译器得出结论a + b 也是int
- Eclipse环境在编写代码的过程中这样做,事实上,当您还在键入时,您会发现许多错误
- 在编译阶段进行类型检查
在动态类型语言(如Python)中,这种检查推迟到运行时(程序运行时)
- 在运行阶段进行类型检查
三、类型检查
类型转换
int a = 2; // a = 2
double a = 2; // a = 2.0 (Implicit)
int a = 18.7; // ERROR
int a = (int) 18.7; // a = 18
double a = 2/3; // a = 0.0
double a = (double)2/3; // a = 0.6666…
How about
String a = 1;
静态检查与动态检查
一种语言能提供的三种自动检查:
- 静态类型检查:程序运行前自动发现bug
- 动态类型检查:当代码被执行时自动发现bug
- 无检查:语言不能帮助你找到错误。你必须自己去观察,或者最终得到错误的答案
不用说,静态捕获一个bug比动态捕获它要好,动态捕获它比不捕获它要好
静态类型检查 >> 动态 >> 无检查
静态类型检查
静态检查意味着在编译时检查bug
漏洞是编程的祸根
静态类型防止了程序中感染的一大类错误:确切地说,通过将操作应用到错误类型的参数而导致的错误
如果你写了一行折断的代码,比如"5" * "6" ,它试图将两个字符串相乘,然后静态类型将在您仍在编程时捕获此错误,而不是等待在执行过程中到达该行
静态类型检查:可在编译阶段发现错误,避免了将错误带入到运行阶段,可提高程序正确性/健壮性
- 语法错误,如额外的标点符号或假词。甚至像Python这样的动态类型的语言也会进行这种静态检查
- 类名/函数名错误, 例如Math.sine(2)(正确的名字是sin)
- 参数数目错误,例如Math.sin(30, 20)
- 返回值类型错误,类似于返回“30”;从声明的函数返回一个int
动态类型检查
非法的参数值:例如,当Y实际上为0时,整数表达式X/Y是错误的;否则它工作。因此,在这个表达式中,除以零不是静态错误,而是动态错误
非法的返回值:当特定返回值不能在类型中表示时
越界:在字符串上使用负或过大索引
空指针:调用空对象引用的方法
静态与动态类型检查
静态检查趋向于类型,独立于变量的特定值的错误。
静态类型保证变量从该集合中得到一定的值,但直到运行时我们才知道它到底具有哪一个值。
因此,如果错误只由某些值引起,如除零或索引超出范围,编译器不会对其产生静态错误
相反,动态检查往往是由特定值引起的错误
静态检查:关于“类型”的检查,不考虑值
动态检查:关于“值”的检查
四、易变性与不可变性
赋值
使用“=”来给变量赋值
例如:
– String foo;
– foo = “IAP 6.092”;
赋值可以与变量声明相结合
例如:
- double badPi = 3.14;
- boolean isJanuary = true;
改变变量或其值
改变一个变量、改变一个变量的值,二者有何区别?
- 改变一个变量:将该变量指向另一个值的存储空间
- 改变一个变量的值:将该变量当前指向的值的存储空间中写入一个新的值
变化是“罪恶”,但程序不能没有变化
尽可能避免变化,以避免副作用
不变性
不变性:重要设计原则
不变数据类型:一旦被创建,其值/不能改变
如果是引用类型,也可以是不变的:一旦确定其指向的对象,不能再被改变
-要使引用不可变,用关键字最后声明它
final int n = 5;
final Person a = new Person(“Ross”);
如果编译器无法确定final变量不会改变,就提示错误,这也是静态类型检查的一部分
所以,尽量使用 final变量作为方法的输入参数、作为局部变量
final表明了程序员的一种“设计决策”
注:
- final类无法派生子类
- final变量无法改变值/引用
- final方法无法被子类重写
易变性与不可变性
不变对象:一旦被创建,始终指向同一个值/引用
可变对象:拥有方法可以修改自己的值/引用
作为不可变类型的字符串
String是一个不可变类型的例子
String对象总是表示相同的字符串
因为字符串是不可变的,一旦创建,字符串对象总是具有相同的值
将字符串添加到字符串的末尾,必须创建一个新的字符串对象:
StringBuilder是一个可变类型的例子
它有删除字符串的部分,插入或替换字符等的方法
这个类有改变对象的值的方法,而不是只返回新的值
有区别吗?最终的值都是一样的
-当只有一个引用指向该值,没有区别
有多个引用的时候,差异就出现了
-当另一个变量T指向与S相同的字符串对象时,另一个变量TB指向与StringBuilder相同的SB,那么不可变和可变对象之间的差异变得更加明显
可变类型的优点
使用不可变类型,对其频繁修改会产生大量的临时拷贝(需要垃圾回收)
-第一个数字(“0”)实际上是在构建最终字符串的过程中复制的N次,第二个数字被复制N-1次,以此类推
-它实际上只需要O(n2)的时间来完成所有的复制,即使我们只连接n个元素
可变类型最少化拷贝以提高效率
-它使用一个简单但巧妙的内部数据结构来避免任何复制,直到最后,当你用一个toString()调用最后一个字符串时
使用可变数据类型,可获得更好的性能,也适合于在多个模块之间共享数据
可变数据类型的风险
既然如此,为何还要用不可变类型?
- StringBuilder 应该能够做所有的String可以做的事情,
但是,不可变类型更“安全”,在其他质量指标上表现更好
- 易变性使你更难理解你的程序在做什么,而且更难执行合同
性能和安全性之间的权衡?折中,看你看重哪个质量指标
风险实例1:传递可变值
-该函数超出了spec范畴,因为它改变了输入参数的值
-但实际上,传递可变对象是一个潜在的错误。它只是在等待一些程序员不经意地改变这个列表,通常有很好的意图,比如重用或性能,但是导致了一个很难追踪的bug。这种错误非常难于跟踪和发现
风险实例2:返回可变值
在这两个例子中List<Integer> 和 Date -如果list和date是不可变类型,问题将完全避免
如何修改代码?
在实例1中:
-通过防御式拷贝,给客户端返回一个全新的Date对象
return new Date(groundhogAnswer.getTime());
-大部分时候该拷贝不会被客户端修改, 可能造成大量的内存浪费
如果使用不可变类型, 则节省了频繁复制的代价
混淆是使可变类型风险的原因
安全的使用可变类型:局部变量,不会涉及共享;只有一个引用
如果有多个引用(别名),使用可变类型就非常不安全
-在List 示例中,同样的list由list((in sum and sumAbsolute)和myData (in main)中指向。一个程序员(sumAbsolute ’s)认为修改列表是可以的;另一个程序员(main ’s)希望列表保持不变。因为别名,主程序员失去了
-在 Date 示例中,有两个变量名指向 Date 对象、groundhogAnswer 和partyDate。这些别名在代码的完全不同的部分,在不同的程序员的控制下,他们可能不知道对方在做什么
五、快照图作为代码级、运行时和矩视图
快照图
这将有助于我们绘制运行时发生的事情的图片,以便理解微妙的问题
快照图表示程序在运行时的内部状态-它的堆栈(正在进行的方法和它们的局部变量)及其堆(当前存在的对象)。用于描述程序运行时的内部状态
为什么我们使用快照图
-便于程序员之间的交流
-便于刻画各类变量随时间变化
-便于解释设计思路
可变值与重新分配变量
快照图给出了一种可视化变量和改变值之间的区别的方法:
-当你赋值给变量或字段时,你正在改变变量箭头的位置。你可以把它指向一个不同的值
-当你分配一个可变值的内容(比如数组或列表)时,你正在改变那个值中的引用
快照图中的原始值和对象值
基本类型的值
原始值由裸露的常量表示。传入箭头是对变量或对象字段值的引用。
对象类型的值
对象值是由其类型标记的圆。
当我们想要显示更多的细节时,我们在其内部写下字段名称,箭头指出它们的值。对于更多的细节,字段可以包括它们声明的类型。
重新赋值和不变值
例如,如果我们有一个字符串变量s,我们可以将它从“a”的值重新赋值为“ab”
String s = "a";
s = s + "b";
String是一个不可变类型的示例,一个类型的值一旦创建就不会更改。
不可变对象(它们的设计者总是想要表示相同的值)在一个快照图中用双线 椭圆表示,就像我们的图表中的字符串对象一样
可变值
相比之下,StringBuilder(一个内置Java类)是一个可变的对象,它表示一个字符串,并且它具有改变对象的值的方法:
StringBuilder sb = new StringBuilder("a");
sb.append("b");
这两个快照图看起来非常不同,这是很好的:可变性和不可变性之间的差异将在代码从bug中起到重要作用。
不变引用
Java也给我们提供了不可变的引用,即:被赋值一次且永不重新分配的变量。若要使引用不可变,则用关键字final声明:
final int n = 5;
如果Java编译器不相信你的final变量只在运行时被分配一次,那么它将产生编译器错误。所以final给出了静态引用的不可变引用。
在快照图中,一个不可变的引用(最终)用双箭头表示。
这是一个id不变的对象(它不能被重新分配到不同的数字),但是它的age可以改变。
我们可以对一个可变值(例如:final StringBuilder sb)有一个不变的引用,即使我们指向同一个对象,它的值也可以改变
引用是不可变的,但指向的值却可以是可变的
我们还可以对不可变的值(如String S)进行可变引用,其中变量的值可以改变,因为它可以被重新指向不同的对象
可变的引用,也可指向不可变的值
我们可以对一个可变值(例如:最终StringBuilder sb)有一个不变的引用,即使我们指向同一个对象,它的值也可以改变
我们还可以对不可变的值(如字符串S)进行可变引用,其中变量的值可以改变,因为它可以被重新指向不同的对象
六、复杂数据类型:数组和集合
数组
数组是另一种类型的T的固定长度序列。例如,这里是如何声明数组变量并构造一个数组值来分配给它
int[] a = new int[100];
int[]数组类型包含所有可能的数组值,但特定数组值一旦创建,就永远不能更改其长度
数组类型的操作包括:
索引:a[2]
分配:a[2]=0
长度:a.length
列表
代替固定长度数组,让我们使用列表类型
列表是另一类型T的可变长度序列
List<Integer> list = new ArrayList<Integer>();
它的一些操作:
索引:list.get(2)
分配:list.set(2, 0)
长度:list.size()
注释1:列表是一个接口
注释2:列表中的成员必须是对象
迭代
迭代数组
int max = 0;
for (int i=0; i<array.length; i++) {
max = Math.max(array[i], max);
}
迭代列表
int max = 0;
for (int x : list) {
max = Math.max(x, max);
}
集合
集合是零个或多个唯一对象的无序集合
对象不能多次出现在集合中。要么是在里面,要么是出去了
- s1.contains(e) 测试集合是否包含元素
- s1.containsAll(s2) 测试是否S1⊇S2
- s1.removeAll(s2) 从S1中删除S2
set是一个抽象的接口
图
图与字典(键值)相似
- map.put(key, val) 添加映射键
- map.get(key) 获取密钥的值
- map.containsKey(key) 测试是否有key映射
- map.remove(key) 删除映射
图是一个抽象的界面
从数组中构建列表
Arrays.asList(new String[] { "a", "b", "c" })
声明列表、集合和映射变量
使用Java集合,我们可以限制集合中包含的对象的类型
当我们添加一个项时,编译器可以执行静态检查,以确保只添加适当类型的项
然后,当我们取出一个项目时,我们保证它的类型将是我们所期望的
声明:
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 >不起作用
-但是,int有一个我们可以使用的整数包装器(例如 Set<Integer> numbers)
-使用时:
sequence.add(5); //将5添加到序列中
int second = sequence.get(1); // 获取第二元素
创建列表、设置和映射变量
Java有助于区分
-一种类型的规格-它做什么? 抽象界面
-执行-代码是什么? 具体类
列表、集合和映射都是接口:
-它们定义了这些类型是如何工作的,但它们不提供实现代码。
-优点:用户有权在不同的情况下选择不同的实现方式
列表、集合和映射的实现:
- List: ArrayList and LinkedList
- Set: HashSet
- Map: HashMap
迭代
迭代器
迭代器是一个对象,它通过元素集合,并逐个返回元素
在Java中使用迭代器时,使用的是for循环通过列表或数组
迭代器有两种方法:
next()返回集合中的下一个元素-这是一个变元方法
hasNext() 测试迭代器是否已到达集合的末尾
暗中破坏迭代器
假设我们有一个以字符串表示的主题列表。我们想要一个方法dropCourse6 ,它将从列表中删除课程6个科目,留下其他科目
首先编写说明书(介绍下一个lecture)
接下来,编写测试用例(要在Chapter 7中教)
实施:
运行:
七、有用不变的类型
基本类型及其封装对象类型都是不可变的
如果需要大量计算,BigInteger和BigDecimal是不可变的
不要使用可变的Date,根据您需要的时间粒度,从java.time 或java.time.ZonedDateTime 使用适当的不可变类型
Java的集合类型(列表、集合、映射)的通常实现方式都是可变的: ArrayList, HashMap等
集合实用程序类具有用于获取这些可变集合的不可修改视图的方法
- Collections.unmodifiableList
- Collections.unmodifiableSet
- Collections.unmodifiableMap
可变数据类型周围的不可变包装
这种包装器得到的结果是不可变的:只能看
Collections.unmodifiableList() 使用一个(可变的)List并将其封装到一个看起来像List的对象,但是它的突变体被禁用 - set() , add() , remove() 等抛出异常。因此,您可以使用变异体构造一个列表,然后将其封装在一个不可修改的包装中(并将引用引用到原来的可变列表中,得到一个不可变的列表)
但是这种“不可变”是在运行阶段获得的,编译阶段无法据此进行静态检查
-如果您尝试对这个不可修改的列表进行sort(),Java将不会在编译时警告您
-您将在运行时得到异常
-但这仍然比什么都没有好,所以使用不可修改的列表、地图和集合可以是减少bug风险的一个很好的方法
包装器
不可修改包装器通过截取将修改集合并抛出未支持操作异常的所有操作来消除修改集合的能力
不可修改包装器有两个主要用途,如下
- 一旦一个集合被建立,它就不可改变。在这种情况下,不保留对后备集合的引用是一种很好的做法。这绝对保证不可变。
- 允许某些客户端只读访问数据结构。您保留对后备集合的引用,但向包装器提交引用。这样,客户端可以查看但不修改,同时保持完整访问权限
public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c); §
public static <T> Set<T> unmodifiableSet(Set<? extends T> s); §
public static <T> List<T> unmodifiableList(List<? extends T> list); § public static <K,V> Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m); §
public static <T> SortedSet<T> unmodifiableSortedSet(SortedSet<? extends T> s); §
Public static <K,V> SortedMap<K,V> unmodifiableSortedMap(SortedMap<K, ? extends V> m);
八、空引用
在Java中,对对象和数组的引用也可以采用特殊值NULL,这意味着引用不指向对象。NULL值是Java类型系统中的一个不幸的漏洞
基元不能为空,编译器将拒绝静态错误的这种尝试:
int size = null; //illegal
可以将null分配给任何非原始变量,编译器在编译时愉快地接受此代码。但是在运行时会出错,因为您不能调用任何方法或使用这些引用中的任何一个字段(抛出NullPointerExceptions):
String name = null;
name.length();
int[] points = null;
points.length;
NULL与空字符串或空数组不一样
非基元和集合如列表的数组可能是非null但包含null作为值
当有人试图使用集合的内容时,这些空值可能会导致错误
NULL值是麻烦和不安全的,因此,建议您将它们从设计词汇中删除
在参数和返回值中隐式地禁止空值