Java cheat sheet: https://yungnickyoung.github.io/Java-Cheatsheet/
1. 异常
1.1 异常的概念
- 异常是类,产生异常——在运行中创建异常对象并抛出异常对象,交给Java虚拟机处理
- 根父类:java.lang.Throwable
- 子类:java.lang.Error(必须修改源代码后,才能继续执行)
- 子类:java.lang.Exception(编译期异常;处理后之后可以继续执行)
- 子类:RuntimeException(运行期异常)
- 异常的处理方式
- throw,抛出异常交给JVM处理——中断,打印信息
- try-catch,会打印出异常信息,但是会继续执行后续代码
- 内部处理流程
- JVM检测出异常,创建包含异常产生的内容、原因、位置的“异常对象”
- 如果没有异常的处理逻辑(try-catch),JVM将异常抛给该方法的调用者
- 直到main函数,如果没有异常的处理逻辑,则交给JVM处理
- 将异常对象打印出在console
- 中断程序
- 其他
- 父类抛出了多个异常,子类应该抛出父类异常、父类异常的子类、不抛出异常
- 父类没有抛出异常,子类不可以抛出异常,只能捕获处理
自定义异常
java public class XXXException extends Exception | RuntimeException{ // 空参构造方法 // 带异常信息的构造方法;其中会调用父类的构造方法 }
- 继承了RuntimeException的自定义异常无需处理
- 继承了Exception的编译期异常,需要throws或try-catch
- 包含两个构造方法,其中带异常信息的构造方法需要调用父类的造方法,来处理异常信息
1.2 异常的处理
五个关键字:try、catch、finally、throw、throws
- throw 抛出异常
- 写在方法内部
- 使用格式:
throw new xxxException("需要打印的信息");
- throw异常后必须处理异常
- throw的是RuntimeException或其子类对象,默认交给JVM处理
- throw的是编译其异常,要么接着throw,要么用try-catch处理
- 不用处理的RuntimeException的子类
- NullPointerException
- ArrayIndexOutOfBoundsException
- Objects类
- 包括static方法,是null-save或null-tolerant的。源码中为对象是null的值进行抛出异常:
public static <T> T requireNonNull(T obj) {}
,为空抛异常,非空返回对象 - 用
Object.requireNonNull(obj)
判断变量是否为空
- 包括static方法,是null-save或null-tolerant的。源码中为对象是null的值进行抛出异常:
- throws 抛出异常
- 写在方法声明
- 若抛出多个异常,可以直接声明父类异常
try-catch 捕获异常
- 避免throw后交由JVM中断运行
使用格式
java try{ // 可能产生异常的代码 } catch(异常类名1 e) { // 将异常信息记录到日志中 // 处理异常对象 } ... catch(异常类名n e) { }
try中的代码可以出现多种异常,也可以使用多个catch来进行处理;但是当一个try对应多个catch的时候,catch的子类异常变量必须写在上面,否则会报错
- 处理完之后,可以继续运行try-catch语句后的代码
- Throwable类中定义的异常处理的方法(catch中可用)
String getMessage()
简短描述String toString()
详细信息void printStackTrace()
异常对象(默认)
- finally
- 无论是否出现异常,都要执行的代码
- finally不能单独使用,而必须要与try语句配合使用,常用来进行“资源释放”(常用于IO流处理中)
- finally代码块中不能有return语句,因为finally中的代码一定会被执行
2. 多线程
2.1 基本概念
- 并发 v.s. 并行
- 并发(多个任务交替执行)
- 并行(多个任务同时执行)
- 进程 v.s. 线程
- 进程:硬盘(永久储存ROM)中的程序到内存(临时存储RAM)中执行,占用内存来运行的程序叫做进程
- 线程:进程中的执行单元,负责程序执行,一个进程可以有多个线程
- 线程属于进程。一个程序运行后至少有一个进程,一个进程可以包含多个线程
- 单核心单线程,需要在多个线程之间轮流切换,虽然切换的速度快(毫秒级别)但效率低
- 多线程的CPU同时执行8个线程,轮流切换的时候,程序被执行的概率提高了8倍。效率高,并且多个线程之间互不影响
- 线程调度
- 分时调度:平均分配CPU占用时间
- 抢占式调度:优先级高的线程优先使用CPU,优先级相同随机选择(JAVA的调度模式)
2.2 多线程的实现
- 方法一:继承java.lang.Thread类
- 定义线程:创建Thread类的子类
- 设置线程任务:在Thread类的子类中定义run方法(在该方法中写上线程任务)
- 创建Thread类的子类对象
- 调用Thread类中的start方法,开启线程,执行run方法
方法二:实现Runnable接口
- 定义线程:创建Runnable的实现类
- 设置线程任务:重写Runnable接口中的run方法
- 创建Runnable接口实现类对象
- 调用Thread类的构造函数,不安地Runnable接口实现类对象,以创建一个线程
- 用Thread类的start方法来执行线程
java PrimeRun p = new PrimeRun(); new Thread(p).start()
方法二的变体:用匿名内部类简化代码```java // 把实现类实现接口,重写run方法,实例化实现类对象合为一步 Runnable r = new Runnable(){ public void run(){
// 线程任务
} };
// 把实例化Thread类对象、调用start方法合为一步 new Thread(r).start();
4. Thread类中的常用方法
1. `public String getName()` 获取线程名称:Thread-x
2. `public void setName()` 设置线程名称
3. `public void start()` JVM调用自定义run方法
4. `public void run()` 自定义线程任务
5. `public static void sleep(long millis)` 暂停执行millis毫秒;写在run方法或main方法中```java
try {
Thread.sleep(10000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
public static Thread currentThread()
打印当前的线程名称:System.out.println(Thread.currentThread().getName())
- Runnable接口相对于Thread类的优势
- 接口多实现,而类单继承,突破了Java语言属性的限制(继承了Runnable类)
- 增加程序的鲁棒性,实现定义线程任务(实例化Runnable的子类对象)和开启线程(实例化Thread类对象)之间的解耦操作,代码可以被多个线程共享,代码和线程独立
- 线程池只能放入Runnable或Callable线程,而不能放入继承Thread的类
- 其他
- 多次启动一个线程是非法的,特别是线程已结束时,不能重新启动
- 调用在Thread类的子类中重写了run方法,创建对象后,调用start方法后执行两个线程:main线程和run方法中的线程。两个线程优先级相同,因此随机选择执行
- 直接在main方法中调用run方法,会将run方法压入同一块栈空间;在main方法中调用start方法,会开辟新的栈空间,将run方法压入新栈中,此时CPU可以在多个线程中使用抢占式调度方法,选择执行。
- 因为多个线程在不同的栈空间,所以会互不影响。
2.2 线程安全
多线程访问了共享数据,可能产生线程安全问题。重复问题是因为多个线程“并行”,同时执行。出现非法数据的问题是因为某个线程抢到了执行权,但此时运行过了判断语句,相当于不判断合法性直接输出了。
避免出现线程安全问题,需要让线程在访问共享数据时,让其他线程等待,保证只有一个线程在运行。
2.2.1 同步代码块
- 用
synchronized
关键字用于方法中的某区块(可能出现线程安全问题的代码),表示该区块中的资源实行互斥访问java synchronized(锁对象){ // 访问了共享资源,可能出现线程安全问题的代码 }
锁对象是任意对象,可以直接用this - 同步技术的原理
- 某线程抢到了CPU的使用权之后,会执行run方法。
- 运行到synchronized代码块后,会检查是否有锁对象。
- 如果有,会获取锁对象,进入到同步中执行
- 如果没有,会进入阻塞状态,等待线程执行完同步中的代码并归还锁对象
- 同步代码块中的线程,没有执行完不会归还锁对象;同步代码块之外的线程,没有锁无法进入同步。
- 程序频繁地判断、获取、释放锁,导致效率降低,但是能够保证只有一个线程在执行共享数据,确保了线程安全
2.2.2 同步方法
用
synchronized
关键字用于方法的声明中。java public synchronized void run(参数列表) { // 访问了共享资源,可能出现线程安全问题的代码 }
同步方法的锁对象就是
this
,谁调用锁对象就是谁;静态同步方法的锁对象是该类的class文件对象(static方法先于实例化对象,不可能是this)。
2.2.3 锁机制
- java.util.concurrent.locks.Lock接口,比
synchronized
方法和语句可获得更广泛的锁定操作。提供了两个方法:void lock()
获取锁void unlock()
释放锁
- 使用 java.util.concurrent.locks.ReentrantLock (是Lock接口的实现类)
- 在成员位置,创建ReentrantLock对象
- 在代码前,调用Lock接口中的lock方法获取锁
- 在代码后,调用Lock接口中的unlock方法释放锁
- 一般用try-catch-finally语句,来保证一定会执行到unlock方法,保证一定会释放锁```java
Lock sync = new ReentrantLock();
public void run(){
sync.lock();
try{
} catch(Exception e){// 访问了共享资源,可能出现线程安全问题的代码
} finally{// 执行操作
} } ```lock.unlock();
2.3 线程状态
- 概述
- NEW 刚被创建,还未调用start方法
- Runnable 持有线程锁,可以在JVM中运行,未必正在运行
- Blocked 线程锁对象被其他线程所持有,争取到了锁对象,进入Runnable状态
- Waiting 用Object.wait方法进入等待状态,无法自动唤醒,需要用Object.notify或Object.notifyAll方法来唤醒
- Timed Waiting(比如Thread.sleep方法) 进入等待状态,可以自动结束休眠并检测CPU状态
- TERMINATED 线程终结(正常退出,或由于没有捕获的异常)
- 线程间通信
- 两个线程用wait和notify方法来协调执行状态(等待或执行)
- 用(唯一的)锁对象调用wait和notify方法
举例```java public class DemoWaitAndNotify { public static void main(String[] args) {
Object obj = new Object(); System.out.println("——————————程序开头的一段代码——————————"); new Thread(){ public void run(){ synchronized(obj) { System.out.println("【顾客】的线程,顾客告诉老板需要什么商品;"); try { System.out.println("【顾客】的线程进入等待状态!"); obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("【顾客】的线程被唤醒!"); } } }.start(); System.out.println("——————————线程之间的一段代码——————————"); new Thread(){ public void run(){ synchronized (obj){ System.out.println("【老板】的线程,请等待五秒让老板准备;"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("【老板】的线程结束,唤醒顾客线程!"); obj.notify(); } } }.start(); System.out.println("——————————线程结尾的一段代码——————————");
} }
<br />console打印内容:
——————————程序开头的一段代码—————————— ——————————线程之间的一段代码—————————— ——————————线程结尾的一段代码—————————— 【顾客】的线程,顾客告诉老板需要什么商品; 【顾客】的线程进入等待状态! 【老板】的线程,请等待五秒让老板准备; 【老板】线程结束,唤醒顾客线程! 【顾客】的线程被唤醒! ```
分析:- 主线程和Thread的对象,优先级相同,随机选择执行顺序。所以主线程中的三个打印语句先执行。
- 如果不用obj.wait(),obj.notify()实现线程之间的通信,完全有可能先执行老板的线程(虽然他在主方法中的位置靠后)。
- 在同一线程中,由上到下顺序执行,因此三个打印语句的顺序是正常的。
- 顾客的线程,被锁对象调用wait()方法而进入waiting状态;从而老板的线程获得CPU使用权,得以执行
- 老板线程执行结束之后,使用锁对象的notify()方法,从而唤醒顾客的线程,接着运行
obj.wait()
之后的代码
- 两个类之间的线程通信
- 需要在两个类中定义带参数的构造方法,传入主方法中的同一个对象作为锁对象
- 将需要协调的代码写入各类中的同步代码块
- 用锁对象的wait和notify方法来决定等待,还是释放线程锁开始执行
2.4 线程池
- 避免频繁创建、销毁,造成效率低下。JDK 1.5之后提供线程池
java.util.concurrent.Executors 类
static newFixedThreadPool(int nThreads)
输入线程池容量,返回ExecutorService接口实现类- 创建Runnable接口的实现类,重写run方法,设置线程任务
- 调用ExecutorService中的方法submit,向线程池中传递Runnable接口的实现类,并执行run方法
调用ExecutorService中的方法shutdown,销毁线程池(不建议使用)
public static void main(String[] args) { // 创建线程池,返回ExecutorService接口的实现类对象 ExecutorService es = Executors.newFixedThreadPool(3); // 创建Runnable接口的实现类对象 Runnable r = new Runnable(){ @Override public void run() { System.out.println(Thread.currentThread().getName()); } }; // 执行线程中的run方法 es.submit(r); es.submit(r); es.submit(r); // 销毁线程池 es.shutdown(); }
线程池的容量为n,如果有m个线程等待运行(m>n),必须等待现有线程执行完之后,再进入线程池
- 线程池会一直开启。当线程结束,会自动将线程归还给线程池,可以继续使用
3. Lambda表达式
- 基本格式:
(参数列表) -> {方法体}
- 使用lambda表达式,可简化匿名内部类的定义。通常使用lambda表达式写需要override的方法即可,编译时JVM可以通过传入lambda表达式的方法的参数列表,推理出你重写的是哪个类的方法。```java
// 升序排列(匿名内部类)
Arrays.sort(arr, new Comparator
() { @Override public int compare(Student o1, Student o2) { return o1.getAge()-o2.getAge(); } });
// 降序排列(lambda表达式) Arrays.sort(arr, (Student o1, Student o2) -> { return o2.getAge()-o1.getAge(); });
// 升序排列(lambda表达式的省略形式)
Arrays.sort(arr, (o1, o2) -> o1.getAge()-o2.getAge());
``
<br />可推理出lambda表达式
(Student o1, Student o2) -> {return o2.getAge()-o1.getAge();}` 重写的是Comparator中的compare方法
- 简化形式
- (参数列表):参数列表中的
数据类型
可以省略 - (参数列表):参数列表中只有一个,
数据类型
和()
都可以省略 - (方法体):只有一行,可以并且只能同时省略
{}
、return
和;
- (参数列表):参数列表中的
- 其他
- 只有有接口,且接口中只有一个抽象方法时才能使用lambda表达式;这种有且仅有一个抽象方法的接口,叫做函数式接口
- lambda表达式必须具有上下文推断。lambda表达式传递进去的方法的参数,必须是对应的接口类型