vlambda博客
学习文章列表

JDK17 |java17学习 第 4 章 异常处理

Chapter 5: Strings, Input/Output,and Files

在本章中,您将更详细地了解 String 类方法。我们还将讨论标准库和 Apache Commons 项目中流行的字符串实用程序。下面将概述 Java 输入/输出流和 java.io 包的相关类,以及 org.apache 的一些类.commons.io 包。文件管理类及其方法在专门的部分中描述。完成本章后,您将能够使用标准 Java API 和 Apache Commons 实用程序编写处理字符串和文件的代码。

本章将涵盖以下主题:

  • 字符串处理
  • I/O 流
  • 文件管理
  • Apache Commons 的 FileUtilsIOUtils 实用程序

Technical requirements

为了能够执行本章提供的代码示例,您将需要以下内容:

  • 装有 Microsoft Windows、Apple macOS 或 Linux 操作系统的计算机
  • Java SE 版本 17 或更高版本
  • 您喜欢的 IDE 或代码编辑器

第 1 章Java 17 入门,在本书中。本章的代码示例文件可在 GitHub 存储库中获得,地址为 https:// /github.com/PacktPublishing/Learn-Java-17-Programming.gitexamples/src/main/java/com/packt/learnjava/ch05_stringsIoStreams 文件夹。

String processing

在主流的编程中,String大概是最流行的类了。在第 1 章中,< em class="italic">Java 17 入门,我们了解了这个类、它的字面量以及它的特定特性,即字符串不变性。在 本节中,我们将解释如何使用标准库中的 String 类方法和实用程序类来处理字符串,特别是 org.apache.commons.lang3 包中的 StringUtils 类。

Methods of the String class

String 类具有 70 多个方法,可以分析、修改和比较字符串,并将数字文字转换为相应的字符串文字。要查看 String 类的所有方法,请在 https://docs.oracle.com/en/java/javase

String analysis

length() 方法 返回 字符串中的字符数,如以下代码所示:

String s7 = "42";
System.out.println(s7.length());    //prints: 2
System.out.println("0 0".length()); //prints: 3

后面的 isEmpty() 方法返回 true 当字符串的长度(字符数)为 0

System.out.println("".isEmpty());   //prints: true
System.out.println(" ".isEmpty());  //prints: false

indexOf()lastIndexOf() 方法 返回 < a id="_idIndexMarker531">此代码片段中显示的字符串中的指定子字符串:

String s6 = "abc42t%";
System.out.println(s6.indexOf(s7));            //prints: 3
System.out.println(s6.indexOf("a"));           //prints: 0
System.out.println(s6.indexOf("xyz"));         //prints: -1
System.out.println("ababa".lastIndexOf("ba")); //prints: 3

可以看到,字符串中第一个字符的位置(索引)为0,没有指定的子字符串导致索引-1

matches() 方法将 正则表达式(作为参数传递)应用于字符串,如下所示:

System.out.println("abc".matches("[a-z]+"));   //prints: true
System.out.println("ab1".matches("[a-z]+"));   //prints: false

正则 表达式超出了本书的范围。您可以在 https://www.regular-expressions 了解 .info。在前面的示例中,[a-z]+ 表达式仅匹配一个或多个字母。

String comparison

第 3 章中,< em class="italic">Java Fundamentals,我们谈到了 仅当两个 String 对象或文字的拼写方式完全相同时才返回 true 的 equals() 方法。下面的代码片段演示了它是如何工作的:

String s1 = "abc";
String s2 = "abc";
String s3 = "acb";
System.out.println(s1.equals(s2));     //prints: true
System.out.println(s1.equals(s3));     //prints: false
System.out.println("abc".equals(s2));  //prints: true
System.out.println("abc".equals(s3));  //prints: false

另一个 String 类,equalsIgnoreCase() 方法做了类似的工作,但忽略了字符大小写的差异,如下所示:

String s4 = "aBc";
String s5 = "Abc";
System.out.println(s4.equals(s5));           //prints: false
System.out.println(s4.equalsIgnoreCase(s5)); //prints: true

contentEquals() 方法的作用类似于 equals() 方法,如下所示:

String s1 = "abc";
String s2 = "abc";
System.out.println(s1.contentEquals(s2));    //prints: true
System.out.println("abc".contentEquals(s2)); //prints: true 

区别equals() 方法检查两个值是否由 String 类表示,而 contentEquals() 只比较字符序列的字符(内容)。字符序列可以用StringStringBuilderStringBuffer、< code class="literal">CharBuffer,或任何其他实现 CharSequence 接口的类。然而,如果两个序列包含相同的字符,contentEquals() 方法将返回 true,而 String 类创建的,">equals() 方法将返回 false

如果 string 包含某个子字符串,则 contains() 方法返回 true , 如下:

String s6 = "abc42t%";
String s7 = "42";
String s8 = "xyz";
System.out.println(s6.contains(s7));    //prints: true
System.out.println(s6.contains(s8));    //prints: false

startsWith()endsWith() 方法执行类似的检查,但仅在字符串的开头或结尾字符串值,如下代码所示:

String s6 = "abc42t%";
String s7 = "42";
System.out.println(s6.startsWith(s7));      //prints: false
System.out.println(s6.startsWith("ab"));    //prints: true
System.out.println(s6.startsWith("42", 3)); //prints: true
System.out.println(s6.endsWith(s7));        //prints: false
System.out.println(s6.endsWith("t%"));      //prints: true

compareTo()compareToIgnoreCase() 方法按字典顺序比较字符串 - 基于字符串中每个字符的 Unicode 值。如果字符串相等,则返回0,如果第一个字符串为 按字典顺序小于第二个字符串(具有较小的 Unicode 值),如果第一个字符串按字典顺序大于第二个字符串(具有较大的 Unicode 值),则为正整数值,如下所示:

String s4 = "aBc";
String s5 = "Abc";
System.out.println(s4.compareTo(s5));             //prints: 32
System.out.println(s4.compareToIgnoreCase(s5));   //prints: 0
System.out.println(s4.codePointAt(0));            //prints: 97
System.out.println(s5.codePointAt(0));            //prints: 65

从这个代码片段中,您可以看到 compareTo()compareToIgnoreCase() 方法是基于组成字符串的字符。 s4 字符串比 s5 字符串大 32 的原因是因为 a 字符 (97) 的代码点大于 A 字符(65),由 32 编写。

给定的示例还表明 codePointAt() 方法返回位于字符串中指定位置的 字符的代码点. Integral types 部分中描述了代码点book/programming/9781803241432/2" linkend="ch02lvl1sec02">第 1 章Java 17 入门 .

String transformation

substring() 方法 返回子字符串,从指定位置开始 (索引),如下:

System.out.println("42".substring(0));   //prints: 42
System.out.println("42".substring(1));   //prints: 2
System.out.println("42".substring(2));   //prints:
System.out.println("42".substring(3)); 
                                //error: index out of range: -1
String s6 = "abc42t%";
System.out.println(s6.substring(3));     //prints: 42t%
System.out.println(s6.substring(3, 5));  //prints: 42

format() 方法 使用传入的第一个参数作为模板,并将其他参数插入到模板顺序。下面的代码示例打印句子 Hey, Nick!请给我 2 个苹果! 三遍:

String t = "Hey, %s! Give me %d apples, please!";
System.out.println(String.format(t, "Nick", 2));
String t1 = String.format(t, "Nick", 2);
System.out.println(t1);
System.out.println(String.format("Hey, 
                  %s! Give me %d apples, please!", "Nick", 2));

%s%d 符号称为 格式说明符。有许多说明符和各种标志允许程序员对结果进行精细控制。您可以在 java.util.Formatter 类的 API 中了解它们。

concat() 方法 算术运算符 ( +),如下所示:

String s7 = "42";
String s8 = "xyz";
String newStr1 = s7.concat(s8);
System.out.println(newStr1);    //prints: 42xyz
String newStr2 = s7 + s8;
System.out.println(newStr2);    //prints: 42xyz

join() 方法的作用类似,但允许添加分隔符:

String newStr1 = String.join(",", "abc", "xyz");
System.out.println(newStr1);        //prints: abc,xyz
List<String> list = List.of("abc","xyz");
String newStr2 = String.join(",", list);
System.out.println(newStr2);        //prints: abc,xyz

下面一组replace()replaceFirst()replaceAll()< /code> methods 用提供的字符替换字符串中的某些字符:

System.out.println("abcbc".replace("bc","42"));
                                                //prints: a4242
System.out.println("abcbc".replaceFirst("bc", "42"));
                                                //prints: a42bc
System.out.println("ab11bcd".replaceAll("[a-z]+", "42"));
                                               //prints: 421142

上述代码的第一行将 "bc" 的所有实例替换为 "42"。第二个仅用 "42" 替换 "bc" 的第一个实例,最后一个替换所有子字符串 将提供的正则表达式与 "42" 匹配。

toLowerCase()toUpperCase() 方法 改变大小写整个 字符串,如下所示:

System.out.println("aBc".toLowerCase());   //prints: abc
System.out.println("aBc".toUpperCase());   //prints: ABC

split() 方法将字符串分解为子字符串,使用提供的字符作为分隔符,如下所示:

String[] arr = "abcbc".split("b");
System.out.println(arr[0]);   //prints: a
System.out.println(arr[1]);   //prints: c
System.out.println(arr[2]);   //prints: c

有几个 valueOf() 方法可以将原始类型的值转换为 String 类型,例如 如下:

float f = 23.42f;
String sf = String.valueOf(f);
System.out.println(sf);         //prints: 23.42

还有()getChars() 方法可以转换一个string 到对应类型的数组,而 chars() 方法 创建一个 IntStream 个字符(它们的代码 点)。我们将在 第 14 章中讨论流/a>,Java 标准流

Methods added with Java 11

Java 11 在 String 类中引入了几个新方法。

repeat() 方法允许您基于同一字符串的多个连接创建一个新的 String 值,如以下代码:

System.out.println("ab".repeat(3)); //prints: ababab
System.out.println("ab".repeat(1)); //prints: ab
System.out.println("ab".repeat(0)); //prints:

isBlank() 方法返回 true 如果字符串的长度为 0 或仅由空格组成,例如:

System.out.println("".isBlank());     //prints: true
System.out.println("   ".isBlank());  //prints: true
System.out.println(" a ".isBlank());  //prints: false

stripLeading() 方法从字符串中删除前导空格,stripTrailing() 方法删除尾随空格,< code class="literal">strip() 方法将两者都删除,如下所示:

String sp = "   abc   ";
System.out.println("'" + sp + "'");                 
                                          //prints: '   abc   '
System.out.println("'" + sp.stripLeading() + "'");  
                                             //prints: 'abc   '
System.out.println("'" + sp.stripTrailing() + "'"); 
                                              //prints: '  abc'
System.out.println("'" + sp.strip() + "'");
                                                //prints: 'abc'

最后,lines() 方法按行终止符分隔字符串并返回结果行的 Stream<String>。行终止符是转义序列换行符 (\n) (\u000a)、回车符 (\r) (\u000d),或回车后紧跟换行符 (\r\ n) (\u000d\u000a),例如:

String line = "Line 1\nLine 2\rLine 3\r\nLine 4";
line.lines().forEach(System.out::println); 

上述代码的输出如下:

JDK17 |java17学习 第 4 章 异常处理

我们将讨论 第 14 章Java 标准流

String utilities

除了 String 类之外,还有许多其他类具有处理 的方法字符串 值。其中最有用的是来自项目 org.apache.commons.lang3 包的 StringUtils 类_idIndexMarker563">称为Apache Commons,由程序员的开源社区维护,称为Apache 软件基金会。我们将在 第 7 章中详细讨论这个项目及其库Java 标准和外部库。要在您的项目中使用它,请在 pom.xml 文件中添加以下依赖项:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.8.1</version>
</dependency>

StringUtils 类是许多程序员的最爱。它通过提供以下 null 安全操作来补充 String 类的方法(当方法以这种方式实现时 - 例如通过检查 null 的值 - 它不抛出 NullPointerException):

  • isBlank(CharSequence cs):如果输入值为空格,则返回true,为空( ""),或 null
  • isNotBlank(CharSequence cs):当上述方法返回true时返回false
  • isEmpty(CharSequence cs):如果输入值为空(""< /code>) 或 null
  • isNotEmpty(CharSequence cs):当上述方法返回true时返回false
  • trim(String str):从输入值中删除前导和尾随空格并处理 null、空("")和空格,如下:
    System.out.println("'" + StringUtils.trim(" x ")+"'");                                              //打印:'x' System.out.println(StringUtils.trim(null));                                             //prints: null System.out.println("'" + StringUtils.trim("") + "'");                                               //打印:'' System.out.println("'" + StringUtils.trim("   ")+"'");                                               //打印:'' 
  • trimToNull(String str):从输入值中删除前导和尾随空格并处理null,空("") 和空格,如下:
    System.out.println("'"+StringUtils.trimToNull(" x ")+"'");                                               //打印:'x' System.out.println(StringUtils.trimToNull(null));                                             //prints: null System.out.println(StringUtils.trimToNull(""));                                             //prints: null System.out.println(StringUtils.trimToNull("   "));                                             //prints: null
  • trimToEmpty(String str):从 输入值中删除前导和尾随空格并处理 null、空("")和空格,如下:
    System.out.println("'" +           StringUtils.trimToEmpty("x") + "'");   //'x' System.out.println("'" +           StringUtils.trimToEmpty(null) + "'");    //'' System.out.println("'" +           StringUtils.trimToEmpty("") + "'");      //'' System.out.println("'" +           StringUtils.trimToEmpty("   ") + "'");   //''< /code>
  • strip(String str)stripToNull(String str)stripToEmpty(String str) :产生与前面的trim(String str)trimToNull(String str)和< code class="literal">trimToEmpty(String str) 方法,但使用更广泛的空白定义(基于 Character.isWhitespace(int codepoint))和因此删除与 trim(String str)trimToNull(String str)trimToEmpty(String str) 方法,等等
  • strip(String str, String stripChars), stripAccents(String input), stripAll(String ... strs), stripAll(String[] strs, String stripChars), stripEnd(String str, String stripChars)< /code> 和 stripStart(String str, String stripChars):从 StringString[] 数组元素
  • startsWith(CharSequence str, CharSequence prefix), startsWithAny(CharSequence string, CharSequence... searchStrings), startsWithIgnoreCase(CharSequence str, CharSequence prefix) 和类似的 endsWith*() 方法:检查 String value 以某个前缀(或后缀)开始(或结束)
  • indexOf, lastIndexOf, contains:检查空安全中的索引方式
  • indexOfAny, lastIndexOfAny, indexOfAnyBut, lastIndexOfAnyBut:返回一个索引
  • containsOnlycontainsNonecontainsAny:检查值是否包含某些字符或不
  • substringleftrightmid:以空安全方式返回子字符串
  • substringBefore, substringAfter, substringBetween:从相对位置返回一个子字符串
  • splitjoin:拆分或连接一个值(分别)
  • removedelete:消除子串
  • replaceoverlay:替换一个值
  • chompchop:去掉结尾
  • appendIfMissing:如果不存在则添加一个值
  • prependIfMissing:如果不存在,则在 String 值的开头添加前缀
  • leftPadrightPadcenterrepeat:添加填充
  • upperCase, lowerCase, swapCase, 大写uncapitalize:改变大小写
  • countMatches:返回子串出现的次数
  • isWhitespace, isAsciiPrintable, isNumeric, isNumericSpaceisAlphaisAlphaNumericisAlphaSpace和< code class="literal">isAlphaNumericSpace:检查某类字符是否存在
  • isAllLowerCaseisAllUpperCase:检查大小写
  • defaultStringdefaultIfBlankdefaultIfEmpty:如果 null
  • rotate:使用循环移位旋转字符
  • reversereverseDelimited:反转字符或分隔字符组
  • abbreviateabbreviateMiddle:使用省略号或其他值缩写一个值
  • difference:返回值的差异
  • getLevenshteinDistance:返回将一个值转换为另一个值所需的更改次数

如您所见,StringUtils 类有一组非常丰富的(我们没有列出所有内容)用于字符串分析、比较和转换的方法,这些方法补充了 String 类的方法。

I/O streams

任何软件 系统都必须接收和生成某种数据,这些数据可以组织为一组隔离的输入/输出或数据流。流可以是有限的或无穷无尽的。程序可以从流(称为输入流)中读取或写入流(称为输出流)。 Java I/O 流是基于字节或基于字符的,这意味着它的数据被解释为原始字节或字符。

java.io 包包含支持许多(但不是全部)可能的数据源的类。它主要围绕文件、网络流和内部内存缓冲区的输入和输入而构建。它不包含网络通信所需的许多类。它们属于 java.netjavax.net 和其他 Java 网络 API 包。只有在建立网络源或目标(例如网络套接字)之后,程序才能使用 InputStreamOutputStream< java.io 包的 /code> 类。

java.nio 包的类与 java.io 包的类具有几乎相同的功能。但是,此外,它们可以在 非阻塞 模式下工作,这可以在某些情况下显着提高性能。我们将在 第 15 章 反应式编程

Stream data

输入 数据必须是二进制的——用 0 和 1 表示——至少因为这是计算机可以读取的格式。数据可以一次读取或写入一个字节,也可以一次读取或写入多个字节的数组。这些字节可以保持二进制,也可以解释为字符。

在第一种情况下,它们可以被 InputStreamOutputStream 类的后代读取为字节或字节数组,例如(如果类属于 java.io 包,则省略包名) ByteArrayInputStream, ByteArrayOutputStream, FileInputStream, FileOutputStream, ObjectInputStream, ObjectOutputStreamjavax.sound.sampled.AudioInputStreamorg.omg.CORBA.portable.OutputStream ;您使用哪一种取决于数据的来源或目的地。 InputStreamOutputStream 类本身是抽象的,无法实例化。

第二种情况,可以解释为字符的数据称为文本数据,基于Reader有面向字符的读写类Writer 也是抽象类。它们的子类的示例是 CharArrayReaderCharArrayWriterInputStreamReader 和 < code class="literal">OutputStreamWriter、PipedReaderPipedWriter StringReaderStringWriter

您可能已经注意到我们成对列出了这些类。但并不是每个输入类都有匹配的输出特化——例如,有 PrintStreamPrintWriter 类支持输出到打印设备,但没有对应的输入伙伴,至少没有名称。但是,有一个 java.util.Scanner 类可以解析已知格式的输入文本。

还有一组配备缓冲区的类,通过一次读取或写入更大的数据块来帮助提高性能,尤其是在访问源或 目标的情况下需要很长的时间。

在本节的其余部分,我们将回顾 java.io 包的类和其他包中一些流行的相关类。

The InputStream class and its subclasses

Java 类库中,InputStream 抽象 类有以下直接实现:ByteArrayInputStream, FileInputStream, ObjectInputStreamPipedInputStreamSequenceInputStreamFilterInputStream和<代码类="literal">javax.sound.sampled.AudioInputStream。

所有这些都可以按原样使用或覆盖 InputStream 类的以下方法:

  • int available():返回可供读取的字节数
  • void close():关闭流并释放资源
  • void mark(int readlimit):标记流中的一个位置,定义可以读取多少字节
  • boolean markSupported():如果支持标记,则返回 true
  • static InputStream nullInputStream():创建一个空流
  • abstract int read():读取流中的下一个字节
  • int read(byte[] b):从流中读取数据到b缓冲区
  • int read(byte[] b, int off, int len):从流中读取 len 或更少字节到 < code class="literal">b 缓冲区
  • byte[] readAllBytes():从流中读取所有剩余的字节
  • int readNBytes(byte[] b, int off, int len):将 len 或更少字节读入 off 偏移处的 "literal">b 缓冲区
  • byte[] readNBytes(int len):将 len 或更少的字节读入 b< /代码>缓冲区
  • void reset():将读取位置重置为上次调用mark()方法的位置
  • long skip(long n):跳过n或更少字节的流;返回实际跳过的字节数
  • long transferTo(OutputStream out):从输入流中读取,逐字节写入提供的输出流;返回实际传输的字节数

abstract int read() 是唯一必须实现的方法,但是这个类的大多数后代 覆盖了许多其他方法 也是。

ByteArrayInputStream

ByteArrayInputStream允许 读取字节数组作为输入流。它具有以下两个构造函数,它们创建类的对象并定义用于读取字节输入流的缓冲区:

  • ByteArrayInputStream(byte[] buffer)
  • ByteArrayInputStream(byte[] buffer, int offset, int length)

除了缓冲区之外,第二个构造函数还允许您设置缓冲区的偏移量和长度。让我们看一个例子,看看如何使用这个类。我们假设有一个包含数据的 byte[] 数组的来源:

byte[] bytesSource(){
    return new byte[]{42, 43, 44};
}

然后,我们可以编写以下内容:

byte[] buffer = bytesSource();
try(ByteArrayInputStream bais = new ByteArrayInputStream(buffer)){
    int data = bais.read();
    while(data != -1) {
        System.out.print(data + " ");   //prints: 42 43 44
        data = bais.read();
    }
} catch (Exception ex){
    ex.printStackTrace();
}

bytesSource() 方法生成填充缓冲区的字节数组,该数组作为 ByteArrayInputStream 类的构造函数传递给范围。然后使用 read() 方法逐字节读取结果流,直到到达流的末尾(并且 read()< /code> 方法返回 -1)。每个新字节都被打印出来(没有换行符,后面有空格,所以所有读取的字节都显示在一行中,由空格分隔)。

前面的代码通常用更紧凑的形式表示,如下:

byte[] buffer = bytesSource();
try(ByteArrayInputStream bais = new ByteArrayInputStream(buffer)){
    int data;
    while ((data = bais.read()) != -1) {
        System.out.print(data + " ");   //prints: 42 43 44
    }
} catch (Exception ex){
    ex.printStackTrace();
}

不仅可以打印字节,还可以以任何其他必要的方式处理它们,包括将它们解释为字符,例如:

byte[] buffer = bytesSource();
try(ByteArrayInputStream bais = 
                             new ByteArrayInputStream(buffer)){
  int data;
  while ((data = bais.read()) != -1) {
      System.out.print(((char)data) + " ");   //prints: * + ,
  }
} catch (Exception ex){
    ex.printStackTrace();
}

但是,在这种情况下,最好使用专门用于字符 处理的 Reader 类之一。我们将在 Reader 和 writer 类及其子类 部分中讨论

FileInputStream

FileInputStream从文件系统中的文件中获取数据 - 原始例如,图像的字节数。它具有以下三个构造函数:

  • FileInputStream(文件文件)
  • FileInputStream(String name)
  • FileInputStream(FileDescriptor fdObj)

每个构造函数都会打开指定为参数的文件。第一个构造函数接受 File 对象;第二,文件系统中文件的路径;第三,文件描述符对象,表示与文件系统中实际文件的现有连接。让我们看下面的例子:

String file =  classLoader.getResource("hello.txt").getFile();
try(FileInputStream fis = new FileInputStream(file)){
    int data;
    while ((data = fis.read()) != -1) {
        System.out.print(((char)data) + " ");   
                                          //prints: H e l l o !
    }
} catch (Exception ex){
    ex.printStackTrace();
}

src/main/resources 文件夹中,我们创建了 hello.txt 文件,其中只有一行 - <代码类="literal">你好!。上述示例的输出如下所示:

JDK17 |java17学习 第 4 章 异常处理

hello.txt 文件中读取字节后,出于演示目的,我们决定将每个 byte 转换为 char 这样你就可以看到代码确实是从指定的文件中读取的,但是 FileReader 类是文本文件处理的更好选择(我们稍后会讨论)。如果没有演员表,结果将如下:

System.out.print((data) + " ");   
                                //prints: 72 101 108 108 111 33

顺便说一句,因为 src/main/resources 文件夹是由 IDE(使用 Maven)放置在类路径上的,所以放置在其中的文件也可以通过类加载器访问使用其 自己的 InputStream 实现创建一个 流:

try(InputStream is = InputOutputStream.class.getResourceAsStream("/hello.txt")){
    int data;
    while ((data = is.read()) != -1) {
        System.out.print((data) + " ");   
             //prints: 72 101 108 108 111 33
    }
} catch (Exception ex){
    ex.printStackTrace();
}

前面示例中的 InputOutputStream 类不是某个库中的类。它只是我们用来运行示例的主类。 InputOutputStream.class.getResourceAsStream() 构造允许您使用已加载 InputOutputStream 类的相同类加载器< a id="_idIndexMarker592"> 在类路径 上查找文件并创建包含其内容的流。在文件管理部分,我们还将介绍其他读取文件的方法。

ObjectInputStream

ObjectInputStream方法集合比集合大很多任何其他 InputStream 实现的方法。原因是它是围绕读取可以是各种类型的对象字段的值而构建的。为了使 ObjectInputStream 能够从输入的数据流构造一个对象,该对象必须是 deserializable,这意味着它首先必须是serializable的——也就是说,可以转换成字节流。通常,这样做是为了通过网络传输对象。在目的地,序列化的对象被反序列化,并恢复原始对象的值。

原始类型和大多数 Java 类,包括 String 类和原始类型包装器,都是可序列化的。如果一个类具有自定义类型的字段,则必须通过实现 java.io.Serizalizable 使其可序列化。如何做到这一点超出了本书的范围。现在,我们将只使用可序列化的类型。让我们看看这个类:

class SomeClass implements Serializable {
    private int field1 = 42;
    private String field2 = "abc";
}

我们必须告诉编译器它是可序列化的。否则,编译将失败。这样做是为了确保在声明类是可序列化的之前,程序员要么检查所有字段并确保它们是可序列化的,要么已经实现了序列化所需的方法。

在我们可以创建输入流并使用 ObjectInputStream 进行反序列化之前,我们需要先序列化对象。这就是为什么我们首先使用 ObjectOutputStreamFileOutputStream 来序列化一个对象并将其写入 someClass.bin 文件。我们将在 OutputStream 类及其子类 部分详细讨论它们。然后,我们使用 FileInputStream 从文件中读取并使用 ObjectInputStream 反序列化文件内容:

String fileName = "someClass.bin";
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName));
     ObjectInputStream ois = new ObjectInputStream(new 
                             FileInputStream(fileName))){
    SomeClass obj = new SomeClass();
    oos.writeObject(obj);
    SomeClass objRead = (SomeClass) ois.readObject();
    System.out.println(objRead.field1);  //prints: 42
    System.out.println(objRead.field2);  //prints: abc
} catch (Exception ex){
    ex.printStackTrace();
}

请注意,必须先创建 文件,然后才能运行上述代码。我们将在创建文件和目录部分展示它是如何完成的。并且,提醒一下,我们使用了 try-with-resources 语句,因为 InputStreamOutputStream 都实现了 Closeable 接口。

PipedInputStream

管道输入 流具有非常特殊的 特化;它被用作线程之间的通信机制之一。一个线程从 PipedInputStream 对象读取数据并将数据传递给另一个线程,该线程将数据写入 PipedOutputStream 对象。这是一个例子:

PipedInputStream pis = new PipedInputStream();
PipedOutputStream pos = new PipedOutputStream(pis);

或者,当一个线程从 PipedOutputStream 对象读取数据而另一个线程写入 PipedInputStream 对象时,数据可以向相反的方向移动如下:

PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos);

在这方面工作的人都熟悉消息,Broken pipe,这意味着提供数据管道流已停止工作。

管道 流也可以在没有任何连接的情况下创建并稍后连接,如下所示:

PipedInputStream pis = new PipedInputStream();
PipedOutputStream pos = new PipedOutputStream();
pos.connect(pis); 

例如,这里有两个类将由不同的线程执行——首先是 PipedOutputWorker 类,如下所示:

class PipedOutputWorker implements Runnable{
    private PipedOutputStream pos;
    public PipedOutputWorker(PipedOutputStream pos) {
        this.pos = pos;
    }
    @Override
    public void run() {
        try {
            for(int i = 1; i < 4; i++){
                pos.write(i);
            }
            pos.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

PipedOutputWorker 类具有 run() 方法(因为它实现了 Runnable interface) 写入 流三个数字 1, < code class="literal">2 和 3,然后关闭。现在,让我们看看 PipedInputWorker 类,如下所示:

class PipedInputWorker implements Runnable{
    private PipedInputStream pis;
    public PipedInputWorker(PipedInputStream pis) {
        this.pis = pis;
    }
    @Override
    public void run() {
        try {
            int i;
            while((i = pis.read()) > -1){
                System.out.print(i + " ");  
            }
            pis.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

它还有一个 run() 方法(因为它实现了一个 Runnable 接口)从流中读取并打印出每个字节直到流结束(由 -1 指示)。现在,让我们连接这些 管道并 执行一个 run() 方法这些类:

PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream();
try {
    pos.connect(pis);
    new Thread(new PipedOutputWorker(pos)).start();
    new Thread(new PipedInputWorker(pis)).start(); 
                                                //prints: 1 2 3
} catch (Exception ex) {
    ex.printStackTrace();
}

如您所见,worker 的对象被传递到 Thread 类的构造函数中。 Thread 对象的 start() 方法执行 run() Runnable 传入的方法。我们看到了预期的结果——PipedInputWorker 打印出由 PipedOutputWorker 写入管道流的所有字节。我们将在 第 8 章 多线程和并发处理

SequenceInputStream

SequenceInputStream 连接 输入流作为以下构造函数之一参数:

  • SequenceInputStream(InputStream s1, InputStream s2)
  • SequenceInputStream(Enumeration e)

枚举是集合尖括号中指示的类型的对象,称为泛型,意思是类型为TSequenceInputStream 类从第一个输入字符串读取直到它结束,然后它从第二个输入字符串读取,依此类推,直到最后一个流结束。例如,让我们在 howAreYou.txt 文件(带有文本,How are you?hello.txt 文件旁边的 ="literal">resources 文件夹。 SequenceInputStream 类可以如下使用:

String file1 = classLoader.getResource("hello.txt").getFile();
String file2 = classLoader.getResource("howAreYou.txt").getFile();
try(FileInputStream fis1 = 
                    new FileInputStream(file1);
    FileInputStream fis2 = 
                    new FileInputStream(file2);
    SequenceInputStream sis=
            new SequenceInputStream(fis1, fis2)){
    int i;
    while((i = sis.read()) > -1){
        System.out.print((char)i);       
                                   //prints: Hello!How are you?
    }
} catch (Exception ex) {
    ex.printStackTrace();
}

类似地,当传入输入流的枚举时,会读取每个流(在我们的例子中,打印)直到结束。

FilterInputStream

FilterInputStream 包装器">InputStream 对象作为构造函数中的参数传递。下面是 FilterInputStream 类的构造函数和两个 read() 方法:

protected volatile InputStream in;
protected FilterInputStream(InputStream in) { this.in = in; }
public int read() throws IOException { return in.read(); }
public int read(byte b[]) throws IOException { 
    return read(b, 0, b.length);
}

InputStream 类的所有其他方法都被类似地覆盖;该函数被委托给分配给 in 属性的对象。

如您所见,构造函数是受保护的,这意味着只有孩子可以访问它。这样的设计对客户端隐藏了流的实际来源,并迫使程序员使用 FilterInputStream 类扩展之一:BufferedInputStream, CheckedInputStream, DataInputStream, PushbackInputStream, javax.crypto.CipherInputStream, java.util.zip.DeflaterInputStream, java.util.zip.InflaterInputStreamjava.security.DigestInputStreamjavax.swing.ProgressMonitorInputStream。或者,您可以创建自定义扩展。但是,在创建自己的扩展之前,请查看列出的类,看看其中一个是否符合您的需求。下面是一个使用 BufferedInputStream 类的例子:

String file = classLoader.getResource("hello.txt").getFile();
try(FileInputStream  fis = new FileInputStream(file);
    FilterInputStream filter = new BufferedInputStream(fis)){
    int i;
    while((i = filter.read()) > -1){
        System.out.print((char)i);     //prints: Hello!
    }
} catch (Exception ex) {
    ex.printStackTrace();
}

BufferedInputStream 类使用缓冲区来提高性能。当流中的字节被跳过或读取时,内部缓冲区会自动从包含的输入流中自动重新填充所需的字节数。

CheckedInputStream 类添加正在读取的数据的校验和,允许使用 getChecksum() 方法。

DataInputStream 类以与机器无关的方式读取输入数据并将其解释为原始 Java 数据类型。

PushbackInputStream 添加了使用unread() 方法。它在代码具有分析刚刚读取的数据并决定不读取它的逻辑的情况下很有用,因此可以在下一步重新读取它。

javax.crypto.CipherInputStream 类将 Cipher 添加到 read()方法。如果 Cipher 被初始化用于解密,javax.crypto.CipherInputStream 将在返回之前尝试解密数据。

java.util.zip.DeflaterInputStream 类以 deflate 压缩格式压缩数据。

java.util.zip.InflaterInputStream 类以 deflate 压缩格式解压缩数据。

java.security.DigestInputStream 类使用通过流的位更新关联的消息摘要。 on (boolean on) 方法打开或关闭 digest 函数。可以使用 getMessageDigest() 方法检索计算出的摘要。

javax.swing.ProgressMonitorInputStream 类提供从 InputStream< 读取的进度 的监视器/代码>。 监控对象可以使用 getProgressMonitor() 方法访问。

javax.sound.sampled.AudioInputStream

AudioInputStream代表 具有指定音频格式和长度的输入流.它有以下两个构造函数:

  • AudioInputStream (InputStream stream, AudioFormat format, long length):接受音频数据的流,请求的格式,以样本帧为单位的长度
  • AudioInputStream (TargetDataLine line):接受指示的目标数据行

javax.sound.sampled.AudioFormat 类描述音频格式属性,例如通道、编码、帧速率等。 javax.sound.sampled.TargetDataLine 类有 open() 方法以指定格式打开行,< code class="literal">read() 方法从数据线的输入缓冲区读取音频数据。

还有 javax.sound.sampled.AudioSystem 类,其方法处理 AudioInputStream 对象。它们可用于读取音频文件、流或 URL,并写入音频文件。它们还可用于将音频流转换为另一种音频格式。

The OutputStream class and its subclasses

OutputStream 类是 InputStream 类的对等点 表示 写入数据而不是读取数据。它是一个抽象类,在 Java 类库 (JCL ): ByteArrayOutputStream, FilterOutputStream, ObjectOutputStream, PipedOutputStreamFileOutputStream

FileOutputStream 类具有以下直接扩展:BufferedOutputStreamCheckedOutputStream、< code class="literal">DataOutputStream, PrintStream, javax.crypto.CipherOutputStream, java.util.zip.DeflaterOutputStreamjava.security.DigestOutputStreamjava.util.zip.InflaterOutputStream< /代码>。

所有这些都可以按原样使用或覆盖 OutputStream 类的以下方法:

  • void close():关闭流并释放资源
  • void flush():强制写出剩余字节
  • static OutputStream nullOutputStream():创建一个不写任何内容的新 OutputStream
  • void write(byte[] b):将提供的字节数组写入输出流
  • void write(byte[] b, int off, int len):写入提供的字节数组的 len 个字节,从off 偏移量,到输出流
  • abstract void write(int b):将提供的字节写入输出流

唯一需要实现的方法是abstract void write(int b),但大部分是OutputStream类的后代也覆盖许多其他方法。

InputStream类及其子类部分了解了输入流之后,所有OutputStream的实现,除了PrintStream 类, 你应该直观地熟悉。所以,我们将在这里只讨论PrintStream 类。

PrintStream

PrintStream 向另一个输出 流添加了将数据打印为的能力人物。我们实际上已经使用过很多次了。 System 类有一个 PrintStream 类的对象设置为 System.out 公共静态属性。这意味着每次我们使用 System.out 打印一些东西时,我们都在使用 PrintStream 类:

System.out.println("Printing a line");

让我们看另一个 PrintStream 类用法的例子:

String fileName = "output.txt";
try(FileOutputStream  fos = new FileOutputStream(fileName);
    PrintStream ps = new PrintStream(fos)){
    ps.println("Hi there!");
} catch (Exception ex) {
    ex.printStackTrace();
}

如您所见,PrintStream 类采用 FileOutputStream 对象并打印由它生成的字符。在这种情况下,它会打印出 FileOutputStream 写入文件的所有字节。顺便说一句,不需要显式创建目标文件。如果不存在,它将在 FileOutputStream 构造函数中自动创建。如果我们在前面的代码运行后打开文件,我们会在其中看到一行——"Hi there!"

或者,可以使用另一个采用 File 对象的 PrintStream 构造函数来实现相同的结果,如下所示:

String fileName = "output.txt";
File file = new File(fileName);
try(PrintStream ps = new PrintStream(file)){
    ps.println("Hi there!");
} catch (Exception ex) {
    ex.printStackTrace();
}

使用将文件名作为参数的 PrintStream 构造函数的第三个变体可以创建更简单的解决方案:

String fileName = "output.txt";
try(PrintStream ps = new PrintStream(fileName)){
    ps.println("Hi there!");
} catch (Exception ex) {
    ex.printStackTrace();
}

前面的 两个例子是可能的,因为 PrintStream 构造函数使用 FileOutputStream 类在幕后,就像我们在 PrintStream 类用法的第一个示例中所做的那样。因此,PrintStream 类有几个构造函数只是为了方便,但它们本质上都具有相同的功能:

  • PrintStream(文件文件)
  • PrintStream(File file, String csn)
  • PrintStream(File file, Charset charset)
  • PrintStream(String fileName)
  • PrintStream(String fileName, String csn)
  • PrintStream(String fileName, Charset charset)
  • PrintStream(OutputStream out)
  • PrintStream(OutputStream out, boolean autoFlush)
  • PrintStream(OutputStream out, boolean autoFlush, String encoding)
  • PrintStream(OutputStream out, boolean autoFlush, Charset charset)

一些构造函数还采用 Charset 实例或只是其名称 (String csn),这允许您应用不同的映射在 16 位 Unicode 代码单元序列和字节序列之间。您只需将它们打印出来即可查看所有可用的字符集,如下所示:

for (String chs : Charset.availableCharsets().keySet()) {
    System.out.println(chs);
}

其他构造函数将 boolean autoFlush 作为参数。此参数表示(当 true 时)当数组被 写入 或遇到行尾符号。

一旦创建了 PrintStream 的对象,它就会提供各种方法,如下所示:

  • void print(T value):打印传入的任何 T 原始类型的值,而不移动到另一行
  • void print(Object obj):在传入的对象上调用toString()方法并打印结果而不移动到另一个线;如果传入的对象为 null 并打印 null,则不会生成 NullPointerException代码>改为
  • void println(T value):打印传入的任何T原始类型的值并移至另一行
  • void println(Object obj):对传入的对象调用toString()方法,打印结果,移动到另一条线;如果传入的对象为 null 并打印 null,则不会生成 NullPointerException代码>改为
  • void println():只是移动到另一行
  • PrintStream printf(String format, Object... values):将提供的format字符串中的占位符替换为提供的values 并将结果写入流
  • PrintStream printf(Locale l, String format, Object... args):与前面的方法相同,但使用提供的Local< /代码>对象;如果提供的 Local 对象为 null,则不应用本地化,并且 this 方法的行为与前面的完全一样
  • PrintStream format(String format, Object... args)PrintStream format(Locale l, String format, Object... args) code>:与 PrintStream printf(String format, Object... values)PrintStream printf(Locale l, String format, Object...args)(已经在列表中描述过),例如:
    System.out.printf("Hi, %s!%n", "亲爱的读者");                                 //打印:嗨,亲爱的读者! System.out.format("嗨,%s!%n", "亲爱的读者");                                 //打印:嗨,亲爱的读者!< /上一个> 

在前面的示例中,(%) 表示格式规则。以下符号 (s) 表示 String 值。此位置的其他可能符号可以是 (d)(十进制)、(f)(浮点)等上。符号 (n) 表示换行符(与 (\n) 转义字符相同)。有许多格式规则。所有这些都在 java.util.Formatter 类的文档中进行了描述。

  • PrintStream append(char c)PrintStream append(CharSequence c)PrintStream append( CharSequence c, int start, int end):将提供的字符追加到流中,例如:
    System.out.printf("Hi %s", "there").append("!\n");                                          //打印:你好! System.out.printf("你好")                .append("一个!\n 两个", 4, 11);   ;                                        //打印:你好!

至此,我们结束对OutputStream子类的讨论,现在转向我们注意另一个类层次结构 - ReaderWriter 类及其来自 JCL 的子类。

The Reader and Writer classes and their subclasses

正如我们提到几次< /a>ReaderWriter 类的功能已经与 非常相似InputStreamOutputStream 类,但专门处理文本。它们将流字节解释为字符,并有自己独立的 InputStreamOutputStream 类层次结构。可以在没有 ReaderWriter 或其任何子类的情况下将流字节作为字符处理。我们已经在前面描述 InputStreamOutputStream 类的部分中看到了这样的示例。但是,使用 ReaderWriter 会使文本处理 更简单,代码更易阅读。

Reader and its subclasses

Reader 类是一个将流作为字符读取的抽象类。它是 InputStream 的模拟,具有以下方法:

  • abstract void close():关闭流和其他使用的资源
  • void mark(int readAheadLimit):标记流中的当前位置
  • boolean markSupported():如果流支持 mark(),则返回 true > 操作
  • static Reader nullReader():创建一个不读取字符的空Reader
  • int read():读取一个字符
  • int read(char[] buf):将字符读入提供的 buf 数组并返回读取字符数
  • abstract int read(char[] buf, int off, int len):将 len 个字符读入数组,从off 索引
  • int read(CharBuffer target):尝试将字符读入提供的 target 缓冲区
  • boolean ready():当流准备好被读取时返回 true
  • void reset():重置标记;但是,并非所有流都支持此操作,有些流支持它但不支持首先设置标记
  • long skip(long n):尝试跳过n个字符;返回跳过字符的计数
  • long transferTo(Writer out):从这个 reader 中读取所有字符,并将字符写入提供的 Writer 对象

如您所见,唯一需要实现的方法是两个抽象的 read()close() 方法。尽管如此,这个类的许多子类也覆盖了其他方法,有时是为了更好的性能或不同的功能。 JCL 中的 Reader 子类是 CharArrayReader, InputStreamReader, PipedReaderStringReaderBufferedReaderFilterReader< /代码>。 BufferedReader 类有一个 LineNumberReader 子类,以及 FilterReader 类有一个 PushbackReader 子类。

Writer and its subclasses

抽象的 Writer 类将 写入字符流。它是 OutputStream 的模拟,具有以下方法:

  • Writer append(char c):将提供的字符追加到流中
  • Writer append(CharSequence c):将提供的字符序列追加到流中
  • Writer append(CharSequence c, int start, int end):将提供的字符序列的子序列追加到流中
  • abstract void close():刷新和关闭流及相关系统资源
  • abstract void flush():刷新流
  • static Writer nullWriter():创建一个新的 Writer 对象,丢弃所有字符
  • void write(char[] c):写入 c 个字符的数组
  • abstract void write(char[] c, int off, int len):写入 len元素class="literal">c 个字符,从 off 索引开始
  • void write(int c):写入一个字符
  • void write(String str):写入提供的字符串
  • void write(String str, int off, int len):从提供的len长度的子串="literal">str 字符串,从 off 索引开始

可以看到,三个抽象方法,write(char[], int, int)flush()close(),必须由这个类的孩子实现。它们通常也覆盖其他方法。

JCL中的Writer子类CharArrayWriter, OutputStreamWriter, PipedWriter, StringWriter, BufferedWriterFilterWriterPrintWriterOutputStreamWriter 类有一个 FileWriter 子类。

Other classes of the java.io package

java.io 包的其他类 包括:

  • Console:允许与基于字符的控制台设备进行交互,与当前 Java 虚拟机实例相关联
  • StreamTokenizer:获取输入流并将其解析为 tokens
  • ObjectStreamClass:类的序列化描述符
  • ObjectStreamField:来自可序列化类的可序列化字段的描述
  • RandomAccessFile:允许随机访问以读取和写入文件,但它的讨论超出了本书的范围
  • File:允许创建和管理文件和目录;在文件管理部分中描述

Console

几种 方法可以创建和运行 Java 虚拟机(< strong class="bold">JVM) 执行应用程序的实例。如果 JVM 从命令行启动,则会自动打开一个控制台窗口。它允许您从键盘在显示屏上打字;但是,JVM 也可以由后台进程启动。在这种情况下,不会创建控制台。

要以编程方式检查控制台是否存在,您可以调用 System.console() 静态方法。如果没有可用的控制台设备,则调用该方法将返回 null。否则,它将返回允许与控制台设备和应用程序用户交互的 Console 类的对象。

让我们 创建 下面的 ConsoleDemo 类:

package com.packt.learnjava.ch05_stringsIoStreams;
import java.io.Console;
public class ConsoleDemo {
    public static void main(String... args)  {
        Console console = System.console();
        System.out.println(console);
    }
}

如果我们像往常一样从 IDE 运行它,结果将如下所示:

JDK17 |java17学习 第 4 章 异常处理

那是因为 JVM 不是从命令行启动的。为了做到这一点,让我们编译我们的应用程序并通过执行 mvn clean package Maven 命令创建一个 .jar 文件项目的根目录。 (我们假设您的计算机上安装了 Maven。)它将删除 target 文件夹,然后重新创建它,编译所有 .java< /code> 文件复制到 target 文件夹中对应的 .class 文件,然后将它们归档到 .jar 文件,learnjava-1.0-SNAPSHOT.jar

现在,我们可以使用以下命令从同一项目根目录启动 ConsoleDemo 应用程序:

java -cp ./target/examples-1.0-SNAPSHOT.jar  
          com.packt.learnjava.ch05_stringsIoStreams.ConsoleDemo

前面的 –cp 命令选项描述了一个类路径,所以在我们的例子中,我们告诉 JVM 在 .jar 文件夹目标中的文件。该命令显示为两行,因为页面宽度无法容纳它。但是,如果您想运行它,请确保将其作为一行执行。结果如下:

JDK17 |java17学习 第 4 章 异常处理

这告诉我们现在有了 Console 类对象。让我们看看我们能用它做什么。该类具有以下方法:

  • String readLine():等到用户按下 Enter 键并从控制台读取该行文本
  • String readLine(String format, Object... args):显示提示( 消息在提供的格式具有占位符用提供的参数替换 ),等到用户点击 Enter 键,然后从控制台读取文本行;如果没有提供任何参数(args),则将格式显示为提示
  • char[] readPassword():执行与 readLine() 函数相同的功能,但不回显键入的字符
  • char[] readPassword(String format, Object... args):执行与 readLine(String format, Object... args) 相同的功能) 但不回显输入的字符

要单独运行以下每个代码部分,您需要注释掉 main 方法中的 console1() 调用并取消注释console2()console3(),使用 mvn package 重新编译,以及然后重新运行之前显示的 java 命令。

让我们用下面的例子来演示前面的方法(console2() 方法):

Console console = System.console();
System.out.print("Enter something 1: "); 
String line = console.readLine();
System.out.println("Entered 1: " + line);
line = console.readLine("Enter something 2: ");
System.out.println("Entered 2: " + line);
line = console.readLine("Enter some%s", "thing 3: ");
System.out.println("Entered 3: " + line);
System.out.print("Enter password: ");
char[] password = console.readPassword();
System.out.println("Entered 4: " + new String(password));
password = console.readPassword("Enter password 5: ");
System.out.println("Entered 5: " + new String(password));
password = console.readPassword("Enter pass%s", "word 6: ");
System.out.println("Entered 6: " + new String(password));

上例的结果如下:

JDK17 |java17学习 第 4 章 异常处理

某些 IDE 无法运行这些示例并抛出 NullPointerException。如果是这种情况,请从命令行运行与控制台相关的示例,如前所述。每次更改代码时不要忘记运行 maven package 命令。

另一组 Console 类方法可以与前面演示的方法结合使用:

  • Console format(String format, Object... args):将提供的format字符串中的占位符替换为提供的args 值并显示结果
  • Console printf(String format, Object... args):行为方式与 format() 方法相同

作为 示例,请查看以下行:

String line = console.format("Enter some%s", "thing:").readLine();

它产生与此行相同的结果:

String line = console.readLine("Enter some%s", "thing:");

最后,Console 类的最后三个方法如下:

  • PrintWriter writer():创建一个与此控制台关联的 PrintWriter 对象,可用于生成字符输出流
  • Reader reader():创建一个与此控制台关联的 Reader 对象,可用于将输入作为字符流读取
  • void flush():刷新控制台并强制立即写入任何缓冲的输出

以下是它们的用法示例(console3() 方法):

try (Reader reader = console.reader()){
    char[] chars = new char[10];
    System.out.print("Enter something: ");
    reader.read(chars);
    System.out.print("Entered: " + new String(chars));
} catch (IOException e) {
    e.printStackTrace();
}
PrintWriter out = console.writer();
out.println("Hello!");
console.flush();

上述代码的结果如下所示:

JDK17 |java17学习 第 4 章 异常处理

ReaderPrintWriter 也可以用于创建其他 InputOutput 流,我们在本节中讨论过。

StreamTokenizer

StreamTokenizer 类解析 输入 流并生成令牌。它的 StreamTokenizer(Reader r) 构造函数接受一个作为标记源的 Reader 对象。每次在 StreamTokenizer 对象上调用 int nextToken() 方法时,都会发生以下情况:

  1. 解析下一个令牌。
  2. StreamTokenizer 实例字段 ttype 由指示令牌类型的值填充:
    • ttype 值可以是以下整数常量之一:TT_WORDTT_NUMBER TT_EOL(行尾)或 TT_EOF(流尾)。李>
    • 如果 ttype 值为 TT_WORD,则 StreamTokenizer 实例, sval 字段由令牌的 String 值填充。
    • 如果 ttype 值为 TT_NUMBER,则 StreamTokenizer 实例字段,nval,由令牌的 double 值填充。
  3. StreamTokenizer 实例的 lineno() 方法返回当前行号。

在说 StreamTokenizer 类的其他方法之前,我们先看一个例子 .假设在项目 resources 文件夹中,有一个 tokens.txt 文件,其中包含以下四行文本:

There
happened
42
events.

以下代码将读取文件并标记其内容(InputOutputStream 类的 streamTokenizer() 方法):

String file =  classLoader.
                           getResource("tokens.txt").getFile();
try(FileReader fr = new FileReader(file);
   BufferedReader br = new BufferedReader(fr)){
  StreamTokenizer st = new StreamTokenizer(br);
  st.eolIsSignificant(true);
  st.commentChar('e');
  System.out.println("Line " + st.lineno() + ":");
  int i;
  while ((i = st.nextToken()) != StreamTokenizer.TT_EOF) {
    switch (i) {
       case StreamTokenizer.TT_EOL:
            System.out.println("\nLine " + st.lineno() + ":");
            break;
       case StreamTokenizer.TT_WORD:
            System.out.println("TT_WORD => " + st.sval);
            break;
       case StreamTokenizer.TT_NUMBER:
            System.out.println("TT_NUMBER => " + st.nval);
            break;
       default:
            System.out.println("Unexpected => " + st.ttype);
       }
  }         
} catch (Exception ex){
    ex.printStackTrace();
}

如果我们运行这段代码,结果如下:

JDK17 |java17学习 第 4 章 异常处理

我们使用了 BufferedReader 类,这是提高效率的好习惯,但在我们的例子中,我们可以轻松避免它,如下所示:

 FileReader fr = new FileReader(filePath);
 StreamTokenizer st = new StreamTokenizer(fr);

结果不会改变。我们还使用了以下三种我们尚未描述的方法:

  • void eolIsSignificant(boolean flag):表示是否应将行尾视为标记
  • void commentChar(int ch):表示注释是由哪个字符开始的,所以该行的其余部分被忽略
  • int lineno():返回当前行号

可以使用 StreamTokenizer 对象调用以下方法:

  • void lowerCaseMode(boolean fl):指示单词标记是否应为小写
  • void normalChar(int ch), void normalChars(int low, int hi):表示特定字符​​或字符范围必须被视为普通(而不是注释字符、单词组件、字符串分隔符、空格或数字字符)
  • void parseNumbers():表示必须将具有双精度浮点数格式的单词标记解释为数字,而不是单词
  • void pushBack():强制nextToken()方法返回的当前值ttype 字段
  • void quoteChar(int ch):表示必须将提供的字符解释为必须按原样获取的字符串值的开头和结尾(作为引号)
  • void resetSyntax():重置此分词器的语法表,使所有字符普通
  • void slashSlashComments(boolean flag):表示必须识别C++风格的注释
  • void slashStarComments(boolean flag):表示C风格的注释被认可
  • String toString():返回token的字符串表示和行号
  • void whitespaceChars(int low, int hi):表示必须解释为空格的字符范围
  • void wordChars(int low, int hi):表示必须解释为单词的字符范围

如您所见,使用上述丰富的方法可以微调文本解释。

ObjectStreamClass and ObjectStreamField

ObjectStreamClassObjectStreamField提供 访问在JVM中加载的类的序列化数据。 ObjectStreamClass 对象可以使用以下查找方法之一找到/创建:

  • static ObjectStreamClass lookup(Class cl):查找可序列化类的描述符
  • static ObjectStreamClass lookupAny(Class cl):查找任何类的描述符,无论是否可序列化

找到ObjectStreamClass且类可序列化后(实现Serializable接口),即可访问 ObjectStreamField 对象,每个对象都包含一个序列化字段的信息。如果类不可序列化,则没有与任何字段关联的 ObjectStreamField 对象。

让我们看看 一个 的例子。这是显示从 ObjectStreamClassObjectStreamField 对象获得的信息的方法

void printInfo(ObjectStreamClass osc) {
    System.out.println(osc.forClass());
    System.out.println("Class name: " + osc.getName());
    System.out.println("SerialVersionUID: " +
                                   osc.getSerialVersionUID());
    ObjectStreamField[] fields = osc.getFields();
    System.out.println("Serialized fields:");
    for (ObjectStreamField osf : fields) {
        System.out.println(osf.getName() + ": ");
        System.out.println("\t" + osf.getType());
        System.out.println("\t" + osf.getTypeCode());
        System.out.println("\t" + osf.getTypeString());
    }
}

为了演示它是如何工作的,我们将创建一个可序列化的 Person1 类:

package com.packt.learnjava.ch05_stringsIoStreams;
import java.io.Serializable;
public class Person1 implements Serializable {
    private int age;
    private String name;
    public Person1(int age, String name) {
        this.age = age;
        this.name = name;
    }
}

我们没有添加方法,因为只有对象状态是可序列化的,而不是方法。现在,让我们运行 代码:

ObjectStreamClass osc1 = 
        ObjectStreamClass.lookup(Person1.class);
printInfo(osc1);

结果如下:

JDK17 |java17学习 第 4 章 异常处理

如您所见,有关于类名和所有字段名和类型的信息。还有两个其他方法可以使用 ObjectStreamField 对象调用:

  • boolean isPrimitive():如果此字段具有原始类型,则返回 true
  • boolean isUnshared():如果此字段未共享(私有或只能从同一个包访问),则返回 true

现在,让我们创建一个不可序列化的 Person2 类:

package com.packt.learnjava.ch05_stringsIoStreams;
public class Person2 {
    private int age;
    private String name;
    public Person2(int age, String name) {
        this.age = age;
        this.name = name;
    }
}

这个时间,我们运行代码只查找类,如下:

ObjectStreamClass osc2 = 
             ObjectStreamClass.lookup(Person2.class);
System.out.println("osc2: " + osc2);    //prints: null

正如预期的那样,使用 lookup() 方法找不到不可序列化的对象。为了找到一个不可序列化的对象,我们需要使用 lookupAny() 方法:

ObjectStreamClass osc3 = 
           ObjectStreamClass.lookupAny(Person2.class);
printInfo(osc3);

如果我们运行前面的示例,结果将如下所示:

JDK17 |java17学习 第 4 章 异常处理

从不可序列化的 对象中,我们能够提取有关类的信息,但不能提取 关于 字段。

The java.util.Scanner class

java.util.Scanner 类是 通常用于 从键盘,但也可以从实现 Readable 接口的任何对象读取文本(此接口只有 int read(CharBuffer buffer) 方法)。它通过分隔符(空格是默认分隔符)将输入值分解为使用不同方法处理的标记。

例如,我们可以从 System.in 读取输入——标准输入流,通常表示键盘输入:

Scanner sc = new Scanner(System.in);
System.out.print("Enter something: ");
while(sc.hasNext()){
    String line = sc.nextLine();
    if("end".equals(line)){
        System.exit(0);
    }
    System.out.println(line);
}

它接受多行(每行在按下 Enter 键后结束),直到输入 end 行,如下所示:

JDK17 |java17学习 第 4 章 异常处理

或者,Scanner 可以从文件中读取行:

String file =  classLoader.getResource("tokens.txt").getFile();
try(Scanner sc = new Scanner(new File(file))){
    while(sc.hasNextLine()){
        System.out.println(sc.nextLine());
    }
} catch (Exception ex){
    ex.printStackTrace();
}

如您所见,我们再次使用了 tokens.txt 文件。结果如下:

JDK17 |java17学习 第 4 章 异常处理

为了演示Scanner通过分隔符打破输入,让我们运行以下命令代码:

String input = "One two three";
Scanner sc = new Scanner(input);
while(sc.hasNext()){
    System.out.println(sc.next());
}

结果如下:

JDK17 |java17学习 第 4 章 异常处理

要使用另一个分隔符,可以设置如下:

String input = "One,two,three";
Scanner sc = new Scanner(input).useDelimiter(",");
while(sc.hasNext()){
    System.out.println(sc.next());
}

结果保持不变:

JDK17 |java17学习 第 4 章 异常处理

也可以使用正则表达式来提取标记,但该主题超出了本书的范围。

Scanner 类有许多其他方法,使其适用于各种来源和所需结果。 findInLine()findWithinHorizo​​n()skip()findAll() 方法 不使用分隔符;他们只是尝试匹配提供的模式。如需 更多信息,请参阅扫描仪文档 (https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Scanner.html )。

File management

我们已经 已经使用了一些方法来使用 JCL 类查找、创建、读取和写入文件。为了支持输入/输出流的演示代码,我们必须这样做。在本节中,我们将更详细地讨论使用 JCL 进行文件管理。

java.io 包中的 File 类表示底层文件系统。 File 类的对象可以使用以下构造函数之一创建:

  • File(String pathname):根据提供的路径名创建一个新的 File 实例
  • File(String parent, String child):根据提供的父路径名和子路径名创建一个新的 File 实例
  • File(File parent, String child):根据提供的父 File 实例>File 对象和子路径名
  • File(URI uri):根据提供的 URI 创建一个新的 File 实例code> 表示路径名的对象

我们现在将看到 在讨论创建和删除文件时使用构造函数的一些示例。

Creating and deleting files and directories

要在文件系统中创建 文件或 目录,首先需要构造一个新的文件 对象使用 文件管理 部分中列出的构造函数之一。例如,假设文件名是 FileName.txt,则 File 对象可以创建为 新文件(“文件名.txt”)。如果必须在目录中创建文件,则必须在文件名前面添加路径(当它传递给构造函数时)或必须使用其他三个构造函数之一,例如以下 (请参阅 Files 类中的 createFile2() 方法):

String path = "demo1" + File.separator + 
                            "demo2" + File.separator;
String fileName = "FileName.txt";
File f = new File(path + fileName);

请注意使用 File.separator 而不是斜杠符号 (/) 或 ( \)。这是因为 File.separator 返回特定于平台的斜杠符号。这是另一个 File 构造函数用法的示例:

String path = "demo1" + File.separator + 
                            "demo2" + File.separator;
String fileName = "FileName.txt";
File f = new File(path, fileName);

可以使用另一个构造函数,如下所示:

String path = "demo1" + File.separator + 
                            "demo2" + File.separator;
String fileName = "FileName.txt";
File f = new File(new File(path), fileName);

但是,如果更喜欢或必须使用通用资源标识符( URI),你可以像这样构造一个File对象:

String path = "demo1" + File.separator + 
                            "demo2" + File.separator;
String fileName = "FileName.txt";
URI uri = new File(path + fileName).toURI();
File f = new File(uri);

然后,必须在新创建的 File 对象上调用以下方法之一:

  • boolean createNewFile():如果同名文件尚不存在,则创建一个新文件并返回true;否则,返回 false
  • static File createTempFile(String prefix, String suffix):在临时文件目录下创建文件
  • static File createTempFile(String prefix, String suffix, File directory):创建目录;提供的前缀和后缀用于生成目录名称

如果您要创建的文件必须放在尚不存在的目录中,则必须首先使用以下方法之一,在 File 对象上调用表示文件的文件系统路径:

  • boolean mkdir():使用提供的名称创建目录
  • boolean mkdirs():使用提供的名称创建目录,包括任何必要但不存在的父目录

在我们看一个代码示例之前,我们需要解释一下delete()< /code> 方法有效:

  • boolean delete():删除文件或空目录,表示可以删除文件,但不能删除所有目录,如下:
    字符串路径 = "demo1" + File.separator +                              "demo2" + File.separator; 字符串文件名 = "文件名.txt"; 文件 f = 新文件(路径 + 文件名); f.delete();

让我们看看如何在以下示例中克服此限制:

String path = "demo1" + File.separator + 
                            "demo2" + File.separator;
String fileName = "FileName.txt";
File f = new File(path + fileName);
try {
    new File(path).mkdirs();
    f.createNewFile();
    f.delete();
    path = StringUtils
            .substringBeforeLast(path, File.separator);
    while (new File(path).delete()) {
        path = StringUtils
             .substringBeforeLast(path, File.separator);
    }
} catch (Exception e) {
    e.printStackTrace();
}

此示例创建和删除文件和所有相关目录。请注意我们对 org.apache.commons.lang3.StringUtils 类的使用,我们在 String utility 部分中讨论了该类。它允许我们从路径中删除刚刚删除的目录并 继续这样做,直到所有嵌套的 目录都被删除,最后删除顶层目录。

Listing files and directories

以下 方法可用于列出目录及其中的文件:

  • String[] list():返回目录中文件和目录的名称
  • File[] listFiles():返回代表目录中文件和目录的File对象
  • static File[] listRoots():列出可用的文件系统根

为了演示上述方法,假设我们已经创建了目录和其中的两个文件,如下所示:

String path1 = "demo1" + File.separator;
String path2 = "demo2" + File.separator;
String path = path1 + path2;
File f1 = new File(path + "file1.txt");
File f2 = new File(path + "file2.txt");
File dir1 = new File(path1);
File dir = new File(path);
dir.mkdirs();
f1.createNewFile();
f2.createNewFile();

之后,我们 应该能够运行以下代码:

System.out.print("\ndir1.list(): ");
for(String d: dir1.list()){
    System.out.print(d + " ");
}
System.out.print("\ndir1.listFiles(): ");
for(File f: dir1.listFiles()){
    System.out.print(f + " ");
}
System.out.print("\ndir.list(): ");
for(String d: dir.list()){
    System.out.print(d + " ");
}
System.out.print("\ndir.listFiles(): ");
for(File f: dir.listFiles()){
    System.out.print(f + " ");
}
System.out.print("\nFile.listRoots(): ");
for(File f: File.listRoots()){
    System.out.print(f + " ");
}

结果应如下所示:

JDK17 |java17学习 第 4 章 异常处理

演示的 方法可以通过向它们添加以下过滤器来增强,以便它们仅列出与过滤器匹配的文件和目录:

  • String[] list(FilenameFilter 过滤器)
  • File[] listFiles(FileFilter filter)
  • File[] listFiles(FilenameFilter filter)

然而,文件过滤器的讨论超出了本书的范围。

Apache Commons’ FileUtils and IOUtils utilities

JCL 的一个受欢迎的 伙伴是 Apache Commons 项目 (https://commons.apache.org),它提供了许多补充JCL功能的库。 org.apache.commons.io 包的类包含在以下根包和子包中:

  • org.apache.commons.io 根包包含具有用于常见任务的静态方法的实用程序类,例如流行的 FileUtilsIOUtils 类,分别在 FileUtils classClass IOUtils class 部分中描述.
  • org.apache.commons.io.input 包包含支持基于 InputStreamReader 实现,例如 XmlStreamReaderReversedLinesFileReader
  • org.apache.commons.io.output 包包含支持基于 OutputStreamWriter 实现,例如 XmlStreamWriterStringBuilderWriter
  • org.apache.commons.io.filefilter 包包含用作文件过滤器的类,例如 DirectoryFileFilter正则表达式文件过滤器
  • org.apache.commons.io.comparator 包包含 java.util.Comparator 文件的各种实现,例如 NameFileComparator
  • org.apache.commons.io.serialization 包提供了一个控制类反序列化的框架。
  • org.apache.commons.io.monitor 包允许监视文件系统、检查目录以及创建、更新或删除文件。您可以将 FileAlterationMonitor 对象作为线程启动,并创建一个 FileAlterationObserver 对象来检查 文件系统在指定的时间间隔。

有关详细信息,请参阅 Apache Commons 项目文档 (https://commons.apache.org/)。

The FileUtils class

流行的 org.apache.commons.io.FileUtils 类允许您对文件执行所有可能的操作,如下所示:

  • 写入文件
  • 从文件中读取
  • 制作目录,包括父目录
  • 复制文件和目录
  • 删除文件和目录
  • 与 URL 相互转换
  • 按过滤器和扩展名列出文件和目录
  • 比较文件内容
  • 获取文件的最后更改日期
  • 计算校验和

如果您计划以编程方式管理文件和目录,则必须在 Apache Commons 项目网站 (https://commons.apache.org/proper/commons-io /javadocs/api-2.7/org/apache/commons/io/FileUtils.html)。

The IOUtils class

org.apache.commons.io.IOUtils 是另一个非常有用的实用程序类,它提供了 以下通用 I/O 流操作方法:

  • closeQuietly 方法关闭流,忽略空值和异常
  • 从流中读取数据的 toXxx/read 方法
  • 将数据写入流的 write 方法
  • copy 方法将所有数据从一个流复制到另一个
  • 比较两个流的内容的 contentEquals 方法

该类中读取流的所有方法都在内部缓冲,因此无需使用 BufferedInputStreamBufferedReader 类. copy 方法都在后台使用 copyLarge 方法,大大提高了它们的性能和效率。

这个类对于管理 I/O 流是必不可少的。在 Apache Commons 项目网站 (https://commons.apache.org/proper/commons-io /javadocs/api-2.7/org/apache/commons/io/IOUtils.html)。

Summary

在本章中,我们讨论了允许分析、比较和转换字符串的 String 类方法。我们还讨论了来自 JCL 和 Apache Commons 项目的流行字符串实用程序。本章的两个主要部分专门介绍了 JCL 和 Apache Commons 项目中的输入/输出流和支持类。文件管理类及其方法也在特定的代码示例中进行了讨论和演示。现在,您应该能够使用标准 Java API 和 Apache Commons 实用程序编写处理字符串和文件的代码。

在下一章中,我们将介绍 Java Collections 框架及其三个主要接口 ListSetMap,包括泛型的讨论和演示。我们还将讨论用于管理数组、对象和 time/date 值的实用程序类。

Quiz

  1. 下面的代码打印什么?
    String str = "&8a!L"; System.out.println(str.indexOf("a!L"));
    1. 3
    2. 2
    3. 1
    4. 0
  2. 下面的代码打印什么?
    String s1 = "x12"; 字符串 s2 = new String("x12"); System.out.println(s1.equals(s2)); 
    1. 错误
    2. 异常
  3. 下面的代码打印什么?
    System.out.println("%wx6".substring(2));
    1. wx
    2. x6
    3. %w
    4. 异常
  4. 下面的代码打印什么?
    System.out.println("ab"+"42".repeat(2));
    1. ab4242
    2. ab42ab42
    3. ab422
    4. 错误
  5. 下面的代码打印什么?
    String s = "  "; System.out.println(s.isBlank()+" "+s.isEmpty()); 
    1. false false
    2. false true
    3. 真真
    4. 真假
  6. 选择所有正确的陈述:
    1. 一个流可以代表一个数据源。
    2. 输入流可以写入文件。
    3. 一个流可以代表一个数据目的地。
    4. 输出流可以在屏幕上显示数据。
  7. 选择关于 java.io 包的类的所有正确陈述:
    1. 阅读器扩展了 InputStream
    2. 阅读器扩展 OutputStream
    3. 阅读器扩展了 java.lang.Object
    4. 阅读器扩展了 java.lang.Input
  8. 选择关于 java.io 包的类的所有正确陈述:
    1. Writer 扩展了 FilterOutputStream
    2. Writer 扩展了 OutputStream
    3. Writer 扩展了 java.lang.Output
    4. Writer 扩展了 java.lang.Object
  9. 选择关于 java.io 包的类的所有正确陈述:
    1. PrintStream 扩展了 FilterOutputStream
    2. PrintStream 扩展了 OutputStream
    3. PrintStream 扩展了 java.lang.Object
    4. PrintStream 扩展了 java.lang.Output
  10. 下面的代码有什么作用?
    字符串路径 = "demo1" + File.separator + "demo2" + File.separator; 字符串文件名 = "文件名.txt"; 文件 f = 新文件(路径,文件名); 尝试 {     新建文件(路径).mkdir();     f.createNewFile(); } 捕捉(异常 e){     e.printStackTrace(); } 
    1. demo2目录下创建两个目录和一个文件
    2. 在其中创建一个目录和一个文件
    3. 不创建任何目录
    4. 例外