【异常】
① 异常也是对象 ↓
② 如果你想亲自抛出异常,一定要先声明:
public void takeRisk() throws BadException { // 抛出之前一定要声明"throws..."
if(true){
throw new BadException(); // 异常的本质还是类,抛出的实际上是new出的对象
}
}
// 另外,这个自己写的BadException异常类一定要继承Throwable类
③ try-catch-finally结构
try {
doSomething(); // 如果这句被catch到异常,try的之后的代码(doOtherthing())不会执行
doOtherthing();
} catch(Exception e) {
e.printStackTrace(); // 只有catch到了相应的异常,该程序块才会执行
} finally {
doNothing(); // finally中的程序块是无论如何都要执行的(无论如何!)
}
有一个细节:
如果try或catch语句块中有return,在中间就返回了,那么finally是否还会执行呢?
会。流程会跳到finally然后回到return,再return返回
还有一个细节:
try一定要配合catch或finally(注意是“或”)
如果是try和finally配合而没有catch,则异常必须要先声明(此时是在ducking);否则可以选择性地声明
④ 上面的代码中,捕捉的是Exception类,这当然可以(依据是异常也是多态的);但并不推荐,因为会区分不开是哪种具体的异常。
因此最好为每个需要处理的异常编写不同的catch块。
★ 极其需要注意:当有多个catch块时,第一个(最上边)catch的异常一定是范围最小的,也就是说,要从小排到大
因为JVM会按顺序取尝试捕捉异常,当上面的异常处理范围更大,下面的小的异常就没有机会被用到。因此编译器直接让这种写法无法通过。
⑤ 对于某块有风险的代码可能引发的异常,你可以catch到,也可以duck掉
ducking就是在“踢皮球”:抛出异常后,位于栈顶的该方法立即被弹出,异常传递给栈上的下一个方法(也就是调用方)
duck的实现也很简单,无非是有throws声明,但不写catch:
void foo() throws BadException { // 声明了throws
doSomething(); // 仅仅是声明而已,什么都不做
}
⑥ 这个异常如果一直未被处理,则会被一直duck到main();
甚至,main()也可以duck掉该异常,但这样的话JVM只能死给你看。
【序列化】
① 存储状态的两种选择:
纯文本存储:给其他非java程序使用
序列化存储:给自己写的java程序使用
② 首先明确,对象有状态(实例变量)和行为(方法)
而序列化的目的就是:状态保持
序列化和解序列化是对对象的操作而不是对类的操作;具体的讲,是对对象的状态(实例变量)的操作;再具体些,是把每项变量的值写到特定格式的文件中
可以理解为:
序列化是把对象碾平、脱水、保存;解序列化是把对象展开、泡开、恢复
③ 序列化图解:
④
★ 将对象序列化存储,写入文件
try {
FileOutputStream fileStream = new FileOutputStream("myGame.var"); // 文件不存在则自动创建
ObjectOutputStream os = new ObjectOutputStream(fileStream);
os.writeObject(null);
os.close(); // fileStream跟着关闭
} catch (Exception e) {
e.printStackTrace();
}
★ 解序列化,还原取出对象
try {
FileInputStream fileStream = new FileInputStream("myGame.var");
ObjectInputStream os = new ObjectInputStream(fileStream);
Object one = os.readObject(); // 读出的是Object类型;读取顺序与写入顺序相同
CameClass someone = (GameClass)one; // 类型转换
os.close();
} catch (Exception e) {
e.printStackTrace();
}
⑤ 串流(stream)要两两连接才能真发挥作用,因为每个串流(类)只需要做好一件事情;
这样就可以通过不同的组合达到最大的适应性
⑥ 对象版图上的所有东西都被序列化
也就是说,当某个对象被序列化时,不仅被该对象引用的实例变量被序列化,引用的对象也被序列化。并且这些都是自动进行的!
⑦ 想要可以被序列化,必须实现Serializable接口
⑧ 序列化是全有或全无的。整个对象版图中,如果某个对象不能被序列化(未实现Serializable接口),则全部失败。
⑨ 如果不想让某个实例变量被序列化,就把它标记为 transient(瞬时)的
如果这样做,当对象被带回来时,该实例变量为被初始化为默认值
⑩ 解序列化比序列化要复杂一些,需要深入理解:
对象从stream中读出来 →
Java虚拟机通过存储的信息判断出对象的class类型 →
Java虚拟机尝试寻找和加载该类 →
新的对象被放在堆上
注意,整个过程中,构造函数没有别执行!这也很好理解,显然调用构造函数会将对象的状态变为默认值,而我们的目的是对象回到存储时的状态
⑫ JVM是根据什么取寻找对象的类的呢?
每个版本的类会有一个唯一的版本ID,它实例化出的类会盖上这个戳记。
在序列化时,被存储的对象会有一个唯一的serialVersionUID;一些对类的修改会改变类的版本号,id不匹配导致还原失败。
至于哪些修改会导致版本ID的改变,见原书P460
我们可以直接把该ID放在类中,让类在演化过程中维持相同的版本ID :
public class Dog {
static final long serialVersionUID = -6849794470754667710L;
// ...
}
⑪ 静态变量是类层面的,显然不会被序列化
【文件的操作和读写】
① 操作文件(不包括读写)用到的类:java.io.File
② File类实例化出的对象可以理解为文件的路径,而不是文件本身
( ↑ 这里说的“文件”包括“目录”)
③ 文件操作
// 创建新的目录(就是文件夹)(mkdir/mkdirs)
File f0 = new File("F://loli"); // 先创建目录"loli"
f0.mkdir();
File f1 = new File("F://loli//A"); // 直接创建"F://loli//A"是会失败的;
f1.mkdir();
File f2 = new File("F://loli//B");
f2.mkdir();
// 列出目录下的内容
File f = new File("F://loli");
if(f.isDirectory()) {
String[] dirContents = f.list();
for(String each : dirContents) {
System.out.println(each);
}
}
// 取得绝对路径
System.out.println(f.getAbsolutePath());
// 删除目录
f1.delete(); // 先删除子目录
f2.delete();
f.delete(); // 否则根目录删除失败
④ 文件读写
try {
File f = new File("F://FBI.txt"); // File对象
FileReader fileReader = new FileReader(f); // FileReader对象
BufferedReader reader = new BufferedReader(fileReader); // BufferedReader对象
// BufferedReader reader = new BufferedReader(new FileReader(new File("F://FBI.txt")));
// ↑ 一步到位,毕竟我们只用到了BufferedReader对象
String line = null;
while((line = reader.readLine()) != null) { // 利用readLine进行逐行读取
System.out.println(line);
}
reader.close(); // 一定要记住关闭文件!!!
} catch(Exception e) {
e.printStackTrace();
}
try {
BufferedWriter writer = new BufferedWriter(new FileWriter(new File("F://FBI.txt")));
// ↑ FileWriter第二个参数为true,则以追加形式写入
writer.write("Loli sukisuki!!!");
writer.close();
} catch(Exception e) {
e.printStackTrace();
}
⑤ BufferedReader的实现机制
实际上,FileReader就可以直接进行读操作,但为了效率(这涉及到解码器的解码次数),
我们使用了BufferedReader类:
它的肚子里有一个长度为8192的char[]数组,每次实际上都往数组中多读了一些,而它的read方法仅仅是读取数组而已
这个8192长度的char[]数组,就是读操作的缓冲区
⑥ BufferedWriter的实现机制
实际上,FileWriter对象直接就可以调用write方法写入;如果这样,每次write时会立即写进去
“写入”这个磁盘操作的成本很高,远远高于了本身的内存成本
因此我们利用了缓冲区
BufferedWriter可以暂存一堆数据,到了满时才一并写入磁盘;这大大减少了实际磁盘操作的次数,是很经济的;
⑦ 如果想强制缓冲区立即进行磁盘写入,只要调用flush()方法
注意哦,读操作没有flush方法,每次char[]数组为空时,会自动读取,没有通过某个方法直接读满这种说法
☀ 《Head First Java》Kathy Sierra & Bert Bates