程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。
而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。
线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池
JDK5新增了一个Executors工厂类来产生线程池,有如下几个方法
public static ExecutorService newCachedThreadPool():根据任务的数量来创建线程对应的线程个数
public class MyTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//线程池:容器,存有一定数量线程对象的容器,线程池可以复用管理线程对象。
//JDK5新增了一个Executors工厂类来产生线程池,有如下几个方法
// public static ExecutorService newCachedThreadPool ():根据任务的数量来创建线程对应的线程个数
//ExecutorService 线程池对象
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(new MyRunnale());
/* executorService.submit(new MyRunnale());
executorService.submit(new MyRunnale());
executorService.submit(new MyRunnale());
executorService.submit(new MyRunnale());*/
Future future = executorService.submit(new MyCallable());
Integer integer = future.get();
System.out.println(integer);
//关闭线程池
executorService.shutdown();
}
}
class MyRunnale implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程执行了这个任务");
}
}
class MyCallable implements Callable{
@Override
public Integer call() throws Exception {
System.out.println("call方法执行了");
return 100;
}
}
public static ExecutorService newFixedThreadPool(int nThreads): 固定初始化几个线程
public static void main(String[] args) {
// public static ExecutorService newFixedThreadPool ( int nThreads):固定初始化几个线程
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 11111111111111111");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 11111111111111111");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 11111111111111111");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 11111111111111111");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 11111111111111111");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 111111111111111");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 11111111111111111");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 11111111111111111");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 11111111111111111");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 11111111111111111");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 11111111111111111");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 11111111111111111");
}
});
//关闭线程池
executorService.shutdown();
}
public static ExecutorService newSingleThreadExecutor(): 初始化一个线程的线程池
public static void main(String[] args) {
// public static ExecutorService newSingleThreadExecutor ():初始化一个线程的线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 111111");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 111111");
}
});
}
这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。它提供了如下方法
Future> submit(Runnable task)
Future submit(Callable task)
使用步骤:
创建线程池对象
创建Runnable实例
提交Runnable实例
关闭线程池
匿名内部类的方式实现多线程程序
new Thread(){代码…}.start();
new Thread(new Runnable(){代码…}).start();
定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。
在Java中,可以通过Timer和TimerTask类来实现定义调度的功能。
public Timer()
public void schedule(TimerTask task, long delay):
public void schedule(TimerTask task,long delay,long period);
public void schedule(TimerTask task, Date time):
public void schedule(TimerTask task, Date firstTime, long period):
public abstract void run()
public boolean cancel()
开发中
Quartz是一个完全由java编写的开源调度框架。
public class MyTest {
public static void main(String[] args) {
//Timer 定时器 一种工具,线程用其安排以后在后台线程中执行的任务。可安排任务执行一次,或者定期重复执行。
Timer timer = new Timer();
//让定时器,执行定时任务
MyTimerTask myTimerTask = new MyTimerTask(timer);
//等3秒之后执行任务
// timer.schedule(myTimerTask,3000);
//等3秒第一次执行任务,以后间隔1秒重复执行定时任务
timer.schedule(myTimerTask, 3000,1000);
//取消定时任务
// myTimerTask.cancel();
//取消定时器
//timer.cancel();
}
}
class MyTimerTask extends TimerTask{
private Timer timer;
public MyTimerTask(Timer timer) {
this.timer = timer;
}
@Override
public void run() {
System.out.println("碰!爆炸了");
//取消定时器
// timer.cancel();
}
}
定时任务的多次执行代码体现
public class MyTest2 {
public static void main(String[] args) throws ParseException {
Timer timer = new Timer();
//在指定日期执行定时任务
MyTask task = new MyTask();
String dateStr="2020-06-07 09:48:00";
//在指定日期执行定时任务
Date date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateStr);
//在指定日期执行定时任务一次
// timer.schedule(task,date);
//在指定日期第一次执行任务,以后间隔1s重复执行
timer.schedule(task, date,1000);
//练习:定时删除一个文件夹
}
}
class MyTask extends TimerTask{
@Override
public void run() {
System.out.println("定时任务执行了");
}
}
public class MyTest {
public static void main(String[] args) throws ParseException {
Timer timer = new Timer();
String dateStr = "2020-06-07 10:16:00";
//在指定日期执行定时任务
Date date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateStr);
MyTask myTask = new MyTask(timer);
timer.schedule(myTask,date);
}
}
class MyTask extends TimerTask{
private Timer timer;
public MyTask(Timer timer) {
this.timer = timer;
}
@Override
public void run() {
File folder = new File("C:\\Users\\ShenMouMou\\Desktop\\demo");
deleteFolder(folder);
//关闭定时器
timer.cancel();
System.out.println("删除完毕");
}
private void deleteFolder(File folder) {
File[] files = folder.listFiles();
for (File f : files) {
if (f.isFile()) {
f.delete();
}else{
deleteFolder(f);
}
}
folder.delete();
}
}
A:多线程有几种实现方案,分别是哪几种? 三种
B:同步有几种方式,分别是什么? 三种 同步代码块 同步方法,Lock
C:启动一个线程是run()还是start()?它们的区别?
A:设计模式概述
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编写、代码设计经验的总结。
使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性以及代码的结构更加清晰.
B:设计模式分类
创建型模式(创建对象的): 单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。
行为型模式(对象的功能): 适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
结构型模式(对象的组成): 模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式。
又叫静态工厂方法模式,它定义一个具体的工厂类负责创建一些类的实例
使用静态工厂模式的优点是实现责任的分割,该模式的核心是工厂类,工厂类含有必要的选择逻辑,可以决定什么时候创建哪一个产品的实例,
而客户端则免去直接创建产品的责任,而仅仅是消费产品。也就是说静态工厂模式在不改变客户端代码的情况可以动态的增加产品。明确了类的职责
这个静态工厂类负责所有对象的创建,如果有新的对象增加,或者某些对象的创建方式不同,
就需要不断的修改工厂类,不利于后期的维护
//工厂类:主要负责创建各种动物实例的创建
public class AniamlFactory {
private AniamlFactory() {}
//创建一个猫的产品
/* public static Cat getCat(){
return new Cat();}
public static Dog getDog() {
return new Dog();}
public static Tiger geTiger() {
return new Tiger();}*/
public static Animal getAnimal(String name){
if("cat".equals(name)){return new Cat();
}else if("dog".equals(name)){return new Dog();
}else if("tiger".equals(name)){return new Tiger();
}else{return null;
}
}
}
public abstract class Animal {
public abstract void eat();
}
public class Cat extends Animal{
@Override
public void eat() {System.out.println("猫吃鱼");
}
}
public class Dog extends Animal{
@Override
public void eat() {
System.out.println("狗吃骨头");
}
}
public class Tiger extends Animal{
@Override
public void eat() {
System.out.println("老虎吃鸡");
}
}
public class MyTest {
public static void main(String[] args) {
/* Dog dog = AniamlFactory.getDog();
//使用
dog.eat();
//使用
Cat cat = AniamlFactory.getCat();
cat.eat();
Tiger tiger = AniamlFactory.geTiger();
tiger.eat();*/
Animal an = AniamlFactory.getAnimal("cat");
an.eat();
an=AniamlFactory.getAnimal("dog");
an.eat();
an=AniamlFactory.getAnimal("tiger");
an.eat();
}
}
工厂方法模式中抽象工厂类负责定义创建对象的接口,具体对象的创建工作由继承抽象工厂的具体类实现。
客户端不需要在负责对象的创建,从而明确了各个类的职责,如果有新的对象增加,
只需要增加一个具体的类和具体的工厂类即可,不影响已有的代码,后期维护容易,增强了系统的扩展性
需要额外的编写代码,增加了工作量
public abstract class Animal {
public abstract void eat();
}
public interface BigFactory {
//创建产品的方法
Animal createAnimal();
}
public class Cat extends Animal {
@Override
public void eat() {
System.out.println("猫吃鱼");
}
}
//创建猫的工厂
public class CatFactroy implements BigFactory {
@Override
public Animal createAnimal() {
return new Cat();
}
}
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("狗吃骨头");
}
}
public class DogFactory implements BigFactory{
@Override
public Animal createAnimal() {
return new Dog();
}
}
public class Tiger extends Animal{
@Override
public void eat() {
System.out.println("老虎吃鸡");
}
}
public class TigerFactory implements BigFactory{
@Override
public Animal createAnimal() {
return new Tiger();
}
}
public class MyTest {
public static void main(String[] args) {
Animal an = new DogFactory().createAnimal();
an.eat();
an = new CatFactroy().createAnimal();
an.eat();
an = new TigerFactory().createAnimal();
an.eat();
}
}
单例设计模式之懒汉式
开发中 饿汉式
面试中 懒汉式
面试就是想面试你们的两种思想:
a: 线程安全思想
b: 延迟加载思想
public class Student {
//懒汉式写法:你要用这个对象的时候,再去创建
private static Student student = null;
//1.私有构造
private Student() {}
//2.提供静态方法,返回一个该类的实例,共外界使用
//多线程环境下能保证这个方法的一定是创建一个对象吗?
//加上synchronized保证多线程环境下的使用也是单列的
//th1 th2
public synchronized static Student getStudent() {
if (student == null) {
//th1 th2
student = new Student();
}
return student;
}
}
public class MyTest {
public static void main(String[] args) {
//单列:保证一个类的对象,在内存中只有一个
//单例模式:懒汉式写法,饿汉式写法
//1.把该类的构造私有,不让他在外界直接new对象。
// Student student = new Student();
Student student = Student.getStudent();
Student student1 = Student.getStudent();
System.out.println(student==student1);
}
}
public class Teacher {//Teacher.class
//饿汉式,这个类一加载,我就创建一个该类的对象
private static Teacher teacher=new Teacher();
//私有构造
private Teacher() {
}
public static Teacher getTeacher(){
return teacher;
}
}
public class MyTest {
public static void main(String[] args) {
Teacher teacher = Teacher.getTeacher();
Teacher teacher1 = Teacher.getTeacher();
System.out.println(teacher);
System.out.println(teacher1);
System.out.println(teacher==teacher1);
//实际开发中,我们使用饿汉式
}
}
每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。可以通过 getRuntime 方法获取当前运行时。
应用程序不能创建自己的 Runtime 类实例。
public Process exec(String command) //执行Dos 命令
发现是单例模式的应用
public static void main(String[] args) throws IOException {
// Runtime 这个类Java就采用的是单列模式的饿汉式
Runtime runtime = Runtime.getRuntime();
//可以执行一些DOS命令
runtime.exec("calc");
runtime.exec("mspaint");
//定时关机
}
public class MyTest2 {
public static void main(String[] args) throws ParseException {
Timer timer = new Timer();
MyTask myTask = new MyTask(timer);
timer.schedule(myTask,new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2020-06-07 11:35:00"));
}
}
class MyTask extends TimerTask{
private Timer timer;
public MyTask(Timer timer) {
this.timer = timer;
}
@Override
public void run() {
//定时关机
try {
Runtime runtime = Runtime.getRuntime();
runtime.exec("shutdown /s /t 0");
timer.cancel();
} catch (IOException e) {
e.printStackTrace();
}
}
}
模版方法模式就是定义一个算法的骨架,而将具体的算法延迟到子类中来实现
a:优点: 使用模版方法模式,在定义算法骨架的同时,可以很灵活的实现具体的算法,满足用户灵活多变的需求
b:缺点: 如果算法骨架有修改的话,则需要修改抽象类
public abstract class CalcClass {
public void calc(){
//算法骨架
long start = System.currentTimeMillis();
calcMethod();
long end = System.currentTimeMillis();
System.out.println("耗时:"+(end-start)+"毫秒");
}
/* private void forMethod() {
for (int i = 0; i < 20000; i++) {
System.out.println("aaa");
}
}
public void copyFile(){
System.out.println("复制文件");
}*/
public abstract void calcMethod();
}
public class CalcFor extends CalcClass {
@Override
public void calcMethod() {
for (int i = 0; i < 5000; i++) {
System.out.println("abc");
}
}
}
public class CopyFile extends CalcClass{
@Override
public void calcMethod() {
System.out.println("复制文件");
}
}
public class MyTest {
public static void main(String[] args) {
//单例设计模式
//计算for循环的耗时
// new CalcClass().calcFor();
CalcClass js = new CalcFor();
js.calc();
js=new CopyFile();
js.calc();
}
}
装饰模式就是使用被装饰类的一个子类的实例,在客户端将这个子类的实例交给装饰类。是继承的替代方案
a:优点
使用装饰模式,可以提供比继承更灵活的扩展对象的功能,它可以动态的添加对象的功能,
并且可以随意的组合这些功能。
b:缺点: 正因为可以随意组合,所以就可能出现一些不合理的逻辑。
//包装类,对某个类的功能进行增强
public class BZPhone implements Phone{
private Phone phone;
//你要包装哪个类,把那个类传过来
public BZPhone(Phone phone) {
this.phone = phone;
}
@Override
public void call() {
this.phone.call();
}
}
public class GamePhone extends BZPhone {
public GamePhone(Phone phone) {
super(phone);
}
@Override
public void call() {
super.call();
System.out.println("游戏功能");
}
}
public class IPhone implements Phone{
public void call(){
System.out.println("打电话");
}
}
public class MusicPhone extends BZPhone{
public MusicPhone(Phone phone) {
super(phone);
}
@Override
public void call() {
super.call();
System.out.println("听歌的功能");
}
}
public interface Phone {
void call();
}
public class VideoPhone extends BZPhone{
public VideoPhone(Phone phone) {
super(phone);
}
@Override
public void call() {
super.call();
System.out.println("看视频的功能");
}
}
public class MyTest {
public static void main(String[] args) {
Phone iPhone = new IPhone();
iPhone.call();
System.out.println("=====================");
MusicPhone musicPhone = new MusicPhone(iPhone);
musicPhone.call();
System.out.println("====================");
VideoPhone videoPhone = new VideoPhone(iPhone);
videoPhone.call();
System.out.println("=============================");
VideoPhone videoPhone1 = new VideoPhone(new MusicPhone(iPhone));
videoPhone1.call();
System.out.println("============================");
MusicPhone musicPhone1 = new MusicPhone(new VideoPhone(iPhone));
musicPhone1.call();
System.out.println("====================");
GamePhone gamePhone = new GamePhone(new MusicPhone(iPhone));
gamePhone.call();
System.out.println("===========================");
GamePhone gamePhone1 = new GamePhone(new MusicPhone(new VideoPhone(iPhone)));
gamePhone1.call();
}
}
岗位类 求职者 猎头(注册方法,注销方法,发布方法)
public class Hunter {
//定义两个集合,用来装求职者,和工作岗位
private ArrayList jobSeekers=new ArrayList<>();
private ArrayList jobs = new ArrayList<>();
//注册求助者
public void addJobSeeker(JobSeeker jobSeeker){
jobSeekers.add(jobSeeker);}
//注册工作岗位
public void addJob(Job job){
jobs.add(job);
//工作岗位过来之后,通知求职者
notifyJobSeeker(job);}
private void notifyJobSeeker(Job job) {
for (JobSeeker jobSeeker : jobSeekers) {
System.out.println(jobSeeker.getName()+"你好!有一份工作:"+job.getJobName()+"薪资:"+job.getSal()+"欢迎你前去面试");
}
}
//注销
public void removeJobSeeker(JobSeeker jobSeeker){
jobSeekers.remove(jobSeeker);
}
}
public class Job {
private String jobName;
private double sal;
public Job() {}
public Job(String jobName, double sal) {
this.jobName = jobName;
this.sal = sal; }
public String getJobName() { }
public void setJobName(String jobName) {
this.jobName = jobName;}
public double getSal() {
return sal;}
public void setSal(double sal) {
this.sal = sal;}
}
public class JobSeeker {
private String name;
public JobSeeker() { }
public JobSeeker(String name) {
this.name = name;}
public String getName() {
return name;}
public void setName(String name) {
this.name = name;}
}
public class MyTest {
public static void main(String[] args) {
//观察者 = 订阅者 + 发布者
JobSeeker zs = new JobSeeker("张三");
JobSeeker ls= new JobSeeker("李四");
JobSeeker ww= new JobSeeker("王五");
//在猎头出注册
Hunter hunter = new Hunter();
hunter.addJobSeeker(zs);
hunter.addJobSeeker(ls);
hunter.addJobSeeker(ww);
Job job= new Job("Java开发工程师", 8000);
hunter.addJob(job);
System.out.println("=====================");
Job job2 = new Job("前端开发工程师", 18000);
hunter.addJob(job2);
System.out.println("========================");
//注销
hunter.removeJobSeeker(ww);
Job job3 = new Job("运维工程师", 5000);
hunter.addJob(job3);
}
}
public static void main(String[] args) throws IOException {
//把一个文件复制多份
RandomAccessFile in = new RandomAccessFile("C:\\Users\\ShenMouMou\\Desktop\\Java与模式.pdf", "rw");
int len=0;
byte[] bytes = new byte[1024 * 8];
for (int i = 0; i < 3; i++) {
FileOutputStream out= new FileOutputStream(new File("C:\\Users\\ShenMouMou\\Desktop", i + "设计模式.pdf"));
while ((len=in.read(bytes))!=-1){
out.write(bytes,0,len);
out.flush();
}
//把指针设置为0位置
in.seek(0);
out.close();
}
in.close();
}
案例演示
需求:我有一个文本文件,我知道数据是键值对形式的,但是不知道内容是什么。
请写一个程序判断是否有“lisi”这样的键存在,如果有就改变其值为”100”
public static void main(String[] args) throws IOException {
//读取配置文件
Properties properties = new Properties();
properties.load(new FileReader("name.properties"));
// System.out.println(properties);
if (properties.containsKey("lisi")) {
//键相同,值覆盖
properties.setProperty("lisi","100");
//再把改后的数据,写回文本文件
properties.store(new FileWriter("name.properties"),null);
System.out.println("修改完成");
}else{
System.out.println("没有lisi的键");
}
}
public static void main(String[] args) throws IOException {
//chaiFen();
//合并
File folder = new File("E:\\music");
File[] files = folder.listFiles();
Vector vector= new Vector<>();
for (File file : files) {
if (file.length()<=1024*1024&&file.getName().endsWith(".mp3")) {
FileInputStream in = new FileInputStream(file);
vector.add(in);
}
}
//SequenceInputStream(Enumeration < ? extends InputStream > e)
Enumeration elements = vector.elements();
SequenceInputStream sequenceInputStream = new SequenceInputStream(elements);
FileOutputStream out = new FileOutputStream(new File(folder,"合并.mp3"));
byte[] bytes = new byte[1024 *8];
int len = 0;
while ((len = sequenceInputStream.read(bytes)) != -1) {
out.write(bytes, 0, len);
}
sequenceInputStream.close();
out.close();
//最后 删除零碎的小文件
for (File file : folder.listFiles()) {
if (file.length() <= 1024 * 1024 && file.getName().endsWith(".mp3")) {
file.delete();
}
}
}
private static void chaiFen() throws IOException {
//把一个文件,拆成多份,每份一M 然后再合并回去
File file = new File("E:\\music");
if (!file.exists()) {
file.mkdirs();
}
FileInputStream in = new FileInputStream("辛晓琪 - 领悟.mp3");
//把缓冲大小定位1M
byte[] bytes = new byte[1024 * 1024];
int len=0;
int index=0;
while ((len=in.read(bytes))!=-1){
index++;
FileOutputStream out = new FileOutputStream(new File(file, index + ".mp3"));
out.write(bytes,0,len);
out.close();
}
in.close();
}
public class MyTest {
public static void main(String[] args) throws FileNotFoundException {
//多个线程,共同复制一个文件
//封装源文件
File srcFile = new File("辛晓琪 - 领悟.mp3");
//获取文件的总大小
long totalLength = srcFile.length();
//totalLength=310;
//定义线程的数量
long threadNum =3;
//计算机每个线程平均要复制的字节大小
long pj = totalLength / threadNum;
//我们得计算每个线程复制的那部分字节数据的起始位置和结束位置
for (long i = 0; i < threadNum; i++) {
//0---100
//101---200
//201---300
long start = pj * i;
long end = (i + 1) * pj - 1;
//System.out.println("线程"+i+"起始位置:"+start+"结束位置:"+end);
//开启线程复制
new CopyFileThread(start, end, srcFile, new File("E:\\领悟.mp3")).start();
}
//如果说文件的大小,不够多个线程均分怎么办?
//我的解决办法是,如果不够均分,我就在补一个线程,把剩下的复制完
if (totalLength % threadNum != 0) {
System.out.println("不均分");
long start = threadNum * pj;
long end = totalLength;
//System.out.println("补得线程"+"起始位置:" + start + "结束位置:" + end);
new CopyFileThread(start, end, srcFile, new File("E:\\领悟.mp3")).start();
}
}
}
class CopyFileThread extends Thread {
private long start;
private long end;
private RandomAccessFile in;
private RandomAccessFile out;
/**
* @param start 每个线程复制的起始位置
* @param end 每个线程复制的结束位置
* @param srcFile 源文件
* @param targetFile 目标文件
*/
public CopyFileThread(long start, long end, File srcFile, File targetFile) throws FileNotFoundException {
this.start = start;
this.end = end;
in = new RandomAccessFile(srcFile, "rw");
out = new RandomAccessFile(targetFile, "rw");
}
@Override
public void run() {
try {
//设置每个线程开始读写的位置
in.seek(start);
out.seek(start);
int len = 0;
byte[] bytes = new byte[1024*8]; //每次缓存1个字节
//如果你是一个字节一个字节来复制, start < end 那就隔离了
//如果说你的缓冲区不是一个字节 start < end 逻辑上隔离 最后一次,可能会多写会覆盖 不影响
while (start < end && (len = in.read(bytes)) != -1) {
start += len;
out.write(bytes, 0, len);
}
in.close();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}