小记:
Reader是带编码转换器的InputStream,它把byte转换为char,
Writer就是带编码转换器的OutputStream,它把char转换为byte并输出
InputStream 字节输入流的超类(抽象) | Reader 字符输入流的超类(抽象) |
---|---|
字节流,以byte为单位 | 字符流,以char为单位 |
读取字节(-1,0~255):int read() | 读取字符(-1,0~65535):int read() |
读到字节数组:int read(byte[] b) | 读到字符数组:int read(char[] c) |
OutputStream | Writer |
---|---|
字节流,以byte为单位 | 字符流,以char为单位 |
写入字节(0~255):void write(int b) | 写入字符(0~65535):void write(int c) |
写入字节数组:void write(byte[] b) | 写入字符数组:void write(char[] c) |
无对应方法 | 写入String:void write(String s) |
Reader和InputStream有什么关系?
除了特殊的CharArrayReader和StringReader,普通的Reader实际上是基于InputStream构造的,因为Reader需
要从InputStream中读入字节流(byte),然后,根据编码设置,再转换为char就可以实现字符流。如果我们
查看FileReader的源码,它在内部实际上持有一个FileInputStream。
既然Reader本质上是一个基于InputStream的byte到char的转换器,
那么,如果我们已经有一个InputStream,想把它转换为Reader,是完全可行的。
InputStreamReader就是这样一个转换器,
它可以把任何InputStream转换为Reader。示例代码如下:
使用InputStreamReader,可以把一个InputStream转换成一个Reader。
// 持有InputStream:
InputStream input = new FileInputStream("src/readme.txt");
// 变换为Reader:
Reader reader = new InputStreamReader(input, "UTF-8");
//上述代码可以通过try (resource)更简洁地改写如下
try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), "UTF-8")) {
// TODO:
}
IO是指Input/Output,即输入和输出。以内存为中心:
- Input指从外部读入数据到内存,例如,把文件从磁盘读取到内存,从网络读取数据到内存等等。
- Output指把数据从内存输出到外部,例如,把数据从内存写入到文件,把数据从内存输出到网络等等。
为什么要把数据读到内存才能处理这些数据?因为代码是在内存中运行的,数据也必须读到内存,最终的表示方式无非是byte数组,字符串等,都必须存放在内存里。
从Java代码来看,输入实际上就是从外部,例如,硬盘上的某个文件,把内容读到内存,并且以Java提供的某种数据类型表示,例如,byte[],String,这样,后续代码才能处理这些数据。
因为内存有“易失性”的特点,所以必须把处理后的数据以某种方式输出,例如,写入到文件。Output实际上就是把Java表示的数据格式,例如,byte[],String等输出到某个地方。
IO流是一种顺序读写数据的模式,它的特点是单向流动。数据类似自来水一样在水管中流动,所以我们把它称为IO流。
File对象
File对象既可以表示文件,也可以表示目录。特别要注意的是,构造一个File对象,即使传入的文件或目录不存在,代码也不会出错,因为构造一个File对象,并不会导致任何磁盘操作。只有当我们调用File对象的某些方法的时候,才真正进行磁盘操作。
调用isFile(),判断该File对象是否是一个已存在的文件,
调用isDirectory(),判断该File对象是否是一个已存在的目录:
用File对象获取到一个文件时,还可以进一步判断文件的权限和大小:
- boolean canRead():是否可读;
- boolean canWrite():是否可写;
- boolean canExecute():是否可执行;
- long length():文件字节大小。
创建和删除文件
当File对象表示一个文件时,可以通过createNewFile()创建一个新文件,用delete()删除该文件:
File file = new File("/path/to/file");
if (file.createNewFile()) {
// 文件创建成功:
// TODO:
if (file.delete()) {
// 删除文件成功:
}
}
创建临时文件
有些时候,程序需要读写一些临时文件,File对象提供了createTempFile()来创建一个临时文件,以及deleteOnExit()在JVM退出时自动删除该文件。
public class Main {
public static void main(String[] args) throws IOException {
File f = File.createTempFile("tmp-", ".txt"); // 提供临时文件的前缀和后缀
f.deleteOnExit(); // JVM退出时自动删除
System.out.println(f.isFile());
System.out.println(f.getAbsolutePath());
}
}
遍历文件和目录
当File对象表示一个目录时,可以使用list()和listFiles()列出目录下的文件和子目录名。
list() 返回一个字符串数组,命名这个抽象路径名所表示的目录中的文件和目录。如果目录为空,则数组为空。如果这个抽象路径名不表示目录,或者发生I/O错误,则返回null。
listFiles()提供了一系列重载方法,可以过滤不想要的文件和目录:
public class Main {
public static void main(String[] args) throws IOException {
File f = new File("C:\\Windows");
File[] fs1 = f.listFiles(); // 列出所有文件和子目录
printFiles(fs1);
File[] fs2 = f.listFiles(new FilenameFilter() { // 仅列出.exe文件
public boolean accept(File dir, String name) {
return name.endsWith(".exe"); // 返回true表示接受该文件
}
});
printFiles(fs2);
}
static void printFiles(File[] files) {
System.out.println("==========");
if (files != null) {
for (File f : files) {
System.out.println(f);
}
}
System.out.println("==========");
}
}
Path
Java标准库还提供了一个Path对象,它位于java.nio.file包。Path对象和File对象类似,但操作更加简单:
public class Main {
public static void main(String[] args) throws IOException {
Path p1 = Paths.get(".", "project", "study"); // 构造一个Path对象
System.out.println(p1);
Path p2 = p1.toAbsolutePath(); // 转换为绝对路径
System.out.println(p2);
Path p3 = p2.normalize(); // 转换为规范路径
System.out.println(p3);
File f = p3.toFile(); // 转换为File对象
System.out.println(f);
for (Path p : Paths.get("..").toAbsolutePath()) { // 可以直接遍历Path
System.out.println(" " + p);
}
}
}
InputStream / OutputStream
小结:read :文件读入内存 write :内存写到文件
InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。
这个抽象类定义的一个最重要的方法就是int read()
OutputStream也是抽象类,它是所有输出流的超类。
这个抽象类定义的一个最重要的方法就是void write(int b)
IO流以byte(字节)为最小单位,因此也称为字节流。例如,我们要从磁盘读入一个文件,包含6个字节,就相当于读入了6个字节的数据:
这6个字节是按顺序读入的,所以是输入字节流。
反过来,我们把6个字节从内存写入磁盘文件,就是输出字节流:
在Java中,InputStream代表输入字节流,OuputStream代表输出字节流,这是最基本的两种IO流。
缓冲
在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。InputStream提供了两个重载方法来支持读取多个字节:
- int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
- int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数
利用上述方法一次读取多个字节时,需要先定义一个byte[]数组作为缓冲区,read()方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。read()方法的返回值不再是字节的int值,而是返回实际读取了多少个字节。如果返回-1,表示没有更多的数据了。
利用缓冲区一次读取多个字节的代码如下:
public void readFile() throws IOException {
try (InputStream input = new FileInputStream("src/readme.txt")) {
// 定义1000个字节大小的缓冲区:
byte[] buffer = new byte[1000];
int n;
while ((n = input.read(buffer)) != -1) { // 读取到缓冲区
System.out.println("read " + n + " bytes.");
}
}
}
ByteArrayInputStream
用FileInputStream可以从文件获取输入流,这是InputStream常用的一个实现类。
ByteArrayInputStream可以在内存中模拟一个InputStream:
public class Main {
public static void main(String[] args) throws IOException {
byte[] data = { 72, 101, 108, 108, 111, 33 };
try (InputStream input = new ByteArrayInputStream(data)) {
int n;
while ((n = input.read()) != -1) {
System.out.println((char)n);
}
}
}
}
Reader / Writer
如果我们需要读写的是字符,并且字符不全是单字节表示的ASCII字符,那么,按照char来读写显然更方便,这种流称为字符流。
Java提供了Reader和Writer表示字符流,字符流传输的最小数据单位是char。
Reader和Writer本质上是一个能自动编解码的InputStream和OutputStream。
使用Reader,数据源虽然是字节,但我们读入的数据都是char类型的字符,原因是Reader内部把读入的byte做了解码,转换成了char。使用InputStream,我们读入的数据和原始二进制数据一模一样,是byte[]数组,但是我们可以自己把二进制byte[]数组按照某种编码转换为字符串。究竟使用Reader还是InputStream,要取决于具体的使用场景。如果数据源不是文本,就只能使用InputStream,如果数据源是文本,使用Reader更方便一些。Writer和OutputStream是类似的。
PrintStream和PrintWriter
PrintStream是一种能接收各种数据类型的输出,打印数据时比较方便:
- System.out是标准输出;
- System.err是标准错误输出。
PrintWriter是基于Writer的输出,它的print()/println()方法最终输出的是char数据
PrintStream是一种FilterOutputStream,它在OutputStream的接口上,
额外提供了一些写入各种数据类型的方法:
- 写入int:print(int)
- 写入boolean:print(boolean)
- 写入String:print(String)
- 写入Object:print(Object),实际上相当于print(object.toString())
PrintStream和OutputStream相比,除了添加了一组print()/println()方法,可以打印各种数据类型,比较方便外,它还有一个额外的优点,就是不会抛出IOException,这样我们在编写代码的时候,就不必捕获IOException。
System.err是系统默认提供的标准错误输出。
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());
}
}
序列化
序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。
为什么要把Java对象序列化呢?
因为序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程,
这样,就相当于把Java对象存储到文件或者通过网络传输出去了。
有序列化,就有反序列化,即把一个二进制内容(也就是byte[]数组)变回Java对象。
有了反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,
或者从网络上读取byte[]并把它“变回”Java对象。
一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口
Serializable接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。
public interface Serializable {}
序列化:
把一个Java对象变为byte[]数组,需要使用ObjectOutputStream。它负责把一个Java对象写入一个字节流:
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
int i = 12345;
String s = "Hello";
//序列化
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
output.writeObject(i);
output.writeUTF(s);
output.writeObject(Double.valueOf(123.456));
}
byte[] a = buffer.toByteArray();
System.out.println(Arrays.toString(a));
System.out.println("=======================");
//反序列化
ByteArrayInputStream bu = new ByteArrayInputStream(a);
try (ObjectInputStream in = new ObjectInputStream(bu)) {
int i1 = (int) in.readObject(); //
String s1 = (String) in.readUTF();
Double d1 = (Double) in.readObject();
System.out.println(i1 + ", " + s1 + ", " + d1);
}
}
}
新i/o
JDK1.4中的java.nio.* 引入了新的JavaI/O 类库, 新IO提高了速度
速度的提高来自于所使用的结构更接近于于操作系统执行I/O的方式:通道和缓冲器.
通道是数据的来源和去处,信道是去来往信道的载具.
通道要么从缓冲器获得数据,要么向缓冲器发送数据.
唯一直接与通道交互的缓冲器是ByteBuffer——即可以存储未加工字节的缓冲器
通过告知分配多少存储空间来创建一个ByteBuffer对象,并且还有一个方法选择集,用于以原始的字节形式或基本数据类型输出和读取数据. 但是没办法输出或读取对象,即使是字符串对象也不行.
channel
旧I/O类库又三个类被修改了,用以产生FileChannel.分别是FileInputStream,FileOutStream,以及用于既读又写的RandomAccessFile.这些都是字节操纵流,与低层的nio性质一致.
Reader和Writer这种字符模式类不能用于产生通道,但是java.nio.channels.Channels类提供了实用方法,用以在通道中产生Reader和Writer.
public class GetChannel {
// private static final int BSIZE = 1024;
public static void main(String[] args) throws Exception {
// 写一个文件: 并获取 getChannel() :返回与此文件输出流关联的唯一FileChannel对象 fc
FileChannel fc = new FileOutputStream("data.txt").getChannel();
// ByteBuffer.wrap(byte[] array) : 将字节数组封装到缓冲区中
fc.write(ByteBuffer.wrap("Some text ".getBytes()));
fc.close();
// 添加到文件末尾:
fc = new RandomAccessFile("data.txt", "rw").getChannel();
fc.position(fc.size()); // 将位置设置为末尾
fc.write(ByteBuffer.wrap("Some more".getBytes()));
fc.close();
// 读文件:
fc = new FileInputStream("data.txt").getChannel();
//分配一个新的字节缓冲区 ByteBuffer allocate(int capacity) capacity -新缓冲区的容量,以字节为单位
//对于只读操作,我们必须显示的用静态的allocate()方法来分配ByteBuffer
ByteBuffer buff = ByteBuffer.allocate(1024);
fc.read(buff);
buff.flip();//一旦调用read()来告知FileChannel 向 ByteBuffer 存储字节,
//就必须调用缓冲器上的flip(),让它做好别人读取字节的准备
//while (buff.hasRemaining()) {
//相对的get方法。读取位于该缓冲区当前位置的字节,然后将该位置加1。
//System.out.print((char) buff.get());
System.out.println(buff.asCharBuffer());
}
}
public class ChannelCopy {
//一个简单的文件复制程序
private static final int BSIZE = 1024;
public static void main(String[] args) throws IOException {
args = new String[]{"data.txt","X.file"};
if (args.length !=2 ){
System.out.println("argument sourcefile destfie");
System.exit(-1);
}
FileChannel
in = new FileInputStream(args[0]).getChannel(),
out = new FileOutputStream(args[1]).getChannel();
//通过缓冲器两个信道直接相连
ByteBuffer buffer = ByteBuffer.allocate(BSIZE);
//把in中的 信息 复制到 out 中
while (in.read(buffer) != -1){
System.out.println("--");
buffer.flip();
out.write(buffer);
buffer.clear();
}
}
}
可以看到,打开一个FileChannel 以用于读 ,而打开另一个用于写。ByteBuffer 被分配了空间
当FileChannel.read()返回-1 时,表示我们已经达到了输入的末尾,每次read()操作后,就会将数据输入到
缓冲器中,flip()作用是翻转这个缓冲区,以便于它的信息可以由write()提取。
write()操作后,信息仍然在缓冲器中,接着clear()操作则对所有的内部指针重新安排,以便缓冲器在另一个read()操作期间能够做好接受数据的准备。
Files:轻松复制、移动、删除或处理文件的工具类,有你需要的所有方法
public class FilesDemo {
public static void main(String[] args) throws IOException {
Path path = Paths.get("D:/work/zk/opensource/javase/ThinkingInJava/bak.txt");
//Path file = Files.createFile(path);
//System.out.println(file);
Files.delete(path);
//记得在main()方法里 调用方法哦---
}
public static void copy() throws IOException {
Path source = Paths.get("D:/work/zk/opensource/javase/ThinkingInJava/bak.txt");
Path target = Paths.get("D:/demo.txt");
//copy 可以设置选项 move方法类似
Files.copy(source,target);
}
public static void read(){
Path file = Paths.get("D:/work/zk/opensource/javase/ThinkingInJava/bak.txt");
try(BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)){
String line;
while ((line = reader.readLine())!=null){
System.out.println(line);
}
}catch (IOException e){
}
}
public static void write(){
Path file = Paths.get("D:/work/zk/opensource/javase/ThinkingInJava/bak.txt");
try(BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8, StandardOpenOption.WRITE)){
//从头写
//writer.write("hello world");
writer.append("hello world");
}catch (IOException e){
}
}
//简化读写
public static void simpleRead() throws IOException{
Path file = Paths.get("D:/work/zk/opensource/javase/ThinkingInJava/bak.txt");
List<String> allLines = Files.readAllLines(file, StandardCharsets.UTF_8);
for (String line : allLines) {
System.out.println(line);
}
}
public static void simpleRead2() throws IOException{
Path file = Paths.get("D:/work/zk/opensource/javase/ThinkingInJava/bak.txt");
byte[] bytes = Files.readAllBytes(file);
System.out.println(new String(bytes));
}
}
指定一个接口,获取其实现类
指定一个接口FooInter,以及一个包(包中包含各种类以及FooInter的实现【不限个数】),如何获取该接口或抽象类的实现类?
public class TestFoo {
public static void main(String[] args) throws IOException {
Path path = Paths.get("src/com/java/chapter18/FooDemo");
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String replace = file.toString().replace('\\', '.');
String dir = replace.substring(0,replace.length()-5).substring(4);
// System.out.println(dir);
try {
//Class.forName(“类的全路径”) 类的全路径=包+类名 本例是从com.java包下开始的
//之所以length-5 是因为类名不带后面的 .java
if(FooInter.class.isAssignableFrom(Class.forName(dir))){
System.out.println(file.getFileName());
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return FileVisitResult.CONTINUE;
}
});
/* URL url = TestFoo.class.getClassLoader().getResource("com/java/chapter1");
System.out.println(url.toString());
Path path1 = Paths.get(url.toString());*/
}
}