JavaSE-04-IO流[下](详细版)

在IO流基础部分,主要学习了FileXX的文件字节流/字符流 ,可以简单进行文件的读写操作,但是能否更高效读写,或者完成其他一些高级功能呢?答案是可以的,在基础流的基础上建立包装即可。

IO流基础部分,请戳:JavaSE-04-IO流[上](详细版)

本文介绍IO流的进阶部分,学习更强大的流。比如能够高效读写的缓冲流,能够转换编码的转换流,能够持久化存储对象的序列化流等等。这些功能更为强大的流,都是在基本的流对象基础之上创建而来的,就像穿上铠甲的武士一样,相当于是对基本流对象的一种增强。

一、缓冲流

缓冲流,也叫高效流,是对4个基本的FileXxx 流的增强,所以也是4个流,按照数据类型分类:

  • 字节缓冲流BufferedInputStreamBufferedOutputStream
  • 字符缓冲流BufferedReaderBufferedWriter

缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。

字节缓冲流

构造方法

  • public BufferedInputStream(InputStream in) :创建一个 新的缓冲输入流。
  • public BufferedOutputStream(OutputStream out): 创建一个新的缓冲输出流。

构造举例,代码如下:

// 创建字节缓冲输入流 BufferedInputStream bis = new BufferedInputStream ( new FileInputStream ( "bis.txt" )); // 创建字节缓冲输出流 BufferedOutputStream bos = new BufferedOutputStream ( new FileOutputStream ( "bos.txt" ));

效率测试

查询API,缓冲流读写方法与基本的流是一致的,我们通过复制大文件w1.bin(4GB),测试它的效率。
1、基本流+单字节

public static void test_copy_speed () { String src = "D:\\01-io-test\\w1.bin" ; String dest = "D:\\01-io-test\\w1-copy.bin" ; long start = System.currentTimeMillis(); try ( FileInputStream is = new FileInputStream (src); FileOutputStream fos = new FileOutputStream (dest);){ int len; while ((len = is.read()) >= 0 ) { fos.write(len); } System.out.println( "传统+单字节,耗时:" +(System.currentTimeMillis()-start)+ "ms" ); } catch (Exception e) { System.out.printf( "拷贝文件异常:" +e.getMessage()); e.printStackTrace(); } } // 耗时20分钟+,直接关了程序;单个字节的读取,非常慢

2、缓冲流+单字节

public static void test_copy_speed_buffer () { String src = "D:\\01-io-test\\w1.bin" ; String dest = "D:\\01-io-test\\w2-copy.bin" ; long start = System.currentTimeMillis(); try ( BufferedInputStream is = new BufferedInputStream ( new FileInputStream (src)); BufferedOutputStream fos = new BufferedOutputStream ( new FileOutputStream (dest));){ int len; while ((len = is.read()) >= 0 ) { fos.write(len); } System.out.println( "缓冲流+单字节,耗时:" +(System.currentTimeMillis()-start)+ "ms" ); } catch (Exception e) { System.out.printf( "拷贝文件异常:" +e.getMessage()); e.printStackTrace(); } } // 输出,4分钟不到 缓冲流+单字节,耗时:205811ms

3、基本流+字节数组

public static void test_copy_speed () { String src = "D:\\01-io-test\\w1.bin" ; String dest = "D:\\01-io-test\\w1-copy.bin" ; long start = System.currentTimeMillis(); try ( FileInputStream is = new FileInputStream (src); FileOutputStream fos = new FileOutputStream (dest);){ byte [] cache = new byte [ 1024 ]; int len; while ((len = is.read(cache)) >= 0 ) { fos.write(cache, 0 ,len); } System.out.println( "传统+字节数组,耗时:" +(System.currentTimeMillis()-start)+ "ms" ); } catch (Exception e) { System.out.printf( "拷贝文件异常:" +e.getMessage()); e.printStackTrace(); } } // 输出,66秒 传统+字节数组,耗时:66029ms

4、缓冲流+字节数组(超级快)

缓冲流的基础上,使用字节数组,加上内置的字节数组,也就是外桶+内桶双结合方式。

public static void test_copy_speed_buffer () { String src = "D:\\01-io-test\\w1.bin" ; String dest = "D:\\01-io-test\\w3-copy.bin" ; long start = System.currentTimeMillis(); try ( BufferedInputStream is = new BufferedInputStream ( new FileInputStream (src)); BufferedOutputStream fos = new BufferedOutputStream ( new FileOutputStream (dest));){ byte [] buffer = new byte [ 1024 ]; int len; while ((len = is.read(buffer)) >= 0 ) { fos.write(buffer, 0 ,len); } System.out.println( "缓冲流+数组,耗时:" +(System.currentTimeMillis()-start)+ "ms" ); } catch (Exception e) { System.out.printf( "拷贝文件异常:" +e.getMessage()); e.printStackTrace(); } } // 输出 缓冲流+数组,耗时:14833ms

总结:

  • 传统+单个字节读取,非常慢;20分钟+
  • 缓冲流+单个字节读取,也慢,但比传统快很多,毕竟有个内置的桶更强;4分钟不到
  • 统+字节数组,按桶读取,提升很多;66秒
  • 缓冲流+字节数组,按桶读取,里面内置还有一个桶,双桶模式,超快!15秒

使用桶(字节数组)去读取,是性能提高的关键!

字符缓冲流

构造方法

  • public BufferedReader(Reader in) :创建一个 新的缓冲输入流。
  • public BufferedWriter(Writer out): 创建一个新的缓冲输出流。

构造举例,代码如下:

// 创建字符缓冲输入流 BufferedReader br = new BufferedReader ( new FileReader ( "br.txt" )); // 创建字符缓冲输出流 BufferedWriter bw = new BufferedWriter ( new FileWriter ( "bw.txt" ));

特有方法

字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。

  • BufferedReader:public String readLine(): 读一行文字。
  • BufferedWriter:public void newLine(): 写一行行分隔符,由系统属性定义符号。

readLine方法演示,代码如下:

public class BufferedReaderDemo { public static void main (String[] args) throws IOException { // 创建流对象 BufferedReader br = new BufferedReader ( new FileReader ( "in.txt" )); // 定义字符串,保存读取的一行文字 String line = null ; // 循环读取,读取到最后返回null while ((line = br.readLine())!= null ) { System.out.print(line); System.out.println( "------" ); } // 释放资源 br.close(); } }

newLine方法演示,代码如下:

public class BufferedWriterDemo throws IOException { public static void main (String[] args) throws IOException  { // 创建流对象 BufferedWriter bw = new BufferedWriter ( new FileWriter ( "out.txt" )); // 写出数据 bw.write( "学习" ); // 写出换行 bw.newLine(); bw.write( "工具" ); bw.newLine(); bw.write( "太多" ); bw.newLine(); // 释放资源 bw.close(); } } 输出效果: 学习 工具 太多

练习:文本排序

请将文本信息恢复顺序。

3.侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必得裨补阙漏,有所广益。 8.愿陛下托臣以讨贼兴复之效,不效,则治臣之罪,以告先帝之灵。若无兴德之言,则责攸之、祎、允等之慢,以彰其咎;陛下亦宜自谋,以咨诹善道,察纳雅言,深追先帝遗诏,臣不胜受恩感激。 4.将军向宠,性行淑均,晓畅军事,试用之于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行阵和睦,优劣得所。 2.宫中府中,俱为一体,陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。 1.先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。 9.今当远离,临表涕零,不知所言。 6.臣本布衣,躬耕于南阳,苟全性命于乱世,不求闻达于诸侯。先帝不以臣卑鄙,猥自枉屈,三顾臣于草庐之中,咨臣以当世之事,由是感激,遂许先帝以驱驰。后值倾覆,受任于败军之际,奉命于危难之间,尔来二十有一年矣。 7.先帝知臣谨慎,故临崩寄臣以大事也。受命以来,夙夜忧叹,恐付托不效,以伤先帝之明,故五月渡泸,深入不毛。今南方已定,兵甲已足,当奖率三军,北定中原,庶竭驽钝,攘除奸凶,兴复汉室,还于旧都。此臣所以报先帝而忠陛下之职分也。至于斟酌损益,进尽忠言,则攸之、祎、允之任也。 5.亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。先帝在时,每与臣论此事,未尝不叹息痛恨于桓、灵也。侍中、尚书、长史、参军,此悉贞良死节之臣,愿陛下亲之信之,则汉室之隆,可计日而待也。

案例分析

    1. 逐行读取文本信息。
    1. 把读取到的文本存储到集合中
    1. 对集合中的文本进行排序
    1. 遍历集合,按顺序,写出文本信息。

案例实现

public class Demo05Test { public static void main (String[] args) throws IOException { //1.创建ArrayList集合,泛型使用String ArrayList<String> list = new ArrayList <>(); //2.创建BufferedReader对象,构造方法中传递FileReader对象 BufferedReader br = new BufferedReader ( new FileReader ( "10_IO\\in.txt" )); //3.创建BufferedWriter对象,构造方法中传递FileWriter对象 BufferedWriter bw = new BufferedWriter ( new FileWriter ( "10_IO\\out.txt" )); //4.使用BufferedReader对象中的方法readLine,以行的方式读取文本 String line; while ((line = br.readLine())!= null ){ //5.把读取到的文本存储到ArrayList集合中 list.add(line); } //6.使用Collections集合工具类中的方法sort,对集合中的元素按照自定义规则排序 Collections.sort(list, new Comparator <String>() { /* o1-o2:升序 o2-o1:降序 */ @Override public int compare (String o1, String o2) { //依次比较集合中两个元素的首字母,升序排序 return o1.charAt( 0 )-o2.charAt( 0 ); } }); //7.遍历ArrayList集合,获取每一个元素 for (String s : list) { //8.使用BufferedWriter对象中的方法wirte,把遍历得到的元素写入到文本中(内存缓冲区中) bw.write(s); //9.写换行 bw.newLine(); } //10.释放资源 bw.close(); br.close(); } }

二、转换流

字符编码

计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。

编码:字符(能看懂的)--字节(看不懂的)

解码:字节(看不懂的)-->字符(能看懂的)

字符编码Character Encoding : 就是一套自然语言的字符与二进制数之间的对应规则。编码表:生活中文字和计算机中二进制的对应规则

字符集

字符集 Charset:也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。

计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有ASCII字符集、GBK字符集、Unicode字符集等。可见,当指定了编码,它所对应的字符集自然就指定了,所以编码才是我们最终要关心的。

ASCII字符集

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字和西文符号)。

基本的ASCII字符集,使用7位(bits)表示一个字符,共128字符。ASCII的扩展字符集使用8位(bits)表示一个字符,共256字符,方便支持欧洲常用字符。

ISO-8859-1字符集

拉丁码表,别名Latin-1,用于显示欧洲使用的语言,包括荷兰、丹麦、德语、意大利语、西班牙语等。

ISO-8859-1使用单字节编码,兼容ASCII编码。

GBxxx字符集

GB就是国标的意思,是为了显示中文而设计的一套字符集。

GB2312:简体中文码表。一个小于127的字符的意义与原来相同。但两个大于127的字符连在一起时,就表示一个汉字,这样大约可以组合了包含7000多个简体汉字,此外数学符号、罗马希腊的字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。

GBK:最常用的中文码表。是在GB2312标准基础上的扩展规范,使用了双字节编码方案,共收录了21003个汉字,完全兼容GB2312标准,同时支持繁体汉字以及日韩汉字等。

GB18030:最新的中文码表。收录汉字70244个,采用多字节编码,每个字可以由1个、2个或4个字节组成。支持中国国内少数民族的文字,同时支持繁体汉字以及日韩汉字等。

Unicode字符集

Unicode编码系统为表达任意语言的任意字符而设计,是业界的一种标准,也称为统一码、标准万国码

它最多使用4个字节的数字来表达每个字母、符号,或者文字。有三种编码方案,UTF-8、UTF-16和UTF-32。最为常用的UTF-8编码。

UTF-8编码,可以用来表示Unicode标准中任何字符,它是电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。所以,我们开发Web应用,也要使用UTF-8编码。它使用一至四个字节为每个字符编码,编码规则:

    1. 128个US-ASCII字符,只需一个字节编码。
    1. 拉丁文等字符,需要二个字节编码。
    1. 大部分常用字(含中文),使用三个字节编码。
    1. 其他极少使用的Unicode辅助字符,使用四字节编码。

编码引出的问题

在IDEA中,使用FileReader 读取项目中的文本文件。由于IDEA的设置,都是默认的UTF-8编码,所以没有任何问题。但是,当读取Windows系统中创建的文本文件时,由于Windows系统的默认是GBK编码,就会出现乱码。

public class ReaderDemo { public static void main (String[] args) throws IOException { FileReader fileReader = new FileReader ( "E:\\File_GBK.txt" ); int read; while ((read = fileReader.read()) != - 1 ) { System.out.print(( char )read); } fileReader.close(); } } 输出结果: ���

那么如何读取GBK编码的文件呢?

InputStreamReader类

转换流java.io.InputStreamReader,是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。

构造方法

  • InputStreamReader(InputStream in): 创建一个使用默认字符集的字符流。
  • InputStreamReader(InputStream in, String charsetName): 创建一个指定字符集的字符流。

构造举例,代码如下:

InputStreamReader isr = new InputStreamReader ( new FileInputStream ( "in.txt" )); InputStreamReader isr2 = new InputStreamReader ( new FileInputStream ( "in.txt" ) , "GBK" );

指定编码读取

public class ReaderDemo2 { public static void main (String[] args) throws IOException { // 定义文件路径,文件为gbk编码 String FileName = "E:\\file_gbk.txt" ; // 创建流对象,默认UTF8编码 InputStreamReader isr = new InputStreamReader ( new FileInputStream (FileName)); // 创建流对象,指定GBK编码 InputStreamReader isr2 = new InputStreamReader ( new FileInputStream (FileName) , "GBK" ); // 定义变量,保存字符 int read; // 使用默认编码字符流读取,乱码 while ((read = isr.read()) != - 1 ) { System.out.print(( char )read); // ��Һ� } isr.close(); // 使用指定编码字符流读取,正常解析 while ((read = isr2.read()) != - 1 ) { System.out.print(( char )read); // 大家好 } isr2.close(); } }

OutputStreamWriter类

转换流java.io.OutputStreamWriter ,是Writer的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。

构造方法

  • OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流。
  • OutputStreamWriter(OutputStream in, String charsetName): 创建一个指定字符集的字符流。

构造举例,代码如下:

OutputStreamWriter isr = new OutputStreamWriter ( new FileOutputStream ( "out.txt" )); OutputStreamWriter isr2 = new OutputStreamWriter ( new FileOutputStream ( "out.txt" ) , "GBK" );

指定编码写出

public class OutputDemo { public static void main (String[] args) throws IOException { // 定义文件路径 String FileName = "E:\\out.txt" ; // 创建流对象,默认UTF8编码 OutputStreamWriter osw = new OutputStreamWriter ( new FileOutputStream (FileName)); // 写出数据 osw.write( "你好" ); // 保存为6个字节 osw.close(); // 定义文件路径 String FileName2 = "E:\\out2.txt" ; // 创建流对象,指定GBK编码 OutputStreamWriter osw2 = new OutputStreamWriter ( new FileOutputStream (FileName2), "GBK" ); // 写出数据 osw2.write( "你好" ); // 保存为4个字节 osw2.close(); } }

转换流理解图解

转换流是字节与字符间的桥梁!

练习:转换文件编码

将GBK编码的文本文件,转换为UTF-8编码的文本文件。

案例分析

    1. 指定GBK编码的转换流,读取文本文件。
    1. 使用UTF-8编码的转换流,写出文本文件。

案例实现

public class TransDemo { public static void main (String[] args) { // 1.定义文件路径 String srcFile = "file_gbk.txt" ; String destFile = "file_utf8.txt" ; // 2.创建流对象 // 2.1 转换输入流,指定GBK编码 InputStreamReader isr = new InputStreamReader ( new FileInputStream (srcFile) , "GBK" ); // 2.2 转换输出流,默认utf8编码 OutputStreamWriter osw = new OutputStreamWriter ( new FileOutputStream (destFile)); // 3.读写数据 // 3.1 定义数组 char [] cbuf = new char [ 1024 ]; // 3.2 定义长度 int len; // 3.3 循环读取 while ((len = isr.read(cbuf))!=- 1 ) { // 循环写出 osw.write(cbuf, 0 ,len); } // 4.释放资源 osw.close(); isr.close(); } }

三、序列化

Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据对象的类型对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。

反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化对象的数据对象的类型对象中存储的数据信息,都可以用来在内存中创建对象。看图理解序列化:

ObjectOutputStream类

java.io.ObjectOutputStream类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。

构造方法

  • public ObjectOutputStream(OutputStream out): 创建一个指定OutputStream的ObjectOutputStream。

构造举例,代码如下:

FileOutputStream fileOut = new FileOutputStream ( "employee.txt" ); ObjectOutputStream out = new ObjectOutputStream (fileOut);

序列化操作

    1. 一个对象要想序列化,必须满足两个条件:
  • 该类必须实现java.io.Serializable接口,Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException

  • 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient 关键字修饰。
public class Employee implements java .io.Serializable { public String name; public String address; public transient int age; // transient瞬态修饰成员,不会被序列化 public void addressCheck () { System.out.println( "Address  check : " + name + " -- " + address); } }

2.写出对象方法

  • public final void writeObject (Object obj) : 将指定的对象写出。
public class SerializeDemo { public static void main (String [] args) { Employee e = new Employee (); e.name = "zhangsan" ; e.address = "beiqinglu" ; e.age = 20 ; try { // 创建序列化流对象 ObjectOutputStream out = new ObjectOutputStream ( new FileOutputStream ( "employee.txt" )); // 写出对象 out.writeObject(e); // 释放资源 out.close(); fileOut.close(); System.out.println( "Serialized data is saved" ); // 姓名,地址被序列化,年龄没有被序列化。 } catch (IOException i)   { i.printStackTrace(); } } } 输出结果: Serialized data is saved

ObjectInputStream类

ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。

构造方法

  • public ObjectInputStream(InputStream in): 创建一个指定InputStream的ObjectInputStream。

反序列化操作1

如果能找到一个对象的class文件,我们可以进行反序列化操作,调用ObjectInputStream读取对象的方法:

  • public final Object readObject () : 读取一个对象。
public class DeserializeDemo { public static void main (String [] args) { Employee e = null ; try { // 创建反序列化流 FileInputStream fileIn = new FileInputStream ( "employee.txt" ); ObjectInputStream in = new ObjectInputStream (fileIn); // 读取一个对象 e = (Employee) in.readObject(); // 释放资源 in.close(); fileIn.close(); } catch (IOException i) { // 捕获其他异常 i.printStackTrace(); return ; } catch (ClassNotFoundException c)  { // 捕获类找不到异常 System.out.println( "Employee class not found" ); c.printStackTrace(); return ; } // 无异常,直接打印输出 System.out.println( "Name: " + e.name); // zhangsan System.out.println( "Address: " + e.address); // beiqinglu System.out.println( "age: " + e.age); // 0 } }

对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个 ClassNotFoundException 异常。

反序列化操作2

另外,当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException异常。发生这个异常的原因如下:

  • 该类的序列版本号与从流中读取的类描述符的版本号不匹配
  • 该类包含未知数据类型
  • 该类没有可访问的无参数构造方法

Serializable 接口给需要序列化的类,提供了一个序列版本号。serialVersionUID 该版本号的目的在于验证序列化的对象和对应类是否版本匹配

public class Employee implements java .io.Serializable { // 加入序列版本号 private static final long serialVersionUID = 1L ; public String name; public String address; // 添加新的属性 ,重新编译, 可以反序列化,该属性赋为默认值. public int eid; public void addressCheck () { System.out.println( "Address  check : " + name + " -- " + address); } }

练习:序列化集合

    1. 将存有多个自定义对象的集合序列化操作,保存到list.txt文件中。
    1. 反序列化list.txt ,并遍历集合,打印对象信息。

案例分析

    1. 把若干学生对象 ,保存到集合中。
    1. 把集合序列化。
    1. 反序列化读取时,只需要读取一次,转换为集合类型。
    1. 遍历集合,可以打印所有的学生信息

案例实现

public class SerTest { public static void main (String[] args) throws Exception { // 创建 学生对象 Student student = new Student ( "老王" , "laow" ); Student student2 = new Student ( "老张" , "laoz" ); Student student3 = new Student ( "老李" , "laol" ); ArrayList<Student> arrayList = new ArrayList <>(); arrayList.add(student); arrayList.add(student2); arrayList.add(student3); // 序列化操作 // serializ(arrayList); // 反序列化 ObjectInputStream ois = new ObjectInputStream ( new FileInputStream ( "list.txt" )); // 读取对象,强转为ArrayList类型 ArrayList<Student> list  = (ArrayList<Student>)ois.readObject(); for ( int i = 0 ; i < list.size(); i++ ){ Student s = list.get(i); System.out.println(s.getName()+ "--" + s.getPwd()); } } private static void serializ (ArrayList<Student> arrayList) throws Exception { // 创建 序列化流 ObjectOutputStream oos = new ObjectOutputStream ( new FileOutputStream ( "list.txt" )); // 写出对象 oos.writeObject(arrayList); // 释放资源 oos.close(); } }

四、打印流

平时我们在控制台打印输出,是调用print方法和println方法完成的,这两个方法都来自于java.io.PrintStream类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。

PrintStream类

  • public PrintStream(String fileName): 使用指定的文件名创建一个新的打印流。

构造举例,代码如下:

PrintStream ps = new PrintStream ( "ps.txt" );

System.out就是PrintStream类型的,只不过它的流向是系统规定的,打印在控制台上。不过,既然是流对象,我们就可以玩一个"小把戏",改变它的流向

public class PrintDemo { public static void main (String[] args) throws IOException { // 调用系统的打印流,控制台直接输出97 System.out.println( 97 ); // 创建打印流,指定文件的名称 PrintStream ps = new PrintStream ( "ps.txt" ); // 设置系统的打印流流向,输出到ps.txt System.setOut(ps); // 调用系统的打印流,ps.txt中输出97 System.out.println( 97 ); } }

五、压缩流和解压缩流

压缩流:负责压缩文件或者文件夹。

解压缩流:负责把压缩包中的文件和文件夹解压出来。

/* *   解压缩流 * * */ public class ZipStreamDemo1 { public static void main (String[] args) throws IOException { //1.创建一个File表示要解压的压缩包 File src = new File ( "D:\\aaa.zip" ); //2.创建一个File表示解压的目的地 File dest = new File ( "D:\\" ); //调用方法 unzip(src,dest); } //定义一个方法用来解压 public static void unzip (File src,File dest) throws IOException { //解压的本质:把压缩包里面的每一个文件或者文件夹读取出来,按照层级拷贝到目的地当中 //创建一个解压缩流用来读取压缩包中的数据 ZipInputStream zip = new ZipInputStream ( new FileInputStream (src)); //要先获取到压缩包里面的每一个zipentry对象 //表示当前在压缩包中获取到的文件或者文件夹 ZipEntry entry; while ((entry = zip.getNextEntry()) != null ){ System.out.println(entry); if (entry.isDirectory()){ //文件夹:需要在目的地dest处创建一个同样的文件夹 File file = new File (dest,entry.toString()); file.mkdirs(); } else { //文件:需要读取到压缩包中的文件,并把他存放到目的地dest文件夹中(按照层级目录进行存放) FileOutputStream fos = new FileOutputStream ( new File (dest,entry.toString())); int b; while ((b = zip.read()) != - 1 ){ //写到目的地 fos.write(b); } fos.close(); //表示在压缩包中的一个文件处理完毕了。 zip.closeEntry(); } } zip.close(); } }
public class ZipStreamDemo2 { public static void main (String[] args) throws IOException { /* *   压缩流 *      需求: *          把D:\\a.txt打包成一个压缩包 * */ //1.创建File对象表示要压缩的文件 File src = new File ( "D:\\a.txt" ); //2.创建File对象表示压缩包的位置 File dest = new File ( "D:\\" ); //3.调用方法用来压缩 toZip(src,dest); } /* *   作用:压缩 *   参数一:表示要压缩的文件 *   参数二:表示压缩包的位置 * */ public static void toZip (File src,File dest) throws IOException { //1.创建压缩流关联压缩包 ZipOutputStream zos = new ZipOutputStream ( new FileOutputStream ( new File (dest, "a.zip" ))); //2.创建ZipEntry对象,表示压缩包里面的每一个文件和文件夹 //参数:压缩包里面的路径 ZipEntry entry = new ZipEntry ( "aaa\\bbb\\a.txt" ); //3.把ZipEntry对象放到压缩包当中 zos.putNextEntry(entry); //4.把src文件中的数据写到压缩包当中 FileInputStream fis = new FileInputStream (src); int b; while ((b = fis.read()) != - 1 ){ zos.write(b); } zos.closeEntry(); zos.close(); } }
public class ZipStreamDemo3 { public static void main (String[] args) throws IOException { /* *   压缩流 *      需求: *          把D:\\aaa文件夹压缩成一个压缩包 * */ //1.创建File对象表示要压缩的文件夹 File src = new File ( "D:\\aaa" ); //2.创建File对象表示压缩包放在哪里(压缩包的父级路径) File destParent = src.getParentFile(); //D:\\ //3.创建File对象表示压缩包的路径 File dest = new File (destParent,src.getName() + ".zip" ); //4.创建压缩流关联压缩包 ZipOutputStream zos = new ZipOutputStream ( new FileOutputStream (dest)); //5.获取src里面的每一个文件,变成ZipEntry对象,放入到压缩包当中 toZip(src,zos,src.getName()); //aaa //6.释放资源 zos.close(); } /* *   作用:获取src里面的每一个文件,变成ZipEntry对象,放入到压缩包当中 *   参数一:数据源 *   参数二:压缩流 *   参数三:压缩包内部的路径 * */ public static void toZip (File src,ZipOutputStream zos,String name) throws IOException { //1.进入src文件夹 File[] files = src.listFiles(); //2.遍历数组 for (File file : files) { if (file.isFile()){ //3.判断-文件,变成ZipEntry对象,放入到压缩包当中 ZipEntry entry = new ZipEntry (name + "\\" + file.getName()); //aaa\\no1\\a.txt zos.putNextEntry(entry); //读取文件中的数据,写到压缩包 FileInputStream fis = new FileInputStream (file); int b; while ((b = fis.read()) != - 1 ){ zos.write(b); } fis.close(); zos.closeEntry(); } else { //4.判断-文件夹,递归 toZip(file,zos,name + "\\" + file.getName()); //     no1            aaa   \\   no1 } } } }

六、属性集

java.util.Properties继承于Hashtable ,来表示一个持久的属性集。它使用键值结构存储数据,每个键及其对应值都是一个字符串。该类也被许多Java类使用,比如获取系统属性时,System.getProperties 方法就是返回一个Properties对象。

Properties类

构造方法

  • public Properties() :创建一个空的属性列表。

基本的存储方法

  • public Object setProperty(String key, String value) : 保存一对属性。
  • public String getProperty(String key):使用此属性列表中指定的键搜索属性值。
  • public Set<String> stringPropertyNames():所有键的名称的集合。
public class ProDemo { public static void main (String[] args) throws FileNotFoundException { // 创建属性集对象 Properties properties = new Properties (); // 添加键值对元素 properties.setProperty( "filename" , "a.txt" ); properties.setProperty( "length" , "209385038" ); properties.setProperty( "location" , "D:\\a.txt" ); // 打印属性集对象 System.out.println(properties); // 通过键,获取属性值 System.out.println(properties.getProperty( "filename" )); System.out.println(properties.getProperty( "length" )); System.out.println(properties.getProperty( "location" )); // 遍历属性集,获取所有键的集合 Set<String> strings = properties.stringPropertyNames(); // 打印键值对 for (String key : strings ) { System.out.println(key+ " -- " +properties.getProperty(key)); } } } 输出结果: {filename=a.txt, length= 209385038 , location=D:\a.txt} a.txt 209385038 D:\a.txt filename -- a.txt length -- 209385038 location -- D:\a.txt

与流相关的方法

  • public void load(InputStream inStream): 从字节输入流中读取键值对。

参数中使用了字节输入流,通过流对象,可以关联到某文件上,这样就能够加载文本中的数据了。文本数据格式:

filename=a.txt length=209385038 location=D:\a.txt

加载代码演示:

public class ProDemo2 { public static void main (String[] args) throws FileNotFoundException { // 创建属性集对象 Properties pro = new Properties (); // 加载文本中信息到属性集 pro.load( new FileInputStream ( "read.txt" )); // 遍历集合并打印 Set<String> strings = pro.stringPropertyNames(); for (String key : strings ) { System.out.println(key+ " -- " +pro.getProperty(key)); } } } 输出结果: filename -- a.txt length -- 209385038 location -- D:\a.txt

小贴士:文本中的数据,必须是键值对形式,可以使用空格、等号、冒号等符号分隔。

七、Path类

File、Path、URL三者的职责比较。

File

  • 职责:表示文件或目录的抽象路径名,用于基本的文件/目录操作。
  • 主要功能
  • 创建、删除、重命名文件/目录。
  • 获取文件属性(如大小、最后修改时间)。
  • 列出目录内容。

  • 局限性

  • 不支持符号链接、文件属性访问等高级操作。
  • 操作是基于平台的,可能在不同系统下表现不一致。

  • 使用场景

  • 简单的文件存在性检查、创建、删除等操作。

Path

  • 职责:表示文件系统中的路径,提供更强大和灵活的路径操作功能。
  • 主要功能
  • 解析和构造路径(如拼接、标准化)。
  • 支持符号链接、文件属性访问。
  • 更好的异常处理和跨平台兼容性。

  • 优势

  • 是 Java NIO 的一部分,功能比 File 更丰富。
  • 提供了 Files 工具类进行文件读写、复制、移动等操作。

  • 使用场景

  • 需要复杂的路径操作或文件管理时,推荐使用 Path

URL

  • 职责:表示统一资源定位符,指向互联网上的资源。
  • 主要功能
  • 定位网络资源(如 HTTP、FTP)。
  • 打开流以读取远程资源内容。

  • 局限性

  • 主要用于网络资源,处理本地文件路径不够灵活。
  • 不适合直接用于本地文件系统的复杂操作。

  • 使用场景

  • 需要访问网络资源或通过协议(如 HTTP)获取数据时。

总结对比

| |

类名 职责 特点 推荐使用场景
File 表示文件或目录的抽象路径名 简单易用,但功能有限 基本的文件存在性检查、创建、删除等
Path 表示文件系统路径,提供高级操作 功能强大,跨平台兼容性好 复杂的路径操作、文件管理
URL 表示网络资源地址 用于访问远程资源 访问网络资源、通过协议获取数据

实践建议

  • 优先使用 Path:对于大多数文件和路径操作,Path 是首选 API,尤其是需要跨平台兼容性和高级功能时。
  • 谨慎使用 File:仅在需要与旧代码兼容或执行简单操作时使用。
  • URL 用于网络资源:当需要访问远程资源(如 HTTP URL)时,使用 URL或其封装类。

Path类图

Path是接口,其实现类,根据JDK的实现版本确定,底层操作系统密切相关。
接口内有静态获取实例的方法Path.of(...)源码分析:

// 第一步:Path接口 public static Path of (String first, String... more) { return FileSystems.getDefault().getPath(first, more); } // 第二步:FileSystems类 public static FileSystem getDefault () { if (VM.isModuleSystemInited()) { return DefaultFileSystemHolder.defaultFileSystem; } else { // always use the platform's default file system during startup return DefaultFileSystemProvider.theFileSystem(); } } // 第三步:JDk对平台的实现,如这里是:windows /** * Creates this platform's default FileSystemProvider. */ public class DefaultFileSystemProvider { private static final WindowsFileSystemProvider INSTANCE = new WindowsFileSystemProvider (); private DefaultFileSystemProvider () { } /** * Returns the platform's default file system provider. */ public static WindowsFileSystemProvider instance () { return INSTANCE; } /** * Returns the platform's default file system. */ public static FileSystem theFileSystem () { return INSTANCE.theFileSystem(); } } // 第四步:该系统的提供者WindowsFileSystemProvider @Override public Path getPath (URI uri) { return WindowsUriSupport.fromUri(theFileSystem, uri); } // 第五步:从文件系统中将标准的uri转换为Path路径 static WindowsPath fromUri (WindowsFileSystem fs, URI uri) { // 省略 return WindowsPath.parse(fs, path); // Path的实现类,其内部静态方法 } // 第六步:创建该系统的Path实例 /** * Creates a Path by parsing the given path. */ static WindowsPath parse (WindowsFileSystem fs, String path) { WindowsPathParser. Result result = WindowsPathParser.parse(path); return new WindowsPath (fs, result.type(), result.root(), result.path()); }

Path其他方法

Path常用方法

使用 Path 是 Java NIO(New I/O)中处理文件路径和操作的核心方式,具有跨平台兼容性、强大的功能以及更清晰的 API 设计。

1、获取 Path 实例

  • 使用 Paths.get() 方法创建:
Path path = Paths.get( "data" , "input.txt" ); // 多参数自动拼接
  • 或从字符串路径创建:
Path path = Paths.get( "/home/user/data/input.txt" );
  • 从 URI 创建:
URI uri = new URI ( "file:/home/user/data/input.txt" ); Path path = Path.of(uri);

⚠️ 避免手动拼接路径字符串,应使用 Paths.get(...) 让系统自动处理不同平台下的路径分隔符。

2、路径拼接与解析

  • 拼接路径resolve()
Path dir = Paths.get( "/home/user" ); Path file = dir.resolve( "data.txt" ); // /home/user/data.txt
  • 相对路径转换为绝对路径toAbsolutePath()
Path relative = Paths.get( "data.txt" ); Path absolute = relative.toAbsolutePath(); // 当前工作目录 + data.txt
  • 规范化路径:normalize()
Path path = Paths.get( "/home/user/./docs/../data.txt" ).normalize(); // 结果为 /home/user/data.txt
  • 获取父路径:getParent()
Path parent = path.getParent(); // 返回上一级目录

3、判断路径属性

  • 判断是否存在Files.exists(path)
if (Files.exists(path)) { System.out.println( "File exists." ); }
  • 判断是否是目录Files.isDirectory(path)
if (Files.isDirectory(path)) { System.out.println( "It's a directory." ); }
  • 判断是否是文件Files.isRegularFile(path)
if (Files.isRegularFile(path)) { System.out.println( "It's a regular file." ); }

⚠️ 所有这些检查都应结合异常处理,例如捕获 IOException

4、文件读写操作(配合 Files 类)

示例:读取文件内容

List<String> lines = Files.readAllLines(path);

示例:写入文件内容

List<String> content = List.of( "Line 1" , "Line 2" ); Files.write(path, content, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);

示例:复制、移动、删除

Path source = Paths.get( "source.txt" ); Path target = Paths.get( "dest.txt" ); // 复制 Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); // 移动(重命名) Files.move(source, target, StandardCopyOption.ATOMIC_MOVE); // 删除 Files.deleteIfExists(target);

5、遍历目录

使用 Files.walk() 遍历子目录

Files.walk(path) .filter(p -> p.toString().endsWith( ".txt" )) .forEach(System.out::println);

使用 DirectoryStream 遍历当前目录

try (DirectoryStream<Path> stream = Files.newDirectoryStream(path, "*.txt" )) { for (Path entry : stream) { System.out.println(entry.getFileName()); } }

6、 异常处理与资源管理

  • 使用 try-with-resources 管理流资源:
try ( BufferedReader reader = Files.newBufferedReader(path)) { String line; while ((line = reader.readLine()) != null ) { System.out.println(line); } } catch (IOException e) { e.printStackTrace(); }

7、 跨平台路径处理建议

  • 使用 File.separator 或直接使用 /(Java 自动适配)
Path path = Paths.get( "data" + File.separator + "input.txt" );
  • 或者统一使用正斜杠 /,Java 会自动识别:
Path path = Paths.get( "data/input.txt" );

8、安全转换到 URI / URL

  • 转换为 URI:
URI uri = path.toUri();
  • 转换为 URL:
URL url = path.toUri().toURL();

⚠️ 不要直接使用 new URL(path.toString()),这可能导致格式错误或不一致。

Path小结

| |

操作类型 推荐方法/类 说明
获取实例 Paths.get() / Path.of() 避免手动拼接路径字符串
路径操作 resolve() , normalize() , getParent() 构建、标准化路径
属性判断 Files.exists() , isDirectory() , isRegularFile() 检查路径状态
文件读写 Files.read , Files.write 推荐使用缓冲流
目录遍历 Files.walk() , DirectoryStream 支持过滤器
异常处理 try-with-resources 自动关闭资源
路径转换 toUri() , toAbsolutePath() 安全可靠
跨平台支持 使用 / 分隔符 Java 自动适配

代码示例

public class TodayTest { public static void main (String[] args) throws Exception { //        test_currentDir(); //        test_path(); //        test_write("ts.txt"); //        test_read("ts.txt"); //        test_copy_move_del(); //        test_write_option(StandardOpenOption.APPEND); } public static void test_uri_url () throws MalformedURLException { Path path = Paths.get( "testdata" , "wsj" , "a.txt" ); System.out.println(path); URI uri = path.toUri(); System.out.println(uri); URL url = uri.toURL(); System.out.println(url); } // 遍历当前目录,可设置条件过滤 public static void test_newDirStream () throws IOException { Files.newDirectoryStream(Paths.get( "testdata" ), p->p.toString().endsWith( ".txt" )).forEach(System.out::println); } // 遍历子目录 public static void test_walk () throws IOException { Files.walk(Paths.get( "testdata" )) .filter(p->p.toString().endsWith( ".txt" )) .forEach(System.out::println); } public static void test_currentDir () { String projectDir = System.getProperty( "user.dir" ); System.out.println( "项目当前目录: " +projectDir); } public static void test_path () throws Exception { // 各种方式创建path实例 // 系统自适配连接符,避免人工出错 Path p1 = Paths.get( "data" , "doc" , "a.txt" ); System.out.println( "path1: " +p1); System.out.println( "path1-绝对路径: " +p1.toAbsolutePath()); Path p2 = Paths.get( "/data/b.txt" ); System.out.println( "path2: " +p2); URI uri = new URI ( "file:/data/c.txt" ); Path p3 = Paths.get(uri); System.out.println( "path3: " +p3); Path p4 = Path.of(uri); System.out.println( "path4: " +p4); // 拼接路径,新实例 Path p5 = Paths.get( "home" , "data" , "doc" ); Path p6 = p5.resolve( "d.txt" ); System.out.println( "p5: " +p5); System.out.println( "p6: " +p6); // 转换为绝对路径 Path absolutePath = p6.toAbsolutePath(); System.out.println( "absolutePath: " +absolutePath); Path p7 = Paths.get( "/home/data/doc/./../g.txt" ); System.out.println( "p7: " +p7); Path p8 = p7.normalize(); System.out.println( "p8: " +p8); Path p9 = p8.getParent(); System.out.println( "p8的parent为p9: " +p9); // 返回上一级 System.out.println( "p9存在?" +Files.exists(p9)); // 存在且为目录 System.out.println( "p9是目录?" +Files.isDirectory(p9)); System.out.println( "p8是文件?" +Files.isRegularFile(p8)); } public static void test_copy_move_del () throws IOException { Path path = Paths.get( "testdata" , "wsj" , "a.txt" ); Path pathParent = path.getParent(); if (Files.notExists(pathParent)) { Files.createDirectories(pathParent); } Files.writeString(path,demoTxt); Files.writeString(path, LocalDateTime.now().toString(),StandardOpenOption.APPEND); Path dest = Paths.get( "a-back.txt" ); Files.copy(path, dest, StandardCopyOption.REPLACE_EXISTING); System.out.println( "文件拷贝成功!" +dest.toAbsolutePath()); } public static void test_read (String fileName) throws IOException { Path path = Paths.get(fileName); List<String> readAllLines = Files.readAllLines(path); readAllLines.forEach(System.out::println); } public static void test_write (String fileName) throws IOException { Path path = Paths.get(fileName); System.out.println(path); Path p1 = Files.writeString(path.toAbsolutePath(), demoTxt); System.out.println( "写出成功:" +p1.toAbsolutePath()); } public static void test_write_option (StandardOpenOption option) throws IOException { Path path = Files.writeString(Paths.get( "my.txt" ), "hello\n" , option); System.out.println( "文件写入成功:" +path); } static String demoTxt = """ you are such a good boy! yes! thank you! That is cool. """ ; }

Files常用方法

Files工具方法,来自JDK1.7版本,大大增强了文件的处理能力,经常和Path一起配合使用。

八、URI和URL

URI(Uniform Resource Identifier)和 URL(Uniform Resource Locator)是用于标识和定位资源的两种标准,它们之间有明确的区别和联系。以下是它们的详细对比:

基本比对

1、定义

| |

名称 定义
URI 统一资源标识符,用于 唯一标识一个资源 ,可以是名称、地址或其他形式。
URL 统一资源定位符,是 URI 的一种子集,用于 定位资源的具体位置 ,通常包含访问该资源的协议(如 http 、 https 、file 等)。

2、区别

| |

特性 URI URL
用途 标识资源(可能是抽象的) 定位资源(具体的网络地址)
是否能独立访问资源 ❌ 不能直接访问资源 ✅ 可以直接通过 URL 访问资源
组成 包含命名(URN)和定位(URL) 包含协议、主机名、路径等
示例 urn:isbn:0451450523 (书籍 ISBN)、 /api/users/1 https://example.com/api/users/1 、 file:///D:/a.txt

3、联系

  • URL 是 URI 的一种实现
  • 所有的 URL 都是 URI,但不是所有的 URI 都是 URL。
  • 换句话说,URL 是 URI 的子集,专注于“如何找到资源”。

  • URI 的分类

  • URN(Uniform Resource Name):仅用于标识资源,不提供访问方式。
  • 示例:urn:isbn:0451450523

  • URL(Uniform Resource Locator):不仅标识资源,还提供访问资源的方法。

  • 示例:https://www.example.com/page.html

4、实际案例分析

你提供的文件地址是一个典型的 URL

file:///D:/gitcode/icp-server/testdata/wsj/a.txt
  • 它使用了 file:// 协议,表示这是一个本地文件系统中的资源。
  • 它符合 URL 的结构(协议 + 路径),可以直接通过浏览器访问。
  • 同时它也是一个有效的 URI,因为它标识了一个具体的资源。

5、示例

| |

类型 示例 说明
URI(非 URL) urn:uuid:123e4567-e89b-12d3-a456-426614174000 仅标识资源,不提供访问方式
URI(也是 URL) https://example.com/path/to/file.txt 标识并定位资源
URI(也是 URL) file:///D:/project/data.txt 使用 file 协议定位本地资源
URI(非 URL) /api/users 相对路径,无法单独访问资源

URI 的通用格式

URI 的标准格式如下:

<scheme>:[//<authority>][/<path>][?<query>][#<fragment>]
  • <scheme>:协议名,如 http, https, file, ftp 等。
  • [//<authority>]:主机和端口等信息(可选),比如 example.com:80
  • [/<path>]:路径部分。
  • [?<query>]:查询参数。
  • [#<fragment>]:片段标识符。

对于 file:// 这种本地协议来说:

  • 没有主机(authority)部分,所以 authority 被省略。
  • 但仍然需要保留 // 来表示 authority 部分的存在(即使为空)。
  • 所以完整的写法是:
file:///<path>

这就是为什么你会看到 file:///D:/project/file.txt , 三个斜杠的来源。我们来拆解一下这个地址:

file:///D:/project/file.txt

| |

部分 内容 说明
file scheme 协议名
// authority 分隔符 表示 authority 部分开始(虽然为空)
/D:/project/file.txt path 文件路径

因为没有主机名,authority 是空的,所以写成 file:// + / + path,即 file:///

对比其他常见协议

| |

协议 示例 说明
HTTP http://example.com/path/to/file.html 包含 authority ( example.com )
HTTPS https://www.google.com/search?q=test 同上
FTP ftp://ftp.example.net/pub/file.txt 同上
FILE (local) file:///D:/project/file.txt authority 空,所以是 file:///

总结对比表

| |

项目 URI URL
全称 Uniform Resource Identifier Uniform Resource Locator
是否能定位资源 ❌ 抽象标识 ✅ 明确定位
是否能访问资源 ❌ 不一定 ✅ 可以直接访问
是否是另一者的子集 超集 子集
示例 urn:isbn:0451450523 , /api/users https://example.com/api/users , file:///D:/a.txt

访问远程资源

要查看远程服务器上的图片或文件,不能使用 file:// 协议(只能访问本地文件),而是需要通过网络协议(如 HTTP、HTTPS、FTP 等)来访问。

访问远程文件的方式

| |

协议 描述 示例
HTTP/HTTPS 最常用,用于 Web 资源访问 https://example.com/images/logo.png
FTP 文件传输协议,适合大文件下载 ftp://ftp.example.net/files/data.zip
SFTP/SCP 安全的远程文件访问协议 需要专用客户端或 Java 库支持
NFS/Samba 局域网内共享文件系统 通常挂载为本地路径后使用 file://

使用 HTTP/HTTPS 查看远程图片或文件

1. 直接在浏览器中打开 URL

适用于静态资源(如图片、PDF、TXT):

https://example.com/images/photo.jpg

浏览器会自动加载并显示该图片。

⚠️ 注意:如果服务器没有设置 CORS 或防盗链机制,可能无法直接访问。

2. Java 中访问远程文件(HTTP/HTTPS)

示例 1:读取远程文本文件内容(如 TXT)

import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.URL; public class RemoteFileReader { public static void main (String[] args) { try { URL url = new URL ( "https://example.com/files/demo.txt" ); try ( BufferedReader reader = new BufferedReader ( new InputStreamReader (url.openStream()))) { String line; while ((line = reader.readLine()) != null ) { System.out.println(line); } } } catch (Exception e) { e.printStackTrace(); } } }

示例 2:下载远程图片并保存到本地

import java.io.InputStream; import java.io.FileOutputStream; import java.net.URL; public class ImageDownloader { public static void main (String[] args) { try { URL url = new URL ( "https://example.com/images/photo.jpg" ); try ( InputStream in = url.openStream(); FileOutputStream out = new FileOutputStream ( "downloaded_photo.jpg" )) { byte [] buffer = new byte [ 1024 ]; int bytesRead; while ((bytesRead = in.read(buffer)) != - 1 ) { out.write(buffer, 0 , bytesRead); } System.out.println( "图片已保存!" ); } } catch (Exception e) { e.printStackTrace(); } } }

带认证的远程资源访问(如需登录)

如果你访问的资源需要权限(如 Token、Basic Auth、Cookie 等),你需要构造带有请求头的 HTTP 请求。

示例:使用 HttpURLConnection 带 Basic 认证访问

import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.Base64; public class AuthenticatedRequest { public static void main (String[] args) { try { String user = "username" ; String pass = "password" ; String auth = Base64.getEncoder().encodeToString((user + ":" + pass).getBytes()); URL url = new URL ( "https://api.example.com/secure/data.txt" ); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod( "GET" ); conn.setRequestProperty( "Authorization" , "Basic " + auth); if (conn.getResponseCode() == 200 ) { try ( BufferedReader reader = new BufferedReader ( new InputStreamReader (conn.getInputStream()))) { String line; while ((line = reader.readLine()) != null ) { System.out.println(line); } } } else { System.out.println( "访问失败,状态码:" + conn.getResponseCode()); } } catch (Exception e) { e.printStackTrace(); } } }

使用 FTP 协议访问远程文件(可选)

你可以使用 Apache Commons VFS 或 java.net.URL 来访问 FTP 资源:

URL url = new URL ( "ftp://anonymous@ftp.example.net/pub/file.txt" ); try ( BufferedReader reader = new BufferedReader ( new InputStreamReader (url.openStream()))) { String line; while ((line = reader.readLine()) != null ) { System.out.println(line); } }

⚠️ 注意:JDK 自带的 FTP 支持有限,建议使用第三方库如 Apache Commons VFS 或 JSch(用于 SFTP)。

Spring Boot 中访问远程资源(推荐)

如果你在 Spring Boot 项目中,可以使用 RestTemplateWebClient

使用 RestTemplate 获取远程文本:

import org.springframework.web.client.RestTemplate; public class SpringRemoteFetcher { public static void main (String[] args) { RestTemplate restTemplate = new RestTemplate (); String result = restTemplate.getForObject( "https://example.com/api/data" , String.class); System.out.println(result); } }

总查看远程资源小结

| |

场景 推荐方式
浏览器查看远程图片 直接输入 https://example.com/images/photo.jpg
Java 程序读取远程文件 使用 URL.openStream() 或 HttpURLConnection
下载远程文件到本地 使用 InputStream + FileOutputStream
需要身份验证的资源 添加请求头(如 Authorization )
FTP/SFTP 文件访问 使用 Apache Commons VFS 或 JSch
Spring Boot 项目 使用 RestTemplate 或 WebClient

九、总结

在IO流基础部分,主要是建立整个Java的IO流框架视野。四个顶级字节流、字符流读写接口,而入参则是与File文件相关,文件系统对文件/目录的处理是我们非常关心的,这样就关联起来了。

而读取时,发现单个字节读取非常慢;于是改为按桶(字节数组)来读取,减少IO次数,性能明显提高几十倍。一切文件都是字节,这是因为计算机底层是使用二进制进行存储的,1字节=8位,1位0或1,所以字节是根基。

那么又为什么有字符流呢?那是为了方便处理文本,否则容易读取不完整,需要计算字符的字节数。说到字符,那就得关联字符编码,这是一种将字符映射到字节存储的手段,所以每个系统都会有一些大大的字符编码表,形象理解为一个个Map,做映射使用。我们日常熟悉的utf-8字符集就是包含了世界任何文字、符号,这才能让计算机都认识,信息才能顺畅流通。每个字符集单个字符的字节数不一致,所以编码出来的文件大小也不一样。

来到IO流的高级部分,发现这些形形色色的流,都是以基础流为核心(入参),然后进行包装,强化某个方面的能力。比如,缓冲流,内置了一个8kb的缓冲区,瞬间将文件的读取性能提高一大截,而加上字节数组的读取,形成双桶(外+内)模式,性能更是超快,也是目前处理大文件的常用手段。后面包括,一些转换流、打印流,都是特定场合才会使用的流。

而在配置层面,Properties属性流,很适合读取项目的配置属性。当然,JDK一直在迭代,File从1.0就有,而发展到现在JDK21,针对其一些使用方法,进行了优化,推出了Path这样更友好,功能更强大的路径类,来替代File,跨平台处理更方便,同时新增Files工具类,针对文件、目录的处理更简单高效。

更多推荐