堆排序

堆排序描述

堆排序是利用堆这种数据结构所设计的一种排序算法。堆实际上是一个完全二叉树结构

问:那么什么是完全二叉树呢?答:假设一个二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树

image-20210911103831357

我们知道堆是一个完全二叉树了,那么堆又分两种堆:大顶堆小顶堆它们符合一个重要的性质:

  • 小顶堆满足:Key[i] <= key[2i + 1] && Key[i] <= key[2i + 2] (父节点小于子节点)
  • 大顶堆满足:Key[i] >= key[2i + 1] && Key[i] >= Key[2i + 2] (父节点大于子节点)

大顶堆最大的元素在跟节点,堆的性质决定了大顶堆中节点一定大于等于其子节点,反之,小顶堆的最小元素在根节点。我们来看看大顶堆和小顶堆的示意图:

image-20210911104452575

堆排序基本思想及步骤

堆排序有以下几个核心的步骤:

  1. 将待排序的数组初始化为大顶堆,该过程即建堆。
  2. 将堆顶元素与最后一个元素进行交换,除去最后一个元素外可以组建为一个新的大顶堆。(获得当前最大的元素
  3. 将第二步堆顶元素跟最后一个元素交换后,新建立的堆不是大顶堆,需要重新建立大顶堆。重复上面的流程,直到堆中仅剩下一个元素。
阅读更多

HR面相关

网易云

1. 自我介绍

您好,我叫李冉冉,15年至19年就读于合肥师范学院物联网工程专业,在校期间参加中国机器人大赛获得二等奖。19年考入上海电力大学,进行计算机技术专硕的学习,在校期间参加研究生数学建模竞赛获得三等奖;然后就是20年12月开始去上海这边的一个研究所进行测试工作的实习;并且自己的小论文在21年4月份也被录用,完成了毕业条件,以上就是我的自我介绍。

2. 怎么学习的,介绍一下学习方法

主要是了解一下大致有哪些内容,然后去b站找视频看,然后跟随视频的进度进行实操,然后做好记录,当中需要注意的地方。

3. 项目的难点,怎么克服的,以及有什么收获

  • 难点,怎么克服的

    这里讲一下建模吧,当时我们队伍是3个人,我在里面负责写程序,另一名同学负责写论文,还有一名同学负责模型的构建。建模的时候我们选的是c题,是一个类似时间序列预测的问题,当时是弄了4天3夜,前两天我们一直卡在第一题,数据处理出来的结果,送入构建好的模型之后得出的效果并不好(分类结果差不多是50%,等于乱猜),当时用了挺多的方法,这里说明一下当时我们那天的要处理的数据是脑电信号,后来我们去知网查找硕博论文,有一篇文章是说脑电信号是低频信号,需要数据进行滤波,然后我们就去查找实现的方法,最后按照那篇论文里的步骤解决了这个问题。

  • 有什么收获

    因为时间很紧张,认识到团队合作的重要性,有什么问题要及时沟通,最后我们把论文提交的时候,我跟他们说要不是你们我早都想放弃了,然后他们跟我说,他们也是这样想的,因为我们当时两天都卡在第一问,比较沮丧。

4. 为什么选择网易云

网易云的老用户了,等级现在是十级。而且网易云音乐是国内音乐类唯一一家在Linux系统上有客户端的公司。

5. 对网易有什么感兴趣的地方

网易的产品做的挺好的,网易云音乐,网易云课堂等等比其他家的同类做的都要好,个人感觉

6. 职业规划

  1. 先是把业务弄熟,积累经验,把整个流程搞清楚,还有就是学习公司的测试框架,阅读源码,看看有没有机会去改进。
  2. 接下来,就是看看整个业务流程中有没有改进的余地,把一些不能自动化的能不能做到自动化这种,还有就是提高沟通能力
  3. 打磨技术~~带领团队

7. 喜欢网易云什么功能,有没有改进的地方

  1. 网易云音乐云盘里导入的歌曲可能不带歌词,这里感觉可以去在线匹配一下。
  2. 有的歌词不是很同步,希望可以手动同步

8. 自身的优缺点

  • 优点,做事比较尽全力吧,面对问题会找不同的办法,和请教前辈去解决
  • 缺点,在公开场合就是陌生人比较多的环境说话会比较紧张,不太敢说话。还有就是有的时候做事情不够细致,会粗心犯错。

9. 优缺点对工作有什么影响

对工作会全力以赴的去完成,针对自身的缺点,需要在工作时更加细致一点,力图避免因为粗心的问题导致的错误。

10. 最有挫败感的一件事

选择方向的时候,选了好几个方向,第一个方向是对话系统看了一年左右的论文,最后因为大家用的技术都差不多感觉改不出来什么就放弃,后来导师帮忙选择了一个课题,然后自己查阅论文也做了一些工作,但是感觉效果不太好,也放弃了,但是这里心态比较不好,感觉要毕不了业,颓废了一段时间,后来也是慢慢看论文找到了现在的方向,和第一个对话系统也算有相关,也算没有全部丢下。

11. 加班的看法

如果工作需要的话可以加班,之前在实习的时候因为要赶时间节点,配合团队leader也加了一部分班。

12. 为什么选择测开,什么时候开始选择测开的

在实习的过程找到bug的感觉也挺好的,而且自己学的东西也能用到工作中提高效率,在当中学到了挺多的,比如就是如何和别人沟通

13. 读研期间最大的挑战

同10

14. 除了学习方面,有什么兴趣

阅读,听音乐,看动漫,看电影,打游戏,打羽毛球

15. 和团队成员有过分歧吗,怎么解决的

Java多线程

synchronized

用法

synchronized可以用来锁方法锁代码块

锁方法又可以分成对象锁和类锁,synchronized加在普通方法上就是锁得是当前对象;加在static 的方法上锁的是当前类。

锁代码块也可以分成对象锁和类锁,在方法中使用synchronized(object)手动指定锁的对象;synchronized(object.class)锁的是类。类锁跟对象没关系。

总体来说,synchronized锁的对象和类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SynTest{
Object obj = new Object();

// 同在方法上
// 对象锁,锁new出的对象
public synchronized void lock1(){}

// 用在方法上
// 类锁,锁new出的类
public synchronized static void lock2(){}

// 锁代码块
// 对象锁
public void lock3(){
synchronized(object){
//todo
}
}
// 类锁
public void lock4(){
stnchronized(SynTest.class){
//todo
}
}
}

注:在普通方法上加锁,锁得是对象,哪个对象调用得这个加锁方法,锁的就是哪个对象。普通同步方法只能用在单例上,因为单例共用一个对象,多例中会有多个对象了,就是多把锁了。

特性

synchronized锁定一段代码或方法的时候,代表同一时刻最多只有一条线程执行这段代码,所以并发的原子性、可见性、有序性它都可以保证。

可重入性:synchronized是可以重入锁,同一个线程拿到锁后再遇到该锁,还可以获取该锁。它底层是通过一个计数器来实现的,获取该锁,计数器+1,在获取该锁,计数器再+1。释放锁,计数器-1,直到计数器为0,其他线程才可以竞争该锁。可重入性的好处是避免死锁。

不可被中断,不能响应超时:线程进入了锁里,其他线程只能在外面阻塞,没办法中断这个线程,如果这个线程发生死锁了,其他线程永远阻塞,可能导致程序崩溃。可以用ReentrantLock替换,它是可以被中断的。

当synchronized正常退出或抛出异常时会主动释放锁。

参考链接:(57条消息) synchronized_csdn_binger的博客-CSDN博客

海量数据面试题

1. 两个大文件找共同的url

给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url?

方案1

预估每个文件的大小为5G $\times$ 64 =320G,远大于内存限制,可以采取分治的办法。

  1. 遍历文件$a$,对每个$url$取$hash(url)%1000$,根据取得的值将url分别存储到1000个小文件中。这样每个肖文杰大约为300M。
  2. 遍历文本$b$​,采取和$a$相同的文件方式将url分别存储到1000个小文件。这样所有可能相同的url都在对应的小文件中,不对应的小文件不可能有相同的url。然后只需要求出1000对小文件中相同的url即可。
  3. 求每对小文件相同的url时,可以把其中一个小文件的url存储到hash_set中,然后遍历另一个小文件中的url,看其是否在刚才构建的hash_set中,如果是,那么就是共同的url,存到文件中即可。

方案2

布隆过滤器

2. 多个大文件的query排序

有10个文件,每个文件1G,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。要求你按照query的频度排序。

方案1

  1. 顺序读取10个文件,按照hash(query)%10的结果将query写入到另外10个文件。这样新生成的文件每个的大小大约也1G(假设hash函数是随机的)。
  2. 找一台2G内存左右的机器,依次对十个文件使用hash_map(query,query_count)来统计每个query出现的次数。利用排序按照出现次数进行排序。将排序好的query和对应的query_count输出到文件中。这样得到了10个排好序的文件。
  3. 对第二步排好序的文件进行归并排序(内排序与外排序相结合)。

方案2

可以使用布隆过滤器

3. 大文件中的内容求频率

有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。

方案1:

  1. 顺序读取文件,对每个词$x$,取$hash(x)%5000$取余,然后按照该值存到5000个小文件中。这样每个文件大约是200k,如果其中有的文件超过了1M,按照这种方法继续去分。
  2. 对每个小文件,统计每个文件中出现的词以及相应的频率,并取出出现频率最大的100个词,并把100词及相应评率存入文件,这样又得到了5000个文件。
  3. 下一步对5000个文件进行归并。

java基础

Java泛型

泛型的本质是参数化类型,既所操作的数据类型被指定为一个参数。

Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
List<Integer> list = new ArrayList<>();


list.add(12);
//这里直接添加会报错
// list.add("a");
Class<? extends List> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add", Object.class);
//但是通过反射添加,是可以的
add.invoke(list, "kl");

for (Object obj : list) {
System.out.println(obj + " " + obj.getClass().toString());
}
System.out.println(list);
}

输出:

1
2
3
12 class java.lang.Integer
kl class java.lang.String
[12, kl]

泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。

常用通配符: T,E,K,V,?

  • ? 表示不确定的 java 类型
  • T (type) 表示具体的一个 java 类型
  • K V (key value) 分别代表 java 键值中的 Key Value
  • E (element) 代表 Element

泛型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
在实例化泛型类时,必须指定T的具体类型
*/
public class Generic<T> {
private T key;
public Generic(){

}
public Generic(T key){
this.key=key;
}
public void setKey(T key){
this.key = key;
}
public T getKey(){return this.key;}

public static void main(String[] args) {
// TODO 实例化泛型类
Generic<Integer> generic = new Generic<>();
generic.setKey(1234);
System.out.println(generic.getKey()+" "+generic.getKey().getClass().getName());
}
}

泛型接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
interface generInter<T>{
public T method();
}
// TODO 不指定类型
class GenerInterfaceImpl2<T> implements generInter<T>{
private T key;
public void setKey(T key){
this.key = key;
}
@Override
public T method() {
return this.key;
}
}
// TODO 指定类型
public class GenerInterfaceImpl implements generInter<String> {
@Override
public String method() {
return "Test 泛型接口";
}

public static void main(String[] args) {
GenerInterfaceImpl generInterface = new GenerInterfaceImpl();
GenerInterfaceImpl2<String> generInterfaceImpl2 = new GenerInterfaceImpl2();
generInterfaceImpl2.setKey("1234");
System.out.println(generInterface.method());
System.out.println(generInterfaceImpl2.method());

}
}

输出:

1
2
Test 泛型接口
1234

泛型方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class GenerMethod {
public static <E> void printArray(List<E> inputArray){
for(E element: inputArray){
System.out.printf("%s ",element);
}
System.out.println();
}

public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("123");
list.add("456");
list.add("789");
list.add(0,"777");
printArray(list);
}
}

输出

1
777 123 456 789 

equals方法

equals() 作用不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类。

Objectequals() 方法:

1
2
3
public boolean equals(Object obj) {
return (this == obj);
}

equals() 方法存在两种使用情况:

  • 类没有覆盖equals()方法:通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Objectequals()方法。
  • 类覆盖了equals()方法:一般我们都覆盖 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。

String中的equals方法:

  • String 中的 equals 方法是被重写过的,因为 Objectequals 方法是比较的对象的内存地址,而 Stringequals 方法比较的是对象的值。
  • 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

hashCode与equals():

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。

hashCode()定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。

为什么要有hashCode

我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode?

  1. HashSet先计算对象的hashCode值来判断对象加入的位置,同时也与其他已经加入的对象进行比较。
  2. 如果没有重复的hashCode则可以加入,如果有重复的hashCode则再去调用equals()方法去检查hashcode相等的对象内容是否相同。
  3. 如果相同则不让其加入,如果不同则,重新散列其位置。

为什么重写equals时必须重写hashCode方法

如果两个对象相等,则 hashcode 一定也是相同的。

两个对象相等,对两个对象分别调用 equals 方法都返回 true。

但是,两个对象有相同的 hashcode 值,它们也不一定是相等的 。

因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖。

hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?

因为 hashCode() 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。

越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode

我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。

静态方法和实例方法有何不同?

1. 调用方式

在外部调用静态方法时,可以使用 类名.方法名 的方式,也可以使用 对象.方法名 的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象

一般建议使用 类名.方法名 的方式来调用静态方法。

2. 访问类成员限制

静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。

重载和重写

重载

重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理;

重写

重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法。

  1. 返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
  2. 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
  3. 构造方法无法被重写

成员变量和局部变量有哪些区别

  1. 从语法形式上看:
    1. 成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;
    2. 成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰
    3. 成员变量和局部变量都能被 final 所修饰
  2. 从变量在内存中的存储形式看:
    1. 成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
  3. 从变量在内存中的生存时间上看:
    1. 成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
  4. 从变量是否有默认值来看
    1. 成员变量如果没有被赋初值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值)
    2. 局部变量则不会自动赋值。

继承与多态

继承

  1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有
  2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
  3. 子类可以用自己的方式实现父类的方法。

多态

多态,顾名思义,表示一个对象具有多种的状态。具体表现为父类的引用指向子类的实例

特点:

  • 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
  • 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
  • 多态不能调用“只在子类存在但在父类不存在”的方法;
  • 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。

依赖注入

定义数据提供接口与实现类
1
2
3
4
5
6
7
package DI.dao;

// 用于模拟依赖注入,
// 公共的接口
public interface UserDao {
void getUser();
}

1
2
3
4
5
6
public class UserDaoImpl implements UserDao{
@Override
public void getUser() {
System.out.println("默认获取用户的数据");
}
}

1
2
3
4
5
6
public class UserDaoMySqlImpl implements UserDao {
@Override
public void getUser() {
System.out.println("获取MYSQL用户数据");
}
}
定义数据使用抽象类与实现类
1
2
3
4
5
6
7
public abstract class UserService {
abstract void getUser();

public static void main(String[] args) {
System.out.println("--");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UserServiceImpl extends UserService{
// TODO userDao有很多实现他的子类
private UserDao userDao;

// TODO 传进来不同的子类
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

@Override
public void getUser() {
userDao.getUser();
}
}
定义测试函数
1
2
3
4
5
6
7
public class Test {
public static void main(String[] args) {
UserServiceImpl userService = new UserServiceImpl();
userService.setUserDao(new UserDaoMySqlImpl());
userService.getUser();
}
}

接口与抽象类

抽象类

在实现多态时,如果一个父类的方法本身不需要实现任何功能,仅仅为了定义方法签名,目的是让子类覆盖他,那么,可以把父类定义为抽象方法。

把一个方法声明为抽象方法,导致了其本身无法执行,所以含有抽象方法的类无法被实例化,那么也必须把这个类声明为abstract,才能正确编译他。

  1. 抽象类有构造方法,是给子类创建对象的
  2. 抽象类中不一定有抽象方法,抽象方法一定在抽象类中

abstract关键字不可以与哪些关键字使用:

  1. private冲突:private修饰的成员不能被继承,从而不可以被子类重写,而abstract修饰的是要求被重写的
  2. final冲突:final修饰的成员是最终成员,不能被重写,所以冲突
  3. static冲突:static修饰成员用类名可以直接访问,但是抽象方法没有方法体,故访问没有方法体的成员没有意义.

接口

  1. 接口不能被实例化
  2. 接口只能包含方法的声明
  3. 接口的成员方法包括:方法,属性,索引器,事件
  4. 接口中不能包含常量,字段,构造函数,静态成员

两者的区别

  • 抽象类可以有构造方法,接口中不能有构造方法
  • 抽象类中可以有普通成员变量,接口中没有普通成员变量
  • 抽象类中可以包含静态方法,接口中不能包含静态方法
  • 一个类可以实现多个接口,但只能继承一个抽象类
  • 接口可以被多重实现,抽象类只能被单一继承
  • 如果抽象类实现接口,则可以把接口中方法映射到抽象类中作为抽象方法而不必实现,而在抽象类的子类中实现接口中方法

相同点:

  • 都可以被继承
  • 都不能被实例化
  • 都可以包含方法声明
  • 派生类必须实现未声明的方法

总结:

  • 接口带来的最大好处就是避免了多继承带来的复杂性和低效性,并且同时可以提供多重继承的好处
  • 接口和抽象类都可以提现多态性,但是抽象类对事物进行抽象,更多的是为了继承,为了扩展,为了实现代码的重用,子类和父类之间提现的是is-a关系,接口则更多的体现一种行为约束,一种规则,一旦实现了这个接口,就要给出这个接口中所以方法的具体实现,也就是实现类对于接口中所有的方法都是有意义是的

多重继承的实现:https://blog.csdn.net/qq_36599564/article/details/102579688


StringBuffer与StringBuilder

可变性

简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[],所以String 对象是不可变的。

StringBuilderStringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。

线程安全性

String的对象是final,线程安全;

StringBuffer中的方法加了同步锁,是线程安全的;

StringBuilder没有对方法加同步锁,故不是安全的。

总结

  • 操作少量的数据: 适用 String
  • 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
  • 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
  • 字符串拼接,String使用+或者concatStringBuilderStringBuffer使用append

异常

image-20210904095550792

所有的异常都继承于Throwable

其两个子类:

  • Exceptions:能被程序本身处理(try-catch
  • Error:无法处理
    • Virtual Machine Error:java虚拟机错误
    • OutOfMemoryError:虚拟机内存不足
    • NoClassDefFoundError:类定义错误

受查异常(必须处理) :

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。常见的受检查异常有: IO 相关的异常、ClassNotFoundExceptionSQLException…。

不受查异常(可以不处理)

Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。

RuntimeException 及其子类都统称为非受检查异常,例如:NullPointerExceptionNumberFormatException(字符串转换为数字)、ArrayIndexOutOfBoundsException(数组越界)、ClassCastException(类型转换错误)、ArithmeticException(算术错误)等。

try-catch-finally

  • try块: 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • catch块:用于处理 try 捕获到的异常。
  • finally 块: 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。

finally块不会被执行的特殊情况

  1. tryfinally块中用了 System.exit(int)退出程序。但是,如果 System.exit(int) 在异常语句之后,finally 还是会被执行
  2. 程序所在的线程死亡
  3. 关闭 CPU

注意: 当 try 语句和 finally 语句中都有 return 语句时,在方法返回之前,finally 语句的内容将被执行,并且 finally 语句的返回值将会覆盖原始的返回值。