第3章:抽象数据类型(ADT)和面向对象编程(OOP) 3.1数据类型和类型检查

大纲

1.编程语言中的数据类型
2.静态与动态数据类型
3.类型检查
4.易变性和不变性
5.快照图
6.复杂的数据类型:数组和集合
7.有用的不可变类型
8.空引用
9.总结

编程语言中的数据类型

类型和变量

类型是一组值,以及可以对这些值执行的操作。
变量:存储一个特定类型值的命名位置

Java中的类型

Java有几种基本类型,例如:

  • int(对于像5和-200这样的整数,但限于±2 ^ 31或大约±20亿的范围)
  • 长(对于大于±2 ^ 63的整数)
  • 布尔值(对于true或false)
  • 双精度浮点数(代表实数的一个子集)
  • char(单个字符,如'A'和'$')

Java也有对象类型,例如:

  • 字符串表示一系列字符。
  • BigInteger表示一个任意大小的整数。

按照Java约定,基本类型是小写字母,而对象类型以大写字母开头。

对象类型的层次结构

根是Object(所有非基元都是对象)

  • 除Object以外的所有类都有一个父类,使用extends子句指定

class Guitar extends Instrument { ... }
如果省略了子句,则默认为Object
一个类是其所有超类的一个实例

  • 从其超类继承可见的字段和方法
  • 可以覆盖(Override)方法来改变他们的行为

包装的基本类型

用于原始类型的不变容器
– Boolean, Integer, Short, Long, Character, Float, Double
典型用例是集合
除非你必须使用包装的基本类型!
语言自动包装和自动解包装

运算符

运算符:执行简单计算的符号

  • 赋值:=
  • 加法:+
  • 减法: -
  • 乘法:*
  • 除法:/

操作顺序:遵循标准的数学规则

  • 1.括号
  • 2.乘法和除法
  • 3.加法和减法

字符串连接(+)

操作

操作是获取输入和生成输出的函数(有时会自行更改值)。

  • 作为中缀,前缀或后缀运算符。 例如,a + b调用操作+:int×int→int。
  • 作为一个对象的方法。 例如,bigint1.add(bigint2)调用操作add:BigInteger×BigInteger→BigInteger。
  • 作为一项功能。 例如,Math.sin(theta)调用sin:double→double操作。 在这里,数学不是一个对象。 这是包含sin函数的类。

重载操作符/操作

一些操作被重载,因为相同的操作名称用于不同的类型
对于Java中的数字基本数据类型,算术运算符+, - ,*,/会严重重载。
方法也可以重载。 大多数编程语言都有一定程度的重载。
(将在3.3节OOP中讨论)

静态输入与动态输入

Java是一种静态类型的语言。

  • 所有变量的类型在编译时已知(在程序运行之前),因此编译器也可以推导出所有表达式的类型。
  • 如果将a和b声明为int,则编译器得出结论a + b也是int。
  • 在编写代码时,Eclipse环境会执行此操作,事实上,您仍然可以在输入时了解许多错误。
  • 在编译阶段进行类型检查

在像Python这样的动态类型语言中,这种检查被推迟到运行时(程序运行时)。

3.类型检查

静态检查和动态检查

一种语言可以提供的三种自动检查:

  • 静态检查:在程序运行之前会自动发现错误。
  • 动态检查:执行代码时会自动发现错误。
  • 不检查:语言根本无助于您找到错误。 你必须亲自观察,否则最终会得到错误的答案。

不用说,静态捕获一个bug比动态捕获它要好,动态捕获它比根本没有捕获它要好。

静态检查

静态检查意味着在编译时检查错误。
错误是编程的祸根。
静态类型可以防止大量的错误感染程序:准确地说,通过对错误类型的参数应用操作而导致的错误。
如果你写了一行代码,如:“5”*“6”,它试图乘以两个字符串,那么静态类型在编程时会捕获这个错误,而不是等到执行过程中到达该行。

  • 语法错误,如额外的标点符号或假词。 即使是像Python这样的动态类型语言也可以进行这种静态检查。
  • 错误的名字,如Math.sine(2)。 (正确的名字是sin)
  • 错误的参数数量,如Math.sin(30,20)。
  • 错误的参数类型,如Math.sin(“30”)。
  • 错误的返回类型,如返回“30”; 从声明的函数返回一个int。

动态检查

非法的参数值。 例如,当y实际上为零时,整数表达式x / y只是错误的; 否则它将运行。 所以在这个表达式中,除零不是一个静态错误,而是一个动态错误。

  • 不具有代表性的返回值,即特定返回值无法在类型中表示时。
  • 超出范围的索引,例如,在字符串上使用负值或太大的索引。
  • 在空对象引用上调用方法。

静态与动态检查

静态检查往往是关于类型的,与变量具有的特定值无关的错误。

  • 静态类型保证了一个变量会从该集合中获得一些值,但是我们直到运行时才知道它具有哪个值。
  • 所以如果错误只会被某些值引起,比如被零除或索引超出范围,那么编译器不会引发关于它的静态错误。

相比之下,动态检查往往是由特定值引起的错误。

原始数据类型不是真正的数字

Java中的一个陷阱 - 以及其他许多编程语言 - 就是它的原始数字类型具有不像我们习惯的整数和实数那样的特例。
结果,真正应该动态检查的一些错误根本不被检查。

  • 整数除法:5/2不返回分数,它返回一个截断的整数。
  • 整数溢出。 如果计算结果过于积极或过于消极而无法适应该有限范围,则会悄然溢出并返回错误答案。 (没有静态/动态检查!)例如,int big = 200000 * 200000;
  • 浮点类型中的特殊值。 NaN(“不是数字”),POSITIVE_INFINITY和NEGATIVE_INFINITY。 例如,double a = 7/0;

4可变性和不变性

赋值

使用“=”给变量赋值
赋值可以和变量声明结合使用

更改变量或其值

改变变量和改变数值有什么区别?

  • 当你分配给变量时,你正在改变变量的箭头指向的地方。 您可以将其指向不同的值。
  • 当分配给可变值的内容时(例如数组或列表),您将在该值内改变引用。

变化是必要的罪恶。
好的程序员可以避免变化的事情,因为它们可能会意外地改变。

不变性

不变性是一个主要的设计原则。
不可变类型是一种类型,它们的值一旦创建就永远不会改变。
Java也为我们提供了不可变的引用:一次赋值且永不重新赋值的变量。

  • 为了使引用不可变,用关键字final声明它。

如果Java编译器不确定最终变量只会在运行时分配一次,那么它将产生编译器错误。 所以最终给你静态检查不可变引用。

最好使用final来声明方法的参数和尽可能多的局部变量。
像变量的类型一样,这些声明是重要的文档,对代码读者很有用,并由编译器进行静态检查。
注意:

  • fianl的类声明意味着它不能被继承。
  • final变量意味着它始终包含相同的值/参考,但不能最终更改
  • final方法意味着它不能被子类覆盖

可变性和不变性

对象是不可变的:一旦创建,它们总是表示相同的值。
对象是可变的:它们具有改变对象值的方法。

字符串作为不可变类型

字符串是不可变类型的一个例子。
一个String对象总是表示相同的字符串。
由于String是不可变的,一旦创建,String对象始终具有相同的值。
要将某些内容添加到字符串的末尾,您必须创建一个新的String对象:

StringBuilder作为一个可变类型

StringBuilder是一个可变类型的例子。
它具有删除字符串部分,插入或替换字符等的方法
该类具有更改对象值的方法,而不仅仅是返回新值:

可变类型的优点

  • 使用不可变的字符串,这会产生大量的临时副本
  • 获得良好的性能是我们使用可变对象的一个原因。
  • 另一个是方便共享:通过共享一个常见的可变数据结构,您的程序的两个部分可以更方便地进行通信。
    “全局变量”
    但是你必须知道全局变量的缺点......

5快照图作为Code-level,Run-time,Moment视图

快照图

为了理解微妙的问题,我们可以绘制运行时发生的事情的图片。
快照图在运行时表示程序的内部状态 - 其堆栈(正在进行的方法及其局部变量)及其堆(当前存在的对象)。
为什么我们使用快照图表?

  • 通过图片相互交流。
  • 为了说明基本类型与对象类型,不可变值与不可变引用,指针别名,堆栈与堆,抽象与具体表示等概念。
  • 帮助解释您的团队项目设计(彼此之间以及与您的技术援助相关)。
  • 为后续课程中丰富的设计符号铺平道路。

可变值与重新分配变量

快照图给我们提供了一种可视化更改变量和更改值之间区别的方法:

  • 当您分配给变量或字段时,您将更改变量的箭头指向的位置。 您可以将其指向不同的值。
  • 当分配给可变值的内容时(例如数组或列表),您将在该值内改变引用。

快照图中的基本类型和对象类型

基本类型的值

  • 基本类型的值由裸常量表示。 传入的箭头是对来自变量或对象字段的值的引用。

对象类型的值

  • 对象类型的值是由其类型标记的圆。
  • 当我们想要显示更多细节时,我们在其中写入字段名称,箭头指向它们的值。 有关更详细的信息,这些字段可以包含它们的声明类型。

重新分配和不可变的值

例如,如果我们有一个字符串变量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)由双箭头表示。

6复杂的数据类型:数组和集合

数组

数组是另一个类型T的固定长度序列。例如,下面是如何声明一个数组变量并构造一个数组值以分配给它:
int [] a = new int [100];
int []数组类型包含所有可能的数组值,但是一旦创建了特定的数组值,永远不会改变其长度。
数组类型的操作包括:

  • 索引:a [2]
  • 赋值:a [2] = 0
  • 长度:a.length

列表

我们使用List类型来代替固定长度的数组。
列表是另一个类型T的可变长度序列。 List <Integer> list = new ArrayList <Integer>();
其部分操作:

  • 索引:list.get(2)
  • 赋值:list.set(2,0)
  • 长度:list.size()

注1:列表是一个接口
注2:列表中的成员必须是一个对象。

集合

Set是零个或多个唯一对象的无序集合。
一个对象不能多次出现在一个集合中。 要么它在或它不在。

  • s1.contains(e)测试集合是否包含元素
  • s1.containsAll(s2)测试是否s1⊇s2
  • s1.removeAll(s2)从s1移除s2

Set是一个抽象接口

地图

地图类似于字典(key)

  • map.put(key,val)添加映射key→val
  • map.get(key)获取一个key的值
  • map.containsKey(key)测试地图是否有key
  • map.remove(key)删除映射

地图是一个抽象界面

声明List,Set和Map变量

使用Java集合,我们可以限制集合中包含的对象的类型。
当我们添加一个项目时,编译器可以执行静态检查,以确保我们只添加适当类型的项目。
然后,当我们取出一个对象时,我们保证它的类型将是我们所期望的。

创建List,Set和Map变量

Java有助于区分

  • 一种类型的规范 - 它有什么作用? 抽象接口 - 实现
  • 代码是什么? 具体类

List,Set和Map都是接口:

  • 他们定义了这些相应的类型是如何工作的,但他们不提供实现代码。
  • 优点:用户有权在不同情况下选择不同的实施方式。

List,Set和Map的实现:

  • 列表:ArrayList和LinkedList
  • 集合:HashSet
  • 地图:HashMap

迭代器是一个可变类型的迭代器

迭代器是一个对象,它遍历一组元素并逐个返回元素。
当你使用for(...:...)循环遍历一个List或数组时,迭代器在Java中被使用。
迭代器有两种方法:

  • next()返回集合中的下一个元素---这是一个可变的方法!
  • hasNext()测试迭代器是否已到达集合的末尾。

7有用的不可变类型

原始类型和原始包装都是不可变的。

  • 如果你需要用大数来计算,BigInteger和BigDecimal是不可变的。

不要使用可变日期,根据您需要的计时粒度,使用java.time或java.time.ZonedDateTime中适当的不可变类型。
Java的集合类型(List,Set,Map)的通常实现都是可变的:ArrayList,HashMap等
Collections实用程序类具有获取这些可变集合的不可修改视图的方法:

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

可变数据类型的不可变包装
Java集合类提供了一个有趣的折衷:不可变的包装器。

  • Collections.unmodifiableList()需要一个(可变的)List,并且用一个看起来像List的对象封装它,但是其禁用的mutator - set(),add(),remove()等会抛出异常。 所以你可以构造一个使用mutator的列表,然后用一个不可修改的包装器来封装它(并且抛弃你对原始可变列表的引用,并且得到一个不变的列表。

缺点是你在运行时获得了不变性,但不是在编译时。

  • 如果你尝试排序()这个不可修改的列表,Java在编译时不会发出警告。
  • 你会在运行时得到一个异常。
  • 但这还是比没有好,所以使用不可修改的列表,地图和集合可以是降低错误风险的一种非常好的方法。

不可修改的包装

不可修改的包装器通过拦截所有修改集合并抛出UnsupportedOperationException的操作来取消修改集合的能力。
不可修改的包装有两个主要用途,如下所示:

  • 一旦建立一个集合就不可变。 在这种情况下,最好不要保留对后备集合的引用。 这绝对保证了不变性。
  • 允许某些客户端只读访问您的数据结构。 您保留对后备集合的引用,但请分发引用。 这样,客户可以看,但不能修改,而你保持完全访问。

8空引用

空引用
在Java中,对对象和数组的引用也可以采用特殊值Null,这意味着引用不指向对象。 空值是Java类型系统中的一个不幸的漏洞。
基元不能为null,编译器会拒绝这种带有静态错误的尝试:
int size = null; //非法
可以将null分配给任何非原始变量,并且编译器在编译时高兴地接受这些代码。 但是你会在运行时遇到错误,因为你不能调用任何方法或者使用带有这些引用之一的任何字段(抛出NullPointerExceptions):
String name = null;
name.length();
int [] points = null;
points.length;
null与空字符串“”或空数组不同。

空引用

非基元和像List这样的集合的数组可能是非空的,但包含null作为值
只要有人试图使用集合的内容,这些空值就可能导致错误。
空值是麻烦和不安全的,所以你最好建议将它们从你的设计词汇表中删除。
空值在参数和返回值中被隐式禁止。

来自Guava(Google)

不小心使用null会导致各种各样的错误。
研究谷歌代码库,我们发现像95%的集合不应该有任何空值,并且让这些快速失败而不是默默接受空值会对开发人员有所帮助。
另外,null很不明确。
很少有人明白空返回值应该是什么意思 - 例如,Map.get(key)可以返回null,因为map中的值为null,或者该值不在map中。 空可能意味着失败,可能意味着成功,几乎意味着任何事情。
使用除null之外的其他内容可以明确您的意思。

总结

静态类型检查:

  • 减少错误保证安全。 静态检查通过在运行时捕获类型错误和其他错误来帮助安全。
  • 容易明白。 它有助于理解,因为类型在代码中已明确说明。
  • 准备改变。 静态检查通过识别需要一起更改的其他位置来更容易地更改代码。 例如,当您更改变量的名称或类型时,编译器会立即在使用该变量的所有位置显示错误,并提醒您更新它们。

可变性对于性能和便利性很有用,但它也会通过要求使用对象的代码在全范围内表现良好而造成bug的风险,这极大地增加了我们必须做的推理和测试,以确保其正确性。
确保你了解不可变对象(如String)和不可变引用(如final变量)之间的区别。
快照图可以帮助理解。

  • 对象是由快照图中的圆圈表示的值,而不可变的对象是具有双边框的值,表示它永远不会更改其值。
  • 引用是一个指向对象的指针,用快照图中的箭头表示,不可变引用是带有双线的箭头,表示不能将箭头移动到指向不同的对象。

关键的设计原则是不变性:尽可能使用不可变对象和不可变引用。

  • 减少错误保证安全。 不可变对象不容易出现锯齿引起的错误。 不可变引用总是指向同一个对象。
  • 容易明白。 因为不可变的对象或引用总是意味着相同的事物,所以代码读者推理起来更简单 - 他们不必追踪所有代码以找到可能更改对象或引用的所有位置,因为 它不能改变。
  • 准备好改变。 如果在运行时不能更改对象或引用,那么当程序更改时,不需要修改依赖于该对象或引用的代码。

脚本宝典为你提供优质服务
脚本宝典 » 第3章:抽象数据类型(ADT)和面向对象编程(OOP) 3.1数据类型和类型检查

发表评论

提供最优质的资源集合

立即查看 了解详情