Java基础3

目录

1. 线程

主线程:执行主(main)方法的线程
单线程程序:java程序中只有一个线程
执行从main方法开始,从上到下依次执行

JVM执行main方法,main方法会进入到栈内存
JVM会找操作系统开辟一条main方法通向cpu的执行路径
cpu就可以通过这个路径来执行main方法
而这个路径有一个名字,叫main(主)线程

主线程和子线程没有主次之分,就算主线程执行完了,如果子线程没有执行完的话,子线程还是会继续执行

1.1 创建多线程程序的方式

1.1.1 自定义线程类继承自 Thread

创建多线程程序的第一种方式:创建Thread类的子类
java.lang.Thread类:是描述线程的类,我们想要实现多线程程序,就必须继承Thread类

实现步骤:
1.创建一个Thread类的子类
2.在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)
3.创建Thread类的子类对象
4.调用Thread类中的方法start方法,开启新的线程,执行run方法
void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
结果是两个线程并发地运行;当前线程(main线程)和另一个线程(创建的新线程,执行其 run 方法)。
多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
java程序属于抢占式调度,哪个线程的优先级高,那个线程优先执行;同一个优先级,随机选择一个执行

我们自己定义的线程

public class MyThread extends Thread{
	//2.重写run
	@Override
	public void run() {
		//3.任务代码
		for (int i = 1; i < 120; i++) {
			System.out.println("线程任务中的"+i);
		}
	}
}

调用我们自己定义的线程

public class ThreadDemo {
	public static void main(String[] args) {
		
		// 3.创建子类线程对象
		MyThread mt = new MyThread();
		// 设置守护线程
		mt.setDaemon(true);
		// 4.开启线程,JVM自动会告诉CPU去执行线程任务代码
		mt.start();
		
		
		for (int i = 1; i < 20; i++) {
			System.out.println("main线程"+i);
		}
	}
}

main线程1
线程任务中的0
main线程2
线程任务中的1
main线程3
线程任务中的2
main线程4
......

1.1.2 实现类去实现Runnable接口

创建多线程程序的第二种方式:实现Runnable接口
java.lang.Runnable
Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run 的无参数方法。
java.lang.Thread类的构造方法
Thread(Runnable target) 分配新的 Thread 对象。
Thread(Runnable target, String name) 分配新的 Thread 对象。

实现步骤:
1.创建一个Runnable接口的实现类
2.在实现类中重写Runnable接口的run方法,设置线程任务
3.创建一个Runnable接口的实现类对象
4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
5.调用Thread类中的start方法,开启新的线程执行run方法

实现Runnable接口创建多线程程序的好处:
1.避免了单继承的局限性
一个类只能继承一个类(一个人只能有一个亲爹),类继承了Thread类就不能继承其他的类
实现了Runnable接口,还可以继承其他的类,实现其他的接口
2.增强了程序的扩展性,降低了程序的耦合性(解耦)
实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)
实现类中,重写了run方法:用来设置线程任务
创建Thread类对象,调用start方法:用来开启新线程

实现Runnable接口,创建实现类

public class MyRunnable implements Runnable{
	//2.重写run方法
	@Override
	public void run() {
		//写任务代码
		for (int i = 1; i < 20; i++) {
			System.out.println("第二种创建线程的任务"+i);
		}
	}
}

调用,创建实现类对象

public class ThreadDemo {
	public static void main(String[] args) {
		// 3.创建实现类对象
		MyRunnable mr = new MyRunnable();
		// 4.创建Thread对象,并把刚刚的实现类对象 作为参数传递
		Thread td = new Thread(mr);
		// 5.开启线程
		td.start();
		
		for (int i = 0; i <20 ; i++) {
            // 打印线程的名字
            System.out.println(Thread.currentThread().getName()+"-->"+i);
        }
	}
}

1.2 Thread.sleep()方法

public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
毫秒数结束之后,线程继续执行

public class Demo01Sleep {
    public static void main(String[] args) {
        // 模拟秒表
        for (int i = 1; i <=60 ; i++) {
            System.out.println(i);

            // 使用Thread类的sleep方法让程序睡眠1秒钟
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

1.3 给线程起名字

设置线程的名称:(了解)
1.使用Thread类中的方法setName(名字)

void setName(String name) 改变线程名称,使之与参数 name 相同。

2.创建一个带参数的构造方法,参数传递线程的名称;调用父类的带参构造方法,把线程名称传递给父类,让父类(Thread)给子线程起一个名字

Thread(String name) 分配新的 Thread 对象。

public class MyThread extends Thread{

    public MyThread(){}

    public MyThread(String name){
        super(name);  // 把线程名称传递给父类,让父类(Thread)给子线程起一个名字
    }

    @Override
    public void run() {
        // 获取线程的名称
        System.out.println(Thread.currentThread().getName());
    }
}

调用

public class Demo01SetThreadName {
    public static void main(String[] args) {
        // 开启多线程
        MyThread mt = new MyThread();
        mt.setName("小强");
        mt.start();

        // 开启多线程  只要再线程的()里面写了名字,就不用再调用setname()方法去设置线程名字了
        new MyThread("旺财").start();
    }
}

// 以下是打印的结果
小强
旺财

1.4 匿名内部类方式实现线程的创建

匿名:没有名字
内部类:写在其他类内部的类

匿名内部类作用:简化代码
把子类继承父类,重写父类的方法,创建子类对象合一步完成
把实现类实现类接口,重写接口中的方法,创建实现类对象合成一步完成

匿名内部类的最终产物:子类/实现类对象,而这个类没有名字

格式:

new 父类/接口(){
  重复父类/接口中的方法
};
public class Demo01InnerClassThread {
    public static void main(String[] args) {
        // 线程的父类是Thread
        // new MyThread().start();
        new Thread(){
            // 重写run方法,设置线程任务
            @Override
            public void run() {
                for (int i = 0; i <20 ; i++) {
                    System.out.println(Thread.currentThread().getName()+"-->"+"黑马");
                }
            }
        }.start();

        // 线程的接口Runnable
        // Runnable r = new RunnableImpl();//多态
        Runnable r = new Runnable(){
            // 重写run方法,设置线程任务
            @Override
            public void run() {
                for (int i = 0; i <20 ; i++) {
                    System.out.println(Thread.currentThread().getName()+"-->"+"程序员");
                }
            }
        };
        new Thread(r).start();

        // 简化接口的方式
        new Thread(new Runnable(){
            // 重写run方法,设置线程任务
            @Override
            public void run() {
                for (int i = 0; i <20 ; i++) {
                    System.out.println(Thread.currentThread().getName()+"-->"+"传智播客");
                }
            }
        }).start();
    }
}

1.5 多线程共享资源

和python一样,多线程共享资源,尤其是涉及到修改资源的时候,数据会出现问题

卖票案例出现了线程安全问题,卖出了不存在的票和重复的票

1.5.1 同步锁

解决线程安全问题的一种方案:使用同步代码块
格式:

synchronized(锁对象){
    可能会出现线程安全问题的代码(访问了共享数据的代码)
}

注意:

  • 通过代码块中的锁对象,可以使用任意的对象
  • 但是必须保证多个线程使用的锁对象是同一个
  • 锁对象作用: 把同步代码块锁住,只让一个线程在同步代码块中执行
public class RunnableImpl implements Runnable{
    // 定义一个多个线程共享的票源
    private  int ticket = 100;
    // 创建一个锁对象
    Object obj = new Object();

    // 设置线程任务:卖票
    @Override
    public void run() {
        // 使用死循环,让卖票操作重复执行
        while(true){
           // 同步代码块
            synchronized (obj){
                // 先判断票是否存在
                if(ticket>0){
                    // 票存在,卖票 ticket--
                    System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                    ticket--;
                }
            }
        }
    }
}

调用

public class Demo01Ticket {
    public static void main(String[] args) {
        //创建Runnable接口的实现类对象
        RunnableImpl run = new RunnableImpl();
        //创建Thread类对象,构造方法中传递Runnable接口的实现类对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        //调用start方法开启多线程
        t0.start();
        t1.start();
        t2.start();
    }
}

1.5.2 同步方法

解决线程安全问题的二种方案:使用同步方法
使用步骤:
1.把访问了共享数据的代码抽取出来,放到一个方法中
2.在方法上添加synchronized修饰符

格式:定义方法的格式

修饰符 synchronized 返回值类型 方法名(参数列表){
   可能会出现线程安全问题的代码(访问了共享数据的代码)
}

注意:同步代码块和同步方法的原理是一样,都是具有锁对象,哪一个线程进来执行,那么该线程就持有这个锁对象

  •  ==这里同步方法使用的锁对象 叫做this对象==
    
  •  ==如果同步方法是静态方法,那么他的锁对象是 当前类.class==
    
public class RunnableImpl implements Runnable{
    //定义一个多个线程共享的票源
    private static int ticket = 100;


    //设置线程任务:卖票
    @Override
    public void run() {
        System.out.println("this:"+this);//this:com.itheima.demo08.Synchronized.RunnableImpl@58ceff1
        //使用死循环,让卖票操作重复执行
        while(true){
            payTicketStatic();
        }
    }

    /*
        静态的同步方法
        锁对象是谁?
        不能是this
        this是创建对象之后产生的,静态方法优先于对象
        静态方法的锁对象是本类的class属性-->class文件对象(反射)
     */
    public static /*synchronized*/ void payTicketStatic(){
        // 因为此处用到了静态的同步方法,因此 对象是 当前类.class
        synchronized (RunnableImpl.class){
            //先判断票是否存在
            if(ticket>0){
                //提高安全问题出现的概率,让程序睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //票存在,卖票 ticket--
                System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                ticket--;
            }
        }

    }

    /*
        定义一个同步方法
        同步方法也会把方法内部的代码锁住
        只让一个线程执行
        同步方法的锁对象是谁?
        就是实现类对象 new RunnableImpl()
        也是就是this
     */
    public /*synchronized*/ void payTicket(){
        // 没有用到静态的同步方法,因此对象是 this
        synchronized (this){
            //先判断票是否存在
            if(ticket>0){
                //提高安全问题出现的概率,让程序睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //票存在,卖票 ticket--
                System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }
}

调用

public class Demo01Ticket {
    public static void main(String[] args) {
        //创建Runnable接口的实现类对象
        RunnableImpl run = new RunnableImpl();
        System.out.println("run:"+run);//run:com.itheima.demo08.Synchronized.RunnableImpl@58ceff1
        //创建Thread类对象,构造方法中传递Runnable接口的实现类对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        //调用start方法开启多线程
        t0.start();
        t1.start();
        t2.start();
    }
}

1.5.3 Lock锁

解决线程安全问题的三种方案:使用Lock锁
java.util.concurrent.locks.Lock接口
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
Lock接口中的方法:
void lock()获取锁。
void unlock() 释放锁。
java.util.concurrent.locks.ReentrantLock implements Lock接口

使用步骤:
1.在成员位置创建一个ReentrantLock对象
2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁

public class RunnableImpl implements Runnable{
    // 定义一个多个线程共享的票源
    private  int ticket = 100;

    // 1.在成员位置创建一个ReentrantLock对象
    Lock l = new ReentrantLock();

    // 设置线程任务:卖票
    @Override
    public void run() {
        // 使用死循环,让卖票操作重复执行
        while(true){
            // 2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
            l.lock();

            // 先判断票是否存在
            if(ticket>0){
                // 提高安全问题出现的概率,让程序睡眠
                try {
                    Thread.sleep(10);
                    // 票存在,卖票 ticket--
                    System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
                    l.unlock();  // 无论程序是否异常,都会把锁释放
                }
            }
        }
    }

1.6 线程之间的通信

在这里插入图片描述

等待唤醒案例:线程之间的通信
创建一个顾客线程(消费者):告知老板要的包子的种类和数量,调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
创建一个老板线程(生产者):花了5秒做包子,做好包子之后,调用notify方法,唤醒顾客吃包子

注意:
顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行
同步使用的锁对象必须保证唯一
只有锁对象才能调用wait和notify方法

Obejct类中的方法
void wait()
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
void notify()
唤醒在此对象监视器上等待的单个线程。会继续执行wait方法之后的代码

public class Demo01WaitAndNotify {
    public static void main(String[] args) {
        // 创建锁对象,保证唯一
        Object obj = new Object();
        // 创建一个顾客线程(消费者)
        new Thread(){
            @Override
            public void run() {
               // 一直等着买包子
               while(true){
                   // 保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                   synchronized (obj){
                       System.out.println("告知老板要的包子的种类和数量");
                       // 调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
                       try {
                           obj.wait();
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }
                       // 唤醒之后执行的代码
                       System.out.println("包子已经做好了,开吃!");
                       System.out.println("---------------------------------------");
                   }
               }
            }
        }.start();

        // 创建一个老板线程(生产者)
        new Thread(){
            @Override
            public void run() {
                // 一直做包子
                while (true){
                    // 花了5秒做包子
                    try {
                        Thread.sleep(5000);//花5秒钟做包子
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    // 保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                    synchronized (obj){
                        System.out.println("老板5秒钟之后做好包子,告知顾客,可以吃包子了");
                        // 做好包子之后,调用notify方法,唤醒顾客吃包子
                        obj.notify();
                    }
                }
            }
        }.start();
    }
}

进入到TimeWaiting(计时等待)有两种方式
1.使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
2.使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态

唤醒的方法:

  • void notify() 唤醒在此对象监视器上等待的单个线程。

  • void notifyAll() 唤醒在此对象监视器上等待的所有线程。

public class Demo02WaitAndNotify {
    public static void main(String[] args) {
        // 创建锁对象,保证唯一
        Object obj = new Object();
        // 创建一个顾客线程(消费者)
        new Thread(){
            @Override
            public void run() {
                // 一直等着买包子
                while(true){
                    // 保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                    synchronized (obj){
                        System.out.println("顾客1告知老板要的包子的种类和数量");
                        // 调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
                        try {
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // 唤醒之后执行的代码
                        System.out.println("包子已经做好了,顾客1开吃!");
                        System.out.println("---------------------------------------");
                    }
                }
            }
        }.start();

        // 创建一个顾客线程(消费者)
        new Thread(){
            @Override
            public void run() {
                // 一直等着买包子
                while(true){
                    // 保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                    synchronized (obj){
                        System.out.println("顾客2告知老板要的包子的种类和数量");
                        // 调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
                        try {
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // 唤醒之后执行的代码
                        System.out.println("包子已经做好了,顾客2开吃!");
                        System.out.println("---------------------------------------");
                    }
                }
            }
        }.start();

        // 创建一个老板线程(生产者)
        new Thread(){
            @Override
            public void run() {
                // 一直做包子
                while (true){
                    // 花了5秒做包子
                    try {
                        Thread.sleep(5000);//花5秒钟做包子
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    // 保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                    synchronized (obj){
                        System.out.println("老板5秒钟之后做好包子,告知顾客,可以吃包子了");
                        // 做好包子之后,调用notify方法,唤醒顾客吃包子
                        // obj.notify(); // 如果有多个等待线程,随机唤醒一个
                        obj.notifyAll();  // 唤醒所有等待的线程
                    }
                }
            }
        }.start();
    }
}

1.7 线程的等待唤醒机制

定义一个包子的资源类

public class BaoZi {
     String  pier ;
     String  xianer ;
     // 包子资源 是否存在  包子资源状态
     boolean  flag = false ;
}

吃货线程类

public class ChiHuo extends Thread{
    private BaoZi bz;
	
    // 定义该线程类的构造方法
    public ChiHuo(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }
    
    @Override
    public void run() {
        while(true){
            synchronized (bz){
                // 如果没有包子的话
                if(bz.flag == false){
                    try {
                        // 吃货进入了等待状态
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                
                // 到这一步说明吃货离开了等待包子来的状态,有了包子,要开始吃包子了
                System.out.println("吃货正在吃"+bz.pier+bz.xianer+"包子");
                // 吃完包子之后把包子的状态改为 false
                bz.flag = false;
                // 唤醒包子铺的制作包子的线程
                bz.notify();
            }
        }
    }
}

包子铺线程类

public class BaoZiPu extends Thread {
    private BaoZi bz;
	// 定义包子铺的构造方法
    public BaoZiPu(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }

    @Override
    public void run() {
        int count = 0;
        // 造包子
        while(true){
            // 同步
            synchronized (bz){
                // 如果有包子资源的话,包子铺线程就进入等待状态,不制作包子了
                if(bz.flag == true){
                    try {
                        // 包子铺进入了线程等待状态
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 如果到了这一步说明吃货顾客吃完了包子,吃货线程唤醒了正在处于等待状态的包子铺线程,让其赶紧制作包子
                System.out.println("包子铺开始做包子");
                if(count%2 == 0){
                    // 冰皮  五仁
                    bz.pier = "冰皮";
                    bz.xianer = "五仁";
                }else{
                    // 薄皮  牛肉大葱
                    bz.pier = "薄皮";
                    bz.xianer = "牛肉大葱";
                }
                count++;
				
                // 制作完包子之后把包子的状态改为 有 true
                bz.flag=true;
                System.out.println("包子造好了:"+bz.pier+bz.xianer);
                System.out.println("吃货来吃吧");
                //唤醒吃货的等待线程,让吃货来吃包子
                bz.notify();
            }
        }
    }
}

调用测试类

public class Demo {
    public static void main(String[] args) {
        // 等待唤醒案例
        BaoZi bz = new BaoZi();
		
        // 初始化线程对象
        ChiHuo ch = new ChiHuo("吃货",bz);
        BaoZiPu bzp = new BaoZiPu("包子铺",bz);

        ch.start();
        bzp.start();
    }
}

// 执行效果
包子铺开始做包子
包子造好了:冰皮五仁
吃货来吃吧
吃货正在吃冰皮五仁包子
包子铺开始做包子
包子造好了:薄皮牛肉大葱
吃货来吃吧
吃货正在吃薄皮牛肉大葱包子

1.8 线程池

线程池:JDK1.5之后提供的
java.util.concurrent.Executors:线程池的工厂类,用来生成线程池

Executors类中的静态方法:

static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程数的线程池

参数:

int nThreads:创建线程池中包含的线程数量

返回值:

ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收

(面向接口编程)
java.util.concurrent.ExecutorService:线程池接口
用来从线程池中获取线程,调用start方法,执行线程任务
submit(Runnable task) 提交一个 Runnable 任务用于执行

关闭/销毁线程池的方法

void shutdown()

线程池的使用步骤:
1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
2.创建一个类,实现Runnable接口,重写run方法,设置线程任务
3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行)

创建一个类,实现Runnable接口,重写run方法,设置线程任务

public class RunnableImpl implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"创建了一个新的线程执行");
    }
}

创建线程池,并且开启线程

public class Demo01ThreadPool {
    public static void main(String[] args) {
        // 1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        // 3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
        es.submit(new RunnableImpl());  // pool-1-thread-1创建了一个新的线程执行
        // 线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程可以继续使用
        es.submit(new RunnableImpl());  // pool-1-thread-1创建了一个新的线程执行
        es.submit(new RunnableImpl());  // pool-1-thread-2创建了一个新的线程执行

        // 4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
        es.shutdown();

        es.submit(new RunnableImpl());  // 抛异常,线程池都没有了,就不能获取线程了
    }
}

2. Lambda表达式

在使用lambda表达式之前的创建线程的方法

创建Runnable接口的实现类

public class RunnableImpl implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+" 新线程创建了");
    }
}

调用

public class Demo01Runnable {
    public static void main(String[] args) {
        // 创建Runnable接口的实现类对象
        RunnableImpl run = new RunnableImpl();
        // 创建Thread类对象,构造方法中传递Runnable接口的实现类
        Thread t = new Thread(run);
        // 调用start方法开启新线程,执行run方法
        t.start();
        

        // 简化代码,使用匿名内部类,实现多线程程序
        Runnable r = new Runnable(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+" 新线程创建了");
            }
        };
        new Thread(r).start();
        

        // 简化代码
        new Thread(new Runnable(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+" 新线程创建了");
            }
        }).start();
    }
}

2.1 使用Lambda表达式创建线程

Lambda表达式的标准格式:
由三部分组成:
a.一些参数
b.一个箭头
c.一段代码

格式:

(参数列表) -> {一些重写方法的代码};

解释说明格式:

  • ():接口中抽象方法的参数列表,没有参数,就空着;有参数就写出参数,多个参数使用逗号分隔
  • ->:传递的意思,把参数传递给方法体{}
  • {}:重写接口的抽象方法的方法体
public class Demo02Lambda {
    public static void main(String[] args) {
        // 使用匿名内部类的方式,实现多线程
        new Thread(new Runnable(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+" 新线程创建了");
            }
        }).start();

        // 使用Lambda表达式,实现多线程
        new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" 新线程创建了");
            }
        ).start();

        // 优化省略Lambda
        new Thread(()->System.out.println(Thread.currentThread().getName()+" 新线程创建了")).start();
    }
}

2.2 Lambda无参无返回值练习

需求:
给定一个厨子Cook接口,内含唯一的抽象方法makeFood,且无参数、无返回值。
使用Lambda的标准格式调用invokeCook方法,打印输出“吃饭啦!”字样

// 定一个厨子Cook接口,内含唯一的抽象方法makeFood
public interface Cook {
    // 定义无参数无返回值的方法makeFood
    public abstract void makeFood();
}

调用

public class Demo01Cook {
    public static void main(String[] args) {
        // 调用invokeCook方法,参数是Cook接口,传递Cook接口的匿名内部类对象
        invokeCook(new Cook() {
            @Override
            public void makeFood() {
                System.out.println("吃饭了");
            }
        });

        // 使用Lambda表达式,简化匿名内部类的书写
        invokeCook(()->{
            System.out.println("吃饭了");
        });

        // 优化省略Lambda
        invokeCook(()-> System.out.println("吃饭了"));
    }

    // 定义一个方法,参数传递Cook接口,方法内部调用Cook接口中的方法makeFood
    public static void invokeCook(Cook cook){
        cook.makeFood();
    }
}

2.3 Lambda有参有返回值练习

需求:
使用数组存储多个Person对象
对数组中的Person对象使用Arrays的sort方法通过年龄进行升序排序

先定义一个Person对象

public class Person {
    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", 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;
    }
}

调用

public class Demo01Arrays {
    public static void main(String[] args) {
        // 使用数组存储多个Person对象
        Person[] arr = {
                new Person("柳岩",38),
                new Person("迪丽热巴",18),
                new Person("古力娜扎",19)
        };

        // 对数组中的Person对象使用Arrays的sort方法通过年龄进行升序(前边-后边)排序
        Arrays.sort(arr, new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge()-o2.getAge();
            }
        });

        // 使用Lambda表达式,简化匿名内部类
        Arrays.sort(arr,(Person o1, Person o2)->{
            return o1.getAge()-o2.getAge();
        });

        // 优化省略Lambda
        Arrays.sort(arr,(o1, o2)->o1.getAge()-o2.getAge());

        // 遍历数组
        for (Person p : arr) {
            System.out.println(p);
        }
    }
}

需求:
给定一个计算器Calculator接口,内含抽象方法calc可以将两个int数字相加得到和值
使用Lambda的标准格式调用invokeCalc方法,完成120和130的相加计算

public interface Calculator {
    // 定义一个计算两个int整数和的方法并返回结果
    public abstract int calc(int a,int b);
}
public class Demo01Calculator {
    public static void main(String[] args) {
        // 调用invokeCalc方法,方法的参数是一个接口,可以使用匿名内部类
        invokeCalc(10, 20, new Calculator() {
            @Override
            public int calc(int a, int b) {
                return a+b;
            }
        });

        // 使用Lambda表达式简化匿名内部类的书写
        invokeCalc(120,130,(int a,int b)->{
            return a + b;
        });

        // 优化省略Lambda
        invokeCalc(120,130,(a,b)-> a + b);
    }

    /*
        定义一个方法
        参数传递两个int类型的整数
        参数传递Calculator接口
        方法内部调用Calculator中的方法calc计算两个整数的和
     */
    // 静态方法去是实现这个接口
    public static void invokeCalc(int a,int b,Calculator c){
        int sum = c.calc(a,b);
        System.out.println(sum);
    }
}

2.4 Lambda的使用前提

Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:

  1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法
    无论是JDK内置的RunnableComparator接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
  2. 使用Lambda必须具有上下文推断
    也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。

备注:有且仅有一个抽象方法的接口,称为“函数式接口”。

3.File类

java.io.File类

文件和目录路径名的抽象表示形式。

java把电脑中的文件和文件夹(目录)封装为了一个File类,我们可以使用File类对文件和文件夹进行操作
我们可以使用File类的方法

  • 创建一个文件/文件夹

  • 删除文件/文件夹

  • 获取文件/文件夹

  • 判断文件/文件夹是否存在

  • 对文件夹进行遍历

  • 获取文件的大小

File类是一个与系统无关的类,任何的操作系统都可以使用这个类中的方法

重点:记住这三个单词
file:文件
directory:文件夹/目录
path:路径

3.1 系统有关的路径分隔符和默认名称分隔符

public class Demo01File {
    public static void main(String[] args) {
        /*
            static String pathSeparator 与系统有关的路径分隔符,为了方便,它被表示为一个字符串。
            static char pathSeparatorChar 与系统有关的路径分隔符。

            static String separator 与系统有关的默认名称分隔符,为了方便,它被表示为一个字符串。
            static char separatorChar 与系统有关的默认名称分隔符。

            操作路径:路径不能写死了
            C:\develop\a\a.txt  windows
            C:/develop/a/a.txt  linux
            "C:"+File.separator+"develop"+File.separator+"a"+File.separator+"a.txt"
         */
        String pathSeparator = File.pathSeparator;
        System.out.println(pathSeparator);  // 路径分隔符 windows:分号;  linux:冒号:

        String separator = File.separator;
        System.out.println(separator);  // 文件名称分隔符 windows:反斜杠\  linux:正斜杠/
    }
}

3.2 路径

路径:
绝对路径:是一个完整的路径
以盘符(c:,D:)开始的路径
c:\a.txt
C:\Users\itcast\IdeaProjects\shungyuan\123.txt
D:\demo\b.txt
相对路径:是一个简化的路径
相对指的是相对于当前项目的根目录(C:\Users\itcast\IdeaProjects\shungyuan)
如果使用当前项目的根目录,路径可以简化书写
C:\Users\itcast\IdeaProjects\shungyuan\123.txt–>简化为: 123.txt(可以省略项目的根目录)

注意:
1.路径是不区分大小写
2.路径中的文件名称分隔符windows使用反斜杠,反斜杠是转义字符,两个反斜杠代表一个普通的反斜杠

File(String pathname) 通过将给定路径名字符串转换为抽象路径名来创建一个新 File 实例。
参数:
String pathname:字符串的路径名称
路径可以是以文件结尾,也可以是以文件夹结尾
路径可以是相对路径,也可以是绝对路径
路径可以是存在,也可以是不存在
创建File对象,只是把字符串路径封装为File对象,不考虑路径的真假情况

private static void show01() {
        File f1 = new File("C:\\Users\\itcast\\IdeaProjects\\shungyuan\\a.txt");
        System.out.println(f1);  // 重写了Object类的toString方法 C:\Users\itcast\IdeaProjects\shungyuan\a.txt

        File f2 = new File("C:\\Users\\itcast\\IdeaProjects\\shungyuan");
        System.out.println(f2);  // C:\Users\itcast\IdeaProjects\shungyuan

        File f3 = new File("b.txt");
        System.out.println(f3);  // b.txt
    }

File(String parent, String child) 根据 parent 路径名字符串和 child 路径名字符串创建一个新 File 实例。
参数:把路径分成了两部分
String parent:父路径
String child:子路径

好处:

父路径和子路径,可以单独书写,使用起来非常灵活;父路径和子路径都可以变化

public class Demo02File {
    public static void main(String[] args) {
        show02("c:\\","a.txt");  // c:\a.txt
        show02("d:\\","a.txt");  // d:\a.txt
    }

    private static void show02(String parent, String child) {
        File file = new File(parent,child);
        System.out.println(file);  // c:\a.txt
    }
}

File(File parent, String child) 根据 parent 抽象路径名和 child 路径名字符串创建一个新 File 实例。
参数:把路径分成了两部分
File parent:父路径
String child:子路径
好处:

  • 父路径和子路径,可以单独书写,使用起来非常灵活;父路径和子路径都可以变化
  • 父路径是File类型,可以使用File的方法对路径进行一些操作,再使用路径创建对象
private static void show03() {
        File parent = new File("c:\\");
        File file = new File(parent,"hello.java");
        System.out.println(file);  // c:\hello.java
    }

3.3 File类获取功能的方法

File类获取功能的方法

  • public String getAbsolutePath() :返回此File的绝对路径名字符串。
  • public String getPath() :将此File转换为路径名字符串。
  • public String getName() :返回由此File表示的文件或目录的名称。
  • public long length() :返回由此File表示的文件的长度。
  • public String getAbsolutePath() :返回此File的绝对路径名字符串。
    获取的构造方法中传递的路径
    无论路径是绝对的还是相对的,getAbsolutePath方法返回的都是绝对路径
private static void show01() {
    File f1 = new File("C:\\Users\\itcast\\IdeaProjects\\shungyuan\\a.txt");
    String absolutePath1 = f1.getAbsolutePath();
    System.out.println(absolutePath1);
    // C:\Users\itcast\IdeaProjects\shungyuan\a.txt

    File f2 = new File("a.txt");
    String absolutePath2 = f2.getAbsolutePath();
    // 传递的是相对路径,但是返回的确实绝对路径
    System.out.println(absolutePath2);
    // C:\Users\itcast\IdeaProjects\shungyuan\a.txt
}

public String getPath() :将此File转换为路径名字符串。
获取的构造方法中传递的路径

toString方法调用的就是getPath方法

源码:
public String toString() {
    return getPath();
}
private static void show02() {
    File f1 = new File("C:\\Users\\itcast\\IdeaProjects\\shungyuan\\a.txt");
    File f2 = new File("a.txt");
    String path1 = f1.getPath();
    System.out.println(path1);  // C:\Users\itcast\IdeaProjects\shungyuan\a.txt
    String path2 = f2.getPath();
    System.out.println(path2);  // a.txt
	
    // 直接打印对象,对象会调用 toString() 方法
    System.out.println(f1);  // C:\Users\itcast\IdeaProjects\shungyuan\a.txt
    // 对象调用toString()方法,而toString()方法则会调用 getPath() 方法
    System.out.println(f1.toString());
    // C:\Users\itcast\IdeaProjects\shungyuan\a.txt
    }

public String getName() :返回由此File表示的文件或目录的名称。
获取的就是构造方法传递路径的结尾部分(文件/文件夹)

private static void show03() {
    File f1 = new File("C:\\Users\\itcast\\IdeaProjects\\shungyuan\\a.txt");
    String name1 = f1.getName();
    System.out.println(name1);  // a.txt

    File f2 = new File("C:\\Users\\itcast\\IdeaProjects\\shungyuan");
    String name2 = f2.getName();
    System.out.println(name2);  // shungyuan
}

public long length() :返回由此File表示的文件的长度。
获取的是构造方法指定的文件的大小,以字节为单位
注意:

  • 文件夹是没有大小概念的,不能获取文件夹的大小
  • 如果构造方法中给出的路径不存在,那么length方法返回0
private static void show04() {
    File f1 = new File("C:\\develop\\a\\1.jpg");
    long l1 = f1.length();
    System.out.println(l1);  // 780831字节

    File f2 = new File("C:\\develop\\a\\2.jpg");
    // 给出的路径是不存在的
    System.out.println(f2.length());  // 0

    File f3 = new File("C:\\develop\\a");
    System.out.println(f3.length());  // 0 文件夹没有大小概念的
}

3.4 File类判断功能的方法

  • public boolean exists() :此File表示的文件或目录是否实际存在。
  • public boolean isDirectory() :此File表示的是否为目录。
  • public boolean isFile() :此File表示的是否为文件。
  • public boolean exists() :此File表示的文件或目录是否实际存在
private static void show01() {
        File f1 = new File("C:\\Users\\itcast\\IdeaProjects\\shungyuan");
        System.out.println(f1.exists());  // true

        File f2 = new File("C:\\Users\\itcast\\IdeaProjects\\shung");
        System.out.println(f2.exists());  // false

        File f3 = new File("shungyuan.iml");  // 相对路径 C:\Users\itcast\IdeaProjects\shungyuan\shungyuan.iml
        System.out.println(f3.exists());  // true

        File f4 = new File("a.txt");
        System.out.println(f4.exists());  // false
    }

public boolean isDirectory() :此File表示的是否为目录。

用于判断构造方法中给定的路径是否以文件夹结尾

public boolean isFile() :此File表示的是否为文件。

用于判断构造方法中给定的路径是否以文件结尾

注意:
电脑的硬盘中只有文件/文件夹,两个方法是互斥
这两个方法使用前提,路径必须是存在的,否则都返回false

private static void show02() {
        File f1 = new File("C:\\Users\\itcast\\IdeaProjects\\shung");

        // 不存在,就没有必要获取
        if(f1.exists()){
            // 路径存在的话,判断到底是文件还是文件夹
            System.out.println(f1.isDirectory());
            System.out.println(f1.isFile());
        }

        File f2 = new File("C:\\Users\\itcast\\IdeaProjects\\shungyuan");
        if(f2.exists()){
            System.out.println(f2.isDirectory());  // true
            System.out.println(f2.isFile());  // false
        }

        File f3 = new File("C:\\Users\\itcast\\IdeaProjects\\shungyuan\\shungyuan.iml");
        if(f3.exists()){
            System.out.println(f3.isDirectory());  // false
            System.out.println(f3.isFile());  // true
        }
    }

3.5 File类创建删除功能的方法

  • public boolean createNewFile() :当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。

  • public boolean delete() :删除由此File表示的文件或目录。

  • public boolean mkdir() :创建由此File表示的目录。

  • public boolean mkdirs() :创建由此File表示的目录,包括任何必需但不存在的父目录。

  • public boolean createNewFile() :当且仅当具有该名称的文件尚不存在时,创建一个新的空文件
    注意:

    1.此方法只能创建文件,不能创建文件夹
    2.创建文件的路径必须存在,否则会抛出异常

public boolean createNewFile() throws IOException
createNewFile声明抛出了IOException,我们调用这个方法,就必须的处理这个异常,要么throws,要么trycatch

private static void show01() throws IOException {
        File f1 = new File("C:\\Users\\itcast\\IdeaProjects\\shungyuan\\08_FileAndRecursion\\1.txt");
        boolean b1 = f1.createNewFile();
        System.out.println("b1:"+b1);

        File f2 = new File("08_FileAndRecursion\\2.txt");
        System.out.println(f2.createNewFile());

        File f3 = new File("08_FileAndRecursion\\新建文件夹");
        System.out.println(f3.createNewFile());  // 不要被名称迷糊,要看类型

        File f4 = new File("08_FileAndRecursi\\3.txt");
        System.out.println(f4.createNewFile());  // 路径不存在,抛出IOException
    }

public boolean mkdir() :创建单级空文件夹
public boolean mkdirs() :既可以创建单级空文件夹,也可以创建多级文件夹
创建文件夹的路径和名称在构造方法中给出(构造方法的参数)
返回值:布尔值
true:文件夹不存在,创建文件夹,返回true
false:文件夹存在,不会创建,返回false;构造方法中给出的路径不存在返回false
注意:
1.此方法只能创建文件夹,不能创建文件

private static void show02() {
        File f1 = new File("08_FileAndRecursion\\aaa");
        boolean b1 = f1.mkdir();
        System.out.println("b1:"+b1);

        File f2 = new File("08_FileAndRecursion\\111\\222\\333\\444");
        boolean b2 = f2.mkdirs();
        System.out.println("b2:"+b2);

        File f3 = new File("08_FileAndRecursion\\abc.txt");
        boolean b3 = f3.mkdirs();  // 看类型,是一个文件
        System.out.println("b3:"+b3);

        File f4 = new File("08_F\\ccc");
        boolean b4 = f4.mkdirs();  // 不会抛出异常,路径不存在,不会创建
        System.out.println("b4:"+b4);
    }

public boolean delete() :删除由此File表示的文件或目录。
此方法,可以删除构造方法路径中给出的文件/文件夹

返回值:布尔值
true:文件/文件夹删除成功,返回true
false:文件夹中有内容,不会删除返回false;构造方法中路径不存在false
注意:
delete方法是直接在硬盘删除文件/文件夹,不走回收站,删除要谨慎

private static void show03() {
        File f1 = new File("08_FileAndRecursion\\新建文件夹");
        boolean b1 = f1.delete();
        System.out.println("b1:"+b1);

        File f2 = new File("08_FileAndRecursion\\abc.txt");
        System.out.println(f2.delete());
    }

3.6 File类遍历(文件夹)目录功能

File类遍历(文件夹)目录功能

  • public String[] list() :返回一个String数组,表示该File目录中的所有子文件或目录。

  • public File[] listFiles() :返回一个File数组,表示该File目录中的所有的子文件或目录。

  • public String[] list() :返回一个String数组,表示该File目录中的所有子文件或目录。

    遍历构造方法中给出的目录,会获取目录中所有文件/文件夹的名称,把获取到的多个名称存储到一个String类型的数组中

注意:

  • list方法和listFiles方法遍历的是构造方法中给出的目录
  • 如果构造方法中给出的目录的路径不存在,会抛出空指针异常
  • 如果构造方法中给出的路径不是一个目录,也会抛出空指针异常
private static void show01() {
        //File file = new File("C:\\Users\\itcast\\IdeaProjects\\shungyuan\\08_FileAndRecursion\\1.txt");//NullPointerException
        //File file = new File("C:\\Users\\itcast\\IdeaProjects\\shungyuan\\08_Fi");//NullPointerException
    
        File file = new File("E:\\写真\\jojo");
        String[] arr = file.list();
        for (String fileName : arr) {
            System.out.println(fileName);
        }
    }

// 打印的结果
433564.jpg
552e0dfd133150ec.jpg
b21c8701a18b87d6df1b8e260a0828381f30fdaa.jpg
Snipaste_2018-12-06_20-11-39.png

public File[] listFiles() :返回一个File数组,表示该File目录中的所有的子文件或目录。
遍历构造方法中给出的目录,会获取目录中所有的文件/文件夹,把文件/文件夹封装为File对象,多个File对象存储到File数组中

private static void show02() {
        File file = new File("E:\\写真\\jojo");
        File[] files = file.listFiles();
        for (File f : files) {
            System.out.println(f);
        }
    }

// 打印的结果
E:\写真\jojo\433564.jpg
E:\写真\jojo\552e0dfd133150ec.jpg
E:\写真\jojo\b21c8701a18b87d6df1b8e260a0828381f30fdaa.jpg
E:\写真\jojo\Snipaste_2018-12-06_20-11-39.png

4. 递归

递归:方法自己调用自己

递归的分类:

  • 递归分为两种,直接递归和间接递归。
  • 直接递归称为方法自身调用自己。
  • 间接递归可以A方法调用B方法,B方法调用C方法,C方法调用A方法。

注意事项:

  • 递归一定要有条件限定,保证递归能够停止下来,否则会发生栈内存溢出。
  • 在递归中虽然有限定条件,但是递归次数不能太多。否则也会发生栈内存溢出。
  • 构造方法,禁止递归

递归的使用前提:
当调用方法的时候,方法的主体不变,每次调用方法的参数不同,可以使用递归

public class Demo01Recurison {
    public static void main(String[] args) {
        // a(); 因为a()方法中的递归没有停止的条件,因此程序运行会报错
        b(1);
    }

    /*
        构造方法,禁止递归
            编译报错:构造方法是创建对象使用的,一直递归会导致内存中有无数多个对象,直接编译报错
     */
    public Demo01Recurison() {
        //Demo01Recurison();
    }

    /*
       在递归中虽然有限定条件,但是递归次数不能太多。否则也会发生栈内存溢出。
       11157
       Exception in thread "main" java.lang.StackOverflowError
    */
    private static void b(int i) {
        System.out.println(i);
        if(i==20000){
            return; //结束方法
        }
        b(++i);
    }

    /*
        递归一定要有条件限定,保证递归能够停止下来,否则会发生栈内存溢出。
        Exception in thread "main" java.lang.StackOverflowError
     */
    private static void a() {
        System.out.println("a方法!");
        a();
    }
}

4.1 递归练习1

使用递归计算1-n之间的和

public class Demo02Recurison {
    public static void main(String[] args) {
        int s = sum(3);
        System.out.println(s);
    }

    /*
        定义一个方法,使用递归计算1-n之间的和
        1+2+3+...+n
        n+(n-1)+(n-2)+...+1
        已知:
            最大值:n
            最小值:1
        使用递归必须明确:
            1.递归的结束条件
                获取到1的时候结束
            2.递归的目的
                获取下一个被加的数字(n-1)
     */
    public static int sum(int n){
        //获取到1的时候结束
        if(n==1){
            return 1;
        }

        //获取下一个被加的数字(n-1)
        return n + sum(n-1);
    }
}

4.2 递归练习2

public class Demo03Recurison {
    public static void main(String[] args) {
        int jiecheng = jc(5);
        System.out.println(jiecheng);
    }

    /*
        定义方法使用递归计算阶乘
        5的阶乘: 5! = 5*(5-1)*(5-2)*(5-3)*(5-4)=5*4*3*2*1
        递归结束的条件
            获取到1的时候结束
        递归的目的
            获取下一个被乘的数字(n-1)
        方法的参数发生变化
            5,4,3,2,1
     */
    public static int jc(int n){
        //获取到1的时候结束
        if(n==1){
            return 1;
        }
        //获取下一个被乘的数字(n-1)
        return n * jc(n-1);
    }
}

4.3 递归练习3

练习:
递归打印多级目录
需求:
遍历c:\abc文件夹,及abc文件夹的子文件夹
c:\abc
c:\abc\abc.txt
c:\abc\abc.java
c:\abc\a
c:\abc\a\a.jpg
c:\abc\a\a.java
c:\abc\b
c:\abc\b\b.java
c:\abc\b\b.txt

public class Demo04Recurison {
    public static void main(String[] args) {
        File file = new File("c:\\abc");
        getAllFile(file);
    }

    /*
        定义一个方法,参数传递File类型的目录
        方法中对目录进行遍历
     */
    public static void getAllFile(File dir){
        System.out.println(dir);//打印被遍历的目录名称
        File[] files = dir.listFiles();
        for (File f : files) {
            // 对遍历得到的File对象f进行判断,判断是否是文件夹
            if(f.isDirectory()){
                // f是一个文件夹,则继续遍历这个文件夹
                // 我们发现getAllFile方法就是传递文件夹,遍历文件夹的方法
                // 所以直接调用getAllFile方法即可:递归(自己调用自己)
                getAllFile(f);
            }else{
                // f是一个文件,直接打印即可
                System.out.println(f);
            }
        }
    }
}

4.4 递归练习4

练习:
递归打印多级目录
需求:
遍历c:\abc文件夹,及abc文件夹的子文件夹
只要.java结尾的文件

public class Demo05Recurison {
    public static void main(String[] args) {
        File file = new File("c:\\abc");
        getAllFile(file);
    }

    /*
        定义一个方法,参数传递File类型的目录
        方法中对目录进行遍历
     */
    public static void getAllFile(File dir){
        // System.out.println(dir);//打印被遍历的目录名称
        File[] files = dir.listFiles();
        for (File f : files) {
            // 对遍历得到的File对象f进行判断,判断是否是文件夹
            if(f.isDirectory()){
                // f是一个文件夹,则继续遍历这个文件夹
                // 我们发现getAllFile方法就是传递文件夹,遍历文件夹的方法
                // 所以直接调用getAllFile方法即可:递归(自己调用自己)
                getAllFile(f);
            }else{
                // f是一个文件,直接打印即可
                // 获取文件的名字,把文件的名字转换为小写,然后判断文件的结尾是不是 .java
                if(f.getName().toLowerCase().endsWith(".java")){
                    System.out.println(f);
                }
            }
        }
    }
}

4.5 文件过滤器

需求:
遍历c:\abc文件夹,及abc文件夹的子文件夹
只要.java结尾的文件

我们可以使用过滤器来实现
在File类中有两个和ListFiles重载的方法,方法的参数传递的就是过滤器

File[] listFiles(FileFilter filter)
java.io.FileFilter接口:用于抽象路径名(File对象)的过滤器。
作用:用来过滤文件(File对象)
抽象方法:用来过滤文件的方法
boolean accept(File pathname) 测试指定抽象路径名是否应该包含在某个路径名列表中。
参数:

File pathname:使用ListFiles方法遍历目录,得到的每一个文件对象

File[] listFiles(FilenameFilter filter)
java.io.FilenameFilter接口:实现此接口的类实例可用于过滤器文件名。
作用:用于过滤文件名称
抽象方法:用来过滤文件的方法
boolean accept(File dir, String name) 测试指定文件是否应该包含在某一文件列表中。
参数:
File dir:构造方法中传递的被遍历的目录
String name:使用ListFiles方法遍历目录,获取的每一个文件/文件夹的名称

注意:
两个过滤器接口是没有实现类的,需要我们自己写实现类,重写过滤的方法accept,在方法中自己定义过滤规则

创建过滤器FileFilter的实现类,重写过滤方法accept,定义过滤规则

import java.io.File;
import java.io.FileFilter;

public class FileFilterImpl implements FileFilter{
    @Override
    public boolean accept(File pathname) {
        /*
            过滤的规则:
            在accept方法中,判断File对象是否是以.java结尾
            是就返回true
            不是就返回false
        */
        // 如果pathname是一个文件夹,返回true,继续遍历这个文件夹
        if(pathname.isDirectory()){
            return true;
        }

        return pathname.getName().toLowerCase().endsWith(".java");
        
        // 或者干脆用下面这样的方法去写
        // return pathname.getName().toLowerCase().endsWith(".java") || pathname.isDirectory()
    }
}
import java.io.File;

public class Demo01Filter {
    public static void main(String[] args) {
        File file = new File("c:\\abc");
        getAllFile(file);
    }

    /*
        定义一个方法,参数传递File类型的目录
        方法中对目录进行遍历
     */
    public static void getAllFile(File dir){
        File[] files = dir.listFiles(new FileFilterImpl());  // 传递过滤器对象
        for (File f : files) {
            // 对遍历得到的File对象f进行判断,判断是否是文件夹
            if(f.isDirectory()){
                // f是一个文件夹,则继续遍历这个文件夹
                // 我们发现getAllFile方法就是传递文件夹,遍历文件夹的方法
                // 所以直接调用getAllFile方法即可:递归(自己调用自己)
                getAllFile(f);
            }else{
                // f是一个文件,直接打印即可
                System.out.println(f);
            }
        }
    }
}

4.6 Lambda优化过滤器

public class Demo02Filter {
    public static void main(String[] args) {
        File file = new File("c:\\abc");
        getAllFile(file);
    }

    public static void getAllFile(File dir){
        // 传递过滤器对象 使用匿名内部类
        /*File[] files = dir.listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                //过滤规则,pathname是文件夹或者是.java结尾的文件返回true
                return pathname.isDirectory() || pathname.getName().toLowerCase().endsWith(".java");
            }
        });*/

        //使用Lambda表达式优化匿名内部类(接口中只有一个抽象方法)
        /*File[] files = dir.listFiles((File pathname)->{
            return pathname.isDirectory() || pathname.getName().toLowerCase().endsWith(".java");
        });*/

        File[] files = dir.listFiles(pathname->pathname.isDirectory() || pathname.getName().toLowerCase().endsWith(".java"));

        /*File[] files = dir.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                //过滤规则,pathname是文件夹或者是.java结尾的文件返回true
                return new File(dir,name).isDirectory() || name.toLowerCase().endsWith(".java");
            }
        });*/

        //使用Lambda表达式优化匿名内部类(接口中只有一个抽象方法)
        /*File[] files = dir.listFiles((File d, String name)->{
            //过滤规则,pathname是文件夹或者是.java结尾的文件返回true
            return new File(d,name).isDirectory() || name.toLowerCase().endsWith(".java");
        });*/

        //File[] files = dir.listFiles((d,name)->new File(d,name).isDirectory() || name.toLowerCase().endsWith(".java"));

        for (File f : files) {
            // 对遍历得到的File对象f进行判断,判断是否是文件夹
            if(f.isDirectory()){
                // f是一个文件夹,则继续遍历这个文件夹
                // 我们发现getAllFile方法就是传递文件夹,遍历文件夹的方法
                // 所以直接调用getAllFile方法即可:递归(自己调用自己)
                getAllFile(f);
            }else{
                // f是一个文件,直接打印即可
                System.out.println(f);
            }
        }
    }
}

5. 字节流

字节的输入和输出流指的是向内存中的输入和输出

字节的输出也就是向内存中输出东西,然后内存再把东西写入文件中

字节的输入也就内存从文件中读取数据,然后把数据输入到显示屏来显示

5.1 字节输出流

java.io.OutputStream:字节输出流

此抽象类是表示输出字节流的所有类的超类。

定义了一些子类共性的成员方法:

  • public void close() :关闭此输出流并释放与此流相关联的任何系统资源。

  • public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。

  • public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。

  • public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。

  • public abstract void write(int b) :将指定的字节输出流。

java.io.FileOutputStream extends OutputStream
FileOutputStream:文件字节输出流
作用:把内存中的数据写入到硬盘的文件中

构造方法:
FileOutputStream(String name)创建一个向具有指定名称的文件中写入数据的输出文件流。FileOutputStream(File file) 创建一个向指定 File 对象表示的文件中写入数据的文件输出流。

参数:写入数据的目的
String name:目的地是一个文件的路径
File file:目的地是一个文件

构造方法的作用:
1.创建一个FileOutputStream对象
2.会根据构造方法中传递的文件/文件路径,创建一个空的文件
3.会把FileOutputStream对象指向创建好的文件

写入数据的原理(内存–>硬盘)

java程序–>JVM(java虚拟机)–>OS(操作系统)–>OS调用写数据的方法–>把数据写入到文件中

字节输出流的使用步骤(重点):

  • 创建一个FileOutputStream对象,构造方法中传递写入数据的目的地

  • 调用FileOutputStream对象中的方法write,把数据写入到文件中

  • 释放资源(流使用会占用一定的内存,使用完毕要把内存清空,提供程序的效率)

public class Demo01OutputStream {
    
    public static void main(String[] args) throws IOException {
        // 1.创建一个FileOutputStream对象,构造方法中传递写入数据的目的地
        // 如果 a.txt 这个文件没有的话,就会自动创建
        FileOutputStream fos = new FileOutputStream("G:\\test\\a.txt");
        
        // 2.调用FileOutputStream对象中的方法write,把数据写入到文件中
        fos.write(97);  // 此时在 a.txt中存储的是: a
        // 3.释放资源(流使用会占用一定的内存,使用完毕要把内存清空,提供程序的效率)
        fos.close();
    }
}

5.1.1 一次写多个字节

public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。
public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。

public class Demo02OutputStream {
    public static void main(String[] args) throws IOException {
        // 创建FileOutputStream对象,构造方法中绑定要写入数据的目的地
        FileOutputStream fos = new FileOutputStream(new File("G:\\test\\b.txt"));
        // 调用FileOutputStream对象中的方法write,把数据写入到文件中
        fos.write(49);  // 1
        fos.write(48);  // 0
        fos.write(48);  // 0
        // 最终会在文件中显示100,这个100是由三个数字拼凑而成的

        /*
            public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。
            一次写多个字节:
            如果写的第一个字节是正数(0-127),那么显示的时候会查询ASCII表
            如果写的第一个字节是负数,那第一个字节会和第二个字节,两个字节组成一个中文显示,查询系统默认码表(GBK)
         */
        byte[] bytes = {65,66,67,68,69};
        fos.write(bytes);
        // 在文件中显示 ABCDE
        
        byte[] bytes = {-65,-66,-67,68,69};
        fos.write(bytes);
        // 在文件中显示 烤紻E(乱码)

        /*
            public void write(byte[] b, int off, int len) :把字节数组的一部分写入到文件中
                int off:数组的开始索引
                int len:写几个字节
         */
        // 从bytes的数组中找到下标是1和2的数据,然后写进去
        fos.write(bytes,1,2);
        // 在文件中显示 BC

        /*
            写入字符的方法:可以使用String类中的方法把字符串,转换为字节数组
                byte[] getBytes()  把字符串转换为字节数组
        */
        byte[] bytes2 = "你好".getBytes();
        System.out.println(Arrays.toString(bytes2));  // [-28, -67, -96, -27, -91, -67]
        fos.write(bytes2);

        //释放资源
        fos.close();
    }
}

5.1.2 追加写/续写

追加写/续写:使用两个参数的构造方法
FileOutputStream(String name, boolean append)创建一个向具有指定 name 的文件中写入数据的输出文件流。
FileOutputStream(File file, boolean append) 创建一个向指定 File 对象表示的文件中写入数据的文件输出流。

参数:
String name,File file:写入数据的目的地
boolean append:追加写开关
true:创建对象不会覆盖源文件,继续在文件的末尾追加写数据
false:创建一个新文件,覆盖源文件

写换行:写换行符号
windows:\r\n
linux:/n
mac:/r

以下两种方式都能在文件中写出10个你好

public class Demo03OutputStream {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("G:\\test2\\a.txt",true);
        for (int i = 1; i <=10 ; i++) {
            fos.write("你好".getBytes());
            fos.write("\r\n".getBytes());
        }
        fos.close();
    }
}
public class Demo03OutputStream {
    public static void main(String[] args) throws IOException {
        File file = new File("G:\\test2\\a.txt");
        FileOutputStream fos = new FileOutputStream(file, true);
        for (int i = 1; i <=10 ; i++) {
            fos.write("你好".getBytes());
            fos.write("\r\n".getBytes());
        }
        fos.close();
    }
}

5.2 字节输入流

java.io.InputStream:字节输入流

此抽象类是表示字节输入流的所有类的超类。

定义了所有子类共性的方法:

  • int read()从输入流中读取数据的下一个字节。
  • int read(byte[] b) 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
  • void close() 关闭此输入流并释放与该流关联的所有系统资源。

java.io.FileInputStream extends InputStream
FileInputStream:文件字节输入流
作用:把硬盘文件中的数据,读取到内存中使用

构造方法:
FileInputStream(String name)
FileInputStream(File file)

参数:读取文件的数据源
String name:文件的路径
File file:文件

构造方法的作用:
1.会创建一个FileInputStream对象
2.会把FileInputStream对象指定构造方法中要读取的文件

读取数据的原理(硬盘–>内存)

java程序–>JVM–>OS–>OS读取数据的方法–>读取文件

字节输入流的使用步骤(重点):

  • 创建FileInputStream对象,构造方法中绑定要读取的数据源
  • 使用FileInputStream对象中的方法read,读取文件
  • 释放资源
public class Demo01InputStream {
    public static void main(String[] args) throws IOException {
        // 1.创建FileInputStream对象,构造方法中绑定要读取的数据源
        FileInputStream fis = new FileInputStream("G:\\test\\a.txt");
        // 2.使用FileInputStream对象中的方法read,读取文件
        // int read()读取文件中的一个字节并返回,读取到文件的末尾返回-1
        /*int len = fis.read();
        System.out.println(len);  // 97 a

        len = fis.read();
        System.out.println(len);  // 98 b

        len = fis.read();
        System.out.println(len);  // 99 c

        len = fis.read();
        System.out.println(len);  // -1

        len = fis.read();
        System.out.println(len);  // -1

        /*
            发现以上读取文件是一个重复的过程,所以可以使用循环优化
            不知道文件中有多少字节,使用while循环
            while循环结束条件,读取到-1的时候结束

            布尔表达式(len = fis.read())!=-1
                1.fis.read():读取一个字节
                2.len = fis.read():把读取到的字节赋值给变量len
                3.(len = fis.read())!=-1:判断变量len是否不等于-1
         */
        int len = 0; // 记录读取到的字节
        while((len = fis.read())!=-1){
            System.out.print(len);//abc
        }

        // 3.释放资源
        fis.close();
    }
}

5.2.1 字节输入流一次读取多个字节的方法

字节输入流一次读取多个字节的方法:
int read(byte[] b) 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
明确两件事情:

  1. 方法的参数byte[]的作用?
    起到缓冲作用,存储每次读取到的多个字节
    数组的长度一把定义为1024(1kb)或者1024的整数倍
  2. 方法的返回值int是什么?
    每次读取的有效字节个数

String类的构造方法
String(byte[] bytes) :把字节数组转换为字符串
String(byte[] bytes, int offset, int length) 把字节数组的一部分转换为字符串 offset:数组的开始索引 length:转换的字节个数

public class Demo02InputStream {
    public static void main(String[] args) throws IOException {
        // 创建FileInputStream对象,构造方法中绑定要读取的数据源
        FileInputStream fis = new FileInputStream("G:\\test\\a.txt");
        // 使用FileInputStream对象中的方法read读取文件
        
        // int read(byte[] b) 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
        /*byte[] bytes = new byte[2];
        int len = fis.read(bytes);
        System.out.println(len);//2
        //System.out.println(Arrays.toString(bytes));  // [65, 66]
        System.out.println(new String(bytes));  // AB

        len = fis.read(bytes);
        System.out.println(len);  // 2
        System.out.println(new String(bytes));  // CD

        len = fis.read(bytes);
        System.out.println(len);  // 1
        System.out.println(new String(bytes));  // ED

        len = fis.read(bytes);
        System.out.println(len);  // -1
        System.out.println(new String(bytes));  // ED*/

        /*
            发现以上读取时一个重复的过程,可以使用循环优化
            不知道文件中有多少字节,所以使用while循环
            while循环结束的条件,读取到-1结束
        */
        byte[] bytes = new byte[1024];  // 存储读取到的多个字节
        int len = 0; // 记录每次读取的有效字节个数
        while((len = fis.read(bytes))!=-1){
            // String(byte[] bytes, int offset, int length) 把字节数组的一部分转换为字符串 offset:数组的开始索引 length:转换的字节个数
            System.out.println(new String(bytes,0,len));
        }

        // 释放资源
        fis.close();
    }
}

5.3 文件复制练习

文件复制练习:一读一写

明确:
数据源: c:\1.jpg
数据的目的地: d:\1.jpg

文件复制的步骤:
1.创建一个字节输入流对象,构造方法中绑定要读取的数据源
2.创建一个字节输出流对象,构造方法中绑定要写入的目的地
3.使用字节输入流对象中的方法read读取文件
4.使用字节输出流中的方法write,把读取到的字节写入到目的地的文件中
5.释放资源

public class Demo01CopyFile {
    public static void main(String[] args) throws IOException {
        long s = System.currentTimeMillis();
        // 1.创建一个字节输入流对象,构造方法中绑定要读取的数据源
        FileInputStream fis = new FileInputStream("c:\\1.jpg");
        // 2.创建一个字节输出流对象,构造方法中绑定要写入的目的地
        FileOutputStream fos = new FileOutputStream("d:\\1.jpg");
        // 一次读取一个字节写入一个字节的方式
        // 3.使用字节输入流对象中的方法read读取文件
        /*int len = 0;
        while((len = fis.read())!=-1){
            // 4.使用字节输出流中的方法write,把读取到的字节写入到目的地的文件中
            fos.write(len);
        }*/

        // 使用数组缓冲读取多个字节,写入多个字节
        byte[] bytes = new byte[1024];
        // 3.使用字节输入流对象中的方法read读取文件
        int len = 0;//每次读取的有效字节个数
        while((len = fis.read(bytes))!=-1){
            // 4.使用字节输出流中的方法write,把读取到的字节写入到目的地的文件中
            fos.write(bytes,0,len);
        }

        // 5.释放资源(先关写的,后关闭读的;如果写完了,肯定读取完毕了)
        fos.close();
        fis.close();
        long e = System.currentTimeMillis();
        System.out.println("复制文件共耗时:"+(e-s)+"毫秒");
    }
}

5.4 使用字节流读取中文文件

1个中文
GBK:占用两个字节
UTF-8:占用3个字节

import java.io.FileInputStream;
import java.io.IOException;

public class Demo01InputStream {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("G:\\ch.txt");
        int len = 0;
        while((len = fis.read())!=-1){
            System.out.println((char)len);
        }
        fis.close();
    }
}

6.字符流

当使用字节流读取文本文件时,可能会有一个小问题。就是遇到中文字符时,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储。所以Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。

6.1 字符输入流

java.io.Reader:字符输入流,是字符输入流的最顶层的父类,定义了一些共性的成员方法,是一个抽象类
共性的成员方法:
int read() 读取单个字符并返回。
int read(char[] cbuf)一次读取多个字符,将字符读入数组。
void close() 关闭该流并释放与之关联的所有资源。

java.io.FileReader extends InputStreamReader extends Reader
FileReader:文件字符输入流
作用:把硬盘文件中的数据以字符的方式读取到内存中

构造方法:
FileReader(String fileName)
FileReader(File file)

参数:读取文件的数据源
String fileName:文件的路径
File file:一个文件

FileReader构造方法的作用:
1.创建一个FileReader对象
2.会把FileReader对象指向要读取的文件

字符输入流的使用步骤:
1.创建FileReader对象,构造方法中绑定要读取的数据源
2.使用FileReader对象中的方法read读取文件
3.释放资源


public class Demo02Reader {
    public static void main(String[] args) throws IOException {
        // 1.创建FileReader对象,构造方法中绑定要读取的数据源
        FileReader fr = new FileReader("G:\\ch.txt");
        // 2.使用FileReader对象中的方法read读取文件
        // int read() 读取单个字符并返回。
        /*int len = 0;
        while((len = fr.read())!=-1){
            System.out.print((char)len);
        }*/

        // int read(char[] cbuf)一次读取多个字符,将字符读入数组。
        char[] cs = new char[1024];//存储读取到的多个字符
        int len = 0;//记录的是每次读取的有效字符个数
        while((len = fr.read(cs))!=-1){
            /*
                String类的构造方法
                String(char[] value) 把字符数组转换为字符串
                String(char[] value, int offset, int count) 把字符数组的一部分转换为字符串 offset数组的开始索引 count转换的个数
            */
            System.out.println(new String(cs,0,len));
        }

        // 3.释放资源
        fr.close();
    }
}

6.2 字符输出流

java.io.Writer:字符输出流,是所有字符输出流的最顶层的父类,是一个抽象类

共性的成员方法:

  • void write(int c) 写入单个字符。
  • void write(char[] cbuf)写入字符数组。
  • abstract void write(char[] cbuf, int off, int len)写入字符数组的某一部分,off数组的开始索引,len写的字符个数。
  • void write(String str)写入字符串。
  • void write(String str, int off, int len) 写入字符串的某一部分,off字符串的开始索引,len写的字符个数。
  • void flush()刷新该流的缓冲。
  • void close() 关闭此流,但要先刷新它。

java.io.FileWriter extends OutputStreamWriter extends Writer
FileWriter:文件字符输出流
作用:把内存中字符数据写入到文件中

构造方法:
FileWriter(File file)根据给定的 File 对象构造一个 FileWriter 对象。
FileWriter(String fileName) 根据给定的文件名构造一个 FileWriter 对象。

参数:写入数据的目的地
String fileName:文件的路径
File file:是一个文件

构造方法的作用:
1.会创建一个FileWriter对象
2.会根据构造方法中传递的文件/文件的路径,创建文件
3.会把FileWriter对象指向创建好的文件

字符输出流的使用步骤(重点):
1.创建FileWriter对象,构造方法中绑定要写入数据的目的地
2.使用FileWriter中的方法write,把数据写入到内存缓冲区中(字符转换为字节的过程)
3.使用FileWriter中的方法flush,把内存缓冲区中的数据,刷新到文件中
4.释放资源(会先把内存缓冲区中的数据刷新到文件中)

public class Demo01Writer {
    public static void main(String[] args) throws IOException {
        // 1.创建FileWriter对象,构造方法中绑定要写入数据的目的地
        FileWriter fw = new FileWriter("G:\\ch.txt");
        // 2.使用FileWriter中的方法write,把数据写入到内存缓冲区中(字符转换为字节的过程)
        // void write(int c) 写入单个字符。
        fw.write(97);
        fw.write("你好啊\r\n");   // 写入中文,通过 \r\n来进行换行
        // 3.使用FileWriter中的方法flush,把内存缓冲区中的数据,刷新到文件中
        fw.flush();
        // 4.释放资源(会先把内存缓冲区中的数据刷新到文件中)
        fw.close();
    }
}

6.3 flush和close方法的区别

flush方法和close方法的区别

flush :刷新缓冲区,流对象可以继续使用。

close: 先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。

public class Demo02CloseAndFlush {
    public static void main(String[] args) throws IOException {
        // 1.创建FileWriter对象,构造方法中绑定要写入数据的目的地
        FileWriter fw = new FileWriter("09_IOAndProperties\\e.txt");
        // 2.使用FileWriter中的方法write,把数据写入到内存缓冲区中(字符转换为字节的过程)
        // void write(int c) 写入单个字符。
        fw.write(97);
        // 3.使用FileWriter中的方法flush,把内存缓冲区中的数据,刷新到文件中
        fw.flush();
        // 刷新之后流可以继续使用
        fw.write(98);

        // 4.释放资源(会先把内存缓冲区中的数据刷新到文件中)
        fw.close();

        // close方法之后流已经关闭了,已经从内存中消失了,流就不能再使用了
        fw.write(99);//IOException: Stream closed
    }
}

6.4 字符输出流写数据的其他方法

字符输出流写数据的其他方法

  • void write(char[] cbuf)写入字符数组。
  • abstract void write(char[] cbuf, int off, int len)写入字符数组的某一部分,off数组的开始索引,len写的字符个数。
  • void write(String str)写入字符串。
  • void write(String str, int off, int len) 写入字符串的某一部分,off字符串的开始索引,len写的字符个数。
public class Demo03Writer {
    public static void main(String[] args) throws IOException {
        FileWriter fw = new FileWriter("09_IOAndProperties\\f.txt");
        char[] cs = {'a','b','c','d','e'};
        // void write(char[] cbuf)写入字符数组。
        fw.write(cs);//abcde

        // void write(char[] cbuf, int off, int len)写入字符数组的某一部分,off数组的开始索引,len写的字符个数。
        fw.write(cs,1,3);  // bcd

        // void write(String str)写入字符串。
        fw.write("传智播客");  // 传智播客

        // void write(String str, int off, int len) 写入字符串的某一部分,off字符串的开始索引,len写的字符个数。
        fw.write("黑马程序员",2,3);  // 程序员

        fw.close();
    }
}

6.5 续写和换行

续写,追加写:使用两个参数的构造方法
FileWriter(String fileName, boolean append)
FileWriter(File file, boolean append)

参数:
String fileName,File file:写入数据的目的地
boolean append:续写开关 true:不会创建新的文件覆盖源文件,可以续写; false:创建新的文件覆盖源文件

换行:换行符号
windows:\r\n
linux:/n
mac:/r

public class Demo04Writer {
    public static void main(String[] args) throws IOException {
        FileWriter fw = new FileWriter("09_IOAndProperties\\g.txt",true);
        for (int i = 0; i <10 ; i++) {
            fw.write("HelloWorld"+i+"\r\n");
        }

        fw.close();
    }
}

7. IO异常的处理

7.1 jdk1.7之前

在jdk1.7之前使用try catch finally 处理流中的异常
格式:

try{
    可能会产出异常的代码
}catch(异常类变量 变量名){
    异常的处理逻辑
}finally{
    一定会指定的代码
        资源释放
}
public class Demo01TryCatch {
    public static void main(String[] args) {
        //提高变量fw的作用域,让finally可以使用
        //变量在定义的时候,可以没有值,但是使用的时候必须有值
        //fw = new FileWriter("09_IOAndProperties\\g.txt",true); 执行失败,fw没有值,fw.close会报错
        FileWriter fw = null;
        try{
            //可能会产出异常的代码
            fw = new FileWriter("w:\\09_IOAndProperties\\g.txt",true);
            for (int i = 0; i <10 ; i++) {
                fw.write("HelloWorld"+i+"\r\n");
            }
        }catch(IOException e){
            //异常的处理逻辑
            System.out.println(e);
        }finally {
            //一定会指定的代码
            //创建对象失败了,fw的默认值就是null,null是不能调用方法的,会抛出NullPointerException,需要增加一个判断,不是null在把资源释放
            if(fw!=null){
                try {
                    //fw.close方法声明抛出了IOException异常对象,所以我们就的处理这个异常对象,要么throws,要么try catch
                    fw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }
}

7.2 JDK7的新特性

在try的后边可以增加一个(),在括号中可以定义流对象
那么这个流对象的作用域就在try中有效

try中的代码执行完毕,会自动把流对象释放,不用写finally
格式:

try(定义流对象;定义流对象....){
    可能会产出异常的代码
}catch(异常类变量 变量名){
    异常的处理逻辑
}
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Demo02JDK7 {
    public static void main(String[] args) {
        try(// 1.创建一个字节输入流对象,构造方法中绑定要读取的数据源
            FileInputStream fis = new FileInputStream("c:\\1.jpg");
            // 2.创建一个字节输出流对象,构造方法中绑定要写入的目的地
            FileOutputStream fos = new FileOutputStream("d:\\1.jpg");){

            // 可能会产出异常的代码
            // 一次读取一个字节写入一个字节的方式
            // 3.使用字节输入流对象中的方法read读取文件
            int len = 0;
            while((len = fis.read())!=-1){
                // 4.使用字节输出流中的方法write,把读取到的字节写入到目的地的文件中
                fos.write(len);
            }

        }catch (IOException e){
            // 异常的处理逻辑
            System.out.println(e);
        }
    }
}

7.3 JDK9新特性

try的前边可以定义流对象
在try后边的()中可以直接引入流对象的名称(变量名)
在try代码执行完毕之后,流对象也可以释放掉,不用写finally
格式:

A a = new A();
B b = new B();
try(a,b){
   可能会产出异常的代码
}catch(异常类变量 变量名){
   异常的处理逻辑
}
public class Demo03JDK9 {
    public static void main(String[] args) throws IOException {
        // 1.创建一个字节输入流对象,构造方法中绑定要读取的数据源
        FileInputStream fis = new FileInputStream("c:\\1.jpg");
        // 2.创建一个字节输出流对象,构造方法中绑定要写入的目的地
        FileOutputStream fos = new FileOutputStream("d:\\1.jpg");

        try(fis;fos){
            // 一次读取一个字节写入一个字节的方式
            // 3.使用字节输入流对象中的方法read读取文件
            int len = 0;
            while((len = fis.read())!=-1){
                // 4.使用字节输出流中的方法write,把读取到的字节写入到目的地的文件中
                fos.write(len);
            }
        }catch (IOException e){
            System.out.println(e);
        }
        // fos.write(1);//Stream Closed
    }
}

8.高效读写的缓冲流

缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。

8.1 字节缓冲流

8.1.1 字节缓冲输出流

java.io.BufferedOutputStream extends OutputStream
BufferedOutputStream:字节缓冲输出流

继承自父类的共性成员方法:

  • public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
  • public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
  • public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。
  • public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
  • public abstract void write(int b) :将指定的字节输出流。

构造方法:
BufferedOutputStream(OutputStream out) 创建一个新的缓冲输出流,以将数据写入指定的底层输出流。
BufferedOutputStream(OutputStream out, int size) 创建一个新的缓冲输出流,以将具有指定缓冲区大小的数据写入指定的底层输出流。

参数:
OutputStream out:字节输出流

我们可以传递FileOutputStream,缓冲流会给FileOutputStream增加一个缓冲区,提高FileOutputStream写入效率

int size:指定缓冲流内部缓冲区的大小,不指定默认

使用步骤(重点)
1.创建FileOutputStream对象,构造方法中绑定要输出的目的地
2.创建BufferedOutputStream对象,构造方法中传递FileOutputStream对象对象,提高FileOutputStream对象效率
3.使用BufferedOutputStream对象中的方法write,把数据写入到内部缓冲区中
4.使用BufferedOutputStream对象中的方法flush,把内部缓冲区中的数据,刷新到文件中
5.释放资源(会先调用flush方法刷新数据,第4部可以省略)

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Demo01BufferedOutputStream {
    public static void main(String[] args) throws IOException {
        // 1.创建FileOutputStream对象,构造方法中绑定要输出的目的地
        FileOutputStream fos = new FileOutputStream("10_IO\\a.txt");
        // 2.创建BufferedOutputStream对象,构造方法中传递FileOutputStream对象对象,提高FileOutputStream对象效率
        BufferedOutputStream bos = new BufferedOutputStream(fos);
        // 3.使用BufferedOutputStream对象中的方法write,把数据写入到内部缓冲区中
        bos.write("我把数据写入到内部缓冲区中".getBytes());
        // 4.使用BufferedOutputStream对象中的方法flush,把内部缓冲区中的数据,刷新到文件中
        bos.flush();
        // 5.释放资源(会先调用flush方法刷新数据,第4步可以省略)
        bos.close();
    }
}

8.1.2 字节缓冲输入流

java.io.BufferedInputStream extends InputStream
BufferedInputStream:字节缓冲输入流

继承自父类的成员方法:
int read()从输入流中读取数据的下一个字节。
int read(byte[] b) 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
void close() 关闭此输入流并释放与该流关联的所有系统资源。

构造方法:

BufferedInputStream(InputStream in) 创建一个 BufferedInputStream 并保存参数,即输入流 in,以便将来使用。
BufferedInputStream(InputStream in, int size) 创建具有指定缓冲区大小的 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用。

参数:
InputStream in:字节输入流
我们可以传递FileInputStream,缓冲流会给FileInputStream增加一个缓冲区,提高FileInputStream的读取效率
int size:指定缓冲流内部缓冲区的大小,不指定默认

使用步骤(重点):
1.创建FileInputStream对象,构造方法中绑定要读取的数据源
2.创建BufferedInputStream对象,构造方法中传递FileInputStream对象,提高FileInputStream对象的读取效率
3.使用BufferedInputStream对象中的方法read,读取文件
4.释放资源

public class Demo02BufferedInputStream {
    public static void main(String[] args) throws IOException {
        // 1.创建FileInputStream对象,构造方法中绑定要读取的数据源
        FileInputStream fis = new FileInputStream("10_IO\\a.txt");
        // 2.创建BufferedInputStream对象,构造方法中传递FileInputStream对象,提高FileInputStream对象的读取效率
        BufferedInputStream bis = new BufferedInputStream(fis);

        // int read(byte[] b) 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
        byte[] bytes =new byte[1024];  // 存储每次读取的数据
        int len = 0;  // 记录每次读取的有效字节个数
        while((len = bis.read(bytes))!=-1){
            System.out.println(new String(bytes,0,len));
        }

        // 4.释放资源
        bis.close();
    }
}

8.1.3 字节输出流和输入流结合

public class BufferedDemo {
    public static void main(String[] args) throws FileNotFoundException {
      	// 记录开始时间
        long start = System.currentTimeMillis();
		// 创建流对象
        try (
			BufferedInputStream bis = new BufferedInputStream(new FileInputStream("G:\Git.exe"));
		 BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("G:\copy_git.exe"));
        ){
          	// 读写数据
            int len;
            // 使用数组来缓冲
            byte[] bytes = new byte[8*1024];
            while ((len = bis.read(bytes)) != -1) {
                // 把数据逐步的写进目标的位置
                bos.write(bytes, 0 , len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        
         //释放资源(先关写的,后关闭读的;如果写完了,肯定读取完毕了)
        bos.close();
        bis.close();
        
		// 记录结束时间
        long end = System.currentTimeMillis();
        System.out.println("缓冲流使用数组复制时间:"+(end - start)+" 毫秒");
    }
}

缓冲流使用数组复制时间:666 毫秒

8.2 字符缓冲流

8.2.1 字符缓冲输出流

java.io.BufferedWriter extends Writer
BufferedWriter:字符缓冲输出流

继承自父类的共性成员方法:

  • void write(int c) 写入单个字符。
  • void write(char[] cbuf)写入字符数组。
  • abstract void write(char[] cbuf, int off, int len)写入字符数组某部分,off数组的开始索引,len写的字符个数。
  • void write(String str)写入字符串。
  • void write(String str, int off, int len) 写入字符串的某一部分,off字符串的开始索引,len写的字符个数。
  • void flush()刷新该流的缓冲。
  • void close() 关闭此流,但要先刷新它。

构造方法:
BufferedWriter(Writer out) 创建一个使用默认大小输出缓冲区的缓冲字符输出流。
BufferedWriter(Writer out, int sz) 创建一个使用给定大小输出缓冲区的新缓冲字符输出流。

参数:
Writer out:字符输出流
我们可以传递FileWriter,缓冲流会给FileWriter增加一个缓冲区,提高FileWriter的写入效率
int sz:指定缓冲区的大小,不写默认大小

特有的成员方法:
void newLine() 写入一个行分隔符。会根据不同的操作系统,获取不同的行分隔符
换行:换行符号
windows:\r\n
linux:/n
mac:/r

使用步骤:
1.创建字符缓冲输出流对象,构造方法中传递字符输出流
2.调用字符缓冲输出流中的方法write,把数据写入到内存缓冲区中
3.调用字符缓冲输出流中的方法flush,把内存缓冲区中的数据,刷新到文件中
4.释放资源

public class Demo03BufferedWriter {
    public static void main(String[] args) throws IOException {
        //1.创建字符缓冲输出流对象,构造方法中传递字符输出流
        BufferedWriter bw = new BufferedWriter(new FileWriter("10_IO\\c.txt"));
        //2.调用字符缓冲输出流中的方法write,把数据写入到内存缓冲区中
        for (int i = 0; i <10 ; i++) {
            bw.write("传智播客");
            //bw.write("\r\n");
            bw.newLine();
        }
        //3.调用字符缓冲输出流中的方法flush,把内存缓冲区中的数据,刷新到文件中
        bw.flush();
        //4.释放资源
        bw.close();
    }
}

8.2.2 字符缓冲输入流

java.io.BufferedReader extends Reader
BufferedReader:字符缓冲输入流

继承自父类的共性成员方法:
int read() 读取单个字符并返回。
int read(char[] cbuf)一次读取多个字符,将字符读入数组。
void close() 关闭该流并释放与之关联的所有资源。

构造方法:
BufferedReader(Reader in) 创建一个使用默认大小输入缓冲区的缓冲字符输入流。
BufferedReader(Reader in, int sz) 创建一个使用指定大小输入缓冲区的缓冲字符输入流。

参数:
Reader in:字符输入流
我们可以传递FileReader,缓冲流会给FileReader增加一个缓冲区,提高FileReader的读取效率

特有的成员方法:
​String readLine() 读取一个文本行。读取一行数据
​行的终止符号:通过下列字符之一即可认为某行已终止:换行 (’\n’)、回车 (’\r’) 或回车后直接跟着换行(\r\n)。

​返回值:
​包含该行内容的字符串,不包含任何行终止符,如果已到达流末尾,则返回 null

使用步骤:
1.创建字符缓冲输入流对象,构造方法中传递字符输入流
2.使用字符缓冲输入流对象中的方法read/readLine读取文本
3.释放资源

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class Demo04BufferedReader {
    public static void main(String[] args) throws IOException {
        //1.创建字符缓冲输入流对象,构造方法中传递字符输入流
        BufferedReader br = new BufferedReader(new FileReader("10_IO\\c.txt"));

        //2.使用字符缓冲输入流对象中的方法read/readLine读取文本
        /*String line = br.readLine();
        System.out.println(line);

        line = br.readLine();
        System.out.println(line);

        line = br.readLine();
        System.out.println(line);

        line = br.readLine();
        System.out.println(line);*/

        /*
            发下以上读取是一个重复的过程,所以可以使用循环优化
            不知道文件中有多少行数据,所以使用while循环
            while的结束条件,读取到null结束
         */
        String line;
        while((line = br.readLine())!=null){
            System.out.println(line);
        }

        //3.释放资源
        br.close();
    }
}

8.3 练习

练习:
对文本的内容进行排序
按照(1,2,3…)顺序排序

分析:
1.创建一个HashMap集合对象,可以:存储每行文本的序号(1,2,3,…);value:存储每行的文本
2.创建字符缓冲输入流对象,构造方法中绑定字符输入流
3.创建字符缓冲输出流对象,构造方法中绑定字符输出流
4.使用字符缓冲输入流中的方法readline,逐行读取文本
5.对读取到的文本进行切割,获取行中的序号和文本内容
6.把切割好的序号和文本的内容存储到HashMap集合中(key序号是有序的,会自动排序1,2,3,4…)
7.遍历HashMap集合,获取每一个键值对
8.把每一个键值对,拼接为一个文本行
9.把拼接好的文本,使用字符缓冲输出流中的方法write,写入到文件中
10.释放资源

public class Demo05Test {
    public static void main(String[] args) throws IOException {
        // 1.创建一个HashMap集合对象,可以:存储每行文本的序号(1,2,3,..);value:存储每行的文本
        HashMap<String,String> map = new HashMap<>();
        // 2.创建字符缓冲输入流对象,构造方法中绑定字符输入流
        BufferedReader br = new BufferedReader(new FileReader("10_IO\\in.txt"));
        // 3.创建字符缓冲输出流对象,构造方法中绑定字符输出流
        BufferedWriter bw = new BufferedWriter(new FileWriter("10_IO\\out.txt"));
        // 4.使用字符缓冲输入流中的方法readline,逐行读取文本
        String line;
        while((line = br.readLine())!=null){
            // 5.对读取到的文本进行切割,获取行中的序号和文本内容
            String[] arr = line.split("\\.");
            // 6.把切割好的序号和文本的内容存储到HashMap集合中(key序号是有序的,会自动排序1,2,3,4..)
            map.put(arr[0],arr[1]);
        }

        // 7.遍历HashMap集合,获取每一个键值对
        for(String key : map.keySet()){
            String value = map.get(key);
            // 8.把每一个键值对,拼接为一个文本行
            line = key + "." + value;
            // 9.把拼接好的文本,使用字符缓冲输出流中的方法write,写入到文件中
            bw.write(line);
            bw.newLine();//写换行
        }
        // 10.释放资源
        bw.close();
        br.close();
    }
}

9. 转换编码的转换流

需要用转换流的原因

import java.io.FileReader;
import java.io.IOException;

/*
    FileReader可以读取IDE默认编码格式(UTF-8)的文件
    FileReader读取系统默认编码(中文GBK)会产生乱码���
 */
public class Demo01FileReader {
    public static void main(String[] args) throws IOException {
        FileReader fr = new FileReader("G:\我是GBK格式的文本.txt");
        int len = 0;
        while((len = fr.read())!=-1){
            System.out.print((char)len);  // ���
        }
        fr.close();
    }
}

9.1 编码的OutputStreamWriter

java.io.OutputStreamWriter extends Writer
OutputStreamWriter: 是字符流通向字节流的桥梁:可使用指定的 charset 将要写入流中的字符编码成字节。(编码:把能看懂的变成看不懂)

继续自父类的共性成员方法:

  • void write(int c) 写入单个字符。
  • void write(char[] cbuf)写入字符数组。
  • abstract void write(char[] cbuf, int off, int len)写入字符数组的某一部分,off数组的开始索引,len写的字符个数。
  • void write(String str)写入字符串。
  • void write(String str, int off, int len) 写入字符串的某一部分,off字符串的开始索引,len写的字符个数。
  • void flush()刷新该流的缓冲。
  • void close() 关闭此流,但要先刷新它。

OutputStreamWriter(OutputStream out)创建使用默认字符编码的 OutputStreamWriter。
OutputStreamWriter(OutputStream out, String charsetName) 创建使用指定字符集的 OutputStreamWriter。

参数:
OutputStream out:字节输出流,可以用来写转换之后的字节到文件中
String charsetName:指定的编码表名称,不区分大小写,可以是utf-8/UTF-8,gbk/GBK,…不指定默认使用UTF-8

使用步骤:
1.创建OutputStreamWriter对象,构造方法中传递字节输出流和指定的编码表名称
2.使用OutputStreamWriter对象中的方法write,把字符转换为字节存储缓冲区中(编码)
3.使用OutputStreamWriter对象中的方法flush,把内存缓冲区中的字节刷新到文件中(使用字节流写字节的过程)
4.释放资源

public class Demo02OutputStreamWriter {
    public static void main(String[] args) throws IOException {
        write_utf_8();
        write_gbk();
    }

    /*
       使用转换流OutputStreamWriter写GBK格式的文件
    */
    private static void write_gbk() throws IOException {
        //1.创建OutputStreamWriter对象,构造方法中传递字节输出流和指定的编码表名称
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("10_IO\\gbk.txt"),"GBK");
        //2.使用OutputStreamWriter对象中的方法write,把字符转换为字节存储缓冲区中(编码)
        osw.write("你好");
        //3.使用OutputStreamWriter对象中的方法flush,把内存缓冲区中的字节刷新到文件中(使用字节流写字节的过程)
        osw.flush();
        //4.释放资源
        osw.close();
    }

    /*
        使用转换流OutputStreamWriter写UTF-8格式的文件
    */
    private static void write_utf_8() throws IOException {
        //1.创建OutputStreamWriter对象,构造方法中传递字节输出流和指定的编码表名称
        //OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("10_IO\\utf_8.txt"),"utf-8");
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("10_IO\\utf_8.txt"));//不指定默认使用UTF-8
        //2.使用OutputStreamWriter对象中的方法write,把字符转换为字节存储缓冲区中(编码)
        osw.write("你好");
        //3.使用OutputStreamWriter对象中的方法flush,把内存缓冲区中的字节刷新到文件中(使用字节流写字节的过程)
        osw.flush();
        //4.释放资源
        osw.close();
    }
}

9.2 解码的InputStreamReader

java.io.InputStreamReader extends Reader
InputStreamReader:是字节流通向字符流的桥梁:它使用指定的 charset 读取字节并将其解码为字符。(解码:把看不懂的变成能看懂的)

继承自父类的共性成员方法:
int read() 读取单个字符并返回。
int read(char[] cbuf)一次读取多个字符,将字符读入数组。
void close() 关闭该流并释放与之关联的所有资源。

构造方法:
InputStreamReader(InputStream in) 创建一个使用默认字符集的 InputStreamReader。
InputStreamReader(InputStream in, String charsetName) 创建使用指定字符集的 InputStreamReader。

参数:
InputStream in:字节输入流,用来读取文件中保存的字节
String charsetName:指定的编码表名称,不区分大小写,可以是utf-8/UTF-8,gbk/GBK,…不指定默认使用UTF-8

使用步骤:
1.创建InputStreamReader对象,构造方法中传递字节输入流和指定的编码表名称
2.使用InputStreamReader对象中的方法read读取文件
3.释放资源

注意事项:
构造方法中指定的编码表名称要和文件的编码相同,否则会发生乱码

public class Demo03InputStreamReader {
    public static void main(String[] args) throws IOException {
        read_utf_8();
        read_gbk();
    }


    /*
        使用InputStreamReader读取GBK格式的文件
    */
    private static void read_gbk() throws IOException {
        //1.创建InputStreamReader对象,构造方法中传递字节输入流和指定的编码表名称
        //InputStreamReader isr = new InputStreamReader(new FileInputStream("10_IO\\gbk.txt"),"UTF-8");//???
        InputStreamReader isr = new InputStreamReader(new FileInputStream("10_IO\\gbk.txt"),"GBK");//你好

        //2.使用InputStreamReader对象中的方法read读取文件
        int len = 0;
        while((len = isr.read())!=-1){
            System.out.println((char)len);
        }
        //3.释放资源
        isr.close();
    }

    /*
        使用InputStreamReader读取UTF-8格式的文件
    */
    private static void read_utf_8() throws IOException {
        //1.创建InputStreamReader对象,构造方法中传递字节输入流和指定的编码表名称
        //InputStreamReader isr = new InputStreamReader(new FileInputStream("10_IO\\utf_8.txt"),"UTF-8");
        InputStreamReader isr = new InputStreamReader(new FileInputStream("10_IO\\utf_8.txt"));//不指定默认使用UTF-8
        //2.使用InputStreamReader对象中的方法read读取文件
        int len = 0;
        while((len = isr.read())!=-1){
            System.out.println((char)len);
        }
        //3.释放资源
        isr.close();
    }
}

9.3 练习

练习:转换文件编码
将GBK编码的文本文件,转换为UTF-8编码的文本文件。

分析:
1.创建InputStreamReader对象,构造方法中传递字节输入流和指定的编码表名称GBK
2.创建OutputStreamWriter对象,构造方法中传递字节输出流和指定的编码表名称UTF-8
3.使用InputStreamReader对象中的方法read读取文件
4.使用OutputStreamWriter对象中的方法write,把读取的数据写入到文件中
5.释放资源

import java.io.*;

public class Demo04Test {
    public static void main(String[] args) throws IOException {
        // 1.创建InputStreamReader对象,构造方法中传递字节输入流和指定的编码表名称GBK
        InputStreamReader isr = new InputStreamReader(new FileInputStream("10_IO\\我是GBK格式的文本.txt"),"GBK");
        // 2.创建OutputStreamWriter对象,构造方法中传递字节输出流和指定的编码表名称UTF-8
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("10_IO\\我是utf_8格式的文件.txt"),"UTF-8");
        // 3.使用InputStreamReader对象中的方法read读取文件
        int len = 0;
        while((len = isr.read())!=-1){
            // 4.使用OutputStreamWriter对象中的方法write,把读取的数据写入到文件中
            osw.write(len);
        }
        // 5.释放资源
        osw.close();
        isr.close();
    }
}

10.持久化存储对象的序列化流

序列化和反序列化的时候,会抛出NotSerializableException没有序列化异常
类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。

Serializable接口也叫标记型接口
要进行序列化和反序列化的类必须实现Serializable接口,就会给类添加一个标记
当我们进行序列化和反序列化的时候,就会检测类上是否有这个标记
有:就可以序列化和反序列化
没有:就会抛出 NotSerializableException异常
去市场买肉–>肉上有一个蓝色章(检测合格)–>放心购买–>买回来怎么吃随意

static关键字:静态关键字
静态优先于非静态加载到内存中(静态优先于对象进入到内存中)
被static修饰的成员变量不能被序列化的,序列化的都是对象

private static int age;
oos.writeObject(new Person("小美女",18));
Object o = ois.readObject();
Person{name='小美女', age=0}

transient关键字:瞬态关键字
被transient修饰成员变量,不能被序列化

private transient int age;
oos.writeObject(new Person("小美女",18));
Object o = ois.readObject();
Person{name='小美女', age=0}

先构造一个标准的类

public class Person implements Serializable{
    private static final long serialVersionUID = 1L;
    private String name;
    // private static int age; 不能被序列化
    // private transient int age;  不能被序列化
    public int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", 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;
    }
}

10.1 流的方式将对象写入文件

java.io.ObjectOutputStream extends OutputStream
ObjectOutputStream:对象的序列化流
作用:把对象以流的方式写入到文件中保存

构造方法:
ObjectOutputStream(OutputStream out) 创建写入指定 OutputStream 的 ObjectOutputStream。

参数:
OutputStream out:字节输出流

特有的成员方法:
void writeObject(Object obj) 将指定的对象写入 ObjectOutputStream。

使用步骤:
1.创建ObjectOutputStream对象,构造方法中传递字节输出流
2.使用ObjectOutputStream对象中的方法writeObject,把对象写入到文件中
3.释放资源

public class Demo01ObjectOutputStream {
    public static void main(String[] args) throws IOException {
        // 1.创建ObjectOutputStream对象,构造方法中传递字节输出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("G:\Person.txt"));
        // 2.使用ObjectOutputStream对象中的方法writeObject,把对象写入到文件中
        oos.writeObject(new Person("小美女",18));
        // 3.释放资源
        oos.close();
    }
}

10.2 流的方式读取文件中保存的对象

java.io.ObjectInputStream extends InputStream
ObjectInputStream:对象的反序列化流
作用:把文件中保存的对象,以流的方式读取出来使用

构造方法:
ObjectInputStream(InputStream in) 创建从指定 InputStream 读取的 ObjectInputStream。

参数:
InputStream in:字节输入流

特有的成员方法:
Object readObject() 从 ObjectInputStream 读取对象。

使用步骤:
1.创建ObjectInputStream对象,构造方法中传递字节输入流
2.使用ObjectInputStream对象中的方法readObject读取保存对象的文件
3.释放资源
4.使用读取出来的对象(打印)

readObject方法声明抛出了ClassNotFoundException(class文件找不到异常)
当不存在对象的class文件时抛出此异常

反序列化的前提:
1.类必须实现Serializable
2.必须存在类对应的class文件

public class Demo02ObjectInputStream {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 1.创建ObjectInputStream对象,构造方法中传递字节输入流
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("G:\Person.txt"));
        // 2.使用ObjectInputStream对象中的方法readObject读取保存对象的文件
        Object o = ois.readObject();
        // 3.释放资源
        ois.close();
        // 4.使用读取出来的对象(打印)
        System.out.println(o);
        Person p = (Person)o;
        System.out.println(p.getName()+p.getAge());
    }
}

10.3 序列化集合练习

练习:序列化集合
当我们想在文件中保存多个对象的时候
可以把多个对象存储到一个集合中
对集合进序列化和反序列化

分析:
1.定义一个存储Person对象的ArrayList集合
2.往ArrayList集合中存储Person对象
3.创建一个序列化流ObjectOutputStream对象
4.使用ObjectOutputStream对象中的方法writeObject,对集合进行序列化
5.创建一个反序列化ObjectInputStream对象
6.使用ObjectInputStream对象中的方法readObject读取文件中保存的集合
7.把Object类型的集合转换为ArrayList类型
8.遍历ArrayList集合
9.释放资源

import java.io.*;
import java.util.ArrayList;

public class Demo03Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 1.定义一个存储Person对象的ArrayList集合
        ArrayList<Person> list = new ArrayList<>();
        // 2.往ArrayList集合中存储Person对象
        list.add(new Person("张三",18));
        list.add(new Person("李四",19));
        list.add(new Person("王五",20));
        
        // 3.创建一个序列化流ObjectOutputStream对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("G:\person_test.txt"));
        // 4.使用ObjectOutputStream对象中的方法writeObject,对集合进行序列化
        oos.writeObject(list);
        // 5.创建一个反序列化ObjectInputStream对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("G:\person_test.txt"));
        // 6.使用ObjectInputStream对象中的方法readObject读取文件中保存的集合
        Object o = ois.readObject();
        // 7.把Object类型的集合转换为ArrayList类型
        ArrayList<Person> list2 = (ArrayList<Person>)o;
        // 8.遍历ArrayList集合
        for (Person p : list2) {
            System.out.println(p);
        }
        
        //9.释放资源
        ois.close();
        oos.close();
    }
}

11.打印流

java.io.PrintStream:打印流
PrintStream 为其他输出流添加了功能,使它们能够方便地打印各种数据值表示形式。

PrintStream特点:
1.只负责数据的输出,不负责数据的读取
2.与其他输出流不同,PrintStream 永远不会抛出 IOException
3.有特有的方法,print,println
void print(任意类型的值)
void println(任意类型的值并换行)

构造方法:
PrintStream(File file):输出的目的地是一个文件
PrintStream(OutputStream out):输出的目的地是一个字节输出流
PrintStream(String fileName) :输出的目的地是一个文件路径
PrintStream extends OutputStream

继承自父类的成员方法:

  • public void close() :关闭此输出流并释放与此流相关联的任何系统资源。

  • public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。

  • public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。

  • public void write(byte[] b, int off, int len) :从指定字节数组写入 len字节,从偏移量 off开始输出到输出流。

  • public abstract void write(int b) :将指定的字节输出流。

    如果使用继承自父类的write方法写数据,那么查看数据的时候会查询编码表 97->a
    如果使用自己特有的方法print/println方法写数据,写的数据原样输出 97->97

public class Demo01PrintStream {
    public static void main(String[] args) throws FileNotFoundException {
        //System.out.println("HelloWorld");

        // 创建打印流PrintStream对象,构造方法中绑定要输出的目的地
        PrintStream ps = new PrintStream("G:\\print.txt");
        // 如果使用继承自父类的write方法写数据,那么查看数据的时候会查询编码表 97->a
        ps.write(97);
        // 如果使用自己特有的方法print/println方法写数据,写的数据原样输出 97->97
        ps.println(97);
        ps.println(8.8);
        ps.println('a');
        ps.println("HelloWorld");
        ps.println(true);

        // 释放资源
        ps.close();
    }
}

可以改变输出语句的目的地(打印流的流向)
输出语句,默认在控制台输出
使用System.setOut方法改变输出语句的目的地改为参数中传递的打印流的目的地
static void setOut(PrintStream out)
重新分配“标准”输出流。

public class Demo02PrintStream {
    public static void main(String[] args) throws FileNotFoundException {
        System.out.println("我是在控制台输出");  // 这样打印的内容在控制台打印
		
        // 这个行代码之下的任何打印语句的输出内容都会输出到控制台
        PrintStream ps = new PrintStream("G:\print_stream.txt");
        System.setOut(ps);  // 把输出语句的目的地改变为打印流的目的地
        System.out.println("我在打印流的目的地中输出");

        ps.close();
    }
}

12.网络编程

12.1 TCP通信的客户端

TCP通信的客户端:向服务器发送连接请求,给服务器发送数据,读取服务器回写的数据
表示客户端的类:
java.net.Socket:此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器间通信的端点。
套接字:包含了IP地址和端口号的网络单位

构造方法:
Socket(String host, int port) 创建一个流套接字并将其连接到指定主机上的指定端口号。
参数:
String host:服务器主机的名称/服务器的IP地址
int port:服务器的端口号

成员方法:
OutputStream getOutputStream() 返回此套接字的输出流。
InputStream getInputStream() 返回此套接字的输入流。
void close() 关闭此套接字。

实现步骤:
1.创建一个客户端对象Socket,构造方法绑定服务器的IP地址和端口号
2.使用Socket对象中的方法getOutputStream()获取网络字节输出流OutputStream对象
3.使用网络字节输出流OutputStream对象中的方法write,给服务器发送数据
4.使用Socket对象中的方法getInputStream()获取网络字节输入流InputStream对象
5.使用网络字节输入流InputStream对象中的方法read,读取服务器回写的数据
6.释放资源(Socket)

注意:
1.客户端和服务器端进行交互,必须使用Socket中提供的网络流,不能使用自己创建的流对象
2.当我们创建客户端对象Socket的时候,就会去请求服务器和服务器经过3次握手建立连接通路
这时如果服务器没有启动,那么就会抛出异常ConnectException: Connection refused: connect
如果服务器已经启动,那么就可以进行交互了

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class TCPClient {
    public static void main(String[] args) throws IOException {
        //1.创建一个客户端对象Socket,构造方法绑定服务器的IP地址和端口号
        Socket socket = new Socket("127.0.0.1",8888);
        //2.使用Socket对象中的方法getOutputStream()获取网络字节输出流OutputStream对象
        OutputStream os = socket.getOutputStream();
        //3.使用网络字节输出流OutputStream对象中的方法write,给服务器发送数据
        os.write("你好服务器".getBytes());

        //4.使用Socket对象中的方法getInputStream()获取网络字节输入流InputStream对象
        InputStream is = socket.getInputStream();

        //5.使用网络字节输入流InputStream对象中的方法read,读取服务器回写的数据
        byte[] bytes = new byte[1024];
        int len = is.read(bytes);
        System.out.println(new String(bytes,0,len));

        //6.释放资源(Socket)
        socket.close();
    }
}

12.2 TCP通信的服务器端

TCP通信的服务器端:接收客户端的请求,读取客户端发送的数据,给客户端回写数据
表示服务器的类:

java.net.ServerSocket:此类实现服务器套接字。

构造方法:
ServerSocket(int port) 创建绑定到特定端口的服务器套接字。

服务器端必须明确一件事情,必须的知道是哪个客户端请求的服务器
所以可以使用accept方法获取到请求的客户端对象Socket

成员方法:
Socket accept() 侦听并接受到此套接字的连接。

服务器的实现步骤:
1.创建服务器ServerSocket对象和系统要指定的端口号
2.使用ServerSocket对象中的方法accept,获取到请求的客户端对象Socket
3.使用Socket对象中的方法getInputStream()获取网络字节输入流InputStream对象
4.使用网络字节输入流InputStream对象中的方法read,读取客户端发送的数据
5.使用Socket对象中的方法getOutputStream()获取网络字节输出流OutputStream对象
6.使用网络字节输出流OutputStream对象中的方法write,给客户端回写数据
7.释放资源(Socket,ServerSocket)

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {
    public static void main(String[] args) throws IOException {
        // 1.创建服务器ServerSocket对象和系统要指定的端口号
        ServerSocket server = new ServerSocket(8888);
        // 2.使用ServerSocket对象中的方法accept,获取到请求的客户端对象Socket
        Socket socket = server.accept();
        // 3.使用Socket对象中的方法getInputStream()获取网络字节输入流InputStream对象
        InputStream is = socket.getInputStream();
        // 4.使用网络字节输入流InputStream对象中的方法read,读取客户端发送的数据
        byte[] bytes = new byte[1024];
        int len = is.read(bytes);
        System.out.println(new String(bytes,0,len));
        // 5.使用Socket对象中的方法getOutputStream()获取网络字节输出流OutputStream对象
        OutputStream os = socket.getOutputStream();
        // 6.使用网络字节输出流OutputStream对象中的方法write,给客户端回写数据
        os.write("收到谢谢".getBytes());
        // 7.释放资源(Socket,ServerSocket)
        socket.close();
        server.close();
    }
}

12.3 文件上传案例的客户端

文件上传案例的客户端:读取本地文件,上传到服务器,读取服务器回写的数据

明确:
数据源:c:\1.jpg
目的地:服务器

实现步骤:
1.创建一个本地字节输入流FileInputStream对象,构造方法中绑定要读取的数据源
2.创建一个客户端Socket对象,构造方法中绑定服务器的IP地址和端口号
3.使用Socket中的方法getOutputStream,获取网络字节输出流OutputStream对象
4.使用本地字节输入流FileInputStream对象中的方法read,读取本地文件
5.使用网络字节输出流OutputStream对象中的方法write,把读取到的文件上传到服务器
6.使用Socket中的方法getInputStream,获取网络字节输入流InputStream对象
7.使用网络字节输入流InputStream对象中的方法read读取服务回写的数据
8.释放资源(FileInputStream,Socket)

public class TCPClient {
    public static void main(String[] args) throws IOException {
        //1.创建一个本地字节输入流FileInputStream对象,构造方法中绑定要读取的数据源
        FileInputStream fis = new FileInputStream("c:\\1.jpg");
        //2.创建一个客户端Socket对象,构造方法中绑定服务器的IP地址和端口号
        Socket socket = new Socket("127.0.0.1",8888);
        //3.使用Socket中的方法getOutputStream,获取网络字节输出流OutputStream对象
        OutputStream os = socket.getOutputStream();
        //4.使用本地字节输入流FileInputStream对象中的方法read,读取本地文件
        int len = 0;
        byte[] bytes = new byte[1024];
        while((len = fis.read(bytes))!=-1){
            //5.使用网络字节输出流OutputStream对象中的方法write,把读取到的文件上传到服务器
            os.write(bytes,0,len);
        }

        /*
            解决:上传完文件,给服务器写一个结束标记
            void shutdownOutput() 禁用此套接字的输出流。
            对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列。
         */
        socket.shutdownOutput();

        //6.使用Socket中的方法getInputStream,获取网络字节输入流InputStream对象
        InputStream is = socket.getInputStream();

        System.out.println("333333333333333333333");

        //7.使用网络字节输入流InputStream对象中的方法read读取服务回写的数据
        while((len = is.read(bytes))!=-1){
            System.out.println(new String(bytes,0,len));
        }

        System.out.println("444444444444444444  while死循环打印不到");

        //8.释放资源(FileInputStream,Socket)
        fis.close();
        socket.close();
    }
}

12.4 文件上传案例服务器端

文件上传案例服务器端:读取客户端上传的文件,保存到服务器的硬盘,给客户端回写"上传成功"

明确:
数据源:客户端上传的文件
目的地:服务器的硬盘 d:\upload\1.jpg

实现步骤:
1.创建一个服务器ServerSocket对象,和系统要指定的端口号
2.使用ServerSocket对象中的方法accept,获取到请求的客户端Socket对象
3.使用Socket对象中的方法getInputStream,获取到网络字节输入流InputStream对象
4.判断d:\upload文件夹是否存在,不存在则创建
5.创建一个本地字节输出流FileOutputStream对象,构造方法中绑定要输出的目的地
6.使用网络字节输入流InputStream对象中的方法read,读取客户端上传的文件
7.使用本地字节输出流FileOutputStream对象中的方法write,把读取到的文件保存到服务器的硬盘上
8.使用Socket对象中的方法getOutputStream,获取到网络字节输出流OutputStream对象
9.使用网络字节输出流OutputStream对象中的方法write,给客户端回写"上传成功"
10.释放资源(FileOutputStream,Socket,ServerSocket)

public class TCPServer {
    public static void main(String[] args) throws IOException {
        //1.创建一个服务器ServerSocket对象,和系统要指定的端口号
        ServerSocket server = new ServerSocket(8888);
        //2.使用ServerSocket对象中的方法accept,获取到请求的客户端Socket对象
        Socket socket = server.accept();
        //3.使用Socket对象中的方法getInputStream,获取到网络字节输入流InputStream对象
        InputStream is = socket.getInputStream();
        //4.判断d:\\upload文件夹是否存在,不存在则创建
        File file =  new File("d:\\upload");
        if(!file.exists()){
            file.mkdirs();
        }


        //5.创建一个本地字节输出流FileOutputStream对象,构造方法中绑定要输出的目的地
        FileOutputStream fos = new FileOutputStream(file+"\\1.jpg");
        //6.使用网络字节输入流InputStream对象中的方法read,读取客户端上传的文件

        System.out.println("11111111111111111111");

        int len =0;
        byte[] bytes = new byte[1024];
        while((len = is.read(bytes))!=-1){
            //7.使用本地字节输出流FileOutputStream对象中的方法write,把读取到的文件保存到服务器的硬盘上
            fos.write(bytes,0,len);
        }

        System.out.println("22222222222222222222222  while死循环打印不到");

        //8.使用Socket对象中的方法getOutputStream,获取到网络字节输出流OutputStream对象
        //9.使用网络字节输出流OutputStream对象中的方法write,给客户端回写"上传成功"
        socket.getOutputStream().write("上传成功".getBytes());
        //10.释放资源(FileOutputStream,Socket,ServerSocket)
        fos.close();
        socket.close();
        server.close();
    }
}

12.5 文件上传案例的客户端优化

public class TCPClient {
    public static void main(String[] args) throws IOException {
        //1.创建一个本地字节输入流FileInputStream对象,构造方法中绑定要读取的数据源
        FileInputStream fis = new FileInputStream("c:\\1.jpg");
        //2.创建一个客户端Socket对象,构造方法中绑定服务器的IP地址和端口号
        Socket socket = new Socket("127.0.0.1",8888);
        //3.使用Socket中的方法getOutputStream,获取网络字节输出流OutputStream对象
        OutputStream os = socket.getOutputStream();
        //4.使用本地字节输入流FileInputStream对象中的方法read,读取本地文件
        int len = 0;
        byte[] bytes = new byte[1024];
        while((len = fis.read(bytes))!=-1){
            //5.使用网络字节输出流OutputStream对象中的方法write,把读取到的文件上传到服务器
            os.write(bytes,0,len);
        }

        /*
            解决:上传完文件,给服务器写一个结束标记
            void shutdownOutput() 禁用此套接字的输出流。
            对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列。
         */
        socket.shutdownOutput();

        //6.使用Socket中的方法getInputStream,获取网络字节输入流InputStream对象
        InputStream is = socket.getInputStream();

        //7.使用网络字节输入流InputStream对象中的方法read读取服务回写的数据
        while((len = is.read(bytes))!=-1){
            System.out.println(new String(bytes,0,len));
        }

        //8.释放资源(FileInputStream,Socket)
        fis.close();
        socket.close();
    }
}

12.6 文件上传案例服务器端优化

public class TCPServer {
    public static void main(String[] args) throws IOException {
        //1.创建一个服务器ServerSocket对象,和系统要指定的端口号
        ServerSocket server = new ServerSocket(8888);
        //2.使用ServerSocket对象中的方法accept,获取到请求的客户端Socket对象

        /*
            让服务器一直处于监听状态(死循环accept方法)
            有一个客户端上传文件,就保存一个文件
         */
        while(true){
            Socket socket = server.accept();

            /*
                使用多线程技术,提高程序的效率
                有一个客户端上传文件,就开启一个线程,完成文件的上传
             */
            new Thread(new Runnable() {
                //完成文件的上传
                @Override
                public void run() {
                   try {
                  //3.使用Socket对象中的方法getInputStream,获取到网络字节输入流InputStream对象
                       InputStream is = socket.getInputStream();
                       //4.判断d:\\upload文件夹是否存在,不存在则创建
                       File file =  new File("d:\\upload");
                       if(!file.exists()){
                           file.mkdirs();
                       }

                    /*
                        自定义一个文件的命名规则:防止同名的文件被覆盖
                        规则:域名+毫秒值+随机数
                     */
                       String fileName = "itcast"+System.currentTimeMillis()+new Random().nextInt(999999)+".jpg";

                     //5.创建一个本地字节输出流FileOutputStream对象,构造方法中绑定要输出的目的地
                       //FileOutputStream fos = new FileOutputStream(file+"\\1.jpg");
                       FileOutputStream fos = new FileOutputStream(file+"\\"+fileName);
                       //6.使用网络字节输入流InputStream对象中的方法read,读取客户端上传的文件


                       int len =0;
                       byte[] bytes = new byte[1024];
                       while((len = is.read(bytes))!=-1){
        //7.使用本地字节输出流FileOutputStream对象中的方法write,把读取到的文件保存到服务器的硬盘上
                           fos.write(bytes,0,len);
                       }

                //8.使用Socket对象中的方法getOutputStream,获取到网络字节输出流OutputStream对象
                //9.使用网络字节输出流OutputStream对象中的方法write,给客户端回写"上传成功"
                       socket.getOutputStream().write("上传成功".getBytes());
                       //10.释放资源(FileOutputStream,Socket,ServerSocket)
                       fos.close();
                       socket.close();
                   }catch (IOException e){
                       System.out.println(e);
                   }
                }
            }).start();
        }

        //服务器就不用关闭
        //server.close();
    }
}

12.7 BS版本TCP服务器

public class TCPServer {
    public static void main(String[] args) throws IOException {
        //创建一个服务器ServerSocket,和系统要指定的端口号
        ServerSocket server = new ServerSocket(8080);
        //使用accept方法获取到请求的客户端对象(浏览器)
        Socket socket = server.accept();
        //使用Socket对象中的方法getInputStream,获取到网络字节输入流InputStream对象
        InputStream is = socket.getInputStream();
        //使用网络字节输入流InputStream对象中的方法read读取客户端的请求信息
        /*byte[] bytes = new byte[1024];
        int len = 0;
        while((len = is.read(bytes))!=-1){
            System.out.println(new String(bytes,0,len));
        }*/

        //把is网络字节输入流对象,转换为字符缓冲输入流
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        //把客户端请求信息的第一行读取出来 GET /11_Net/web/index.html HTTP/1.1
        String line = br.readLine();
        //把读取的信息进行切割,只要中间部分 /11_Net/web/index.html
        String[] arr = line.split(" ");
        //把路径前边的/去掉,进行截取 11_Net/web/index.html
        String htmlpath = arr[1].substring(1);

        //创建一个本地字节输入流,构造方法中绑定要读取的html路径
        FileInputStream fis = new FileInputStream(htmlpath);
        //使用Socket中的方法getOutputStream获取网络字节输出流OutputStream对象
        OutputStream os = socket.getOutputStream();

        // 写入HTTP协议响应头,固定写法
        os.write("HTTP/1.1 200 OK\r\n".getBytes());
        os.write("Content-Type:text/html\r\n".getBytes());
        // 必须要写入空行,否则浏览器不解析
        os.write("\r\n".getBytes());

        //一读一写复制文件,把服务读取的html文件回写到客户端
        int len = 0;
        byte[] bytes = new byte[1024];
        while((len = fis.read(bytes))!=-1){
            os.write(bytes,0,len);
        }

        //释放资源
        fis.close();
        socket.close();
        server.close();
    }
}
public class TCPServerThread {
    public static void main(String[] args) throws IOException {
        //创建一个服务器ServerSocket,和系统要指定的端口号
        ServerSocket server = new ServerSocket(8080);

        /*
            浏览器解析服务器回写的html页面,页面中如果有图片,那么浏览器就会单独的开启一个线程,读取服务器的图片
            我们就的让服务器一直处于监听状态,客户端请求一次,服务器就回写一次
         */
        while(true){
            //使用accept方法获取到请求的客户端对象(浏览器)
            Socket socket = server.accept();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //使用Socket对象中的方法getInputStream,获取到网络字节输入流InputStream对象
                        InputStream is = socket.getInputStream();
                        //使用网络字节输入流InputStream对象中的方法read读取客户端的请求信息
                        /*byte[] bytes = new byte[1024];
                        int len = 0;
                        while((len = is.read(bytes))!=-1){
                            System.out.println(new String(bytes,0,len));
                        }*/

                        //把is网络字节输入流对象,转换为字符缓冲输入流
                        BufferedReader br = new BufferedReader(new InputStreamReader(is));
                        //把客户端请求信息的第一行读取出来 GET /11_Net/web/index.html HTTP/1.1
                        String line = br.readLine();
                        System.out.println(line);
                        //把读取的信息进行切割,只要中间部分 /11_Net/web/index.html
                        String[] arr = line.split(" ");
                        //把路径前边的/去掉,进行截取 11_Net/web/index.html
                        String htmlpath = arr[1].substring(1);

                        //创建一个本地字节输入流,构造方法中绑定要读取的html路径
                        FileInputStream fis = new FileInputStream(htmlpath);
                        //使用Socket中的方法getOutputStream获取网络字节输出流OutputStream对象
                        OutputStream os = socket.getOutputStream();

                        // 写入HTTP协议响应头,固定写法
                        os.write("HTTP/1.1 200 OK\r\n".getBytes());
                        os.write("Content-Type:text/html\r\n".getBytes());
                        // 必须要写入空行,否则浏览器不解析
                        os.write("\r\n".getBytes());

                        //一读一写复制文件,把服务读取的html文件回写到客户端
                        int len = 0;
                        byte[] bytes = new byte[1024];
                        while((len = fis.read(bytes))!=-1){
                            os.write(bytes,0,len);
                        }

                        //释放资源
                        fis.close();
                        socket.close();
                    }catch (IOException e){
                        e.printStackTrace();
                    }
                }
            }).start();


        }

        //server.close();
    }
}

13.函数式接口

函数式接口:有且只有一个抽象方法的接口,称之为函数式接口
当然接口中可以包含其他的方法(默认,静态,私有),只不过抽象方法一定只能有一个

@FunctionalInterface注解
作用:可以检测接口是否是一个函数式接口
是:编译成功
否:编译失败(接口中没有抽象方法抽象方法的个数多余1个)

定义一个函数式接口

@FunctionalInterface
public interface MyFunctionalInterface {
    // 定义一个抽象方法(这个接口中只有一个抽象方法)
    public abstract void method();
}

编写一个实现类去实现定义好的接口

public class MyFunctionalInterfaceImpl implements MyFunctionalInterface{
    @Override
    public void method() {
		System.out.println("实现类重写的接口方法执行了");
    }
}

函数式接口的使用:一般可以作为方法的参数和返回值类型

public class Demo {
    // 定义一个方法,参数使用函数式接口MyFunctionalInterface
    public static void show(MyFunctionalInterface myInter){
        myInter.method();
    }

    public static void main(String[] args) {
        // 调用show方法,方法的参数是一个接口,所以可以传递接口的实现类对象
        show(new MyFunctionalInterfaceImpl());

        // 调用show方法,方法的参数是一个接口,所以我们可以传递接口的匿名内部类
        show(new MyFunctionalInterface() {
            @Override
            public void method() {
                System.out.println("使用匿名内部类重写接口中的抽象方法");
            }
        });

        // 调用show方法,方法的参数是一个函数式接口,所以我们可以Lambda表达式
        show(()->{
            System.out.println("使用Lambda表达式重写接口中的抽象方法");
        });

        // 简化Lambda表达式
        show(()-> System.out.println("使用Lambda表达式重写接口中的抽象方法"));
    }
}

// 执行的结果
实现类重写的接口方法执行了
匿名内部类重写接口中的方法
使用Lambda表达式重写接口中的抽象方法
使用简化Lambda表达式重写接口中的抽象方法

13.1日志案例

日志案例

发现以下代码存在的一些性能浪费的问题
调用showLog方法,传递的第二个参数是一个拼接后的字符串
先把字符串拼接好,然后在调用showLog方法
showLog方法中如果传递的日志等级不是1级
那么就不会是如此拼接后的字符串
所以感觉字符串就白拼接了,存在了浪费

public class Demo01Logger {
    // 定义一个根据日志的级别,显示日志信息的方法
    public static void showLog(int level, String message){
        // 对日志的等级进行判断,如果是1级别,那么输出日志信息
        if(level==1){
            System.out.println(message);
        }
    }

    public static void main(String[] args) {
        // 定义三个日志信息
        String msg1 = "Hello";
        String msg2 = "World";
        String msg3 = "Java";

        // 调用showLog方法,传递日志级别和日志信息
        // 在调用showLog()方法的时候,会首先拼接号字符串,然后再判断日志的等级
        // 这样的话,即使日志的等级不是1,那么字符串也会进行拼接,这样的话就会造成性能的浪费
        showLog(2,msg1+msg2+msg3);
    }
}

13.1.1改进日志的方式

定义一个拼接消息的抽象方法,返回被拼接的消息

@FunctionalInterface  // 检查接口中是不是只有一个抽象方法
public interface MessageBuilder {
    //定义一个拼接消息的抽象方法,返回被拼接的消息
    public abstract String builderMessage();
}

使用Lambda优化日志

Lambda的特点:延迟加载
Lambda的使用前提,必须存在函数式接口(也就是说只能有一个抽象方法)

public class Demo02Lambda {
    // 定义一个显示日志的方法,方法的参数传递日志的等级和MessageBuilder接口
    public static void showLog(int level, MessageBuilder mb){
        // 对日志的等级进行判断,如果是1级,则调用MessageBuilder接口中的builderMessage方法
        if(level==1){
            System.out.println(mb.builderMessage());
        }
    }

    public static void main(String[] args) {
        // 定义三个日志信息
        String msg1 = "Hello";
        String msg2 = "World";
        String msg3 = "Java";

        // 调用showLog方法,参数MessageBuilder是一个函数式接口,所以可以传递Lambda表达式
        /*showLog(2,()->{
            // 返回一个拼接好的字符串
            return  msg1+msg2+msg3;
        });*/

        /*
            使用Lambda表达式作为参数传递,仅仅是把参数传递到showLog方法中
            只有满足条件,日志的等级是1级
                才会调用接口MessageBuilder中的方法builderMessage
                才会进行字符串的拼接
            如果条件不满足,日志的等级不是1级
                那么MessageBuilder接口中的方法builderMessage也不会执行
                所以拼接字符串的代码也不会执行
            所以不会存在性能的浪费
         */
        showLog(1,()->{
            // 下面的 Lambda表达式 算是重写了 抽象方法 builderMessage()
            System.out.println("日志的等级是1,拼接字符串的功能执行了");
            // 返回一个拼接好的字符串
            return  msg1+msg2+msg3;
        });
    }
}

// 打印的结果
日志的等级是1,拼接字符串的功能执行了
HelloWorldJava

13.2 使用Lambda作为参数和返回值

作为参数

例如java.lang.Runnable接口就是一个函数式接口,
假设有一个startThread方法使用该接口作为参数,那么就可以使用Lambda进行传参。
这种情况其实和Thread类的构造方法参数为Runnable没有本质区别。

public class Demo01Runnable {
    // 定义一个方法startThread,方法的参数使用函数式接口Runnable
    public static void startThread(Runnable run){
        // 开启多线程
        new Thread(run).start();
    }

    public static void main(String[] args) {
        // 调用startThread方法,方法的参数是一个接口,那么我们可以传递这个接口的匿名内部类
        startThread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"-->"+"线程启动了");
            }
        });

        // 调用startThread方法,方法的参数是一个函数式接口,所以可以传递Lambda表达式
        startThread(()->{
            System.out.println(Thread.currentThread().getName()+"-->"+"线程启动了");
        });

        // 优化Lambda表达式
        startThread(()->System.out.println(Thread.currentThread().getName()+"-->"+"线程启动了"));
    }
}

// 打印的结果
Thread-2-->线程启动了
Thread-0-->线程启动了
Thread-1-->线程启动了

作为返回值

如果一个方法的返回值类型是一个函数式接口,那么就可以直接返回一个Lambda表达式。
当需要通过一个方法来获取一个java.util.Comparator接口类型的对象作为排序器时,就可以调该方法获取。

public class Demo02Comparator {
    // 定义一个方法,方法的返回值类型使用函数式接口Comparator
    public static Comparator<String> getComparator(){
        // 方法的返回值类型是一个接口,那么我们可以返回这个接口的匿名内部类
        /*return new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                //按照字符串的降序排序
                return o2.length()-o1.length();
            }
        };*/

        // 方法的返回值类型是一个函数式接口,所有我们可以返回一个Lambda表达式
        /*return (String o1, String o2)->{
            //按照字符串的降序排序
            return o2.length()-o1.length();
        };*/

        // 继续优化Lambda表达式
        return (o1, o2)->o2.length()-o1.length();
    }

    public static void main(String[] args) {
        // 创建一个字符串数组
        String[] arr = {"aaa","b","cccccc","dddddddddddd"};
        // 输出排序前的数组
        System.out.println(Arrays.toString(arr));  // [aaa, b, cccccc, dddddddddddd]
        // 调用Arrays中的sort方法,对字符串数组进行排序
        Arrays.sort(arr,getComparator());
        // 输出排序后的数组
        System.out.println(Arrays.toString(arr));  // [dddddddddddd, cccccc, aaa, b]
    }
}

13.3 生产型接口Supplier

常用的函数式接口
java.util.function.Supplier接口仅包含一个无参的方法:T get()。用来获取一个泛型参数指定类型的对象数据。
Supplier接口被称之为生产型接口,指定接口的泛型是什么类型,那么接口中的get方法就会生产什么类型的数据

public class Demo01Supplier {
    // 定义一个方法,方法的参数传递Supplier<T>接口,泛型执行String,get方法就会返回一个String
    public static String getString(Supplier<String> sup){
        return sup.get();
    }

    public static void main(String[] args) {
        // 接口当做参数放入函数中,在函数内部实现了对接口方法的实现,函数获取被实现的方法的返回值 
        String string = getString(new Supplier<String>() {
            @Override
            public String get() {
                return "胡歌";
            }
        });
        
        // 调用getString方法,方法的参数Supplier是一个函数式接口,所以可以传递Lambda表达式
        // 传入的Lambda表达式就是get()方法要获取的内容
        String s = getString(()->{
            //生产一个字符串,并返回
            return "胡歌";
        });
        System.out.println(s);

        // 优化Lambda表达式
        String s2 = getString(()->"胡歌");
        System.out.println(s2);
    }
}

// 执行的结果
胡歌
胡歌

13.3.1 求数组元素最大值

使用Supplier接口作为方法参数类型,通过Lambda表达式求出int数组中的最大值。
提示:接口的泛型请使用java.lang.Integer类。

public class Demo02Test {
   // 定义一个方法,用于获取int类型数组中元素的最大值,方法的参数传递Supplier接口,泛型使用Integer
   public static int getMax(Supplier<Integer> sup){
       return sup.get();
   }

    public static void main(String[] args) {
        // 定义一个int类型的数组,并赋值
        int[] arr = {100,0,-50,880,99,33,-30};
        // 调用getMax方法,方法的参数Supplier是一个函数式接口,所以可以传递Lambda表达式
        int maxValue = getMax(()->{
            // 获取数组的最大值,并返回
            // 定义一个变量,把数组中的第一个元素赋值给该变量,记录数组中元素的最大值
            int max = arr[0];
            // 遍历数组,获取数组中的其他元素
            for (int i : arr) {
                //使用其他的元素和最大值比较
                if(i>max){
                    // 如果i大于max,则替换max作为最大值
                    max = i;
                }
            }
            // 返回最大值
            return max;
        });
        System.out.println("数组中元素的最大值是:"+maxValue);  // 数组中最大的值是:880
    }
}

13.4 消费型接口Consumer

java.util.function.Consumer接口则正好与Supplier接口相反,
它不是生产一个数据,而是消费一个数据,其数据类型由泛型决定。
Consumer接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据。

Consumer接口是一个消费型接口,泛型执行什么类型,就可以使用accept方法消费什么类型的数据
至于具体怎么消费(使用),需要自定义(输出,计算…)

public class Demo01Consumer {
    /*
        定义一个方法
        方法的参数传递一个字符串的姓名
        方法的参数传递Consumer接口,泛型使用String
        可以使用Consumer接口消费字符串的姓名
     */
    public static void method(String name, Consumer<String> con){
        con.accept(name);
    }

    public static void main(String[] args) {
        // 调用method方法,传递字符串姓名,方法的另一个参数是Consumer接口,是一个函数式接口,所以可以传递Lambda表达式
        // 传递的Lamdba表达式是accept()的内容
        method("赵丽颖",(String name)->{
            // 对传递的字符串进行消费
            // 消费方式:直接输出字符串
            // System.out.println(name);

            // 消费方式:把字符串进行反转输出
            String reName = new StringBuffer(name).reverse().toString();
            System.out.println(reName);
        });
    }
}

13.4.1 Consumer接口的默认方法andThen

作用:需要两个Consumer接口,可以把两个Consumer接口组合到一起,在对数据进行消费

例如:
Consumer con1
Consumer con2
String s = “hello”;
con1.accept(s);
con2.accept(s);
连接两个Consumer接口 再进行消费
con1.andThen(con2).accept(s); 谁写前边谁先消费

import java.util.function.Consumer;

public class Demo02AndThen {
    // 定义一个方法,方法的参数传递一个字符串和两个Consumer接口,Consumer接口的泛型使用字符串
    public static void method(String s, Consumer<String> con1 ,Consumer<String> con2){
        //con1.accept(s);
        //con2.accept(s);
        // 使用andThen方法,把两个Consumer接口连接到一起,在消费数据
        con1.andThen(con2).accept(s);//con1连接con2,先执行con1消费数据,在执行con2消费数据
    }

    public static void main(String[] args) {
        // 调用method方法,传递一个字符串,两个Lambda表达式
        method("Hello",
                (t)->{
                    // 消费方式:把字符串转换为大写输出
                    System.out.println(t.toUpperCase());  // HELLO
                },
                (t)->{
                    // 消费方式:把字符串转换为小写输出
                    System.out.println(t.toLowerCase());  // hello
                });
    }
}

13.4.2 案例练习

字符串数组当中存有多条信息,请按照格式“姓名:XX。性别:XX。”的格式将信息打印出来。
要求将打印姓名的动作作为第一个Consumer接口的Lambda实例,
将打印性别的动作作为第二个Consumer接口的Lambda实例,
将两个Consumer接口按照顺序“拼接”到一起。

public class Demo03Test {
    // 定义一个方法,参数传递String类型的数组和两个Consumer接口,泛型使用String
    public static void printInfo(String[] arr, Consumer<String> con1,Consumer<String> con2){
        // 遍历字符串数组
        for (String message : arr) {
            // 使用andThen方法连接两个Consumer接口,消费字符串
            con1.andThen(con2).accept(message);
        }
    } 

    public static void main(String[] args) {
        // 定义一个字符串类型的数组
        String[] arr = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男" };

        // 调用printInfo方法,传递一个字符串数组,和两个Lambda表达式
        printInfo(arr,(message)->{
            // 消费方式:对message进行切割,获取姓名,按照指定的格式输出
            String name = message.split(",")[0];
            System.out.print("姓名: "+name);
        },(message)->{
            // 消费方式:对message进行切割,获取年龄,按照指定的格式输出
            String age = message.split(",")[1];
            System.out.println("。年龄: "+age+"。");
        });
    }
}

// 程序执行的结果
姓名迪丽热巴
年龄女
姓名古力娜扎
年龄女
姓名马尔扎哈
年龄男

13.5数据类型判断型接口Predicate

java.util.function.Predicate接口
作用:对某种数据类型的数据进行判断,结果返回一个boolean值

Predicate接口中包含一个抽象方法:
boolean test(T t):用来对指定数据类型数据进行判断的方法
结果:
符合条件,返回true
不符合条件,返回false

public class Demo01Predicate {
    /*
        定义一个方法
        参数传递一个String类型的字符串
        传递一个Predicate接口,泛型使用String
        使用Predicate中的方法test对字符串进行判断,并把判断的结果返回
    */
    public static boolean checkString(String s, Predicate<String> pre){
        return  pre.test(s);
    }

    public static void main(String[] args) {
        // 定义一个字符串
        String s = "abcdef";

        // 调用checkString方法对字符串进行校验,参数传递字符串和Lambda表达式
        /*boolean b = checkString(s,(String str)->{
            //对参数传递的字符串进行判断,判断字符串的长度是否大于5,并把判断的结果返回
            return str.length()>5;
        });*/

        // 优化Lambda表达式
        boolean b = checkString(s,str->str.length()>5);
        System.out.println(b);  // true
    }
}

13.5.1 Predicate中的逻辑表达式

逻辑表达式:可以连接多个判断的条件
&&:与运算符,有false则false
||:或运算符,有true则true
!:非(取反)运算符,非真则假,非假则真

需求:判断一个字符串,有两个判断的条件
1.判断字符串的长度是否大于5
2.判断字符串中是否包含a
两个条件必须同时满足,我们就可以使用&&运算符连接两个条件

Predicate接口中有一个方法and,表示并且关系,也可以用于连接两个判断条件

default Predicate<T> and(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> this.test(t) && other.test(t);
}

方法内部的两个判断条件,也是使用&&运算符连接起来的

and

public class Demo02Predicate_and {
    /*
        定义一个方法,方法的参数,传递一个字符串
        传递两个Predicate接口
            一个用于判断字符串的长度是否大于5
            一个用于判断字符串中是否包含a
            两个条件必须同时满足
    */
    public static boolean checkString(String s, Predicate<String> pre1,Predicate<String> pre2){
        // return pre1.test(s) && pre2.test(s);
        return pre1.and(pre2).test(s);//等价于return pre1.test(s) && pre2.test(s);
    }

    public static void main(String[] args) {
        // 定义一个字符串
        String s = "abcdef";
        // 调用checkString方法,参数传递字符串和两个Lambda表达式
        boolean b = checkString(s,(String str)->{
            // 判断字符串的长度是否大于5
            return str.length()>5;
        },(String str)->{
            // 判断字符串中是否包含a
            return str.contains("a");
        });
        System.out.println(b);
    }
}

or

需求:判断一个字符串,有两个判断的条件
1.判断字符串的长度是否大于5
2.判断字符串中是否包含a
满足一个条件即可,我们就可以使用||运算符连接两个条件

Predicate接口中有一个方法or,表示或者关系,也可以用于连接两个判断条件

default Predicate<T> or(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> test(t) || other.test(t);
}

方法内部的两个判断条件,也是使用||运算符连接起来的

import java.util.function.Predicate;

public class Demo03Predicate_or {
    /*
            定义一个方法,方法的参数,传递一个字符串
            传递两个Predicate接口
                一个用于判断字符串的长度是否大于5
                一个用于判断字符串中是否包含a
                满足一个条件即可
         */
    public static boolean checkString(String s, Predicate<String> pre1, Predicate<String> pre2){
        // return pre1.test(s) || pre2.test(s);
        return  pre1.or(pre2).test(s);//等价于return pre1.test(s) || pre2.test(s);
    }

    public static void main(String[] args) {
        // 定义一个字符串
        String s = "bc";
        // 调用checkString方法,参数传递字符串和两个Lambda表达式
        boolean b = checkString(s,(String str)->{
            // 判断字符串的长度是否大于5
            return str.length()>5;
        },(String str)->{
            // 判断字符串中是否包含a
            return str.contains("a");
        });
        System.out.println(b);
    }
}

negate

需求:判断一个字符串长度是否大于5
如果字符串的长度大于5,那返回false
如果字符串的长度不大于5,那么返回true
所以我们可以使用取反符号!对判断的结果进行取反

Predicate接口中有一个方法negate,也表示取反的意思

default Predicate<T> negate() {
   return (t) -> !test(t);
}
public class Demo04Predicate_negate {
    /*
           定义一个方法,方法的参数,传递一个字符串
           使用Predicate接口判断字符串的长度是否大于5
    */
    public static boolean checkString(String s, Predicate<String> pre){
        //return !pre.test(s);
        return  pre.negate().test(s);  // 等效于return !pre.test(s);
    }

    public static void main(String[] args) {
        // 定义一个字符串
        String s = "abc";
        // 调用checkString方法,参数传递字符串和Lambda表达式
        boolean b = checkString(s,(String str)->{
            //判断字符串的长度是否大于5,并返回结果
            return str.length()>5;
        });
        System.out.println(b);
    }
}

13.5.2 集合信息筛选

练习:集合信息筛选
数组当中有多条“姓名+性别”的信息如下,
String[] array = { “迪丽热巴,女”, “古力娜扎,女”, “马尔扎哈,男”, “赵丽颖,女” };
请通过Predicate接口的拼装将符合要求的字符串筛选到集合ArrayList中,

需要同时满足两个条件:

  1. 必须为女生;
  2. 姓名为4个字。

分析:
1.有两个判断条件,所以需要使用两个Predicate接口,对条件进行判断
2.必须同时满足两个条件,所以可以使用and方法连接两个判断条件

public class Demo05Test {
    /*
        定义一个方法
        方法的参数传递一个包含人员信息的数组
        传递两个Predicate接口,用于对数组中的信息进行过滤
        把满足条件的信息存到ArrayList集合中并返回
    */
    public static ArrayList<String> filter(String[] arr,Predicate<String> pre1,Predicate<String> pre2){
        //定义一个ArrayList集合,存储过滤之后的信息
        ArrayList<String> list = new ArrayList<>();
        //遍历数组,获取数组中的每一条信息
        for (String s : arr) {
            //使用Predicate接口中的方法test对获取到的字符串进行判断
            boolean b = pre1.and(pre2).test(s);
            //对得到的布尔值进行判断
            if(b){
                //条件成立,两个条件都满足,把信息存储到ArrayList集合中
                list.add(s);
            }
        }
        //把集合返回
        return list;
    }

    public static void main(String[] args) {
        //定义一个储存字符串的数组
        String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男", "赵丽颖,女" };
        //调用filter方法,传递字符串数组和两个Lambda表达式
        ArrayList<String> list = filter(array,(String s)->{
            //获取字符串中的性别,判断是否为女
           return s.split(",")[1].equals("女");
        },(String s)->{
            //获取字符串中的姓名,判断长度是否为4个字符
           return s.split(",")[0].length()==4;
        });
        //遍历集合
        for (String s : list) {
            System.out.println(s);
        }
    }
}

// 执行的结果
迪丽热巴,女
古力娜扎,

13.6 Function根据类型T的参数获取类型R的结果

java.util.function.Function<T,R>接口用来根据一个类型的数据得到另一个类型的数据,
前者称为前置条件,后者称为后置条件。
Function接口中最主要的抽象方法为:R apply(T t),根据类型T的参数获取类型R的结果。
使用的场景例如:将String类型转换为Integer类型。

public class Demo01Function {
    /*
        定义一个方法
        方法的参数传递一个字符串类型的整数
        方法的参数传递一个Function接口,泛型使用<String,Integer>
        使用Function接口中的方法apply,把字符串类型的整数,转换为Integer类型的整数
   */
    public static void change(String s, Function<String,Integer> fun){
        // Integer in = fun.apply(s);
        int in = fun.apply(s);//自动拆箱 Integer->int
        System.out.println(in);
    }

    public static void main(String[] args) {
        // 定义一个字符串类型的整数
        String s = "1234";
        // 调用change方法,传递字符串类型的整数,和Lambda表达式
        change(s,(String str)->{
            // 把字符串类型的整数,转换为Integer类型的整数返回
            return Integer.parseInt(str);
        });
        // 优化Lambda
        change(s,str->Integer.parseInt(str));
    }
}

13.6.1 Function接口中的默认方法andThen

需求:
把String类型的"123",转换为Inteter类型,把转换后的结果加10
把增加之后的Integer类型的数据,转换为String类型

分析:
转换了两次
第一次是把String类型转换为了Integer类型
所以我们可以使用Function<String,Integer> fun1
Integer i = fun1.apply(“123”)+10;
第二次是把Integer类型转换为String类型
所以我们可以使用Function<Integer,String> fun2
String s = fun2.apply(i);
我们可以使用andThen方法,把两次转换组合在一起使用
String s = fun1.andThen(fun2).apply(“123”);
fun1先调用apply方法,把字符串转换为Integer
fun2再调用apply方法,把Integer转换为字符串

public class Demo02Function_andThen {
    /*
        定义一个方法
        参数串一个字符串类型的整数
        参数再传递两个Function接口
            一个泛型使用Function<String,Integer>
            一个泛型使用Function<Integer,String>
   */
    public static void change(String s, Function<String,Integer> fun1,Function<Integer,String> fun2){
        String ss = fun1.andThen(fun2).apply(s);
        System.out.println(ss);
    }

    public static void main(String[] args) {
        // 定义一个字符串类型的整数
        String s = "123";
        // 调用change方法,传递字符串和两个Lambda表达式
        change(s,(String str)->{
            // 把字符串转换为整数+10
            return Integer.parseInt(str)+10;
        },(Integer i)->{
            // 把整数转换为字符串,整数和字符串相加结果是 整数
            return i+"";
        });

        // 优化Lambda表达式
        change(s,str->Integer.parseInt(str)+10,i->i+"");
    }
}

13.6.2 自定义函数模型拼接

请使用Function进行函数模型的拼接,按照顺序需要执行的多个函数操作为:
String str = “赵丽颖,20”;

分析:
1. 将字符串截取数字年龄部分,得到字符串;
Function<String,String> “赵丽颖,20”->“20”
2. 将上一步的字符串转换成为int类型的数字;
Function<String,Integer> “20”->20
3. 将上一步的int数字累加100,得到结果int数字。
Function<Integer,Integer> 20->120

public class Demo03Test {
    /*
        定义一个方法
        参数传递包含姓名和年龄的字符串
        参数再传递3个Function接口用于类型转换
    */
    public static int change(String s, Function<String,String> fun1,
                             Function<String,Integer> fun2,Function<Integer,Integer> fun3){
        //使用andThen方法把三个转换组合到一起
        return fun1.andThen(fun2).andThen(fun3).apply(s);
    }

    public static void main(String[] args) {
        //定义一个字符串
        String str = "赵丽颖,20";
        //调用change方法,参数传递字符串和3个Lambda表达式
        int num = change(str,(String s)->{
            //"赵丽颖,20"->"20"
           return s.split(",")[1];
        },(String s)->{
            //"20"->20
            return Integer.parseInt(s);
        },(Integer i)->{
            //20->120
            return i+100;
        });
        System.out.println(num);
    }
}

// 程序执行的结果
120

14.流接口srteam

使用传统的方式,遍历集合,对集合中的数据进行过滤

public class Demo01List {
    public static void main(String[] args) {
        //创建一个List集合,存储姓名
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张强");
        list.add("张三丰");

        //对list集合中的元素进行过滤,只要以张开头的元素,存储到一个新的集合中
        List<String> listA = new ArrayList<>();
        for(String s : list){
            if(s.startsWith("张")){
                listA.add(s);
            }
        }

        //对listA集合进行过滤,只要姓名长度为3的人,存储到一个新集合中
        List<String> listB = new ArrayList<>();
        for (String s : listA) {
            if(s.length()==3){
                listB.add(s);
            }
        }

        //遍历listB集合
        for (String s : listB) {
            System.out.println(s);
        }
    }
}

使用Stream流的方式,遍历集合,对集合中的数据进行过滤
Stream流是JDK1.8之后出现的
关注的是做什么,而不是怎么做

public class Demo02Stream {
    public static void main(String[] args) {
        //创建一个List集合,存储姓名
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张强");
        list.add("张三丰");

        //对list集合中的元素进行过滤,只要以张开头的元素,存储到一个新的集合中
        //对listA集合进行过滤,只要姓名长度为3的人,存储到一个新集合中
        //遍历listB集合
        list.stream()
            .filter(name->name.startsWith("张"))
            .filter(name->name.length()==3)
            .forEach(name-> System.out.println(name));
	}
}

14.1 获取 stream 流接口的方式

java.util.stream.Stream是Java 8新加入的最常用的流接口。(这并不是一个函数式接口。)
获取一个流非常简单,有以下几种常用的方式:

  • 所有的Collection集合都可以通过stream默认方法获取流;
    default Stream stream()
  • Stream接口的静态方法of可以获取数组对应的流。
    static Stream of(T… values)
    参数是一个可变参数,那么我们就可以传递一个数组
public class Demo01GetStream {
    public static void main(String[] args) {
        // 把集合转换为Stream流
        List<String> list = new ArrayList<>();
        Stream<String> stream1 = list.stream();

        Set<String> set = new HashSet<>();
        Stream<String> stream2 = set.stream();

        Map<String,String> map = new HashMap<>();
        // 获取键,存储到一个Set集合中
        Set<String> keySet = map.keySet();
        Stream<String> stream3 = keySet.stream();

        // 获取值,存储到一个Collection集合中
        Collection<String> values = map.values();
        Stream<String> stream4 = values.stream();

        // 获取键值对(键与值的映射关系 entrySet)
        Set<Map.Entry<String, String>> entries = map.entrySet();
        Stream<Map.Entry<String, String>> stream5 = entries.stream();

        // 把数组转换为Stream流
        Stream<Integer> stream6 = Stream.of(1, 2, 3, 4, 5);
        // 可变参数可以传递数组
        Integer[] arr = {1,2,3,4,5};
        Stream<Integer> stream7 = Stream.of(arr);
        String[] arr2 = {"a","bb","ccc"};
        Stream<String> stream8 = Stream.of(arr2);
    }
}

14.2 常用方法_forEach

终结方法

void forEach(Consumer<? super T> action);
该方法接收一个Consumer接口函数,会将每一个流元素交给该函数进行处理。
Consumer接口是一个消费型的函数式接口,可以传递Lambda表达式,消费数据

简单记:
forEach方法,用来遍历流中的数据
是一个终结方法,遍历之后就不能继续调用Stream流中的其他方法

public class Demo02Stream_forEach {
    public static void main(String[] args) {
        // 获取一个Stream流
        Stream<String> stream = Stream.of("张三", "李四", "王五", "赵六", "田七");
        // 使用Stream流中的方法forEach对Stream流中的数据进行遍历
        /*stream.forEach((String name)->{
            System.out.println(name);
        });*/

        stream.forEach(name->System.out.println(name));
    }
}

14.3 常用方法_filter

需要Return

能够连续的进行过滤

Stream流中的常用方法_filter:用于对Stream流中的数据进行过滤
Stream filter(Predicate<? super T> predicate);
filter方法的参数Predicate是一个函数式接口,所以可以传递Lambda表达式,对数据进行过滤
Predicate中的抽象方法:
boolean test(T t);

public class Demo03Stream_filter {
    public static void main(String[] args) {
        //创建一个Stream流
        Stream<String> stream = Stream.of("张三丰", "张翠山", "赵敏", "周芷若", "张无忌");
        //对Stream流中的元素进行过滤,只要姓张的人
        Stream<String> stream2 = stream.filter((String name)->{return name.startsWith("张");});
        //遍历stream2流
        stream2.forEach(name-> System.out.println(name));

        /*
            Stream流属于管道流,只能被消费(使用)一次
            第一个Stream流调用完毕方法,数据就会流转到下一个Stream上
            而这时第一个Stream流已经使用完毕,就会关闭了
            所以第一个Stream流就不能再调用方法了
            IllegalStateException: stream has already been operated upon or closed
         */
        //遍历stream流
        stream.forEach(name-> System.out.println(name));  // 也就是说这段代码会引发异常
    }
}

14.4 常用方法_map

需要Return

Stream流中的常用方法_map: 用于类型转换
如果需要将流中的元素映射到另一个流中,可以使用map方法.
Stream map(Function<? super T, ? extends R> mapper);
该接口需要一个Function函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。
Function中的抽象方法:
R apply(T t);

public class Demo04Stream_map {
    public static void main(String[] args) {
        // 获取一个String类型的Stream流
        Stream<String> stream = Stream.of("1", "2", "3", "4");
        // 使用map方法,把字符串类型的整数,转换(映射)为Integer类型的整数
        Stream<Integer> stream2 = stream.map((String s)->{
            return Integer.parseInt(s);
        });
        // 遍历Stream2流
        stream2.forEach(i-> System.out.println(i+"你"));
    }
}

// 程序执行的结果
1234

14.5 常用方法_count

Stream流中的常用方法_count:用于统计Stream流中元素的个数
long count();
count方法是一个终结方法,返回值是一个long类型的整数
所以不能再继续调用Stream流中的其他方法了

public class Demo05Stream_count {
    public static void main(String[] args) {
        //获取一个Stream流
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        list.add(6);
        list.add(7);
        Stream<Integer> stream = list.stream();
        long count = stream.count();
        System.out.println(count);  // 7
    }
}

14.6 常用方法_limit

Stream流中的常用方法_limit:用于截取流中的元素
limit方法可以对流进行截取,只取用前n个。方法签名:
Stream limit(long maxSize);
参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作
limit方法是一个延迟方法,只是对流中的元素进行截取,返回的是一个新的流,所以可以继续调用Stream流中的其他方法

public class Demo06Stream_limit {
    public static void main(String[] args) {
        // 获取一个Stream流
        String[] arr = {"美羊羊","喜洋洋","懒洋洋","灰太狼","红太狼"};
        Stream<String> stream = Stream.of(arr);
        // 使用limit对Stream流中的元素进行截取,只要前3个元素
        Stream<String> stream2 = stream.limit(3);
        // 遍历stream2流
        stream2.forEach(name-> System.out.println(name));
    }
}

// 程序执行结果
美羊羊
喜洋洋
懒洋洋

14.7 常用方法_skip

Stream流中的常用方法_skip:用于跳过元素
如果希望跳过前几个元素,可以使用skip方法获取一个截取之后的新流:
Stream skip(long n);
如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。

public class Demo07Stream_skip {
    public static void main(String[] args) {
        // 获取一个Stream流
        String[] arr = {"美羊羊","喜洋洋","懒洋洋","灰太狼","红太狼"};
        Stream<String> stream = Stream.of(arr);
        // 使用skip方法跳过前3个元素
        Stream<String> stream2 = stream.skip(3);
        // 遍历stream2流
        stream2.forEach(name-> System.out.println(name));
    }
}

14.8 常用方法_concat

Stream流中的常用方法_concat:用于把流组合到一起
如果有两个流,希望合并成为一个流,那么可以使用Stream接口的静态方法concat
static Stream concat(Stream<? extends T> a, Stream<? extends T> b)

    public static void main(String[] args) {
        // 创建一个Stream流
        Stream<String> stream1 = Stream.of("张三丰", "张翠山", "赵敏", "周芷若", "张无忌");
        // 获取一个Stream流
        String[] arr = {"美羊羊","喜洋洋","懒洋洋","灰太狼","红太狼"};
        Stream<String> stream2 = Stream.of(arr);
        // 把以上两个流组合为一个流
        Stream<String> concat = Stream.concat(stream1, stream2);
        // 遍历concat流
        concat.forEach(name-> System.out.println(name));
    }
}

14.9 集合元素的处理方式

先定义一个之后会用到的Person类

public class Person {
    private String name;

    public Person() {
    }

    public Person(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

传统方式

现在有两个ArrayList集合存储队伍当中的多个成员姓名,要求使用传统的for循环(或增强for循环)依次进行以下若干操作步骤:

  1. 第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
  2. 第一个队伍筛选之后只要前3个人;存储到一个新集合中。
  3. 第二个队伍只要姓张的成员姓名;存储到一个新集合中。
  4. 第二个队伍筛选之后不要前个人;存储到一个新集合中。
  5. 将两个队伍合并为一个队;存储到一个新集合中。
  6. 根据姓名创建Person对象;存储到一个新集合中。
  7. 打印整个队伍的Person对象信息。
public class Demo01StreamTest {
    public static void main(String[] args) {
        //第一支队伍
        ArrayList<String> one = new ArrayList<>();
        one.add("迪丽热巴");
        one.add("宋远桥");
        one.add("苏星河");
        one.add("石破天");
        one.add("石中玉");
        one.add("老子");
        one.add("庄子");
        one.add("洪七公");
        //1. 第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
        ArrayList<String> one1 = new ArrayList<>();
        for (String name : one) {
            if(name.length()==3){
                one1.add(name);
            }
        }
        //2. 第一个队伍筛选之后只要前3个人;存储到一个新集合中。
        ArrayList<String> one2 = new ArrayList<>();
        for (int i = 0; i <3 ; i++) {
            one2.add(one1.get(i));//i = 0,1,2
        }

        //第二支队伍
        ArrayList<String> two = new ArrayList<>();
        two.add("古力娜扎");
        two.add("张无忌");
        two.add("赵丽颖");
        two.add("张三丰");
        two.add("尼古拉斯赵四");
        two.add("张天爱");
        two.add("张二狗");
        //3. 第二个队伍只要姓张的成员姓名;存储到一个新集合中。
        ArrayList<String> two1 = new ArrayList<>();
        for (String name : two) {
            if(name.startsWith("张")){
                two1.add(name);
            }
        }
        //4. 第二个队伍筛选之后不要前2个人;存储到一个新集合中。
        ArrayList<String> two2 = new ArrayList<>();
        for (int i = 2; i <two1.size() ; i++) {
            two2.add(two1.get(i)); //i 不包含0 1
        }

        //5. 将两个队伍合并为一个队伍;存储到一个新集合中。
        ArrayList<String> all = new ArrayList<>();
        all.addAll(one2);
        all.addAll(two2);

        //6. 根据姓名创建Person对象;存储到一个新集合中。
        ArrayList<Person> list = new ArrayList<>();
        for (String name : all) {
            list.add(new Person(name));
        }

        //7. 打印整个队伍的Person对象信息。
        for (Person person : list) {
            System.out.println(person);
        }
    }
}

Stream方式

public class Demo02StreamTest {
    public static void main(String[] args) {
        //第一支队伍
        ArrayList<String> one = new ArrayList<>();
        one.add("迪丽热巴");
        one.add("宋远桥");
        one.add("苏星河");
        one.add("石破天");
        one.add("石中玉");
        one.add("老子");
        one.add("庄子");
        one.add("洪七公");
        //1. 第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
        //2. 第一个队伍筛选之后只要前3个人;存储到一个新集合中。
        Stream<String> oneStream = one.stream().filter(name -> name.length() == 3).limit(3);

        //第二支队伍
        ArrayList<String> two = new ArrayList<>();
        two.add("古力娜扎");
        two.add("张无忌");
        two.add("赵丽颖");
        two.add("张三丰");
        two.add("尼古拉斯赵四");
        two.add("张天爱");
        two.add("张二狗");
        //3. 第二个队伍只要姓张的成员姓名;存储到一个新集合中。
        //4. 第二个队伍筛选之后不要前2个人;存储到一个新集合中。
        Stream<String> twoStream = two.stream().filter(name -> name.startsWith("张")).skip(2);

        //5. 将两个队伍合并为一个队伍;存储到一个新集合中。
        //6. 根据姓名创建Person对象;存储到一个新集合中。
        //7. 打印整个队伍的Person对象信息。
        Stream.concat(oneStream,twoStream).map(name->new Person(name)).forEach(p-> System.out.println(p));
    }
}

15.方法引用

双冒号 :: 为引用运算符,而它所在的表达式被称为方法引用。如果Lambda要表达的函数方案已经存在于某个方 法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者。

定义一个打印的函数式接口

@FunctionalInterface
public interface Printable {
    // 定义字符串的抽象方法
    void print(String s);
}

调用

public class Demo01Printable {
    // 定义一个方法,参数传递Printable接口,对字符串进行打印
    public static void printString(Printable p) {
        p.print("HelloWorld");
    }

    public static void main(String[] args) {
        // 调用printString方法,方法的参数Printable是一个函数式接口,所以可以传递Lambda
        printString((s) -> {
            System.out.println(s);
        });

        /*
            分析:
                Lambda表达式的目的,打印参数传递的字符串
                把参数s,传递给了System.out对象,调用out对象中的方法println对字符串进行了输出
                注意:
                    1.System.out对象是已经存在的
                    2.println方法也是已经存在的
                所以我们可以使用方法引用来优化Lambda表达式
                可以使用System.out方法直接引用(调用)println方法
        */
        printString(System.out::println);
    }
}

// 程序执行结果
helloWorld
helloWorld

15.1 通过对象名引用成员方法

使用前提是对象名是已经存在的,成员方法也是已经存在(成员方法是我们自己定义的,不是系统自带的方法)
就可以使用对象名来引用成员方法

定义一个打印的函数式接口

@FunctionalInterface
public interface Printable {
    // 定义字符串的抽象方法
    void print(String s);
}

定义一个成员方法

public class MethodRerObject {
    // 定义一个成员方法,传递字符串,把字符串按照大写输出
    public void printUpperCaseString(String str){
        System.out.println(str.toUpperCase());
    }
}

调用

public class Demo01ObjectMethodReference {
    // 定义一个方法,方法的参数传递Printable接口
    public static void printString(Printable p){
        p.print("Hello");
    }

    public static void main(String[] args) {
        
        // 没使用lambda之前的旧方法
         printString(new Printable() {
            @Override
            public void print(String str) {
                MethodRerObject methodRerObject = new MethodRerObject();
                methodRerObject.printUpperCaseString(str + "1");
            }
        });
        
        
        // 调用printString方法,方法的参数Printable是一个函数式接口,所以可以传递Lambda表达式
        printString((s)->{
            // 创建MethodRerObject对象
            MethodRerObject obj = new MethodRerObject();
            // 调用MethodRerObject对象中的成员方法printUpperCaseString,把字符串按照大写输出
            obj.printUpperCaseString(s);
        });

        /*
            使用方法引用优化Lambda
            对象是已经存在的MethodRerObject
            成员方法也是已经存在的printUpperCaseString
            所以我们可以使用对象名引用成员方法
       */
        // 创建MethodRerObject对象
        MethodRerObject obj = new MethodRerObject();
        printString(obj::printUpperCaseString);
    }
}

// 程序执行的结果
HELLO
HELLO

15.2 通过类名引用静态成员方法

@FunctionalInterface
public interface Calcable {
    // 定义一个抽象方法,传递一个整数,对整数进行绝对值计算并返回
    int calsAbs(int number);
}

调用

通过类名引用静态成员方法
类已经存在,静态成员方法也已经存在
就可以通过类名直接引用静态成员方法

public class Demo01StaticMethodReference {
    // 定义一个方法,方法的参数传递要计算绝对值的整数,和函数式接口Calcable
    public static int method(int number,Calcable c){
       return c.calsAbs(number);
    }

    public static void main(String[] args) {
        // 使用lambda表达式之前的操作方法
        int result = method(-10, new Calcable() {
            @Override
            public int calsAbs(int num) {
                return Math.abs(num);
            }
        });
        System.out.println(result);
        
        
        // 调用method方法,传递计算绝对值得整数,和Lambda表达式
        int number = method(-10,(n)->{
            //对参数进行绝对值得计算并返回结果
            return Math.abs(n);
        });
        System.out.println(number);

        /*
            使用方法引用优化Lambda表达式
            Math类是存在的
            abs计算绝对值的静态方法也是已经存在的
            所以我们可以直接通过类名引用静态方法
        */
        int number2 = method(-10,Math::abs);
        System.out.println(number2);
    }
}

// 程序执行的结果
10
10

15.3 通过super引用成员方法

定义一个函数式接口

@FunctionalInterface
public interface Greetable {
    // 定义一个见面的方法
    void greet();
}

定义父类

public class Human {
    // 定义一个sayHello的方法
    public void sayHello(){
        System.out.println("Hello 我是Human!");
    }
}

定义子类并且调用

public class Man extends Human{
    // 子类重写父类sayHello的方法
    @Override
    public void sayHello() {
        System.out.println("Hello 我是Man!");
    }

    // 定义一个方法参数传递Greetable接口
    public void method(Greetable g){
        g.greet();
    }

    public void show(){
        // 调用method方法,方法的参数Greetable是一个函数式接口,所以可以传递Lambda
        method(()->{
            //创建父类Human对象
            Human h = new Human();
            //调用父类的sayHello方法
            h.sayHello();
        });

        // 因为有子父类关系,所以存在的一个关键字super,代表父类,所以我们可以直接使用super调用父类的成员方法
       method(()->{
            super.sayHello();
        });

      /*
           使用super引用类的成员方法
           super是已经存在的
           父类的成员方法sayHello也是已经存在的
           所以我们可以直接使用super引用父类的成员方法
     */
      method(super::sayHello);
    }

    public static void main(String[] args) {
        new Man().show();
    }
}

15.4 通过this引用本类成员方法

定义一个富有的函数式接口

@FunctionalInterface
public interface Richable {
    // 定义一个想买什么就买什么的方法
    void buy();
}

调用

public class Husband {
    // 定义一个买房子的方法
    public void buyHouse(){
        System.out.println("北京二环内买一套四合院!");
    }

    // 定义一个结婚的方法,参数传递Richable接口
    public void marry(Richable r){
        r.buy();
    }

    // 定义一个非常高兴的方法
    public void soHappy(){
        //调用结婚的方法,方法的参数Richable是一个函数式接口,传递Lambda表达式
       marry(()->{
            //使用this.成员方法,调用本类买房子的方法
            this.buyHouse();
        });

        /*
            使用方法引用优化Lambda表达式
            this是已经存在的
            本类的成员方法buyHouse也是已经存在的
            所以我们可以直接使用this引用本类的成员方法buyHouse
       */
        marry(this::buyHouse);
    }

    public static void main(String[] args) {
        new Husband().soHappy();
    }
}

// 程序执行的结果
北京二环内买一套四合院!
北京二环内买一套四合院!

15.5 类的构造器(构造方法)引用

先构造一个标准的类

public class Person {
    private String name;

    public Person() {
    }

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

定义一个创建Person对象的函数式接口

@FunctionalInterface
public interface PersonBuilder {
    // 定义一个方法,根据传递的姓名,创建Person对象返回
    // 定义一个builderPerson()方法,方法的返回值类型是 Person类型
    public Person builderPerson(String name);
}

调用

public class Demo {
    // 定义一个方法,参数传递姓名和PersonBuilder接口,方法中通过姓名创建Person对象
    public static void printName(String name,PersonBuilder pb){
        Person person = pb.builderPerson(name);
        System.out.println(person.getName());
    }

    public static void main(String[] args) {
        // 调用printName方法,方法的参数PersonBuilder接口是一个函数式接口,可以传递Lambda
        printName("迪丽热巴",(String name)->{
            return new Person(name);
        });

        /*
            使用方法引用优化Lambda表达式
            构造方法new Person(String name) 已知
            创建对象已知 new
            就可以使用Person引用new创建对象
        */
        // 使用Person类的带参构造方法,通过传递的姓名创建对象
        printName("古力娜扎",Person::new); 
    }
}

// 程序的执行结果
迪丽热巴
古力娜扎

15.6 数组的构造器引用

定义一个创建数组的函数式接口

@FunctionalInterface
public interface ArrayBuilder {
    // 定义一个创建int类型数组的方法,参数传递数组的长度,返回创建好的int类型数组
    int[] builderArray(int length);
}

调用

public class Demo {
    /*
        定义一个方法
        方法的参数传递创建数组的长度和ArrayBuilder接口
        方法内部根据传递的长度使用ArrayBuilder中的方法创建数组并返回
    */
    public static int[] createArray(int length, ArrayBuilder ab){
        return  ab.builderArray(length);
    }

    public static void main(String[] args) {
        // 调用createArray方法,传递数组的长度和Lambda表达式
        int[] arr1 = createArray(10,(len)->{
            // 根据数组的长度,创建数组并返回
            return new int[len];
        });
        System.out.println(arr1.length);//10

        /*
            使用方法引用优化Lambda表达式
            已知创建的就是int[]数组
            数组的长度也是已知的
            就可以使用方法引用
            int[]引用new,根据参数传递的长度来创建数组
         */
        int[] arr2 =createArray(10,int[]::new);
        System.out.println(Arrays.toString(arr2));
        System.out.println(arr2.length);  // 10
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值