JavaSE-04-IO流[下](详细版)
JavaSE-04-IO流[下](详细版)
在IO流基础部分,主要学习了FileXX的文件字节流/字符流 ,可以简单进行文件的读写操作,但是能否更高效读写,或者完成其他一些高级功能呢?答案是可以的,在基础流的基础上建立包装即可。
IO流基础部分,请戳:JavaSE-04-IO流[上](详细版)
本文介绍IO流的进阶部分,学习更强大的流。比如能够高效读写的缓冲流,能够转换编码的转换流,能够持久化存储对象的序列化流等等。这些功能更为强大的流,都是在基本的流对象基础之上创建而来的,就像穿上铠甲的武士一样,相当于是对基本流对象的一种增强。
一、缓冲流
缓冲流,也叫高效流,是对4个基本的FileXxx 流的增强,所以也是4个流,按照数据类型分类:
- 字节缓冲流:
BufferedInputStream,BufferedOutputStream - 字符缓冲流:
BufferedReader,BufferedWriter
缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统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.亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。先帝在时,每与臣论此事,未尝不叹息痛恨于桓、灵也。侍中、尚书、长史、参军,此悉贞良死节之臣,愿陛下亲之信之,则汉室之隆,可计日而待也。
案例分析
-
- 逐行读取文本信息。
-
- 把读取到的文本存储到集合中
-
- 对集合中的文本进行排序
-
- 遍历集合,按顺序,写出文本信息。
案例实现
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编码。它使用一至四个字节为每个字符编码,编码规则:
-
- 128个US-ASCII字符,只需一个字节编码。
-
- 拉丁文等字符,需要二个字节编码。
-
- 大部分常用字(含中文),使用三个字节编码。
-
- 其他极少使用的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编码的文本文件。
案例分析
-
- 指定GBK编码的转换流,读取文本文件。
-
- 使用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);
序列化操作
-
- 一个对象要想序列化,必须满足两个条件:
-
该类必须实现
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); } }
练习:序列化集合
-
- 将存有多个自定义对象的集合序列化操作,保存到
list.txt文件中。
- 将存有多个自定义对象的集合序列化操作,保存到
-
- 反序列化
list.txt,并遍历集合,打印对象信息。
- 反序列化
案例分析
-
- 把若干学生对象 ,保存到集合中。
-
- 把集合序列化。
-
- 反序列化读取时,只需要读取一次,转换为集合类型。
-
- 遍历集合,可以打印所有的学生信息
案例实现
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 项目中,可以使用 RestTemplate 或 WebClient:
使用 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工具类,针对文件、目录的处理更简单高效。
更多推荐
所有评论(0)