软件构造 | 期末查缺补漏
总体观
软件构造的三维度八度图是由软件工程师Steve McConnell提出的概念,用于描述软件构建过程中的三个关键维度和八个要素。这些维度和要素可以帮助软件开发团队全面考虑软件构建的方方面面,从而提高软件质量和开发效率。
下面是软件构造的三维度八度图的三个关键维度和八个要素:
三个关键维度:
- 作用域(Scope):定义软件项目的功能和目标,包括需求分析、功能规格等。
- 资源(Resources):涉及到开发软件所需的人员、时间、技术、工具等资源。
- 质量(Quality):关注软件的质量特性,如可靠性、性能、安全性等。
八个要素:
- 工具(Tools):用于支持软件开发、测试和管理的工具。
- 文档(Documentation):包括需求文档、设计文档、用户手册等。
- 设计(Design):软件架构和设计的方案。
- 构建(Construction):软件编程和编码的实施。
- 调试(Debugging):识别和修复软件中的错误和缺陷。
- 测试(Testing):对软件进行单元测试、集成测试、系统测试等各个阶段的测试。
- 部署(Deployment):将软件部署到目标环境中,并确保正常运行。
- 管理(Management):包括项目管理、团队管理、进度跟踪等管理活动。
这个三维度八度图提醒软件开发团队在软件构建过程中不仅要关注代码编写,还要考虑到需求管理、资源分配、质量保证等各个方面。通过全面考虑这些要素,软件开发团队可以更好地规划、执行和控制软件项目,达到项目的目标。
版本控制和git
22题2
在执行了上述git指令后,HEAD指向了master分支。让我们逐步分析这个过程:
-
git checkout -b test
: 创建并切换到test分支。也就是说,现在程序员从master分支创建了一个新的分支test,并且切换到了test分支上工作。此时,HEAD指向test分支上的最新commit节点。 -
程序员在test分支上修改了代码,使用
git add *
将修改的文件添加到暂存区,然后使用git commit -m "first"
进行提交,生成了一个新的commit节点,此时test分支上有了第一个commit。 -
git checkout master
: 切换回master分支。这个指令让HEAD指向了master分支上的最新commit节点,也就是说程序员又回到了在master分支上工作。 -
程序员在master分支上再次修改了代码,添加到暂存区并提交了第二次commit,生成了另一个新的commit节点。
-
最后,程序员执行了
git merge test
,将test分支与master分支进行合并。在合并完成后,master分支上会包含test分支上的更改,并形成一个新的合并commit。此时,HEAD仍然指向master分支上最新的合并commit节点。
综上所述,在执行完上述一系列的操作后,HEAD指向了master分支的最新合并commit节点。
关键名词
Version Control System
VCS 版本控制系统Repository
仓库git remote
获得当前配置的所有远程仓库git remote add [shortname] [url]
添加一个新的远程仓库
Git commit
在Git中,一个commit可以指向一个或多个父级。具体分析如下:
-
一个commit指向一个父亲:这是Git中最常见的情况,通常发生在正常的代码提交历史中。每个提交(commit)除了保存了文件快照外,还包含了指向其直接父提交(parent commit)的引用。这种情况下,每个提交都有一个唯一的父提交,形成一个线性的提交历史。
-
一个commit指向多个父亲:这种情况在Git中称为合并提交(merge commit)。合并提交发生在分支合并时,当将一个分支的更改合并到另一个分支时,会创建一个合并提交,该提交指向合并的两个父提交。合并提交会保留合并之前两个分支的所有更改,并将它们合并在一起。这种情况下,一个提交有多个父提交,形成一个分支合并的历史记录。
总的来说,一个commit指向一个父亲是线性的提交历史,而一个commit指向多个父亲是分支合并的历史记录。Git通过这种灵活的方式来管理代码提交历史,使得多人协作、分支管理等操作变得更加高效和灵活。
Git repository
中的三个主要部分是 working directory(工作目录)、staging area(暂存区)和 git directory(仓库目录)。
-
Working Directory(工作目录):工作目录是包含实际项目文件的地方,即我们修改、添加、删除文件的目录。当我们在工作目录中修改文件时,Git会跟踪这些变化,并在适当的时候将这些变化提交到仓库。
-
Staging Area(暂存区):暂存区是一个缓冲区,用于存放我们已经修改过的文件,并准备将这些修改提交到仓库。在暂存区中,我们可以选择性地将一部分修改添加到下一次提交中,而不是一次性提交所有变化。
-
Git Directory(仓库目录):仓库目录是Git的核心部分,包含了项目的完整提交历史、分支、标签等信息。在仓库目录中,Git会保存项目的所有版本记录、元数据和配置信息。
各部分之间的关系:
- 当我们对工作目录中的文件进行修改后,Git会跟踪这些变化。
- 我们使用
git add
命令将工作目录中的修改添加到暂存区,Git会将这些修改暂时存放在暂存区中。 - 最后,我们使用
git commit
命令将暂存区中的修改提交到仓库目录中,形成一个新的提交。这样,文件的修改就会保存在Git仓库的提交历史中。
在整个工作流程中,工作目录、暂存区和仓库目录之间的交互关系使得Git能够追踪文件的修改历史、管理版本控制,并允许我们灵活地控制要提交的修改内容。
Object Graph对象图
在Git中,每个提交(commit)都被表示为一个节点(node)在对象图(object graph)中。对象图是Git中用来存储数据对象的数据结构,其中每个数据对象(如提交、文件、目录等)被表示为一个独立的对象,而节点则代表这些对象之间的关系。
每个提交节点包含了提交的元数据(作者、时间、提交消息等)以及指向各自数据对象的哈希值。通过这种方式,Git能够构建出一个有向无环图(DAG),其中每个节点代表一个提交,每个提交指向它的父提交或多个父提交(在分支合并时)。
通过对象图,Git能够记录项目的完整提交历史,追踪文件的修改,同时保留每个提交所包含的所有信息。这使得Git非常强大和灵活,可以轻松处理多个分支、合并以及版本控制等操作。
因此,节点在对象图中代表了Git中的提交,它们之间的连接形成了一个有向图,反映了项目的提交历史及其关系。理解节点在对象图中的作用有助于更深入地理解Git是如何管理和跟踪项目的变化的。
数据类型
- 22题4
为什么错:
B. 一个 immutable 的 ADT,也可能会包含 mutator 方法
这说法不恰当,因为一个不可变(immutable)的 ADT(抽象数据类型)应该是没有 mutator 方法的。不可变对象的设计原则之一就是不提供能够修改对象状态的方法,一旦对象被创建后,其状态就不能再被改变。因此,一个完全不可变的 ADT 不应该包含 mutator 方法。
为什么对:
A. 客户端构造得到的 Set/Map/List 对象并不一定都是 mutable 的
- 这一说法正确,因为Java中的集合类(Set、Map、List等)通常提供了不同类型的实现,有些实现是可变的(mutable),即可以修改其中的元素;而有些实现是不可变的(immutable),一旦创建就不能修改。因此,客户端构造得到的集合对象并不一定都是可变的。
C. 声明变量时,在前面加上 final 关键字,可保证该变量是 immutable 的以提高安全性
D. 使用 immutable 类型可以降低程序蕴含 bug 的风险,但可能导致时空性能变差
- 这说法正确,使用不可变类型可以降低程序中因为状态变化而引入的bug风险,因为不可变对象的状态在创建后不会改变,更容易理解和维护。但由于不可变对象在发生变化时需要创建新的对象,可能会导致时空性能变差,尤其是在频繁修改大型对象时。
综上所述,选项B中的说法不恰当,其他选项A、C、D中的说法是正确的。
- 21第5题
在Java中,dynamic checking通常指的是在运行时对代码执行的检查,比如数组越界、空指针异常、类型转换异常等。这些检查是由JVM(Java虚拟机)在运行时自动执行的。
现在,我们来看这些选项:
A.
final StringBuilder sb = new StringBuilder("abc");
sb.append("d");
sb = new StringBuilder("e");
这段代码是合法的。final
修饰的sb
引用不能指向另一个对象,但这里只是修改了sb
引用的值(这实际上是可以的,因为final
修饰的是引用而不是引用指向的对象),并且调用了append
方法。这段代码在运行时不会抛出任何异常,因此能够通过dynamic checking。
B.
List<String> strs = new ArrayList<>();
strs.add("HIT");
for(String s : strs) if(s.startsWith("HIT")) strs.remove(s);
这段代码在运行时会抛出ConcurrentModificationException
,因为在增强for循环中直接修改了集合strs
。这种修改在迭代过程中是不允许的,因为它会破坏迭代器的内部状态。因此,这段代码不能通过dynamic checking。
C.
List<String> list = new ArrayList<>();
List<String> copy = Collections.unmodifiableList(list);
copy.add("c");
这段代码在运行时会抛出UnsupportedOperationException
,因为Collections.unmodifiableList
返回的列表是不允许修改的。尝试调用其add
方法会抛出异常。因此,这段代码不能通过dynamic checking。
D.
String text = "hello" + new String("world");
text = text + false + 1 + 0.1;
这段代码是合法的,并且会正确执行。Java会自动将false
、1
和0.1
转换为字符串,并与前面的字符串进行连接。这段代码在运行时不会抛出任何异常,因此能够通过dynamic checking。
综上所述,能够通过dynamic checking的是A和D。但如果题目只要求选择一个最恰当的答案,那么D选项更加“纯粹”,因为它没有修改任何集合或尝试执行不允许的操作,而A选项尽管最终是合法的,但包含了修改final
引用的操作(尽管这在Java中是允许的)。所以,如果必须选择一个,则D是最恰当的答案。
- 21第6题
Mutability and Immutability
String
是一种不变对象
String s = "a";
s = s.concat("b");
需要重新为s赋值,让s指向一个新的引用。否则s仍然是“a”
StringBuilder
是一种可变对象
StringBuilder sb = new StringBuilder("a");
sb.append("b");
不必为sb重新改引用。
Defensive Copying防御式拷贝
假设我们有一个Person
类,其中包含一个List
类型的属性List<String> phoneNumbers
,表示电话号码列表。如果我们希望在Person
类的方法中避免原始的phoneNumbers
列表被修改,就可以使用防御式拷贝来处理。
示例代码如下所示:
import java.util.ArrayList;
import java.util.List;public class Person {private List<String> phoneNumbers;public Person(List<String> phoneNumbers) {// 使用防御式拷贝,复制传入的phoneNumbers列表this.phoneNumbers = new ArrayList<>(phoneNumbers);}// 获取电话号码列表public List<String> getPhoneNumbers() {// 返回一个新的列表,而不是直接返回原始phoneNumbers列表return new ArrayList<>(phoneNumbers);}
}
在上面的示例中,Person
类中的构造方法和getPhoneNumbers
方法都使用防御式拷贝来处理phoneNumbers
属性。在构造方法中,传入的phoneNumbers
列表会被复制一份,而不是直接引用;在getPhoneNumbers
方法中,同样也会返回一个新的列表的副本,而不是直接返回原始列表。
通过这种方式,无论在何时调用Person
类的方法,都不会影响原始的电话号码列表,因为每次都是操作副本而不是直接操作原始数据。这样可以确保数据对象的不可变性,避免数据被意外修改或破坏。
snapshot digrams
- 基本数据类型:一个单箭头
- 对象数据类型:单箭头指向椭圆
- 不可变对象:单箭头指向双线椭圆
- 不可变引用,双线箭头。
ADT
软件构造 | Designing Specification-CSDN博客
软件构造 | Abstract Data Type (ADT)-CSDN博客
- 22第6题
答案:A
- 22第7题
C. 如果 R 中的两个值被 AF 映射为 A 中的同一个值,那么这两个值所代表的对象是等价的
这个说法不完全准确。在抽象数据类型(ADT)的上下文中,如果两个具体的表示(R 中的值)通过抽象函数(AF)映射到相同的抽象值(A 中的值),这通常意味着这两个具体的表示在抽象级别上是不可区分的。然而,说它们“是等价的”可能过于宽泛,因为“等价”在不同的上下文中可能有不同的含义。在 ADT 的上下文中,我们通常只是说它们在抽象级别上是“不可区分的”。
- 22第8题
- 22第9题
在这个问题中,我们有两个类 X 和 Y,其中 Y 是 X 的子类。这两个类都有一个名为 hit
的方法,但它们的参数类型不同:X 类有一个 hit(X x)
方法,而 Y 类有两个 hit
方法:一个是从 X 类继承的 hit(X x)
,另一个是重载的 hit(Y y)
。
现在,我们来看客户端代码中的对象和方法调用:
X a = new X();
声明了一个 X 类型的变量 a,并初始化为 X 的一个实例。Y b = new Y();
声明了一个 Y 类型的变量 b,并初始化为 Y 的一个实例。X c = new Y();
声明了一个 X 类型的变量 c,但实际上存储了一个 Y 的实例(这是向上转型,因为 Y 是 X 的子类)。
现在,我们分析每个选项:
A. a.hit(b)
- a 是 X 的一个实例,它有一个
hit(X x)
方法。因为 b 是 Y 的一个实例,而 Y 是 X 的子类,所以 Y 的实例可以赋值给 X 类型的参数。因此,这个调用是合法的。
B. b.hit(a)
- b 是 Y 的一个实例,因此它有两个
hit
方法:一个是hit(X x)
(从 X 继承的),另一个是hit(Y y)
(Y 类自己定义的)。因为 a 是 X 的一个实例,不是 Y 的实例,所以这里调用的是hit(X x)
方法(即继承自 X 的那个),这也是合法的。
C. c.hit(b)
- c 是 X 类型的一个变量,但实际上存储了一个 Y 的实例(向上转型)。由于我们在 Java 中只能使用 c 引用 Y 对象中的 X 类定义的方法(多态性),所以我们不能使用
hit(Y y)
方法(因为它是 Y 类特有的,且没有在 X 类中定义)。但是,c 可以调用hit(X x)
方法,因为 b 是 Y 的实例,而 Y 是 X 的子类,所以调用c.hit(b)
实际上是调用了 Y 类中的hit(X x)
方法(由于多态性),这也是合法的。
因此,所有的选项 A、B、C 都是可以通过编译器的静态检查的。答案是 D(以上都对)。
- 21第10题
最恰当的选项是 C。
关于 hashCode()
的说法,我们可以逐一分析选项:
A. 错误。即使你不打算将 ADT 对象放入集合(如 HashSet, HashMap 等)中使用,重写 hashCode()
仍然可能是有意义的。例如,如果你打算在自定义的数据结构中实现某种基于哈希的查找或存储机制,那么重写 hashCode()
会很有帮助。但如果你完全确定你的 ADT 对象永远不会基于哈希进行比较或存储,那么这个选项在技术上可能是正确的,但从一般编程实践的角度来看,它并不是“最恰当”的。
B. 错误。根据 Java 的 hashCode()
契约,两个不相等的对象可以返回相同的哈希码(虽然理想的哈希函数应该尽可能少地产生碰撞)。equals()
方法用于确定两个对象是否相等,而 hashCode()
方法仅用于生成一个整数,该整数用于哈希数据结构中的桶定位。
C. 正确。根据 Java 的 hashCode()
和 equals()
契约,如果子类没有引入新的属性来影响相等性,并且没有重写 equals()
方法,那么它通常也不需要重写 hashCode()
方法。这是因为父类的 hashCode()
实现已经考虑了所有影响相等性的属性。
D. 部分正确,但过于绝对。虽然通常建议在计算哈希码时考虑对象的所有重要属性,但这也取决于你的具体需求和设计决策。例如,如果某些属性对相等性没有影响,或者你知道某些属性在哈希数据结构中的分布是均匀的,那么你可能不需要在 hashCode()
中包含这些属性。
关键名词解释
Representation invariant(RI)
:表示不变式,是对数据表示的约束条件或规则,确保数据表示的合法性和一致性。RI定义了在特定数据表示下,数据的哪些属性必须满足特定条件,不得处于无效状态。RI在类的内部应该被维护和遵守,以确保程序的正确性和可靠性。Representation(rep
):字段:表示数据类型的内部数据结构或状态,是ADT(Abstract Data Type)中的具体实现。rep描述了如何组织和存储数据的具体细节,包括数据的存储形式、数据结构和数据之间的关系。rep的设计应符合RI的约束,以保证数据表示的有效性。Abstraction Function(AF)
:表示抽象函数,是ADT的抽象视角,描述了数据表示和抽象之间的映射关系。AF定义了如何将具体的数据表示映射到抽象的概念或视角上,帮助理解数据的含义和行为。spec
规约:传入参数,返回参数,条件,功能要求。Creators
:构造器是用于创建新对象并初始化其状态的特殊类型的方法。在Java中,构造器的主要作用是初始化类的新实例。Producers
:用于基于现有对象生成新的对象。生产器方法通常不会改变原始对象的状态,而是返回一个新对象,继承或衍生自原始对象。Observers
:观察器是一种类型的方法,用于查看对象的状态或属性,而不修改它们Mutators
:变值器是一种方法类型,用于改变对象的状态或属性值
OOP
软件构造 | Object-Oriented Programming (OOP)-CSDN博客
- 22 第10题
- 22第11题
]答案是B。
让我们逐一分析这些选项:
A.这是正确的。在Java中,LinkedList<Object>
是实现了List<Object>
接口的类,因此我们可以说LinkedList<Object>
是List<Object>
的子类型
B. 这是不正确的。在Java的泛型系统中,List<Integer>
并不是List<Object>
的子类型。这是因为Java的泛型是不变的(invariant),这意味着List<Integer>
和List<Object>
是两个完全不相关的类型。你不能将一个List<Integer>
的实例赋值给一个List<Object>
的变量
C.这是正确的。在Java中,所有的类(包括List
接口的实现)都是Object
的子类。因此,可以将任何类的实例(包括Integer
和List
的实现)存储在Object[]
数组中。
关键名词解释
Interface
:接口中只有方法的定义,没有实现;可以继承和扩展,可以有多个实现类。Enumerations
:枚举Abstract Class
:抽象类是包含抽象方法的类,用abstract
关键字声明。抽象类不能被实例化,只能被用作父类。抽象类可以包含抽象方法和非抽象方法,非抽象方法可以有具体的实现Polymorphism
多态Overriding
:重写一个方法:使用@Override 编译器compiler会检查覆盖方法和被覆盖方法的签名是否完全一致。动态多态性。overloading
重载,静态多态性,参数多态性。方法名相同但参数列表不同,根据调用的方法和传递的参数类型确定具体的方法调用。Generics
:泛型编程是一种编程风格,其中数据类型和函数是根据待指定的类型编写的,随后在需要时根据参数提供的特定类型进行实例化。Subtyping Polymorphism
子类型多态。包括接口多态。
Design Patterns
软件构造 | Design Patterns for Reuse and Maintainability-CSDN博客
- 22第12题
答案C
在考虑如何在两个ADT(抽象数据类型)A和B之间建立委派(delegation)关系时,我们需要关注的是如何设计这种关系以便能够灵活地适应未来的变化。委派是一种设计模式,其中一个对象(在这里是A)将其部分职责委派给另一个对象(在这里是B)。
现在,我们来分析给出的选项:
A. 这个选项在A的创建时就确定了委派对象。如果未来需要更改委派对象,你将不得不重新创建A的实例,这限制了灵活性。
B. 这个选项在A的内部直接创建了一个B的子类型的实例,并且这个实例在A的生命周期内是不可变的(除非在A内部有额外的逻辑来改变它)。这同样限制了灵活性,因为你不能更改委派的对象,除非A的设计允许它这么做。
C. 这个选项允许你在A的实例被创建之后,通过调用setDelegation
方法来更改委派的对象。这提供了最大的灵活性,因为你可以在任何时候更改委派的对象,而不需要重新创建A的实例。
D. 这个选项在每次调用hit()
方法时都创建一个新的委派对象,这通常不是委派模式所期望的。委派模式通常意味着将职责委派给一个持久存在的对象,而不是每次需要时都创建一个新的对象。
- 22 第13题
Factory Method(工厂方法)设计模式不是通过delegation机制实现的
- 22第14题
最恰当的实现机制是 B
在考虑ADT(抽象数据类型)A如何允许客户端遍历其内部复杂数据结构时,我们需要选择一个既安全又易于使用的机制。以下是各个选项的分析:
A. 这个选项直接暴露了ADT的内部数据结构,可能导致表示泄露(representation exposure),即客户端可能直接修改内部数据结构,破坏ADT的封装性。
B. 这个选项是Java等语言中常见的遍历机制,它允许ADT提供一个迭代器(Iterator)对象,客户端通过这个迭代器来遍历ADT的内部数据结构,而不需要直接访问内部数据结构。这样既保证了ADT的封装性,又提供了遍历的灵活性。
C. 这个选项虽然提供了遍历的功能,但没有遵循标准的遍历接口(如Java的Iterable和Iterator),这可能导致与其他代码的不兼容,且如果其他ADT也需要类似的功能,就需要重复实现这些方法。
D. 这个选项过于严格,完全禁止了遍历操作,这在很多情况下是不现实的,因为遍历是ADT的常见需求。
- 21第11题
不恰当的说法是 B
关于 inheritance(继承)和 delegation(委托)的说法,我们可以逐一分析这些选项:
A. 这是正确的。无论是继承还是委托,它们都是代码复用的重要手段。通过继承,子类可以复用父类的代码;通过委托,一个类可以将其部分功能委托给另一个类来处理。
B. 这个说法可能不太恰当。按照面向对象设计的原则,继承通常表示“是一种”的关系,即子类(B)是父类(A)的一种特殊形式。如果B只是A的一个具有少量额外功能的版本,那么使用继承可能不是最佳选择,因为这可能导致继承层次过深或违反里氏替换原则(Liskov Substitution Principle)。在这种情况下,组合(composition,一种形式的委托)可能是更好的选择。
C. 这是正确的。在委托中,类A通常包含一个类型为B的私有成员变量,这样A就可以通过该变量调用B的方法来复用B的功能。
D. 这个说法也是正确的。继承是在类定义时确定的,它定义了子类与父类之间的静态关系。而委托是在运行时通过对象之间的交互来实现的,因此它发生在对象层面。
- 21第13
答案:A
关键名词
delegation
:委派
各类模式特点
Creational patterns ->Factory Method pattern
:工厂方法,有一个中间类Factory
专门用来生成对象。
Product p = new ConcreteTwo().makeObject();
Structural patterns -> Adapter
适配器模式:适配器模式的关键在于它提供了一种机制,使得不兼容的接口能够协同工作,而不需要修改现有的类代码。这有助于提高代码的灵活性和可维护性。
// 目标接口
interface Target {void request();
}// 需要适配的类
class LegacySystem {public void doSomething() {System.out.println("Doing something with Legacy System");}
}// 对象适配器
class LegacySystemAdapter implements Target {private LegacySystem legacySystem;public LegacySystemAdapter(LegacySystem legacySystem) {this.legacySystem = legacySystem;}@Overridepublic void request() {// 使用组合的方式调用doSomething方法legacySystem.doSomething();}
}
Structural patterns -> Decorator
public abstract class CoffeeDecorator implements Coffee { protected Coffee coffee; public CoffeeDecorator(Coffee coffee) { this.coffee = coffee; } @Override public double getCost() { return coffee.getCost(); } @Override public String getDescription() { return coffee.getDescription(); }
}// 装饰器
public class MilkDecorator extends CoffeeDecorator { public MilkDecorator(Coffee coffee) { super(coffee); } @Override public double getCost() { return super.getCost() + 0.5; // 假设加奶增加0.5元 } @Override public String getDescription() { return super.getDescription() + ", Milk"; }
}
Behavioral patterns ->Strategy
:有多种不同的算法来实现同一个任务,但需要client根据需要 动态切换算法.
public class PaymentClient { private PaymentStrategy paymentStrategy; public PaymentClient(PaymentStrategy strategy) { this.paymentStrategy = strategy; } public void makePayment(int amount) { paymentStrategy.pay(amount); } public static void main(String[] args) { PaymentClient client = new PaymentClient(new CreditCardStrategy("John Doe", "1234567890123456", "123", "12/25")); client.makePayment(100); // 100 paid with credit card client = new PaymentClient(new PaypalStrategy("user@example.com", "password")); client.makePayment(50); // 50 paid using Paypal. }
}
Behavioral patterns ->Template Method
设计模式是一种行为设计模式,它在一个方法中定义了一个算法的骨架,并允许子类为一个或多个步骤提供实现。模板方法使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。
public abstract class OrderProcessTemplate {public boolean isGift;public abstract void doSelect();public abstract void doPayment();public void giftWrap() {System.out.println("Gift wrap done.");
}public abstract void doDelivery();public final void processOrder() {doSelect();doPayment();if (isGift)giftWrap();doDelivery();}
}
Behavioral patterns ->Iterator
客户端希望遍历被放入 容器/集合类的一组ADT对象.
// 聚合接口
interface Aggregate { Iterator createIterator();
} // 迭代器接口
interface Iterator { boolean hasNext(); Object next();
} // 具体聚合(例如一个列表)
class MyList implements Aggregate { private List<Object> elements = new ArrayList<>(); // 添加元素的方法 public void add(Object element) { elements.add(element); } // 创建迭代器的方法 @Override public Iterator createIterator() { return new MyListIterator(this); } // 内部类:具体迭代器 private class MyListIterator implements Iterator { private int currentIndex = 0; private MyList list; public MyListIterator(MyList list) { this.list = list; } @Override public boolean hasNext() { return currentIndex < list.elements.size(); } @Override public Object next() { if (!hasNext()) { throw new NoSuchElementException(); } return list.elements.get(currentIndex++); } }
} // 客户端代码
public class Client { public static void main(String[] args) { MyList list = new MyList(); list.add("Element 1"); list.add("Element 2"); list.add("Element 3"); Iterator iterator = list.createIterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } }
}
visitor
public interface Visitor { void visit(NodeA nodeA); void visit(NodeB nodeB);
} public interface Node { void accept(Visitor visitor);
}
异常
- 21第15题
关于给出的四个关于 assert
和 exception
的做法,我们来逐一分析它们:
A. 对 Java 中的 error 和 unchecked 异常,编程时最好不要用 try/catch 的方式来捕获
这个做法通常是正确的。Error
是 Java 中表示系统级错误的类,比如 OutOfMemoryError
,它们通常是无法恢复的,因此不建议用 try/catch 来捕获。而 unchecked 异常(运行时异常,如 NullPointerException
)通常表示编程错误,也不应该通过 try/catch 来处理,除非你有特别的理由需要这样做。
B. 一个方法根据传入的磁盘路径读入一个文件,该方法内部首先应该检查文件是否存在,若不存在,则用 assert false 语句终止程序执行
这个做法是不恰当的。assert
语句在 Java 中主要用于在开发和测试期间进行调试,它用于检查一个条件是否为真。如果条件为假,并且启用了断言(默认情况下断言是禁用的),那么程序会抛出一个 AssertionError
。但是,对于文件不存在这种业务逻辑上的错误,应该使用异常(比如 FileNotFoundException
)来处理,而不是 assert
。
C. 在一个方法的最开始,最好应检查 pre-condition 是否满足,若不满足,用“抛出异常”的方式提示用户比用 assert 语句更合适
这个做法是正确的。当方法的 pre-condition(前置条件)不满足时,使用异常来通知调用者是更合适的选择,因为异常是 Java 中用于处理错误情况的机制。
D. 在一个方法的 return 语句之前(若无 return 则在方法最后),应检查 post-condition 是否满足,若不满足,用 assert 语句比用“抛出异常”的方式更合适
这个做法在某些情况下是合适的,特别是当 post-condition 的不满足表示了一个内部错误或不一致的状态,而不是一个可以由调用者处理的错误时。但是,这也取决于具体的情况和上下文。在某些情况下,即使 post-condition 不满足,也可能需要抛出异常来通知调用者。
综上所述,最不恰当的做法是 B,因为对于文件不存在这种业务逻辑上的错误,应该使用异常来处理,而不是 assert
。所以正确答案是 B。
try -catch
当我们在Java中处理异常时,我们通常会遵循一套标准的模式来确保程序的健壮性和可维护性。以下是一个具体的例子,展示了如何在Java中使用try-catch-finally块来处理异常。
假设我们有一个从文件中读取数据的方法,并且这个方法可能会遇到FileNotFoundException
和IOException
这两种类型的异常。
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException; public class ExceptionHandlingExample { public static void main(String[] args) { String filePath = "example.txt"; // 假设这是我们要读取的文件路径 String fileContent = readFile(filePath); System.out.println("文件内容: " + fileContent); } public static String readFile(String filePath) { StringBuilder contentBuilder = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { String line; while ((line = reader.readLine()) != null) { contentBuilder.append(line).append("\n"); } return contentBuilder.toString(); } catch (FileNotFoundException e) { // 处理文件未找到的情况 System.err.println("文件未找到: " + filePath); e.printStackTrace(); return null; // 或者抛出一个更具体的自定义异常 } catch (IOException e) { // 处理其他I/O错误 System.err.println("读取文件时发生I/O错误: " + filePath); e.printStackTrace(); return null; // 或者抛出一个更具体的自定义异常 } finally { // 无论是否发生异常,这里的代码都会被执行 // 在这个例子中,由于我们使用了try-with-resources语句,所以不需要显式关闭reader // 如果这里没有使用try-with-resources,我们需要在这里显式调用reader.close() System.out.println("文件读取完成(无论是否成功)"); } }
}
- try块:包含可能会抛出异常的代码。在这个例子中,我们使用
BufferedReader
从文件中读取内容。由于文件可能不存在或者其他I/O问题,这段代码可能会抛出FileNotFoundException
或IOException
。 - catch块:用于捕获并处理try块中抛出的异常。在这个例子中,我们有两个catch块,一个用于处理
FileNotFoundException
,另一个用于处理IOException
。在每个catch块中,我们都打印出错误消息,并调用e.printStackTrace()
来打印异常的堆栈跟踪。然后,我们返回一个null值(或者可以抛出一个更具体的自定义异常)。 - finally块:无论try块中的代码是否抛出异常,finally块中的代码都会被执行。在这个例子中,我们打印出一条消息来表示文件读取已经完成(无论是否成功)。注意,由于我们使用了Java 7引入的try-with-resources语句,所以我们不需要在finally块中显式关闭
BufferedReader
。try-with-resources语句会自动关闭实现了AutoCloseable
或Closeable
接口的资源。
这个例子展示了Java异常处理机制的基本用法,包括try-catch-finally块的使用、异常类型的捕获以及资源的自动管理。
throw
class MyException extends Exception { public MyException(String message) { super(message); }
} public void doSomething() { // ... 一些代码 ... if (/* 某个条件 */) { throw new MyException("发生了某种错误"); } // ... 其他代码 ...
}
显示的抛出异常。
如果某方法实现中包含了 throw new xxException(...)
的代码,则该方法的 spec(规范或接口)中一定会包含 throws xxException
。