1.异常
异常 :指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。
注意:
①在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处理异常的方式是中断处理。
②异常不是语法错误,语法错误是不能编译通过。
1.1.异常的管理体系
异常机制其实是帮助我们找到程序中的问题,异常的根类是 java.lang.Throwable(非抽象非接口) ,其下有两个子类: java.lang.Error 与 java.lang.Exception ,平常所说的异常指 java.lang.Exception 。
其中error是无法处理,只能避免,我们要解决的是Exception,exception具有众多的子类,其中Exception的子类RuntimeException是运行期异常,是最为常见的异常之一子类。
Throwable体系: Error:严重错误Error,无法通过处理的错误,只能事先避免,好比绝症。 Exception:表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。好比感冒、阑尾炎。
1.2. Throwable中的常用方法:
*public void printStackTrace() :打印异常的详细信息。包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。
*public String getMessage() :获取发生异常的原因。提示给用户的时候,就提示错误原因。
*public String toString() :获取异常的类型和异常描述信息(不用)。出现异常,不要紧张,把异常的简单类名,拷贝到API中去查
我们平常说的异常就是指Exception,因为这类异常一旦出现,我们就要对代码进行更正,修复程序。
异常(Exception)的分类:根据在编译时期还是运行时期去检查异常?
①编译时期异常:checked异常:在编译时期,就会检查,如果没有处理异常,则编译失败。(如日期格式化异常) ,必须处理;是RuntimeException
以外的异常,类型上都属于Exception
类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException
、SQLException
等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
②运行时期异常:runtime异常:在运行时期,检查异常.在编译时期,运行异常不会编译器检测(不报错)。(如数学异常) ;
都是RuntimeException
类及其子类异常,如NullPointerException
(空指针异常)、IndexOutOfBoundsException
(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch
语句捕获它,也没有用throws
子句声明抛出它,也会编译通过。
Error
(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError
。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(
NoClassDefFoundError
)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。 Exception
类有一个重要的子类 RuntimeException。RuntimeException 类及其子类表示“JVM 常用操作”引发的错误。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException
、ArithmeticException
)和 ArrayIndexOutOfBoundException
。
Exception
和Error
)分为可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)。
RuntimeException
及其子类以外,其他的Exception
类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。
RuntimeException
与其子类)和错误(Error
)。
Exception
这种异常分两大类运行时异常和非运行时异常(编译异常)。程序中应当尽可能去处理这些异常。 1.2 异常处理
Java异常处理的五个关键字:try、catch、finally、throw、throws
1.2.1 throw关键字-方法内部-校验抛出一个指定的异常对象
在编写程序时,我们必须要考虑程序出现问题的情况。比如,在定义方法时,方法需要接受参数。那么,当调用方法使用接受到的参数时,首先需要先对参数数据进行合法的判断,数据若不合法,就应该告诉调用者,传递合法的数据进来。这时需要使用抛出异常的方式来告诉调用者。在java中,提供了一个throw关键字,它用来抛出一个指定的异常对象。那么,抛出一个异常具体如何操作呢? 1. 创建一个异常对象。封装一些提示信息(信息可以自己编写)。 2. 需要将这个异常对象告知给调用者。怎么告知呢?怎么将这个异常对象传递到调用者处呢?通过关键字throw 就可以完成。throw 异常对象。 throw用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行
格式:throw new 异常类名(参数);
throw new NullPointerException("要访问的arr数组不存在"); throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");
public class ThrowDemo { public static void main(String[] args) { //创建一个数组 int[] arr = {2,4,52,2}; //根据索引找对应的元素 int index = 4; int element = getElement(arr, index); System.out.println(element); System.out.println("over"); } /* * 根据 索引找到数组中对应的元素 */ public static int getElement(int[] arr,int index){ //判断 索引是否越界 if(index<0 || index>arr.length‐1){ /* 判断条件如果满足,当执行完throw抛出异常对象后,方法已经无法继续运算。 这时就会结束当前方法的执行,并将异常告知给调用者。这时就需要通过异常来解决。 */ throw new ArrayIndexOutOfBoundsException("哥们,角标越界了~~~"); } int element = arr[index]; return element; } }
注意:throw关键字抛出指定的异常对象,我们就必须处理这个对象,处理方式:两种:
①throw关键字后边创建的是RuntimeException或者是RuntimeException的子类对象,我们可以不处理,默认交给JVM处理(打印异常对象,中断程序)例如:若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException
、ArithmeticException
)和 ArrayIndexOutOfBoundException;
②throw关键字后边创建的是编译异常(写代码的时候报错),我们就必须处理这个异常,要么throws,要么try-catch;否则就无法跑起来,如果没有处理异常,则编译失败。(如日期格式化异常) ,必须处理;是RuntimeException
以外的异常,类型上都属于Exception
类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException
、SQLException
等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
1.3.Objects非空判断-相当于throw抛出异常
学习过一个类Objects吗,曾经提到过它由一些静态的实用方法组成,这些方法是null-save(空指针安全的)或null-tolerant(容忍空指针的),那么在它的源码中,对对象为null的值进行了抛出异常操作。
public static <T> T requireNonNull(T obj) :查看指定引用对象不是null。
-----在未来,对传递的参数是否为空,可以用objects.requireNonNull进行判断,减少代码书写,提升效率
public static <T> T requireNonNull(T obj) { if (obj == null) throw new NullPointerException(); return obj; }
Objects.requireNonNull(obj,“传递的对象的值是null");
1.4 throws方法上声明异常-而throw是在方法内部抛出异常
声明异常:将问题标识出来,报告给调用者。如果方法内通过throw抛出了编译时异常,而没有捕获处理(稍后讲解该方式),那么必须通过throws进行声明,让调用者去处理。最终是给jvm(虚拟机处理)-》中断过程
①关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常).
声明异常格式:声明异常的代码演示: throws用于进行异常类的声明,若该方法可能有多种异常情况产生,那么在throws后面可以写多个异常类,用逗号隔开。 一般配合throw使用
public static T requireNonNull(T obj) {
if (obj == null) throw new NullPointerException();
return obj;
}
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{ }
1.5 try-catch异常
throws的缺陷在于,throws是声明在方法上,因此被throws声明的方法某一行出现异常后,这一行代码无法继续下去。
try-catch则可以避免,当然是try某一行,如果是全部try内部则也不会继续执行。catch可以有多个~
格式:
try{ 编写可能会出现异常的代码
}catch(异常类型 变量e){ // try抛出啥异常类型,catch放啥异常类型
处理异常的代码 //记录日志/打印异常信息/继续抛出异常
}
也就是说 try-catch组合不是把异常对象抛出去给调用者,而是用catch来接收这个对象,进行相应的异常逻辑处理
如何获取异常信息: Throwable类中定义了一些查看方法:
**public String getMessage() :获取异常的描述信息,原因(提示给用户的时候,就提示错误原因。
**public String toString() :获取异常的类型和异常描述信息(不用)。重写object类的tostring方法
**public void printStackTrace() :打印异常的跟踪栈信息并输出到控制台。包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。 最全面的
1.6 finally
一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到。而finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行的。什么时候的代码必须最终执行?当我们在try语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),我们都得在使用完之后,最终关闭打开的资源。 finally的语法: try...catch....finally:自身需要处理异常,最终还得关闭资源。
当只有在try或者catch中调用退出JVM的相关方法,此时finally才不会执行,否则finally永远会执行。
java9引入 List.of("a","b"); 生成一个List<String> 后 元素就是不可变的了 如果再增加list中的元素就会报错
运行时异常被抛出可以不处理。即不捕获也不声明抛出。
如果finally有return语句,永远返回finally中的结果,避免该情况.
如果父类抛出了多个异常,子类重写父类方法时,抛出和父类相同的异常或者是父类异常的子类或者不抛出异常。
父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出
一句话:父抛子可抛也可不抛,父不抛子不可抛,只能自己try-catch;建议子父类异常一一致就行,省的麻烦;
1.7 自定义异常类
异常类如何定义:
*自定义一个编译期异常: 自定义类继承于 java.lang.Exception 。
*自定义一个运行时期的异常类:自定义类继承于 java.lang.RuntimeException
当然也可以继承运行期异常 runtimeException,这样自动抛出,中断
2. 多线程复习
并发,一段时间内,cpu在多个任务之间,交替执行,一会a任务,一会b任务
并行,一段时间内,cpu同时执行多个任务。
进程:指的是内存中运行的程序,每个进程都有独立的内存空间,一个应用程序可以同时运行多个进程,进程也是程序的一次执行过程,是系统运行程序的基本单位,系统运行一个程序是进程的创建-运行到消亡的过程。
线程:是进程的一个执行单元,可以理解为一个进程可以有多个线程。电脑可以看到进程每个程序的。一句话:进入到内存的程序叫进程,一个进程可以有多个线程。也称之为多线程程序,可以理解一个程序的一个进程,而程序有多个功能,同时进行多个功能,就是多个线程操作;单核单线程电脑,则会在多个线程之间高速的切换线程执行(1/N秒)
硬盘存储叫rom,内存叫ram(临时存在)
2.1 线程的调度-分为分时调度和抢占式调度
分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。参考电脑进程边上的详细进行,可以见到多个线程。多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
2.2创建线程类
首先要了解,主线程的含义:其实主线程就是指的是main方法的线程,单线程指的是java程序中只有一个线程,执行从main方法开始,自上到下依次执行。
Java使用java.lang.Thread
类代表**线程**,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来**创建**并**启动多线程**的步骤如下:
代码如下:
测试类:
public class Demo01 { public static void main(String[] args) { //创建自定义线程对象 MyThread mt = new MyThread("新的线程!"); //开启新线程 mt.start(); //在主方法中执行for循环 for (int i = 0; i < 10; i++) { System.out.println("main线程!"+i); } } }
自定义线程类:
public class MyThread extends Thread { //定义指定线程名称的构造方法 public MyThread(String name) { //调用父类的String参数的构造方法,指定线程的名称 super(name); } /** * 重写run方法,完成该线程执行的逻辑 */ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(getName()+":正在执行!"+i); } } }
单线程中,主线程出现问题,下面代码则无法运行。
2.2 如何创建多线程
java.lang包下有个类 Thread类代表线程。
切记 源码提醒我们,我们是执行start方法来执行run方法,也就是开辟一个新线程。源码说 start()使得该线程开始执行,java虚拟机调用该线程的run方法。
public class Demo01 { public static void main(String[] args) { //创建自定义线程对象 MyThread mt = new MyThread("新的线程!"); //开启新线程 mt.start(); //在主方法中执行for循环 for (int i = 0; i < 10; i++) { System.out.println("main线程!"+i); } } }
public class MyThread extends Thread { //定义指定线程名称的构造方法 public MyThread(String name) { //调用父类的String参数的构造方法,指定线程的名称 super(name); } /** * 重写run方法,完成该线程执行的逻辑 */ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(getName()+":正在执行!"+i); } } }
内存原理如:多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
2.3 多线程的一些常用方法
2.3.1 获取线程名称
首先要记得:子类可以调用父类的任何非私有方法,因为JAVA中有重写的概念,如果在子类中定义某方法与其父类有相同的名称和参数,该方法被重写,在Java中子类可继承父类中的方法,而不需要重新编写相同的方法。但有时子类并不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写。若子类中的方法与父类中的某一方法具有相同的方法名、返回类型和参数表,则新方法将覆盖原有的方法。如需父类中原有的方法,可使用super关键字(super.datacreat();),该关键字引用了当前类的父类。
其中currentThread返回的是线程对象的引用,也就可以t.getName();
所谓引用就是对象,此处是线程对象
*获取线程名字
*设置线程名字
*线程暂停执行sleep方法
sleep 毫秒级方法延迟执行当前线程。
2.4 创建线程的第二种方式,不是lang包下Thread类 而是lang包下的Runable接口,但是runable接口没有start方法,需要thread类实现,传递runable接口实现类对象给它
thread与runable区别如上:
使用匿名内部类方式实现线程的创建-把子类的继承父类,重写父类run方法和创建子类对象一并执行,最终产物就是一个子类/实现类对象
多态实现接口=实现类,多态方法
2.5 线程安全问题
实现Runnable接口比继承Thread类所具有的优势:
1. 适合多个相同的程序代码的线程去共享同一个资源。
2. 可以避免java中的单继承的局限性。
3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
多线程操作同一事物(共享的数据),可能存在同时操作或者出现不存在的数据,称之为线程安全问题。
这里要注意:我们创建的一个runable接口实现对象,然后放到三个线程中,这样可以保证我们用的是一个数据源,共享数据。否则创建三个接口实现对象,就各自是各自的。
多线程安全问题的分析:
解决方案:无论是否失去cpu执行权,都只让其他线程等待中。
解决方法:* 同步代码块
* 同步方法
* 锁机制
同步代码块格式:
synchronized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
注意:通过同步代码块的锁对象可以是任意的对象
必须保证多个线程使用的是同一个锁对象
锁对象的作用:把同步代码块锁住,只让一个线程在同步代码块中执行,任何适合,最多允许一个线程拥有同步锁,拿到锁就进入代码块,其他线程只能等待中(blocked):
*************1同步代码块解决方案如下:
同步代码块技术原理如下:代价就是频繁校验锁,效率低
同步代码块使用的一个锁对象,叫同步锁也叫对象锁,或者叫对象监视器,同步代码块将检查线程有没有同步锁,没有等待,有的就执行完归还锁对象。
***********2同步方法解决方案
使用步骤:1.把访问了共享数据的代码抽取出来,放到一个方法中
2.在方法上添加synchronized修饰符
格式: 修饰符 synchronized 返回值类型 方法名(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
原理:
定义一个同步方法,同步方法将方法内部的代码锁住,只让一个线程进,那么同步方法的锁对象是谁?答案是同步方法的锁对象就是实现类对象new RunableImpl();也就是this,谁调用方法,谁就是this的对象
总结:同步方法默认的对象就是线程的实现类对象:
验证是不是一个this
**************3 静态同步方法解决方案
静态同步方法的锁对象是谁,不能是this,为什么?
因为this 是创建对象之后产生的,而静态方法是优先于对象产生,因此静态同步方法的锁对象是本类的class属性->>>class文件对象(反射)
也就是在那个类,就用哪个类的.class属性做锁对象
******** 锁---解决线程安全问题第三种方案-----
jdk1.5之后,新出一个包 java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
****public void lock() :加同步锁。
****public void unlock() :释放同步锁。使用如下:
也就是在可能出现线程安全的地方代码前后,也就是使用共享资源的代码进行lock和unlock操作。
当然在线程接口实现类中创建以下代码
***在成员位置创建一个ReentrantLock对象
***在可能出现问题的代码前后加锁和释放锁。
public class Ticket implements Runnable{ private int ticket = 100; Lock lock = new ReentrantLock(); /* * 执行卖票操作 */ @Override public void run() { //每个窗口卖票的操作 //窗口 永远开启 while(true){ lock.lock(); if(ticket>0){//有票 可以卖 //出票操作 //使用sleep模拟一下出票时间 try { Thread.sleep(50); } catch (InterruptedException e) { // TODO Auto‐generated catch block e.printStackTrace(); } //获取当前线程对象的名字 String name = Thread.currentThread().getName(); System.out.println(name+"正在卖:"+ticket‐‐); } lock.unlock(); } } }
更好的写法是用finally包裹:
**************线程的几种状态--6种
new 新建状态
blocked 阻塞状态
runtime 运行状态
teaminated 死亡状态
time-waiting 休眠状态
waiting 无限等待状态
最为重要的状态:无线等待状态 waiting:一个正在无线等待另外一个线程执行一个特别的(唤醒)多做的线程处于这个状态--也就是线程通信-一个图片学习它:
等待唤醒案例-线程之间的通信:
创建一个顾客线程,告诉老板要的包子种类和数量,调用wait方法,放弃cpu执行,进入到Waiting状态
创建一个老板线程,花了五秒做包子,做好包子后,调用notify方法,唤醒顾客吃包子
****注意:
顾客和老板必须使用同步代码块包裹起来,保证等待和唤醒只有一个能够执行,且同步使用的锁对象是唯一的,只有锁对象才能调用wait和notify方法
也就是 notify是随机唤醒等待的其他线程,notifyall是唤醒所有线程。切记对象监视器就是对象锁。
**************线程通信**************
线程通信就是线程的合作,我们可以理解为多个线程处于同一个资源下进行处理,但是处理的动作不相同,比如登录、注册等功能。对于同一个资源就是包子或者用户数据。
为什么要处理线程通信? 因为多线程并发执行时,默认情况下cpu是随机切换线程的,当我们需要他们多线程协同完成任务时,我们希望他们有规律的进行,避免对同一共享变量资源 的争夺,如何有效调用资源处理,这种手段就是等待唤醒机制。--对共享资源的判断
哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行(看看它是否获取执行权力),因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
总结如下:
***************包子案例升华- 等待唤醒机制
---测试
package com.itheima.jdbc; public class Demo { public static void main(String[] args) { BaoZi baoZi=new BaoZi(); //创建包子铺线程 new BaoZiPu(baoZi).start(); new ChiHuo(baoZi).start(); } }
----包子
package com.itheima.jdbc; public class BaoZi { String pi; String xian; boolean flag= false; public BaoZi() { } public BaoZi(String pi, String xian, boolean flag) { this.pi = pi; this.xian = xian; this.flag = flag; } }
----包子铺
package com.itheima.jdbc; public class BaoZiPu extends Thread{ // 包子铺与包子是互斥关系,同一时间只有一个能执行,锁对象必须保证唯一,可以用包子对象作为锁对象,需要将包子对象作为参数传递进来 /** * 需要在成员位置创建一个包子变量 * 使用带参构造方法,为包子变量进行赋值 * * 包子铺是一个线程类,可继承Thread */ //1.在成员变量位置定义一个包子变量 private BaoZi baoZi; //2. 使用带参构造方法,为包子变量赋值 public BaoZiPu(BaoZi baoZi) { this.baoZi = baoZi; } //3.设置线程任务 run :生成包子 @Override public void run() { //定义一个变量 int count=0; //4.使用同步技术保障两个线程只有一个执行 synchronized (baoZi){ if (baoZi.flag==true){ //有包子 try { baoZi.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //没包子 if (count%2==0){ baoZi.pi="薄皮包子"; baoZi.xian="三鲜"; }else { baoZi.pi="厚皮包子"; baoZi.xian="红豆"; } count++; System.out.println("包子铺正在生产"+baoZi.pi+baoZi.xian+"包子"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } //5 包子生产好 baoZi.flag=true; baoZi.notify(); System.out.println(baoZi.pi+baoZi.xian+"包子已经生产好了,吃货可以开吃了"); System.out.println("***********"); } } }
-----吃货
package com.itheima.jdbc; public class ChiHuo extends Thread{ private BaoZi baoZi; public ChiHuo(BaoZi baoZi) { this.baoZi = baoZi; } @Override public void run() { synchronized (baoZi){ if (baoZi.flag==false){ try { baoZi.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //吃货线程被唤醒之后 System.out.println("吃货正在吃"+baoZi.pi+baoZi.xian+"包子"); baoZi.flag=false; // 唤醒包子铺 baoZi.notify(); System.out.println("吃货已经吃完"+baoZi.pi+baoZi.xian+"包子,请包子铺开始生产包子吧"); System.out.println("-----------------------------------------------"); } } }
当前存在的问题--频繁创建线程和销毁线程需要时间,效率会降低,有没有一种办事使得线程可以复用,就是执行完一个任务,并不销毁,继续执行其他任务?-----线程池了解下:
*****************线程池--容器概念,容器就可以使用集合 arrayList hashSet
LinkedList<Thread>,HashMap,一般用LinkedList<Thread>,泛型用Thread,具体思路使用集合的方法,移除已使用的线程,使用队列思想
JDK1.5之后,已经内置了线程池,可以直接使用,上面是线程池的底层原理
合理利用线程池能够带来三个好处:
*************线程池的使用 jdk1.5之后,在java.util.concurrent里面有Excecutors类,这个类是生产线程池的工厂类,使用其静态方法给定线程数,生产出指定可重用的线程数的线程池。
Java里面线程池的顶级接口是java.util.concurrent.Executor
,但是严格意义上讲Executor
并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService
。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors
线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。
Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:
public Future<?> submit(Runnable task)
:获取线程池中的某一个线程对象,并执行,也就是从线程池中取出一个线程进行执行操作
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
使用线程池中线程对象的步骤:
-----Runnable实现类代码:
public class MyRunnable implements Runnable { @Override public void run() { System.out.println("我要一个教练"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("教练来了: " + Thread.currentThread().getName()); System.out.println("教我游泳,交完后,教练回到了游泳池"); } }
----测试
public class ThreadPoolDemo { public static void main(String[] args) { // 创建线程池对象 ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象 // 创建Runnable实例对象 MyRunnable r = new MyRunnable(); //自己创建线程对象的方式 // Thread t = new Thread(r); // t.start(); ---> 调用MyRunnable中的run() // 从线程池中获取线程对象,然后调用MyRunnable中的run() service.submit(r); // 再获取个线程对象,调用MyRunnable中的run() service.submit(r); service.submit(r); // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。 // 将使用完的线程又归还到了线程池中 // 关闭线程池 //service.shutdown(); } }
关闭shutDown() 一般不用,无返回值,销毁的话,相当于线程池就没法再用,所以一般不用销毁。否则报错 rejectedExceutionException
如果多个线程,超过线程池数量,则超过线程池的线程会等待其他线程执行完再做。
***********************函数式编程思想
一句话,通过函数式编程,忽略面向对象的复杂语法,强调做啥,而不是以什么形式去做,简单来说2X=y,给啥x,得不同y;
面向对象的思想:
? 做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情.
函数式编程思想:
? 只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程
public class Demo01Runnable { public static void main(String[] args) { // 匿名内部类 Runnable task = new Runnable() { @Override public void run() { // 覆盖重写抽象方法 System.out.println("多线程任务执行!"); } }; new Thread(task).start(); // 启动线程 } }
本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个Runnable
接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。
----------------
代码分析
对于Runnable
的匿名内部类用法,可以分析出几点内容:
Thread
类需要Runnable
接口作为参数,其中的抽象run
方法是用来指定线程任务内容的核心;
run
的方法体,**不得不**需要Runnable
接口的实现类;
RunnableImpl
实现类的麻烦,**不得不**使用匿名内部类;
run
方法,所以方法名称、方法参数、方法返回值**不得不**再写一遍,且不能写错;
***********jdk 1.8 lambada表达式---简化代码
public class LambadaDemo { public static void main(String[] args) { //使用匿名内部类实现多线程 new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"线程创建了"); } }).start(); //使用lambada表达式 这里下面()括号就是上面new Runable括号里面参数,没有参数就空着 {}里面就是上面大括号里面的代码体 new Thread(()->{ System.out.println(Thread.currentThread().getName()+"线程创建了"); } ).start(); } }
//使用lambada表达式 这里下面()括号就是上面new Runable括号里面
参数,没有参数就空着 {}里面就是上面大括号里面的代码体
Lambda省去面向对象的条条框框,格式由**3个部分**组成:
Lambda表达式的**标准格式**为:
(参数类型 参数名称) -> { 代码语句 }
格式说明:
->
是新引入的语法格式,代表指向动作。
----------无参无返回值
public class LambadaDemoTwo { public static void main(String[] args) { //调用invokeCook 方法 参数是cook接口,传递cook接口匿名内部类对象 invokeCook(new ChuZi() { @Override public void makeFood() { System.out.println("做饭了"); } }); invokeCook(()->{ System.out.println("做饭了"); }); } //顶一个一个方法,参数传递Cook接口,方法内部调用cook接口方法 makeFood private static void invokeCook(ChuZi chuZi){ chuZi.makeFood(); } }
public interface ChuZi { public abstract void makeFood(); }
---------------有参数有返回值
需求:
使用数组存储多个Person对象
对数组中的Person对象使用Arrays的sort方法通过年龄进行升序排序
下面举例演示java.util.Comparator<T>
接口的使用场景代码,其中的抽象方法定义为:
public abstract int compare(T o1, T o2);
当需要对一个对象数组进行排序时,Arrays.sort
方法需要一个Comparator
接口实例来指定排序的规则。假设有一个Person
类,含有String name
和int age
两个成员变量:
public class Person { private String name; private int age; // 省略构造器、toString方法与Getter Setter }
传统写法
如果使用传统的代码对Person[]
数组进行排序,写法如下:
import java.util.Arrays; import java.util.Comparator; public class Demo06Comparator { public static void main(String[] args) { // 本来年龄乱序的对象数组 Person[] array = { new Person("古力娜扎", 19), new Person("迪丽热巴", 18), new Person("马尔扎哈", 20) }; // 匿名内部类 Comparator<Person> comp = new Comparator<Person>() { @Override public int compare(Person o1, Person o2) { return o1.getAge() - o2.getAge(); } }; Arrays.sort(array, comp); // 第二个参数为排序规则,即Comparator接口实例 for (Person person : array) { System.out.println(person); } } }
这种做法在面向对象的思想中,似乎也是“理所当然”的。其中Comparator
接口的实例(使用了匿名内部类)代表了“按照年龄从小到大”的排序规则。
代码分析
下面我们来搞清楚上述代码真正要做什么事情。
Arrays.sort
方法需要排序规则,即Comparator
接口的实例,抽象方法compare
是关键;
compare
的方法体,**不得不**需要Comparator
接口的实现类;
ComparatorImpl
实现类的麻烦,**不得不**使用匿名内部类;
compare
方法,所以方法名称、方法参数、方法返回值**不得不**再写一遍,且不能写错;
import java.util.Arrays; public class Demo07ComparatorLambda { public static void main(String[] args) { Person[] array = { new Person("古力娜扎", 19), new Person("迪丽热巴", 18), new Person("马尔扎哈", 20) }; Arrays.sort(array, (Person a, Person b) -> { return a.getAge() - b.getAge(); }); for (Person person : array) { System.out.println(person); } } }
使用Lambda标准格式(有参有返回)
题目
给定一个计算器Calculator
接口,内含抽象方法calc
可以将两个int数字相加得到和值:
public interface Calculator {
int calc(int a, int b);
}
在下面的代码中,请使用Lambda的**标准格式**调用invokeCalc
方法,完成120和130的相加计算:
public class Demo08InvokeCalc { public static void main(String[] args) { // TODO 请在此使用Lambda【标准格式】调用invokeCalc方法来计算120+130的结果? } private static void invokeCalc(int a, int b, Calculator calculator) { int result = calculator.calc(a, b); System.out.println("结果是:" + result); } }
解答
public static void main(String[] args) {
invokeCalc(120, 130, (int a, int b) -> {
return a + b;
});
}
备注:小括号代表Calculator
接口calc
抽象方法的参数,大括号代表calc
的方法体。
3.10 Lambda省略格式
可推导即可省略
Lambda强调的是“做什么”而不是“怎么做”,所以凡是可以根据上下文推导得知的信息,都可以省略。例如上例还可以使用Lambda的省略写法:
public static void main(String[] args) {
invokeCalc(120, 130, (a, b) -> a + b);
}
省略规则
在Lambda标准格式的基础上,使用省略写法的规则为:
备注:掌握这些省略规则后,请对应地回顾本章开头的多线程案例。
3.11 练习:使用Lambda省略格式
题目
仍然使用前文含有唯一makeFood
抽象方法的厨子Cook
接口,在下面的代码中,请使用Lambda的**省略格式**调用invokeCook
方法,打印输出“吃饭啦!”字样:
public class Demo09InvokeCook { public static void main(String[] args) { // TODO 请在此使用Lambda【省略格式】调用invokeCook方法 } private static void invokeCook(Cook cook) { cook.makeFood(); } }
解答
public static void main(String[] args) {
invokeCook(() -> System.out.println("吃饭啦!"));
}
3.12 Lambda的使用前提
Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
Runnable
、Comparator
接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
备注:有且仅有一个抽象方法的接口,称为“**函数式接口**”。
-------------------------------------------------------------------------------
3.File类
java.io.File类是文件和目录(文件夹意思)路径名的抽象表示,主要用于文件和目录的创建,查找和删除等操作。切记file与操作系统无关!!!
一句话:java把电脑中的文件和文件夹目录封装为一个file类,我们可以使用file类对文件和文件夹进行相关的操作主要是以下:
**创建一个文件夹或文件
**删除一个文件或文件夹
**获取一个文件或文件夹
**判断文件或文件夹是否存在
**对文件夹进行遍历
**获取文件的大小
三大关键字:file 文件 directory 文件夹目录 path 目录
file类中四个静态成员变量,可以通过类名直接访问
public class FileDemoOne { public static void main(String[] args) { //路径分割符 静态方法pathSeparator 与系统有关的 String pathSeparator = File.pathSeparator; System.out.println(pathSeparator);// ;分号 //文件分隔符 String separator = File.separator; System.out.println(separator); // } }
路径分割符 windows是; linux是 :跟环境变量里面一模一样
文件分隔符 windows 是/ linux是
以上四个静态成员变量是两种方式,返回值char和字符串是差不多,是源码里面加了+“”;
---------------绝对路径与相对路径------------------------
绝对路径:完整的路径,是以盘符c/d/e/d开始;c:\a.txt
相对路径:简化的路径,是相对于当前项目的根目录 D:CodeIDEAeesy_spring)相对它而已简化书写:123.txt
注意:路径不区分大小写
路径中的文件名称分隔符windows是反斜杠,反斜杠是转义字符,两个反斜杠代表一个普通的反斜杠。
----------------file类的构造方法-------------------
学习一个类,主要是静态变量,可以直接类名.静态变量进行操作。而接下来学习构造方法,完成对象创建,对象创建后即可.方法进行业务实现。
public File(String pathname)
:通过将给定的**路径名字符串**转换为抽象路径名来创建新的 File实例。public File(String parent, String child)
:从**父路径名字符串和子路径名字符串**创建新的 File实例。 父路径和子路径拼接 更加灵活
public File(File parent, String child)
:从**父抽象路径名和子路径名字符串**创建新的 File实例。 好处是灵活而且是父路径是file类 ,可以进行父路径进行操作(使用方法)
// 文件路径名 String pathname = "D:\aaa.txt"; File file1 = new File(pathname); // 文件路径名 String pathname2 = "D:\aaa\bbb.txt"; File file2 = new File(pathname2); // 通过父路径和子路径字符串 String parent = "d:\aaa"; String child = "bbb.txt"; File file3 = new File(parent, child); // 通过父级File对象和子路径字符串 File parentDir = new File("d:\aaa"); String child = "bbb.txt"; File file4 = new File(parentDir, child);
这个路径pathname 就是字符串的路径名称,可以是文件结尾,也可以是文件夹结尾,可以是存在也可以是不存在的,可以是相对路径,也可以是绝对路径。切:记 创建file对象:只是把字符串路径封装为file对象,不考虑路径真假情况。
private static void showOne(){ // 1.文件结尾 File file = new File("E:\01 软件开发\Java学习\01-java进阶\01 基础加强\day08_File类、递归\a.txt"); System.out.println(file); //此处注意 new是对象,打印是路径,说明重写了tostring方法 //2.文件夹结尾 File file2 = new File("E:\01 软件开发\Java学习\01-java进阶\01 基础加强\day08_File类、递归"); System.out.println(file2); //此处注意 new是对象,打印是路径,说明重写了tostring方法 E: 1 软件开发Java学习 1-java进阶 1 基础加强day08_File类、递归 //3. 相对路径 文件结尾 File file3 = new File("b.txt"); System.out.println(file3); //此处注意 new是对象,打印是路径,说明重写了tostring方法 b.txt }
切记,对象打印成路径,说明重写了toString方法,源码也确实如下调用getPath方法:
public String getPath() { return path; }
public String toString() { return getPath(); }
----------------------file类方法之获取方法
public String getAbsolutePath()
:返回此File的绝对路径名字符串。无论构造给的是啥都返回绝对路径
public String getPath()
:将此File转换为路径名字符串。 给啥路径返回啥路径,给绝对返回绝对,给相对返回相对
public String getName()
:返回由此File表示的文件或目录的名称。也就是返回的其实是路径的结尾部分
public long length()
:返回由此File表示的文件的长度。 也就是文件大小,以字节为单位,不能获取文件夹大小,文件夹没有大小,文件才有,否则返回0;
public class FileGet { public static void main(String[] args) { File f = new File("d:/aaa/bbb.java"); System.out.println("文件绝对路径:"+f.getAbsolutePath()); System.out.println("文件构造路径:"+f.getPath()); System.out.println("文件名称:"+f.getName()); System.out.println("文件长度:"+f.length()+"字节"); File f2 = new File("d:/aaa"); System.out.println("目录绝对路径:"+f2.getAbsolutePath()); System.out.println("目录构造路径:"+f2.getPath()); System.out.println("目录名称:"+f2.getName()); System.out.println("目录长度:"+f2.length()); } } 输出结果: 文件绝对路径:d:aaabb.java 文件构造路径:d:aaabb.java 文件名称:bbb.java 文件长度:636字节 目录绝对路径:d:aaa 目录构造路径:d:aaa 目录名称:aaa 目录长度:4096
API中说明:length(),表示文件的长度。但是File对象表示目录,则返回值未指定。
绝对路径和相对路径
public class FilePath { public static void main(String[] args) { // D盘下的bbb.java文件 File f = new File("D:\bbb.java"); System.out.println(f.getAbsolutePath()); // 项目下的bbb.java文件 File f2 = new File("bbb.java"); System.out.println(f2.getAbsolutePath()); } } 输出结果: D:bb.java D:idea_project_test4bb.java
----------------------file类方法之判断方法
public boolean exists()
:此File表示的文件或目录是否实际存在。判断构造方法路径是否存在
public boolean isDirectory()
:此File表示的是否为目录。 判断是否是文件夹结尾
public boolean isFile()
:此File表示的是否为文件。 判断是否是文件结尾public class FileIs { public static void main(String[] args) { File f = new File("d:\aaa\bbb.java"); File f2 = new File("d:\aaa"); // 判断是否存在 System.out.println("d:\aaa\bbb.java 是否存在:"+f.exists()); System.out.println("d:\aaa 是否存在:"+f2.exists()); // 判断是文件还是目录 System.out.println("d:\aaa 文件?:"+f2.isFile()); System.out.println("d:\aaa 目录?:"+f2.isDirectory()); } } 输出结果: d:aaabb.java 是否存在:true d:aaa 是否存在:true d:aaa 文件?:false d:aaa 目录?:true
----------------------file类方法之删除方法
public boolean createNewFile()
:当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。 路径必须存在,否则报错,而且文件若存在,不会创建,返回falsepublic boolean delete()
:删除由此File表示的文件或目录。不走回收站public boolean mkdir()
:创建由此File表示的单级目录。
public boolean mkdirs()
:创建由此File表示的目录,包括任何必需但不存在的父目录。 public class FileCreateDelete { public static void main(String[] args) throws IOException { // 文件的创建 File f = new File("aaa.txt"); System.out.println("是否存在:"+f.exists()); // false System.out.println("是否创建:"+f.createNewFile()); // true System.out.println("是否存在:"+f.exists()); // true // 目录的创建 File f2= new File("newDir"); System.out.println("是否存在:"+f2.exists());// false System.out.println("是否创建:"+f2.mkdir()); // true System.out.println("是否存在:"+f2.exists());// true // 创建多级目录 File f3= new File("newDira\newDirb"); System.out.println(f3.mkdir());// false File f4= new File("newDira\newDirb"); System.out.println(f4.mkdirs());// true // 文件的删除 System.out.println(f.delete());// true // 目录的删除 System.out.println(f2.delete());// true System.out.println(f4.delete());// false } }
---------------------目录的遍历
public String[] list()
:返回一个String数组,表示该File目录中的所有子文件或目录。遍历的是构造方法中给出目录,隐藏文件也可以拿出来
public File[] listFiles()
:返回一个File数组,表示该File目录中的所有的子文件或目录。 遍历是构造方法中给出目录
public class FileFor { public static void main(String[] args) { File dir = new File("d:\java_code"); //获取当前目录下的文件以及文件夹的名称。 String[] names = dir.list(); for(String name : names){ System.out.println(name); } //获取当前目录下的文件以及文件夹对象,只要拿到了文件对象,那么就可以获取更多信息 File[] files = dir.listFiles(); for (File file : files) { System.out.println(file); } } }
--------- .idea a.txt eesy_spring.iml pom.xml src target --------- D:CodeIDEAeesy_spring.idea D:CodeIDEAeesy_springa.txt D:CodeIDEAeesy_springeesy_spring.iml D:CodeIDEAeesy_springpom.xml D:CodeIDEAeesy_springsrc D:CodeIDEAeesy_spring arget
----------------跳过递归 文件过滤器 FileFilter
其实fileFilter过滤器是listFiles的重载方法,给出过滤器的要求,指定过滤。
java.io.FileFilter
是一个接口,是File的过滤器。 该接口的对象可以传递给File类的listFiles(FileFilter)
作为参数, 接口中只有一个方法。
boolean accept(File pathname)
:测试pathname是否应该包含在当前File目录中,符合则返回true。
分析:
accept
方法,参数为File,表示当前File下所有的子文件和子目录。保留住则返回true,过滤掉则返回false。保留规则:
listFiles(FileFilter)
返回的数组元素中,子文件对象都是符合条件的,可以直接打印。 ------------4 字节字符流
我们把数据的传输,可以看做是一种数据的流动,按照流动的方向,以内存为基准,分为输入input
和输出output
,即流向内存是输入流,流出内存的输出流。Java中I/O操作主要是指使用java.io
包下的内容,进行输入、输出操作。**输入**也叫做**读取**数据,**输出**也叫做作**写出**数据。
IO流根据数据的流向分为:**输入流**和**输出流**。
其他设备
上读取到内存
中的流。
内存
中写出到其他设备
上的流。 IO流根据数据的类型分为:**字节流**和**字符流**。
顶级父类IO流
IO流最先学习就是字节流,一个字节8个bit(二进制),它可以读取任意文件,
2.2 字节输出流【OutputStream】
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)
:将指定的字节输出流。 小贴士:
close方法,当完成流的操作时,必须调用此方法,释放系统资源。
由于OutputStream其是抽象,得看具体的实现类子类
fileOutPutStream
也就是说 构造方法创建一个fileoutputStream对象,会根据传递的文件或路径,创建一个空文件,会把fileoutputStream对象指向创建好的文件。
OutputStream
有很多子类,我们从最简单的一个子类开始。
java.io.FileOutputStream
类是文件输出流,用于将数据写出到文件。
构造方法
public FileOutputStream(File file)
:创建文件输出流以写入由指定的 File对象表示的文件。
public FileOutputStream(String name)
: 创建文件输出流以指定的名称写入文件。当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据。
*****写入原理 内存-硬盘
java程序找jvm虚拟机,虚拟机找os操作系统,操作系统调用写数据的方法,将数据写入到文件中。
public class FileOutputStreamConstructor throws IOException { public static void main(String[] args) { // 使用File对象创建流对象 File file = new File("a.txt"); FileOutputStream fos = new FileOutputStream(file); // 使用文件名称创建流对象 FileOutputStream fos = new FileOutputStream("b.txt"); } }
分为两种构造方法:使用文件名称创建字节流对象是最简单的,而使用file对象创建字节输出流较为复杂(需要先有个file对象)
切记创建 FileOutputStream存在文件找不到异常
字节输出流对象调用write存在IO异常,两者异常不同。建议try
public class OutPutStreamDemoOne { public static void main(String[] args) { try { //创建FileOutputStream的流对象,构造方法中传递相对路径的文件名称 FileOutputStream fos= new FileOutputStream("a.txt"); // 调用write方法,把数据写入到a.txt中 fos.write(97); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
字节输出流,写数据时候,会把10进制的整数97 会转换为二进制整数97 -1100001,也就是a;也就是说默认文件打开时候,会把字节转为字符供我们看,但如何转换就是下图:
如果写出127以内,查找ascii码表,其他找系统默认表,中文系统就是gbk
****一次写入多个字节
public static void main(String[] args) { try { //创建FileOutputStream的流对象,构造方法中传递相对路径的文件名称 FileOutputStream fos= new FileOutputStream("a.txt"); // 调用write方法,把数据写入到a.txt中 fos.write(97); fos.write(97); fos.write(97); 结果: aaa
write(byte[] b)
,每次可以写出数组中的数据,代码使用演示: 如果一次写多个字节:那么第一个字节是正数(0-127) 显示时候查询ascii码表;如果写的第一个字节是负数,那第一个字节会和第二个字节,组合成一个中文显示,查询系统默认码表gbk;
public class FOSWrite { public static void main(String[] args) throws IOException { // 使用文件名称创建流对象 FileOutputStream fos = new FileOutputStream("fos.txt"); // 字符串转换为字节数组 byte[] b = "黑马程序员".getBytes(); // 写出字节数组数据 fos.write(b); // 关闭资源 fos.close(); } } 输出结果: 黑马程序员
//创建FileOutputStream的流对象,构造方法中传递相对路径的文件名称 FileOutputStream fos= new FileOutputStream("a.txt"); // 调用write方法,把数据写入到a.txt中 byte[] bytes ={66,67,68,69}; fos.write(bytes);
byte[] bytes ={-66,-67,68,69};
fos.write(bytes);
窘DE
*************使用string类中的方法 getBytes() 把字符串改为字节数组,再用io流输出 utf-8中三个字节是一个中文,
gbk中两个字节是一个中文
//创建FileOutputStream的流对象,构造方法中传递相对路径的文件名称 FileOutputStream fos= new FileOutputStream("a.txt"); // 调用write方法,把数据写入到a.txt中 byte[] bytes1 = "你好".getBytes(StandardCharsets.UTF_8); System.out.println(Arrays.toString(bytes1));//[-28, -67, -96, -27, -91, -67] fos.write(bytes1);//你好
***************追加写数据,续写
数据追加续写
经过以上的演示,每次程序运行,创建输出流对象,都会清空目标文件中的数据。如何保留目标文件中数据,还能继续添加新数据呢?
public FileOutputStream(File file, boolean append)
: 创建文件输出流以写入由指定的 File对象表示的文件。public FileOutputStream(String name, boolean append)
: 创建文件输出流以指定的名称写入文件。这两个构造方法,参数中都需要传入一个boolean类型的值,true
表示追加数据,false
表示清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了,代码使用演示:
public class FOSWrite { public static void main(String[] args) throws IOException { // 使用文件名称创建流对象 FileOutputStream fos = new FileOutputStream("fos.txt",true); // 字符串转换为字节数组 byte[] b = "abcde".getBytes(); // 写出从索引2开始,2个字节。索引2是c,两个字节,也就是cd。 fos.write(b); // 关闭资源 fos.close(); } } 文件操作前:cd 文件操作后:cdabcde
写出换行
Windows系统里,换行符号是
。把
以指定是否追加续写了,代码使用演示:
public class FOSWrite { public static void main(String[] args) throws IOException { // 使用文件名称创建流对象 FileOutputStream fos = new FileOutputStream("fos.txt",true); // 定义字节数组 byte[] words = {97,98,99,100,101}; // 遍历数组 for (int i = 0; i < words.length; i++) { // 写出一个字节 fos.write(words[i]); // 写出一个换行, 换行符号转成数组写出 fos.write(" ".getBytes()); } // 关闭资源 fos.close(); } } 输出结果: a b c d e
和换行符
:
回车+换行
,即
;
换行
,即
;
回车
,即
。从 Mac OS X开始与Linux统**************字节输入流-其中stringbuffer和stringbuilder是
字符串缓冲区
java.io.InputStream
抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。
public void close()
:关闭此输入流并释放与此流相关联的任何系统资源。public abstract int read()
: 从输入流读取数据的下一个字节。
public int read(byte[] b)
: 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。 小贴士:
close方法,当完成流的操作时,必须调用此方法,释放系统资源。
java.io.FileInputStream
类是文件输入流,从文件中读取字节。
构造方法
FileInputStream(File file)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名。
FileInputStream(String name)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出FileNotFoundException
。
public class FileInputStreamConstructor throws IOException{ public static void main(String[] args) { // 使用File对象创建流对象 File file = new File("a.txt"); FileInputStream fos = new FileInputStream(file); // 使用文件名称创建流对象 FileInputStream fos = new FileInputStream("b.txt"); } }
读取字节数据
read
方法,每次可以读取一个字节的数据,提升为int类型,读取到文件末尾,返回-1
,代码使用演示:public class FISRead { public static void main(String[] args) throws IOException{ // 使用文件名称创建流对象 FileInputStream fis = new FileInputStream("read.txt"); // 读取数据,返回一个字节 int read = fis.read(); System.out.println((char) read); read = fis.read(); System.out.println((char) read); read = fis.read(); System.out.println((char) read); read = fis.read(); System.out.println((char) read); read = fis.read(); System.out.println((char) read); // 读取到末尾,返回-1 read = fis.read(); System.out.println( read); // 关闭资源 fis.close(); } } 输出结果: a b c d e -1
循环改进读取方式,代码使用演示:
public class FISRead { public static void main(String[] args) throws IOException{ // 使用文件名称创建流对象 FileInputStream fis = new FileInputStream("read.txt"); // 定义变量,保存数据 int b ; // 循环读取 while ((b = fis.read())!=-1) { System.out.println((char)b); } // 关闭资源 fis.close(); } } 输出结果: a b c d e
小贴士:
使用字节数组读取:read(byte[] b)
,每次读取b的长度个字节到数组中,返回读取到的有效字节个数,读取到末尾时,返回-1
,代码使用演示:
明确 方法参数byte[] 的作用是啥?起到缓冲作用,存储每次读取到的每个字节,数组长度一般是1024的整数倍
方法返回值int是啥意思?每次读取到的有效字节个数
public class FISRead { public static void main(String[] args) throws IOException{ // 使用文件名称创建流对象. FileInputStream fis = new FileInputStream("read.txt"); // 文件中为abcde // 定义变量,作为有效个数 int len ; // 定义字节数组,作为装字节数据的容器 byte[] b = new byte[2]; // 循环读取 while (( len= fis.read(b))!=-1) { // 每次读取后,把数组变成字符串打印 System.out.println(new String(b)); } // 关闭资源 fis.close(); } } 输出结果: ab cd ed
错误数据d
,是由于最后一次读取时,只读取一个字节e
,数组中,上次读取的数据没有被完全替换,所以要通过len
,获取有效的字节,代码使用演示:
public class FISRead { public static void main(String[] args) throws IOException{ // 使用文件名称创建流对象. FileInputStream fis = new FileInputStream("read.txt"); // 文件中为abcde // 定义变量,作为有效个数 int len ; // 定义字节数组,作为装字节数据的容器 byte[] b = new byte[2]; // 循环读取 while (( len= fis.read(b))!=-1) { // 每次读取后,把数组的有效字节部分,变成字符串打印 System.out.println(new String(b,0,len));// len 每次读取的有效字节个数 } // 关闭资源 fis.close(); } } 输出结果: ab cd e
一次读取一个字节的原理:
public static void main(String[] args) { try { //创建fileInputStream流对象,构造方法中绑定要读取的数据源 FileInputStream fis = new FileInputStream("a.txt");//你好 //使用fileInputStream对象的方法read,读取文件 byte[] bytes = new byte[2]; int read = fis.read(bytes); System.out.println(read);//长度2 System.out.println(bytes);//[B@74a14482 地址值 System.out.println(Arrays.toString(bytes));//[97, 98] } catch (Exception e) { e.printStackTrace(); }
System.out.println(Arrays.toString(bytes));//[97, 98] System.out.println(new String(bytes));//ab
如果定义的数组长度过长,输出容易有空格,浪费!!!如下解决
**************字节流练习:图片复制 字节可以复制任何数据因为都是字节存储
public class Copy { public static void main(String[] args) throws IOException { // 1.创建流对象 // 1.1 指定数据源 FileInputStream fis = new FileInputStream("D:\test.jpg"); // 1.2 指定目的地 FileOutputStream fos = new FileOutputStream("test_copy.jpg"); // 2.读写数据 // 2.1 定义数组 byte[] b = new byte[1024]; // 2.2 定义长度 int len; // 2.3 循环读取 while ((len = fis.read(b))!=‐1) { // 2.4 写出数据 fos.write(b, 0 , len); } // 3.关闭资源 fos.close(); fis.close(); } }
********************字符流-遇到中文字符会出现读取不完整线下,字符流专门处理文本文件
一个中文gbk占用2个字节,在utf-8中占用3个字节,而字节流read只能一次读取两个字节,会产生乱码。字符流一次读取一个字符,无论中文还是英文都可以正常读取。
java.io.read是字符输入流的最顶级父类,是一个抽象类,有一些共性的成员方法
其中inputStreamReader是转换流,本次重点学习fileReader,其用于读取字符文件的便捷类,此类构造方法假定默认字符编码和默认字节缓冲区大小是适当的。
fileReader extends inputStreamReader extends Reader
构造方法:创建一个fileReader对象,会把fileReader对象指向要读取的文件。
fileReader(String filename)创造一个新的fileReader,参数是读取文件的名称
fileReader(File file) 创造一个新的fileReader,参数是要读取文件的file对象
使用步骤:创造fileReader对象,构造方法中绑定要读取的数据源;
使用fileReader对象中的方法read读取文件
释放资源
其方法如下:
read()
read(char[] chars)
public class ReaderDemoOne { public static void main(String[] args) { try { //1.创建filereader对象,给定文件名称 FileReader fr = new FileReader("a.txt"); //2.调用方法读取文件 char[] chars = new char[1024]; int len=0; while ((len=fr.read(chars))!=-1){ System.out.println((char)len); System.out.println(chars); System.out.println(new String(chars)); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
****************字符输出流
writer 也是抽象类,写入字符流的顶级父类,子类必须实现的方法仅有 writer(char[] ,int ,int)和flush()、close();但是多数子类将重写此处定义的一些方法,以提供更高的效率或其他功能。
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()
关闭此流,但要先刷新它。 3.4 FileWriter类 继承outputStream 继承writer 切记它存在将数据write到内存缓存区中(有一段字符转为字节的过程),在用flush将内存缓冲区的数据刷新到文件中。而close方法也可以先把内存缓冲区中的数据刷新到文件再释放资源,因此有close可以没有flush;
java.io.FileWriter
类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。
构造方法
FileWriter(File file)
: 创建一个新的 FileWriter,给定要读取的File对象。FileWriter(String fileName)
: 创建一个新的 FileWriter,给定要读取的文件的名称。当你创建一个流对象时,必须传入一个文件路径,类似于FileOutputStream。
public class WriterDemo { public static void main(String[] args) { try { //1.创建fileWriter对象,构造方法中绑定要写入的数据的目的地 FileWriter fileWriter = new FileWriter("d.txt"); //2.使用fileWriter方法write,把数据写入到内存缓冲区中(字符转为字节的过程) fileWriter.write(97); //3.使用fileWriter方法flush 刷新数据到文件中 fileWriter.flush(); fileWriter.close(); } catch (IOException e) { e.printStackTrace(); } } }
关闭和刷新
因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要flush
方法了。
flush
:刷新缓冲区,流对象可以继续使用。
close
:先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。public class FWWrite { public static void main(String[] args) throws IOException { // 使用文件名称创建流对象 FileWriter fw = new FileWriter("fw.txt"); // 写出数据,通过flush fw.write('刷'); // 写出第1个字符 fw.flush(); fw.write('新'); // 继续写出第2个字符,写出成功 fw.flush(); // 写出数据,通过close fw.write('关'); // 写出第1个字符 fw.close(); fw.write('闭'); // 继续写出第2个字符,【报错】java.io.IOException: Stream closed fw.close(); } }
小贴士:即便是flush方法写出了数据,操作的最后还是要调用close方法,释放系统资源。
写出其他数据
write(char[] cbuf)
和 write(char[] cbuf, int off, int len)
,每次可以写出字符数组中的数据,用法类似FileOutputStream,代码使用演示: public class FWWrite { public static void main(String[] args) throws IOException { // 使用文件名称创建流对象 FileWriter fw = new FileWriter("fw.txt"); // 字符串转换为字节数组 char[] chars = "黑马程序员".toCharArray(); // 写出字符数组 fw.write(chars); // 黑马程序员 // 写出从索引2开始,2个字节。索引2是'程',两个字节,也就是'程序'。 fw.write(b,2,2); // 程序 // 关闭资源 fos.close(); } }
write(String str)
和 write(String str, int off, int len)
,每次可以写出字符串中的数据,更为方便,代码使用演示: public class FWWrite { public static void main(String[] args) throws IOException { // 使用文件名称创建流对象 FileWriter fw = new FileWriter("fw.txt"); // 字符串 String msg = "黑马程序员"; // 写出字符数组 fw.write(msg); //黑马程序员 // 写出从索引2开始,2个字节。索引2是'程',两个字节,也就是'程序'。 fw.write(msg,2,2); // 程序 // 关闭资源 fos.close(); } }
public class FWWrite { public static void main(String[] args) throws IOException { // 使用文件名称创建流对象,可以续写数据 FileWriter fw = new FileWriter("fw.txt",true); // 写出字符串 fw.write("黑马"); // 写出换行 fw.write(" "); // 写出字符串 fw.write("程序员"); // 关闭资源 fw.close(); } } 输出结果: 黑马 程序员
小贴士:字符流,只能操作文本文件,不能操作图片,视频等非文本文件。
当我们单纯读或者写文本文件时 使用字符流 其他情况使用字节流
IO异常的处理
JDK7前处理
之前的入门练习,我们一直把异常抛出,而实际开发中并不能这样处理,建议使用try...catch...finally
代码块,处理异常部分,代码使用演示:
需要注意的是 需要提高变量的作用域,进而使得在finally中可以使用,且需要赋值null,否则finally会报错
public class HandleException1 { public static void main(String[] args) { // 声明变量 FileWriter fw = null; try { //创建流对象 fw = new FileWriter("fw.txt"); // 写出数据 fw.write("黑马程序员"); //黑马程序员 } catch (IOException e) { e.printStackTrace(); } finally { try { if (fw != null) { fw.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
JDK7的处理(扩展知识点了解内容)
还可以使用JDK7优化后的try-with-resource
语句,该语句确保了每个资源在语句结束时关闭。所谓的资源(resource)是指在程序完成后,必须关闭的对象。切记空指针异常是因为某个对象为空,而空是不能调用方法的,进而造成了空指针异常。也就是说流对象创建在try后面括号里面,这样就自动释放,不用finally中再写,不用写一些代码,提高效率
格式:
try (创建流对象语句,如果多个,使用';'隔开) { // 读写数据 } catch (IOException e) { e.printStackTrace(); } 代码使用演示: public class HandleException2 { public static void main(String[] args) { // 创建流对象 try ( FileWriter fw = new FileWriter("fw.txt"); ) { // 写出数据 fw.write("黑马程序员"); //黑马程序员 } catch (IOException e) { e.printStackTrace(); } } }
JDK9的改进(扩展知识点了解内容)
JDK9中try-with-resource
的改进,对于**引入对象**的方式,支持的更加简洁。被引入的对象(也就是对象名),同样可以自动关闭,无需手动close,我们来了解一下格式。切记jdk9 创建对象流可能存在异常(需要throws),这样就比较麻烦些,相对还是jdk7比较好用。
改进前格式:
// 被final修饰的对象 final Resource resource1 = new Resource("resource1"); // 普通对象 Resource resource2 = new Resource("resource2"); // 引入方式:创建新的变量保存 try (Resource r1 = resource1; Resource r2 = resource2) { // 使用对象 }
改进后格式:
// 被final修饰的对象 final Resource resource1 = new Resource("resource1"); // 普通对象 Resource resource2 = new Resource("resource2"); // 引入方式:直接引入 try (resource1; resource2) { // 使用对象 }
改进后,代码使用演示:
public class TryDemo { public static void main(String[] args) throws IOException { // 创建流对象 final FileReader fr = new FileReader("in.txt"); FileWriter fw = new FileWriter("out.txt"); // 引入到try中 try (fr; fw) { // 定义变量 int b; // 读取数据 while ((b = fr.read())!=-1) { // 写出数据 fw.write(b); } } catch (IOException e) { e.printStackTrace(); } } }
换行符号 windows linux /n
***********属性集-其实也是集合 java.util.properties继承于hashtable
java.util.Properties
继承于Hashtable
,来表示一个持久的属性集。它使用键值结构存储数据,每个键及其对应值都是一个字符串。该类也被许多Java类使用,比如获取系统属性时,System.getProperties
方法就是返回一个Properties
对象。
properties继承hashtable,而hashtable继承于map集合,而hashtable最早期是单线程的双键集合,被hashMap取代而淘汰了,但其子类properties还活跃着。因为propertis是唯一和io流相结合的集合。可以使用properties集合中的方法store,把集合中的临时数据持久化写入到硬盘中存储。可以使用properties集合中的方法load,把硬盘中保存的文件(键值对),读取到集合中使用,且properties集合是一个双列集合,key和value默认都是字符串,这句话意思是创建properties对象时,不用给泛型,它自动默认string了。
构造方法
public Properties()
:创建一个空的属性列表。 基本的存储方法
public Object setProperty(String key, String value)
: 保存一对属性。底层是调用hashtable方法putpublic String getProperty(String key)
:使用此属性列表中指定的键搜索属性值。 底层调用的是map集合中get方法,只是map的get的key多类型,而此处是string key;public Set<String> stringPropertyNames()
:所有键的名称的集合。public class ProDemo { public static void main(String[] args) throws FileNotFoundException { // 创建属性集对象 Properties properties = new Properties(); // 添加键值对元素 properties.setProperty("filename", "a.txt"); properties.setProperty("length", "209385038"); properties.setProperty("location", "D:\a.txt"); // 打印属性集对象 System.out.println(properties); // 通过键,获取属性值 System.out.println(properties.getProperty("filename")); System.out.println(properties.getProperty("length")); System.out.println(properties.getProperty("location")); // 遍历属性集,获取所有键的集合 Set<String> strings = properties.stringPropertyNames(); // 打印键值对 for (String key : strings ) { System.out.println(key+" -- "+properties.getProperty(key)); } } } 输出结果: {filename=a.txt, length=209385038, location=D:a.txt} a.txt 209385038 D:a.txt filename -- a.txt length -- 209385038 location -- D:a.txt
与流相关的方法
public void load(InputStream inStream)
: 从字节输入流中读取键值对。 参数中使用了字节输入流,通过流对象,可以关联到某文件上,这样就能够加载文本中的数据了。文本数据格式:
filename=a.txt length=209385038 location=D:a.txt
public class ProDemo2 { public static void main(String[] args) throws FileNotFoundException { // 创建属性集对象 Properties pro = new Properties(); // 加载文本中信息到属性集 pro.load(new FileInputStream("read.txt")); // 遍历集合并打印 Set<String> strings = pro.stringPropertyNames(); for (String key : strings ) { System.out.println(key+" -- "+pro.getProperty(key)); } } } 输出结果: filename -- a.txt length -- 209385038 location -- D:a.txt
小贴士:文本中的数据,必须是键值对形式,可以使用空格、等号、冒号等符号分隔。
// 可以使用properties结合中方法store, 把集合中的临时数据持久化写入到硬盘中存储
// void store(outputStream out,String comments):字节输出流,不能写入中文,
comments是解释说明的注释,保存文件是干啥的,不能用中文
// void store(Writer writer,String comments):字符输出流,可以有中文,
comments是解释说明的注释,保存文件是干啥的,不能用中文
*******************properties 的load方法最为关键-读取文件键值对读取到properties集合中使用。
// 可以使用properties结合中方法load, 把集合中的临时数据持久化写入到硬盘中存储
// void store(inputStream input):字节输入流,不能写入中文,
// void store(reader reader):字符输入流,可以有中文,使用步骤
private static void showTwo() { // 可以使用properties结合中方法store, 把集合中的临时数据持久化写入到硬盘中存储 // void store(outputStream out,String comments):字节输出流,不能写入中文,comments是解释说明的注释,保存文件是干啥的,不能用中文 // void store(Writer writer,String comments):字符输出流,可以有中文,comments是解释说明的注释,保存文件是干啥的,不能用中文 Properties properties = new Properties(); properties.setProperty("123","1233"); properties.setProperty("456","4564"); properties.setProperty("789","7895"); Set<String> strings = properties.stringPropertyNames(); try { FileWriter fileWriter = new FileWriter("c.txt"); //添加备注 properties.store(fileWriter,"save data"); fileWriter.close(); } catch (IOException e) { e.printStackTrace(); }
------------5 缓冲流、转换流、序列化流、Files
一些更强大的流。比如能够高效读写的缓冲流,能够转换编码的转换流,能够持久化存储对象的序列化流等等。这些功能更为强大的流,都是在基本的流对象基础之上创建而来的,就像穿上铠甲的武士一样,相当于是对基本流对象的一种增强。
缓冲流,也叫高效流,是对4个基本的FileXxx
流的增强,所以也是4个流,按照数据类型分类:
BufferedInputStream
,BufferedOutputStream
BufferedReader
,BufferedWriter
缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,然后一次性返回给调用处,这样更快速,从而提高读写的效率。
构造方法
public BufferedInputStream(InputStream in)
:创建一个 新的缓冲输入流。我们传递一个fileinputstring流,缓冲区增加一个缓冲区,提供读取效率
public BufferedOutputStream(OutputStream out,int size)
: 创建一个新的缓冲输出流。 我们传递一个fileoutputstring流,缓冲区增加一个缓冲区,提供输出效率 ,int size 指定缓冲流内部缓冲区的大小,不指定默认。使用步骤:
public class BufferDemoOne { public static void main(String[] args) { showOne(); showTwo(); } private static void showTwo() { try { //FileOutputStream,构造方法中绑定读取的目录文件 FileOutputStream fos = new FileOutputStream("c.txt"); //BufferedOutputStream BufferedOutputStream bos = new BufferedOutputStream(fos); //使用FileOutputStream对象的方法read bos.write("把我写到缓存区中".getBytes(StandardCharsets.UTF_8)); //调用刷新到文件中 bos.flush(); bos.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } private static void showOne() { try { //创建fileinputStream流,绑定读取文件 FileInputStream fis = new FileInputStream("c.txt"); //创建bufferinputStream流对象,传递fileinputstream对象 BufferedInputStream bis = new BufferedInputStream(fis); byte[] bytes = new byte[1024]; int len=0;//len 会保存每个读取的字节数字 while((len=bis.read(bytes))!=-1){ System.out.println(len); System.out.println(new String(bytes,0,len)); } bis.close(); fis.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
****************字符缓冲流
构造方法
public BufferedReader(Reader in)
:创建一个 新的缓冲输入流。
public BufferedWriter(Writer out)
: 创建一个新的缓冲输出流。 构造举例,代码如下:
// 创建字符缓冲输入流 BufferedReader br = new BufferedReader(new FileReader("br.txt")); // 创建字符缓冲输出流 BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));
特有方法
字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。
public String readLine()
: 读一行文字。 行的终止符通过换行符号 回车或者回车后面跟着换行为止,读取不含换行符或回车,需要自己加public void newLine()
: 写一行行分隔符,由系统属性定义符号。 自动给予各自系统的换行符。readLine
方法演示,代码如下
public class BufferedReaderDemo { public static void main(String[] args) throws IOException { // 创建流对象 BufferedReader br = new BufferedReader(new FileReader("in.txt")); // 定义字符串,保存读取的一行文字 String line = null; // 循环读取,读取到最后返回null while ((line = br.readLine())!=null) { System.out.print(line); System.out.println("------"); } // 释放资源 br.close(); } }
newLine
方法演示,代码如下:
public class BufferedWriterDemo throws IOException { public static void main(String[] args) throws IOException { // 创建流对象 BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt")); // 写出数据 bw.write("黑马"); // 写出换行 bw.newLine(); bw.write("程序"); bw.newLine(); bw.write("员"); bw.newLine(); // 释放资源 bw.close(); } } 输出效果: 黑马 程序 员
3.侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必得裨补阙漏,有所广益。 8.愿陛下托臣以讨贼兴复之效,不效,则治臣之罪,以告先帝之灵。若无兴德之言,则责攸之、祎、允等之慢,以彰其咎;陛下亦宜自谋,以咨诹善道,察纳雅言,深追先帝遗诏,臣不胜受恩感激。 4.将军向宠,性行淑均,晓畅军事,试用之于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行阵和睦,优劣得所。 2.宫中府中,俱为一体,陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。 1.先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。 9.今当远离,临表涕零,不知所言。 6.臣本布衣,躬耕于南阳,苟全性命于乱世,不求闻达于诸侯。先帝不以臣卑鄙,猥自枉屈,三顾臣于草庐之中,咨臣以当世之事,由是感激,遂许先帝以驱驰。后值倾覆,受任于败军之际,奉命于危难之间,尔来二十有一年矣。 7.先帝知臣谨慎,故临崩寄臣以大事也。受命以来,夙夜忧叹,恐付托不效,以伤先帝之明,故五月渡泸,深入不毛。今南方已定,兵甲已足,当奖率三军,北定中原,庶竭驽钝,攘除奸凶,兴复汉室,还于旧都。此臣所以报先帝而忠陛下之职分也。至于斟酌损益,进尽忠言,则攸之、祎、允之任也。
public class BufferedTest { public static void main(String[] args) throws IOException { // 创建map集合,保存文本数据,键为序号,值为文字 HashMap<String, String> lineMap = new HashMap<>(); // 创建流对象 BufferedReader br = new BufferedReader(new FileReader("in.txt")); BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt")); // 读取数据 String line = null; while ((line = br.readLine())!=null) { // 解析文本 String[] split = line.split("\."); // 保存到集合 lineMap.put(split[0],split[1]); } // 释放资源 br.close(); // 遍历map集合 for (int i = 1; i <= lineMap.size(); i++) { String key = String.valueOf(i); // 获取map中文本 String value = lineMap.get(key); // 写出拼接文本 bw.write(key+"."+value); // 写出换行 bw.newLine(); } // 释放资源 bw.close(); } }
*************学习转换流前要学习下字符集编码问题********
们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为**编码** 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为**解码**
字符编码Character Encoding
: 就是一套自然语言的字符与二进制数之间的对应规则。
编码表:生活中文字和计算机中二进制的对应规则
字符集
Charset
:也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。 计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有ASCII(美国)字符集、GBK字符集、Unicode字符集等。
ascii码表基础用1个字节中7位bit表示数据,因为是正数,而负数就前面第一位表示负数。gbk最新是gbk18030,多字节编码,一般gbk是默认2个字节,最新支持4个字节表示。
unicode字符集:也可以使用4个字节表示字母,符号或文字。一般用utf-8;
*********编码引出的问题
在IDEA中,使用FileReader
读取项目中的文本文件。由于IDEA的设置,都是默认的UTF-8
编码,所以没有任何问题。但是,当读取Windows系统中创建的文本文件时,由于Windows系统的默认是GBK编码,就会出现乱码。
public class ReaderDemo { public static void main(String[] args) throws IOException { FileReader fileReader = new FileReader("E:\File_GBK.txt"); int read; while ((read = fileReader.read()) != -1) { System.out.print((char)read); } fileReader.close(); } } 输出结果: ???
那么如何读取GBK编码的文件呢? 是InputStreamReader类
InputStreamReader类是字节流通向字符流的桥梁,可以指定charset字符集读取字节并将其解码为字符。
转换流java.io.InputStreamReader
,是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。
构造方法
InputStreamReader(InputStream in)
: 创建一个使用默认字符集的字符流。
InputStreamReader(InputStream in, String charsetName)
: 创建一个指定字符集的字符流。 构造举例,代码如下:
InputStreamReader isr = new InputStreamReader(new FileInputStream("in.txt")); InputStreamReader isr2 = new InputStreamReader(new FileInputStream("in.txt") , "GBK"); 指定编码输出 public class ReaderDemo2 { public static void main(String[] args) throws IOException { // 定义文件路径,文件为gbk编码 String FileName = "E:\file_gbk.txt"; // 创建流对象,默认UTF8编码 InputStreamReader isr = new InputStreamReader(new FileInputStream(FileName)); // 创建流对象,指定GBK编码 InputStreamReader isr2 = new InputStreamReader(new FileInputStream(FileName) , "GBK"); // 定义变量,保存字符 int read; // 使用默认编码字符流读取,乱码 while ((read = isr.read()) != -1) { System.out.print((char)read); // ???? } isr.close(); // 使用指定编码字符流读取,正常解析 while ((read = isr2.read()) != -1) { System.out.print((char)read);// 大家好 } isr2.close(); } }
OutputStreamWriter类
转换流java.io.OutputStreamWriter
,是Writer的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。 既可以查询ide默认码表,也可以指定,两者是IO流中转换流的关键所在,底层也是调用字符字节写出。
构造方法
OutputStreamWriter(OutputStream in)
: 创建一个使用默认字符集的字符流。
OutputStreamWriter(OutputStream in, String charsetName)
: 创建一个指定字符集的字符流。 构造举例,代码如下:
OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream("out.txt")); OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream("out.txt") , "GBK"); 指定编码写出 public class OutputDemo { public static void main(String[] args) throws IOException { // 定义文件路径 String FileName = "E:\out.txt"; // 创建流对象,默认UTF8编码 OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(FileName)); // 写出数据 osw.write("你好"); // 保存为6个字节 osw.close(); // 定义文件路径 String FileName2 = "E:\out2.txt"; // 创建流对象,指定GBK编码 OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream(FileName2),"GBK"); // 写出数据 osw2.write("你好");// 保存为4个字节 osw2.close(); }
public class TransDemo { public static void main(String[] args) { // 1.定义文件路径 String srcFile = "file_gbk.txt"; String destFile = "file_utf8.txt"; // 2.创建流对象 // 2.1 转换输入流,指定GBK编码 InputStreamReader isr = new InputStreamReader(new FileInputStream(srcFile) , "GBK"); // 2.2 转换输出流,默认utf8编码 OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(destFile)); // 3.读写数据 // 3.1 定义数组 char[] cbuf = new char[1024]; // 3.2 定义长度 int len; // 3.3 循环读取 while ((len = isr.read(cbuf))!=-1) { // 循环写出 osw.write(cbuf,0,len); } // 4.释放资源 osw.close(); isr.close(); } }
*************序列化流和反序列化流
Person p = newPerson(“123”,18)
把对象以流的方式,写入到文件中保存,叫写对象,也叫对象的序列化,对象中包含的不仅仅是字符,因此我们要使用字节流
objectOutputStream:对象的序列化流 使用方法 writeObject(p)
把文件中保存的对象,以流的方式读取出来,叫做读对象,也叫对象的反序列化,读取的文件保存都是字节,使用字节流。
objectinputStream:对象的反序列化流 使用方法 readObject()';
转换图如下:
虽然在序列化的帮助下,他们能够使用信号而不是实际的太空船发送数据,他们意识到发送大量数据仍然是一个挑战。 序列化使流程更便宜,更快,但仍然很慢。 因此,不同的科学家提出了不同的想法来减少数据量。 一些科学家建议压缩数据,有些建议使用不同的机制来表示它,以便可以反序列化。比如XML,JSON,msgpack等。
序列化是一种用来处理对象流
的机制。
所谓对象流:就是将对象的内容进行流化。可以对流化后的对象进行读写操作
,也可将流化后的对象传输于网络
之间。
序列化是为了解决在对对象流进行读写操作时所引发的问题。
序列化的实现:将需要被序列化的类实现Serializable接口(标记接口)
,该接口没有需要实现的方法,implements Serializable只是为了标注
该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象;接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。
什么时候使用序列化呢?
实现分布式对象
。主要应用例如:
RMI(即远程调用Remote Method Invocation)
要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象时一样。
可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的"深复制",即
复制对象本身及引用的对象本身
。序列化一个对象可能得到整个对象序列。
比如:将某个类序列化后存为文件,下次读取时只需将文件中的数据
反序列化
就可以将原先的类还原到内存中。也可以将类序列化为流数据进行传输。总的来说就是将一个已经实例化的类转成文件存储
,下次需要实例化的时候只要反序列化即可将类实例化到内存中并保留序列化时类中的所有变量和状态。
序列化以后就都是字节流了,无论原来是什么东西,都能
变成一样的东西
,就可以进行通用的格式传输或保存,传输结束以后,要再次使用,就进行反序列化还原,这样对象还是对象,文件还是文件 因为JAVA中要将对象序列化为 流的形式
进行传输。
所以涉及到数据传输或者存储的类,严格意义上来说都要加上序列化ID,这也是一种良好的编程习惯
。 public class ObjectDemoOne { public static void main(String[] args) { showOne(); showTwo(); } private static void showTwo() { try { ObjectInputStream ois= new ObjectInputStream(new FileInputStream("a.txt")); Object o = ois.readObject(); System.out.println(o); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } private static void showOne() { try { //创建objectoutputStream对象,构造方法中传递字节输出流 ObjectOutputStream os=new ObjectOutputStream(new FileOutputStream("a.txt")); os.writeObject(new Person("123",1)); System.out.println(new Person("123",1)); os.close(); } catch (IOException e) { e.printStackTrace(); } } }
对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个 ClassNotFoundException
异常。
反序列化操作2
另外,当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException
异常。发生这个异常的原因如下:
Serializable
接口给需要序列化的类,提供了一个序列版本号。serialVersionUID
该版本号的目的在于验证序列化的对象和对应类是否版本匹配。
public class Employee implements java.io.Serializable { // 加入序列版本号 private static final long serialVersionUID = 1L; public String name; public String address; // 添加新的属性 ,重新编译, 可以反序列化,该属性赋为默认值. public int eid; public void addressCheck() { System.out.println("Address check : " + name + " -- " + address); } }
Person o = (Person) ois.readObject(); 强转类型。
*************static静态关键字与transitent瞬态关键字*********
static关键字是静态的,不属于对象,被static修饰的关键字是优先于非静态加载到内存中,也就是优先于对象加载到内存中,且static关键字不能被序列化,序列化的都是对象
tansient关键字修饰的变量也不能被序列化,但没有static其他功能。
序列化集合-当我们想在文件中保存多个对象时候,可以把多个对象放在集合中,对集合进行序列化和反序列化。
list.txt
文件中。
list.txt
,并遍历集合,打印对象信息。 案例分析
案例实现
public class SerTest { public static void main(String[] args) throws Exception { // 创建 学生对象 Student student = new Student("老王", "laow"); Student student2 = new Student("老张", "laoz"); Student student3 = new Student("老李", "laol"); ArrayList<Student> arrayList = new ArrayList<>(); arrayList.add(student); arrayList.add(student2); arrayList.add(student3); // 序列化操作 // serializ(arrayList); // 反序列化 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("list.txt")); // 读取对象,强转为ArrayList类型 ArrayList<Student> list = (ArrayList<Student>)ois.readObject(); for (int i = 0; i < list.size(); i++ ){ Student s = list.get(i); System.out.println(s.getName()+"--"+ s.getPwd()); } } private static void serializ(ArrayList<Student> arrayList) throws Exception { // 创建 序列化流 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.txt")); // 写出对象 oos.writeObject(arrayList); // 释放资源 oos.close(); } }
平时我们在控制台打印输出,是调用print
方法和println
方法完成的,这两个方法都来自于java.io.PrintStream
类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。
4.2 PrintStream类
构造方法
public PrintStream(String fileName)
: 使用指定的文件名创建一个新的打印流。 构造举例,代码如下:
PrintStream ps = new PrintStream("ps.txt");
改变打印流向
System.out
就是PrintStream
类型的,只不过它的流向是系统规定的,打印在控制台上。不过,既然是流对象,我们就可以玩一个"小把戏",改变它的流向。
public class PrintDemo {
public static void main(String[] args) throws IOException {
// 调用系统的打印流,控制台直接输出97
System.out.println(97);
// 创建打印流,指定文件的名称
PrintStream ps = new PrintStream("ps.txt");
// 设置系统的打印流流向,输出到ps.txt
System.setOut(ps);
// 调用系统的打印流,ps.txt中输出97
System.out.println(97);
}
}
-------------6 网络编程
******网络编程之软件结构 cs/bs
******网络编程之网络通信协议 tcp/ip (有连接) 和 udp(无连接,耗资小)
网络层:网络层是整个TCP/IP协议的核心,它主要用于将传输的数据进行分组,将分组数据发送到目标计算机或者网络。
在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”。
? 完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等。
***************网络编程三要素 ip 端口 协议
常用命令
ipconfig
ping 空格 IP地址
ping 220.181.57.216
端口号
网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?
如果说**IP地址**可以唯一标识网络中的设备,那么**端口号**就可以唯一标识设备中的进程(应用程序)了。
利用协议
+IP地址
+端口号
三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。
TCP能够实现两台计算机之间的数据交互,通信两端要严格区分客户端client和服务器端server:通信步骤如下:
1.服务器端程序,需要先与客户端启动,等待客户端连接-服务器端不会主动,经过三次握手,这也就是tcp安全的地方
2.客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。
需要注意的是:客户端client和服务端server建立的逻辑连接其实是一个对象,这个对象就是IO对象,两端通信不仅仅是字符,因此是字节流对象。实际上一来一回两个端,需要4个IO流对象
又由于多个客户端可以和服务器同时交互,服务器端需要明确哪个客户端,服务器端有个方法accept,可以区分客户端对象。服务器没有IO流对象,服务器使用客户端的字节输入流读取客户端发送过来的数据,服务器使用客户端字节输出流发送给客户端回写数据。
在Java中,提供了两个类用于实现TCP通信程序,由于是网络编程,因此是在net包下的:
java.net.Socket
类表示。创建Socket
对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。
java.net.ServerSocket
类表示。创建ServerSocket
对象,相当于开启一个服务,并等待客户端的连接。 Socket类
Socket
类:该类实现客户端套接字,套接字指的是两台设备之间通讯的端点。套接字就是包含了ip地址和端口号的网络单位,从下面构造方法就知道,套接字对象包含ip地址和端口号值。
构造方法
public Socket(String host, int port)
:创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的host是null ,则相当于指定地址为回送地址。
小贴士:回送地址(127.x.x.x) 是本机回送地址(Loopback Address),主要用于网络软件测试以及本地机进程间通信,无论什么程序,一旦使用回送地址发送数据,立即返回,不进行任何网络传输。
构造举例,代码如下:
Socket client = new Socket("127.0.0.1", 6666);
成员方法
public InputStream getInputStream()
: 返回此套接字的输入流。进行通信
public OutputStream getOutputStream()
: 返回此套接字的输出流。 进行通信
public void close()
:关闭此套接字。 也就是关闭流
public void shutdownOutput()
: 禁用此套接字的输出流。
使用步骤:
1创建一个客户端对象 socket,构造方法中绑定服务器的ip地址和端口号(Connection refused: connect)切记如果服务器端未启动,则此处未来执行时候会报异常,因为这一步本身就包含了三次握手操作。
2.使用客户端对象scoket对象的方法getOutputStream()获取网络字节输出流outputStream对象。
3.使用网络字节输出流outputStream对象中的方法write,给服务器端发送数据。
4.使用socket对象中方法getInputStream()获取网络字节输入流inputStream对象。
5.使用网络 字节输入流inputStream对象中的方法read,读取服务器回写数据。
6.关闭socket(因为流从socket获取,关他即可)
/** * tcp通信的客户端:向服务器发送链接请求,读取服务器回写数据 */ public class TcpClient { public static void main(String[] args) { //1.创建一个客户端对象socket,构造方法中绑定服务器的ip地址和端口号 Socket socket = null; try { socket = new Socket("127.0.0.1",8888); //2.创建socket对象中的方法getOutputStream()的write,给服务器发送数据 OutputStream outputStream = socket.getOutputStream(); outputStream.write("你好,服务器".getBytes(StandardCharsets.UTF_8)); InputStream inputStream = socket.getInputStream(); byte[] bytes = new byte[1024]; int read = inputStream.read(bytes); System.out.println(new String(bytes,0,read)); } catch (IOException e) { e.printStackTrace(); }finally { if (socket!=null){ try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }}
2.3 ServerSocket类
ServerSocket
类:这个类实现了服务器套接字,该对象等待通过网络的请求。
构造方法
public ServerSocket(int port)
:使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上,参数port就是端口号。 为什么要指定端口号,因为端口号随机一变,客户端就无法正常访问到,因此要指定。构造举例,代码如下:
ServerSocket server = new ServerSocket(6666);
成员方法
public Socket accept()
:侦听并接受连接,返回一个新的Socket对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。使用步骤:
1.创建服务器serversocket对象,且和系统要指定的端口号
2.使用serversocket对象中的方法accept,获取到请求的客户端对象socket
3.由于服务器端与客户端交流,因此流是相通的。使用socket对象中的方法getinputStream()获取网络字节流inputStream对象。
4.使用网络字节流inputStream对象中的方法read,读取客户端发送的数据。
5.使用socket对象中的方法getOutputStream对象获取outputStream对象。
6.使用网络字节输出流outputStream对象的方法write,回写给客户端的数据
7.关闭资源 socket和serversocket.closed();
public class TcpServer { public static void main(String[] args) { try { //1.创建服务器对象 ServerSocket ss= new ServerSocket(8888); //2.使用服务器对象获取客户端对象scoket Socket accept = ss.accept(); //3.使用socket对象中方法 getinputStream InputStream is = accept.getInputStream(); //4.使用is对象中方法read读取客户端发送的数据 byte[] bytes = new byte[1024]; int read = is.read(bytes); System.out.println(new String(bytes,0, read)); //5.获取getOuputStream OutputStream outputStream = accept.getOutputStream(); outputStream.write("你好,客户端".getBytes(StandardCharsets.UTF_8)); //6.关闭资源 accept.close(); ss.close(); } catch (IOException e) { e.printStackTrace(); } } }
*************文件上传下载案例************
一句话:客户端读取本地的文件,把文件上传到服务器,服务器再把上传的文件保存到服务器的硬盘上
public class ClientUpload { public static void main(String[] args) { try { //1.读取本地数据,使用本地流,创建本地输入流对象inputStream,构造方法中绑定要读取的数据源 FileInputStream fileInputStream = new FileInputStream("a.txt"); //2.创建客户端socket对象,绑定服务器的ip地址和端口号 Socket socket = new Socket("127.0.0.1", 8889); //3.使用socket对象的网络输出流,传递输出的数据,也就是文件 byte[] bytes = new byte[1024 * 1024]; OutputStream outputStream = socket.getOutputStream(); int len=0; while ((len=fileInputStream.read(bytes))!=-1){ outputStream.write(bytes,0,len); } //4.使用socket对象的网络输入流,读取服务的回写数据 InputStream inputStream = socket.getInputStream(); while ((len=inputStream.read(bytes))!=-1){ System.out.println(new String(bytes,0,len)); } inputStream.close(); outputStream.close(); socket.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
public class ServerDownLoad { public static void main(String[] args) { try { //1.创建本地流,绑定本地文件夹,给予输出流文件写出目的地 FileOutputStream fileOutputStream = new FileOutputStream("g.txt"); //2.创建服务端套接字对象,锁定要端口 ServerSocket serverSocket = new ServerSocket(8889); //3.获取客户端套接字的对象 Socket accept = serverSocket.accept(); //4.使用客户端对象的方法getInputStream() InputStream inputStream = accept.getInputStream(); byte[] bytes = new byte[1024 * 1024]; int len=0; while ((len=inputStream.read(bytes))!=-1){ //5.使用本地流,写出数据 fileOutputStream.write(bytes,0,len); } //6.使用客户端对象的getOutputStream方法,回写数据 OutputStream outputStream = accept.getOutputStream(); outputStream.write("感谢,已上传成功".getBytes(StandardCharsets.UTF_8)); serverSocket.close(); fileOutputStream.close(); accept.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
注意:常规写法,容易进入while死循环,原因是根本不会告知服务器-1,也就是结束标记未知,那么释放资源就不会结束。因此需要套接字里面的shutdowmout()方法
**********上传文件名写死的问题
文件名称写死的问题
服务端,保存文件的名称如果写死,那么最终导致服务器硬盘,只会保留一个文件,建议使用系统时间优化,保证文件名称唯一,代码如下:
FileOutputStream fis = new FileOutputStream(System.currentTimeMillis()+
".jpg") // 文件名称
BufferedOutputStream bos = new BufferedOutputStream(fis);
循环接收的问题
服务端,指保存一个文件就关闭了,之后的用户无法再上传,这是不符合实际的,使用循环改进,可以不断的接收不同用户的文件,代码如下:
// 每次接收新的连接,创建一个Socket
while(true){
Socket accept = serverSocket.accept();
......
}
效率问题
服务端,在接收大文件时,可能耗费几秒钟的时间,此时不能接收其他用户上传,所以,使用多线程技术优化,代码如下:
while(true){
Socket accept = serverSocket.accept();
// accept 交给子线程处理.
new Thread(() -> {
......
InputStream bis = accept.getInputStream();
......
}).start();
}
*************************多线程技术应用
public class FileUpload_Server { public static void main(String[] args) throws IOException { System.out.println("服务器 启动..... "); // 1. 创建服务端ServerSocket ServerSocket serverSocket = new ServerSocket(6666); // 2. 循环接收,建立连接 while (true) { Socket accept = serverSocket.accept(); /* 3. socket对象交给子线程处理,进行读写操作 Runnable接口中,只有一个run方法,使用lambda表达式简化格式 */ new Thread(() -> { try ( //3.1 获取输入流对象 BufferedInputStream bis = new BufferedInputStream(accept.getInputStream()); //3.2 创建输出流对象, 保存到本地 . FileOutputStream fis = new FileOutputStream(System.currentTimeMillis() + ".jpg"); BufferedOutputStream bos = new BufferedOutputStream(fis);) { // 3.3 读写数据 byte[] b = new byte[1024 * 8]; int len; while ((len = bis.read(b)) != -1) { bos.write(b, 0, len); } //4. 关闭 资源 bos.close(); bis.close(); accept.close(); System.out.println("文件上传已保存"); } catch (IOException e) { e.printStackTrace(); } }).start(); } } }
-------------7 函数式接口-当我们定义某个接口只有一个抽象方法就叫函数式接口
函数式接口在Java中是指:有且仅有一个抽象方法的接口。函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导
注:“语法糖”是指使用更加方便,但是原理不变的代码语法。例如在遍历集合时使用的for-each语法,其实底层的实现原理仍然是迭代器,这便是“语法糖”。从应用层面来讲,Java中的Lambda可以被当做是匿名内部类的“语法糖”,但是二者在原理上是不同的。
格式:
修饰符 interface 接口名称 { public abstract 返回值类型 方法名称(可选参数信息); // 其他非抽象方法内容 }
public interface MyFunctionalInterface { void myMethod(); }
其他方法 默认方法 静态方法 私有方法是都可以有的,但是只有一个抽象方法。
@FunctionalInterface注解加载在接口上 就是校验是一个函数式接口
有且仅有一个抽象方法,才能通过。
*******使用函数式接口
第一使用:函数式接口作为方法的参数和返回值类使用
public class Demo { public static void show(MyFunctionalInterface myFunctionalInterface){ myFunctionalInterface.method(); } public static void main(String[] args) { //调用show方法,传入接口实现类对象,匿名对象 show(new MyFunctionIInterFaceImpl()); show(new MyFunctionalInterface() { @Override public void method() { System.out.println("使用匿名内部类重写接口中的抽象方法"); } }); show(()->{ System.out.println("使用lambada表达式实现函数式接口"); }); } }
***********函数式编程就是使用我们lambada表达式或者方法引用带来的简化编程代码工作。
首先学习lambada表达式的延迟执行
******lambada表达式延迟执行
有些代码执行后,不一定立即使用结果,从而造成性能浪费,而lambada表达式是延迟执行的,这正好可以作为解决方法,提升性能。
应用场景1:性能浪费的日志案例,
日志是可以帮我们快速定位问题,记录程序运行过程中的情况,以便项目的监控和优化,一种典型场景就是对参数有条件的使用,例如日志消息的拼接后,在满足条件的情况下进行打印输出
public class LambadaDemoOne { public static void main(String[] args) { String msg1="hello"; String msg2="world"; String msg3="java"; showOne(2,msg1+msg2+msg3); } private static void showOne(int level, String message) { if (level==1){ System.out.println(message); } } }
package com.itheima.lambada; public class LambadaDemoOne { public static void main(String[] args) { String msg1="hello"; String msg2="world"; String msg3="java"; showOne(2,msg1+msg2+msg3); //lambada表达式 showTwo(1,()->{ return msg1+msg2+msg3; }); //匿名内部类方式 showTwo(1, new MessageBuilder() { @Override public String builderMessage() { return msg1+msg2+msg3; } }); } private static void showTwo(int level, MessageBuilder messageBuilder) { if (level==1){ System.out.println(messageBuilder.builderMessage()); } } private static void showOne(int level, String message) { if (level==1){ System.out.println(message); } } }
传统方法中,传递参数时,拼接字符串存在白拼接,浪费系统内存资源。
******lambada表达式作为参数和返回值-当方法参数是一个函数式接口类型
***********lambada作为函数返回值类型-比匿名内部类简单的多
如果一个方法的返回值类型是一个函数式接口,那么就可以直接返回一个Lambda表达式。当需要通过一个方法来获取一个 java.util.Comparator 接口类型的对象作为排序器时,就可以调该方法获取。
************常用的函数式接口
JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在 java.util.function 包中被提供。下面是最简单的几个接口及使用示例。
***java.util.function.Supplier 《T》接口仅包含一个无参的方法: T get() 。用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。 未指定泛型意思是,你给啥泛型 就是返回啥泛型,get无参 泛型通过。supplier表示生产型接口,给啥泛型返回啥泛型。
使用 Supplier 接口作为方法参数类型,通过Lambda表达式求出int数组中的最大值。提示:接口的泛型请使用 java.lang.Integer 类。
***********java.util.function.Consumer 接口则正好与Supplier接口相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型决定。
Consumer 接口中包含抽象方法 void accept(T t) ,意为消费一个指定泛型的数据。基本使用如
import java.util.function.Consumer; public class Demo09Consumer { private static void consumeString(Consumer<String> function) { function.accept("Hello"); } public static void main(String[] args) { consumeString(s ‐> System.out.println(s)); } }
默认方法:andThen 如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的default方法 andThen 。下面是JDK的源代码:
备注: java.util.Objects 的 requireNonNull 静态方法将会在参数为null时主动抛出 NullPointerException 异常。这省去了重复编写if语句和抛出空指针异常的麻烦。
**********练习
下面的字符串数组当中存有多条信息,请按照格式“ 姓名:XX。性别:XX。 ”的格式将信息打印出来。要求将打印姓名的动作作为第一个 Consumer 接口的Lambda实例,将打印性别的动作作为第二个 Consumer 接口的Lambda实例,将两个 Consumer 接口按照顺序“拼接”到一起。
public static void main(String[] args) { String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男" }; }
import java.util.function.Consumer; public class DemoConsumer { public static void main(String[] args) { String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男" }; printInfo(s ‐> System.out.print("姓名:" + s.split(",")[0]), s ‐> System.out.println("。性别:" + s.split(",")[1] + "。"), array); } private static void printInfo(Consumer<String> one, Consumer<String> two, String[] array) { for (String info : array) { one.andThen(two).accept(info); // 姓名:迪丽热巴。性别:女。 } } }
***Predicate接口有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用 java.util.function.Predicate 接口。抽象方法:test Predicate 接口中包含一个抽象方法: boolean test(T t) 。用于条件判断的场景:
import java.util.function.Predicate; public class Demo15PredicateTest { private static void method(Predicate<String> predicate) { boolean veryLong = predicate.test("HelloWorld"); System.out.println("字符串很长吗:" + veryLong); } public static void main(String[] args) { method(s ‐> s.length() > 5); } }
条件判断的标准是传入的Lambda表达式逻辑,只要字符串长度大于5则认为很长。
默认方法:and 既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个 Predicate 条件使用“与”逻辑连接起来实现“并且”的效果时,可以使用default方法 and 。其JDK源码为:
default Predicate<T> and(Predicate<? super T> other) { Objects.requireNonNull(other); return (t) ‐> test(t) && other.test(t); } 如果要判断一个字符串既要包含大写“H”,又要包含大写“W”,那么: import java.util.function.Predicate; public class Demo16PredicateAnd { private static void method(Predicate<String> one, Predicate<String> two) { boolean isValid = one.and(two).test("Helloworld"); System.out.println("字符串符合要求吗:" + isValid); } public static void main(String[] args) { method(s ‐> s.contains("H"), s ‐> s.contains("W")); } }
-------------8 Stream流、方法引用
*****stream流不同于IO流(专注读取和输出)-stream流目的将集合或数组进行处理,简化操作。当然要将集合或数组转为stream流
传统集合的多步遍历代码几乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如:
public class Demo01ForEach { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("张无忌"); list.add("周芷若"); list.add("赵敏"); list.add("张强"); list.add("张三丰"); for (String name : list) { System.out.println(name); } }
试想一下,如果希望对集合中的元素进行筛选过滤:
1. 将集合A根据条件一过滤为子集B;
2. 然后再根据条件二过滤为子集C。
那怎么办?在Java 8之前的做法可能为:
public class StreamDemoOne { public static void main(String[] args) { //创建List集合,存储姓名 List<String> list = new ArrayList<>(); list.add("张无忌"); list.add("张三丰"); list.add("张华"); list.add("赵敏"); list.add("尹天仇"); list.add("柳飘飘"); //开始过滤,含有张的元素,存储到一个集合中 List<String> listTwo = new ArrayList<>(); for (String name : list) { if (name.startsWith("张")){ listTwo.add(name);//[张无忌, 张三丰, 张华] } } //继续过滤,含有张且名字三个字的,存储到一个集合中 List<String> listThree = new ArrayList<>(); for (String name : listTwo) { if (name.length()==3){ listThree.add(name);//[张无忌, 张三丰] } } } }
每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循环是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使用另一个循环从头开始。那,Lambda的衍生物Stream能给我们带来怎样更加优雅的写法呢?
public class Demo03StreamFilter { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("张无忌"); list.add("周芷若"); list.add("赵敏"); list.add("张强"); list.add("张三丰"); list.stream() .filter(s ‐> s.startsWith("张")) .filter(s ‐> s.length() == 3) .forEach(System.out::println); } }
按照stream方式进行过滤,其中filter参数就是Predicate,就是过滤器意思,函数式接口,因此给予lambada表达式,然后继续链式编程,最后以collect(Collectors.toList)结束。
public class StreamDemoTwo { public static void main(String[] args) { //创建List集合,存储姓名 List<String> list = new ArrayList<>(); list.add("张无忌"); list.add("张三丰"); list.add("张华"); list.add("赵敏"); list.add("尹天仇"); list.add("柳飘飘"); //stream 流的写法 List<String> name = list.stream().filter(s -> s.startsWith("张")).filter(s -> s.length() == 3).collect(Collectors.toList()); System.out.println(name.toString());//[张无忌, 张三丰] } }
过滤、映射、跳过、计数等多步操作,这是一种集合元素的处理方案,而方案就是一种“函数模型”。图中的每一个方框都是一个“流”,调用指定的方法,可以从一个流模型转换为另一个流模型。而最右侧的数字 3是最终结果。这里的 filter 、 map 、 skip 都是在对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法 count 执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。备注:“Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,流本身并不存储任何元素(或其地址值)。 Stream(流)是一个来自数据源的元素队列
stream(流)是一个来自数据源的元素队列元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。数据源 流的来源。 可以是集合,数组 等。和以前的Collection操作不同,
Stream操作还有两个基础的特征:
Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
内部迭代: 以前对集合遍历都是通过Iterator或者增强for的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式,流可以直接调用遍历方法。
当使用一个流的时候,通常包括三个基本步骤:
获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。
*****获取流三种方式:根据Collection获取流+根据Map获取流 +根据数组获取流
java.util.stream.Stream 是Java 8新加入的最常用的流接口。(这并不是一个函数式接口。)获取一个流非常简单,有以下几种常用的方式:
①所有的 Collection 集合都可以通过 stream 默认方法获取流;
②Stream 接口的静态方法 of 可以获取(其参数是可变参数,因此是可以传递数组的)数组对应的流。
public class StreamDemoThree { public static void main(String[] args) { //创建List集合,存储姓名 List<String> list = new ArrayList<>(); list.add("张无忌"); //collection集合,直接调用默认方法stream获取流 Stream<String> stream = list.stream(); Set<String> set= new HashSet<>();//set也是单列集合 Stream<String> stream1 = set.stream(); Map<String, String> map= new HashMap<>(); map.put("123","1234"); map.put("456","3456"); map.put("678","6789"); //间接使用stream流,获取键的集合,再使用stream,,或通过values获取值集合stream再获取流 Stream<String> stream2 = map.keySet().stream(); Stream<String> stream3 = map.values().stream(); //继续间接使用 获取map的键值对关系 Set<Map.Entry<String, String>> entries = map.entrySet(); List<Map.Entry<String, String>> collect = entries.stream().collect(Collectors.toList()); System.out.println(entries);//[123=1234, 456=3456, 678=6789] System.out.println(collect);//[123=1234, 456=3456, 678=6789] //数组获取静态方法的of流 Integer[] integers={1,2,3,4}; Stream<Integer> integers1 = Stream.of(integers); } }
根据Collection获取流首先, java.util.Collection 接口中加入了default方法 stream 用来获取流,所以其所有实现类均可获取流。
public class Demo04GetStream { public static void main(String[] args) { List<String> list = new ArrayList<>(); // ... Stream<String> stream1 = list.stream(); Set<String> set = new HashSet<>(); // ... Stream<String> stream2 = set.stream(); Vector<String> vector = new Vector<>(); Stream<String> stream3 = vector.stream(); } }
根据Map获取流 java.util.Map 接口不是 Collection 的子接口,且其K-V数据结构不符合流元素的单一特征,所以获取对应的流需要分key、value或entry等情况:
public class Demo05GetStream { public static void main(String[] args) { Map<String, String> map = new HashMap<>(); // ... Stream<String> keyStream = map.keySet().stream(); Stream<String> valueStream = map.values().stream(); Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream(); } }
根据数组获取流如果使用的不是集合或映射而是数组,由于数组对象不可能添加默认方法,所以 Stream 接口中提供了静态方法 of ,使用很简单:
public class Demo06GetStream { public static void main(String[] args) { String[] array = { "张无忌", "张翠山", "张三丰", "张一元" }; Stream<String> stream = Stream.of(array); } }
流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:
*延迟方法:返回值类型仍然是 Stream 接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为延迟方法。)也就是还是流
*终结方法:返回值类型不再是 Stream 接口自身类型的方法,因此不再支持类似 StringBuilder 那样的链式调用。也就是不是流了。
本小节中,终结方法包括 count 和 forEach 方法。
①逐一处理:forEach ---虽然方法名字叫 forEach ,但是与for循环中的“for-each”昵称不同。
void forEach(Consumer<? super T> action);
该方法接收一个 Consumer 接口函数,会将每一个流元素交给该函数进行处理。 复习Consumer接口
java.util.function.Consumer<T>接口是一个消费型接口。 Consumer接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据。
基本使用:
public class Demo12StreamForEach { public static void main(String[] args) { Stream<String> stream = Stream.of("张无忌", "张三丰", "周芷若"); stream.forEach(name‐> System.out.println(name)); } }
②过滤:filter 可以通过 filter 方法将一个流转换成另一个子集流。
方法签名:该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。
复习Predicate接口此前我们已经学习过 java.util.stream.Predicate 函数式接口,其中唯一的抽象方法为:
boolean test(T t);
该方法将会产生一个boolean值结果,代表指定的条件是否满足。如果结果为true,那么Stream流的 filter 方法将会留用元素;如果结果为false,那么 filter 方法将会舍弃元素。基本使用 Stream流中的 filter 方法基本使用的代码如:
public class Demo07StreamFilter { public static void main(String[] args) { Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若"); Stream<String> result = original.filter(s ‐> s.startsWith("张")); } }
映射:map 如果需要将流中的元素映射到另一个流中,可以使用 map 方法。方法签名:
Stream map(Function<? super T, ? extends R> mapper);
该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。
复习Function接口此前我们已经学习过 java.util.stream.Function 函数式接口,其中唯一的抽象方法为:
R apply(T t);
这可以将一种T类型转换成为R类型,而这种转换的动作,就称为“映射”。
import java.util.stream.Stream; public class Demo08StreamMap { public static void main(String[] args) { Stream<String> original = Stream.of("10", "12", "18"); Stream<Integer> result = original.map(str‐>Integer.parseInt(str)); } }
这段代码中, map 方法的参数通过方法引用,将字符串类型转换成为了int类型(并自动装箱为 Integer 类对象)。
统计个数:count 正如旧集合 Collection 当中的 size 方法一样,流提供 count 方法来数一数其中的元素个数
long count();
该方法返回一个long值代表元素个数(不再像旧集合那样是int值)。基本使用
public class Demo09StreamCount { public static void main(String[] args) { Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若"); Stream<String> result = original.filter(s ‐> s.startsWith("张")); System.out.println(result.count()); // 2 } }
取用前几个:limit limit 方法可以对流进行截取,只取用前n个。方法签名
Stream<T> limit(long maxSize);
参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作。基本使用
public class Demo10StreamLimit { public static void main(String[] args) { Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若"); Stream<String> result = original.limit(2); System.out.println(result.count()); // 2 } }
跳过前几个:skip 如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流
Stream<T> skip(long n);
public class Demo11StreamSkip { public static void main(String[] args) { Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若"); Stream<String> result = original.skip(2); System.out.println(result.count()); // 1 } }
组合:concat 如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat :备注:这是一个静态方法,与 java.lang.String 当中的 concat 方法是不同的。
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
public class Demo12StreamConcat { public static void main(String[] args) { Stream<String> streamA = Stream.of("张无忌"); Stream<String> streamB = Stream.of("张翠山"); Stream<String> result = Stream.concat(streamA, streamB); } }
根据姓名创建person对象,存储到新集合中的做法。