1 文本 I/O
1.1 字符流
1.1.1 什么是字符流
在Java中,字符流是指提供了基于字符的I/O能力的API。
Java 1.0中提供的基于字节的I/O流API只能支持8位字节流,无法妥善地处理16位Unicode字符。由于需要支持Unicode处理国际化字符,因此Java 1.1 对基础流式I/O库进行了重大的修改,核心是增加了字符流相关的API,处理国际化Unicode字符的编码和解码。
字符流是以字符(char)为单位读写数据的:一次处理一个 Unicode。字符流的底层仍然是基本的字节流,它封装了字符的编码解码算法。
字符流以Reader和Writer为核心抽象类,所有的字符输入流类均继承Reader,所有的字符输出流均继承Writer。如下图所示:
字符流相关子类可以分为节点流和处理流:上图中带阴影的是节点流,不带阴影的是处理流。
1.1.2 【案例】FileWriter示例
编写代码,使用FileWriter将字符写入文件。代码示意如下:
package api_04;
import java.io.FileWriter;
import java.io.Writer;
public class WriterDemo1 {
public static void main(String[] args){
Writer writer = null;
try{
writer = new FileWriter("./src/api_04/demo");
writer.write("Hello World!\n");
writer.write("Hello FileWriter!\n");
}catch (Exception e){
e.printStackTrace();
}finally {
if (writer != null){
try{
writer.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
}
1.1.3 【案例】FileReader示例
编写代码,使用FileReader从文件中读取字符。代码示意如下:
package api_04;
import java.io.FileReader;
import java.io.Reader;
public class ReaderDemo1 {
public static void main(String[] args) {
Reader reader = null;
try{
reader = new FileReader("./src/api_04/demo");
char[] data = new char[5];
int len = 0;
while (len !=-1){
System.out.print(new String(data,0,len));
len = reader.read(data);
}
}catch (Exception e){
e.printStackTrace();
}finally {
if (reader != null){
try{
reader.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
}
1.1.4 try-with-resources语法
在前面的案例中,为了妥善的处理I/O操作中可能出现的异常,并保证在程序结束后关流,我们使用了try-catch-finally语句块。但在实际编码中遇到很多不方便的情况,例如:
- finally语句块不能访问try语句块中声明的变量
- finally语句块中需要再次添加try-catch语句块
幸好Java 7 引入了try-with-resources语法,可以很好地简化上述代码。
这种称为try-with-resources语法。其要求为:
1、try后面的括号称为资源说明头,用于创建语句块中使用的资源对象
2、资源说明头中创建的对象必须实现java.lang.AutoCloseable接口,该接口只有一个方法-close()
3、资源说明头中可以包含多个创建对象的语句,用分号隔开,最后的分号可以省略
4、不论如何退出try语句块,都会自动调用所有资源对象的close方法
5、退出try语句块时,会以与声明资源对象相反的顺序去调用资源对象的close方法
1.1.5 【案例】try-with-resources示例
编写代码,测试try-with-resources的用法。代码示意如下:
package api_04;
import java.io.Closeable;
import java.io.IOException;
public class TWRDemo {
public static void main(String[] args) {
try(
MyStream1 stream1 = new MyStream1();
MyStream2 stream2 = new MyStream2();
){
System.out.println("try...");
}catch (Exception e){
e.printStackTrace();
}
}
}
class MyStream1 implements Closeable{
@Override
public void close() throws IOException {
System.out.println("MyStream1 close...");
}
}
class MyStream2 implements Closeable{
@Override
public void close() throws IOException {
System.out.println("MyStream2 close...");
}
}
1.2 字符缓冲流
1.2.1 字符缓冲流概述
BufferedReader和BufferedWriter分别是字符输入流和字符输出流对应的缓冲流,内置了缓冲区,通过缓冲区来减少实际的物理读写操作,进而提高读写效率。这两个类默认的缓冲区大小均为8192个字符,并支持通过构造器来设置缓冲区大小。
BufferedReader中还提供了一些增强的I/O操作方法,例如:
- readLine:读取文本中的一行内容,以'\n','\r'来识别,返回的内容不包含换行符
- skip:跳过指定数量的字符
BufferedWriter中没有提供相似的writeLine方法,但是提供了newLine方法,调用时可以向流中输出一个换行符,以达到换行的效果。
1.2.2 【案例】BufferedWriter示例
编写代码,测试BufferedWriter的用法。代码示意如下:
package api_04;
import java.io.*;
public class BufferedWriterDemo {
public static void main(String[] args) {
try(
Writer writer = new FileWriter("./src/api_04/demo2");
BufferedWriter bufferedWriter = new BufferedWriter(writer);
){
bufferedWriter.write("Hello world!");
bufferedWriter.newLine();
bufferedWriter.write("Hello BufferedWriter!");
bufferedWriter.newLine();
}catch (Exception e){
e.printStackTrace();
}
}
}
1.2.3 【案例】BufferedReader示例
编写代码,测试BufferedReader的用法。代码示意如下:
package api_04;
import java.io.*;
public class BufferedReaderDemo {
public static void main(String[] args) {
try(
Reader reader = new FileReader("./src/api_04/demo2");
BufferedReader bufferedReader = new BufferedReader(reader);
){
String line = bufferedReader.readLine();
while (line!=null) {
System.out.println(line);
line = bufferedReader.readLine();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
1.3 转换流
1.3.1 转换流概述
转换流用于实现字节流和字符流之间的转换。
InputStreamReader是字节输入流到字符输入流的桥梁,用于将InputStream转换为Reader,可以读取字节数据并根据指定的字符集转换为字符数据。
OutputStreamWriter是字符输出流到字节输出流的桥梁,用于将OutputStream转换为Writer,可以根据指定的字符集将写出的字符数据转换为字节数据。
1.3.2 指定字符编码
InputStreamReader 的构造方法允许设置字符集
- InputStreamReader(InputStream in,String charsetName):基于给定的字节输入流以及字符编码创建
- InputStreamReader(InputStream in):该构造方法会根据系统默认字符集创建
OutputStreamWriter 的构造方法:
- OutputStreamWriter(OutputStream out,String charsetName):基于给定的字节输出流以及字符编码创建
- OutputStreamWriter(OutputStream out):该构造方法会根据系统默认字符集创建
1.3.3 【案例】OutputStreamWriter示例
编写代码,测试OutputStreamWriter的用法,并指定字符编码。代码示意如下:
package api_04;
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
public class OutputStreamWriterDemo {
public static void main(String[] args) {
try(
FileOutputStream fos
= new FileOutputStream("./src/api_04/demo3");
OutputStreamWriter osw
= new OutputStreamWriter(fos,"utf-8");
BufferedWriter bw = new BufferedWriter(osw)
){
bw.write("世界你好!");
bw.newLine();
bw.write("转换流你好!");
bw.newLine();
System.out.println("写出完毕!");
}catch (Exception e){
e.printStackTrace();
}
}
}
1.3.4 【案例】InputStreamReader示例
编写代码,测试InputStreamReader的用法,并指定字符编码。代码示意如下:
package api_04;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
public class InputStreamReaderDemo {
public static void main(String[] args) {
try(
FileInputStream fis
= new FileInputStream("./src/api_04/demo3");
InputStreamReader isr
= new InputStreamReader(fis,"utf-8");
BufferedReader br = new BufferedReader(isr);
){
String line =br.readLine();
while(line!=null){
System.out.println(line);
line = br.readLine();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
1.3.5 【案例】数据统计示例
掌握了I/O相关API后,可以结合之前所学的Java基础知识,实现基础的数据统计操作。
weather_data_ny_201906.csv文件是某气象网站提供的纽约市2019年6月的天气数据。文件内容如下图所示:
现有如下需求:
1、数据预处理:提取weather_data_ny_201906.csv文件中的"STATION","DATE","MAX"三列的值,写入新的文件data1.csv,新文件中需要表头行,的数据继续使用英文逗号分隔,但是数据前后不再包含双引号。data1.csv文件如下图所示:
2、基于data1.csv,统计每个站点的6月平均温度,结果四舍五入保留小数点后2位,写入data2.csv,data2.csv文件的表头为"STATION", "AVG"。data2.csv文件如下图所示:
代码示意如下:
package api_04;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
public class DataPreProcessDemo {
public static void main(String[] args) {
try(
FileReader fr =new FileReader("./src/api_04/weather_data_ny_201906.csv");
BufferedReader br =new BufferedReader(fr);
FileWriter fw =new FileWriter("./src/api_04/data1.csv");
BufferedWriter bw =new BufferedWriter(fw)
){
// 写出第一行表头行
bw.write("STATION,DATE,MAX");
// 读取数据
String line =br.readLine(); // 表头行,需跳过
line = br.readLine(); // 第一行数据
while(line!=null){
StringBuilder builder = new StringBuilder();
// 处理数据
String[] dataArray = line.replaceFirst("\"","") // 去掉首个字符串
.split("\",\""); // 使用","切分为字符串数组
builder.append(dataArray[0].trim()).append(",") // 拼接STATION列
.append(dataArray[5].trim()).append(",") // 拼接DATE列
.append(dataArray[9].trim());
// 写出一行新数据行
bw.newLine();
bw.write(builder.toString());
// 读取一行新的数据
line = br.readLine();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
package api_04;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class StatisticsDemo {
public static void main(String[] args) {
try(
FileReader fr =new FileReader("./src/api_04/data1.csv");
BufferedReader br =new BufferedReader(fr);
FileWriter fw =new FileWriter("./src/api_04/data2.csv");
BufferedWriter bw =new BufferedWriter(fw)
){
// 写出第一行表头行
bw.write("STATION,AVG");
/*
* 处理数据思路:
* 分组求均值,按STATION分组,求组内均值
* 共5个站点,分5组
* 需要过滤掉非6月的数据
*/
// 存储各个站点的温度和,站点id和数组下标对应关系
// 99727199999-0, 72505394728-1, 72055399999-2,
// 99774399999-3, 72503014732-4
BigDecimal[] tempDataArray = new BigDecimal[5];
for(int i =0; i < tempDataArray.length; i++){
tempDataArray[i]=new BigDecimal(0);
}
int[] dataCount = new int[5]; // 统计数据条数
String[] stationArray = new String[]
{"99727199999", "72505394728", "72055399999"
,"99774399999","72503014732"};
// 读取数据
String line =br.readLine(); // 表头行,需跳过
line = br.readLine(); // 第一行数据
while(line!=null){
// 处理数据
String[] dataArray = line.split(",");
// 过滤非6月数据
if (!dataArray[1].startsWith("2019-06")){
line=br.readLine();
continue; // 不处理该行数据
}
BigDecimal temp = new BigDecimal(dataArray[2]);
switch (dataArray[0]){
case "99727199999":
tempDataArray[0] = tempDataArray[0].add(temp);
dataCount[0] = dataCount[0] + 1;
break;
case "72505394728":
tempDataArray[1] = tempDataArray[1].add(temp);
dataCount[1] = dataCount[1] + 1;
break;
case "72055399999":
tempDataArray[2] = tempDataArray[2].add(temp);
dataCount[2] = dataCount[2] + 1;
break;
case "99774399999":
tempDataArray[3] = tempDataArray[3].add(temp);
dataCount[3] = dataCount[3] + 1;
break;
case "72503014732":
tempDataArray[4] = tempDataArray[4].add(temp);
dataCount[4] = dataCount[4] + 1;
break;
default:
System.out.println("站点名称未匹配:"+dataArray[0]);
break;
}
// 读取一行新的数据
line = br.readLine();
}
// 求均值
for(int i=0;i < tempDataArray.length;i++) {
BigDecimal avg = tempDataArray[i]
.divide(new BigDecimal(dataCount[i]),2, RoundingMode.HALF_UP);
StringBuilder builder = new StringBuilder();
builder.append(stationArray[i]).append(",").append(avg);
// 写出一行新数据行
bw.newLine();
bw.write(builder.toString());
}
}catch (Exception e){
e.printStackTrace();
}
}
}
2 日期与时间API
2.1 Date类
2.1.1 Java中的日期时间
日期和时间在应用程序中具有非常广泛和多样的应用,包括日期时间数据的获取和封装、日期时间数据的计算,以及日期时间数据的时区转换和格式转换等多个方面。
- 大部分的应用程序都有日志模块,记录应用运行过程中的程序的执行情况和用户的各类操作
- 一些特定的功能会用到计时器功能,如订单的支付限时为15分钟
- 通过用户的生日和当前日期计算用户的年龄
- 通过用户的登录时间和当前时间计算用户在线时长等
- 一些面向国际化的应用程序也存在日期时间数据的时区转换和格式转换的需求
Java中提供了多种日期时间相关的工具类和丰富的API,使开发者可以简化日期时间操作,以提高应用程序的开发效率。
2.1.2 纪元(Epoch)
在计算机领域,“纪元”是计算机测量系统时间的日期和时间。大多数计算机系统将时间确定为一个数字,表示从特定的任意日期和时间以来经过的秒数。 例如,Unix和POSIX将时间测量为自1970年1月1日00:00:00 GMT 以来经过的秒数,该时间点称为 Unix“纪元”。
由于我国处于东八区,因此基准时间为1970年1月1日8时0分0秒。北京时间2023年1月1日凌晨可以表示为1672502400。
这种用于表示日期和时间的数字就是在开发中广泛使用的时间戳(Timestamp),常有精确到秒和精确到毫秒2种表示形式。
Java中使用的时间戳支持精确到秒、毫秒和纳秒,之前在统计程序运行效率中使用的System.currentTimeMillis()方法,就是返回当前的毫秒级时间戳。
2.1.3 Date类
java.util.Date类封装日期和时间信息,表示一个特定瞬间的类,时间可以精确到毫秒。Date的每一个实例用于表示一个时间,内部维护一个long值,该值保存的是自标准基准时间以来的毫秒数。
Date类是Java中使用最为广泛的一个日期时间类之一。
由于设计问题,该类中的很多方法已被标记过时,使用时应注意避免。查看下图:
这种已经被标记过时的方法,应尽量避免使用。
2.1.4【案例】Date类示例
编写代码,测试Date类的用法。代码示意如下:
package api_04;
import java.util.Date;
public class DateDemo {
public static void main(String[] args) {
Date now = new Date();
System.out.println(now);
// Date大部分方法都过时了
// now.getYear();
/*
* 获取Date内部维护的毫秒值
*/
long time = now.getTime();
System.out.println(time);
/*
* 设置一个毫秒使当前Date表示该时间
*/
now.setTime(0);
System.out.println(now);
}
}
2.2 SimpleDateFormat类
2.2.1 DateFormat 类概述
在输出Date对象代表的时间时,会自动调用Date类中的toString()方法。Date类对Object类中的toString()方法进行了重写,按照“dow mon dd hh:mm:ss zzz yyyy”(星期 月份 日期 小时:分钟:秒 时区 年份)的格式输出该Date对象代表的时间。在很多应用中,开发者需要指定自定义的时间输出格式。Java中提供的java.text.DateFormat用来满足开发者对日期时间格式化的需求。
DateFormat是日期/时间格式化子类的抽象类,通过这个类可以完成日期和文本之间的转换。DateFormat类支持日期的格式化(日期→文本)、日期的解析(文本→日期)和日期的规范化。
DateFormat为抽象类,不能直接使用。在实际开发中,比较常用的是DateFormat类的子类——SimpleDateFormat。
2.2.2 SimpleDateFormat 类
SimpleDateFormat类的构造方法:
- SimpleDateFormat( )
- SimpleDateFormat(String pattern):用给定的日期时间格式pattern构造SimpleDateFormat对象
参数pattern是一个字符串,代表日期时间的自定义格式,常用的格式为“yyyy-MM-dd HH:mm:ss”,其中,yyyy表示年份,MM表示月份,dd表示日期,HH表示小时,mm表示分钟,ss表示秒。
SimpleDateFormat的常用方法:
- String format(Date date):将 Date 对象代表的时间格式化为字符串
- Date parse(String source):将字符串解析为 Date 对象
2.2.3 日期模式匹配字符
日期模式匹配字符如下表所示:
2.2.4【案例】日期格式化示例
编写代码,测试日期格式化输出。代码示意如下:
package api_04;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SimpleDateFormatDemo1 {
public static void main(String[] args) {
Date now = new Date();
System.out.println(now);
/*
* yyyy-MM-dd HH:mm:ss
*/
SimpleDateFormat sdf
= new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss a E");
/*
* String format(Date date)
* 将给定的Date按照当前SDF指定的日期
* 格式转换为字符串
*/
String line = sdf.format(now);
System.out.println(line);
}
}
2.3 Java 8 新日期时间类
2.3.1新日期时间类概述
从Java 8开始,java.time包提供了新的日期和时间API,主要涉及的类型有:
- 本地日期和时间:LocalDateTime,LocalDate,LocalTime
- 带时区的日期和时间:ZonedDateTime
- 时刻:Instant
- 时区:ZoneId,ZoneOffset
- 时间间隔:Duration
以及一套新的用于取代SimpleDateFormat的格式化类型DateTimeFormatter。
和旧的API相比,新API严格区分了时刻、本地日期、本地时间和带时区的日期时间,并且,对日期和时间进行运算更加方便。
此外,新API修正了旧API不合理的常量设计:
- Month的范围用1~12表示1月到12月
- Week的范围用1~7表示周一到周日
2.3.2本地化日期时间 API
在很多本地化的应用中,考虑时区是没有必要的。Java 8在java.time包中提供了简化版的日期时间操作类,包括LocalDate、LocalTime和LocalDateTime。
- LocalDate表示本地日期
- LocalTime表示本地时间
- LocalDateTime可以看成前两者的组合,表示本地日期和时间
2.3.3【案例】LocalDateTime示例
编写代码,测试LocalDateTime。代码示意如下:
package api_04;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
public class LocalDateTimeDemo {
public static void main(String[] args) {
System.out.println("-------------构建LocalDateTime对象-------------");
// 获取当前时间的ldt对象
LocalDateTime ldt1 = LocalDateTime.now();
System.out.println("ldt1:\t"+ldt1);
// 获取表示指定时间的ldt对象
LocalDateTime ldt2 = LocalDateTime.of(2022,9,18,22,0,0,0);
System.out.println("ldt2:\t"+ldt2);
System.out.println("-------------获取日期时间信息方法-------------");
System.out.println(ldt1.getYear()+":"+ldt1.getMonthValue()
+":"+ldt1.getDayOfMonth());
System.out.println(ldt1.getHour()+":"+ldt1.getMinute()
+":"+ldt1.getSecond());
System.out.println("-------------日期时间计算方法-------------");
LocalDateTime ldt3 = ldt1.plus(6, ChronoUnit.HOURS)
.plusMinutes(10);
System.out.println("ldt3:\t"+ldt3);
LocalDateTime ldt4 = ldt1.minus(1, ChronoUnit.HOURS)
.minusMonths(1);
System.out.println("ldt4:\t"+ldt4);
}
}
2.3.4【案例】ZonedDateTime示例
编写代码,测试ZonedDateTime。代码示意如下:
package api_04;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class ZonedDateTimeDemo {
public static void main(String[] args) {
// 直接创建ZonedDateTime对象
ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
// 用指定时区获取当前时间
ZonedDateTime zny = ZonedDateTime.now(ZoneId.of("America/Los_Angeles"));
System.out.println(zbj);
System.out.println(zny);
// 通过LocalDateTime转换
LocalDateTime ldt = LocalDateTime.of(2023, 1, 2, 8, 15, 33);
ZonedDateTime zdt1 = ldt.atZone(ZoneId.systemDefault());
ZonedDateTime zdt2 = ldt.atZone(ZoneId.of("America/New_York"));
System.out.println(zdt1);
System.out.println(zdt2);
// 时区转换
// 以中国时区获取当前时间:
ZonedDateTime zdt3 = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
// 转换为巴黎时间:
ZonedDateTime zdt4 = zbj.withZoneSameInstant(ZoneId.of("Europe/Paris"));
System.out.println(zdt3);
System.out.println(zdt4);
}
}
2.3.5【案例】DateTimeFormatter示例
编写代码,测试DateTimeFormatter。代码示意如下:
package api_04;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public class DateTimeFormatterDemo {
public static void main(String[] args) {
ZonedDateTime zdt = ZonedDateTime.now();
// 日期格式化
DateTimeFormatter formatter1 =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm ZZZZ");
System.out.println(formatter1.format(zdt));
DateTimeFormatter formatter2 =
DateTimeFormatter.ofPattern("yyyy MMM dd EE HH:mm", Locale.CHINA);
System.out.println(formatter2.format(zdt));
DateTimeFormatter formatter3 =
DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US);
System.out.println(formatter3.format(zdt));
// 字符串转时间
DateTimeFormatter formatter4 =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String time = "2020-05-12 20:13:15";
LocalDateTime ldt = LocalDateTime.parse(time,formatter4);
System.out.println(ldt);
}
}
2.3.6【案例】Instant示例
编写代码,测试Instant。代码示意如下:
public class InstantDemo {
public static void main(String[] args) {
// 获取当前时间对象
Instant i1 = Instant.now();
// 输出当前时间,默认使用零时区
System.out.println("defalut:\t"+i1);
// 获取时间戳
System.out.println("timestamp:\t"+i1.getEpochSecond());
System.out.println("timestamp:\t"+i1.toEpochMilli());
// 通过Clock的API获取东八区对应的时钟
Clock offsetClock = Clock.offset(Clock.systemUTC(),
Duration.ofHours(8));
// 获取当前时间对象,设置使用东八区时钟
Instant i2 = Instant.now(offsetClock);
// 输出当前时间
System.out.println("UTC+8:\t \t "+i2);
// 基于指定时间获取Instant对象
Instant i3 = Instant.ofEpochSecond(i1.getEpochSecond()
-3600);
System.out.println("one hour ago:\t "+i3);
// 基于字符串获取Instant对象
Instant i4 = Instant.parse("2022-09-18T06:00:00Z");
System.out.println("from String:\t "+i4);
// Instant相关操作
Instant i5 = Instant.parse("2022-09-18T06:00:00Z");
System.out.println("i1:\t\t"+i1);
// 时间的加法运算
Instant i6 = i1.plus(3L, ChronoUnit.HOURS);
System.out.println("i1+3 hour:\t"+i2);
// 时间的减法运算
Instant i7 = i1.minus(5L,ChronoUnit.HOURS);
System.out.println("i1-5 hour:\t"+i3);
// 两个时间的差值
System.out.println("i1-i2 in seconds:"
+i1.until(i2,ChronoUnit.SECONDS));
// 判断时间先后
System.out.println("i1 is before i2:"+i1.isBefore(i2));
System.out.println("i1 is after i2:"+i1.isAfter(i2));
}
}