第一章 Java核心内容

一、数据结构

1. 数组

特点:
内存地址连续,使用之前必须要指定数组长度。
可以通过下标访问的方式访问成员,<span style="font-weight:bold;color:red">查询效率高</span>
(增删效率低)增删操作会给系统带来<span style="font-weight:bold;color:red">性能消耗 </span>[保证数据下标越界问题,需要动态扩容]

2. 链表

链表常用的有 3 类:单链表、双向链表、循环链表。

2.1 单链表

单链表实现图示:

Data 数据 + Next 指针,组成一个单链表的内存结构 ; 第一个内存结构称为 链头,最后一个内存结构称为 链尾; 链尾的。
Next 指针设置为 NULL [指向空]; 单链表的遍历方向单一(只能从链头一直遍历到链尾)。
单链表操作集:


2.2 双向链表

双向链表实现图示:

Data 数据 + Next 指针,组成一个单链表的内存结构 ; 第一个内存结构称为 链头,最后一个内存结构称为 链尾; 链尾的
Next 指针设置为 NULL [指向空]; 单链表的遍历方向单一(只能从链头一直遍历到链尾)。
双向链表操作集:

2.3 循环链表


循环链表分为单向、双向两种; 单向的实现就是在单链表的基础上,把链尾的 Next 指针直接指向链头,形成一个闭环;
双向的实现就是在双向链表的基础上,把链尾的 Next 指针指向链头,再把链头的 Prev 指针指向链尾,形成一个闭环;
循环链表没有链头和链尾的说法,因为是闭环的,所以每一个内存结构都可以充当链头和链尾;
循环链表操作集:





3. 树

树(tree)是一种抽象数据类型(ADT),用来模拟具有树状结构性质的数据集合。它是由 n(n>0)个有限**节点**通过连接它们的**边**组成一个具有层次关系的集合。把它叫做"树"是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
它具有以下的特点:
每个节点都只有有限个子节点或无子节点;
没有父节点的节点称为根节点;
每一个非根节点有且只有一个父节点;
除了根节点外,每个子节点可以分为多个不相交的子树;
树里面没有环路(cycle)。
为什么需要树?
因为它结合了另外两种数据结构的优点: 一种是有序数组,另一种是链表。在树中查找数据项的速度和在有序数组中查找一样快, 并且插入数据项和删除数据项的速度也和链表一样。
在有序数组中插入数据项太慢
假设数组中的所有数据项都有序的排列---这就是有序数组,用二分查找法可以在有序数组中快速地查找特定的值。
二分查找法的过程是先査看数组的正中间的数据项,如果那个数据项值比要找的大,就缩小査找范围,在数组的后半段中找;如果小, 就在前半段找。反复这个过程,查找数据所需的时间是 O(logN)。同时也可以迅速地遍历有序数组, 按顺序访问每个数据项。
然而,想在有序数组中插入一个新数据项,就必须首先査找新数据项插入的位置,然后把所有 比新数据项大的数据项向后移动一位,来给新数据项腾出空间。这样多次的移动很费时,平均来讲 要移动数组中一半的数据项(N/2 次移动)。
删除数据项也需要多次的移动,所以也很慢。 显而易见,如果要做很多的插入和删除操作,就不该选用有序数组。
在链表中查找太慢
链表的插入和删除操作都很快。它们只需要改变一些引用的值就行了。这些操作的时间复杂度是 0(1)(是大 O 表示法中最小的时间复杂度)。
但是在链表中查找数据项可不那么容易。查找必须从头开始,依次访问链表中的每一个数据项,把每个数据项的值和要找的数据项做比较,直到找到该数据项为止,平均需要访问 N/2 个数据项。这个过程很慢,费时 O(N)(注意,对排序来说比较快的,对数据结构操作来说是比较慢的。)。
我们可以通过有序的链表来加快查找速度,链表中的数据项是有序的,但这样做是没有用的。即使是有序的链表还是必须从头开始依次访问数据项,因为链表中不能直接访问某个数据项,必须通过数据项间的链式引用才可以。(当然有序链表访问节点还是比无序链表快多了,但查找任意的数据项时它也无能为力了。)
树的种类
无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树;
有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树;
二叉树:每个节点最多含有两个子树的树称为二叉树;
完全二叉树:对于一颗二叉树,假设其深度为 d(d>1)。除了第 d 层外,其它各层的节点数目均已达最大值,且第 d 层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树;
满二叉树:所有叶节点都在最底层的完全二叉树;
平衡二叉树(AVL 树):当且仅当任何节点的两棵子树的高度差不大于 1 的二叉树;
排序二叉树(二叉查找树(英语:Binary Search Tree)):也称二叉搜索树、有序二叉树;
霍夫曼树:带权路径最短的二叉树称为哈夫曼树或最优二叉树;
B 树:一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有多于两个子树。
树的抽象(ADT)
树作为一种抽象的数据类型,至少要支持以下的基本方法。

java 复制代码
| 方法名               | 描述 |
| ------------------- | ----------------------------------------------- |
| getElement()        | 返回存放于当前节点处的对象 |
| setElement(e)       | 将对象 e 存入当前节点,并返回其中此前所存的内容 |
| getParent()         | 返回当前节点的父节点 |
| getFirstChild()     | 返回当前节点的长子 |
| getNextSibling()    | 返回当前节点的最大弟弟 |
3.1 树的实现
3.1.1 使用数组实现

树可以使用数组实现,节点在数组中的位置对应于它在树中的位置。
下标为 0 的节点是根,下标为 1 的节点是根的左子节点,依次类推,按从左到右的顺序存储树的每一层。

树中的每个位置,无论是否存在节点,都对应数组中的一个位置。把节点插入树的一个位置, 意味着要在数组的相应位置插入一个数据项。树中没有节点的位置在数组中的对应位置用 0 或 null 来表示。
基于这种思想,找节点的子节点和父节点可以利用简单的算术计算它们在数组中的索引值。设节点索引值为 index,则节点的左子节点是: 2 * index + 1,它的右子节点是 **2 * index + 2**,它的父节点是(index - 1)/ 2。
大多数情况下用数组表示树不是很有效率。不满的节点和删除掉的节点在数组中留下了洞,浪费存储空间。更坏的是,删除节点时需要移动子树的话,子树中的每个节点都要移到数组中新的位置去,这在比较大的树中是很费时的。
不过,如果不允许删除操作,数组表示可能会很有用。

3.1.2 使用链表实现
java 复制代码
public interface Tree {
    /**
    * 返回当前节点中存放的对象
    *
    * @return Object */
    Object getElem();
    /**
    * 将对象 obj 存入当前节点,并返回此前的内容
    *
    * @return Object */
    Object setElem(Object obj);
    /**
    * 返回当前节点的父节点
    *
    * @return TreeLinkedList */
    TreeLinkedList getParent();
    /**
    * 返回当前节点的长子
    *
    * @return TreeLinkedList */
    TreeLinkedList getFirstChild();
    /**
    * 返回当前节点的最大弟弟
    *
    * @return TreeLinkedList */
    TreeLinkedList getNextSibling();
    /**
    * 返回当前节点后代元素的数目,即以当前节点为根的子树的规模
    *
    * @return int */
    int getSize();
    /**
    * 返回当前节点的高度
    *
    * @return int */
    int getHeight();
    /**
    * 返回当前节点的深度
    *
    * @return int */
    int getDepth();
}

对应实现

java 复制代码
public class TreeLinkedList implements Tree {
    /**
    * 树根节点
    */
    private Object element;
    /**
    * 父节点、长子及最大的弟弟
    */
    private TreeLinkedList parent, firstChild, nextSibling;
    /**
    * (单节点树)构造方法
    */
    public TreeLinkedList() {
        this(null, null, null, null);
    }
    /**
    * 构造方法
    *
    * @param object 树根节点
    * @param parent 父节点
    * @param firstChild 长子
    * @param nextSibling 最大的弟弟
    */
    public TreeLinkedList(Object object, TreeLinkedList parent, TreeLinkedList firstChild, TreeLinkedList nextSibling) {
        this.element = object;
        this.parent = parent;
        this.firstChild = firstChild;
        this.nextSibling = nextSibling;
    }
    public Object getElem() {
        return element;
    }
    public Object setElem(Object obj) {
        Object bak = element;
        element = obj;
        return bak;
    }
    public TreeLinkedList getParent() {
        return parent;
    }
    public TreeLinkedList getFirstChild() {
        return firstChild;
    }
    public TreeLinkedList getNextSibling() {
        return nextSibling;
    }
    public int getSize() {
        //当前节点也是自己的后代
        int size = 1;
        //从长子开始
        TreeLinkedList subtree = firstChild;
        //依次
        while (null != subtree) {
            //累加
            size += subtree.getSize();
            //所有孩子的后代数目
            subtree = subtree.getNextSibling();
        }
        //得到当前节点的后代总数
        return size;
    }
    public int getHeight() {
        int height = -1;
        //从长子开始
        TreeLinkedList subtree = firstChild;
        while (null != subtree) {
            //在所有孩子中取最大高度
            height = Math.max(height, subtree.getHeight());
            subtree = subtree.getNextSibling();
        }
        //即可得到当前节点的高度
        return height + 1;
    }
    public int getDepth() {
        int depth = 0;
        //从父亲开始
        TreeLinkedList p = parent;
        while (null != p) {
            depth++;
            //访问各个真祖先
            p = p.getParent();
        }
        //真祖先的数目,即为当前节点的深度
        return depth;
    }
}
3.1.3 树的遍历

所谓树的遍历(Traversal),就是按照某种次序访问树中的节点,且每个节点恰好访问一次。也就是说,按照被访问的次序,可以得到由树中所有节点排成的一个序列。
前序遍历
对任一(子)树的前序遍历,将首先访问其根节点,然后再递归地对其下的各棵子树进行前序遍历。对于同一根节点下的各棵子树,遍历的次序通常是任意的;但若换成有序树,则可以按照兄弟间相应的次序对它们实施遍历。由前序遍历生成的节点序列,称作前序遍历序列。

树的前序遍历序列:{a, b, e, j, k, f, l, p, c, d, g, m, h, n, o, i}
后续遍历
对称地,对任一(子)树的后序遍历将首先递归地对根节点下的各棵子树进行后序遍历,最后才访问根节点。由后序遍历生成的节点序列,称作后序遍历序列。

树的后序遍历序列:{j, k, e, p, l, f, b, c, m, g, n, o, h, i, d, a}
层次遍历
除了上述两种最常见的遍历算法,还有其它一些遍历算法,层次遍历(Traversal by level )算法就是其中的一种。在这种遍历中,各节点被访问的次序取决于它们各自的深度,其策略可以总结为"深度小的节点优先访问"。
对于同一深度的节点,访问的次序可以是随机的,通常取决于它们的存储次序,即首先访问由 firstChild 指定的长子,然后根据 nextSibling 确定后续节点的次序。当然,若是有序树,则同深度节点的访问次序将与有序树确定的次序一致。

树的层次遍历序列:{a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p}

3.2 二叉树

二叉树具有如下特点:
某节点的左子树节点值仅包含小于该节点值;
某节点的右子树节点值仅包含大于该节点值;
左右子树每个也必须是二叉查找树;
顺序排序。

3.2.1 二叉树的实现

Node 类
首先,需要有一个节点对象的类。这些对象包含数据,数据代表要存储的内容(例如,在员工 数据库中的员工记录),而且还有指向节点的两个子节点的引用。

java 复制代码
import lombok.Data;

@Data
public class Node<T> {
    /**
    * 角标
    */
    private Integer index;
    /**
    * 数据
    */
    private T data;
    /**
    * 左节点
    */
    private Node leftChild;
    /**
    * 右节点
    */
    private Node rightChild;
    /**
    * 构造函数
    *
    * @param index 角标
    * @param data 数据
    */
    public Node(Integer index, T data) {
        this.index = index;
        this.data = data;
        this.leftChild = null;
        this.rightChild = null;
    }
}

Tree 类
还需要有一个表示树本身的类,由这个类实例化的对象含有所有的节点,这个类是 Tree 类。它只有一个数据字段:一个表示根的 Node 变量。它不需要包含其他节点的数据字段,因为其他节点都可以从根开始访问到。
Tree 类有很多方法。它们用来查询、插入和删除节点;进行各种不同的遍历;显示树。下面是这个类的骨架:

java 复制代码
public class Tree<T> {
    private Node<T> root;

    public Node<T> find(int key) {
        return null;
    }

    public void insert(int id, T data) {
    }

    public Node delete(int id) {
        return null;
    }
}
3.2.2 遍历二叉树

作为树的一种特例,二叉树自然继承了一般树结构的前序、后序以及层次等遍历方法。这三个遍历算法的实现与普通树大同小异,这里不再赘述。
需要特别指出的是,对二叉树还可以定义一个新的遍历方法⎯ ⎯ 中序遍历(Inorder traversal)。顾名思义,在访问每个节点之前,首先遍历其左子树;待该节点被访问过后,才遍历其右子树。类似地,由中序遍历确定的节点序列,称作中序遍历序列。

二叉树的中序遍历序列:{a, b, c, d, e, f, g, h, l, m, n, o, p}
中序遍历二叉搜索树会使所有的节点按关键字值升序被访问到。如果希望在二叉树中创建有序 的数据序列,这是一种方法。

java 复制代码
private void inOrder(Node<T> localRoot) {
    if (null != localRoot) {
        inOrder(localRoot.getLeftChild());
        System.out.println(localRoot.getIndex());
        inOrder(localRoot.getRightChild());
    }
}
3.2.3 二叉搜索树

二叉搜索树(英语:Binary Search Tree),也称为二叉查找树、有序二叉树(ordered binary tree)或排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:

  1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
  3. 任意节点的左、右子树也分别为二叉查找树;
  4. 没有键值相等的节点。
    二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度较低。为 O(log n)。二叉查找树是基础性数据结构,用于构建更为抽象的数据结构,如集合、多重集、关联数组等。
    二叉查找树的查找过程和次优二叉树类似,通常采取二叉链表作为二叉查找树的存储结构。中序遍历二叉查找树可得到一个关键字的有序序列,一个无序序列可以通过构造一棵二叉查找树变成一个有序序列,构造树的过程即为对无序序列进行查找的过程。
    每次插入的新的结点都是二叉查找树上新的叶子结点,在进行插入操作时,不必移动其它结点,只需改动某个结点的指针,由空变为非空即可。搜索、插入、删除的复杂度等于树高,期望 O(log n),最坏 O(n)(数列有序,树退化成线性表)。
    虽然二叉查找树的最坏效率是 O(n),但它支持动态查询,且有很多改进版的二叉查找树可以使树高为 O(log n),从而将最坏效率降至 O(log n),如 AVL 树、红黑树等。
3.2.4 在二叉搜索树插入节点的算法

向一个二叉搜索树 b 中插入一个节点 s 的算法,过程为:

  1. 若 b 是空树,则将 s 所指节点作为根节点插入,否则:
  2. 若 s->data 等于 b 的根节点的数据域之值,则返回,否则:
  3. 若 s->data 小于 b 的根节点的数据域之值,则把 s 所指节点插入到左子树中,否则:
  4. 把 s 所指节点插入到右子树中。(新插入节点总是叶子节点)
java 复制代码
public class Tree<T> {
    private Node<T> root;
    public Node<T> find(int key) {
        return null;
    }
    public void insert(int id, T data) {
        Node<T> newNode = new Node<>();
        newNode.setIndex(id);
        newNode.setData(data);
        if (null == root) {
            root = newNode;
        }else {
            //从根节点开始查找
            Node<T> current = root;
            //声明父节点的引用
            Node<T> parent;
            while (true) {
                //父节点的引用指向当前节点
                parent = current;
                //如果角标小于当前节点,插入到左节点
                if (id < current.getIndex()) {
                    current = current.getLeftChild();
                    //节点为空才进行赋值,否则继续查找
                    if (null == current) {
                        parent.setLeftChild(newNode);
                        return;
                    }
                }else {
                    //否则插入到右节点
                    current = current.getRightChild();
                    if (null == current) {
                        parent.setRightChild(newNode);
                        return;
                    }
                }
            }
        }
    }
    public Node delete(int id) {
        return null;
    }
}
3.2.5 二叉搜索树的查找算法

在二叉搜索树 b 中查找 x 的过程为:

  1. 若 b 是空树,则搜索失败,否则:
  2. 若 x 等于 b 的根节点的数据域之值,则查找成功;否则:
  3. 若 x 小于 b 的根节点的数据域之值,则搜索左子树;否则:
  4. 查找右子树。
java 复制代码
public Node<T> find(int key) {
    if (null == root) {
        return null;
    }
    Node<T> current = root;
    //如果不是当前节点
    while (current.getIndex() != key) {
        if (key < current.getIndex()) {
            current = current.getLeftChild();
        }else {
            current = current.getRightChild();
        }
        //如果左右节点均为 null,查找失败
        if (null == current) {
            return null;
        }
    }
    return current;
}
3.2.6 在二叉查找树删除结点的算法

删除节点是二叉搜索树常用的一般操作中最复杂的。但是,删除节点在很多树的应用中又非常重要,所以要详细研究并总结特点。
删除节点要从查找要删的节点开始入手,方法与前面介绍的 find()和 insert()相同。找到节点后,这个要删除的节点要分三种情况讨论:

  1. 该节点是叶节点(没有子节点)。
  2. 该节点有一个子节点。
  3. 该节点有两个子节点。
    情况一:删除没有子节点的节点
    要删除叶节点,只需要改变该节点的父节点的对应子字段的值,由指向该节点、改为 null 就可以了。要删除的节点仍然存在,但它已经不是树的一部分了。

    因为 Java 语言有垃圾自动收集的机制,所以不需要非得把节点本身给删掉。
    情况二:删除只有一个子节点的节点
    第二种情况也不是很难。这个节点只有两个连接:连向父节点的和连向它惟一的子节点的。
    需要从这个序列中"剪断"这个节点,把它的子节点直接连到它的父节点上。这个过程要求改变父节点适当的引用(左子节点还是右子节点),指向要删除节点的子节点。

    情况三:删除有两个子节点的节点
    下面有趣的情况出现了。如果要删除的节点有两个子节点,就不能只是用它的一个子节点代替它。为什么不能这样呢?看图:

    假设要删除节点 25,并且用它的根是 35 的右子树取代它。那么 35 的左子节点应该是谁呢?是要删除节点 25 的左子节点 15,还是 35原来的左子节点 30?然而在这两种情况中 30 都会被放得不对,但又不能删掉它。
    对每一个节点来说,比该节点的关键字值次高的节点是它的中序后继,可以简称为该节点的后继。在上图中,节点 30 就是节点 25 的后继。
    这里有一个窍门:删除有两个子节点的节点,用它的中序后继来代替该节点。如下图:

    这里还有更麻烦的情况是它的后继自己也有子节点,后面会讨论这种可能性。
    找后继节点
    怎么找节点的后继呢?算法如下:
    首先,找到初始节点的右子节点 A,它的关键字值一定比初始节点大。然后转到 A 的左子节点那里(如果有的话),然后到这个左子节点的左子节点,以此类推,顺着左子节点的路径一直向下找。这个路径上的最后一个左子节点就是初始节点的后继。
    如果初始节点的右子节点没有左子节点,那么这个右子节点本身就是后继。

    以下是找后继节点的代码:
java 复制代码
private Node<T> getSuccessor(Node<T> delNode) {
    Node<T> successorParent = delNode;
    Node<T> successor = delNode;
    //go to rightChild
    Node<T> current = delNode.getRightChild();
    while (current != null) {
        //一直往下找左节点
        successorParent = successor;
        successor = current;
        current = current.getLeftChild();
    }
    //跳出循环,此时 successor 为最后的一个左节点,也就是被删除节点的后继节点
    //这里的判断先忽视,在后面会讲
    if (successor != delNode.getRightChild()) {
        successorParent.setLeftChild(successor.getRightChild());
        successor.setRightChild(delNode.getRightChild());
    }
    return successor;
}

这个方法首先找到 delNode 的右子节点,然后,在 while 循环中,顺着这个右子节点所有左子节点的路径向下查找。当 while 循环中止时,successor 就存有 delNode 的后继。
找到后继后,还需要访问它的父节点,所以在 while 循环中还需要保留当前节点的父节点。
正如看到的那样,后继节点可能与 current 有两种位置关系,current 就是要删除的节点。后继可能是 current 的右子节点,或者也可能是 current 右子节点的左子孙节点。下面来依次看看这两种情况。
后继节点是 delNode 的右子节点
如果后继是 cunent 的右子节点,情况就简单了一点,因为只需要把后继为根的子树移到删除的节点的位置。这个操作只需要两个步骤:

  1. 把 current 从它父节点的 rightChild 字段删掉(当然也可能是 leftChild 字段),把这个字段指向后继。
  2. 把 current 的左子节点移出来,把它插到后继的 leftChild 字段。
    下图演示了这种情况,要删除节点 75,后继节点是其右节点:

    接上之前的代码:
java 复制代码
public Node<T> delete(int key) {
    //...接前面的 else if
    else {
        //查找后继节点
        Node<T> successor = getSuccessor(current);
        //情况 3.1 如果如果删除节点有两个子节点且后继节点是删除节点的右子节点
        if (current == root) {
            root = successor;
        } else if (isLeftChild) {
            parent.setLeftChild(successor);
        } else {
            parent.setRightChild(successor);
        }
        successor.setLeftChild(current.getLeftChild());
    }
    return current;
}

第一步:如果要删除的节点 current 是根,它没有父节点,所以就只需要把根置为后继。 否则,要删除的节点或者是左子节点或者是右子节点了(图 8.19 中它是右子节点),因此 需要把它父节点的对应的字段指向 successor。当 delete()方法返回,current 失去了作用范 围后,就没有引用指向 current 保存的节点,它就会被 Java 的垃圾收集机制销毁。
第二步:把 successor 的左子节点指向的位置设为 current 的左子节点。
如果后继有子节点怎么办呢?首先,后继节点是肯定不会有左子节点的。无论后继是要删除节 点的右子节点还是这个右子节点的左子节点之一,这条在查找后继节点的算法中可以验证。
另一方面,后继很有可能有右子节点。当后继是被删除节点的右子节点时,这种情况不会带来 多大问题。移动后继的时候,它的右子树只要跟着移动就可以了。这和要删除节点的右子节点没有 冲突,因为后继就是右子节点。
下面这种情况就需要很小心了。
后继节点是 delNode 右子节点的左后代
如果 successor 是要删除节点右子节点的左后代,执行删除操作需要以下四个步骤:

  1. 把后继父节点的 leftChild 字段置为 successor 的右子节点。
  2. 把 successor 的 rightChild 字段置为要删除节点的右子节点。
  3. 把 current 从它父节点的 rightChild 字段移除,把这个字段置为 successor。
  4. 把 current 的左子节点从 current 移除,successor 的 leftChild 字段置为 current 的左子节点。

    第 1 步和第 2 步由 getSuccessor()方法完成(已经在前面写上了),第 3 步和第 4 步由delete()方法完成。
    通过标记删除
    看到这里,删除操作已经全部完成了,真的是相当棘手的操作,难就难在节点的改变上。那么我们可不可以不改变节点,达到删除的目的?。
    答案是可以的,在 node 类中加了一个 Boolean 的字段,名称如 deleted。要删除一个节点时,就把此节点的这个字段置为 true。其他操作,像 find(),在查找之前先判断这个节点是不是标志为已删除了。
    这样,删除的节点不会改变树的结构。当然,这样做存储中还保留着这种"己经删除"的节点。
    如果树中没有那么多删除操作时,这也不失为一个好方法。(例如,已经离职的员工的档案要永久保存在员工记录中。)
    下面是删除操作的完整代码:
java 复制代码
public Node<T> delete(int key) {
    if (null == root) {
        return null;
    }
    Node<T> current = root;
    Node<T> parent = root;
    boolean isLeftChild = true;
    //删除操作第一步,查找要删除的节点
    while (current.getIndex() != key) {
        parent = current;
        if (key < current.getIndex()) {
            isLeftChild = true;
            current = current.getLeftChild();
        } else {
            isLeftChild = false;
            current = current.getRightChild();
        }
        //如果左右节点均为 null,没有找到要删除的元素
        if (null == current) {
            return null;
        }
    }
    //跳出循环,找到要删除的元素:current
    if (null == current.getLeftChild() && null == current.getRightChild()) {
        //情况 1:如果当前节点没有子节点
        if (current == root) {
            //如果当前节点是根节点,将树清空
            root = null;
            return current;
        } else if (isLeftChild) {
            //如果当前节点是其父节点的做节点,将父节点的左节点清空
            parent.setLeftChild(null);
        } else {
            parent.setRightChild(null);
        }
    } else if (null == current.getRightChild()) {
        //情况 2.1:如果删除节点只有一个子节点且没有右节点
        if (current == root) {
            root = current.getLeftChild();
        } else if (isLeftChild) {
            parent.setLeftChild(current.getLeftChild());
        } else {
            parent.setRightChild(current.getLeftChild());
        }
    } else if (null == current.getLeftChild()) {
        //情况 2.2 如果删除节点只有一个子节点且没有左节点
        if (current == root) {
            root = current.getRightChild();
        } else if (isLeftChild) {
            parent.setLeftChild(current.getRightChild());
        } else {
            parent.setRightChild(current.getRightChild());
        }
    } else {
        //查找后继节点
        Node<T> successor = getSuccessor(current);
        //情况 3.1 如果如果删除节点有两个子节点且后继节点是删除节点的右子节点
        if (current == root) {
            root = successor;
        } else if (isLeftChild) {
            parent.setLeftChild(successor);
        } else {
            parent.setRightChild(successor);
        }
        successor.setLeftChild(current.getLeftChild());
    }
    return current;
}
private Node<T> getSuccessor(Node<T> delNode) {
    Node<T> successorParent = delNode;
    Node<T> successor = delNode;
    //go to rightChild
    Node<T> current = delNode.getRightChild();
    while (current != null) {
        //一直往下找左节点
        successorParent = successor;
        successor = current;
        current = current.getLeftChild();
    }
    //跳出循环,此时 successor 为最后的一个左节点,也就是被删除节点的后继节点
    //如果 successor 是要删除节点右子节点的左后代
    if (successor != delNode.getRightChild()) {
        //把后继节点的父节点的 leftChild 字段置为 successor 的右子节点
        successorParent.setLeftChild(successor.getRightChild());
        //把 successor 的 rightChild 字段置为要删除节点的右子节点。
        successor.setRightChild(delNode.getRightChild());
    }
    return successor;
}
3.3 红黑树

有了二叉搜索树,为什么还需要平衡二叉树?
二叉搜索树容易退化成一条链。
这时,查找的时间复杂度从 O ( l o g 2 N ) O(log_2N)O(log 2N)也将退化成 O ( N ) O(N)O(N)。
引入对左右子树高度差有限制的平衡二叉树,保证查找操作的最坏时间复杂度也为O ( l o g 2N ) O(log_2N)O(log 2N)。
有了平衡二叉树,为什么还需要红黑树?
AVL 的左右子树高度差不能超过 1,每次进行插入/删除操作时,几乎都需要通过旋转操作保持平衡。
在频繁进行插入/删除的场景中,频繁的旋转操作使得 AVL 的性能大打折扣。
红黑树通过牺牲严格的平衡,换取插入/删除时少量的旋转操作,整体性能优于 AVL
红黑树插入时的不平衡,不超过两次旋转就可以解决;删除时的不平衡,不超过三次旋转就能解决。
红黑树的红黑规则,保证最坏的情况下,也能在 O ( l o g 2 N ) O(log_2N)O(log 2N)时间内完成查找操作。

3.3.1 红黑规则

一棵典型的红黑树,如图所示。

从图示,可以发现红黑树的一些规律:
节点不是红色就是黑色,根节点是黑色。
红黑树的叶子节点并非传统的叶子节点,红黑树的叶子节点是 null 节点(空节点)且为黑色。
同一路径,不存在连续的红色节点。
以上是我们能发现的一些规律,这些规律其实是红黑规则的一部分。
红黑规则

  1. 节点不是黑色,就是红色(非黑即红)。
  2. 根节点为黑色。
  3. 叶节点为黑色(叶节点是指末梢的空节点 `Nil`或`Null`)。
  4. 一个节点为红色,则其两个子节点必须是黑色的(根到叶子的所有路径,不可能存在两个连续的红色节点)。
  5. 每个节点到叶子节点的所有路径,都包含相同数目的黑色节点(相同的黑色高度)。
    说明
    约束 4 和 5,保证了红黑树的大致平衡:根到叶子的所有路径中,最长路径不会超过最短路径的 2 倍。
    使得红黑树在最坏的情况下,也能有 O ( l o g 2 N ) O(log_2N)O(log 2N)的查找效率。
    黑色高度为 3 时,最短路径:黑色 → 黑色 → 黑色,最长路径:黑色 → 红色 → 黑色 → 红色 → 黑色。
    最短路径的长度为 2(不算 Nil 的叶子节点),最长路径为 4。
    关于叶子节点:Java 实现中,null 代表空节点,无法看到黑色的空节点,反而能看到传统的红色叶子节点。
    默认新插入的节点为红色:因为父节点为黑色的概率较大,插入新节点为红色,可以避免颜色冲突。
3.3.2 红黑树的应用

Java 中,TreeMap、TreeSet 都使用红黑树作为底层数据结构。
JDK 1.8 开始,HashMap 也引入了红黑树:当冲突的链表长度超过 8 时,自动转为红黑树。
Linux 底层的 CFS 进程调度算法中,vruntime 使用红黑树进行存储。
多路复用技术的 Epoll,其核心结构是红黑树 + 双向链表。

3.3.3 红黑树的定义

红黑树要比二叉搜索树多一个颜色属性。
同时,为了方便确认插入位置,还可以多一个 parent 属性,用于表示当前节点的父节点。
因此,红黑树节点的定义如下:

java 复制代码
class RedBlackTreeNode {
    public int val;
    public RedBlackTreeNode left;
    public RedBlackTreeNode right;
    // 记录节点颜色的 color 属性,暂定 true 表示红色
    public boolean color;
    // 为了方便迭代插入,所需的 parent 属性
    public RedBlackTreeNode parent;
    // 一些构造函数,根据实际需求构建
    public RedBlackTreeNode() {
    }
}

红黑树中,有一个 root 属性,用于记录当前红黑树的根节点。

java 复制代码
public class RedBlackTree {
    // 当前红黑树的根节点,默认为 null
    private RedBlackTreeNode root;
}
3.3.4 红黑树的左旋

当红黑规则不满足时,需要对节点进行变色或旋转操作。
回忆二叉树的左旋:
手工推演(先冲突,再移动):
根节点成为右儿子的左子树。
右儿子原有的左子树成为根节点的右子树。
代码实现(先空位,再补齐):
右儿子的左子树成为根节点的右子树。
根节点成为右儿子的左子树。
红黑树的左旋
红黑树节点中,包含父节点的引用。
进行左旋时,不仅需要更新左右子树的引用,还需要更新父节点的引用。
左旋需要三大步:
空出右儿子的左子树: 右儿子的左子树成为根节点的右子树;若右儿子的左子树不为空,需要更新左子树的父节点为根节点。
空出根节点的父节点:右儿子的父节点更新为根节点的父节点;父节点指向右儿子:
若父节点为 null,root 将指向右儿子,右儿子成为整棵树的根节点;
根节点是父节点的左子树,则右儿子成为父节点的左儿子;
根节点是父节点的右子树,则右儿子成为父节点的右儿子。
根节点和右儿子成功会师: 上述两步,空出了根节点的父节点和右儿子的左子树。
这时直接更新,即可将根节点变成右儿子的左子树。
给出一个不是很正确的示意图

具体代码如下:

java 复制代码
public void leftRotate(RedBlackTreeNode p) {
    // 在当前节点不为 null 时,才进行左旋操作
    if (p != null) {
        // 先记录 p 的右儿子
        RedBlackTreeNode rightChild = p.right;
        // 1. 空出右儿子的左子树
        p.right = rightChild.left;
        // 左子树不为空,需要更新父节点
        if (rightChild.left != null) {
            rightChild.left.parent = p;
        }
        // 2. 空出根节点的父节点
        rightChild.parent = p.parent;
        // 父节点指向右儿子
        if (p.parent == null) { // 右儿子成为新的根节点
            this.root = rightChild;
        } else if (p == p.parent.left) { // 右儿子成为父节点的左儿子
            p.parent.left = rightChild;
        } else { // 右儿子成为父节点的右儿子
            p.parent.right = rightChild;
        }
        // 3. 右儿子和根节点成功会师,根节点成为左子树
        rightChild.left = p;
        p.parent = rightChild;
    }
}
3.3.5 红黑树的右旋

回忆二叉树的右旋:
手工推演(先冲突,再移动):
根节点成为左儿子的右子树。
左儿子原有的右子树成为根节点的左子树。
代码实现(先空位,再补齐):
左儿子的右子树成为根节点的左子树。
根节点成为左儿子右子树。
红黑树的右旋
与红黑树的左旋一样,由于父节点引用的存在,不仅需要更新左右子树的引用,还需要更新父节点的引用。
右旋需要三大步:
空出左儿子的右子树: 左儿子的右子树成为根节点的左子树;若左儿子的右子树不为空,需要更新右子树的父节点为根节点。
**空出根节点的父节点:**左儿子的父节点更新为根节点的父节点;父节点指向左儿子:
父节点为空,左儿子成为整棵树的根节点。
根节点为父节点的左子树,左儿子成为父节点的左子树。
根节点为父节点的右子树,左儿子成为根节点的右子树。
**根节点和左儿子成功会师:**上述两步,空出了根节点的父节点和左儿子的右子树。
这时直接更新,即可将根节点变成左儿子的右子树。
给出一个不是很正确的示意图

具体代码如下:

java 复制代码
public void rightRotate(RedBlackTreeNode p) {
    if (p != null) {
        // 记录 p 的左儿子
        RedBlackTreeNode leftChild = p.left;
        // 1. 空出左儿子的右子树
        p.left = leftChild.right;
        // 右子树不为空,需要更新父节点
        if (leftChild.right != null) {
            leftChild.right.parent = p;
        }
        // 2. 空出根节点的父节点
        leftChild.parent = p.parent;
        // 父节点指向左儿子
        if (p.parent == null) { // 左儿子成为整棵树根节点
            this.root = leftChild;
        } else if (p.parent.left == p) { // 左儿子成为父节点左儿子
            p.parent.left = leftChild;
        } else { // 左儿子成为父节点的右儿子
            p.parent.right = leftChild;
        }
        // 3. 顺利会师
        leftChild.right = p;
        p.parent = leftChild;
    }
}
3.3.6 新增节点

一些规则:
新插入的节点默认为红色,原因:插入黑色节点会影响黑色高度,对红黑树的影响更大;
新增节点 x 时,循环的依据: x != null && x != root && x.parent.color == RED,即节点非空、不是整棵树的根节点(保证存在父节点)且父节点为红色(违反红黑规则 4,需要调整)。
完成循环调整后,需要将整棵树的根节点设为黑色,以满足红黑规则 1;同时,根节点设为黑色,不会影响从根节点开始的所有路径的黑色高度。

3.3.6.1 父亲为祖父的左儿子

情况一:父亲和叔叔都是红色
当父亲为祖父的左儿子,父亲和叔叔都是红色时:
(1)将父亲和叔叔改成黑色,以满足红黑规则 4。
(2)父亲和叔叔变成黑色了,黑色高度变化,需要将祖父变成红色,以满足红黑规则 5。
(3)从祖父开始,继续调整。
示意图如下:

情况二:叔叔为黑色,自己是父亲的左儿子
父亲为祖父的左儿子,叔叔为黑色,自己是父亲的左儿子。
(1)父亲变成黑色,祖父变成红色(右子树的黑色高度变低)。
(2)对祖父进行右旋,让父节点成为新的祖父,以恢复右子树的黑色高度。
(3)不满足循环条件,退出循环。
示意图如下:

情况三:叔叔为黑色,自己是父亲的右儿子
父亲为祖父的左儿子,叔叔为黑色,自己是父亲的右儿子。
(1)父亲成为新的 x,对父亲进行左旋操作,构造情况二的初始状态。
(2)按照情况二,对新的 x(原父亲)进行处理。
示意图如下:

3.3.6.2 父亲为祖父的右儿子

情况一:父亲和叔叔都是红色
父亲为祖父的右儿子,父亲和叔叔都是红色。
(1)将父亲和叔叔都变成黑色,以保证红黑规则 4。
(2)将祖父变成红色,以保证红色规则 5(相同的黑色高度)。
(3)从祖父开始,继续调整。
示意图如下:

情况二:叔叔为黑色,自己是父亲的右儿子
父亲为祖父的右儿子,叔叔为黑色,自己是父亲的右儿子。
(1)父亲变成黑色,祖父变成红色(左子树的黑色高度降低)。
(2)对祖父进行左旋操作,以恢复左子树的黑色高度。
(3)不满足循环条件,退出循环。
示意图如下:

情况三:叔叔为黑色,自己是父亲的左儿子
父亲是祖父的右儿子,叔叔为黑色,自己是父亲的左儿子。
(1)父节点成为新的 X,对父亲进行右旋操作,构造情况二的初始情况。
(2)按照情况二,对新的 x(原父节点)进行处理。
示意图如下:

3.3.6.3 规律总结

循环条件: x != null && x != root && x.parent.color == RED,即节点非空、不是整棵树的根节点(保证存在父节点)且父节点为红色。
最终处理:将整棵树的根节点变成黑色,以满足红黑规则 1,又不会违反红黑规则 5。
对父亲是祖父的左儿子或右儿子的处理是对称的,只需要理解左儿子时的处理方法,就可以举一反三,知道对右儿子的处理方法。
父亲为祖父的左儿子:
父亲和叔叔都是红色,将父亲和叔叔变成黑色,祖父变成红色,继续对祖父进行调整。
叔叔是黑色,自己是父亲的左儿子:父亲变成黑色,祖父变成红色;对祖父进行右旋以满足红黑规则;此时节点不满足循环条件,可以退出循环。
叔叔是黑色,自己数父亲的右儿子:父亲成为新的 X,对父亲执行左旋操作,构造情况 2;按照情况 2 继续进行处理。
总结: 父叔同色,只进行变色操作;父叔异色,自己是右儿子,则进行 LR 操作;父叔异色,自己是左儿子,则进行 R 操作。
父亲为祖父的右儿子:
父叔同色,只进行变色操作。
父叔异色,自己是左儿子,则进行 RL 操作。
父叔异色,自己是右儿子,则进行 L 操作。

3.3.6.4 代码实现

根据上面的分析,不难写出新增红黑节点后的代码。
假设新增的节点为 p,则代码如下。

java 复制代码
public void fixAfterInsert(RedBlackTreeNode x) {
    // 新插入的节点,默认为红色
    x.color = RED;
    // p 不为 null、不是整棵树的根节点、父亲为红色,需要调整
    while (x != null && this.root != x && x.parent.color == RED) {
        // 父亲是祖父的左儿子
        if (parentOf(x) == parentOf(parentOf(x)).left) {
            // 父亲和叔叔都是红色
            RedBlackTreeNode uncle = parentOf(parentOf(x)).right;
            if (uncle.color == RED) {
                // 父亲和叔叔都变成黑色
                parentOf(x).color = BLACK;
                uncle.color = BLACK;
                // 祖父变成红色,继续从祖父开始进行调整
                parentOf(parentOf(x)).color = RED;
                x = parentOf(parentOf(x));
            } else { // 叔叔为黑色
                // 自己是父亲的右儿子,需要对父亲左旋
                if (x == parentOf(x).right) {
                    x = parentOf(x);
                    leftRotate(x);
                }
                // 自己是父亲的左儿子,变色后右旋,保持黑色高度
                parentOf(x).color = BLACK;
                parentOf(parentOf(x)).color = RED;
                rightRotate(parentOf(parentOf(x)));
            }
        } else { //父亲是祖父的右儿子
            RedBlackTreeNode uncle = parentOf(parentOf(x)).left;
            // 父亲和叔叔都是红色
            if (uncle.color == RED) {
                // 叔叔和父亲变成黑色
                parentOf(x).color = BLACK;
                uncle.color = BLACK;
                // 祖父变为红色,从祖父开始继续调整
                parentOf(parentOf(x)).color = RED;
                x = parentOf(parentOf(x));
            } else {
                // 自己是父亲的左儿子,以父亲为中心右旋
                if (parentOf(x).left == x) {
                    x = parentOf(x);
                    rightRotate(x);
                }
                // 自己是父亲的右儿子,变色后左旋,保持黑色高度
                parentOf(x).color = BLACK;
                parentOf(parentOf(x)).color = RED;
                leftRotate(parentOf(parentOf(x)));
            }
        }
     }
    // 最后将根节点置为黑色,以满足红黑规则 1,又不会破坏规则 5
    this.root.color = BLACK;
}
private static RedBlackTreeNode parentOf(RedBlackTreeNode p) {
    return (p == null ? null : p.parent);
}
3.3.7 删除节点

一些规则:
删除节点时,通过节点替换实现删除。
假设替换节点为 x,需要在 x 替换被删节点后,从 x 开始进行调整。
调整操作,循环的依据: x != root && x.color == BLACK,即替换节点不能为整棵树的根节点,替换节点的颜色为黑色(改变了红黑高度)。
完成循环调整后,需要将 x 设为黑色,结束调整。

3.3.7.1 自己是父亲的左儿子

情况一:兄弟为红色
此时,自己为黑色、兄弟为红色、父节点为黑色(满足红黑规则 4)。
(1)将兄弟变成黑色,父节点变成红色;这时,以父节点为起点的左子树黑色高度降低。
(2)对父节点进行左旋,以恢复左子树黑色高度;同时,兄弟的左孩子成为新的兄弟。
此时,自己和兄弟都是黑色,可能满足满足情况 2、3 和 4、4。
示意图如下:

情况二:兄弟为黑色,左右侄子也是黑色
此时,自己和兄弟都是黑色,父节点为黑色或红色;兄弟的两个儿子,都是黑色。
(1)将兄弟变成为红色,x 指向父节点,继续进行调整。
示意图如下:

情况三:兄弟为黑色,右侄子为黑色
此时,自己和兄弟均为黑色,父节点为红色或黑色;右侄子为黑色、左侄子为红色;
(1)将左侄子变成黑色,兄弟变为红色;这时,以兄弟为起点的右子树黑色高度降低。
(2)将兄弟节点右旋,以恢复右子树的黑色高度;这时,左侄子将成为新的右兄弟。
此时,兄弟的右儿子为红色,满足情况 4;继续按照情况 4,对节点 x 进行调整。
示意图如下:

情况四:兄弟为黑色,右侄子为红色
此时,自己和兄弟都是黑色,父节点为红色或黑色;右侄子为红色,左侄子为黑色或红色。
(1)兄弟颜色改成与父节点一致,右侄子和父节点都变成黑色。
(2)为了保证父节点变为黑色后,不影响所有路径的黑色高度,需要将父节点左旋(兄弟节点上提)。
(3)x 指向根节点,结束循环。
示意图如下:

3.3.7.2 自己是父亲的右儿子

情况一:兄弟是红色节点
此时,兄弟是红色节点,父节点必为黑色;若兄弟有左右儿子,左右儿子必为黑色(满足红黑规则 4)。
(1)将兄弟变成黑色节点,父节点变成红色;这时,以父节点为起点的右子树黑色高度降低。
(2)将父节点右旋,以恢复右子树的黑色高度;这时,兄弟的右孩子成为新的兄弟。
此时,自己和兄弟都是黑色,将满足情况 2、3 和 4、4。
示意图如下:

情况二:兄弟是黑色,左右侄子也是黑色
此时,自己和兄弟是黑色,父节点可以为红色或黑色。
(1)将兄弟变成红色,x 指向父节点,继续对父节点进行调整。
示意图如下:

情况三:兄弟为黑色,左侄子为黑色
此时,自己和兄弟均为黑色,父节点为黑色或红色;左侄子为黑色,右侄子为红色。
(1)将右侄子变成黑色,兄弟变成红色;这是,以兄弟为起点的左子树黑色高度降低。
(2)将兄弟左旋,以恢复左子树的黑色高度;这时,右侄子成为新的兄弟。
此时,将满足情况 4,可以按照情况 4,继续进行调整。
示意图如下:

情况四:兄弟为黑色,左侄子为红色
此时,自己和兄弟均为黑色,父节点为红色或黑色;左侄子为红色,右侄子为红色或黑色。
(1)将兄弟变成与父节点一样的颜色,左侄子和父节点变成黑色。
(2)为了保证父节点变成黑色,不会影响所有路径的黑色高度,需要将父节点右旋(兄弟上提)
(3)x 指向根节点,退出循环。
示意图如下:

3.3.7.3 规律总结

循环条件:`x != root && x.color = BLACK`,x 不是根节点且颜色为黑色。
收尾操作:将 x 置为黑色。
x 为父亲的左儿子或右儿子,处理操作是对称的;同样只需要记住左儿子时的操作,即可举一反三。
x 为父亲的左儿子
兄弟为红色:将兄弟变成黑色,父节点变成红色;对父节点左旋,恢复左子树的黑色高度,左侄子成为新的兄弟。
兄弟为黑色,左右侄子为黑色:兄弟变成红色,x 指向父节点,继续进行调整。
兄弟为黑色,右侄子为黑色(左侄子为红色):左侄子变成黑色,兄弟变成红色;兄弟右旋,恢复右子树的黑色高度,左侄子成为新的兄弟。
兄弟为黑色,右侄子为红色:兄弟变成父节点颜色,父节点和右侄子变成黑色;父节点左旋,x 指向整棵树的根节点,结束循环。

3.3.7.4 代码实现

删除节点后,调整红黑树的代码如下。

java 复制代码
public void fixAfterDeletion(RedBlackTreeNode x) {
    // x 不是根节点且颜色为黑色,开始循环调整
    while (x != root && x.color == BLACK) {
        // x 是父亲的左儿子
        if (x == parentOf(x).left) {
            RedBlackTreeNode brother = parentOf(x).right;
            // 兄弟为红色
            if (brother.color == RED) {
                // 兄弟变成黑色,父节点变成红色
                brother.color = BLACK;
                parentOf(x).color = RED;
                // 父节点左旋,恢复左子树的黑色高度
                leftRotate(parentOf(x));
                // 更新兄弟
                brother = parentOf(x).right;
            }
            // 兄弟为黑色,左右侄子为黑色
            if (brother.left.color == BLACK && brother.right.color == BLACK) {
                // 兄弟变成红色
                brother.color = RED;
                // 从父节点开始继续调整
                x = parentOf(x);
            } else {
                // 右侄子为黑色(左侄子为红色)
                if (brother.right.color == BLACK) {
                    // 左侄子变为黑色,兄弟变成红色
                    brother.left.color = BLACK;
                    brother.color = RED;
                    // 兄弟右旋,恢复右子树黑色高度
                    rightRotate(brother);
                    // 左侄子成为新的兄弟
                    brother = parentOf(x).right;
                }
                // 右侄子为红色,兄弟变成父节点颜色
                brother.color = parentOf(x).color;
                // 父节点和右侄子变成黑色
                parentOf(x).color = BLACK;
                brother.right.color = BLACK;
                // 父节点左旋
                leftRotate(parentOf(x));
                // x 指向根节点
                x = root;
            }
        } else {
            RedBlackTreeNode brother = parentOf(x).left;
            // 兄弟为红色
            if (brother.color == RED) {
                // 兄弟变黑色,父亲变红色
                brother.color = BLACK;
                parentOf(x).color = RED;
                // 父亲右旋,恢复红黑色高度
                rightRotate(parentOf(x));
                // 更新兄弟为右侄子
                brother = parentOf(x).left;
            }
            // 兄弟的左右儿子为黑色
            if (brother.left.color == BLACK && brother.right.color == BLACK) {
                // 兄弟变为红色
                brother.color = RED;
                // x 指向父节点,继续进行调整
                x = parentOf(x);
            } else {
                // 左侄子为黑色(右侄子为红色)
                if (brother.left.color == BLACK) {
                    // 右侄子变黑色,兄弟变红色
                    brother.right.color = BLACK;
                    brother.color = RED;
                    // 对兄弟左旋
                    leftRotate(brother);
                    // 右侄子成为新的兄弟
                    brother = parentOf(x).left;
                }
                // 左侄子为红色,兄弟改为父节点颜色
                brother.color = parentOf(x).color;
                // 父节点和左侄子变成黑色
                brother.left.color = BLACK;
                parentOf(x).color = BLACK;
                // 兄弟节点上提(右旋父节点)
                rightRotate(parentOf(x));
                // x 指向根节点
                x = root;
            }
        }
    }
    // 更新 x 为黑色
    x.color = BLACK;
}

二、集合

1. Collection 接口

Collection是所有单列集合的父接口。

在 Collection 中定义了单列集合(List 和 Set)通用的一些方法,这些方法可用于操作所有的单列集合。方法如下:

java 复制代码
public boolean add(E e) : 把给定的对象添加到当前集合中 。
public void clear() :清空集合中所有的元素。
public boolean remove(E e) : 把给定的对象在当前集合中删除。
public boolean contains(Object obj) : 判断当前集合中是否包含给定的对象。
public boolean isEmpty() : 判断当前集合是否为空。
public int size() : 返回集合中元素的个数。
public Object[] toArray() : 把集合中的元素,存储到数组中

List 接口
`java.util.List` 接口继承自 Collection 接口,是单列集合的一个重要分支,习惯性地会将实现了 List 接口的对象称为 List 集合。在 List 集合中允许出现重复的元素,所有的元素是以一种线性方式进行存储的,在程序中可以通过索引来访问集合中的指定元素。另外,List 集合还有一个特点就是元素有序,即元素的存入顺序和取出顺序一致。
List 集合中的元素是可以重复的!
List 集合中的元素是有序的!
List 集合中的元素是带有索引值的 【它的子类依旧就被这个特点】
ArrayList 集合
`java.util.ArrayList` 集 合 数 据 存 储 的 结 构 是 ** 数 组 结 构 ** 。 <span style="background:yellow">元素增删慢,查找快,</span>由于日常开发中使用最多的功能是查询数据,遍历数据,所以 `ArrayList` 是最常用的集合之一。
LinkedList 集合
`java.util.LinkedList` 集 合 数 据 存 储 的 结 构 是 ** 链 表 结 构 ** 。 <span style="background:yellow">方便元素添加、删除的集合。</span>
常用方法:

java 复制代码
public void addFirst(E e) :将指定元素插入此列表的开头。
public void addLast(E e) :将指定元素添加到此列表的结尾。
public E getFirst() :返回此列表的第一个元素。
public E getLast() :返回此列表的最后一个元素。
public E removeFirst() :移除并返回此列表的第一个元素。
public E removeLast() :移除并返回此列表的最后一个元素。
public E pop() :从此列表所表示的堆栈处弹出一个元素。
public void push(E e) :将元素推入此列表所表示的堆栈。
public boolean isEmpty() :如果列表不包含元素,则返回 true。

Set 接口
HashSet 集合
`java.util.HashSet`是 Set 接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不能保证不一致)。
`java.util.HashSet`底层的实现其实是一个`java.util.HashMap` 支持。
唯一性是依赖于 `hashCode` 和`equals`方法。
HashSet 集合存储数据的结构(哈希表)
哈希表:在`JDK1.8`之前,哈希表底层采用数组+链表实现,即使用数组处理冲突,同一 hash 值的链表都存储在一个数组里。但是当位于一个数组中的元素较多,即 hash 值相等的元素较多时,通过 key 值依次查找的效率较低。而在`JDK1.8`中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值 8 时,将链表转换为红黑树,这样大大减少了查找时间。
哈希表是由数组+链表+红黑树(`JDK1.8`增加了红黑树部分)实现的。
LinkedHashSet 集合
`HashSet`保证元素唯一,可是元素存放进去是没有顺序的,那么我们要保证有序,怎么办呢?在`HashSet`下面有一个子类 `java.util.LinkedHashSet` ,它是链表和哈希表组合的一个数据存储结构。

2. Map 接口

Map 接口继承树

2.1 Map 接口概述

Map 与 Collection 并列存在。用于保存具有 映射关系的数据:key-value。
Map 中的 key 和 value 都可以是任何引用类型的数据。
Map 中的 key 用 Set 来存放, 不允许重复,即同一个 Map 对象所对应的类,须重写 hashCode()和 equals()方法。

  • 常用 String 类作为 Map 的"键"。
  • key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value。
  • Map 接口的常用实现类:`HashMap`、`TreeMap`、`LinkedHashMap`和`Properties`。其中,`HashMap`是 Map 接口使用频率最高的实现类。
2.2 常用实现类结构

`Map`:双列数据,存储`key-value`对的数据 ---类似于高中的函数:`y = f(x)`。
`HashMap`:作为 Map 的主要实现类;线程不安全的,效率高;存储`null`的`key`和`value`。
`LinkedHashMap`:保证在遍历`map`元素时,可以照添加的顺序实现遍历。
原因是在原的`HashMap`底层结构基础上,添加了一对指针,指向前一个和后一个元素。
对于频繁的遍历操作, 此类执行效率高于`HashMap`。
`TreeMap`:保证照添加的`key-value`对进行排序,实现排序遍历。此时考虑 key 的自然排序或定制排序,底层使用红黑树。
`Hashtable`:作为古老的实现类;线程安全的,效率低;不能存储`null`的`key`和`value`。

  • Properties:常用来处理配置文件。`key`和`value`都是`String`类型。
    `HashMap`的底层的实现:数组+链表 (`jdk7`及之前) 数组+链表+红黑树 (`jdk 8`)。
2.3 存储结构的理解


Map 中的 key:无序的、不可重复的,使用 Set 存储所的 key。 key 所在的类要重写 equals()和 hashCode() (以 HashMap 为例)。
Map 中的 value:无序的、可重复的,使用 Collection 存储所的 value。value 所在的类要重写 equals()。
一个键值对:key-value 构成了一个 Entry 对象。
Map 中的 entry:无序的、不可重复的,使用 Set 存储所的 entry。

2.4 常用方法

添加 、 删除、修改操作 :
Object put(Object key,Object value):将指定 key-value 添加到(或修改)当前 map 对象中。
void putAll(Map m):将 m 中的所有 key-value 对存放到当前 map 中。
Object remove(Object key):移除指定 key 的 key-value 对,并返回 value。
void clear():清空当前 map 中的所有数据。
元素 查询的操作:
Object get(Object key):获取指定 key 对应的 value。
boolean containsKey(Object key):是否包含指定的 key。
boolean containsValue(Object value):是否包含指定的 value。
int size():返回 map 中 key-value 对的个数。
boolean isEmpty():判断当前 map 是否为空。
boolean equals(Object obj):判断当前 map 和参数对象 obj 是否相等。
元 视图操作的方法:
Set keySet():返回所有 key 构成的 Set 集合。
Collection values():返回所有 value 构成的 Collection 集合。
Set entrySet():返回所有 key-value 对构成的 Set 集合。

3. Iterator 迭代
4. 工具类
4.1 Collections
4.1.1 概述

Collections:对 Collection、Map 等集合进行操作的工具类。
Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、 对集合对象实现同步控制等方法。

4.1.2 排序操作 (均为 static 方法)

reverse(List) 反转 List 中元素的顺序。
shuffle(List) 对 List 集合元素进行随机排序。
sort(List) 根据元素的自然排序对指定 List 集合元素进行升序排序。
sort(List,Comparator) 根据指定的 Comparator 产生的顺序对 List 集合元素进行排序。
swap(List,int,int) 将指定 List 集合中的 i 处元素和 j 处元素进行交换。

4.1.3 查找和替换

Object max(Collection) :根据元素的自然排序,返回集合中最大元素。
Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回集合中的最大元素。
Object min(Collection)
Object min(Collection,Comparator)
int frequency(Collection,Object):返回集合中指定元素出现的次数。
void copy(List dest,List src):将 src 中的内容复制到 dest 中,前提是 dest.size()>=src.size()
boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值。

java 复制代码
ArrayList list = new ArrayList();
list.add(123);
list.add(43);
list.add(765);
list.add(-97);
list.add(0);
List dest = Arrays.asList(new Object[list.size()]);
Collections.copy(dest,list);
4.1.4 同步控制

Collections 类中提供了多个`synchronizedXxx()`方法,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。

java 复制代码
List list1 = Collections.synchronizedList(list); // 返回的 list1 即使线程安全的。

Collection 和 Collections 异同?
Collection 是集合类的一个接口,存储单列数据,其下有两个子接口 List、Set,主要实现类有`ArrayList、LinkedList、HashSet、LinkedHashSet`等。
而 Collections 是操作集合的工具类。

4.2 Arrays

`JDK` 提供了一个工具类专门用来操作数组的工具类,即 Arrays,该 Arrays 工具类提供了大量的静态方法,在实际项目开发中,推荐使用,这样既快捷又不会发生错误。但在面试时,若出现对数组操作的题目,就决不允许使用 Arrays 类提供的方法,因为面试官考察的是我们对数组的操作能力,而不是对 Arrays 类的应用。

4.2.1 常用的方法表
html 复制代码
| **方法声明** | **功能描述**
|
| :----------------------------------------------------------- | :------------------------------------- |
| public static List asList(T... a)                              | 返回由指定数组支持的固定大小的列表 |
| public static String toString(int[] a)                       | 返回指定数组的内容的字符串表示形式 |
| public static void sort(int[] a)                             | 按照数字顺序排列指定的数组 |
| public static int binarySearch(Object[] a, Object key)       | 使用二叉搜索算法搜索指定对象的指定数组 |
| public static int[] copyOfRange(int[] original, int from, int to) | 将指定数组的指定范围复制到新数组中 |
| public static void fill(Object[] a, Object val)              | 将指定数组的指定范围复制到新数组中 |
4.2.2 数组转集合
java 复制代码
import java.util.*;
import java.io.*;

public class Example{
    public static void main(String args[]) throws IOException{
        int n = 5; // 5 个元素
        String[] name = new String[n];
        for(int i = 0; i < n; i++){
            name[i] = String.valueOf(i);
        }
        List<String> list = Arrays.asList(name);
        System.out.println();
        for(String li: list){
            String str = li;
            System.out.print(str + " ");
        }
    }
}
4.2.3 集合转数组
java 复制代码
import java.util.*;

public class Main{
    public static void main(String[] args){
        List<String> list = new ArrayList<String>();
        list.add("咕");
        list.add("泡");
        list.add("教");
        list.add("育");
        list.add("https://www.gupaoedu.cn");
        String[] s1 = list.toArray(new String[0]);
        for(int i = 0; i < s1.length; ++i){
            String contents = s1[i];
            System.out.print(contents);
        }
    }
}
4.2.4 数组转换为字符串

在程序开发中,经常需要把数组以字符串的形式输出,可以使用 Arrays 工具类的 toString(int[] arr),需要注意的是,该方法并不是对 Object 类 toString() 方法的重写,只是用于返回指定数组的字符串形式。

java 复制代码
import java.util.*;

public class Example {
    public static void main(String[] args){
        int[] arr = {9, 8, 3, 5, 2};
        String arrString = Arrays.toString(arr);
        System.out.println(arrString);
    }
}

注意:Arrays 类的 toString(int[] arr) 方法可将任意整数转为字符串。

4.2.5 排序

Arrays 工具类中的静态方法 sort() 可以对数组进行排序。

java 复制代码
import java.util.*;

public class Example {
    public static void main(String[] args){
        int[] arr = {9, 8, 3, 5, 2}; //初始化一个数组
        System.out.print("排序前:");
        printArray(arr);
        Arrays.sort(arr);
        System.out.print("排序后:");
        printArray(arr);
    }
    public static void printArray(int[] arr){ //定义打印数组方法
        System.out.print("[");
        for(int x=0; x<arr.length; x++){
            if(x != arr.length - 1){
                System.out.print(arr[x] + ",");
            } else {
                System.out.println(arr[x] + "]");
            }
        }
    }
}
4.2.6 查找元素

Arrays 工具类中的静态方法 binarySearch(Object[] a, Object key) 用于查找元素。

java 复制代码
import java.util.*;

public class Example {
    public static void main(String[] args){
        int[] arr = {9, 8, 3, 5, 2}; //初始化一个数组
        Arrays.sort(arr);
        int index = Arrays.binarySearch(arr, 3);
        //输出元素所在的索引位置
        System.out.println("数组排序后元素 3 的索引是:" + index);
    }
}
4.2.7 拷贝元素

在程序开发中,经常需要在不破坏原数组的情况下使用数组中的部分元素,可以 Arrays 工具类中的静态方法 copyOfRange(int[] original, int from, int to) 方法将数组中指定范围的元素复制到一个新的数组中。
copyOfRange(int[] original, int from, int to)方法参数说明如下:
参数 original 表示被复制的数组。
参数 from 表示被复制元素的初始索引(包括)。
参数 to 表示被复制元素的最后索引(不包括)。

java 复制代码
import java.util.*;

public class Example {
    public static void main(String[] args){
        int[] arr = {9, 8, 3, 5, 2}; //初始化一个数组
        int[] copied = Arrays.copyOfRange(arr, 1, 7);
        for(int i=0; i<copied.length; i++){
            System.out.print(copied[i] + " ");
        }
    }
}
4.2.8 填充元素

程序开发中,经常需要用一个值替换数组中的所有元素,使用 Array 的 fill(Object[] a, Object val) 方法,该方法可以将指定的值赋给数组中的每一个元素。

java 复制代码
import java.util.*;

public class Example {
    public static void main(String[] args){
        int[] arr = {1, 2, 3, 4}; //初始化一个数组
        Arrays.fill(arr, 8);
        for(int i=0; i<arr.length; i++){
            System.out.println(i + ": " +arr[i]);
        }
    }
}
5. 比较器
5.1 Comparable
5.1.1 Comparable 简介

Comparable 是排序接口。
若一个类实现了 Comparable 接口,就意味着"该类支持排序"。 即然实现 Comparable 接口的类支持排序,假设现在存在"实现 Comparable 接口的类的对象的 List 列表(或数组)",则该 List 列表(或数组)可以通过 Collections.sort(或 Arrays.sort)进行排序。
此外,"实现 Comparable 接口的类的对象"可以用作"有序映射(如 TreeMap)"中的键或"有序集合(TreeSet)"中的元素,而不需要指定比较器。

5.1.2 Comparable 定义

Comparable 接口仅仅只包括一个函数,它的定义如下:

java 复制代码
package java.lang;

import java.util.*;

public interface Comparable<T> {
    public int compareTo(T o);
}

说明:假设我们通过 x.compareTo(y) 来"比较 x 和 y 的大小"。若返回"负数",意味着"x 比 y 小";返回"零",意味着"x 等于 y";返回"正数",意味着"x 大于 y"。

5.2 Comparator
5.1.3 Comparator 简介

Comparator 是比较器接口。
我们若需要控制某个类的次序,而该类本身不支持排序(即没有实现 Comparable 接口);那么,我们可以建立一个"该类的比较器"来进行排序。这个"比较器"只需要实现Comparator 接口即可。
也就是说,我们可以通过"实现 Comparator 类来新建一个比较器",然后通过该比较器对类进行排序。

5.1.4 Comparator 定义

Comparator 接口仅仅只包括两个个函数,它的定义如下:

java 复制代码
package java.util;

public interface Comparator<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);
}

说明:
若一个类要实现Comparator接口:它一定要实现compareTo(T o1, T o2) 函数,但可以不实现 equals(Object obj) 函数。
为什么可以不实现 equals(Object obj) 函数呢? 因为任何类,默认都是已经实现了equals(Object obj)的。 Java中的一切类都是继承于java.lang.Object,在Object.java中实现了equals(Object obj)函数;所以,其它所有的类也相当于都实现了该函数。
int compare(T o1, T o2) 是"比较o1和o2的大小"。返回"负数",意味着"o1比o2小";返回"零",意味着"o1等于o2";返回"正数",意味着"o1大于o2"。

5.3 Comparator 和 Comparable 比较

Comparable是排序接口;若一个类实现了Comparable接口,就意味着"该类支持排序"。
而Comparator是比较器;我们若需要控制某个类的次序,可以建立一个"该类的比较器"来进行排序。
我们不难发现:Comparable相当于"内部比较器",而Comparator相当于"外部比较器"。

5.4 利用Comparable排序

Person类定义

java 复制代码
package com.gupaoedu;

public class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(){

    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    /**
     * 实现 "Comparable<String>" 的接口,即重写compareTo<T t>函数。
     * 这里是通过"person的名字"进行比较的
     * @param person
     * @return
     */
    @Override
    public int compareTo(Person person) {
        return name.compareTo(person.name);
        //return this.name - person.name;
    }
}

说明:

  1. Person类代表一个人,Persong类中有两个属性:age(年纪) 和 name"人名"。
  2. Person类实现了Comparable接口,因此它能被排序。
    排序测试:
java 复制代码
@org.junit.Test
public void test01(){
    ArrayList<Person> list = new ArrayList<>();
    list.add(new Person("ccc", 20));
    list.add(new Person("AAA", 30));
    list.add(new Person("bbb", 10));
    list.add(new Person("ddd", 40));
    System.out.println("-----原始排序开始-----");
    for(Person person : list){
        System.out.println(person.toString());
    }
    System.out.println("-----原始排序结束-----");

    Collections.sort(list);
    System.out.println("-----名字排序开始-----");
    for(Person person : list){
        System.out.println(person.toString());
    }
    System.out.println("-----名字排序结束-----");
}

运行结果:
shell
-----原始排序开始-----
Person{name='ccc', age=20}
Person{name='AAA', age=30}
Person{name='bbb', age=10}
Person{name='ddd', age=40}
-----原始排序结束-----
-----名字排序开始-----
Person{name='AAA', age=30}
Person{name='bbb', age=10}
Person{name='ccc', age=20}
Person{name='ddd', age=40}
-----名字排序结束-----

5.5 利用Comparator排序
5.5.1 通书写方式

Person类定义

java 复制代码
package com.gupaoedu;

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

    public Person() {

    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

AscAgeComparator类定义

java 复制代码
package com.zhangzemin;

import java.util.Comparator;

public class AscAgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person p1, Person p2) {
    	//升序排序
        return p1.getAge() - p2.getAge();
        //降序排序
        //return p2.getAge() - p1.getAge();
    }
}

排序测试

java 复制代码
@org.junit.Test
public void test02(){
    ArrayList<Person> list = new ArrayList<>();
    list.add(new Person("ccc", 20));
    list.add(new Person("AAA", 30));
    list.add(new Person("bbb", 10));
    list.add(new Person("ddd", 40));
    System.out.println("-----原始排序开始-----");
    for(Person person : list){
        System.out.println(person.toString());
    }
    System.out.println("-----原始排序结束-----");

    Collections.sort(list,new AscAgeComparator());
    System.out.println("-----年龄排序开始-----");
    for(Person person : list){
        System.out.println(person.toString());
    }
    System.out.println("-----年龄排序结束-----");
}

运行结果:
shell
-----原始排序开始-----
Person{name='ccc', age=20}
Person{name='AAA', age=30}
Person{name='bbb', age=10}
Person{name='ddd', age=40}
-----原始排序结束-----
-----年龄排序开始-----
Person{name='bbb', age=10}
Person{name='ccc', age=20}
Person{name='AAA', age=30}
Person{name='ddd', age=40}
-----年龄排序结束-----

5.5.2 匿名内部类书写方式

Person类定义

java 复制代码
package com.gupaoedu;

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

    public Person() {

    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

排序测试

java 复制代码
@org.junit.Test
public void test03() {
    ArrayList<Person> list = new ArrayList<>();
    list.add(new Person("ccc", 20));
    list.add(new Person("AAA", 30));
    list.add(new Person("bbb", 10));
    list.add(new Person("ddd", 40));
    System.out.println("-----原始排序开始-----");
    for (Person person : list) {
        System.out.println(person.toString());
    }
    System.out.println("-----原始排序结束-----");

    Collections.sort(list, new Comparator<Person>() {
        @Override
        public int compare(Person p1, Person p2) {
            //升序排序
            return p1.getAge() - p2.getAge();
            //降序排序
            //return p2.getAge() - p1.getAge();
        }
    });
    System.out.println("-----年龄升序排序开始-----");
    for (Person person : list) {
        System.out.println(person.toString());
    }
    System.out.println("-----年龄升序排序结束-----");
}

运行结果:
shell
-----原始排序开始-----
Person{name='ccc', age=20}
Person{name='AAA', age=30}
Person{name='bbb', age=10}
Person{name='ddd', age=40}
-----原始排序结束-----
-----年龄排序开始-----
Person{name='bbb', age=10}
Person{name='ccc', age=20}
Person{name='AAA', age=30}
Person{name='ddd', age=40}
-----年龄排序结束-----

三、ArrayList源码

ArrayList`本质就是动态数组,动态扩容。

3.1 初始化
java 复制代码
```java
/**
* 一些初始化的信息
*/
// 序列化 ID
private static final long serialVersionUID = 8683452581122892189L;

// 初始容量
private static final int DEFAULT_CAPACITY = 10;

// 用于空实例的共享数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};

// 用于默认大小的空实例的数组,区别于 EMPTY_ELEMENTDATA[] 数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//  真实存储 ArrayList元素的数组缓冲区,其长度即为 ArrayList的容量,在第一次添加元素时就会扩展为 DEFAULT_CAPACITY
transient Object[] elementData;

//  ArrayList的实际长度(保存的元素的个数)
private int size;

// 当前 ArrayList被结构化修改的次数,结构化修改指的是 修改了列表长度或者以某种方式扰乱了列表使之在迭代时不安全的操作
//  该字段由 Iterator或者 ListIterator或返回以上二者的方法使用,如果这个值发生了意料之外的改变,迭代器就会报并发修改异常。这提供了快速失败解决方案
// 在其子类下使用 modCound是可选择的(protected修饰了),如果子类希望提供快速失败的迭代那么它也可以在它的 add()等结构化修改方法中修改 modCount的值,单次的结构修改方法的调用最多只能修改一次 modCount的值;但是如果不希望提供 快速失败 的迭代器,子类可以忽略 modCount
// 我之前一直以为 modCount定义在 迭代器中。。。,现在才知道定义在被迭代的对象内
protected transient int modCount; // 定义在 AbstractList中

//  列表最大能分配的量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
3.2 构造方法

空参构造。

java 复制代码
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

有参构造,指定集合的长度创建一个 ArrayList。

java 复制代码
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 依据输入的大小,初始化 elementData[] 数组(真实存储元素的数组)
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        // 使用默认的大小(也就是 10)
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        // 针对负数等随便输入的东西,报错非法参数异常(非法的初始容量:xxx)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

有参构造,指定一个已有的集合创建 ArrayList(原集合内的元素会添加到 新建的ArrayList中),此时 ArrayList的长度等于 传入的集合的长度。

java 复制代码
public ArrayList(Collection<? extends E> c) {
    // 将参数集合转为数组
    elementData = c.toArray();
    // 判断数组大小
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class) {
            elementData = Arrays.copyOf(elementData, size, Object[].class);
        }
    } else {
        // 如果传参的集合为空,当前 ArrayList的 elementData[]数组就是空的
        this.elementData = EMPTY_ELEMENTDATA;
    }
}
3.3 add方法

在列表的末尾添加元素,事实上就是在 `elementData[]`数组中添加下一个值。

java 复制代码
public boolean add(E e) {
    //  判断列表容量够不够,实际判断 elementData[]数组里还有没有多余的空闲空间
    ensureCapacityInternal(size + 1); // increments modCount
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    // 确保容量够用,calculateCapacity()计算扩容
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果列表是默认空参构造创建的,并且本次是第一次 add
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 那么会直接把容量加到 10
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 否则就正常 +1就行
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
    // 结构化修改,增加该值,保证该集合是快速失败的
    modCount++;

    // overflow-conscious code
    // 判断是否会溢出
    if (minCapacity - elementData.length > 0)
        // 如果会溢出,就需要扩容了(也就是说 原有的 size+1 > capacity了,装不下了)
        grow(minCapacity);
}

private void grow(int minCapacity) {    
    // overflow-conscious code    
    // 获取原来的容量    
    int oldCapacity = elementData.length;    
    // 获取常规的扩容大小    
    int newCapacity = oldCapacity + (oldCapacity >> 1);    
    // 如果常规扩容之后还不够大,索性就你要多少就给你多少吧    
    if (newCapacity - minCapacity < 0)        
        newCapacity = minCapacity;    
    //  MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8,也就是说如果你要的容量很大,那么就调用 另一个方法    
    if (newCapacity - MAX_ARRAY_SIZE > 0)        
        // 定义放在下面        
        newCapacity = hugeCapacity(minCapacity);    
    // minCapacity is usually close to size, so this is a win:    
    // 真实的数组拷贝  
    elementData = Arrays.copyOf(elementData, newCapacity);
}
3.4 get方法

返回列表中 指定位置的元素。

java 复制代码
public E get(int index) {    
    // 检查索引是否正常,定义放在下面    
    rangeCheck(index);    
    // 返回 elementData[]数组中,该下标的元素    
    return elementData(index);
}

private void rangeCheck(int index) {    
    // 如果指定的值大于数组的最大下标,就抛出异常    
    if(index >= size) {        
        // 抛出异常,outOfBoudnsMsg()方法定义放在下面        
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));    
    }
}

private String outOfBoundsMsg(int index) {    
    return "Index: " + index + ", Size: " + size;
}
3.5 set方法

使用传入的参数,替换指定位置的元素,并返回原来的值。

java 复制代码
public E set(int index, E element) {
    // 同 get()方法一样,先检查 index是不是比 size小
    rangeCheck(index);
    
    // 获取该位置上原来的值
    E oldValue = elementData[index];
    // 在 elementData[]数组中替换该位置下的值
    elementData[index] = element;
    // 返回原来的值
    return oldValue;
}
3.6 remove方法

移除列表中指定位置的元素,数组中后续的元素都会左移一位,并且返回被移除的元素。

java 复制代码
public E remove(int index) {
    // 检查索引越界
    rangeCheck(index);
    // 结构化修改,变更 modCount的值
    modCount++;
    // 获取旧的值
    E oldValue = elementData(index);
    
    // 计算需要移动的数字的个数
    int numMoved = size - index - 1;
    if(numMoved > 0) {
        // 通过数组拷贝的形式,将从待移除位置开始往后的所有元素前移一位
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    }
    // 将最后一个元素置为 null(因为数组拷贝的关系,在 elementData[size-1] 和 elementData[size-2]中存的都是最后一个值,重复了)
    elementData[--size] = null; // clear to let GC do its work
    // 返回旧的值
    return oldValue;
}
3.7 FailFast机制

快速失败机制,是`java`集合类应对并发访问在对集合进行迭代过程中,内部对象结构发生变化一种防护措施.这种错误检测的机制为这种有可能发生错误,通过抛出`java.util.ConcurrentModificationException`

java 复制代码
import java.util.Iterator;
import java.util.List;

public class ThreadIterate extends Thread {

    private List list;

    public ThreadIterate(List list){
        this.list = list;
    }

    @Override
    public void run() {
        while(true){
            for (Iterator iteratorTmp = list.iterator();iteratorTmp.hasNext();) {
                iteratorTmp.next();
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
java 复制代码
public class ThreadAdd extends Thread{

    private List list;

    public ThreadAdd(List list){
        this.list = list;
    }

    public void run(){
        for (int i = 0; i < 100; i++) {
            System.out.println("loop execute : " + i);
            try {
                Thread.sleep(5);
                list.add(i);
            }catch (Exception e){

            }
        }
    }
}
java 复制代码
import java.util.ArrayList;
import java.util.List;

public class ThreadMain {

    private static List list = new ArrayList();

    public static void main(String[] args) {
        new ThreadAdd(list).start();
        new ThreadIterate(list).start();
    }
}

执行结果:
shell
loop execute : 0
loop execute : 1
loop execute : 2
Exception in thread "Thread-1" java.util.ConcurrentModificationException
at java.util.ArrayListItr.checkForComodification(ArrayList.java:911) at java.util.ArrayListItr.next(ArrayList.java:861)
at com.gupao.arraylist.ThreadIterate.run(ThreadIterate.java:18)
loop execute : 3
loop execute : 4
loop execute : 5
loop execute : 6
loop execute : 7

四、LinkedList源码

`LinkedList`类中的一个内部私有类Node。

java 复制代码
private static class Node<E> {
    E item;//节点值
    Node<E> next;//后继节点
    Node<E> prev;//前驱节点

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}
4.1 构造方法

空构造方法。

java 复制代码
public LinkedList() {
}

用已有的集合创建链表的构造方法。

java 复制代码
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}
4.2 add方法

add(E e) 方法:将元素添加到链表尾部。

java 复制代码
public boolean add(E e) {
    linkLast(e);//这里就只调用了这一个方法
    return true;
}

链接使e作为最后一个元素。

java 复制代码
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;//新建节点
    if (l == null)
        first = newNode;
    else
        l.next = newNode;//指向后继元素也就是指向下一个元素
    size++;
    modCount++;
}

add(int index,E e):在指定位置添加元素。

java 复制代码
public void add(int index, E element) {
    checkPositionIndex(index); //检查索引是否处于[0-size]之间

    if (index == size)//添加在链表尾部
        linkLast(element);
    else//添加在链表中间
        linkBefore(element, node(index));
}

addAll(Collection c ):将集合插入到链表尾部。

java 复制代码
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

addAll(int index, Collection c): 将集合从指定位置开始插入。

java 复制代码
public boolean addAll(int index, Collection<? extends E> c) {
    //1:检查index范围是否在size之内
    checkPositionIndex(index);

    //2:toArray()方法把集合的数据存到对象数组中
    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        return false;

    //3:得到插入位置的前驱节点和后继节点
    Node<E> pred, succ;
    //如果插入位置为尾部,前驱节点为last,后继节点为null
    if (index == size) {
        succ = null;
        pred = last;
    }
    //否则,调用node()方法得到后继节点,再得到前驱节点
    else {
        succ = node(index);
        pred = succ.prev;
    }

    // 4:遍历数据将数据插入
    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        //创建新节点
        Node<E> newNode = new Node<>(pred, e, null);
        //如果插入位置在链表头部
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        pred = newNode;
    }

    //如果插入位置在尾部,重置last节点
    if (succ == null) {
        last = pred;
    }
    //否则,将插入的链表与先前链表连接起来
    else {
        pred.next = succ;
        succ.prev = pred;
    }

    size += numNew;
    modCount++;
    return true;
}

上面可以看出addAll方法通常包括下面四个步骤:

  • 检查index范围是否在size之内。
  • toArray()方法把集合的数据存到对象数组中。
  • 得到插入位置的前驱和后继节点。
  • 遍历数据,将数据插入到指定位置。
    addFirst(E e): 将元素添加到链表头部。
java 复制代码
public void addFirst(E e) {
     linkFirst(e);
 }

addLast(E e): 将元素添加到链表尾部,与 add(E e) 方法一样。

java 复制代码
public void addLast(E e) {
    linkLast(e);
}
4.3 根据位置取数据的方法
java 复制代码
public E get(int index) {
    //检查index范围是否在size之内
    checkElementIndex(index);
    //调用Node(index)去找到index对应的node然后返回它的值
    return node(index).item;
}

获取头节点(index=0)数据方法。

java 复制代码
public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}

public E element() {
    return getFirst();
}

public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

public E peekFirst() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

区别:** `getFirst(),element(),peek(),peekFirst()` 这四个获取头结点方法的区别在于对链表为空时的处理,是抛出异常还是返回null,其中`getFirst()` 和`element()` 方法将会在链表为空时,抛出异常。
element()方法的内部就是使用`getFirst()`实现的。它们会在链表为空时,抛出`NoSuchElementException`。
获取尾节点(index=-1)数据方法。

java 复制代码
public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}
public E peekLast() {
    final Node<E> l = last;
    return (l == null) ? null : l.item;
}

两者区别:`getLast()` 方法在链表为空时,会抛出`NoSuchElementException`,而`peekLast()` 则不会,只是会返回 null。

4.4 根据对象得到索引的方法

int indexOf(Object o): 从头遍历找。

java 复制代码
public int indexOf(Object o) {
    int index = 0;
    if (o == null) {
        //从头遍历
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else {
        //从头遍历
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}

int lastIndexOf(Object o): 从尾遍历找。

java 复制代码
public int lastIndexOf(Object o) {
    int index = size;
    if (o == null) {
        //从尾遍历
        for (Node<E> x = last; x != null; x = x.prev) {
            index--;
            if (x.item == null)
                return index;
        }
    } else {
        //从尾遍历
        for (Node<E> x = last; x != null; x = x.prev) {
            index--;
            if (o.equals(x.item))
                return index;
        }
    }
    return -1;
}

检查链表是否包含某对象的方法:contains(Object o): 检查对象o是否存在于链表中。

java 复制代码
public boolean contains(Object o) {
	return indexOf(o) != -1;
}
4.5 删除方法

`remove() ,removeFirst(),pop()`:删除头节点。

java 复制代码
public E pop() {
    return removeFirst();
}

public E remove() {
    return removeFirst();
}

public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

`removeLast(),pollLast()`: 删除尾节点。

java 复制代码
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}

public E pollLast() {
    final Node<E> l = last;
    return (l == null) ? null : unlinkLast(l);
}

区别: `removeLast()`在链表为空时将抛出`NoSuchElementException`,而`pollLast()`方法返回null。
`remove(Object o)`:删除指定元素。

java 复制代码
public boolean remove(Object o) {
    //如果删除对象为null
    if (o == null) {
        //从头开始遍历
        for (Node<E> x = first; x != null; x = x.next) {
            //找到元素
            if (x.item == null) {
                //从链表中移除找到的元素
                unlink(x);
                return true;
            }
        }
    } else {
        //从头开始遍历
        for (Node<E> x = first; x != null; x = x.next) {
            //找到元素
            if (o.equals(x.item)) {
                //从链表中移除找到的元素
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

当删除指定对象时,只需调用remove(Object o)即可,不过该方法一次只会删除一个匹配的对象,如果删除了匹配对象,返回true,否则false。
unlink(Node x) 方法:

java 复制代码
E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;//得到后继节点
    final Node<E> prev = x.prev;//得到前驱节点

    //删除前驱指针
    if (prev == null) {
        first = next;//如果删除的节点是头节点,令头节点指向该节点的后继节点
    } else {
        prev.next = next;//将前驱节点的后继节点指向后继节点
        x.prev = null;
    }

    //删除后继指针
    if (next == null) {
        last = prev;//如果删除的节点是尾节点,令尾节点指向该节点的前驱节点
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}

remove(int index):删除指定位置的元素。

java 复制代码
public E remove(int index) {
    //检查index范围
    checkElementIndex(index);
    //将节点删除
    return unlink(node(index));
}
4.6 LinkedList类常用方法测试
java 复制代码
import java.util.Iterator;
import java.util.LinkedList;

public class LinkedListDemo {
    public static void main(String[] srgs) {
        //创建存放int类型的linkedList
        LinkedList<Integer> linkedList = new LinkedList<>();
        /************************** linkedList的基本操作 ************************/
        linkedList.addFirst(0); // 添加元素到列表开头
        linkedList.add(1); // 在列表结尾添加元素
        linkedList.add(2, 2); // 在指定位置添加元素
        linkedList.addLast(3); // 添加元素到列表结尾
        
        System.out.println("LinkedList(直接输出的): " + linkedList);

        System.out.println("getFirst()获得第一个元素: " + linkedList.getFirst()); // 返回此列表的第一个元素
        System.out.println("getLast()获得第最后一个元素: " + linkedList.getLast()); // 返回此列表的最后一个元素
        System.out.println("removeFirst()删除第一个元素并返回: " + linkedList.removeFirst()); // 移除并返回此列表的第一个元素
        System.out.println("removeLast()删除最后一个元素并返回: " + linkedList.removeLast()); // 移除并返回此列表的最后一个元素
        System.out.println("After remove:" + linkedList);
        System.out.println("contains()方法判断列表是否包含1这个元素:" + linkedList.contains(1)); // 判断此列表包含指定元素,如果是,则返回true
        System.out.println("该linkedList的大小 : " + linkedList.size()); // 返回此列表的元素个数

        /************************** 位置访问操作 ************************/
        System.out.println("-----------------------------------------");
        linkedList.set(1, 3); // 将此列表中指定位置的元素替换为指定的元素
        System.out.println("After set(1, 3):" + linkedList);
        System.out.println("get(1)获得指定位置(这里为1)的元素: " + linkedList.get(1)); // 返回此列表中指定位置处的元素

        /************************** Search操作 ************************/
        System.out.println("-----------------------------------------");
        linkedList.add(3);
        System.out.println("indexOf(3): " + linkedList.indexOf(3)); // 返回此列表中首次出现的指定元素的索引
        System.out.println("lastIndexOf(3): " + linkedList.lastIndexOf(3));// 返回此列表中最后出现的指定元素的索引

        /************************** Queue操作 ************************/
        System.out.println("-----------------------------------------");
        System.out.println("peek(): " + linkedList.peek()); // 获取但不移除此列表的头
        System.out.println("element(): " + linkedList.element()); // 获取但不移除此列表的头
        linkedList.poll(); // 获取并移除此列表的头
        System.out.println("After poll():" + linkedList);
        linkedList.remove();
        System.out.println("After remove():" + linkedList); // 获取并移除此列表的头
        linkedList.offer(4);
        System.out.println("After offer(4):" + linkedList); // 将指定元素添加到此列表的末尾

        /************************** Deque操作 ************************/
        System.out.println("-----------------------------------------");
        linkedList.offerFirst(2); // 在此列表的开头插入指定的元素
        System.out.println("After offerFirst(2):" + linkedList);
        linkedList.offerLast(5); // 在此列表末尾插入指定的元素
        System.out.println("After offerLast(5):" + linkedList);
        System.out.println("peekFirst(): " + linkedList.peekFirst()); // 获取但不移除此列表的第一个元素
        System.out.println("peekLast(): " + linkedList.peekLast()); // 获取但不移除此列表的第一个元素
        linkedList.pollFirst(); // 获取并移除此列表的第一个元素
        System.out.println("After pollFirst():" + linkedList);
        linkedList.pollLast(); // 获取并移除此列表的最后一个元素
        System.out.println("After pollLast():" + linkedList);
        linkedList.push(2); // 将元素推入此列表所表示的堆栈(插入到列表的头)
        System.out.println("After push(2):" + linkedList);
        linkedList.pop(); // 从此列表所表示的堆栈处弹出一个元素(获取并移除列表第一个元素)
        System.out.println("After pop():" + linkedList);
        linkedList.add(3);
        linkedList.removeFirstOccurrence(3); // 从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表)
        System.out.println("After removeFirstOccurrence(3):" + linkedList);
        linkedList.removeLastOccurrence(3); // 从此列表中移除最后一次出现的指定元素(从尾部到头部遍历列表)
        System.out.println("After removeFirstOccurrence(3):" + linkedList);

        /************************** 遍历操作 ************************/
        System.out.println("-----------------------------------------");
        linkedList.clear();
        for (int i = 0; i < 100000; i++) {
            linkedList.add(i);
        }
        // 迭代器遍历
        long start = System.currentTimeMillis();
        Iterator<Integer> iterator = linkedList.iterator();
        while (iterator.hasNext()) {
            iterator.next();
        }
        long end = System.currentTimeMillis();
        System.out.println("Iterator:" + (end - start) + " ms");

        // 顺序遍历(随机遍历)
        start = System.currentTimeMillis();
        for (int i = 0; i < linkedList.size(); i++) {
            linkedList.get(i);
        }
        end = System.currentTimeMillis();
        System.out.println("for:" + (end - start) + " ms");

        // 另一种for循环遍历
        start = System.currentTimeMillis();
        for (Integer i : linkedList)
            ;
        end = System.currentTimeMillis();
        System.out.println("for2:" + (end - start) + " ms");

        // 通过pollFirst()或pollLast()来遍历LinkedList
        LinkedList<Integer> temp1 = new LinkedList<>();
        temp1.addAll(linkedList);
        start = System.currentTimeMillis();
        while (temp1.size() != 0) {
            temp1.pollFirst();
        }
        end = System.currentTimeMillis();
        System.out.println("pollFirst()或pollLast():" + (end - start) + " ms");

        // 通过removeFirst()或removeLast()来遍历LinkedList
        LinkedList<Integer> temp2 = new LinkedList<>();
        temp2.addAll(linkedList);
        start = System.currentTimeMillis();
        while (temp2.size() != 0) {
            temp2.removeFirst();
        }
        end = System.currentTimeMillis();
        System.out.println("removeFirst()或removeLast():" + (end - start) + " ms");
    }
}

五、Vector

`Vector`和`ArrayList`有一些相似,其内部都是通过一个容量能够动态增长的数组来实现的。不同点是Vector是线程安全的。因为其内部有很多同步代码快来保证线程安全。
每个操作方法都加的有synchronized关键字,针对性能来说会比较大的影响,慢慢就被放弃了。
可以增加代码的灵活度,在我们需要同步是时候就通过如下代码实现。

java 复制代码
List syncList = Collections.synchronizedList(list);

然后再使用操作方法时就会是安全的了,转换之后再操作,其本质上就是这样:

java 复制代码
public E get(int index) {
    synchronized (mutex) {return list.get(index);}
}

public E set(int index, E element) {
    synchronized (mutex) {return list.set(index, element);}
}

public void add(int index, E element) {
    synchronized (mutex) {list.add(index, element);}
}

public E remove(int index) {
    synchronized (mutex) {return list.remove(index);}
}

六、Set接口和Map接口

6.1 HashSet

HashSet实现Set接口,由哈希表支持,它不保证set的迭代顺序,特别是它不保证该顺序永久不变,运行使用null。
本质上是将数据保持在 HashMap中 key就是我们添加的内容,value就是我们定义的一个Object对象。
底层数据结构是哈希表,HashSet的本质是一个"没有重复元素"的集合,他是通过`HashMap`实现的.HashSet中含有一个HashMap类型的成员变量`map`。

java 复制代码
public HashSet() {
    map = new HashMap<>();
}

// add方法
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
6.2 TreeSet

基于TreeMap的 NavigableSet实现。使用元素的自然顺序对元素进行排序,或者根据创建 set 时提供的 Comparator进行排序,具体取决于使用的构造方法。
本质是将数据保存在TreeMap中,key是我们添加的内容,value是定义的一个Object对象。

java 复制代码
public TreeSet() {
    this(new TreeMap<E,Object>());
}
6.3 HashMap
6.3.1 特点:

HashMap是一个集合,键值对的集合,源码中每个节点用Node<K,V>表示。

java 复制代码
static class Node<K,V> implements Map.Entry<K,V> {
   final int hash;
   final K key;
   V value;
   Node<K,V> next;
}

Node是一个内部类,这里的key为键,value为值,next指向下一个元素,可以看出HashMap中的元素不是一个单纯的键值对,还包含下一个元素的引用。

6.3.2 数据结构:

HashMap的数据结构为数组+(链表或红黑树)。
数组的特点:查询效率高,插入,删除效率低。
链表的特点:查询效率低,插入删除效率高。
在HashMap底层使用数组加(链表或红黑树)的结构完美的解决了数组和链表的问题,使得查询和插入,删除的效率都很高。
java1.7 之前是数组+链表 ,之后是 数组+链表+红黑树。

6.3.3 HashMap存储元素的过程
java 复制代码
HashMap<String,String> map = new HashMap<String,String>();
map.put("刘德华","张惠妹");
map.put("张学友","大S");

首先,计算出键"刘德华"的hashcode,该值用来定位要将这个元素存放到数组中的什么位置。
什么是hashcode?
在Object类中有一个方法:public native int hashCode();
该方法用native修饰,所以是一个本地方法,所谓本地方法就是非java代码,这个代码通常用c或c++写成,在java中可以去调用它。
调用这个方法会生成一个int型的整数,我们叫它哈希码,哈希码和调用它的对象地址和内容有关。
哈希码的特点是:
对于同一个对象如果没有被修改(使用equals比较返回true)那么无论何时它的hashcode值都是相同的。
对于两个对象如果他们的equals返回false,那么他们的hashcode值也有可能相等。
明白了hashcode我们再来看元素如何通过hashcode定位到要存储在数组的哪里,通过hashcode值和数组长度取模我们可以得到元素存储的下标。
刘德华的hashcode为20977295 数组长度为 16则要存储在数组索引为 20977295%16=1的地方。
可以分两种情况:

  1. 数组索引为1的地方是空的,这种情况很简单,直接将元素放进去就好了。
  2. 已经有元素占据了索引为1的位置,这种情况下我们需要判断一下该位置的元素和当前元素是否相等,使用equals来比较。
    如果使用默认的规则是比较两个对象的地址。也就是两者需要是同一个对象才相等,当然我们也可以重写equals方法来实现我们自己的比较规则最常见的是通过比较属性值来判断是否相等。
    如果两者相等则直接覆盖,如果不等则在原元素下面使用链表的结构存储该元素。
    每个元素节点都有一个next属性指向下一个节点,这里由数组结构变成了数组+链表结构,红黑树又是怎么回事呢?
    因为链表中元素太多的时候会影响查找效率,所以当链表的元素个数达到8的时候使用链表存储就转变成了使用红黑树存储,原因就是红黑树是平衡二叉树,在查找性能方面比链表要高。
6.3.4 HashMap中有两个重要的参数

初始容量大小和加载因子,初始容量大小是创建时给数组分配的容量大小,默认值为16,加载因子默认0.75f,用数组容量大小乘以加载因子得到一个值,一旦数组中存储的元素个数超过该值就会调用rehash方法将数组容量增加到原来的两倍,专业术语叫做扩容。
在做扩容的时候会生成一个新的数组,原来的所有数据需要重新计算哈希码值重新分配到新的数组,所以扩容的操作非常消耗性能。

6.3.5 HashMap的put()和get()的实现

map.put(k,v)实现原理:
首先将k,v封装到Node对象当中(节点)。
它的底层会调用K的hashCode()方法得出hash值。
通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。
java1.8 中put 源码:put 中调用 putVal()方法:
首先判断map中是否有数据,没有就执行resize方法。
如果要插入的键值对要存放的这个位置刚好没有元素,那么把他封装成Node对象,放在这个位置上即可。
如果这个元素的key与要插入的一样,那么就替换一下。
如果当前节点是TreeNode类型的数据,执行putTreeVal方法。
遍历这条链子上的数据,完成了操作后多做了一件事情,判断,并且可能执行treeifyBin方法。
map.get(k)实现原理
先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。
通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。重点理解如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着参数K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。

6.3.6 java 1.7 和 java1.8 HashMap 的区别

jdk1.7中HashMap采用的是位桶+链表的方式,即我们常说的散列链表的方式。
jdk1.8中采用的是位桶+链表/红黑树的方式,也是非线程安全的。当某个位桶的链表的长度达到某个阀值(8)的时候,这个链表就将转换成红黑树。
在jdk1.8中,如果链表长度大于8且节点数组长度大于64的时候,就把链表下所有的节点转为红黑树。
树形化还有一个要求就是数组长度必须大于等于64,否则继续采用扩容策略
总的来说,HashMap默认采用数组+单链表方式存储元素,当元素出现哈希冲突时,会存储到该位置的单链表中。但是单链表不会一直增加元素,当元素个数超过8个时,会尝试将单链表转化为红黑树存储。但是在转化前,会再判断一次当前数组的长度,只有数组长度大于64才处理。否则,进行扩容操作。

java 复制代码
static final int TREEIFY_THRESHOLD = 8;
 
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
}
  
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab;
    Node<K,V> p;
    int n, i;
    //如果当前map中无数据,执行resize方法。并且返回n
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
     //如果要插入的键值对要存放的这个位置刚好没有元素,那么把他封装成Node对象,放在这个位置上即可
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
    //否则的话,说明这上面有元素
        else {
            Node<K,V> e; K k;
        //如果这个元素的key与要插入的一样,那么就替换一下。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
        //1.如果当前节点是TreeNode类型的数据,执行putTreeVal方法
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
        //还是遍历这条链子上的数据,跟jdk7没什么区别
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
            //2.完成了操作后多做了一件事情,判断,并且可能执行treeifyBin方法
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null) //true || --
                    e.value = value;
           //3.
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
    //判断阈值,决定是否扩容
        if (++size > threshold)
            resize();
        //4.
        afterNodeInsertion(evict);
        return null;
    }
6.4 TreeMap

TreeMap具有如下特点:
不允许出现重复的key;
可以插入null键,null值;
可以对元素进行排序;
无序集合(插入和遍历顺序不一致);

6.4.1 构造函数

`TreeMap()` 使用默认构造函数构造`TreeMap`的时候,使用默认的比较器来进行`key`的比较,对TreeMap进行**升序**排序;
`TreeMap(Comparator<? super K> comparator)` 带比较器(`comparator`)的构造函数,用户可以自定义比较器,按照自己的需要对TreeMap进行排序;
`TreeMap(Map<? extends K, ? extends V> copyFrom)` 基于一个map创建一个新的TreeMap,使用默认比较器升序排序;
`TreeMap(SortedMap<K, ? extends V> copyFrom)` 基于一个SortMap创建一个新的TreeMap(SortMap是有序的)。

java 复制代码
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

public class TreeMapTest {
    public static void main(String[] args) {
        HashMap<Integer, String> hmap = new HashMap<>();
        hmap.put(13, "Yellow");
        hmap.put(3, "Red");
        hmap.put(2, "Green");
        hmap.put(33, "Blue");
        System.out.println("key & values in hmap:");
        for (Map.Entry entry : hmap.entrySet()) {
            System.out.println("key: " + entry.getKey() + ", value: " + entry.getValue());
        }
        TreeMap<Integer, String> tmap = new TreeMap<>(hmap);
        System.out.println("key & values in tmap:");
        for (Map.Entry entry : tmap.entrySet()) {
            System.out.println("key: " + entry.getKey() + ", value: " + entry.getValue());
        }
    }
}

执行结果:
shell
key & values in hmap:
key: 33, value: Blue
key: 2, value: Green
key: 3, value: Red
key: 13, value: Yellow
key & values in tmap:
key: 2, value: Green
key: 3, value: Red
key: 13, value: Yellow
key: 33, value: Blue
可以看出,hmap中没有排序,实际看到的打印出的"顺序"是键值对的添加顺序的倒序;而tmap则是按key的大小升序排序的。

java 复制代码
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

public class TreeMapTest {
    public static void main(String[] args) {
        TreeMap<Integer, String> tmap = new TreeMap<>();
        System.out.println("tmap is empty: " + tmap.isEmpty());
        tmap.put(13, "Yellow");
        tmap.put(3, "Red");
        tmap.put(2, "Green");
        tmap.put(33, "Blue");
        System.out.println("key & values in tmap:");
        for (Map.Entry entry : tmap.entrySet()) {
            System.out.println("key: " + entry.getKey() + ", value: " + entry.getValue());
        }
        System.out.println("size of tmap is: " + tmap.size());
        System.out.println("tmap contains value Purple: " + tmap.containsValue("Purple"));
        System.out.println("tmap contains key 12: " + tmap.containsKey(12));
        System.out.println("last key in tmap is: " + tmap.lastKey());
        System.out.println("key is 14 & value is " + tmap.get(14));
        System.out.println("remove key 13");
        tmap.remove(13);
        System.out.println("tmap contains key 13: " + tmap.containsKey(13));
        System.out.println("key in tmap:");
        Iterator iterator = tmap.keySet().iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
        System.out.println("clear tmap");
        tmap.clear();
        System.out.println("size of tmap: " + tmap.size());
    }
}

执行结果:
tmap is empty: true
key & values in tmap:
key: 2, value: Green
key: 3, value: Red
key: 13, value: Yellow
key: 33, value: Blue
size of tmap is: 4
tmap contains value Purple: false
tmap contains key 12: false
last key in tmap is: 33
key is 14 & value is null
remove key 13
tmap contains key 13: false
key in tmap:
2
3
33
clear tmap
size of tmap: 0

6.5 TreeSet和TreeMap的区别

相同点:
1、都是有序集合。
2、TreeMap是TreeSet的底层结构。
3、运行速度都比hash慢。
区别:
1、TreeSet只存储一个对象,而TreeMap存储两个对象Key和Value(仅仅key对象有序)。
2、TreeSet中不能有重复对象,而TreeMap中可以存在。
3、TreeMap的底层采用红黑树的实现,完成数据有序的插入,排序。
PS 红黑树的特点:
1:每个节点要么是红色/黑色。
2:根节点是黑色的。
3:所有的叶节点都是黑色空节点。
4:每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点)。
5:从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。

6.6 HashSet和HashMap的区别
java 复制代码
| HashMap                                     | HashSet                                                      |
| ------------------------------------------- | ------------------------------------------------------------ |
| HashMap实现了Map接口                         | HashSet实现了Set接口                                         |
| HashMap储存键值对                            | HashSet仅仅存储对象                                          | 
| 使用put()方法将元素放入map中                  | 使用add()方法将元素放入set中                                 |
| HashMap中使用键对象来计算hashcode值           | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的化,那么返回false |
| HashMap比较快,因为是使用唯一的键来获取对象    | HashSet较HashMap来说比较慢                                   |

七、泛型

7.1 为什么使用泛型

早期的Object类型可以接收任意的对象类型,但是在实际的使用中,会有类型转换的问题。也就存在这隐患,所以Java提供了泛型来解决这个安全问题。

java 复制代码
public static void main(String[] args) {
    //测试一下泛型的经典案例
    ArrayList arrayList = new ArrayList();
    arrayList.add("helloWorld");
    arrayList.add("taiziyenezha");
    arrayList.add(88);//由于集合没有做任何限定,任何类型都可以给其中存放
    for (int i = 0; i < arrayList.size(); i++) {
        //需求:打印每个字符串的长度,就要把对象转成String类型
        String str = (String) arrayList.get(i);
        System.out.println(str.length());
    }
}

运行这段代码,程序在运行时发生了异常:
> Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
发生了数据类型转换异常,这是为什么?
由于`ArrayList`可以存放任意类型的元素。例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,导致取出时强制转换为String类型后,引发了`ClassCastException`,因此程序崩溃了。
这显然不是我们所期望的,如果程序有潜在的错误,我们`更期望在编译时被告知错误`,而不是在运行时报异常。而为了解决类似这样的问题(在编译阶段就可以解决),在jdk1.5后,泛型应运而生。让你在设计API时可以指定类或方法支持泛型,这样我们使用API的时候也变得更为简洁,并得到了编译时期的语法检查。

java 复制代码
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("helloWorld");
arrayList.add("taiziyenezha");
arrayList.add(88);// 在编译阶段,编译器就会报错

这样可以避免了我们类型强转时出现异常。

7.2 什么是泛型

泛型
是一种把明确类型的工作推迟到创建对象或者调用方法的时候才去明确的特殊的类型。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,而这种参数类型可以用在**类、方法和接口**中,分别被称为`泛型类`、`泛型方法`、`泛型接口`。
> 注意:一般在创建对象时,将未知的类型确定具体的类型。当没有指定泛型时,默认类型为Object类型。

7.3 使用泛型的好处

避免了类型强转的麻烦。
它提供了编译期的**类型安全**,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现ClassCastException。

7.4 泛型的使用

泛型虽然通常会被大量的使用在集合当中,但是我们也可以完整的学习泛型只是。泛型有三种使用方式,分别为:泛型类、泛型方法、泛型接口。将数据类型作为参数进行传递。

7.4.1 泛型类

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种集合框架容器类,如:List、Set、Map。
泛型类的定义格式:`修饰符 class 类名<代表泛型的变量> { }`

java 复制代码
/**
 * @param <T> 这里解释下<T>中的T:
 *           此处的T可以随便写为任意标识,常见的有T、E等形式的参数表示泛型
 *           泛型在定义的时候不具体,使用的时候才变得具体。
 *           在使用的时候确定泛型的具体数据类型。即在创建对象的时候确定泛型。
 */
public class GenericsClassDemo<T> {
    //t这个成员变量的类型为T,T的类型由外部指定
    private T t;
    //泛型构造方法形参t的类型也为T,T的类型由外部指定
    public GenericsClassDemo(T t) {
        this.t = t;
    }
    //泛型方法getT的返回值类型为T,T的类型由外部指定
    public T getT() {
        return t;
    }
}

泛型在定义的时候不具体,使用的时候才变得具体。在使用的时候确定泛型的具体数据类型。即:**在创建对象的时候确定泛型。**
例如:`Generic<String> genericString = new Generic<String>("helloGenerics");`
此时,泛型标识T的类型就是String类型,那我们之前写的类就可以这么认为:

java 复制代码
public class GenericsClassDemo<String> {
    private String t;
    public GenericsClassDemo(String t) {
        this.t = t;
    }
    public String getT() {
        return t;
    }
}

当你的泛型类型想变为Integer类型时,也是很方便的。直接在创建时,T写为Integer类型即可:
`Generic<Integer> genericInteger = new Generic<Integer>(666);`

注意:定义的泛型类,就一定要传入泛型类型实参么?
并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。即跟之前的经典案例一样,没有写`ArrayList`的泛型类型,容易出现类型强转的问题。

7.4.2 泛型方法

泛型方法,是在调用方法的时候指明泛型的具体类型 。
定义格式:`修饰符 <代表泛型的变量> 返回值类型 方法名(参数){ }`

java 复制代码
/**
 *
 * @param t 传入泛型的参数
 * @param <T> 泛型的类型
 * @return T 返回值为T类型
 * 说明:
 *   1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
 *   2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 *   3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 *   4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E等形式的参数常用于表示泛型。
 */
public <T> T genercMethod(T t){
    System.out.println(t.getClass());
    System.out.println(t);
    return t;
}

// 调用方法时,确定泛型的类型
public static void main(String[] args) {
    GenericsClassDemo<String> genericString  = new GenericsClassDemo("helloGeneric"); //这里的泛型跟下面调用的泛型方法可以不一样。
    String str = genericString.genercMethod("hello");//传入的是String类型,返回的也是String类型
    Integer i = genericString.genercMethod(123);//传入的是Integer类型,返回的也是Integer类型
}

这里我们可以看下结果:
class java.lang.String
hello
class java.lang.Integer
123
这里可以看出,泛型方法随着我们的传入参数类型不同,他得到的类型也不同。泛型方法能使方法独立于类而产生变化。

7.4.3 泛型接口

泛型接口与泛型类的定义及使用基本相同。**泛型接口常被用在各种类的生产器中。
定义格式:`修饰符 interface接口名<代表泛型的变量> { }`

java 复制代码
/**
 * 定义一个泛型接口
 */
public interface GenericsInteface<T> {
    public abstract void add(T t); 
}

1. 定义类时确定泛型的类型

java 复制代码
public class GenericsImp implements GenericsInteface<String> {
    @Override
    public void add(String s) {
        System.out.println("设置了泛型为String类型");
    }
}

2. 始终不确定泛型的类型,直到创建对象时,确定泛型的类型

java 复制代码
public class GenericsImp<T> implements GenericsInteface<T> {
    @Override
    public void add(T t) {
        System.out.println("没有设置类型");
    }
}

确定泛型:

java 复制代码
public class GenericsTest {
    public static void main(String[] args) {
        GenericsImp<Integer> gi = new GenericsImp<>();
        gi.add(66);
    }
}
7.4.4 泛型通配符

当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符<?>表示。但是一旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。
通配符基本使用
泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。

java 复制代码
// ?代表可以接收任意类型
// 泛型不存在继承、多态关系,泛型左右两边要一样
//ArrayList<Object> list = new ArrayList<String>();这种是错误的
//泛型通配符?:左边写<?> 右边的泛型可以是任意类型
ArrayList<?> list1 = ``new` `ArrayList<Object>();
ArrayList<?> list2 = ``new` `ArrayList<String>();
ArrayList<?> list3 = ``new` `ArrayList<Integer>();

注意:泛型不存在继承、多态关系,泛型左右两边要一样,jdk1.7后右边的泛型可以省略,而泛型通配符?,右边的泛型可以是任意类型。
泛型通配符?主要应用在参数传递方面,让我们一起瞧瞧呗:

java 复制代码
public static void main(String[] args) {
    ArrayList<Integer> list1 = new ArrayList<Integer>();
    test(list1);
    ArrayList<String> list2 = new ArrayList<String>();
    test(list2);
}
public static void test(ArrayList<?> coll){
}

可以传递不同类似进去方法中了!
通配符高级使用
之前设置泛型的时候,实际上是可以任意设置的,只要是类就可以设置。但是在JAVA的泛型中可以指定一个泛型的**上限**和**下限**。
泛型的上限:
格式:类型名称 <? extends 类 > 对象名称。
意义: 只能接收该类型及其子类。
泛型的下限:
格式:类型名称 <? super 类 > 对象名称。
意义:只能接收该类型及其父类型。
比如:现已知Object类,Animal类,Dog类,Cat类,其中Animal是Dog,Cat的父类。

java 复制代码
class Animal{}//父类
class Dog extends Animal{}//子类
class Cat extends Animal{}//子类

可以看出,泛型的上限只能是该类型的类型及其子类。

我们再来看看泛型的下限<? super 类 >:

java 复制代码
ArrayList<? super Animal> list5 = new ArrayList<Object>();
ArrayList<? super Animal> list6 = new ArrayList<Animal>();
// ArrayList<? super Animal> list7 = new ArrayList<Dog>();//报错
// ArrayList<? super Animal> list8 = new ArrayList<Cat>();//报错

可以看出,泛型的下限只能是该类型的类型及其父类。

一般泛型的上限和下限也是用来参数的传递:
再比如:现已知Object类,String 类,Number类,Integer类,其中Number是Integer的父类。

java 复制代码
public static void main(String[] args) {
    Collection<Integer> list1 = new ArrayList<Integer>();
    Collection<String> list2 = new ArrayList<String>();
    Collection<Number> list3 = new ArrayList<Number>();
    Collection<Object> list4 = new ArrayList<Object>();
    getElement(list1);
    getElement(list2);//报错
    getElement(list3);
    getElement(list4);//报错
    getElement2(list1);//报错
    getElement2(list2);//报错
    getElement2(list3);
    getElement2(list4);
}
// 泛型的上限:此时的泛型?,必须是Number类型或者Number类型的子类
public static void getElement1(Collection<? extends Number> coll){}
// 泛型的下限:此时的泛型?,必须是Number类型或者Number类型的父类
public static void getElement2(Collection<? super Number> coll){}

我们在定义泛型类,泛型方法,泛型接口的时候经常会碰见很多不同的通配符,比如 T,E,K,V 等等,这些通配符又都是什么意思呢?
本质上这些个都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,?是这样约定的:
表示不确定的 java 类型T (type) 表示具体的一个java类型K V (key value) 分别代表java键值中的Key ValueE (element) 代表Element

八、反射

Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键。

8.1 反射机制原理

反射机制允许程序在执行期借助于 ReflectionAPI取得任何类的内部信息(比如成员变量,构造器,成员方法等等),井能操作对象的属性及方法。反射在设计模式和框架底层都会用到。
加载完类之后,在堆中就产生了ー个Cass类型的对象(一个类只有一个Class对象),这个对象包含了类的完整结构信息。通过这个对象得到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,形象的称之为反射。

8.2 Java反射机制可以哪些

在运行时判断任意一个对象所属的类。
在运行时构造任意一个类的对象。
在运行时得到任意一个类所具有的成员变量和方法。
在运行时调用任意一个对象的成员变量和方法。
生成动态代理。

8.3 反射相关的主要类

Java反射机制的实现要借助以下四个类:

  • java.lang.Class:代表一个类, Class对象表示某个类加載后在堆中的对象。
  • java. lang.reflec.Method:代表类的方法,Method对象表示某个类的方法。
  • java.lng.reflect.Field:代表类的成员变量,Field对象表示,某个类的成员变量。
  • java.lang.reflect.Constructor:代表类的构造方法,Constructor对象表示构造器。
8.4 如何使用反射
8.4.1 实例化对象

通过Object类的getClass方法获得(基本不用)。

java 复制代码
class Person {}
public class TestDemo {
    public static void main(String[] args) throwsException {
        Person per = newPerson() ; // 正着操作
        Class<?> cls = per.getClass() ; // 取得Class对象
        System.out.println(cls.getName()); // 反着来
    }
}

使用:"类.class"方式取得。

java 复制代码
class Person {}
public class TestDemo { 
    public static void main(String[] args) throwsException {
        Class<?> cls = Person.class; // 取得Class对象
        System.out.println(cls.getName()); // 反着来
    }
}

使用Class类内部定义的static forName( )方法。

java 复制代码
class Person {}
public class TestDemo {
    public static void main(String[] args) throwsException {
        Class<?> cls = Class.forName("cn.mldn.demo.Person") ; // 取得Class对象
        System.out.println(cls.getName()); // 反着来
    }
}
8.4.2 简单应用

传统的工厂模式代码。

java 复制代码
interface Fruit {
    public void eat();
}
class Apple implements Fruit{
    public void eat() {
        System.out.println("吃苹果。");
    }
}
class Factory {
    public static Fruit getInstance (String className){
        if ("apple".equals(className)) {
             returnnewApple();
         }
         return null;
     }
}
public class Factory Demo {
    public static void main(String[]args){
        Fruit f = Factory.getInstance("apple");
        f.eat();
     }
}

工厂设计模式之中有一个最大的问题:
如果现在接口的子类增加了,那么工厂类肯定需要修改,这是它所面临的最大问题,而这个最大问题造成的关键性的病因是new,那么如果说现在不使用关键字new了,变为了反射机制呢?
反射机制实例化对象的时候实际上只需要"包.类"就可以,于是根据此操作,修改工厂设计模式。

java 复制代码
interface Fruit {
    public void eat();
}
class Apple implements Fruit {
    public void eat() {
        System.out.println("吃苹果。");
    }
}
class Orange implements Fruit {
    public void eat() {
        System.out.println("吃橘子。");
    }
}
class Factory {
    public static Fruit getInstance(String className) {
        Fruit f = null;
        try {
            f = (Fruit) Class.forName(className).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return f;
    }
}
public class Factory Demo {
    public static void main(String[] args) {
        Fruit f = Factory.getInstance("cn.mldn.demo.Orange");
        f.eat();
    }
}

这个时候即使增加了接口的子类,工厂类照样可以完成对象的实例化操作,这个才是真正的工厂类,可以应对于所有的变化。
如果单独从开发角度而言,与开发者关系不大,但是对于日后学习的一些框架技术这个就是它实现的命脉,在日后的程序开发上,如果发现操作的过程之中需要传递了一个完整的"包.类"名称的时候几乎都是反射机制作用。

8.4.3 反射优化

关闭访问检查。
Method和 Field、 Constructor对象都有 setAccessible( )方法。
setAccessible作用是启动和禁用访问安全检查的开关。
参数值为true表示反射的对象在使用时取消访问检查,提高反射的效率。参数值为 false则表示反射的对象执行访问检查。

8.4.4 获取Class对象

已知一个类的全类名,且该类在类路径下,可通过 Class:类的静态方法forname0获取,可能抛出 Classnotfoundexception,实例:`Class cls1 = Class forname("java. lang Cat");`多用于配置文件,读取类全路径,加载类。
若已知具体的类,通过类的 class获取,该方式最为安全可靠,程序性能最高实例: Class cls2=Cat. class; 多用于参数传递,比如通过反射得到对应构造器对象。
已知某个类的实例,调用该实例的 getclass(0方法获取Cass对象,实例:Class cla2=对象。 getclass( )。应用场景:通过创建好的对象,获取 Class对象。
其他方式
`Classloader cl = car.getclass().getclassloader();`
`Class clazz4 = cl. loadclass("类的全类名");`
基本数据(int,char, boolean, float double,byte,long, short)按如下方式得到Class类。
`Class cls = 基本数据类型.class`
基本数据类型对应的包装类,可以通过.type得到 Class类对象。
`Class cls = 包装类.TYPE`

8.4.5 类的加载过程



加载阶段
JVM在该阶段的主要目的是将字节码从不同的数据源(可能是class文件、也可能是jar包,甚至网络)转化为二进制字节流加载到内存中,井生成一个代表该类的java.lang.Class对象。
连接阶段

  1. 验证
    目的是为了确保 Class文件的字节流中包含的信息符合当前虚拟机的要求,井且不会危害虚拟机自身的安全。
    包括:文件格式验证(是否以魔数 oxcafebabe开头)、元数据验证、字节码验证和符号引用验证
    可以考虑使用 -Xverify:none参数来关闭大部分的类验证措施,缩短虚拟机类加载。
  2. 准备
    JVM会在该阶段对静态变量,分配内存并默认初始化(对应数据类型的默认初始值如0、0L、null、 false等)。这些变量所使用的内存都将在方法区中进行分配。
  3. 解析
    符号引用替换为直接引用。
    (1)初始化阶段
    到初始化阶段,オ真正开始执行类中定义的Java程序代码,此阶段是执行< clinit>( )方法的过程( )方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并。
    虚拟机会保证一个类的( )方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类。那么只会有一个线程去执行这个类的( )方法,其他线程都需要阻塞等待,直到活动线程执行( )方法完毕。

九、注解

定义:注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。
作用分类:

  • 编写文档:通过代码里标识的元数据生成文档【生成文档doc文档】
  • 代码分析:通过代码里标识的元数据对代码进行分析【使用反射】
  • 编译检查:通过代码里标识的元数据让编译器能够实现基本的编译检查【Override】
9.1 内置注解

Java 定义了一套注解,共有 7 个,3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中。
作用在代码的注解

java 复制代码
- @Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
- @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
- @SuppressWarnings - 指示编译器去忽略注解中声明的警告。
  - all to suppress all warnings (抑制所有警告)
  - boxing to suppress warnings relative to boxing/unboxing operations(抑制装箱、拆箱操作时候的警告)
  - cast to suppress warnings relative to cast operations (抑制映射相关的警告)
  - dep-ann to suppress warnings relative to deprecated annotation(抑制启用注释的警告)
  - deprecation to suppress warnings relative to deprecation(抑制过期方法警告)
  - fallthrough to suppress warnings relative to missing breaks in switch statements(抑制确在switch中缺失breaks的警告)
  - finally to suppress warnings relative to finally block that don't return (抑制finally模块没有返回的警告)
  - hiding to suppress warnings relative to locals that hide variable()
  - incomplete-switch to suppress warnings relative to missing entries in a switch statement (enum case)(忽略没有完整的switch语句)
  - nls to suppress warnings relative to non-nls string literals(忽略非nls格式的字符)
  - null to suppress warnings relative to null analysis(忽略对null的操作)
  - rawtypes to suppress warnings relative to un-specific types when using generics on class params(使用generics时忽略没有指定相应的类型)
  - restriction to suppress warnings relative to usage of discouraged or forbidden references
  - serial to suppress warnings relative to missing serialVersionUID field for a serializable class(忽略在serializable类中没有声明serialVersionUID变量)
  - static-access to suppress warnings relative to incorrect static access(抑制不正确的静态访问方式警告)
  - synthetic-access to suppress warnings relative to unoptimized access from inner classes(抑制子类没有按最优方法访问内部类的警告)
  - unchecked to suppress warnings relative to unchecked operations(抑制没有进行类型检查操作的警告)
  - unqualified-field-access to suppress warnings relative to field access unqualified (抑制没有权限访问的域的警告)
  - unused to suppress warnings relative to unused code  (抑制没被使用过的代码的警告)

作用在其他注解的注解(或者说 元注解)

java 复制代码
- @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
- @Documented - 标记这些注解是否包含在用户文档中。
- @Target - 标记这个注解应该是哪种 Java 成员。
- @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)

从 Java 7 开始,额外添加了 3 个注解

java 复制代码
- @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
- @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
- @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
相关推荐
奋斗的小花生37 分钟前
c++ 多态性
开发语言·c++
魔道不误砍柴功40 分钟前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_23440 分钟前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨43 分钟前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
老猿讲编程1 小时前
一个例子来说明Ada语言的实时性支持
开发语言·ada
Chrikk2 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man2 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
测开小菜鸟2 小时前
使用python向钉钉群聊发送消息
java·python·钉钉