11--字符流&缓冲区&编码表

1、字符流

IO流:按照操作的数据特点,将IO流分成两类:

     字节流:

              字节输入流:

                       InputStream:

                                 FileInputStream:

              字节输出流:

                       OutputStream:

                                 FileOutputStream:

     字符流:

              字符输入流:

              字符输出流:

1.1、字符输入流

Reader:字符输入流的超类(根类、基类)

FileReader:用来读取字符文件的便捷类。此类的构造方法假定默认字符编码默认字节缓冲区大小都是适当的。

字符输入流读取数据的底层:

         数据在存储设备上是以二进制(字节数据)存放的。字符输入流的底层其实使用的依然是字节输入流。底层先使用字节流从文件中获取字节数据。然后根据当前系统的编码表特点将某些字节合并在一起转成一个字符。最后字符输入流读取到程序中的数据是字符数据,但是在读取的过程中需要将字节转成字符。

默认的字符编码:如果使用的开发工具没有进行编码表的特殊设置,一般使用的都是系统默认的编码表。中文简体版,使用的编码表默认GBK。

默认的字节缓冲区:字符输入流的内部会有一个临时的字节数组(默认8192空间),将字节流读取的字节先临时存储在其中。然后根据编码表进行字节转字符的过程。

1.2、演示字符输入流

/*
 * 演示字符输入流:
 * 	read() : 一次只能读取一个字符数据。
 * 	read(char[] cbuf) : 一次读取多个字符数据.
 * 	read方法读取到文件末尾,返回的依然是-1.
 */
public class FileReaderDemo {
	public static void main(String[] args) throws IOException {
		demo3();
	}
	// 一次读取多个字符数据模版
	public static void demo3() throws IOException {
		// 1、创建输入流对象
		FileReader fr = new FileReader("e:/1.txt");				
		
		// 2、定义变量,记录每次读取的字符个数
		int len = 0;
		
		// 3、定义数组,用于保存每次读取的多个字符数据
		char[] cbuf = new char[1024];
		
		// 4、使用循环读取数据
		while(  ( len = fr.read( cbuf ) ) != -1 ){
			// 处理读取到的字符数据,数据在字符数组cbuf中,共计有len个有效的字符数据
			String s = new String( cbuf , 0 , len );
			System.out.println(s);
		}
		
		// 5、关流
		fr.close();
		
		
	}
	// 一次读取一个字符数据模版代码
	public static void demo2() throws IOException {
		// 1、创建输入流对象
		FileReader fr = new FileReader("e:/1.txt");		
		
		// 2、定义变量,保存每次读取的字符数据
		int ch = 0;
		
		// 3、使用循环读取数据
		while( ( ch = fr.read() ) != -1 ){
			// 处理读取到的字符数据,字符存放在ch变量中。
			System.out.println( (char)(ch) );
		}
		// 4、关流
		fr.close();
	}
	// FileReader的最基本读取
	public static void demo() throws IOException {
		
		// 创建输入流对象
		FileReader fr = new FileReader("e:/1.txt");
		
		int ch = fr.read();
		System.out.println(ch);
		
		ch = fr.read();
		System.out.println(ch);
		
		ch = fr.read();
		System.out.println(ch);
		
		ch = fr.read();
		System.out.println(ch);
		
		ch = fr.read();
		System.out.println(ch);
		
		// 关流
		fr.close();
	}
}

1.3、字符输出流

FileWriter:用来写入字符文件的便捷类。此类的构造方法假定默认字符编码默认字节缓冲区大小都是可接受的。

1.4、演示字符输出流

/*
 * 演示字符输出流
 */
public class FileWriterDemo {
	public static void main(String[] args) throws IOException {
		demo2();
	}
	// 演示写单个字符数据
	public static void demo2() throws IOException {
		//创建输出流对象
		FileWriter fw = new FileWriter("e:/fw.txt");
		
		for( int i = 50 ; i < 50000 ; i ++){
			fw.write(i);
		}
		
		fw.close();
	}
	// 写字符数据
	public static void demo() throws IOException {
		//创建输出流对象
		FileWriter fw = new FileWriter("e:/fw.txt");
		
		// 写数据
		fw.write("演示字符输出流");
		/*
		 * 字符输出流:在字符输出流的内部,需要先将要写的字符转成字节数据,
		 * 然后将字节数据存储在内部的字节数组中。
		 * 
		 * 如果在程序中:
		 * 	1、没有关闭流对象,
		 * 	2、字节数组没有被写满,
		 *  3、没有调用字符输出流的flush方法。
		 *  所有要写的数据被转成字节之后,全部都保存在字节数组中,根本就没有真正写到底层文件中。
		 * 
		 * 只要使用的是字符输出流:
		 * 	不管能不能调用close关闭流,再每次只要调用write方法之后,就调用一次flush方法。
		 * flush方法的作用:是将存放在字节数组中的数据刷新出去,目的能够及时写到底层文件。
		 */
		fw.flush();
		/*
		 *  关闭流 :
		 *  在调用close之前,close的内部也会先调用flush,将数组中的数据刷新到底层
		 *  然后才会关闭流对象。
		 */
		fw.close();
	}
}

1.5、字符流复制图片

/*
 * 演示使用字符流操作非字符格式的数据 :
 * 	注意:字符流不能操作非字符格式的数据。
 */
public class CopyFile {
	public static void main(String[] args) throws IOException {
		
		// 定义输入流读取数据
		FileReader fr = new FileReader("e:/1.jpg");
		// 定义输出流,写数据
		FileWriter fw = new FileWriter("c:/1.jpg");
		
		// 定义字符数组
		char[] cbuf = new char[1024];
		int len = 0;
		while( ( len = fr.read(cbuf) ) != -1 ){
			fw.write(cbuf, 0, len);
			// 写一次,刷新一次
			fw.flush();
		}
		// 关流
		fr.close();
		fw.close();
	}
}

1.6、flush和close

flush:它的作用是将缓冲区中的数据刷新到底层文件夹中。但是它不会将流关闭,也就说刷新完成之后,还以继续调用写的功能将数据往出写。

close:它是将流关闭。但是在关流之前也会调用flush刷新数据到底层。流一旦关闭,就无法在继续写数据。

1.7、编码表

字符流的底层: 字节流  +  编码表组成的。

编码表:它是将生活中的字符和计算机可以识别的二进制进行一一对应的的设置。这个表格就称为编码表。

ASCII表:它是专门为老美等国家使用的。将英文字符和二进制进行一一对应。

生活中文字                                       十进制                                                   二进制

a                                                                       97                                                   01100001

b                                                                        98                                                   01100010

A                                                                        65                                                   01000001

B                                                                        66                                                   01000010

 

ASCII表:它允许使用一个字节表示一个字符。并且这个字节的最高必须是0 , 0xxx xxxx

欧洲:通用码表ISO-8859-1(拉丁文编码表)。它也采用一个字节表示一个字符。但是最高位没有要求。因此它兼容ASCII1xxx xxxx 或者 0xxx xxxx

中国:GB2312、GBK、GB18030等。它们采用的2个字节表示一个汉字(字符)。它兼容ASCII

全球通用编码表:unicode:采用的是2个字节表示一个字符。它兼容ASCII

UTF-8:它根据数据的特点采用1、2、3字节表示一个字符。

必须记住的编码表:ASCIIISO-8859-1GB2312GBKUTF-8.

1.8、字符流的缓冲区问题

在FileReader和FileWriter的API中都在描述:

         它们的构造函数使用了本地默认的字符编码和默认的字节缓冲区。

FileReader:它的主要所用是用来读取字符数据。但是字符流是不能直接读取二进制。它的底层借助字节输入流,从底层读取字节数据,将读取到的字节数据存储在字节缓冲区中。当我们调用read方法的时候,FileReader就会根据当前系统默认的编码表,结合编码表的特点就可以从缓冲区中取出若干个字节将其转成字符数据。最终的read就会返回字符数据。

FileWriter:它的主要写字符数据。但是它也不能直接将字符写到文件中。所以在FileWriter的底层也是在借助字节输出流。将最后的字节写到底层。

当我们调用FileWriter中的write方法写字符的数据,其实根本就没有将数据写到底层去。其实需要写出的字符数据会使用FileWriter的默认编码表将字符转成字节,存储在字节缓冲区中。如果缓冲区写满、或者调用close、或者调用flush这时才会将缓冲区中的所有字节交给字节输出流写到底层。

2、编码、解码和乱码

2.1、编码介绍

编码:将字符转成字节数据的过程。字符-------》字节。将人能看懂的转成看不懂(二进制)。

代码测试:

         String充当字符

         byte[] 充当字节

/*
 * 演示编码 : 字符----->字节
 * 	字符:String 
 * 	字节:byte[]
 * 
 * GBK编码格式:
 * 	你:-60 11000100
 * 	   -29 11100011
 * 	好:-70 10111010
	   -61 11000011
 * 
 * UTF-8编码格式:
 * 	你:-28 11100100
 * 	   -67 10111101
       -96 10100000
 * 	好:-27 11100101
	   -91 10100101
	   -67 10111101
 * 
 * 任何的编码表的二进制都有自己的规律:
 * 	GBK格式的二进制:任何一个汉字都采用2个字节表示:
 * 		第一个字节:1xxx xxxx  
 * 		第二个字节:0|1xxx xxxx
 * 	utf-8格式的二进制:任何一个汉字都采用3个字节表示:
 * 		第一个字节:1110 xxxx
 * 		第二个字节:10xx xxxx
 * 		第三个字节:10xx xxxx
 * 
 *  如果UTF-8采用2个字节表示某个字符:
 *  	第一个字节:110x xxxx
 * 		第二个字节:10xx xxxx
 * 	如果utf-8采用1个字节表示某个字符:
 * 		一个字节:0xxx xxxx
 * 
 */
public class Demo {
	public static void main(String[] args) throws UnsupportedEncodingException {

		// 演示编码
		String s = "你好";

		// 将字符串转成字节数组(编码)
		byte[] bs = s.getBytes();

		for (int i = 0; i < bs.length; i++) {
			System.out.print(bs[i] + ",");
		}
		System.out.println();
		byte[] bs2 = s.getBytes( "GBK" );

		for (int i = 0; i < bs2.length; i++) {
			System.out.print(bs2[i] + ",");
		}
		
		System.out.println();
		byte[] bs3 = s.getBytes( "UTF-8" );

		for (int i = 0; i < bs3.length; i++) {
			System.out.print(bs3[i] + ",");
		}

	}
}

2.2、解码介绍

解码:将字节转成字符的过程。字节-------》字符。看不懂的(二进制)转成人可以看懂的。

/*
 * 介绍解码的过程:
 * 	字节----->字符
 */
public class DecodeDemo {
	public static void main(String[] args) throws UnsupportedEncodingException {
		
		// 字节数组 , GBK格式的二进制
		byte[] b = {-60 , -29 , -70 , -61 };
		
		/*
		 * 将字节数组转成字符串
		 *  使用字符串的构造方法完成
		 *  new String(b) : 使用的就是平台默认的编码表(GBK)
		 */
		String s = new String(b);
		System.out.println( s );
		
		/*
		 * 下面的字节数组中的给出的字节数据,是汉字使用的UTF-8编码之后的字节数据
		 */
		byte[] b2 = {-28 , -67 , -96 , -27 , -91 , - 67 };
		/*
		 * 解码的时候,使用的GBK
		 */
		String s2 = new String( b2 /* , "utf-8"*/);
		System.out.println(s2);
	}
}

2.3、乱码介绍

乱码:编码和解码的时候使用的编码表不一致导致了乱码数据。

/*
 * 演示乱码的解决:
 * 	 如果有乱码数据,先编码,编码的时候一定要用解码时的编码表(解码错时的那个编码表)。
 * 	 这时就得到了二进制的字节数据,然后再使用正确的编码表对二进制进行解码。
 */
public class Demo {
	public static void main(String[] args) throws IOException {
		
		// 字符数据
		String s = "你好";
		// 编码 , 已经是字节数据
		byte[] bs = s.getBytes("UTF-8");
		// 解码  , 使用的GBK
		String s2 = new String( bs );
		System.out.println(s2);
		/*
		 * 现在的程序中:s2中已经是乱码数据了。需要对其进行处理
		 */
		// 编码,石宏解码时的错误的编码表进行编码
		byte[] bs2 = s2.getBytes();
		// 再进行解码
		String s3 = new String( bs2 , "UTF-8" );
		System.out.println(s3);
		
		/*
		 * 乱码的处理,经常使用下面代码完成
		 */
		String s4 = new String(  s2.getBytes( "GBK" ) , "UTF-8" );
		System.out.println(s4);
	}
}

3、转换流

转换流的主要功能是完成字节和字符之间的转换功能。可以在转换的过程中人为的指定所用的编码表。

3.1、字节转字符的输入转换流

InputStreamReader 是字节流通向字符流的桥梁:它使用指定的 charset 读取字节并将其解码为字符。它使用的字符集可以由名称指定或显式给定,或者可以接受平台默认的字符集。

InputStreamReader的使用场景:当读取数据的时候,如果被读取的数据的编码格式不是系统默认的格式,这时就不能使用FileReader读取,只能通过InputStreamReader进行读取,并且指定对应的编码表。

/*
 * 演示 字节转字符的输入转换流:
 */
public class InputStreamReaderDemo {
	public static void main(String[] args) throws IOException {
		
		/*
		 * FileReader :它使用的本地默认的编码表(GBK)读取字符数据
		 * 现在1.txt文件采用的UTF-8格式保存的数据
		 * 不能直接使用FileReader类读取1.txt中的数据
		 */
		//FileReader fr = new FileReader("e:/1.txt");
		//System.out.println((char)fr.read());
		
		// 创建一个用于读取字节数据的字节输入流
		FileInputStream fis = new FileInputStream("e:/1.txt");
		/*
		 *  创建字节转字符的输入转换流:
		 *   new InputStreamReader(fis , "UTF-8") : 才能根据指定的编码表将字节转成字符
		 *   new InputStreamReader(fis ) :它使用的本地默认编码表,和FileReader没有区别
		 */
		InputStreamReader isr = new InputStreamReader(fis , "UTF-8");
		char[] cbuf = new char[1024];
		int len = 0 ;
		while( ( len = isr.read(cbuf) ) != -1 ){
			System.out.println( new String( cbuf , 0 , len ));
		}
		// 关流
		isr.close();
	}
}

3.2、字符转字节的输出转换流

OutputStreamWriter 是字符流通向字节流的桥梁:可使用指定的 charset 将要写入流中的字符编码成字节。它使用的字符集可以由名称指定或显式给定,否则将接受平台默认的字符集。

/*
 * 字符转字节的输出转换流:
 * 	当我们要输出的字符数据不使用本地默认的编码表将字符转成字节的时候,就需要指定编码表
 * 	将字符转成字节
 */
public class OutputStreamWriterDemo {
	public static void main(String[] args) throws IOException {
		
		// 创建字节输出流,用于将转换后的字节写出去的
		FileOutputStream fos = new FileOutputStream("e:/ows.txt");
		/*
		 *  创建字符转字节的输出转换流
		 *  new OutputStreamWriter( fos , "gbk" ) : 指定GBK编码表和不写编码表效果一样。功能就和FileWriter相同
		 *  new OutputStreamWriter( fos , "UTF-8" ) : 就会使用指定的编码表将字符转成字节
		 */
		OutputStreamWriter osw = new OutputStreamWriter( fos , "UTF-8" );
		// 写数据
		osw.write("你好");
		
		// 字符流,写一次,刷新一次
		osw.flush();
		
		// 关流
		osw.close();
	}
}

3.3、转换流的细节

不管是输入转换流还是输出转换流,它的构造函数都在接收字节流。为什么它的构造函数方法需要接收字节流呢?

转换流它们的功能仅仅是可以根据指定的编码表完成转换的,而不能和底层的字节进行读写操作。构造方法中传递的那个字节的主要作用就用来读写底层的字节数据。

读数据的时候编码表不一致使用InputStreamReader

写数据的时候编码表不一致使用OutputStreamWriter

4、Scanner类

4.1、Scanner介绍

Scanner类:它是JDK1.5时期出现的。可以完成读数据的操作。类似一个强大的输入流。可以从键盘读取数据、从文件中读取数据、可以从网络读取数据、可以从字符串中获取数据。

/*
 * 演示Scanner这个类
 */
public class ScannerDemo {
	public static void main(String[] args) throws IOException {
		/*
		 创建Scanner对象
		Scanner(InputStream source) , 使用的默认编码表
		键盘录入: 
		Scanner sc = new Scanner( System.in );
		String line = sc.nextLine();
		System.out.println(line);
		*/
		
		Scanner sc = new Scanner( new File("e:/ows.txt")  , "UTF-8" );
		while(  sc.hasNextLine()  ){
			String line = sc.nextLine();
			System.out.println(line);
		}
		// 关流
		sc.close();
		
		/*
		Scanner sc2 = new Scanner( new FileInputStream("e:/ows.txt") , "UTF-8" );
		String line2 = sc2.nextLine();
		System.out.println(line2);
		sc2.close();
		*/
		
	}
}

5、字符流缓冲区

5.1、缓冲区

缓冲区:它是一个临时存储数据的区域。目的是提高程序对数据的效率问题。

IO流(字节或字符流)缓冲区:它的缓冲区主要为了提供读写的效率。

字节流缓冲区:类的内部定义了一个byte数组,空间是8192个。

         字节输入流缓冲区:BufferedInputStream

         字节输出流缓冲区:BufferedOutputStream

字符流缓冲区:类的内部定义char数组,空间也是8192个。

         字符输入流缓冲区:BufferedReader

         字符输出流缓冲区:BufferedWriter

5.2、BufferedWriter

BufferedWriter构造方法中接受一个Writer对象,其实原因:缓冲区类本身只是为了对数据进行临时存储而已。所有的具有缓存功能的缓冲区类它们都不能和底层真正进行数据交互。因此它们的构造方法中都需要接受一个可以和底层交互的流对象。

/*
 * 演示字符输出流缓冲区的使用
 */
public class BufferedWriterDemo {
	public static void main(String[] args) throws IOException {
		
		// 创建字符输出流缓冲区对象
		BufferedWriter bufw = new BufferedWriter( new FileWriter("e:/bufw.txt") );
		// 写数据
		bufw.write("学习编程");
		/*
		 * BufferedWriter:类中的newLine方法,目的就是达到换行的。
		 * 	只是这个方法会根据不同的操作系统,使用适合当前系统的换行符号。
		 */
		bufw.newLine();
		bufw.write("正在学习的是Java");
		// 关流
		bufw.close();
		
	}
}

5.3、BufferedReader

BufferedReader类中的readLine方法等价于Sacnner读取文件时使用的nextLine方法。它们判断的依据:从第一个字符开始往后读取,遇到系统默认的行分隔符,才认为是一行结束了。如果没有行分隔符,它就一直获取字符数据。

/*
 * 演示字符输入流缓冲区, 
 * 主要目的是为使用其中的readLine方法,可以读取一行数据
 */
public class BufferedReaderDemo {
	public static void main(String[] args) throws IOException {
		
		// 创建缓冲区对象
		BufferedReader bufr = new BufferedReader( new FileReader("e:/bufw.txt") );
		/*
		 * 1、一次读取一个字符
		 * 2、一次读取多个字符
		 * 3、一次读取一行数据
		 */
		// 一次读取的上一行数据,需要定义String变量接受这一行数据
		String line = null;
		while( ( line = bufr.readLine() ) != null ){
			// 处理读取到的字符数据
			System.out.println(line);
		}
		// 关流
		bufr.close();
	}
}

6、序列化流

序列化、反序列化:它的主要功能是将堆内存中的对象写到文件中。或者从文件中读取已经写的对象。

例如:Person p = new Person(“赵四”,23);  通过IO流,将Person对象写文件中。

序列化:将对象写到文件中。

反序列化:将文件中的对象读取到内存中。

6.1、序列化

public class Person {
	private String name;
	private int age;
	public Person(String name, int age) {
		this.name = name;
		this.age = age;
	}
	@Override
	public String toString() {
		return "Person [name=" + name + ", age=" + age + "]";
	}

}
/*
 * 演示 对象的序列化 : 将new的对象(堆中),通过IO流写到文件中
 */
public class ObjectOutputStreamDemo {
	public static void main(String[] args) throws Exception {
		
		// 创建序列化流对象
		ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("e:/oos.obj") );
		
		// 创建对象
		Person p = new Person( "秋香" , 12);
		// 写对象
		oos.writeObject(p);
		
		// 关流
		oos.close();
	}
}

运行代码发生下面的异常:

异常原因:JDK中规定,如果一个类的对象要被序列化,要求这个对象所属的类必须实现序列化接口,如果没有实现就会发生序列化异常。

类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化

6.2、标记型接口

在介绍序列化接口的时候,发现接口中根本就没有方法。在Java中,有部分的要求不需要任何的方法来体现,但是有必须进行一定的区分。JAVA就定义了部分的接口,而接口中不提供任何方法,只要某个类实现这个接口,那么这个类就会具备一些特定的要求,没有实现接口,那么如果还想使用,程序运行就会出现异常。

上面说的这种接口就被称为标记型接口。

6.3、反序列化

/*
 * 演示反序列化
 */
public class ObjectInputStreamDemo {
	public static void main(String[] args) throws Exception {
		
		// 创建反序列化对象
		ObjectInputStream ois = new ObjectInputStream( new FileInputStream("e:/oos.obj") );
		try{
			Object obj = null;
			// 读取对象
			while( ( obj = ois.readObject() ) != null ){
				System.out.println(obj);
			}
		}catch( EOFException e){
			System.out.println("对象读取结束了");
		}
		// 关流
		ois.close();
	}
}

6.4、序列化和反序列化的问题

如果一个对象已经被序列化了,在序列化之后,对象所对应的class文件被修改过。这时使用反序列化读取被序列化的对象,发生下面的异常

Exception in thread "main" java.io.InvalidClassException: com.kuaixueit.seri.Person; local class incompatible: stream classdesc serialVersionUID = 5812231447154912992, local class serialVersionUID = 1889240669086691516

    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:621)

    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1623)

    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)

    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1774)

    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)

    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)

    at com.kuaixueit.seri.ObjectInputStreamDemo.main(ObjectInputStreamDemo.java:18)

这个异常的原因:

在反序列化的时候JVM会检查当前classpath下的class文件和流中读取的class文件是否一致。如果不一致就发生上面的异常。

解决异常:在定义类的时候,类实现了序列化接口的同时在类中添加这个类的序列化版本号。这样不管以后怎么去改这个类,版本号都是固定的。

6.5、必须掌握

  1. 被序列化的类一定要实现序列化接口。
  2. 在类中添加序列化版本号。

由于序列化是将对象在堆中的所有数据写到文件中。类中静态的内容(静态成员变量在方法区的静态区中)是不会被写出去的。

如果类中的某些数据不需要被序列化同时还是非静态的,这时可以给成员变量添加瞬态关键字修饰。

7、流的操作规律

         1、确定的操作的方向:确定到底是读数据,还是写数据。

                   读数据:输入流

                   写数据:输出流

         2、确定操作数据的格式:字节还是字符

                   可以确定最后使用的字节输入、字符输入、字节输出、字符输出。如果在不确定是什么数据,统一使用字节流。        

         3、读写数据的编码表是否一致(和默认编码表是否一致)。不一致,就需要使用的转换流

                   读:InputStreamReader、Scanner

                   写:OutputStreamWriter

         4、如果是希望能提供效率的读写,一般需要使用Buffered开始的缓冲区流

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

QB哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值