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语句块。但在实际编码中遇到很多不方便的情况,例如:
幸好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 字符缓冲流概述
BufferedReader和BufferedWriter分别是字符输入流和字符输出流对应的缓冲流,内置了缓冲区,通过缓冲区来减少实际的物理读写操作,进而提高读写效率。这两个类默认的缓冲区大小均为8192个字符,并支持通过构造器来设置缓冲区大小。
BufferedReader中还提供了一些增强的I/O操作方法,例如:
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 转换流概述
转换流用于实现字节流和字符流之间的转换。
InputStreamReader是字节输入流到字符输入流的桥梁,用于将InputStream转换为Reader,可以读取字节数据并根据指定的字符集转换为字符数据。
OutputStreamWriter是字符输出流到字节输出流的桥梁,用于将OutputStream转换为Writer,可以根据指定的字符集将写出的字符数据转换为字节数据。
1.3.2 指定字符编码
InputStreamReader 的构造方法允许设置字符集
OutputStreamWriter 的构造方法:
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.1.1 Java中的日期时间
日期和时间在应用程序中具有非常广泛和多样的应用,包括日期时间数据的获取和封装、日期时间数据的计算,以及日期时间数据的时区转换和格式转换等多个方面。
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.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类的构造方法:
参数pattern是一个字符串,代表日期时间的自定义格式,常用的格式为“yyyy-MM-dd HH:mm:ss”,其中,yyyy表示年份,MM表示月份,dd表示日期,HH表示小时,mm表示分钟,ss表示秒。
SimpleDateFormat的常用方法:
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.1新日期时间类概述
从Java 8开始,java.time包提供了新的日期和时间API,主要涉及的类型有:
以及一套新的用于取代SimpleDateFormat的格式化类型DateTimeFormatter。
和旧的API相比,新API严格区分了时刻、本地日期、本地时间和带时区的日期时间,并且,对日期和时间进行运算更加方便。
此外,新API修正了旧API不合理的常量设计:
2.3.2本地化日期时间 API
在很多本地化的应用中,考虑时区是没有必要的。Java 8在java.time包中提供了简化版的日期时间操作类,包括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));
}
}