A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

© 卞建彬 中级黑马   /  2018-9-20 15:21  /  772 人查看  /  0 人回复  /   0 人收藏 转载请遵从CC协议 禁止商业使用本文



卞建彬:


day03 List Set

数据结构

数据结构: 就是数据的存储方式
          不同的数据结构代表了不同的存储方式
          不同的数据结构中, 会有不同的存入, 取出, 查找的方式和效率, 数据的存放方式也不同


栈的特点:
    先进后出 (FILO, First In Last Out)
    入口和出口在同一侧
   
入栈(压栈): 将元素存入栈
出栈(弹栈): 从栈中取出元素

栈的适用场景:
    栈内存 (main方法先进栈调用, main方法中的其他方法都调用完毕后, main才能出栈)
    反转内容
队列

队列的特点:
    先进先出 (FIFO, First In First Out)
    入口和出口在两端
   
队列的适用场景:
    秒杀, 抢购
    在线售票
    处理高并发场景


数组
      
数组的特点:
    查询快: 通过 第一个元素地址值 + 索引 可以快速计算出该索引元素的地址值
    增删慢: 增加一个元素, 要创建长度+1的新数组, 然后将原数组元素复制到新数组, 然后存入新元素; 删除类似
        
数组的适用场景:
    查询多, 增删少的数据存储场景  
链表

链表: 链表由多个 节点(Node / Entry) 组成 ​
   
单向链表: 每个节点存储 数据 和 下一个节点的地址值​

双向链表: 每个节点存储 数据, 上一个节点地址值 和 下一个节点地址值
  ​
链表的特点:
    查询慢: 要找到其中某个节点, 只能从第一个节点一个一个向后寻找
    增删快: 只需要修改保存的下一个节点的地址值, 就可以快速完成增删
        
链表的适用场景:
    查询少, 增删多的场景
    链表可以实现栈和队列的结构, 因为栈和队列增删频繁

代码中链表节点的实现:

// 单向链表的节点
class Node<T> {
    T data;       // 存储的数据
    Node next;    // 下一个节点的地址值
}

// 双向链表的节点
class Node<T> {
    T data;       // 存储的数据
    Node before;  // 上一个节点的地址值
    Node after;   // 下一个节点的地址值
}

红黑树

红黑树: 是一种 平衡 二叉 查找 树
    平衡: 左子节点和右子节点数量相等
    二叉: 每个节点最多有2个子节点
    查找: 节点存储的元素是按照大小顺序存储的
    特点:
        元素存储过程中就完成了大小排序
        查询比链表快, 增删比数组快 (数组和链表的折中)

红黑树的适用场景:
    查询和增删都有, 需要元素自动排序的场景

代码中树节点的实现

// 树的节点
class Entry<T> {
    T data;           // 存储的数据
    Entry left;       // 左子节点的地址值
    Entry right;      // 右子节点的地址值
    Entry parent;     // 父节点的地址值
}

List集合

List介绍和常用方法
List集合体系的特点:
    1. 元素存取有序 (存入和取出元素的顺序一致)  
    2. 元素可以重复  
    3. 有索引
List子体系中的实现类都具有上述特点

java.util.List接口:
    // 常用特有成员方法 (都是按照索引来操作的)
    void add(int index, E element): 将指定的元素, 添加到该集合中的指定位置上
    E get(int index): 返回集合中指定位置的元素
    E remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素
    E set(int index, E element): 用指定元素替换集合中指定位置的元素, 返回值的更新前的元素

ArrayList集合的特点
ArrayList底层的数据结构:
    数组
   
ArrayList的特点:
    查询快
    增删慢
    线程不安全, 效率高
   
ArrayList适用场景:
    存储的数据"查询多, 增删少"的场景.


LinkedList集合的特点和特有方法

LinkedList底层的数据结构:
    链表
   
LinkedList的特点:
    查询慢
    增删快
    线程不安全, 效率高
   
LinkedList适用场景:
    存储的数据"查询少, 增删多"的场景.
   
java.util.LinkedList<E>类: 链表结构, 查询慢, 增删快
    // 特有成员方法(主要操作开头和末尾元素)
    void addFirst(E e): 将指定元素插入此列表的开头
    void addLast(E e): 将指定元素添加到此列表的结尾
    E getFirst(): 返回此列表的第一个元素
    E getLast(): 返回此列表的最后一个元素
    E removeFirst(): 移除并返回此列表的第一个元素
    E removeLast(): 移除并返回此列表的最后一个元素
    E pop(): (其实就是removeFirst())从此列表所表示的栈中弹出一个元素
    void push(E e): (其实就是addFirst())将元素添加到此列表所表示的栈中

Vector集合
JDK 1.0 版本中只有一个 Vector集合
JDK 1.2 开始增加了Collection集合体系

Vector底层的数据结构:
    数组
   
Vector的特点:
    查询慢
    增删快
    (同步)线程安全, 效率低
   
Vector目前几乎没人使用


Set集合体系

HashSet集合
Set集合体系特点:
    1. 元素不可重复
    2. 没有索引
   
HashSet特点:
    1. 元素不可重复
    2. 没有索引
    3. 元素存取无序 (存入和取出顺序有可能不一致)
    4. 底层采用 哈希表 结构. (查询快)
        哈希表 = 数组 + 链表或红黑树

java.util.HashSet类:
    // 常用方法
    boolean add(E e): 添加元素, 根据元素的 hashCode() 和 equals() 方法判断是否重复. 重复则不添加并返回false, 不重复则添加并返回true

HashSet原理: 哈希值
哈希值:
    一个十进制数值, 一般是通过将该对象的内部地址转换成一个整数来实现的

public native int hashCode();
    可以调用系统本地代码(C/C++)计算出一个对象地址的哈希值
   
hashCode()方法的作用
    方法内部的算法用于将对象计算为一个哈希值, 便于根据哈希值比较对象是否"相等"
    哈希值主要是为了提高对象存储在 哈希表 中的效率
   
注意:
    1. 如果我们不满意Object中的哈希值计算方法, 可以重写hashCode()方法.
       但在Java代码中不能直接重写带有 native 的方法, 重写时应该将 native 去掉
            @Override
            public int hashCode() {}
    2. hashCode() 方法有可能将"不同的对象"计算出"相同的哈希值", 这称为"哈希冲突", 在出现冲突后, 一般再通过 equals() 方法来继续判断对象是否"相等"
        
        "重地"  "通话"


HashSet原理: 哈希表结构

哈希表:
    JDK 8以前   : 哈希表 = 数组 + 链表
    JDK 8及之后 : 哈希表 = 数组 + 链表或红黑树
    数组中存储的每个元素, 是哈希值相同的一组节点的链表或红黑树

HashSet原理: 存储元素不重复的原理

HashSet集合保证添加元素不重复的原理:
    调用 add(E e) 添加元素时, 先调用 hashCode() 获取哈希值, 和当前HashSet集合中的元素比较
        如果哈希值不同, 则认为元素不重复, 添加, 并返回true
        如果哈希值相同, 则有可能是哈希冲突, 所以继续调用元素的 equals() 方法和所有哈希值相同的元素比较
            如果 equals() 比较所有元素都没有相同的, 则认为元素不重复, 添加, 并返回true
            如果 equals() 比较出有相同的元素, 则认为元素重复, 不添加, 并返回false

HashSet存储自定义元素的去重

自定义JavaBean对象实现在HashSet中去重:
    JavaBean默认继承Object类中的 hashCode() 和 equals() 方法, 都是根据对象地址值判断是否重复的
    要根据属性值判断是否重复, 应该在JavaBean类中重写 hashCode() 和 equals() 方法, 使其按照属性值比较

LinkedHashSet集合

LinkedHashSet特点:
    1. 元素存取有序 (存入和取出顺序一致)
    2. 元素不可重复
    3. 没有索引
   
LinkedHashSet底层数据结构:
    哈希表 + 链表   (也就是: 数组 + 链表或红黑树 + 链表)
    其中, 哈希表用于存储数据, 额外的链表用于记录元素添加时的先后顺序, 以便在获取元素时保持顺序一致

总结: 什么时候用List, 什么时候用Set?
    要存储的元素可以重复的, 用List集合:
        增删少, 用ArrayList
        增删多, 用LinkedList
    要存储的数据要求不重复, 或者相对一个集合去重, 用Set集合:
        不要求存取顺序一致, 用HashSet
        要求存取顺序一致, 用LinkedHashSet
可变参数

可变参数:
    JDK 5 出现. 指同一个类型参数个数可变的参数
    可变参数的本质就是一个"数组"
   
    格式: 用在方法的参数中
       修饰符 返回值类型 方法名(int... 变量名) {
           // 可以直接将 变量名 当作 数组名 使用
       }
       方法名();

注意事项:
    1. 可变参数可以传递的参数个数, 可以是 0个, 1个, 多个
    2. 一个方法的参数列表中, 只能有一个可变参数
    3. 如果方法的参数有多个, 可变参数必须写在参数列表的最后


Collections集合工具类

Collections集合工具类: addAll(), shuffle()

java.util.Collections类: 操作集合的工具类
    // 静态方法
    static <T> boolean addAll(Collection<? super T> c, T... elements):往集合中添加一些元素
    static void shuffle(List<?> list): 打乱集合顺序
    static <T> void sort(List<T> list): 将集合中元素按照默认规则排序
    static <T> void sort(List<T> list,Comparator<? super T> c):将集合中元素按照指定规则排序


Collections集合工具类: sort(List<T> list)

sort(List<T> list): 默认按照"升序"将元素排序
    数字, 字母, 都可以按照升序排序
   
自定义JavaBean对象默认不能排序, 因为不知道如何比较哪个对象大, 哪个对象小
自定义JavaBean对象要想排序, 需要实现 Comparable<E> 接口, 重写 int compareTo(E e) 方法
    规则:
        this-参数: 升序(从小到大)
        参数-this: 降序(从大到小)

Collections集合工具类: sort(List<T> listComparator<? super T> )

Comparable接口和Comparator接口区别
    Comparable: 让JavaBean自身具有可比较性 (自己和其他人比)
    Comparator: 定义一个比较器类, 用比较器对象比 (让第三个人来帮两个人比较)

Comparator使用方式:
    定义类作为比较器, 实现Comparator<E>接口, 重写 int compare(E o1, E o2) 方法, 泛型为要比较的元素的类型
    在Collections.sort(List<T> list,Comparator<? super T> c)方法中传入自定义比较器对象
    规则:
        o1-o2: 升序(从小到大)
        o2-o1: 降序(从大到小)

day04 Map集合
Map体系

Map集合概述

java.util.Map<K, V>接口

Map集合特点:
    1. 是双列集合, 一个元素包含两个值 (键key, 值value) 键值对
    2. key和value的类型可以相同, 也可以不同
    3. key不允许重复, value可以重复
    4. key和value是一一对应的, 一个键只能对应一个值

Map集合适合存储"一对一关系"的数据
   

public class Node<K, V> {
    K key;
    V value;
}

Map常用子类

java.util.Map<K, V>接口: 双列集合的根接口, 规定了共性的方法
    |
    |_ HashMap<K, V>类: 底层哈希表. key存取无序不可重复  
        |_ LinkedHashMap<K, V>类: 底层哈希表+链表. key存取有序不可重复
        
映射: 键和值的对应关系 mapping

HashSet底层使用的就是HashMap
LinkedHashSet底层使用的就是LinkedHashMap


Map中常用方法

java.util.Map接口: 双列集合的顶层
    // 成员方法
    V put(K key, V value): 添加/修改键值对. 如果键存在, 则用新值替换已有值
    V remove(Object key): 根据键删除键值对, 返回被删除元素的值
    V get(Object key): 根据键获取值. 如果键不存在, 则返回null
    boolean containsKey(Object key): 判断是否包含指定的键
   
    Set<K> keySet(): 获取Map集合中所有的键, 存储到Set集合中
    Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的Entry对象的集合(Set集合)
   
   
    public class Entry<K,V> {  // 键值对 对象
        K key;
        V value;
   }



Map遍历方式一: keySet()方法实现通过键找值
Set<K> keySet(): 获取Map集合中所有的键, 存储到Set集合中
   
keySet()遍历步骤:
    1. Map对象调用 keySet() 方法, 获取包含所有key的Set集合
    2. 遍历Set集合, 获取每个key
    3. 通过Map对象调用 get(Object key) 方法根据key获取到value
   
   

Entry键值对对象介绍
java.util.Map.Entry接口:
    // 常用成员方法
    K getKey(): 获取Entry对象中的键
    V getValue(): 获取Entry对象中的值
   
Entry对象就是一个节点, 节点中存储了key和value
拿到一个Entry对象就可以从中获取key和value
   
   ​
java.util.Map接口
    Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)

Map遍历方式2: 通过entrySet()获取Entry对象形式遍历
java.util.Map接口
    Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)
   
entrySet()方法遍历Map步骤:
    1. Map对象调用 entrySet() 获取包含所有Entry对象的Set集合
    2. 遍历Set集合, 获取每个Entry对象
    3. 调用Entry对象的 getKey() 和 getValue() 方法获取键和值
   


HashMap存储自定义数据类型作为键

HashMap存储自定义JavaBean对象作为key保证key唯一不重复, 需要让JavaBean重写 hashCode() 和 equals() 方法

LinkedHashMap

LinkedHashMap底层: 哈希表 + 链表
    key不允许重复, 但key存取有序

LinkedHashSet的底层就是使用LinkedHashMap实现的
HashSet  HashMap

Hashtable

Hashtable和HashMap:
    相同点:
        1. 底层都是哈希表
    不同点:
        1. Hashtable不允许存储 null 值和 null 键; HashMap允许存储 null 值和 null 键
        2. Hashtable线程安全效率低; HashMap线程不安全效率高
        
Hashtable目前很少使用.
但其子类 Properties 集合, 可以与IO流结合使用, 应用较多
JDK9对集合添加的优化

使用集合添加大量元素时, 反复add(...)比较麻烦. JDK 9 为集合提供了一些静态方法, 可以方便的对集合进行初始化

java.util.List
    // 静态方法
    static List<E> of(E... e): 返回包含指定元素的 不可变List 集合
   
java.util.Set
    // 静态方法
    static Set<E> of(E... e): 返回包含指定元素的 不可变Set 集合
   
java.util.Map
    // 静态方法
    static Map<K, V> of(K k1, V v1, ...): 返回包含指定键值对的 不可变Map 集合
   
注意事项:
    1. of() 方法只适用于List接口, Set接口, Map接口, 不适用于接口的实现类
    2. of() 方法的返回值是一个不可变的集合, 集合不能再使用 add(), put() 方法添加元素, 会抛出异常
    3. Set接口和Map接口在调用 of() 方法的时候, 不能有重复的元素, 否则会抛出异常

将 不可变集合 的元素转移到常见集合实现类中:
    //List
    ArrayList<Integer> list = new ArrayList<>(List.of(1,2,3));

    //Set
    HashSet<Integer> set = new HashSet<>(Set.of(1,2,3));

   
// Map
  HashMap<Integer, String> map = new HashMap<>(Map.of(1,"a",2,"b"));



Debug调试模式介绍

断点:
    breakpoint, 在debug模式下, 程序运行到断点会暂停住, 便于我们在程序运行过程中查看
Debug调试程序:
    可以让代码逐行执行,查看代码执行的过程,调试程序中出现的bug
使用方式:
    在行号的右边,鼠标左键单击,添加断点(每个方法的第一行,哪里有bug添加到哪里)
    右键,选择Debug执行程序
    程序就会停留在添加的第一个断点处
执行程序:
    f8:逐行执行程序
    f7:进入到方法中
    shift+f8:跳出方法
    f9:跳到下一个断点,如果没有下一个断点,那么就结束程序
    ctrl+f2:退出debug模式,停止程序
    Console:切换到控制台

day05 异常 多线程
异常

异常的概念和体系

异常: 指的是程序在执行过程中, 出现的非正常的情况, 最终会导致JVM的非正常停止
    注意:
        在Java中, 每种异常都有对应的类来描述, 发生了一个异常, 就是出现了这个异常类的"对象"
        异常不是语法错误
   
// 异常的体系结构
java.lang.Throwable  // 体系最顶层
    |_ Error        // 不应该试图捕获的严重问题, 不能处理的错误
    |_ Exception    // 可以处理的异常
// 异常信息详解:
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.HashMap$HashIterator.nextEntry(Unknown Source)
    at java.util.HashMap$KeyIterator.next(Unknown Source)
    at com.itheima.practice_08.MapTest.main(MapTest.java:31)

解释:
    Exception in thread "main": 在名为"main"的线程发生异常
    java.util.ConcurrentModificationException: 异常的具体类型, 这里是并发修改异常
    at xxx.xxx.xxx: 异常发生的位置, 因为层层方法调用, 所以会出现多个类的方法, 这些方法组成了异常链
        只要不是我们的包名, 一般不用看. 是我们的包名, 才看
        at com.itheima.practice_08.MapTest.main(MapTest.java:31):
            说明这个异常, 发生在com.itheima.practice_08下的MapTest类的main方法中, 具体是MapTest.java文件中的第31行
        一般 (MapTest.java:31) 这一部分是可以鼠标点击的, 点击后就直接跳转到报错的这一行


异常的分类

// 异常的体系结构
java.lang.Throwable  // 体系最顶层
    |_ Error                 // 错误
    |_ Exception             // 编译时异常
        |_ RuntimeException   // 运行时异常

错误(Error): 不能捕获处理的严重问题.
必须将程序停下来, 修改代码才能解决
    错误的类名都是 "XxxError" 方式
异常(Exception): 可以捕获处理的问题.

    程序执行起来后, 如果有合适的处理方式, 即使发生异常, 程序也能处理该异常并继续运行
    异常的类名都是 "XxxException" 方式
    1. 编译时异常:
        编译时期就会发生的异常, 必须在编译时期处理  
2. 运行时异常:
        编译时正常, 运行时才会发生的异常


异常的产生过程解析

异常是如何产生的

异常产生的过程:
    1. 当执行的代码发生异常操作时, JVM会创建一个对应的异常类对象, 包含异常的内容, 原因, 位置
    2. 如果执行代码的方法没有对异常进行 try...catch 处理, 则会向该方法调用处的方法抛(向上层抛). 如果所有方法(包括main()方法)都没有 try...catch 处理异常, 则该异常会被JVM按照默认的处理方式处理
    3. JVM对于异常的默认处理方式是:
        1. 将异常信息(内容, 原因, 位置)打印到控制台
        2. 终止当前的程序



异常关键字: throw制造异常

throw 关键字有什么作用
throw 制造一个异常的语法格式是怎样的

throw关键字作用:
    在方法中抛出指定的异常对象
   
格式:
    throw new 异常类名("异常原因字符串");

    ​
注意:
    1. throw 必须写在方法的内部
    2. throw 后面new的异常对象, 必须是 "Exception" 或 "Excetion的子类" 的对象
    3. 一个方法内部 throw 了一个异常对象, 则该方法可以分为2种情况来处理该异常:
        如果 throw 的是"运行时异常"(RuntimeException及其子类)对象, 那么可以不处理
            该异常最终会交给JVM处理, 结果就是: 打印异常信息到控制台, 并立刻结束程序
        如果 throw 的是"编译时异常"(Exception及其子类), 则必须处理:
            处理方式1: throws 抛出
            处理方式2: try...catch 捕获


Objects工具类方法: requireNonNull()非空判断

java.util.Objects: 操作各种对象相关的工具类
    // 静态方法
    static <T> T requireNonNull(T obj): 检查传入参数是不是null. 是null则抛异常; 非null则返回该对象
    static <T> T requireNonNull(T obj, String message): 检查传入参数是不是null. 是null则抛异常, 同时带有原因; 非null则返回该对象
   
可以用来简化代码的编写:
    // 普通方式
    if (obj == null) {
        throw new NullPointerException();
   }

    // Objects.requireNonNull(T obj)方式
    Objects.requireNonNull(obj);


异常的处理方式1: throws声明抛出异常


提前了解IO用到的一些异常类名:
    FileNotFoundException: 文件找不到时抛出的异常
    IOException: 只要是IO操作的异常都属于该异常, 是FileNotFoundException的父类

异常处理的第一种方式:
    throws, 声明抛出 (方法自己不处理, 交给方法调用者处理, "甩锅给别人")
    作用: 告诉方法的调用者, 调用此方法有可能发生异常, 需要在编写代码时处理

格式:
    修饰符 返回值类型 方法名() throws 异常类名1, 异常类名2, ... {
      
    }

​注意:
    1. throws 必须写在方法声明上
    2. throws 后面的异常类名, 一般是 Exception 或 Exception的子类
        (RuntimeException及其子类也行, 但是没有什么意义)
    3. 方法内部如果抛出了多个异常对象, throws 后也必须声明多个异常
        如果抛出的异常对象有子父类关系, 那么直接声明父类异常即可
    4. 调用了一个带有 throws 声明抛出异常的方法, 就必须处理该异常:
        要么继续声明 throws 抛出
        要么 try...catch 捕获处理异常


异常处理方式2: 捕获异常

try...catch:
    捕获并处理异常 (方法内部自己处理异常, 不交给别人, "自己背锅")

格式:

    try {
        // 可能产生异常的代码
   } catch (异常类名 变量名) {
        // 处理异常的代码
        // 一般会将异常信息存储到日志中
   }
    ...
    } catch (异常类名 变量名) {
        // 处理异常的代码
        // 一般会将异常信息存储到日志中
   }

注意:
    1. try 中可能会抛出多种异常, 就可以写多个 catch 分别处理每种异常
    2. 如果 try 中产生了异常, 就会从产生异常的那一行代码直接跳转到对应的 catch 中执行处理代码, 然后继续执行 try...catch 之后的其他代码; 如果 try 中没有产生异常, 那就不会执行 catch , 执行完 try 中的代码后, 继续执行 try...catch 之后的其他代码
   

// JDK 7增加
try {
    // 可能发生异常的代码
} catch (异常类型1 | 异常类型2 | 异常类型3 | ... 异常对象名) {
    // 处理任意一个异常的代码
}

try {
    // 可能发生异常的代码
} catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
    // 处理异常的代码
}

Throwable中的3个异常处理方法

java.lang.Throwable
    // 成员方法
    String getMessage(): 异常的信息. 没有原因返回null
    String toString(): 异常的类型和原因信息
    void printStackTrace(): 使用标准错误输出流打印异常信息


finally代码块
格式:

    try {
        // 可能发生异常的代码
   } catch(异常类型 异常变量名) {
        // 处理异常
   }
    ...
    catch(异常类型 异常变量名) {
        // 处理异常
   } finally {
        // 无论是否发生异常, 是否捕获, 最后都会执行的代码.
        // 通常在这里执行释放资源的操作
   }

注意:
    1. finally 必须和 try...catch 一起使用
    2. finally 一般用于释放资源 (IO流时用到)

异常注意事项1: 捕获多个异常的3种方式

捕获多个异常:
    1. 多个异常分别 try...catch 处理
    2. 一个 try 多个 catch
        如果异常存在继承关系, 子类异常在上, 父类异常在下
    3. 多个异常, 一次捕获一次处理
        用Exception多态捕获
   
    //1. 多个异常分别try...catch处理
    try {
       // 可能发生异常的代码
   } catch (异常类型 变量名) {
       // 处理异常
   }
   
    try {
       // 可能发生异常的代码
   } catch (异常类型 变量名) {
       // 处理异常
   }
   
    //2. 一个try多个catch
    try {
        // 可能发生异常的代码
        // 可能发生异常的代码
        // 可能发生异常的代码
   }catch (异常类型1 变量名){  // 子类异常在上
        // 处理异常1
   }catch (异常类型2 变量名){  // 父类异常在下
        // 处理异常2
   }

    //3.
    try {
        // 可能发生异常的代码
        // 可能发生异常的代码
        // 可能发生异常的代码
        // 可能发生异常的代码
        // 可能发生异常的代码
   }catch (Exception e){  // 父类异常多态接收
        // 处理异常
   }



运行时异常, 可以不抛出 throws 也不捕获 try catch, 交给JVM处理
   

异常注意事项2: finally中有return语句

如果 finally 代码块中有 return 语句, 则永远返回 finally 中的 return 语句的值
应该避免在 finally 中写 return 语句

异常注意事项3: 子父类继承重写方法时的异常要求

子父类继承关系下, 子类重写父类带有throws的方法:
    1. 如果父类抛出多个异常, 子类重写父类方法时可以有3种方式:
        a: 抛出和父类相同的异常
        b: 抛出父类异常的子类
        c: 不抛出异常
    2. 父类方法没有抛出异常, 子类重写父类该方法时也不可抛出异常
        此时子类产生该异常, 只能捕获处理, 不能声明抛出
   
   
一般情况下:
    父类方法声明的异常是什么样的, 子类重写的方法声明异常就什么样, 保持一致即可

自定义异常类

如果Java提供的异常类不足以满足我们的需求, 我们也可以自己定义异常类
    定义编译时异常: 继承 Exception
    定义运行时异常: 继承 RuntimeException


为了在抛出异常时, 显示一些提示信息, 我们可以定义2个构造, 一个无参, 一个有String参数


多线程

计算机基本概念: 并发与并行

并发: (交替执行) 指两个或多个事件在"同一时间段内"发生
并行: (同时执行) 指两个或多个事件在"同一时刻"发生 (同时发生)
   
我们研究的是"并发"

并发的实现, 是依靠电脑CPU快速地在多个任务之间切换执行实现的

计算机基本概念: 进程

进程: 一个应用程序在内存中的一次执行过程

    每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程
    进程也是程序的一次执行过程,是系统运行程序的基本单位
    系统运行一个程序即是一个进程从创建、运行到消亡的过程

计算机基本概念: 线程

线程: 是进程内的一个独立执行单元 (一条代码执行路径)
    一个程序运行后至少有一个进程, 一个进程中可以包含多个线程
   
    公司 > 部门 > 员工
    程序 > 进程 > 线程

多线程的好处:
    效率高
    多个线程之间互不影响

线程的调度
多个线程要并发执行, 哪个线程被执行, 哪个线程不被执行, 就是调度

线程的调度方式:
    1. 分时调度: 所有线程轮流使用CPU, 平分占用CPU的时间
    2. 抢占式调度: 优先让优先级高的线程使用CPU; 如果优先级相同, 则随机选择一个线程执行
   
   Java使用的是"抢占式"调度

主线程

Exception in thread "main"


主线程:
    我们以前编写的代码, 也在一条线程中执行, 该线程叫作"main线程", 也称为"主线程"

如果我们没有额外创建线程, 那么我们的程序就只有一个线程, 即主线程, 此时程序是"单线程"的
单线程的执行特点:
    同一个线程内的代码, 从上往下依次执行

创建多线程程序的第一种方式: 继承Thread

实现多线程的第一种方式:
    1. 定义类, 继承 Thread 类
    2. 重写 run() 方法, run方法内部是线程要执行的任务
    3. 创建Thread子类的对象, 调用 start() 方法启动线程

java.lang.Thread类: 表示线程. 实现了Runnable接口
    void start(): 启动线程, 即让线程开始执行run()方法中的代码
   
注意:
    必须调用 start() 方法来开启线程, 不能直接调用 run() 方法, 调用 run() 会变成单线程
    同一个线程对象, 不能多次调用 start() 方法
    Java是抢占式调度, 不同线程的代码, 执行顺序是随机的


day06 线程 同步 线程间通信
线程
多线程原理1: 线程执行的随机性
CPU执行哪个线程是随机的, 不能人为干预
Java线程调度是抢占式的, 多个线程互相抢夺CPU的执行权

CPU高速随机切换 (本质)

多线程原理2: 多线程的内存

多线程情况下, 每个线程都有各自的栈内存
每个线程各自的方法调用, 进的是各自线程的栈
栈是每个线程各自的, 堆是所有线程共用的

Thread常用方法:

java.lang.Thread类: 表示线程. 实现了Runnable接口
    // 构造方法
    Thread(): 创建Thead对象
    Thread(String threadName): 创建Thead对象并指定线程名
    Thread(Runnable target): 通过Runnable对象创建Thread对象
    Thread(Runnable target, String threadName): 通过Runnable对象创建对象并指定线程名
    // 成员方法
    void run(): 用于让子类重写, 表示该线程要执行的任务.不能直接调用
    void start(): 启动线程, 即让线程开始执行run()方法中的代码
    String getName(): 获取线程的名称
    void setName(String name): 设置线程名称
    // 静态方法
    static Thread currentThread(): 返回对当前正在执行的线程对象的引用
    static void sleep(long millis): 让所在线程睡眠指定的毫秒




创建多线程程序的方式2: 实现Runnable接口

创建线程的第2种方式:
    1. 定义类, 实现Runnable接口
    2. 重写 run() 方法, 要执行的代码(任务)
    3. 创建Runnable实现类对象 (任务对象)
    4. 创建Thread类对象, 在构造方法中传入Runnable实现类对象 (将任务和线程绑定)
    5. 通过Thread对象调用start()方法启动线程

java.lang.Thread类: 表示线程. 实现了Runnable接口
    // 构造方法
    Thread Thread(Runnable target): 通过Runnable对象创建Thread对象
    Thread Thread(Runnable target, String threadName): 通过Runnable对象创建对象并指定线程名


Thread和Runnable的区别

实现Runnable的好处:
    1. 避免单继承的局限性
    2. 增强了程序的扩展性, 降低了程序的耦合性(解耦)
        线程是Thread, 任务是Runnable实现类对象. 相当于将线程和任务分离

耦合性: 相互之间的关系的紧密程度
耦合性高: 相互之间的关系非常紧密
耦合性低: 相互之间的关系不太紧密
我们追求 "低耦合"
匿名内部类方式创建线程




线程安全问题
模拟电影院卖票: 线程安全问题概述




模拟电影院卖票: 代码实现

多个卖票窗口 : 多个Thread对象
卖票任务    : Runnable实现类, 重写run()方法
共享的票    : 因为多个Thread对象使用同一个Runnable实现类对象, 所以将票数定义为Runnable实现类的成员变量





线程安全问题的原因
问题发生场景:
    多个线程操作共享资源
问题发生原因:
    JVM是抢占式调度, CPU在每个线程之间切换是随机的, 代码执行到什么位置是不确定的
    在操作共享资源时, 由于一个线程还没有执行完, 另一个线程就来操作, 就会出现问题
如何解决:
    在操作共享资源时, 让线程一个一个来执行, 不要并发操作共享变量, 就可以解决问题

解决线程安全问题方式1: 同步代码块

解决多线程操作共享数据的安全问题的3种方式:
    1. 同步代码块
    2. 同步方法
    3. Lock锁机制

同步代码块: 使用 synchronized 关键字修饰的代码块, 并传入一个当作锁的对象
格式:
    synchronized (锁对象) {
        // 操作共享数据的代码

    }


注意:
    锁对象可以是"任意类型的一个对象"
    锁对象必须是"被多个线程共享的唯一的"对象
    锁对象的作用: 只让一个线程在同步代码块中执行



同步技术解决线程安全问题的原理

锁对象, 也称为"同步锁", "对象监视器"

同步的原理:
    线程进入同步代码块前, 会争夺锁对象, 只有一个线程会抢到锁对象
    进入同步代码块的线程, 会持有锁对象, 并执行同步代码块中的代码
    此时同步代码块外的线程, 处于阻塞状态, 只能等待
    当同步代码块内的线程执行完代码块, 会离开同步代码块, 并归还锁对象给同步代码块
    等在同步代码块外的其他线程就可以继续争夺锁对象


解决线程安全问题方式2: 同步方法

同步方法: 使用 synchronized 关键字修饰的方法, 具有默认的锁对象

非静态同步方法的锁对象: this

    // 非静态同步方法
    public synchronized void method(){
        // 可能会产生线程安全问题的代码
    }

静态同步方法

静态同步方法:
    public static synchronized void method(){
        // 可能会产生线程安全问题的代码
    }



静态同步方法的锁对象: 当前类的字节码对象 Class对象


获取一个类的字节码对象的3种方式:
    1. 对象名.getClass()         
    2. 类名.class                 
    3. Class.forName("类的全名");  

字节码对象的特点:
    同一个类, 他的字节码对象只有"唯一的一个"

锁对象必须是多个线程共享的同一个对象


解决线程安全问题方式3: Lock锁

java.util.concurrent.locks.Lock接口: JDK 5 新增的Lock接口
    // 成员方法
    void lock(): 获取锁
    void unlock(): 释放锁

java.util.concurrent.locks.ReentrantLock类: Lock的实现类

使用方式:

public class RunnableImpl implements Runnable {
    // 成员变量创建锁对象, 该锁对象也要所有线程共享唯一一个
    Lock lock = new ReentrantLock();

    @Override
    public void run() {
        // 加锁
        lock.lock();
        try {
            // 操作共享变量的代码...
        } finally {
            // 在finally中保证释放锁
            lock.unlock();  
        }
    }
}

线程安全和效率的特点:
    线程安全, 效率低
    线程不安全, 效率高

线程间的通信
线程状态概述

Object类中关于线程的方法:
java.lang.Object类:
    // 成员方法 (<<注意>>: 只能通过"锁对象"调用)
    void notify(): 随机唤醒在同一个锁对象上的某一个处于等待状态的线程
    void notifyAll(): 唤醒所有在同一个锁对象上处于等待状态的线程
    void wait(): 让当前线程处于"无限等待"状态
    void wait(long timeout): 让当前线程处于"计时等待"状态, 时间到或被唤醒后结束此状态
    void wait(long timeout, int nanos): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态
    "注意!! 以上方法只能通过锁对象调用"

线程的生命周期中, 可以出现有6种状态:
    1. "NEW 新建"
       线程被创建, 但没有调用 start() 启动
    2. "RUNNABLE 可运行"
       调用 start()方法后已启动, 但可能正在执行 run() 方法的代码, 也可能正在等待CPU的调度
    3. "BLOCKED (锁)阻塞"
       线程试图获取锁, 但此时锁被其他线程持有
    4. "WAITING 无限等待"
       通过锁对象调用无参的 wait() 进入此状态.
       等待其他线程通过锁对象执行 notify() 或 notifyAll() 才能结束这个状态
    5. "TIMED_WAITING 计时等待"
       如通过锁对象调用有参的 wait(long millis) 或 sleep(long millis), 则进入此状态.
       直到时间结束之前被其他线程通过锁对象执行 notify()或 notifyAll()唤醒, 或时间结束自动唤醒
    6. "TERMINATED 终止"
       run()方法结束(执行结束, 或内部出现异常), 则进入此状态


等待唤醒案例: 生产者消费者问题需求分析

老板(生产者)是一个线程, 该线程的任务是: 生产包子, 然后通知顾客来吃
顾客(消费者)是另外一个线程, 该线程的任务是: 消费包子, 然后让老板做

两个线程之间, 必须顾客要包子, 然后老板做包子, 然后顾客吃包子, 这是相互协作的场景, 所以线程之间要相互进行通信, 告知彼此应该谁来做事



wait() 和 sleep() 的区别:
    1. wait会释放锁, 恢复时需要重新获取锁; sleep不会释放锁
    2. wait可以被notify/notifyAll唤醒; sleep不会
    3. wait要用锁对象调用; sleep要用Thread类名调用
________________________________________






0 个回复

您需要登录后才可以回帖 登录 | 加入黑马