多线程基础
一、线程相关概念
- 程序:是为完成特定任务、用某种语言编写的一组指令的集合。简单的说:就是我们写的代码。
- 进程:指运行中的程序
- 比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间;
- 进程是程序的一次执行过程,或是正在运行的一个程序;
- 进程是动态过程:有它自身的产生、存在和消亡的过程。
- 线程
- 线程有进程创建,是进程的一个实体;
- 一个进程可以有多个线程。
- 单线程:同一时刻,只允许执行一个线程。
- 多线程:同一个时刻,可以执行多个线程,比如:一个QQ进程,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件。
- 并发:同一时刻,多个任务交替执行,造成一种“貌似同时”的错觉,单核CPU的多任务就是并发。
- 并行:同一时刻,多个任务同时执行,多核CPU可以实现并行。
二、创建线程的两种方法
在Java中,创建线程有两种方法:
- 继承Thread类,重写run方法(Thread类也实现了Runnable接口);
- 实现Runnable接口,重写run方法。
继承Thread类
- 当一个类继承了 Thread 类,该类就可以当做线程使用;
- 我们会重写 run 方法,写上自己的业务代码;
- Thread 类 实现了 Runnable 接口的 run 方法。
- 关于
run()
方法:run()
方法只是一个普通方法,没有真正的启动一个线程,真正启动线程的方法是start()
方法;- 当调用线程对象的
start()
方法后会启动线程,无需等待run()
方法中的业务代码执行完毕,此时线程进入就绪态; - 当CPU调度使当前线程为运行线程时,该线程就进入了运行态,此时开始执行
run()
方法中的语句; start()
方法会调用start0()
方法,start0()
是本地方法,是 JVM 调用, 底层是 c/c++实现。
实现Runnable接口
- Java是单继承的,在某些情况下一个类可能已经继承了某个父类,这时在用继承Thread类方法来创建线程显然不可能了;
- java设计者们提供了另外一个方式创建线程,就是通过实现Runnable接口来创建线程;
- 实现
Runnable
接口的线程对象不能直接调用start()
方法,需要创建Thread对象,把线程对象(实现 Runnable),放入Thread才可以调用线程相关方法。
代码演示:
package Thread;
public class test {
public static void main(String[] args) {
//继承Thread类启动线程
Cat cat = new Cat();
cat.start();
//实现Runnable接口启动线程
Dog dog = new Dog();
//dog.start(); 这里不能调用 start
//创建了Thread对象,把dog对象(实现 Runnable),放入Thread
Thread thread = new Thread(dog);
thread.start();
}
}
class Cat extends Thread
{
int cnt = 0;
@Override
public void run() {
while (cnt < 5)
{
System.out.println("猫咪叫~~"+ (++cnt) + " " + "线程名:"+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Dog implements Runnable
{
int cnt = 0;
@Override
public void run() {
while (cnt < 5)
{
System.out.println("小狗叫~~"+ (++cnt) + " " + "线程名:"+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
两种方法的区别
- 从Java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,从JDK帮助文档中我们可以看到Thread类本身就实现了Runnable接口;
- 实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制,建议使用Runnable。
线程终止
可以通过为线程对象设置变量来通知线程终止。
如下代码演示:
package Thread;
public class test {
public static void main(String[] args) throws InterruptedException {
Dog dog = new Dog();
Thread thread = new Thread(dog);
thread.start();
System.out.println("线程等待2秒后退出...");
thread.sleep(2000);
dog.setLoop(false);//设置变量通知线程退出
}
}
class Dog implements Runnable
{
private boolean loop = true;
@Override
public void run() {
while (loop)
{
System.out.println("小狗叫~~"+" " + "线程名:"+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void setLoop(boolean loop) {
this.loop = loop;
}
}
三、线程常用方法
第一组
setName
:设置线程名称,使之与参数name相同getName
:返回该线程的名称start
:使该线程开始执行,Java虚拟机底层调用该线程的start0()
方法run
:调用线程对象run方法;setPriority
:更改线程的优先级getPriority
:获取线程的优先级sleep
:线程的静态方法,在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)interrupt
:中断线程,但并没有真正的结束线程,所以一般用于中断正在休眠线程getState()
:获取线程的状态
第二组
yield
:线程的礼让。让出CPU,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功join
:线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务
用户线程和守护线程
- 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束;
- 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束,常见的守护线程:垃圾回收机制。
- 将线程设置为守护线程:用
setDaemon(true)
设置。 - 代码演示:
package Thread;
public class test {
public static void main(String[] args) throws InterruptedException {
Dog dog1 = new Dog();
//dog.start(); 这里不能调用 start
//创建了Thread对象,把dog对象(实现 Runnable),放入Thread
Thread thread1 = new Thread(dog1);
//将thread1设置为守护线程,当main线程结束时,守护线程自动结束
thread1.setDaemon(true);
thread1.start();
for (int i = 0; i < 10; i++) {
Thread.sleep(1000);
System.out.println("main线程运行中..."+ " 线程名:"+Thread.currentThread().getName());
}
}
}
class Dog implements Runnable
{
int cnt = 0;
@Override
public void run() {
while (true)
{
System.out.println("守护线程运行中..."+ (++cnt) + " " + "线程名:"+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
四、线程的状态
JDK 中用
Thread.State
枚举了表示线程的几种状态NEW
:尚未启动的线程处于此状态。RUNNABLE
在Java虚拟机中执行的线程处于此状态。BLOCKED
被阻塞等待监视器锁定的线程处于此状态。WAITING
正在等待另一个线程执行特定动作的线程处于此状态。TIMED_WAITING
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。TERMINATED
已退出的线程处于此状态。
用
getState()
来获取当前线程的状态。线程状态转化图
五、线程同步机制
Synchronized
线程同步
- 在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性。
- 也可以这里理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作。
具体同步方法:
Synchronized
同步代码块:得到对象的锁才能操作同步代码
synchronized(对象){
// 需要被同步的代码
}
同步方法
public synchronized void m(String name){
// 需要被同步的代码
}
互斥锁
基本介绍
- Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性;
- 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象;
- 关键字
synchronized
与对象的互斥锁联系。当某个对象用synchronized
修饰时,表明该对象在任一时刻只能由一个线程访问; - 同步的局限性:导致程序的执行效率要降低;
- 同步方法(非静态的)的锁可以是this(当前类对象),也可以是其他对象(要求是同一个对象);
- 同步方法(静态的)的锁为当前类本身,因为静态类加载时可能无对象。
注意事项
- 当修饰静态方法时,锁定的是当前类的
Class 对象
(类锁) - 当修饰非静态方法时,锁定的是当前实例对象的
this
(实例锁)
- 当修饰静态方法时,锁定的是当前类的
实现的步骤
- 先分析上锁的代码;
- 选择同步代码块或同步方法;
- 要求多个线程的锁的对象为同一个即可。
互斥锁的几种实现方法
主函数
public class test {
public static void main(String[] args) throws InterruptedException {
tickets1 tickets1 = new tickets1();
new Thread(tickets1, "火车票窗口A").start();
new Thread(tickets1, "火车票窗口B").start();
new Thread(tickets1, "火车票窗口C").start();
//第二种票
Thread.sleep(2000);
tickets2 tickets2 = new tickets2();
new Thread(tickets2, "飞机票窗口A").start();
new Thread(tickets2, "飞机票窗口B").start();
new Thread(tickets2, "飞机票窗口C").start();
//第三种票
Thread.sleep(2000);
tickets3 tickets3 = new tickets3();
new Thread(tickets3, "高铁票窗口A").start();
new Thread(tickets3, "高铁票窗口B").start();
new Thread(tickets3, "高铁票窗口C").start();
}
}
- 同步方法
class tickets1 implements Runnable {
private static int nums = 30;
static boolean flag = true;
@Override
public void run() {
while (flag) {
sell();
}
}
//同步方法,只能保证一个对象的线程同步,若有多个对象,则不能保证同步
public synchronized void sell() {
if (nums <= 0) {
System.out.println("火车票已卖完!");
flag = false;
return;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 售出火车一张票," + " 剩余票数=" + (--nums));
}
}
- 实例锁
class tickets2 implements Runnable {
Object o = new Object();
private static int nums = 30;
static boolean flag = true;
@Override
public void run() {
while (flag) {
//当只有一个对象的线程时,可以使用synchronized(o/this)实现线程同步,o可以不为static
//若有多个对象时,应对同一个对象加锁,static Object o = new Object();synchronized(o)
synchronized (/*o*/ this) {
if (nums <= 0) {
System.out.println("飞机票已卖完!");
flag = false;
return;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 售出一飞机张票," + " 剩余票数=" + (--nums));
}
}
}
}
- 类锁
class tickets3 implements Runnable {
private static int nums = 30;
static boolean flag = true;
@Override
public void run() {
while (flag) {
//对静态方法同步时,用类锁,即tickets3.class(类对象)
synchronized (tickets3.class) {
if (nums <= 0) {
System.out.println("高铁票已卖完!");
flag = false;
return;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 售出一高铁张票," + " 剩余票数=" + (--nums));
}
}
}
}
死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
- 产生的必要条件
- 互斥使用,即当资源被一个线程占用时,别的线程不能使用;
- 不可抢占,资源请求者不能强制从资源占有者手中抢夺资源,资源只能由占有者主动释放;
- 请求和保持,当资源请求者在请求其他资源的同时保持对原因资源的占有;
- 循环等待,多个线程存在环路的锁依赖关系而永远等待下去,例如T1占有T2的资源,T2占有T3的资源,T3占有T1的资源,这种情况可能会形成一个等待环路。
- 代码模拟死锁
package Thread;
public class test {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new DeadLock(1));
Thread thread2 = new Thread(new DeadLock(0));
thread1.setName("线程A");
thread2.setName("线程B");
thread1.start();
thread2.start();
}
}
class DeadLock implements Runnable
{
private int flag;
static Object o1 = new Object();
static Object o2 = new Object();
public DeadLock(int flag) {
this.flag = flag;
}
@Override
public void run() {
if(flag == 1)
{
synchronized (o1)
{
System.out.println(Thread.currentThread().getName()+"进入1");
synchronized (o2) //获取o2的监视权
{
System.out.println(Thread.currentThread().getName()+"进入2");
}
}
}
if(flag == 0)
{
synchronized (o2)
{
System.out.println(Thread.currentThread().getName()+"进入3");
synchronized (o1)
{
System.out.println(Thread.currentThread().getName()+"进入4");
}
}
}
}
}
释放锁
- 下面操作会释放锁
- 当前线程的同步方法、同步代码块执行结束;
- 当前线程在同步代码块、同步方法中遇到break、return;
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束;
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释
放锁。
- 下面操作不会释放锁
- 线程执行同步代码块或同步方法时,程序调用
Thread.sleep()
、Thread.yield()
方法暂停当前线程的执行,不会释放锁; - 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁。提示:应尽量避免使用suspend()和resume()来控制线程,方法不再推荐使用。
- 线程执行同步代码块或同步方法时,程序调用
Lock
- 基本介绍
- 从JDK 5.0开始,Java提供了更强大的线程同步机制–通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。ReentrantLock
类实现了Lock,它拥有与synchronized
相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock
,可以显式加锁、释放锁,- 注意:如果同步代码有异常,要将unlock()写入finally语句块
- 使用方法
class A{
private final ReentrantLock lock = new ReenTrantLock();
public viod m()
{
lock.lock();
try{
//保证线程安全的代码
}
finally{
lock.unlock();
}
}
}
- 与
synchronized
比较- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放;
- Lock只有代码块锁,
synchronized
有代码块锁和方法锁 - 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。
IO流
IO流体系图
一、File类
- 获取文件的相关信息
file.getName()
:文件名字file.getAbsolutePath()
:文件绝对路径file.getParent()
:文件父级目录file.length()
:文件大小(字节)file.exists()
:文件是否存在file.isFile()
:判断是不是一个文件file.isDirectory()
:判断是不是一个目录
- 新建和删除文件
public boolean createNewFile()
:创建文件。若文件存在,则不创建,返回falsepublic boolean mkdir()
:创建一级目录public boolean mkdirs()
:创建多级目录public boolean delete()
:删除空目录或文件(Java中的删除不走回收站)
二、IO流
流的分类
按操作数据单位不同分为:字节流(8 bit)二进制文件,字符流(按字符)文本文件
按数据流的流向不同分为:输入流,输出流
按流的角色的不同分为:节点流,处理流/包装流
字节流和字符流
(抽象基类) 字节流 字符流 输入流 InputStream Reader 输出流 OutputStream Writer - Java的IO流共涉及40多个类,实际上非常规则,都是从如上4个抽象基类派生的。
- 由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。
IO流的原理即流的分类
InputStream & Reader
InputStream 和 Reader 是所有输入流的基类。
程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件IO资源
- InputStream
int read()
:从输入流中读取数据的下一个字节。返回 0 到 255 范围内的 int 字节值。如果因
为已经到达流末尾而没有可用的字节,则返回值 -1。int read(byte[] b)
:从此输入流中将最多b.length
个字节的数据读入一个 byte 数组中。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。否则以整数形式返回实际读取的字节数。int read(byte[] b, int off,int len)
:将输入流中最多len
个数据字节读入 byte 数组。尝试读取len
个字节,但读取的字节也可能小于该值。以整数形式返回实际读取的字节数。如果因为流位于文件末尾而没有可用的字节,则返回值 -1。public void close() throws IOException
:关闭此输入流并释放与该流关联的所有系统资源。
- Reader
int read()
:读取单个字符。作为整数读取的字符,范围在 0 到 65535 之间 (0x00-0xffff)(2个字节的Unicode码),如果已到达流的末尾,则返回 -1int read(char[] cbuf)
:将字符读入数组。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。int read(char[] cbuf,int off,int len)
:将字符读入数组的某一部分。存到数组cbuf中,从off处开始存储,最多读len个字符。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。public void close() throws IOException
:关闭此输入流并释放与该流关联的所有系统资源。
OutputStream & Writer
- OutputStream 和 Writer 是所有输出流的基类。
- OutputStream
void write(int b)
:将指定的字节写入此输出流。write 的常规协定是:向输出流写入一个字节。要写入的字节是参数 b 的八个低位。b 的 24 个高位将被忽略。 即写入0~255范围的。void write(byte[] b)
:将b.length
个字节从指定的 byte 数组写入此输出流。write(b) 的常规协定是:应该与调用write(b, 0, b.length)
的效果完全相同。void write(byte[] b,int off,int len)
:将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此输出流。public void flush()throws IOException
:刷新此输出流并强制写出所有缓冲的输出字节,调用此方法指示应将这些字节立即写入它们预期的目标。public void close() throws IOException
:关闭此输出流并释放与该流关联的所有系统资源。
- Writer
void write(int c)
:写入单个字符。要写入的字符包含在给定整数值的 16 个低位中,16 高位被忽略。 即写入0 到 65535 之间的Unicode码。void write(char[] cbuf)
:写入字符数组。void write(char[] cbuf,int off,int len)
:写入字符数组的某一部分。从off开始,写入len个字符void write(String str)
:写入字符串。void write(String str,int off,int len)
:写入字符串的某一部分。void flush()
:刷新该流的缓冲,则立即将它们写入预期目标。public void close() throws IOException
:关闭此输出流并释放与该流关联的所有系统资源。
节点流(文件流)
- 读取文件
- 创建一个对象流,将已存在的文件加载进流:
FileReader fr = new FileReader(new File(“Test.txt”));
- 创建一个临时存放数据的数组:
char[] ch = new char[1024];
- 调用流对象的读取方法将流中的数据读入到数组中:
fr.read(ch);
- 关闭资源:
fr.close();
- 代码演示:
- 创建一个对象流,将已存在的文件加载进流:
FileReader fr = null;
try{
fr=new FileReader(new File("c:\\test.txt"));
char[]buf=new char[1024];
int len;
while((len=fr.read(buf))!=-1){
System.out.print(new String(buf,0,len));
}
}catch(IOException e){
System.out.println("read-Exception :"+e.getMessage());
}finally{
if(fr!=null){
try{
fr.close();
}catch(IOException e){
System.out.println("close-Exception :"+e.getMessage());
}
}
}
- 写入文件
- 创建流对象,建立数据存放文件:
FileWriter fw = new FileWriter(new File(“Test.txt”));
- 调用流对象的写入方法,将数据写入流:
fw.write("HelloWorld!");
- 关闭流资源,并将流中的数据清空到文件中:
fw.close();
- 代码演示
- 创建流对象,建立数据存放文件:
FileWriter fw = null;
try {
fw = new FileWriter(new File("Test.txt"));
fw.write("atguigu-songhongkang");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fw != null)
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
- 注意事项
- 定义文件路径时,可以用”
/
“或者”\\
“。 - 在写入一个文件时,如果使用构造器
FileOutputStream(file)
,则目录下有同名文件将被覆盖。 - 如果使用构造器
FileOutputStream(file,true)
,则目录下的同名文件不会被覆盖,在文件内容末尾追加内容。 - 在读取文件时,必须保证该文件已存在,否则报异常。
- 字节流操作字节,比如:
.mp3, .avi, .rmvb, mp4, .jpg, .doc, .ppt
- 字符流操作字符,只能操作普通文本文件。最常见的文本文件:
.txt, .java, .c,.cpp
等语言的源代码。尤其注意.doc,excel,ppt这些不是文本文件。
- 定义文件路径时,可以用”
缓冲流
基本介绍
为了提高数据读写的速度,Java API提供了带缓冲功能的流类,在使用这些流类
时,会创建一个内部缓冲区数组,缺省使用**8192个字节(8Kb)**的缓冲区。缓冲流要“套接”在相应的节点流之上,根据数据操作单位可以把缓冲流分为:
BufferedInputStream
和BufferedOutputStream
BufferedReader
和BufferedWriter
注意事项
- 当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区;
- 当使用
BufferedInputStream
读取字节文件时,BufferedInputStream
会一次性从文件中读取8192个(8Kb),存在缓冲区中,直到缓冲区用完了,才重新从文件中读取下一个8192个字节数组; - 向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区写满,
BufferedOutputStream
才会把缓冲区中的数据一次性写到文件里。使用方法flush()
可以强制将缓冲区的内容全部写入输出流; - 关闭流的顺序和打开流的顺序相反。只要关闭最外层流即可,关闭最外层流也会相应关闭内层节点流;
flush()
方法的使用:手动将buffer中内容写入文件;- 如果是带缓冲区的流对象的
close()
方法,不但会关闭流,还会在关闭流之前刷新缓冲区,关闭后不能再写出。
代码演示
BufferedReader br = null;
BufferedWriter bw = null;
try {
// 创建缓冲流对象:它是处理流,是对节点流的包装
br = new BufferedReader(new FileReader("d:\\IOTest\\source.txt"));
bw = new BufferedWriter(new FileWriter("d:\\IOTest\\dest.txt"));
String str;
while ((str = br.readLine()) != null) { // 一次读取字符文本文件的一行字符
bw.write(str); // 一次写入一行字符串
bw.newLine(); // 写入行分隔符
}
bw.flush(); // 刷新缓冲区
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭IO流对象
try {
if (bw != null) {
bw.close(); // 关闭过滤流时,会自动关闭它所包装的底层节点流
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (br != null) {
br.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
转换流
- 转换流提供了在字节流和字符流之间的转换
- Java API提供了两个转换流:
InputStreamReader
:(Reader的子类)InputStream转换为ReaderOutputStreamWriter
:(Writer的子类)OutputStream转换为Writer- 字节流中的数据都是字符时,转成字符流操作更高效。
- 很多时候我们使用转换流来处理文件乱码问题,实现编码和解码的功能。
InputStreamReader
实现将字节的输入流按指定字符集转换为字符的输入流,若不指定则默认。
需要和InputStream“套接”。
构造器:
public InputStreamReader(InputStream in)
public InputSreamReader(InputStream in,String charsetName)
如:
Reader isr = new InputStreamReader(System.in,"gbk");
OutputStreamWriter
- 实现将字符的输出流按指定字符集转换为字节的输出流。
- 需要和OutputStream“套接”。
- 构造器
public OutputStreamWriter(OutputStream out)
public OutputSreamWriter(OutputStream out,String charsetName)
代码演示
public void testMyInput () throws Exception
{
FileInputStream fis = new FileInputStream("1.txt");
FileOutputStream fos = new FileOutputStream("2.txt");
InputStreamReader isr = new InputStreamReader(fis, "GBK");
OutputStreamWriter osw = new OutputStreamWriter(fos, "GBK");
BufferedReader br = new BufferedReader(isr);
BufferedWriter bw = new BufferedWriter(osw);
String str = null;
while ((str = br.readLine()) != null) {
bw.write(str);
bw.newLine();
bw.flush();
}
bw.close();
br.close();
}
对象流
引入:
有如下需求:
将int num= 100 这个int数据保存到文件中,注意不是100数字,而是int 100,并且,能够从文件中直接恢复int 100。
将Dog dog = new Dog(”小黄”,3) 这个 dog对象 保存到 文件中,并且能够从文件恢复。
上面的要求,就是能够将 基本数据类型或者对象进行序列化和反序列化操作
- 序列化与反序列化示意图
- 基本介绍
- 功能:提供了对基本类型或对象类型的序列化和反序列化的方法
ObjectOutputStream
提供序列化功能ObjectInputStream
提供反序列化功能
- 使用细节
- 读写顺序要一致;
- 要求序列化或反序列化对象,需要实现
Serializable
; - 序列化的类中建议添加
SerialVersionUID
,为了提高版本的兼容性; - 序列化对象时,默认将里面所有属性都进行序列化,但除了static或transient修饰的成员;
- 序列化对象时,要求里面属性的类型也需要实现序列化接口;
- 序列化具备可继承性,也就是如果某类已经实现了序列化,则它的所有子类也已经默认实现了序列化。
- 序列化代码演示
/*
需求:
使用ObjectOutputStream 序列化 基本数据类型和一个 Dog对象(name,age),并保存到data.dat文件中
*/
package demo;
import java.io.*;
public class test {
public static void main(String[] args) throws Exception {
//序列化后,保存的文件格式,不是存文本,而是按照他的格式来保存
String filePath = "src\\data.dat";
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));
//序列化数据到 e:\data.dat
oos.writeInt(100);// int -> Integer (实现了 Serializable)
oos.writeBoolean(true);// boolean -> Boolean (实现了 Serializable)
oos.writeChar('a');// char -> Character (实现了 Serializable)
oos.writeDouble(9.5);// double -> Double (实现了 Serializable)
oos.writeUTF("你好世界!");//String
//保存一个 dog 对象
oos.writeObject(new Dog("POP", 10, "日本", "白色"));
oos.close();
System.out.println("数据保存完毕(序列化形式)");
}
}
class Dog implements Serializable
{
String name;
int age;
String country;
String colour;
public Dog(String name, int age, String country, String colour) {
this.name = name;
this.age = age;
this.country = country;
this.colour = colour;
}
}
- 反序列化代码演示
/*
需求:
使用ObjectlnputStream 读取 data.dat 并反序列化恢复数据
*/
package demo;
import java.io.*;
public class test {
public static void main(String[] args) throws Exception {
// 1.创建流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("src\\data.dat"));
// 2.读取, 注意顺序
System.out.println(ois.readInt());
System.out.println(ois.readBoolean());
System.out.println(ois.readChar());
System.out.println(ois.readDouble());
System.out.println(ois.readUTF());
System.out.println(ois.readObject());
// 3.关闭
ois.close();
System.out.println("以反序列化的方式读取(恢复)ok~");
}
}
class Dog implements Serializable
{
String name;
int age;
String country;
String colour;
public Dog(String name, int age, String country, String colour) {
this.name = name;
this.age = age;
this.country = country;
this.colour = colour;
}
}
/*
输出:
100
true
a
9.5
你好世界!
demo.Dog@54a097cc
以反序列化的方式读取(恢复)ok~
*/
*标准输入输出流
System.in
和System.out
分别代表了系统标准的输入和输出设备- 默认输入设备是:键盘,输出设备是:显示器
- System.in的类型是InputStream
- System.out的类型是PrintStream,其是FilterOutputStream 和OutputStream的子类
- 重定向:通过System类的setIn,setOut方法对默认设备进行改变。
public static void setIn(InputStream in)
public static void setOut(PrintStream out)
Properties类
引入:
有如下配置文件
mysql.properties
,内容为:ip=192.168.0.13 user=root pwd=12345
设计程序读取相关值。
- 基本介绍
- 专门用于读写配置文件的集合类;
- 配置文件的格式:
键=值
- 键值对无空格,值不需要用引号一起来,默认类型是String
- 常见方法
load
:加载配置文件的键值对到Properties对象list
:将数据显示到指定设备getProperty(key)
:根据键获取值setProperty(key,value)
:设置键值对到Properties对象store
:将Properties中的键值对存储到配置文件,在idea中,保存信息到配置文件,如果含有中文,会存储为unicode码
- 配置文件读取代码
package demo;
import java.io.*;
import java.util.Properties;
public class test {
public static void main(String[] args) throws Exception {
//1. 创建 Properties 对象
Properties properties = new Properties();
//2. 加载指定配置文件
properties.load(new FileReader("src\\mysql.properties"));
//3. 把 k-v 显示控制台
properties.list(System.out);
//4. 根据 key 获取对应的值
String user = properties.getProperty("user");
String pwd = properties.getProperty("pwd");
System.out.println("用户名=" + user);
System.out.println("密码是=" + pwd);
}
}
- 配置文件设置代码
package demo;
import java.io.*;
import java.util.Properties;
public class test {
public static void main(String[] args) throws Exception {
Properties properties = new Properties();
//创建
//1.如果该文件没有 key 就是创建
//2.如果该文件有 key ,就是修改
properties.setProperty("charset", "utf8");
properties.setProperty("user", "李华");//注意保存时,是中文的 unicode 码值
properties.setProperty("user", "Jack");//注意保存时,英文正常保存
properties.setProperty("pwd", "888888");
//将 k-v 存储文件中即可
properties.store(new FileOutputStream("src\\mysql2.properties"), null);
System.out.println("保存配置文件成功~");
}
}