《面试宝典》读书笔记 Ⅲ

异常处理、输入输出流、Java 平台与内存管理

finally 块不一定会被执行

  • 当程序在进入try 块之前就出现异常,会直接结束,不会执行 finally 块中的代码;
  • 当程序在 try 块中强制退出时,也不会去执行 finally 块中的代码。

异常处理的原理

异常处理的目的:为了提高程序的安全性与鲁棒性。
JVM 将出现的错误表示为一个异常并抛出,这个异常可以在 catch 块中进行捕获,然后进行处理。
Java 中把异常当作对象来处理,并定义了一个基类(java.lang.Throwable)作为所有异常的父类。

异常的分类

Error

表示程序在运行期间出现了非常严重的错误,并且该错误是不可恢复的。
JVM 层次的严重错误,会导致程序终止执行,编译器不会检查 Error 是否被处理。

Exception

表示可恢复的异常,是编译器可以捕获到的。包含两种类型:

  • 检查异常(checked exception):
    程序中最经常碰到的异常,发生在编译阶段,编译器强制程序去捕获此类型的异常。
    • 异常的发生并不会导致程序出错,进行处理后可以继续执行后续的操作。例如,SQL 异常;
    • 程序依赖于不可靠的外部条件。例如,IO 异常。
  • 运行异常(runtime exception):
    编译器没有强制对其进行捕获并处理。
    • 多线程用 Thread.run() 方法抛出,这个线程也就退出了;
    • 单线程用 main() 方法抛出,整个程序也就退出了。

如果不对运行时的异常进行处理,后果是非常严重的,一旦发生,要么是线程终止,要么是主程序终止。

需要注意的问题

  • 在进行异常捕获时,正确的写法是:先捕获子类,再捕获基类的异常信息。
  • 尽早抛出异常,同时对捕获的异常进行处理。
  • 可以根据实际的需求自定义异常类
  • 异常能处理就处理,不能处理就抛出。

对于最终没有处理的异常,JVM 会进行处理。


IO 流的实现机制

Java IO 类在设计时采用了装饰者(Decorator)设计模式。
在 java.io 包中有许多的流,流的作用主要是为了改善程序设计性能并且使用方便。

字节流

以字节(8 bit)为单位,包含两个抽象类:InputStream、OutputStream。
处理时,不会用到缓存

字符流

以字符(16 bit)为单位,包含两个抽象类:Reader、Writer。
处理时,用到了缓存。

File 类

对文件或目录进行管理与操作在编程中有着非常重要的作用,Java 提供了一个非常重要的类(File)来管理文件和文件夹。

Java Socket

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个双向链路的一端称为一个 Socket。
Socket 也称为套接字,可以用来实现不同虚拟机或不同计算机之间的通信。任何一个 Socket 都是由 IP 地址和端口号唯一确定的。

生命周期

可以分为三个阶段:

  1. 打开 Socket
  2. 使用 Socket 收发数据
  3. 关闭 Socket

Java中,可以使用 ServerSocket 来作为服务器端,Socket 作为客户端来实现网络通信。

Java NIO

非阻塞IO(Nonblocking IO)通过 Selector、Channel、Buffer 来实现非阻塞的 IO 操作:

NIO 的实现主要采用了反应器(Reactor)设计模式,可以用来处理多个事件源。
与传统 Socket 方式相比,由于 NIO 采用了非阻塞的方式,在处理大量并发请求时,使用 NIO 要比使用 Socket 效率高出很多。

Java 序列化

提供了两种对象持久化的方式:序列化、外部序列化。

序列化(Serialization)

序列化是一种将对象以一连串的字节描述的过程,用于解决在对对象进行读写操作时所引发的问题。
所有要实现序列化的类都必须实现 Serializable 接口,位于 java.lang 包中,它里面没有包含任何方法。
特点:

  • 如果一个类能被序列化,那么它的子类也能够被序列化;
  • 由于 static 代表类的成员,transient 代表对象的临时数据,因此被声明为这两种类型的数据成员是不能够被序列化的。

由于序列化的使用会影响系统的性能,因此如果不是必须要使用序列化,应尽可能不要使用序列化,需要使用序列化的情况:

  • 需要通过网络来发送对象,或对象的状态需要被持久化到数据库或文件中;
  • 序列化能实现深复制,即可以复制引用的对象。

反序列化

将流转换为对象,每个类都有一个特定的 serialVersionUID,通过其判定类的兼容性。
自定义 serialVersionUID 的优点:

  • 提高程序的运行效率。通过显式声明 serialVersionUID 的方式,省去了计算的过程,因此提高了程序的运行效率;
  • 提高程序不同平台的兼容性。各个平台的编译器在计算 serialVersionUID 时,有可能会采用不同的计算方式;
  • 增强程序各个版本的兼容性。后期对类进行修改时,类的 serialVersionUID 值将会发生变化。

外部序列化

使用外部序列化时,Externalizable 接口中的读写方法必须由开发人员来实现,编写程序的难度更大。
由于把控制权交给了开发人员,具有更多的灵活性。
实现只序列化部分属性:

  • 开发人员可以根据实际需求,实现 readExternal 与 wirteExternal 方法,来控制序列化与反序列化所使用的属性;
  • 使用关键字 transient,被修饰的属性是临时的,不会被序列化。

System.out.println()

提供了一种非常有效简单的方法来实现控制台的输出,该方法默认接收一个字符串类型的变量作为参数。

1
2
3
4
5
class Test
{
System.out.println(1 + 2 + "");
System.out.println("" + 1 + 2);
}

运行结果为:

1
2
3
12

JVM 解释执行的过程

代码的装入

装入代码的工作,由类装载器完成。

代码的校验

被装入的代码由字节码校验器进行检查。

代码的执行

  • 即时编译方式
  • 解释执行方式

Java 平台

Java 平台主要包含两个模块:JVM、Java API(Application Program Interface)。

JVM

一个虚构出来的计算机,有自己完善的硬件结构,屏蔽了与具体操作系统平台相关的信息。
每当一个 Java 程序运行时,都会有一个对应的 JVM 实例,只有当程序运行结束后,这个 JVM 才会退出。

Java API

Java 为了方便开发人员开发而设计的,这些接口也是用 Java 语言编写的,并且运行在 JVM 上。

加载 class 文件的原理机制

Java 语言是一种具有动态性的解释型语言,类只有被加载到 JVM 中后才能运行。
类加载器本身也是一个类,由 ClassLoader 和它的子类来实现,其实质是把类文件从硬盘读取到内存中

类的加载方式

  • 隐式:使用 new 等方式创建对象,会隐式地调用类的加载器把对应的类加载到 JVM 中;
  • 显式:通过直接调用 class.forName() 方法,把所需的类加载到 JVM 中。

类的分类

类的加载是动态的,并不会一次性将所有类全部加载后再运行。而是保证基础类完全加载到 JVM 中,至于其他类,则在需要时才加载。

  • 系统类
  • 扩展类
  • 自定义类

Java 针对这 3 种不同的类,提供了 3 种类型的加载器:

类加载的步骤

  • 装载:根据查找路径找到对应的 class 文件,然后导入。
  • 链接:
    • 检查,检查待加载的 class 文件的正确性;
    • 准备,给类中的静态变量分配存储空间;
    • 解析,将符号引用转换成直接引用(可选)。
  • 初始化:对静态变量、静态代码块执行初始化工作。

GC(Garbage Collection)

垃圾回收是一个非常重要的概念,主要作用是回收程序中不再使用的内存
垃圾回收器负责完成的三项任务:

  1. 分配内存
  2. 确保被引用对象的内存不被错误地回收
  3. 回收不再被引用对象的内存空间

为了实现垃圾回收,垃圾回收器必须跟踪内存的使用情况,必定会增加 JVM 的负担,从而降低程序的执行效率。

使用有向图来记录和管理堆内存中的所有对象。“不可达”的对象都是可被垃圾回收的。

垃圾回收算法

  • 引用计数算法(Reference Counting Collector)
  • 追踪回收算法(Tracing Collector)
  • 压缩回收算法(Compacting Collector)
  • 复制回收算法(Coping Collector)
  • 按代回收算法(Generational Collector)

内存泄漏问题

判断垃圾回收的标准

  • 给对象赋予了空值 null,以后再没有被使用过;
  • 给对象赋予了新值,重新分配了内存空间。

内存泄漏的原因

  • 静态集合类。例如 HashMap、Vector,如果这些容器为静态的,由于它们的生命周期与程序一致,那么容器中的对象在程序结束之前将不能被释放。
  • 各种连接。例如数据库连接,如果在访问数据库的过程中,不显式地关闭,将会造成大量的对象无法被回收。
  • 监听器。在释放对象的同时往往没有相应地删除监听器。
  • 变量不合理的作用域:
    • 变量定义的作用范围大于其使用范围;
    • 没有及时地把对象设置为 null。
  • 单例模式。由于单例对象以静态变量的方式存储,因此它在 JVM 的整个生命周期中都存在。

堆与栈的区别

栈内存用来存放基本数据类型、引用变量。
主要是用来执行程序的,存取速度更快。但大小和生存周期必须是确定的,缺乏一定的灵活性。

堆内存用来存放运行时创建的对象
主要用来存放对象的,可以在运行时动态地分配内存,生存期不用提前告诉编译器,但这也导致了其存取速度的缓慢

存储在栈中的变量通过压栈、弹栈操作,将会在栈中被回收;而存储在堆中的对象将会由垃圾回收器来自动回收。