输出流 OutputStream
OutputStream是Java标准库提供的最基本的输出流。
和InputStream类似,OutputStream也是抽象类,它是所有输出流的超类。这个抽象类定义的一个最重要的方法就是void write(int b)
public abstract void write(int b) throws IOException;
这个方法会写入一个字节到输出流。要注意的是,虽然传入的是int参数,但只会写入一个字节,即只写入int最低8位表示字节的部分
复制一个文件
通过InputStream读取数据,通过OutputStream写入数据
public static void copy(String from, String to) throws IOException {
File f = new File(from);
File t = new File(to);
if (!f.exists()) {
throw new FileNotFoundException();
}
if (!t.exists()) {
try {
t.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
try (InputStream input = new FileInputStream(f);
OutputStream output = new FileOutputStream(t)) {
int fLenght = (int) f.length();
byte[] data = new byte[fLenght];
System.out.println("文件长度为" + fLenght);
int n;
while ((n = input.read(data)) != -1) {
System.out.println("读取数据中...");
}
System.out.println("读取数据成功,cc写入数据中...");
output.write(data);
System.out.println("写入成功");
}
}
Filter模式(或者装饰器模式:Decorator)
当我们需要给一个“基础”InputStream附加各种功能时,我们先确定这个能提供数据源的InputStream,因为我们需要的数据总得来自某个地方,例如,FileInputStream,数据来源自文件:
InputStream file = new FileInputStream("test.gz")
我们希望FileInputStream能提供缓冲的功能来提高读取的效率,因此我们用BufferedInputStream包装这个InputStream,得到的包装类型是BufferedInputStream,但它仍然被视为一个InputStream:
InputStream buffered = new BufferedInputStream(file);
最后,如果这个文件已经被gzip压缩,我们可以封装一个GZIPInputStream
,
InputStream gzip = new GZIPInputStream(buffered);
无论我们封装多少次,得到的对象始终是InputStream,我们直接用InputStream来引用它,就可以正常读取
上述这种通过一个“基础”组件再叠加各种“附加”功能组件的模式,称之为Filter模式(或者装饰器模式:Decorator)
操作Zip
ZipInputStream是一种FilterInputStream,他直接读取zip包的内容。
读取
创建一个 ZipInputStream,循环调用getNextEntry(),直到返回null,表示zip流结束
getNextEntry返回一个zipEntry,一个ZipEntry表示一个压缩文件或目录,如果是压缩文件,我们就用read()方法不断读取,直到返回-1
try (ZipInputStream zip = new ZipInputStream(new FileInputStream(...))) {
ZipEntry entry = null;
while ((entry = zip.getNextEntry()) != null) {
String name = entry.getName();
if (!entry.isDirectory()) {
int n;
while ((n = zip.read()) != -1) {
...
}
}
}
}
写入zip包
try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(...))) {
File[] files = ...
for (File file : files) {
zip.putNextEntry(new ZipEntry(file.getName()));
zip.write(Files.readAllBytes(file.toPath()));
zip.closeEntry();
}
}
序列化
序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。
为什么需要序列化,因为通过byte[]可以保存到文件,也可以通过网络发送出去。
顾名思义反序列化就是将byte[]数组转位java对象。
java对象序列化
要实现序列化,就要实现java.io.Serializable接口,而Serializable接口没有定义任何方法和属性,
public interface Serializable {
}
它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。
借助ObjectOutputStream,他负责将数据对象写入字节流
public class Main {
public static void main(String[] args) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
// 写入int:
output.writeInt(12345);
// 写入String:
output.writeUTF("Hello");
// 写入Object:
output.writeObject(Double.valueOf(123.456));
}
System.out.println(Arrays.toString(buffer.toByteArray()));
}
}
打印的buffer是一个byte数组
反序列化
try (ObjectInputStream input = new ObjectInputStream(...)) {
int n = input.readInt();
String s = input.readUTF();
Double d = (Double) input.readObject();
}
反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行。
字符流 Reader
Reader是IO提供的另一个输入流接口,跟InputStream区别是,InputStream是以字节流为准,单位是byte,而Reader是一个字符流,单位是char。
这个方法读取字符流的下一个字符,并返回字符表示的int(也就是可以用char来转换。),范围是0~65535(char表示范围,像InputStream的byte只能是0-255)。如果已读到末尾,返回-1。
FileReader
FileReader是Reader的子类,类似于FileInputStream于InputStream的关系。
// Java8不支持UTF_8
try (Reader reader = new FileReader("./test.txt", StandardCharsets.UTF_8)) {
int n;
while ((n = reader.read()) != -1) {
System.out.println((char) n);
}
}
也支持缓冲区读取
try (Reader reader = new FileReader("./test.txt")) {
char[] buffer = new char[5];
int n;
// n返回读取的char数
while ((n = reader.read(buffer)) != -1) {
System.out.println(buffer);
}
}
打印结果
hello
worl
d.中文l
CharArrayReader
CharArrayReader可以在内存中模拟一个Reader,它的作用实际上是把一个char[]数组变成一个Reader,这和ByteArrayInputStream非常类似:
try (Reader reader = new CharArrayReader("Hello".toCharArray())) {
}
StringReader
直接把String作为数据源。
try (Reader reader = new StringReader("Hello")) {
}
Reader和InputStream
普通的Reader实际上是基于InputStream构造的,因为Reader需要从InputStream中读入字节流(byte),然后,根据编码设置,再转换为char就可以实现字符流。如果我们查看FileReader的源码,它在内部实际上持有一个FileInputStream;
InputStreamReader可以把任务InputStream转位Reader
// 持有InputStream:
InputStream input = new FileInputStream("src/readme.txt");
// 变换为Reader:
Reader reader = new InputStreamReader(input, "UTF-8");
Writer
Reader是带编码转换器的InputStream,它把byte转换为char,而Writer就是带编码转换器的OutputStream,它把char转换为byte并输出(把char转位byte然后写进去)。
Writer是所有字符输出流的超类,它提供的方法主要有:
- 写入一个字符(0~65535):void write(int c);
- 写入字符数组的所有字符:void write(char[] c);
- 写入String表示的所有字符:void write(String s)。
FileWirter
跟FileReader类似
try (Reader reader = new FileReader("./test.txt"); Writer writer = new FileWriter("./test2.txt")) {
char[] buffer = new char[5];
int n;
while ((n = reader.read(buffer)) != -1) {
System.out.println("正在写入数据" + buffer);
writer.write(buffer);
}
System.out.println("写入成功");
}
CharArrayWriter
CharArrayWriter可以在内存中创建一个Writer,它的作用实际上是构造一个缓冲区,可以写入char,最后得到写入的char[]数组,这和ByteArrayOutputStream非常类似:
try (CharArrayWriter writer = new CharArrayWriter()) {
writer.write(65);
writer.write(66);
writer.write(67);
char[] data = writer.toCharArray(); // { 'A', 'B', 'C' }
}
内存中创建一个Writer,模拟写入。
StringWriter
StringWriter也是一个基于内存的Writer,它和CharArrayWriter类似。实际上,StringWriter在内部维护了一个StringBuffer,并对外提供了Writer接口。
OutputStreamWriter
与InputStreamReader相似。OutputStreamWriter可以讲OutputStream转位Writer
try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) {
// TODO:
}
小结
- inputStream用户读取文件的字节流,单位是byte,read方法返回字节的int表示(0-255)
- Reader基于InputStream封装,他是以char为单位的字符流,read方法返回char的int表示,可以用(char) n来转换返回的数据。
- outputStream用户用来写入的字节流,单位是byte。
- Writer是基于outputStream封装的字符流,单位是char。
- InputStream: FileInputStream. ByteArrayInputStream
- outStream: FIleOutputStream byteArrayOutputStream
- Reader: FIleReader CharByteReader StringReader InputStreamReader(InputStream->Reader)
- Writer: FileWriter charByteWriter StringWriter OutputStreamWriter(OutputStream->Writer)
PrintStream和PrintWriter
PrintStream是一种FilterOutputStream,它在OutputStream的接口上,额外提供了一些写入各种数据类型的方法:
- 写入int:print(int)
- 写入boolean:print(boolean)
- 写入String:print(String)
- 写入Object:print(Object),实际上相当于print(object.toString())
…
以及对应的一组println()方法,它会自动加上换行符。
看着很像System.out.xxx
事实上System.out.println()
实际上就是使用PrintStream
打印各种数据。其中,System.out
是系统默认提供的PrintStream
PrintStream和OutputStream相比,除了添加了一组print()/println()方法,可以打印各种数据类型,比较方便外,它还有一个额外的优点,就是不会抛出IOException,这样我们在编写代码的时候,就不必捕获IOException。
有PirntStream就有PrintWriter,
PrintStream最终输出的总是byte数据,而PrintWriter则是扩展了Writer接口,它的print()/println()方法最终输出的是char数据。
public class Main {
public static void main(String[] args) {
StringWriter buffer = new StringWriter();
try (PrintWriter pw = new PrintWriter(buffer)) {
pw.println("Hello");
pw.println(12345);
pw.println(true);
}
System.out.println(buffer.toString());
}
}
小结
PrintStream是一种能接收各种数据类型的输出,打印数据时比较方便:
- System.out是标准输出;
- System.err是标准错误输出。
PrintWriter是基于Writer的输出。
Files
虽然Files是java.nio包里面的类,但他俩封装了很多读写文件的简单方法,极大的方便了我们读写文件。
byte[] data = Files.readAllBytes(Path.of("/path/to/file.txt"));
// 默认使用UTF-8编码读取:
String content1 = Files.readString(Path.of("/path/to/file.txt"));
// 可指定编码:
String content2 = Files.readString(Path.of("/path", "to", "file.txt"), StandardCharsets.ISO_8859_1);
// 按行读取并返回每行内容:
List<String> lines = Files.readAllLines(Path.of("/path/to/file.txt"));
Files.readString是java11后支持的
写入文件
// 写入二进制文件:
byte[] data = ...
Files.write(Path.of("/path/to/file.txt"), data);
// 写入文本并指定编码:
Files.writeString(Path.of("/path/to/file.txt"), "文本内容...", StandardCharsets.ISO_8859_1);
// 按行写入文本:
List<String> lines = ...
Files.write(Path.of("/path/to/file.txt"), lines);
Files工具类还有copy()、delete()、exists()、move()等快捷方法操作文件和目录。
Files提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,不可一次读入几个G的大文件。读写大型文件仍然要使用文件流,每次只读写一部分文件内容。
对于简单的小文件读写操作,可以使用Files工具类简化代码。
线程(补充)
死锁
public void add(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value += m;
synchronized(lockB) { // 获得lockB的锁
this.another += m;
} // 释放lockB的锁
} // 释放lockA的锁
}
public void dec(int m) {
synchronized(lockB) { // 获得lockB的锁
this.another -= m;
synchronized(lockA) { // 获得lockA的锁
this.value -= m;
} // 释放lockA的锁
} // 释放lockB的锁
}
对于上述代码,线程1和线程2如果分别执行add()和dec()方法时:
线程1:进入add(),获得lockA;
线程2:进入dec(),获得lockB。
随后:
线程1:准备获得lockB,失败,等待中;
线程2:准备获得lockA,失败,等待中。
两个线程持有不同的所,然后各自试图获取对方手里的所,造成双方无限等待,这就是死锁。
避免死锁的方法是多线程获取锁的顺序要一致。
使用wait和notify
synchronied解决了多线程竞争的问题,但没解决多线程协调问题。
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
}
public synchronized String getTask() {
while (queue.isEmpty()) {
}
return queue.remove();
}
}
如上,看似getTask通过while循环调用queu.isEmpty判断是否有数据,没有就一直等待,等其他线程调用addTask加入数据后,就能取出数据。
但是,getTask这个方法已经锁了当前的实例,导致其他线程,根本无法调用addTask。解决这个方法就是调用wait
方法
public synchronized String getTask() {
while (queue.isEmpty()) {
this.wait();
}
return queue.remove();
}
调用getTask时候已经获得所,然后调用到wait之后,线程会进入等待状态,直到未来某个时刻,被其他线程唤醒。wait才会返回。
必须在synchronized块中才能调用wait()方法,因为wait()方法调用时,会释放线程获得的锁,wait()方法返回后,线程又会重新试图获得锁。
public synchronized String getTask() {
while (queue.isEmpty()) {
// 释放this锁:
this.wait();
// 重新获取this锁
}
return queue.remove();
}
如何唤醒呢?通过notify/notifyAll
public synchronized void addTask(String s) {
this.queue.add(s);
this.notifyAll(); // 唤醒在this锁等待的线程
}
注意到在往队列中添加了任务后,线程立刻对this锁对象调用notify()方法,这个方法会唤醒一个正在this锁等待的线程(就是在getTask()中位于this.wait()的线程),从而使得等待线程从this.wait()方法返回。
ReentrantLock
XML
XML是可扩展标记语言(eXtensible Markup Language)的缩写,它是一种数据表示格式,可以描述非常复杂的数据结构,常用于传输和存储数据。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE note SYSTEM "book.dtd">
<book id="1">
<name>Java核心技术</name>
<author>Cay S. Horstmann</author>
<isbn lang="CN">1234567</isbn>
<tags>
<tag>Java</tag>
<tag>Network</tag>
</tags>
<pubDate/>
</book>