目录
2.3.1字符输入流(InputStreamReader)与字符输出流(OutputStreamWriter )
在编写程序时,经常会有调用底层系统来进行文件读写的操作,Java是面向对象的编程语言,所以在进行文件读写操作时,就需要利用到File类和IO流,将具体文件封装成File对象,再通过IO流对其进行读写操作。
1.File类
1.1File类概述
File对象是文件和目录路径的抽象表示,文件和目录都可以通过File封装成对象,再通过此对象访问对应的文件及目录进行相应操作。
注意:不能同时有多个流操作同一个File对象,否则可能导致文件数据丢失、不完整等问题。
对于File而言,其封装的并不一定是一个真正存在的文件,仅仅是一个路径名而已。它可以是存在的,也可以是不存在的。将来是要通过具体的操作把这个路径的内容转换为具体存在的。
一般来说,读取内容时封装的File对象所指的文件是需要真实存在的,因为这是我们的数据源。
我们也可以在将其路径名封装成File对象之后,再通过File类的方法创建此文件。如:
//在指定目录下创建一个txt文件
public static void main(String[] args) throws IOException {
File f1 = new File("D:\\Demo\\java.txt");
//等价于 File f1 = new File("D:\\Demo","java.txt");
//打印创建结果 true
System.out.println(f1.createNewFile());
}
1.2File类创建功能
方法名 | 说明 |
---|---|
public boolean createNewFile() | 当具有该名称的文件不存在时,创建一个由该抽象路径名命名的新空文件 |
public boolean mkdir() | 创建由此抽象路径名命名的目录 |
public boolean mkdirs() | 创建由此抽象路径名命名的目录,包括任何必需但不存在的父目录 |
1.3File类判断功能
方法名 | 说明 |
---|---|
public boolean isDirectory() | 测试此抽象路径名表示的File是否为目录 |
public boolean isFile() | 测试此抽象路径名表示的File是否为文件 |
public boolean exists() | 测试此抽象路径名表示的File是否存在 |
1.4File类获取功能
方法名 | 说明 |
---|---|
public String getAbsolutePath() | 返回此抽象路径名的绝对路径名字符串 |
public String getPath() | 将此抽象路径名转换为路径名字符串 |
public String getName() | 返回由此抽象路径名表示的文件或目录的名称 |
public String[] list() | 返回此抽象路径名表示的目录中的文件和目录的名称字符串数组 |
public File[] listFiles() | 返回此抽象路径名表示的目录中的文件和目录的File对象数组 |
public class FileDemo04 {
public static void main(String[] args) {
//创建一个File对象
File f = new File("myFile\\java.txt");
// public boolean isDirectory():测试此抽象路径名表示的File是否为目录
// public boolean isFile():测试此抽象路径名表示的File是否为文件
// public boolean exists():测试此抽象路径名表示的File是否存在
System.out.println(f.isDirectory());
System.out.println(f.isFile());
System.out.println(f.exists());
// public String getAbsolutePath():返回此抽象路径名的绝对路径名字符串
// public String getPath():将此抽象路径名转换为路径名字符串
// public String getName():返回由此抽象路径名表示的文件或目录的名称
System.out.println(f.getAbsolutePath());
System.out.println(f.getPath());
System.out.println(f.getName());
System.out.println("--------");
// public String[] list():返回此抽象路径名表示的目录中的文件和目录的名称字符串数组
// public File[] listFiles():返回此抽象路径名表示的目录中的文件和目录的File对象数组
File f2 = new File("E:\\itcast");
String[] strArray = f2.list();
for(String str : strArray) {
System.out.println(str);
}
System.out.println("--------");
File[] fileArray = f2.listFiles();
for(File file : fileArray) {
// System.out.println(file);
// System.out.println(file.getName());
if(file.isFile()) {
System.out.println(file.getName());
}
}
}
}
1.5File类删除功能
方法名 | 说明 |
---|---|
public boolean delete() | 删除由此抽象路径名表示的文件或目录 |
在删除目录时,若目录下有文件需要先删除目录下的文件,否则无法删除目录
2.IO流
IO:输入/输出(Input/Output)
流:是一种抽象概念,是对数据传输的总称。也就是说数据在设备间的传输称为流,流的本质是数据传输。
IO流就是用来处理设备间数据传输问题的。常见的应用:文件复制;文件上传;文件下载。
分类
从数据流向来看,分为输入流(读数据 )和输出流(写数据)。
按照数据类型来看,分为字节流和字符流,字节流和字符流之间可以相互转化。
一般在操作纯文本时,优先使用字符流;
如果操作的是图片、视频、音频等二进制文件,优先使用字节流;
如果不确定文件类型,优先使用字节流,字节流是万能的流。
2.1字节流
字节流抽象基类(不能直接实例化,需要用非抽象子类实例化)
InputStream:这个抽象类是表示字节输入流的所有类的超类
OutputStream:这个抽象类是表示字节输出流的所有类的超类
2.1.1字节流写数据
方法名 | 说明 |
---|---|
void write(int b) | 将指定的字节写入此文件输出流 一次写一个字节数据 |
void write(byte[] b) | 将 b.length字节从指定的字节数组写入此文件输出流 一次写一个字节数组数据 |
void write(byte[] b, int off, int len) | 将 len字节从指定的字节数组开始,从偏移量off开始写入此文件输出流 一次写一个字节数组的部分数据 |
public class FileOutputStreamDemo02 {
public static void main(String[] args) throws IOException {
//FileOutputStream(String name):创建文件输出流以指定的名称写入文件
FileOutputStream fos = new FileOutputStream("myByteStream\\fos.txt");
//new File(name)
// FileOutputStream fos = new FileOutputStream(new File("myByteStream\\fos.txt"));
//FileOutputStream(File file):创建文件输出流以写入由指定的 File对象表示的文件
// File file = new File("myByteStream\\fos.txt");
// FileOutputStream fos2 = new FileOutputStream(file);
// FileOutputStream fos2 = new FileOutputStream(new File("myByteStream\\fos.txt"));
//void write(int b):将指定的字节写入此文件输出流
// fos.write(97);
// fos.write(98);
// fos.write(99);
// fos.write(100);
// fos.write(101);
// void write(byte[] b):将 b.length字节从指定的字节数组写入此文件输出流
// byte[] bys = {97, 98, 99, 100, 101};
//byte[] getBytes():返回字符串对应的字节数组
byte[] bys = "abcde".getBytes();
// fos.write(bys);
//void write(byte[] b, int off, int len):将 len字节从指定的字节数组开始,从偏移量off开始写入此文件输出流
// fos.write(bys,0,bys.length);
fos.write(bys,1,3);
//释放资源
fos.close();
}
}
如果需要文件内容能够实现追加写入,我们可以在创建文件输出流的的时候增加一个append参数,如果第二个参数为true ,则字节将写入文件的末尾而不是开头。如下:
public class FileOutputStreamDemo03 {
public static void main(String[] args) throws IOException {
//创建字节输出流对象
// FileOutputStream fos = new FileOutputStream("myByteStream\\fos.txt");
FileOutputStream fos = new FileOutputStream("myByteStream\\fos.txt",true);
//写数据
for (int i = 0; i < 10; i++) {
fos.write("hello".getBytes());
fos.write("\r\n".getBytes());
}
//释放资源
fos.close();
}
}
注意:本文案例在处理可能出现的运行时异常都是直接抛出,也可以利用try-catch-finally的方式直接捕捉处理,此时可以将资源清理工作放在finally内,确保执行完后能够及时释放资源。如:
public class FileOutputStreamDemo04 {
public static void main(String[] args) {
//加入finally来实现释放资源
FileOutputStream fos = null;
try {
fos = new FileOutputStream("myByteStream\\fos.txt");
fos.write("hello".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2.1.2 字节流读数据
(1)每次读取一个字节
public class FileInputStreamDemo01 {
public static void main(String[] args) throws IOException {
//创建字节输入流对象
//FileInputStream(String name)
FileInputStream fis = new FileInputStream("myByteStream\\fos.txt");
int by;
/*
fis.read():读数据
by=fis.read():把读取到的数据赋值给by
by != -1:判断读取到的数据是否是-1
*/
while ((by=fis.read())!=-1) {
System.out.print((char)by);
}
//释放资源
fis.close();
}
}
(2)每次读取一个字节数组
public class FileInputStreamDemo02 {
public static void main(String[] args) throws IOException {
//创建字节输入流对象
FileInputStream fis = new FileInputStream("myByteStream\\fos.txt");
byte[] bys = new byte[1024]; //一般设置数组长度为1024及其整数倍
int len;
while ((len=fis.read(bys))!=-1) {
System.out.print(new String(bys,0,len));
}
//释放资源
fis.close();
}
}
2.2字节缓冲流
在字节流的基础上增加了缓冲区,使用缓冲流后可以减少I/O读写操作带来的系统开销。
对于输出流,当程序向其写入数据时,缓冲区会先存储起来并不立即写入文件,直到缓冲区达到指定大小或者调用 flush() 方法才会将缓冲区中的数据全部写入文件;
对于输入流,缓冲区的作用则是提高读取文件的效率,在外界对流进行读取时,输入流将字节数据先读入到缓冲区,等缓冲区满了再返回给程序。这样就可以大大减少和磁盘、网络设备等硬件的交互次数,提高了I/O 的效率和性能。
构造方法
方法名 | 说明 |
---|---|
BufferedOutputStream(OutputStream out) | 创建字节缓冲输出流对象 |
BufferedInputStream(InputStream in) | 创建字节缓冲输入流对象 |
public class BufferStreamDemo {
public static void main(String[] args) throws IOException {
//字节缓冲输出流:BufferedOutputStream(OutputStream out)
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("myByteStream\\bos.txt"));
//写数据
bos.write("hello\r\n".getBytes());
bos.write("world\r\n".getBytes());
//释放资源
bos.close();
//字节缓冲输入流:BufferedInputStream(InputStream in)
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("myByteStream\\bos.txt"));
//一次读取一个字节数据
// int by;
// while ((by=bis.read())!=-1) {
// System.out.print((char)by);
// }
//一次读取一个字节数组数据
byte[] bys = new byte[1024];
int len;
while ((len=bis.read(bys))!=-1) {
System.out.print(new String(bys,0,len));
}
//释放资源
bis.close();
}
}
在字节流的使用中,一般都采用字节缓冲流一次读取一个字节数组的方式进行读写操作,因为这样能减少系统开销,效率更高。
2.3字符流
字符流主要是为了处理字节流不方便处理的中文文本,字符流 = 字节流 + 编码表
用字节流复制文本文件时,文本文件也会有中文,但是没有问题,原因是最终底层操作会自动识别字节拼接成中文,汉字在存储的时候,无论选择哪种编码存储,第一个字节都是负数。
常见的编码格式有ASCII 编码、UTF-8 编码、GBK编码和 GB2312 编码等
常用编码/解码相关方法
方法名 | 说明 |
---|---|
byte[] getBytes() | 使用平台的默认字符集将该 String编码为一系列字节 |
byte[] getBytes(String charsetName) | 使用指定的字符集将该 String编码为一系列字节 |
String(byte[] bytes) | 使用平台的默认字符集解码指定的字节数组来创建字符串 |
String(byte[] bytes, String charsetName) | 通过指定的字符集解码指定的字节数组来创建字符串 |
2.3.1字符输入流(InputStreamReader)与字符输出流(OutputStreamWriter )
InputStreamReader:是从字节流到字符流的桥梁它读取字节,并使用指定的编码将其解码为字符,它使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集。
使用时,如果不需要指定编码格式,可用子类 FileReader。
OutputStreamWriter:是从字符流到字节流的桥梁,是从字符流到字节流的桥梁,使用指定的编码将写入的字符编码为字节,它使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集。
使用时,如果不需要指定编码格式,可用子类 FileWriter。
构造函数
方法名 | 说明 |
---|---|
InputStreamReader(InputStream in) | 使用默认字符编码创建InputStreamReader对象 |
InputStreamReader(InputStream in,String chatset) | 使用指定的字符编码创建InputStreamReader对象 |
OutputStreamWriter(OutputStream out) | 使用默认字符编码创建OutputStreamWriter对象 |
OutputStreamWriter(OutputStream out,String charset) | 使用指定的字符编码创建OutputStreamWriter对象 |
public class ConversionStreamDemo {
public static void main(String[] args) throws IOException {
//OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("myCharStream\\osw.txt"));
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("myCharStream\\osw.txt"),"GBK");
osw.write("中国");
osw.close();
//InputStreamReader isr = new InputStreamReader(new FileInputStream("myCharStream\\osw.txt"));
InputStreamReader isr = new InputStreamReader(new FileInputStream("myCharStream\\osw.txt"),"GBK");
//一次读取一个字符数据
int ch;
while ((ch=isr.read())!=-1) {
System.out.print((char)ch);
}
isr.close();
}
}
(1)字符流写数据的方式
方法名 | 说明 |
---|---|
void write(int c) | 写一个字符 |
void write(char[] cbuf) | 写入一个字符数组 |
void write(char[] cbuf, int off, int len) | 写入字符数组的一部分 |
void write(String str) | 写一个字符串 |
void write(String str, int off, int len) | 写一个字符串的一部分 |
刷新流和关闭流
方法名 | 说明 |
---|---|
flush() | 刷新流,之后还可以继续写数据 |
close() | 关闭流,释放资源,但是在关闭之前会先刷新流。一旦关闭,就不能再写数据 |
字符流读数据的方式
方法名 | 说明 |
---|---|
int read() | 一次读一个字符数据 |
int read(char[] cbuf) | 一次读一个字符数组数据 |
案例
/*
使用IO流对象,把模块目录下的“ConversionStreamDemo.java” 复制到模块目录下的“Copy.java”.
实现步骤:
根据数据源创建字符输入流对象
根据目的地创建字符输出流对象
读写数据,复制文件
释放资源
*/
public class CopyJavaDemo01 {
public static void main(String[] args) throws IOException {
//根据数据源创建字符输入流对象
InputStreamReader isr = new InputStreamReader(new FileInputStream("myCharStream\\ConversionStreamDemo.java"));
//根据目的地创建字符输出流对象
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("myCharStream\\Copy.java"));
//读写数据,复制文件
//一次读写一个字符数据
// int ch;
// while ((ch=isr.read())!=-1) {
// osw.write(ch);
// }
//一次读写一个字符数组数据
char[] chs = new char[1024];
int len;
while ((len=isr.read(chs))!=-1) {
osw.write(chs,0,len);
}
//释放资源
osw.close();
isr.close();
}
}
2.3.2字符缓冲流
BufferedWriter:将文本写入字符输出流,缓冲字符,以提供单个字符,数组和字符串的高效写入,可以指定缓冲区大小,或者可以接受默认大小。
BufferedReader:从字符输入流读取文本,缓冲字符,以提供字符,数组和行的高效读取,可以指定缓冲区大小,或者可以使用默认大小。
构造函数
方法名 | 说明 |
---|---|
BufferedWriter(Writer out) | 创建字符缓冲输出流对象 |
BufferedReader(Reader in) | 创建字符缓冲输入流对象 |
字符缓冲流特有功能
BufferedWriter:
方法名 | 说明 |
---|---|
void newLine() | 写一个行分隔符(换行符),行分隔符字符串由系统属性定义 |
BufferedReader:
方法名 | 说明 |
---|---|
String readLine() | 读一行文字。 结果包含行的内容的字符串,不包括任何行终止字符如果流的结尾已经到达,则为null |
public class BufferedStreamDemo02 {
public static void main(String[] args) throws IOException {
//创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("myCharStream\\bw.txt"));
//写数据
for (int i = 0; i < 10; i++) {
bw.write("hello" + i);
//bw.write("\r\n");
bw.newLine();
bw.flush();
}
//释放资源
bw.close();
//创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("myCharStream\\bw.txt"));
String line;
while ((line=br.readLine())!=null) {
System.out.println(line);
}
br.close();
}
}
3.特殊流
3.1标准I/O流
System类中有两个静态的成员变量
public static final InputStream in:标准输入流。通常该流对应于键盘输入或由主机环境或用户指定的另一个输入源。
public static final PrintStream out:标准输出流。通常该流对应于显示输出或由主机环境或用户指定的另一个输出目标。
我们最常用的System.out,本质上就是一个标准输出流
3.2打印流
特点:
(1)只负责输出数据,不负责读取数据
(2)永远不会抛出IOException
(3)有自己的特有方法
3.2.1字节打印流(PrintStream)
PrintStream(String fileName):使用指定的文件名创建新的打印流。
public static void setOut(PrintStream out):重新分配“标准”输出流,改变输出语句的目的地。
使用继承父类的方法写数据,查看的时候会转码;使用自己的特有方法写数据,查看的数据原样输出。
public class PrintStreamDemo {
public static void main(String[] args) throws IOException {
//PrintStream(String fileName):使用指定的文件名创建新的打印流
PrintStream ps = new PrintStream("myOtherStream\\ps.txt");
//写数据
//字节输出流有的方法
// ps.write(97);
//使用特有方法写数据
// ps.print(97);
// ps.println();
// ps.print(98);
ps.println(97);
ps.println(98);
//释放资源
ps.close();
}
}
3.2.2字符打印流(PrintWriter)
构造方法
方法名 | 说明 |
---|---|
PrintWriter(String fileName) | 使用指定的文件名创建一个新的PrintWriter,而不需要自动执行刷新 |
PrintWriter(Writer out, boolean autoFlush) | 创建一个新的PrintWriter out:字符输出流 autoFlush: 一个布尔值,如果为真,则println , printf ,或format方法将刷新输出缓冲区 |
public class PrintWriterDemo {
public static void main(String[] args) throws IOException {
//PrintWriter(String fileName) :使用指定的文件名创建一个新的PrintWriter,而不需要自动执行行刷新
// PrintWriter pw = new PrintWriter("myOtherStream\\pw.txt");
// pw.write("hello");
// pw.write("\r\n");
// pw.flush();
// pw.write("world");
// pw.write("\r\n");
// pw.flush();
// pw.println("hello");
/*
pw.write("hello");
pw.write("\r\n");
*/
// pw.flush();
// pw.println("world");
// pw.flush();
//PrintWriter(Writer out, boolean autoFlush):创建一个新的PrintWriter,自动刷新
PrintWriter pw = new PrintWriter(new FileWriter("myOtherStream\\pw.txt"),true);
// PrintWriter pw = new PrintWriter(new FileWriter("myOtherStream\\pw.txt"),false);
pw.println("hello");
/*
pw.write("hello");
pw.write("\r\n");
pw.flush();
*/
pw.println("world");
pw.close();
}
}
3.3对象序列化/反序列化流
序列化和反序列化是将一个对象转换成字节流以及从字节流恢复为对象的过程。
3.3.1序列化流ObjectOutputStream
构造方法
方法名 | 说明 |
---|---|
ObjectOutputStream(OutputStream out) | 创建一个写入指定的OutputStream的ObjectOutputStream |
序列化方法
方法名 | 说明 |
---|---|
void writeObject(Object obj) | 将指定的对象写入ObjectOutputStream |
对象要想被序列化,该对象所属的类必须实现Serializable 接口,标记该类是可序列化的,只是一个标记,不需要重写任何东西。
transient修饰符
用于修饰不想被序列化的成员,给该成员变量加transient关键字修饰,该关键字标记的成员变量不参与序列化过程,后续反序列化访问该属性时,会得到其默认初始值。
3.3.2反序列化流ObjectInputStream
ObjectInputStream反序列化先前,需要先使用ObjectOutputStream将对象及其数据进行序列化。
构造方法
方法名 | 说明 |
---|---|
ObjectInputStream(InputStream in) | 创建从指定的InputStream读取的ObjectInputStream |
反序列化方法
方法名 | 说明 |
---|---|
Object readObject() | 从ObjectInputStream读取一个对象 |
版本ID(serialVersionUID)
用对象序列化流序列化了一个对象后,假如我们修改了对象所属的类文件,读取数据会抛出InvalidClassException异常
解决办法:
重新序列化
给对象所属的类加一个serialVersionUID
//对象实体类
public class Student implements Serializable {
private static final long serialVersionUID = 42L;
private String name;
// private int age;
private transient int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
// @Override
// public String toString() {
// return "Student{" +
// "name='" + name + '\'' +
// ", age=" + age +
// '}';
// }
}
//测试类
public class ObjectStreamDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
write();
read();
}
//反序列化
private static void read() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("myOtherStream\\oos.txt"));
Object obj = ois.readObject();
Student s = (Student) obj;
System.out.println(s.getName() + "," + s.getAge());
ois.close();
}
//序列化
private static void write() throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("myOtherStream\\oos.txt"));
Student s = new Student("林青霞", 30);
oos.writeObject(s);
oos.close();
}
}
4.Properties集合
Properties是一个Map体系的集合类
Properties可以保存到流中或从流中加载
属性列表中的每个键及其对应的值都是一个字符串
4.1Properties作为Map集合的特有方法
方法名 | 说明 |
---|---|
Object setProperty(String key, String value) | 设置集合的键和值,都是String类型,底层调用 Hashtable方法 put |
String getProperty(String key) | 使用此属性列表中指定的键搜索属性 |
Set<String> stringPropertyNames() | 从该属性列表中返回一个不可修改的键集,其中键及其对应的值是字符串 |
public class PropertiesDemo02 {
public static void main(String[] args) {
//创建集合对象
Properties prop = new Properties();
//Object setProperty(String key, String value):设置集合的键和值,都是String类型,底层调用Hashtable方法put
prop.setProperty("itheima001", "林青霞");
/*
Object setProperty(String key, String value) {
return put(key, value);
}
Object put(Object key, Object value) {
return map.put(key, value);
}
*/
prop.setProperty("itheima002", "张曼玉");
prop.setProperty("itheima003", "王祖贤");
//String getProperty(String key):使用此属性列表中指定的键搜索属性
// System.out.println(prop.getProperty("itheima001"));
// System.out.println(prop.getProperty("itheima0011"));
// System.out.println(prop);
//Set<String> stringPropertyNames():从该属性列表中返回一个不可修改的键集,其中键及其对应的值是字符串
Set<String> names = prop.stringPropertyNames();
for (String key : names) {
// System.out.println(key);
String value = prop.getProperty(key);
System.out.println(key + "," + value);
}
}
}
4.2Properties和IO流相结合的方法
方法名 | 说明 |
---|---|
void load(InputStream inStream) | 从输入字节流读取属性列表(键和元素对) |
void load(Reader reader) | 从输入字符流读取属性列表(键和元素对) |
void store(OutputStream out, String comments) | 将此属性列表(键和元素对)写入此 Properties表中,以适合于使用 load(InputStream)方法的格式写入输出字节流 |
void store(Writer writer, String comments) | 将此属性列表(键和元素对)写入此 Properties表中,以适合使用 load(Reader)方法的格式写入输出字符流 |
public class PropertiesDemo03 {
public static void main(String[] args) throws IOException {
//把集合中的数据保存到文件
// myStore();
//把文件中的数据加载到集合
myLoad();
}
private static void myLoad() throws IOException {
Properties prop = new Properties();
//void load(Reader reader):
FileReader fr = new FileReader("myOtherStream\\fw.txt");
prop.load(fr);
fr.close();
System.out.println(prop);
}
private static void myStore() throws IOException {
Properties prop = new Properties();
prop.setProperty("itheima001","林青霞");
prop.setProperty("itheima002","张曼玉");
prop.setProperty("itheima003","王祖贤");
//void store(Writer writer, String comments):
FileWriter fw = new FileWriter("myOtherStream\\fw.txt");
prop.store(fw,null);
fw.close();
}
}