Java系列笔记第五章:异常、多线程、lambda、函数式编程

1. 异常

1.1 异常的类型

异常的根类是java.lang.Throwable, 有两个子类,java.lang.Error和java.lang.Exception。 一般异常指Exception。

try {
    //可能出错的代码
}catch(Exception e) {
    //对错误的处理
}
  • 如果内存申请过多,就会抛出内存溢出错误,而不是异常。
  • 错误必须修改源代码,否则无法处理。

1.2 异常的产生过程

JVM检测到程序出现异常,做如下两件事。

  1. 根据异常产生的原因创建一个异常对象,包含了异常的内容、原因、位置。
  2. 如果产生异常的方法中没用try-catch处理异常,就会把异常抛出给方法的调用者。

如果没被处理,异常就会一层一层向上抛出,直到被JVM接收。JVM接收了异常后,会做两件事。

  1. 将异常对象(内容、原因、位置)以红字打印到控制台。
  2. 终止当前程序。

1.3 异常的处理

Tips : 判断对象是否为空,可以直接用Objects.requireNonNull(待判断对象obj,"提示信息");,如果对象为空,会抛出异常。

1.3.1 throw

在指定的方法中抛出指定的异常。
throw new ***Exception("异常的一些信息")

注意事项:

  • 必须写在方法内部。
  • new 的对象必须是Exception及其子类对象。
  • 抛出异常后,我们必须处理异常。
  • 如果new的是RuntimeException对象,那么可以默认不处理,交给JVM,其他对象都必须处理。
  • 处理方式有两种。
    • try-catch包裹。
    • 在此方法中throws出去。
package ExceptionDemo;

public class throwDemo {
    public static void main(String[] args) {
        int[] arr = new int[]{1, 2, 3, 4};
        try {
            System.out.println(getElement(arr, 4));

        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            System.out.println(getElement(null, 5));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static int getElement(int[] arr, int index) throws Exception {
        if (arr == null) {
            throw new NullPointerException("数组为空!");
        }
        if (index >= arr.length) {
            throw new IndexOutOfBoundsException("数组越界! 错误的索引为:" + index);
        } else return arr[index];
    }
}


//===========输出===========//
java.lang.IndexOutOfBoundsException: 数组越界! 错误的索引为:4
	at ExceptionDemo.throwDemo.getElement(throwDemo.java:29)
	at ExceptionDemo.throwDemo.main(throwDemo.java:10)
java.lang.NullPointerException: 数组为空!
	at ExceptionDemo.throwDemo.getElement(throwDemo.java:26)
	at ExceptionDemo.throwDemo.main(throwDemo.java:16)
1.3.2 throws

将异常标识出来,交给方法调用者去处理。

  • 在方法声明时使用。
  • 方法内抛出多少异常,就要声明多少异常。
  • 如果抛出的异常存在子父类关系,声明父类即可。
  • 如果调用了抛出异常的方法,就必须处理异常。
public void fun() throws **Exception, **Exception,{
    //...
    throw new ***Exception();
    //...
}
1.3.3 try-catch 异常捕获
try{
    //可能产生异常的代码
}catch(aaaException e){
    //处理aaa异常
}catch(bbbException e){
    //处理bbb异常
}...

1.4 Throwable类

Throwable类提供了三个方法。

  1. getMessage(), 返回详细消息字符串。
  2. toString(), 返回简短描述。
  3. printStackTrace(), JVM打印异常对象默认调用此方法。

1.5 finally代码块

如果出现异常,但是必须执行某些代码,比如释放资源,就需要用到finally代码块。无论是否出现异常,都要执行。

  • 注意: finally中如果出现了return语句,那么执行的一定是finally中的return,try中的return会被忽略。
try{
    //...
}catch(Exception e){
    //...
}finally{
    //释放资源
}

1.6 多个异常捕获的注意事项

1.6.1 多个异常分别处理
try{
    //代码1
}catch(Exception e){
    //处理1
}

try{
    //代码2
}catch(Exception e){
    //处理2
}
1.6.2 多个异常一次捕获多次处理

catch里如果有子父类关系,那么子类异常必须在前,父类异常必须在后。

try{
    //可能产生异常的代码
}catch(sonException e){
    //处理子类异常
}catch(fatherException e){
    //处理父类异常
}...
1.6.3 多个异常一次捕获一次处理

用一个比较高的异常来捕获所有异常,比如最高的Exception类。

1.6.4 子类父类抛出异常
  • 父类的异常是什么样,子类异常一般就是什么样。
  • 子类的异常必须和父类异常一样或者是父类异常的子类。
  • 子类继承有异常的父类时,也可以没异常。
  • 若父类没异常,子类如果产生异常,就必须使用try-catch来处理。

1.7 自定义异常

继承自Exception或者RuntimeException类。

需要实现两个构造方法。

  • 无参构造方法。
  • 带参构造方法。
package ExceptionDemo;

public class MyException extends Exception {
    public MyException() {
        super();
    }

    public MyException(String message) {
        super(message);
    }
}

2. 多线程

2.1 并发与并行

  • 并发:同一个时间段内发生。(交替执行)
  • 并行:同时发生。

2.2 多线程与多进程

进程:

线程:进程中的一个执行单元。一个进程至少有一个线程。

2.2.1 线程调度
  • 分时调度: 平均分配每个线程的CPU时间。
  • 抢占式调度: 优先让优先级高的线程使用CPU,Java就是抢占式调度。

2.3 主线程

执行主方法(main方法)的线程,就叫主线程。之前编写的都是单线程程序。从main方法开始,从上到下依次执行。

2.4 创建线程的第一种方法 : 通过继承Thread类创建线程

2.4.1 通过继承Thread类创建线程

将类声明为Thread的子类,重写run方法。调用对象的start方法即可。

  • 一个线程结束后不能被再次启动。
package MultiThreadDemo;

public class People extends Thread{
    private String name;

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

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(this.name + "->" + i);
        }
    }
}



package MultiThreadDemo;

public class test {
    public static void main(String[] args) {
        People p1 = new People("张三");
        People p2 = new People("李四");
        p1.start();
        p2.start();
    }
}

//===========输出===========//
张三->0
李四->0
张三->1
李四->1
张三->2
李四->2
张三->3
李四->3
张三->4
李四->4
张三->5
李四->5
张三->6
李四->6
张三->7
李四->7
张三->8
李四->8
张三->9
李四->9
张三->10
李四->10
张三->11
李四->11
张三->12
张三->13
张三->14
张三->15
张三->16
张三->17
张三->18
李四->12
张三->19
李四->13
李四->14
李四->15
李四->16
李四->17
李四->18
李四->19
2.4.2 多线程原理
  • 如上述代码,会随机打印张三和李四。因为两个线程会同时竞争CPU。
2.4.3 多线程内存图解

start方法会开辟一个新的栈空间,执行run方法,和另一个线程同时运行。
在这里插入图片描述

2.5 Thread类的常用方法

2.5.1 获取线程名称
  • getName() 获取当前线程的名称。

    package MultiThreadDemo;
    
    public class getNameDemo extends Thread{
        @Override
        public void run() {
            System.out.println(getName());
        }
    }
    
  • 通过Thread.currentThread()方法返回当前线程的引用,并通过当前线程的getName方法获取名称。

    package MultiThreadDemo;
    
    public class getNameDemo extends Thread{
        @Override
        public void run() {
            Thread t = Thread.currentThread();
            System.out.println(t.getName());
        }
    }
    
  • 对于main方法来说,因为没有继承Thread类,所以只能使用Thread.currentThread().getName() 来获取线程名称。

2.5.2 设置线程名称
  • setName() 方法。主函数内通过线程对象来调用此方法。

  • 创建一个带参数的构造方法,调用父类的带参构造方法,让父类Thread去设置名称。

    public getNameDemo(String name) {
        super(name);
    }
    
2.5.3 线程暂停

Thread.sleep(long 毫秒) 当前正在执行的线程睡眠毫秒数。

2.5 创建线程的第二种方法 : 实现Runnable接口

2.5.1 通过实现Runnable接口来创建线程
  • 实现Runnable接口的run()方法。

    package MultiThreadDemo;
    
    public class RunnableDemo implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.print(Thread.currentThread().getName()+"   ");
            }
        }
    }
    

    package MultiThreadDemo;

    public class RunnableTest {
    public static void main(String[] args) {
    Runnable p1 = new RunnableDemo();
    Runnable p2 = new RunnableDemo();
    Runnable p3 = new RunnableDemo();
    new Thread(p1,“线程1”).start();
    new Thread(p2,“线程2”).start();
    new Thread(p3,“线程3”).start();
    }
    }

    /===========输出===========//
    线程1 线程3 线程2 线程3 线程1 线程3 线程2 线程3 线程1 线程3 线程2 线程3 线程1 线程3 线程2 线程3 线程2 线程1 线程2 线程3 线程2 线程1 线程2 线程3 线程2 线程1 线程2 线程1 线程1 线程1

    
    

2.6 ThreadRunnable的区别

  1. Runnable避免了单继承的局限性。

    • 继承Thread类就不能继承其他类,实现Runnable接口的时候还能继承其他类。
  2. Runnable接口增强了程序的扩展性,利于解耦。

    • 将设置线程任务和开启新线程进行了分离。

2.7 匿名内部类新建线程

  1. 匿名继承Thread类。

    public static void main(String[] args) {
            new Thread(){
                @Override
                public void run() {
                    System.out.println("匿名内部类新建线程");
                }
            }.start();
        }
    

2. 匿名实现Runnable接口

  ```java
  new Thread(new Runnable() {
      @Override
      public void run() {
          System.out.println("匿名实现Runnable接口");
      }
  }).start();
  ```

### 2.8 线程安全问题

多线程,如果访问共享数据,就有可能出问题。

#### 2.8.1 线程安全问题产生的原理

#### 2.8.2 预防线程安全问题

- 线程同步技术

1. 同步代码块

  ```java
  synchronized(锁对象){
      //可能出现同步问题的代码
  }
  ```

  锁对象,也叫同步锁、对象锁、对象监视器。

  拿不到锁对象的线程会进入阻塞状态,直到另一个线程归还锁对象。

  同步中的线程不执行完同步代码块中的代码,不会释放线程锁对象。

  程序频繁地判断锁,会降低效率。

2. 同步方法

将方法设置为同步,用`synchronized`关键词修饰。

- 对于普通方法,同步锁就是this。
- 对于静态方法,同步锁是本类的class属性-->class文件对象,以后反射会讲。

```java
package MultiThreadDemo;

public class SynchronizedMethodDemo implements Runnable {
  private int num = 100;

  @Override
  public void run() {
      while (true) {
          sold();
      }
  }

  public synchronized void sold() {
      if (num > 0) {
          System.out.println(Thread.currentThread().getName() + " -> " + num);
          num--;
      }
  }
}
  1. 锁机制Lock

jdk1.5以后提供的工具,提供了比synchronized代码块和synchronized方法更广泛的锁定操作。

java.util.concurrent.locks

  • void lock()
  • void unlock()

使用步骤:

  • 在成员变量位置创建一个ReentrantLock对象;
  • 在可能出现安全问题的代码前加锁;
  • 在可能出现安全问题的代码后释放锁。
  • 注意:
    • 加锁后的代码最好放进try-catch语句中执行,将释放锁的代码放进finally代码块,防止代码出现异常导致锁无法释放。
package MultiThreadDemo;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockDemo implements Runnable {
    Lock lock = new ReentrantLock();
    private int num = 100;

    @Override
    public void run() {
        while (true) {

            //加锁
            lock.lock();

            //需要保证安全的代码块
            try {
                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + "->" + num);
                    num--;
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {

                //保证无论是否出现异常都会释放锁
                lock.unlock();
            }
        }
    }
}

2.9 线程状态

2.9.1 线程的六种状态

NEW新建状态
RUNNABLE运行状态(拿到了CPU时间)
BLOCKED阻塞状态(没拿到CPU时间)
TERMINATED死亡状态(run方法结束,或发生异常)
TIMED_WATING休眠状态,计时等待(比如Thread.sleep())
WATING无限等待状态(Object.wait()),调用Object.notify()唤醒。

2.9.2 TIMED_WATING等待唤醒状态
2.9.3 BLOCKED状态

RUNNABLE <----和其他线程争CPU时间---->BLOCKED

2.9.4 WAIT状态

等待唤醒案例:线程之间的通信。

消费者请求某个商品,进入wait状态。

生产者开始生产,生产出来商品后通知消费者,调用notify方法唤醒消费者。

这就是线程通信。

  • 生产者线程和消费者线程必须用同步代码块包裹起来,保证等待和唤醒只有一个在执行。
  • 锁对象必须保证是唯一的。
  • 只有锁对象才能调用waitnotify方法。
package MultiThreadDemo;

public class Wait_Notify {
    public static void main(String[] args) {
        //创建锁对象
        Object obj = new Object();

        new Thread() {
            @Override
            public void run() {
                //一直吃包子
                while (true) {
                    //同步代码块,消费者等着吃包子
                    synchronized (obj) {
                        System.out.println("现在消费者等着吃包子。");
                        try {
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("开始吃。");
                        System.out.println("========================");
                    }
                }
            }
        }.start();

        new Thread() {
            @Override
            public void run() {
                //一直做包子
                while (true) {
                    try {
                        //做包子
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //同步代码块
                    synchronized (obj) {
                        //包子做好了,通知消费者去吃。
                        System.out.println("包子做好了。");
                        obj.notify();
                    }
                }
            }
        }.start();
    }
}
2.9.5 带参数waitnotifyAll方法
  • 进入计时等待的方法:

    • sleep(long timeout)方法。
    • wait(long timeout)方法,如果线程在timeout毫秒值后还没被唤醒,就会自动醒来,进入RUNNABLEBLOCKED状态。
  • 唤醒方法:

    • notify() 随机唤醒一个线程。
    • notifyAll() 唤醒对象锁上的所有线程,比如好几个都在等着吃饭的顾客。

2.10 等待与唤醒机制

2.10.1 线程通信

注意:

  1. 哪怕只通知了一个线程唤醒,这个线程也不一定能进入RUNNABLE状态,因为线程是在同步块内中断的,所以需要尝试获取同步锁,如果拿到了锁,才会运行,否则进入BLOCKED状态。

  2. wait()notify()方法必须在同步块内使用。

2.11 线程池

2.11.1 线程池基础概念

如果并发线程很多,且创建的线程执行很短时间就结束了,频繁创建线程是很消耗资源的。所以引入线程池,将执行完的线程复用,继续执行其他任务。

线程池就是一个集合,一般使用LinkedList集合。LinkedList<Thread>

JDK 1.6以后,Java内置了线程池。

线程池的好处:

  • 降低资源消耗,系统不需要频繁创建销毁线程。
  • 提高响应速度,任务到达后不用等系统创建线程就能立即执行。
2.11.2 线程池的使用

java.util.concurrent.Executors : 线程池的工厂类,用于生产线程池。

类内有一个静态方法,用于创建线程池。

public static ExecutorService newFixedThreadPool(int nThreads)

  • 创建一个固定数量线程的线程池。
  • 返回一个ExecutorService接口实现类对象。
  • 可以使用ExecutorService接口来接收这个对象,叫面向接口编程。

ExecutorService : 线程池接口

  • submit(Runnable tast) 提交一个Runnable任务用于执行。
  • shutdown() 关闭或者销毁线程池。

线程池的使用步骤:

  • 使用线程池的工厂类ExecutorService的静态方法newFixedThreadPool生产一个指定线程数量的线程池。
  • 创建一个Runnable接口的实现类,重写run方法。
  • 调用ExecutorServicesubmit方法传递线程任务,开启线程。
  • 调用ExecutorServiceshutdown方法销毁线程池。(不推荐)
  • 不关闭线程池的情况下程序不会退出。
package ThreadPoll;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPollDemo {
    public static void main(String[] args) {
        //创建一个含有三个线程的线程池
        ExecutorService thread_poll = Executors.newFixedThreadPool(3);
        Task task1 = new Task(1);
        Task task2 = new Task(2);
        Task task3 = new Task(3);
        Task task4 = new Task(4);
        Task task5 = new Task(5);
        thread_poll.submit(task1);
        thread_poll.submit(task2);
        thread_poll.submit(task3);
        thread_poll.submit(task4);
        thread_poll.submit(task5);
        thread_poll.shutdown();
    }
}

class Task implements Runnable {
    private int num;

    public Task(int num) {
        this.num = num;
    }

    @Override
    public void run() {
        System.out.println("开始执行任务  ->" + this.num);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行任务完毕  ->" + this.num);
        System.out.println("=========================");
    }
}

//================输出==============
开始执行任务  ->1
开始执行任务  ->2
开始执行任务  ->3
执行任务完毕  ->1
=========================
执行任务完毕  ->3
=========================
执行任务完毕  ->2
=========================
开始执行任务  ->5
开始执行任务  ->4
执行任务完毕  ->5
=========================
执行任务完毕  ->4
=========================

2. 函数式编程 : lambda表达式

2.1 函数式编程思想概述

强调做什么,而不是以什么形式去做。

只要能得到结果就行,谁去做、怎么做都不重要。

2.2引入lambda : 冗余的Runnable代码

实现Runnable接口的方式:

  1. 实现Runnable接口。

    • 新建一个Runnable接口的实现类,重写run()方法。
    • 主函数创建Runnable实现类对象。
    • 创建Thread类对象。构造方法内放进Runnable实现类对象。
    • 调用Thread类对象的start()方法开启新线程。
  2. 创建匿名内部类,重写run()方法。

分析: 实际上,只有run()方法内的方法体才是重要的。

我们并不需要新建一个对象,而是把方法体传递给Thread类对象。

使用lambda表达式来创建新线程 :

new Thread(() -> {
    System.out.println(Thread.currentThread().getName() + " 启动!");
    }
).start();

()-> 中,括号代表参数。空的括号就是无参。

2.3 lambda格式

2.3.1 lambda表达式格式

三个部分组成:

  1. 一些参数()

    • 有参数就写参数,没参数就空着,多个参数用逗号分开。
  2. 一个箭头->

    • 把参数传递给方法体。
  3. 一段代码{}

    • 重写接口的抽象方法的方法体。

格式 : ()->{ //... }

2.3.2 lambda示例1
package LambdaDemo;

public interface Cooker {
    abstract void cooking();
}


package LambdaDemo;

public class LambdaTest {
    public static void main(String[] args) {
        //普通方式调用接口
        fun(new Cooker() {
            @Override
            public void cooking() {
                System.out.println("我在匿名内部类里做饭!");
            }
        });

        //Lambda方式调用
        fun(() -> {
            System.out.println("我在Lambda里做饭!");
        });
    }

    public static void fun(Cooker cooker) {
        cooker.cooking();
    }
}
//================输出==============
我在匿名内部类里做饭!
我在Lambda里做饭!
2.3.3 lambda表达式来排序
package LambdaDemo;

import java.util.Objects;

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

    public Person(String name, int age) {
        this.name = Objects.requireNonNullElse(name, "Default");
        this.age = age;
    }

    @Override
    public String toString() {
        return "[" +name +", "+ age + "]";
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

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

    public void setAge(int age) {
        this.age = age;
    }
}


package LambdaDemo;

import java.util.Arrays;

public class LambdaArraysSort {
    public static void main(String[] args) {
        Person p1 = new Person("张三", 20);
        Person p2 = new Person("李四", 25);
        Person p3 = new Person("王二", 20);
        Person[] people = new Person[]{p1, p2, p3};
        System.out.println(Arrays.toString(people));

        //Lambda表达式排序
        //年龄升序,年龄相同则姓名升序
        Arrays.sort(people, ((o1, o2) -> {
            if (o1.getAge() != o2.getAge()) {
                return o1.getAge() - o2.getAge();
            } else {
                return o1.getName().compareTo(o2.getName());
            }
        }));
        System.out.println(Arrays.toString(people));
    }
}

//================输出==============
[[张三, 20], [李四, 25], [王二, 20]]
[[张三, 20], [王二, 20], [李四, 25]]
2.3.4 使用有参数有返回值的lambda表达式

定义一个计算器接口,接收两个变量,返回一个结果。

package LambdaDemo.HasParamsAndReturn;

public interface Calculator<T> {
    abstract T calc(T o1, T o2);
}


package LambdaDemo.HasParamsAndReturn;

public class LambdaCalc {
    public static void main(String[] args) {
        myCalc("ss", "aa", (String o1, String o2) -> {
            return o1 + o2;
        });
    }

    public static <T> void myCalc(T o1, T o2, Calculator<T> calculator){
        T result = calculator.calc(o1,o2);
        System.out.println(result);
    }
}

//================输出==============
ssaa
2.3.5 省略格式的lambda表达式

因为lambda表达式可推导的部分都是可省略的,所以上文中的代码

(String o1, String o2) -> {return o1 + o2;});

可以改为

(o1, o2) -> o1 + o2

其中o1o2的类型均可推导,return关键词可以省略。

省略规则 :

  • 小括号内的参数类型可以省略。
  • 如果只有一个参数,小括号可省略。
  • 如果大括号内只有一条语句,可以省略return和分号和大括号,无论是否有返回值。

再举个例子 :

new Thread( () -> System.out.println("线程启动!") ).start();

2.4 lambda的使用前提

  1. 必须是只有一个抽象方法的接口。
  2. 必须具有上下文推断,

小提示 : 有且只有一个抽象方法的接口叫函数式接口

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值