Java cheat sheet: https://yungnickyoung.github.io/Java-Cheatsheet/

1. 异常

1.1 异常的概念

  1. 异常是类,产生异常——在运行中创建异常对象并抛出异常对象,交给Java虚拟机处理
  2. 根父类:java.lang.Throwable
    1. 子类:java.lang.Error(必须修改源代码后,才能继续执行)
    2. 子类:java.lang.Exception(编译期异常;处理后之后可以继续执行)
      1. 子类:RuntimeException(运行期异常)
  3. 异常的处理方式
    1. throw,抛出异常交给JVM处理——中断,打印信息
    2. try-catch,会打印出异常信息,但是会继续执行后续代码
  4. 内部处理流程
    1. JVM检测出异常,创建包含异常产生的内容、原因、位置的“异常对象”
    2. 如果没有异常的处理逻辑(try-catch),JVM将异常抛给该方法的调用者
    3. 直到main函数,如果没有异常的处理逻辑,则交给JVM处理
      1. 将异常对象打印出在console
      2. 中断程序
  5. 其他
    1. 父类抛出了多个异常,子类应该抛出父类异常、父类异常的子类、不抛出异常
    2. 父类没有抛出异常,子类不可以抛出异常,只能捕获处理
  6. 自定义异常java public class XXXException extends Exception | RuntimeException{ // 空参构造方法 // 带异常信息的构造方法;其中会调用父类的构造方法 }

    1. 继承了RuntimeException的自定义异常无需处理
    2. 继承了Exception的编译期异常,需要throws或try-catch
    3. 包含两个构造方法,其中带异常信息的构造方法需要调用父类的造方法,来处理异常信息

1.2 异常的处理

五个关键字:try、catch、finally、throw、throws

  1. throw 抛出异常
    1. 写在方法内部
    2. 使用格式:throw new xxxException("需要打印的信息");
    3. throw异常后必须处理异常
      1. throw的是RuntimeException或其子类对象,默认交给JVM处理
      2. throw的是编译其异常,要么接着throw,要么用try-catch处理
    4. 不用处理的RuntimeException的子类
      1. NullPointerException
      2. ArrayIndexOutOfBoundsException
    5. Objects类
      1. 包括static方法,是null-save或null-tolerant的。源码中为对象是null的值进行抛出异常:public static <T> T requireNonNull(T obj) {},为空抛异常,非空返回对象
      2. Object.requireNonNull(obj)判断变量是否为空
  2. throws 抛出异常
    1. 写在方法声明
    2. 若抛出多个异常,可以直接声明父类异常
  3. try-catch 捕获异常

    1. 避免throw后交由JVM中断运行
    2. 使用格式java try{ // 可能产生异常的代码 } catch(异常类名1 e) { // 将异常信息记录到日志中 // 处理异常对象 } ... catch(异常类名n e) { }

    3. try中的代码可以出现多种异常,也可以使用多个catch来进行处理;但是当一个try对应多个catch的时候,catch的子类异常变量必须写在上面,否则会报错

    4. 处理完之后,可以继续运行try-catch语句后的代码
    5. Throwable类中定义的异常处理的方法(catch中可用)
      1. String getMessage() 简短描述
      2. String toString() 详细信息
      3. void printStackTrace() 异常对象(默认)
  4. finally
    1. 无论是否出现异常,都要执行的代码
    2. finally不能单独使用,而必须要与try语句配合使用,常用来进行“资源释放”(常用于IO流处理中)
    3. finally代码块中不能有return语句,因为finally中的代码一定会被执行

2. 多线程

2.1 基本概念

  1. 并发 v.s. 并行
    1. 并发(多个任务交替执行)
    2. 并行(多个任务同时执行)
  2. 进程 v.s. 线程
    1. 进程:硬盘(永久储存ROM)中的程序到内存(临时存储RAM)中执行,占用内存来运行的程序叫做进程
    2. 线程:进程中的执行单元,负责程序执行,一个进程可以有多个线程
      1. 线程属于进程。一个程序运行后至少有一个进程,一个进程可以包含多个线程
      2. 单核心单线程,需要在多个线程之间轮流切换,虽然切换的速度快(毫秒级别)但效率低
      3. 多线程的CPU同时执行8个线程,轮流切换的时候,程序被执行的概率提高了8倍。效率高,并且多个线程之间互不影响
  3. 线程调度
    1. 分时调度:平均分配CPU占用时间
    2. 抢占式调度:优先级高的线程优先使用CPU,优先级相同随机选择(JAVA的调度模式)

2.2 多线程的实现

  1. 方法一:继承java.lang.Thread类
    1. 定义线程:创建Thread类的子类
    2. 设置线程任务:在Thread类的子类中定义run方法(在该方法中写上线程任务)
    3. 创建Thread类的子类对象
    4. 调用Thread类中的start方法,开启线程,执行run方法
  2. 方法二:实现Runnable接口

    1. 定义线程:创建Runnable的实现类
    2. 设置线程任务:重写Runnable接口中的run方法
    3. 创建Runnable接口实现类对象
    4. 调用Thread类的构造函数,不安地Runnable接口实现类对象,以创建一个线程
    5. 用Thread类的start方法来执行线程java PrimeRun p = new PrimeRun(); new Thread(p).start()
  3. 方法二的变体:用匿名内部类简化代码```java // 把实现类实现接口,重写run方法,实例化实现类对象合为一步 Runnable r = new Runnable(){ public void run(){

    1. // 线程任务

    } };

// 把实例化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();
}
  1. public static Thread currentThread() 打印当前的线程名称:System.out.println(Thread.currentThread().getName())
    1. Runnable接口相对于Thread类的优势
  2. 接口多实现,而类单继承,突破了Java语言属性的限制(继承了Runnable类)
  3. 增加程序的鲁棒性,实现定义线程任务(实例化Runnable的子类对象)和开启线程(实例化Thread类对象)之间的解耦操作,代码可以被多个线程共享,代码和线程独立
  4. 线程池只能放入Runnable或Callable线程,而不能放入继承Thread的类
    1. 其他
  5. 多次启动一个线程是非法的,特别是线程已结束时,不能重新启动
  6. 调用在Thread类的子类中重写了run方法,创建对象后,调用start方法后执行两个线程:main线程run方法中的线程。两个线程优先级相同,因此随机选择执行
  7. 直接在main方法中调用run方法,会将run方法压入同一块栈空间;在main方法中调用start方法,会开辟新的栈空间,将run方法压入新栈中,此时CPU可以在多个线程中使用抢占式调度方法,选择执行。
  8. 因为多个线程在不同的栈空间,所以会互不影响。

2.2 线程安全

多线程访问了共享数据,可能产生线程安全问题。重复问题是因为多个线程“并行”,同时执行。出现非法数据的问题是因为某个线程抢到了执行权,但此时运行过了判断语句,相当于不判断合法性直接输出了。

避免出现线程安全问题,需要让线程在访问共享数据时,让其他线程等待,保证只有一个线程在运行。

2.2.1 同步代码块

  1. synchronized 关键字用于方法中的某区块(可能出现线程安全问题的代码),表示该区块中的资源实行互斥访问java synchronized(锁对象){ // 访问了共享资源,可能出现线程安全问题的代码 }
    锁对象是任意对象,可以直接用this
  2. 同步技术的原理
    1. 某线程抢到了CPU的使用权之后,会执行run方法。
    2. 运行到synchronized代码块后,会检查是否有锁对象。
      1. 如果有,会获取锁对象,进入到同步中执行
      2. 如果没有,会进入阻塞状态,等待线程执行完同步中的代码并归还锁对象
  3. 同步代码块中的线程,没有执行完不会归还锁对象;同步代码块之外的线程,没有锁无法进入同步。
  4. 程序频繁地判断、获取、释放锁,导致效率降低,但是能够保证只有一个线程在执行共享数据,确保了线程安全

2.2.2 同步方法

  1. synchronized 关键字用于方法的声明中。java public synchronized void run(参数列表) { // 访问了共享资源,可能出现线程安全问题的代码 }

  2. 同步方法的锁对象就是 this,谁调用锁对象就是谁;静态同步方法的锁对象是该类的class文件对象(static方法先于实例化对象,不可能是this)。

2.2.3 锁机制

  1. java.util.concurrent.locks.Lock接口,比synchronized方法和语句可获得更广泛的锁定操作。提供了两个方法:
    1. void lock() 获取锁
    2. void unlock() 释放锁
  2. 使用 java.util.concurrent.locks.ReentrantLock (是Lock接口的实现类)
    1. 在成员位置,创建ReentrantLock对象
    2. 在代码前,调用Lock接口中的lock方法获取锁
    3. 在代码后,调用Lock接口中的unlock方法释放锁
  3. 一般用try-catch-finally语句,来保证一定会执行到unlock方法,保证一定会释放锁```java Lock sync = new ReentrantLock(); public void run(){ sync.lock(); try{
     // 访问了共享资源,可能出现线程安全问题的代码
    
    } catch(Exception e){
     // 执行操作
    
    } finally{
     lock.unlock();
    
    } } ```

2.3 线程状态

3. Java 异常、多线程与Lambda表达式 - 图1

  1. 概述
    1. NEW 刚被创建,还未调用start方法
    2. Runnable 持有线程锁,可以在JVM中运行,未必正在运行
    3. Blocked 线程锁对象被其他线程所持有,争取到了锁对象,进入Runnable状态
    4. Waiting 用Object.wait方法进入等待状态,无法自动唤醒,需要用Object.notify或Object.notifyAll方法来唤醒
    5. Timed Waiting(比如Thread.sleep方法) 进入等待状态,可以自动结束休眠并检测CPU状态
    6. TERMINATED 线程终结(正常退出,或由于没有捕获的异常)
  2. 线程间通信
    1. 两个线程用wait和notify方法来协调执行状态(等待或执行)
    2. 用(唯一的)锁对象调用wait和notify方法
  3. 举例```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打印内容:
    

    ——————————程序开头的一段代码—————————— ——————————线程之间的一段代码—————————— ——————————线程结尾的一段代码—————————— 【顾客】的线程,顾客告诉老板需要什么商品; 【顾客】的线程进入等待状态! 【老板】的线程,请等待五秒让老板准备; 【老板】线程结束,唤醒顾客线程! 【顾客】的线程被唤醒! ```
    分析:

    1. 主线程和Thread的对象,优先级相同,随机选择执行顺序。所以主线程中的三个打印语句先执行。
    2. 如果不用obj.wait(),obj.notify()实现线程之间的通信,完全有可能先执行老板的线程(虽然他在主方法中的位置靠后)。
    3. 在同一线程中,由上到下顺序执行,因此三个打印语句的顺序是正常的。
    4. 顾客的线程,被锁对象调用wait()方法而进入waiting状态;从而老板的线程获得CPU使用权,得以执行
    5. 老板线程执行结束之后,使用锁对象的notify()方法,从而唤醒顾客的线程,接着运行 obj.wait() 之后的代码
  4. 两个类之间的线程通信
    1. 需要在两个类中定义带参数的构造方法,传入主方法中的同一个对象作为锁对象
    2. 将需要协调的代码写入各类中的同步代码块
    3. 用锁对象的wait和notify方法来决定等待,还是释放线程锁开始执行

2.4 线程池

  1. 避免频繁创建、销毁,造成效率低下。JDK 1.5之后提供线程池
  2. java.util.concurrent.Executors 类

    1. static newFixedThreadPool(int nThreads) 输入线程池容量,返回ExecutorService接口实现类
    2. 创建Runnable接口的实现类,重写run方法,设置线程任务
    3. 调用ExecutorService中的方法submit,向线程池中传递Runnable接口的实现类,并执行run方法
    4. 调用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();
      }
      
  3. 线程池的容量为n,如果有m个线程等待运行(m>n),必须等待现有线程执行完之后,再进入线程池

  4. 线程池会一直开启。当线程结束,会自动将线程归还给线程池,可以继续使用

3. Lambda表达式

  1. 基本格式:(参数列表) -> {方法体}
  2. 使用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方法

  1. 简化形式
    1. (参数列表):参数列表中的数据类型可以省略
    2. (参数列表):参数列表中只有一个,数据类型()都可以省略
    3. (方法体):只有一行,可以并且只能同时省略{}return;
  2. 其他
    1. 只有有接口,且接口中只有一个抽象方法时才能使用lambda表达式;这种有且仅有一个抽象方法的接口,叫做函数式接口
    2. lambda表达式必须具有上下文推断。lambda表达式传递进去的方法的参数,必须是对应的接口类型