《疯狂java讲义》读书笔记(六):Annotation(注释)和输入/输出
(一)Annotation(注释)
5个基本的Annotation如下:
- @Override:用来指定方法覆载的,可以强制一个子类必须覆盖父类的方法。不过只能修饰方法。
- @Deprecated:标记已过时,比如说一个类或者元素过时了。
- @SuppressWarings:抑制编译器警告。使用该注解修饰某个类取消显示某个编译器经过,同时又修饰该类里的某个方法取消显示另一个编译器警告,那么该方法将会同时取消显示另一个编译器警告,那么该方法将会同时取消显示这两个编译器警告。
- @SafeVarargs:开发者不希望看见堆污染的警告,可以采用该注解修饰引发该警告的方法或者构造器,抑制堆污染警告还可以使用@SuppressWarings(“unchecked”)修饰。
- @FunctionalInterface:如果接口中只有一个抽象方法,那么该接口就是函数式接口,该注解就是用来指定某个接口必须是函数式接口的。
(二)输入/输出
1.File类
不管是文件还是目录都是用File来操作,File能新建、删除、重命名文件和目录,File不能访问文件内容本身,如果需要访问文件内容本身,则需要使用输入/输出流。在目录操作相关的方法中有一个String[] list()
,用来列出File对象的所有子文件名和路径名,返回String数组,该方法可以接受一个FilenameFilter参数,通过该参数可以只列出符合条件的文件,相当于是个文件过滤器。 FilenameFilter接口也是一个函数式接口,可以用lambda表达式创建实现该接口的对象。
2.理解Java的IO流
从不同角度可以有多种分法。比如说输入流和输出流;字节流和字符流;节点流和处理流。
输入流:只能从中读取数据,而不能向其写入数据。
输出流:只能向其写入数据,而不能从中读取数据。
由InputStream和Reader
作为基类,而输出流则主要由OutputStream和Writer
作为基类,不过都是抽象基类,没法直接创建实例。
字节流和字符流的用法几乎相同,后面如果涉及到一些方法会省略描述,自行查阅API即可。字节流操作的数据单元是8位的字节,而字符流操作的数据单元是16位的字符。字节流主要是由InputStream和OutputStream
作为基类,字符流是Reader和Writer
作为基类。
节点流是指可以从一个特定的IO设备读/写数据的流,处理流用于对一个已存在的流进行连接或封装,通过封装后的流来实现数据读/写功能。
3.对象序列化
对象序列化的目标是将对象保存到磁盘里,或者允许在网络中直接传输对象,对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,通过网络将这种二进制流传输到另一个网络节点。其它程序一旦获得了这种二进制流,都可以将这种二进制流恢复成原来的Java对象。也就是说对象的序列化时将一个Java对象写入IO流,反序列化是从IO中恢复该Java对象。
为了让某个类可序列化,那必须得实现Seralizable或者Externalizable
接口之一。不过Java的很多类都已经是实现了Serializable,该接口是一个标记接口,实现该接口无须实现任何方法,它只是表明这个类的实例是可序列化的。
下面演示以下序列化和反序列化的过程。
//新建一个Person类实现Serializble接口
public class Person implements Serializable {
private String name;
private int age;
public Person(String name,int age){
System.out.println("有参构造器");
this.name=name;
this.age=age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
下面的程序是将一个Person对象写入磁盘文件,运行之后会发现生成了一个object.txt,文件内容就是 Person对象。
//序列化
public class WriteObject {
public static void main(String[] args) {
try {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("object.txt"));
Person per=new Person("王二小",15);
oos.writeObject(per);
} catch (IOException e) {
e.printStackTrace();
}
}
}
readObject()方法是用来读取流中的对象,运行的时候我们可以发现程序没有调用构造器,这说明反序列化机制不需要通过构造器来初始化Java对象。
//反序列化
public class ReadObject {
public static void main(String[] args) {
try {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("object.txt"));
Person p=(Person)ois.readObject();
System.out.println("name:"+p.getName()+" age:"+p.getAge());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
//运行结果
//name:王二小 age:15
如果Person类中出现了一个引用类型,那么序列化会怎么样的。毋庸置疑,这个引用类型必须是可序列化的才可以,否则这个Person类不能序列化。
下面再举一个例子,新建一个Teacher类。注意该类持有一个Student对象的引用,这里不新建Student类了,实际上和Person类是一样的,除了名字不一样。
public class Teacher implements Serializable {
private String name;
private Student student;
public Teacher(String name,Student student){
this.name=name;
this.student=student;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Student getStudent() {
return student;
}
public void setStudent(Student student) {
this.student = student;
}
}
如果我想实现下面这三行代码,那么想一想,如果先序列化t1,那么系统会把t1和Student对象一起序列化, 如果在序列化t2,Student对象会在序列化一遍,如果程序在显示序列化stu,那么又得在序列化亿遍Student对象,貌似会产生3个Student对象。
Student stu=new Student("孙悟空",500);
Teacher t1=new Teacher("唐僧",stu);
Teacher t2=new Teacher("菩提祖师",stu);
如果再反序列化的时候,那么还是会得到三个Student对象,就会导致t1和t2所引用的Student对象不是一个,显然这就错了。所以Java序列化机制有一种特殊的序列化算法:、
所有保存到磁盘的对象都有一个序列化编号。
当程序试图序列化一个对象的时候,会先检车该对象是否被序列化过,从未被序列化过,就会将该对象转换成字节序列然后输出。
如果某个对象序列化过,将只是直接输出一个系列化编号,而不是再次重新序列化该对象。
也就是只有第一次调用writeObject()方法的时候才会将对象转换成字节序列并输出。
下面来序列化看看。实际上只序列化了三个对象,t1、t2和stu。
public class WriteTeacher {
public static void main(String[] args) {
try {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("teacher.txt"));
Student stu=new Student("孙悟空",500);
Teacher t1=new Teacher("唐僧",stu);
Teacher t2=new Teacher("菩提祖师",stu);
oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(stu);
oos.writeObject(t2);
} catch (IOException e) {
e.printStackTrace();
}
}
}
反序列化验证。
public class WriteTeacher {
public static void main(String[] args) {
try {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("teacher.txt"));
Teacher t1=(Teacher)ois.readObject();
Teacher t2=(Teacher)ois.readObject();
Student stu=(Student)ois.readObject();
Teacher t3=(Teacher)ois.readObject();
System.out.println("t1的student引用和stu相同嘛:"+(t1.getStudent()==stu));
System.out.println("t2的student引用和stu相同嘛:"+(t2.getStudent()==stu));
System.out.println("t2和t3是否是同一个对象:"+(t2==t3));
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**输出结果
* t1的student引用和stu相同嘛:true
* t2的student引用和stu相同嘛:true
* t2和t3是否是同一个对象:true
*/
多次序列化同一个Java对象,只有第一次序列化时才会把该Java对象转换成字节序列并输出,这样可能引起一个潜在的问题,就是当程序序列化一个可变对象的时候,只有第一次使用writeObject()方法输出时才会将该对象转换成字节序列并输出,当程序再次调用writeObject()方法,程序只是输出序列化编号,即使后面该对象的实例变量值改变了,改变的值也不会被输出。
这个输出结果应该可以说明问题了,输出还是王二小,而不是李四。
public class Mutable implements Serializable {
public static void main(String[] args) {
try {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("mutable.txt"));
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("mutable.txt"));
Person per=new Person("王二小",15);
oos.writeObject(per);
per.setName("李四");
oos.writeObject(per);
Person p1=(Person)ois.readObject();
Person p2=(Person)ois.readObject();
System.out.println(p1==p2);
System.out.println(p2.getName());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
/*
输出结果
有参构造器
true
王二小
*/