【JavaSE 第十六天】

【JavaSE 第十六天】

一、List 接口的实现类 ArrayList

1. ArrayList 集合的特点

ArrayList 类实现接口 List,ArrayList 具备了 List 接口的特性: (有序、重复、索引)

  • ArrayList 集合底层的实现原理是数组,大小可变(存储对象的时候长度无需考虑)。
  • 数组的特点:查询速度快,增删慢。
  • 数组的默认长度是10个,每次的扩容是原来长度的1.5倍。
  • ArrayList 是线程不安全的集合,但是运行速度快。(API 文档中显示是:不同步)

2. ArrayList 源码解析

(1)ArrayList 类的成员变量
 private static final int DEFAULT_CAPACITY = 10; // 默认容量
 private static final Object[] EMPTY_ELEMENTDATA = {}; // 空数组(被 final 修饰不可改变)
transient Object[] elementData; // ArrayList 集合中的核心数组(实际在这里面存数据,可以改变长度)
private int size; // 记录数组中存储个数
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // 数组扩容的最大值
(2) ArrayList 集合类的构造方法
// 无参数构造方法
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 数组没有长度
// 有参数的构造方法
public ArrayList(int 10) {
    if (initialCapacity > 0) {
        // 创建了一个长度为10的数组
    	this.elementData = new Object[10];
    } else if (initialCapacity == 0) {
    	this.elementData = EMPTY_ELEMENTDATA;
    } else {
    	throw new IllegalArgumentException("Illegal Capacity: "+
    initialCapacity);
    }
}
(3)ArrayList 集合类的方法 add()
// main 方法中使用
public static void main(String[] args) {
	new ArrayList<>().add("abc"); // 集合中添加元素
}
// 方法 add()
public boolean add("abc") {
    // 只要向数组中存数据,检就要检查容量:(1),安全保障
    ensureCapacityInternal(size + 1);  // 总保障容量加一,保障安全
    // abc 存储到数组中,存储数组0索引,size 计数器++
    elementData[size++] = "abc"; // 数组扩容为10
    return true;
}
add() 中用到的: ensureCapacityInternal() 方法的源码:(嵌套着calculateCapacity()方法)
// 检查集合中数组的容量,参数是1(方便理解),源码是(int minCapacicy)最小容量
private void ensureCapacityInternal(int minCapacity = 1) { // 传入 1
    // calculateCapacity 计算容量,方法的参是数组, 1
    // ensureExplicitCapacity (10) 用来扩容
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

calculateCapacity() 方法的源码:

// 计算容量的方法,返回10
private static int calculateCapacity(Object[] elementData, int minCapacity = 1) {
    // 存储元素的数组 == 默认的空的数组  构造方法中有赋值
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 两个值中返回最大值   max(10,1)
    	return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
add() 中用到的:ensureExplicitCapacity() 方法的源码:(嵌套着 grow() 方法)
//扩容 
private void ensureExplicitCapacity(int minCapacity = 10) { // 传入 10
	 // 私有的方法
    modCount++; // 记录集合被操作了多少次
   // 10 - 数组的长度(就是0) > 0
    if (minCapacity - elementData.length > 0)
        // grow方法(10) 用来数组增长
    	grow(minCapacity);
}

grow() 方法的源码:

 // 增长的方法,参数是(10)
 private void grow(int minCapacity = 10) { // 传入 10
     // 变量 oldCapacity 保存原有数组的长度(为零)
     int oldCapacity = elementData.length; // 原有的数组容量为 0
     // 新的数组容量 = 原有数组的容量 + (原有数组的容量 / 2)
     int newCapacity = oldCapacity + (oldCapacity >> 1); // 新的数组容量为0  位移
     // 0 - 10 < 0 新容量-计算出的容量
     if (newCapacity - minCapacity < 0)
     	newCapacity = minCapacity; // 新的数组容量变为10
     // 判断是否超过最大容量
     if (newCapacity - MAX_ARRAY_SIZE > 0) // 判断是否超过最大容量,如果超过最大容量,就直接赋值为最大容量
     newCapacity = hugeCapacity(minCapacity);
	 // 数组的复制,原始数组和新的容量
     elementData = Arrays.copyOf(elementData, newCapacity);
 }

二、List 接口的实现类 LinkedList

1. LinkedList 集合的特点

LinkedList 类实现接口 List,LinkedList 具备了 List 接口的特性 (有序,重复,索引)

  • LinkedList 底层实现原理是链表,双向链表
  • LinkedList 增删速度快
  • LinkedList 查询速度慢
  • LinkedList 是线程不安全的集合,但是运行速度快

2. LinkedList 集合的特有方法

集合是链表的实现,可以单独操作链表的开头元素和结尾元素

  • void addFirst(E e) 元素插入到链表开头
  • void addLast(E e) 元素插入到链表结尾
  • E getFirst() 获取链表开头的元素
  • E getLast() 获取链表结尾的元素
  • E removeFirst() 移除链表开头的元素
  • E removeLast() 移除链表结尾的元素
  • void push(E e) 把元素推入堆栈中
  • E pop() 把元素从堆栈中弹出

LinkedList 不能使用多态性(会出现一些问题),要使用纯子类。

public static void main(String[] args) {
	linkedAdd();
	linkedGet();
	linkedRemove();
	linkedPushPop();
}

// 1.void addFirst(E e) 元素插入到链表开头
// 2.void addLast(E e) 元素插入到链表结尾
public static void linkedAdd(){
    LinkedList<String> linkedList = new LinkedList<String>();
    linkedList.add("a"); // 结尾添加
    linkedList.add("b"); // 结尾添加
    linkedList.add("c"); // 结尾添加
    linkedList.add("d"); // 结尾添加
    System.out.println("linkedList = " + linkedList);
    // 结尾添加
    linkedList.addLast("f");
    linkedList.add("g"); // 再次添加 "g" 在 "f" 之后
    // 开头添加
    linkedList.addFirst("e");
    System.out.println("linkedList = " + linkedList);
}

// 3.E getFirst() 获取链表开头的元素
// 4.E getLast() 获取链表结尾的元素
public static void linkedGet(){
    LinkedList<String> linkedList = new LinkedList<String>();
    linkedList.add("a"); // 结尾添加
    linkedList.add("b"); // 结尾添加
    linkedList.add("c"); // 结尾添加
    linkedList.add("d"); // 结尾添加
    System.out.println("linkedList = " + linkedList);
    // 获取开头元素
    String first = linkedList.getFirst();
    // 获取结尾元素
    String last = linkedList.getLast();
    System.out.println("first = " + first);
    System.out.println("last = " + last);
    System.out.println("linkedList = " + linkedList);
} 

// 5.E removeFirst() 移除链表开头的元素
// 6.E removeLast() 移除链表结尾的元素
public static void linkedRemove(){
    LinkedList<String> linkedList = new LinkedList<String>();
    linkedList.add("a"); 
    linkedList.add("b"); 
    linkedList.add("c"); 
    linkedList.add("d"); 
    System.out.println("linkedList = " + linkedList);
    // 移除开头元素,返回被移除之前的值
    String first = linkedList.removeFirst();
    // 移除结尾元素,返回被移除之前的值
    String last = linkedList.removeLast();
    System.out.println("first = " + first);
    System.out.println("last = " + last);
    System.out.println("linkedList = " + linkedList);
}

// 7.void push(E e) 元素推入堆栈中
// 8.E pop() 元素从堆栈中弹出
public static void linkedPushPop(){
    LinkedList<String> linkedList = new LinkedList<String>();
    // 元素推入堆栈中 永远向开头的位置添加元素
    linkedList.push("a"); // 本质就是 addFirst() 开头添加
    linkedList.push("b");
    linkedList.push("c");
    System.out.println("linkedList = " + linkedList);
    String pop = linkedList.pop(); // 本质就是 removeFirst() 移除开头
    System.out.println(pop);
    System.out.println("linkedList = " + linkedList);
}

3. LinkedList 源码解析

(1)LinkedList 集合的成员变量
transient int size = 0; // 集合中存储元素个数计数器
transient Node<E> first; // 第一个元素是谁
transient Node<E> last; // 最后一个元素是谁
(2)LinkedList 集合的成员内部类 Node (节点)
// 链表当中,每个节点对象使用内部类来表示的
private static class Node<E> { // 核心部分 
        E item;       // 我们存储的元素
        Node<E> next; // 下一个节点对象,当是最后一个元素时它为 null
        Node<E> prev; // 上一个节点对象,当是第一个元素时它为 null
    // 构造方法,创建对象,传递上一个地址值,下一个地址值,存储的元素
    Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
}

注:双向链表具有 Node<E> next; Node<E> prev; 两个节点,而单向链表只具有 Node<E> next; 一个节点

(3)LinkedList 集合的方法 add() 添加元素
// 注:① 为第一次操作的注释 ② 为第二次操作后的注释 不标注则为通用注释
// ①添加元素 e 处 传递要存储的元素 abc
// ②如果再次添加元素 e
void linkLast(E "abc") { // 源码:void linkLast(E e)
    // 声明新的节点对象为 last      last 是一个成员变量,默认值为 null
    final Node<E> l = last; // ①相当于 final Node<E> l = null       ② l 变为了 "abc"节点
    // 创建新的节点对象,传递了三个参数:最后一个对象,"abc"(存储的元素),上一个对象:null
    final Node<E> newNode = new Node<>(l, e, null);
    // 新节点赋值给最后一个节点
    last = newNode;
    if (l == null) // 判断新存储的节点是否为 null
        // 新存储的节点赋值给第一个节点
    	first = newNode;
    else
    	l.next = newNode;
    size++;
    modCount++;
}
(4)LinkedList 集合的方法 get() 获取元素
// 集合的获取的方法,应用了折半查找
// index 是索引,size 是长度计数器
Node<E> node(int index) {
    // 判断索引是否小于长度的一半,折半思想
    if (index < (size >> 1)) {
    	Node<E> x = first;
    	for (int i = 0; i < index; i++)
        	x = x.next;
   		return x;
    } else {
   		Node<E> x = last;
    	for (int i = size - 1; i > index; i--)
    		x = x.prev;
    	return x;
    }
}

三、Collection 下的 Set 集合

Set 集合,是接口 Set,继承 Collection 接口,Set集合不存储重复元素(Set 集合没有索引,Set 集合中有的有序,有的无序)
Set 接口下的所有实现类,都会具有不存储重复元素的特性
Set 接口的方法,和父接口 Collection 中的方法完全一样

1. Set 集合存储和遍历

public static void main(String[] args) {
    // Set 集合存储并迭代
    Set<String> set = new HashSet<String>();
    // 存储元素方法 add
    set.add("a");
    set.add("b");
    set.add("c");
    set.add("d");
    // set.add("d"); 重复的元素就无法存入
    System.out.println("set = " + set);
    Iterator<String> it = set.iterator();
    while (it.hasNext()){
        System.out.println(it.next());
    }
}

2. Set 接口实现类 HashSet 类

  • HashSet 集合类的特点 :
    • 实现 Set 接口,底层调用的是 HashMap 集合
    • HashSet 的底层实现原理是哈希表
    • HashSet 不保证迭代顺序,元素存储和取出的顺序不一定
    • 线程不安全,但是运行速度快
(1)HashSet 接口的实现类 LinkedHashSet

底层的数据结构是哈希表,继承 HashSet(单向链)
LinkedHashSet 数据是双向链表,有序的集合,存储和取出的顺序一样

测试:

public static void main(String[] args) {
    Set<String> set = new LinkedHashSet<>();
    set.add("b");
    set.add("e");
    set.add("c");
    set.add("a");
    set.add("d");
    System.out.println("set = " + set);
}

3. 对象的哈希值

每个类继承 Object 类,Object 类定义了一个方法,叫做哈希值方法:

// 哈希值方法
public native int hashCode(); // C++语言编写,不开源

方法使用没有区别:方法返回 int 类型的值,这个值就称为哈希值
哈希值的结果不知道是怎么计算的(不开源),调用 toString() 方法的时候,发现返回的十六进制数和哈希值是一样的:“@1b6d3586” 叫哈希值 (根本和内存地址是无关的,以前叫做对象的内存地址是为了方便之前知识的理解,对象的内存地址根本不可能被知道,否则可能会被人为更改,导致内部程序错乱)

public static void main(String[] args) {
    Person p = new Person();
    int code = p.hashCode();
    // 结果是 int 类型的变量 460141958 (是什么,无所谓,该数字就是对象的哈希值)
    System.out.println(code);
    // com.xxxxxxx.xxxx.Person@1b6d3586 返回的是包名和类名和数字:460141958 的十六进制表示
    System.out.println(p.toString());
 }

由于哈希值方法的修饰符是 “public” ,所以可以进行重写,删去 “native”

    /**
     * 重写父类的方法
     * 返回 int 值
     */
    public int hashCode(){
        return 9527; // 可以随便写哈希数值
    }

4. String 类的哈希值

字符串类重写方法 hashCode(),自定义了哈希值,哈希值的计算方法是:
h = 31 × 上一次的计算结果 + 字符数组中元素的 ASCII 码值
乘 31 的目的:减少相同哈希值的计算(但是还是有重复的哈希值)

(1)String 类的哈希值重写方法源码:
    public int hasCode(){
        int h = hash;
        if(h == 0 && value.length > 0){
            char val[] = value;
            for(int i = 0;i < value.length;i++){
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
(2)String 类的哈希值重写方法源码的计算方法:
在此方法下的计算过程:
String s1 = "abc";
value 字符数组 ['a','b','c'] = char val[];
h = 31 * h + val[i]; // 'a' = 97
h = 31 * 0 + 97;
h = 31 * h + val[i]; // 'b' = 98
h = 31 * 97 + 98;
h = 31 * h + val[i]; // 'c' = 99
h = 31 * 3105 + 99;
h = 96354;
最终得到 String 类 对象的哈希值
(3)String 类的哈希值:
    // 字符串String对象的哈希值
    private static void stringHash(){
		String s1 = new String("abc");
		String s2 = new String("abc");
        System.out.println(s1 == s2); // 一旦经过 new 创建对象,确实是比较的对象的内存地址,返回值是 false
        String s1 = "abc";
        String s2 = "abc";
        System.out.println(s1 == s2); // "abc" 的地址值就会由 s1 传递给 s2 这样的地址值就是相同的,返回值就是 true
        // String 类继承 Object,可以使用方法 hashCode()
        System.out.println(s1.hashCode() == s2.hashCode()); // 哈希值与(new)创建对象无关,两个哈希值都是 int 类型且数值相同,所以证明两个哈希值相同返回值是 true
        /**
         * 结论:
         * String 类继承 Object 类
         * String 类重写父类的方法 hashCode() 自己定义了哈希值,没有使用父类的方法
         */
        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());
     
		// 就由此引出了 String 类的哈希值重写方法源码

        /**
         *  字符串内容不一样,有没有可能计算出相同的哈希值
         */
        String s3 = "通话";
        String s4 = "重地";
        System.out.println(s3.hashCode());
        System.out.println(s4.hashCode());
        // 1179395
        // 1179395
        // 由此字符串内容不一样,可能计算出相同的哈希值
        System.out.println(s3.equals(s4)); // 肯定是 false
    }

5. 哈希值的相关问题:

问题:
(1)两个对象A、B 两个对象哈希值相同,equals 方法一定返回 true 吗?
(2)两个对象A、B 两个对象 equals 方法返回 true,两个对象的哈希值一定相同吗?

Sun 公司官方规定的结论 : (1)两个对象的哈希值相同,不要求 equals 一定返回 true。(2)两个对象的 equals 返回 true,两个对象的哈希值必须一致。并且重写方法必须遵循官方协定,不可违背。

6. 哈希表的数据结构

简单来说:哈希表是数组和链表的组合体

// 伪代码:
class Node{
    E element; // 存储的元素
    Node next; // 下一个元素
}
public static void main(){
    Node[] node = new Node[5];
}
  • 哈希表的底层数组长度默认是16个,扩容为原来长度的2倍
  • 加载因子默认是0.75F,数组中存储元素的个数达到长度的75%,数组就要进行扩容

具体解释:
①哈希表的实例化:数组
②该数组越长,遍历性能越差
③遍历与底层桶的容量成正比
④初始容量就是数组的长度
⑥加载因子就是阈值
⑦数组的扩容指标,默认为0.75F
⑧数组存储元素的个数到达了长度的75%就会进行数组的扩容
(此类为基本操作提供了稳定的性能,这些基本操作包括 add,remove,contains,size,假定哈希函数将这些元素正确的分布在桶中。对此 set 进行迭代所需要的时间与 HashSet 实例的大小(元素的数量)和底层的 HashMap 实例(桶的数量)的“容量”的和成正比例。因此,如果迭代性能很重要,则不要将初始容量设置得太高,或将加载因子设置得太低。)

7. 哈希表存储对象的过程

public static void main(String[] args) {
    Set<String> set = new HashSet<String>();
    // 存储对象
    set.add("abc");
    set.add("bbc");
    set.add(new String("abc")); // 故意写出两个 "abc"
    set.add("通话");
    set.add("重地");
    System.out.println("set = " + set);
}

哈希表是单向链
哈希表存储对象的过程

8. 哈希表存储自定义的对象

(1)创建 Student 类:
public class Student {
    private int age;
    private String name;

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

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

    @Override
    // 重写的 equals() 方法 ,idea自动生成
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;

        if (age != student.age) return false;
        return name != null ? name.equals(student.name) : student.name == null;
    }

    @Override
    // 重写的 hashCode() 的方法,idea自动生成
    public int hashCode() {
        int result = age;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        return result;
    }

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

(2)集合 HashSet,哈希表存储自定义的对象:
/**
 * 集合 HashSet,哈希表存储自定义的对象:
 * 特殊要求:对于 Student 对象,只要学生的姓名和年龄相同,就认为是同一个对象,只存储一个
 * 需要: Student 类自己重写 hasCode() 和 equals() 方法   使用快捷键就会自动重写
 */
public static void main(String[] args) {
    Set<Student> set = new HashSet<Student>();
    // 存储 Student 的对象
    set.add(new Student("a1",201));
    set.add(new Student("a2",202));
    set.add(new Student("a2",202));
    set.add(new Student("a3",203));
    set.add(new Student("a4",204));
    System.out.println("set = " + set);
}

9. 哈希表源码

HashSet (单向链)集合本身不具备任何功能,他的内部调用了另一个集合对象 HashMap

  • 无参数构造方法
  public HashSet() {
  	map = new HashMap<>();
  }
  • HashMap 类的成员变量
  // 哈希表数组的初始化容量:16
  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
  static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量
  static final float DEFAULT_LOAD_FACTOR = 0.75f; // 加载因子
  static final int TREEIFY_THRESHOLD = 8; // 阈值,转红黑树
  static final int UNTREEIFY_THRESHOLD = 6; //阈值,解除红黑树
  static final int MIN_TREEIFY_CAPACITY = 64; //阈值,转红黑树
  • HashMap 内部类 Node
  //节点
  static class Node<K,V> implements Map.Entry<K,V> {
          final int hash; // 对象哈希值
          final K key; // 存储的对象
          V value; // 使用 Set 的集合,value 没有值(空的数组)
          Node<K,V> next; // 链表的下一个节点
  }
  • Set 集合存储方法 add() ,调用的是 HashMap 集合的方法 put()
// HashMap 存储对象的方法 put,Key 是存储的元素,V 是空的对象(单列集合)
public V put(K key, V value) {
    // 存储值,并调用另一个方法传递新计算的哈希值并传递要存储的元素
	return putVal(hash(key), key, value, false, true);
}
 // put() 中调用的方法 putVal() 中的 hash() 方法
 // 传递存储的对象,再次计算哈希值
 // 目的:尽量降低哈希值的碰撞
 static final int hash(Object key) { 
   int h;
   return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 }
// put() 中调用的方法 putVal() 的源码
// 存储值,重写计算的哈希值,要存储值
final V putVal(int hash, K key, V value, boolean false, boolean true) {
	// 声明了 一个 Node 类型数组,又一个 Node类型数组,两个变量 n 、i
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 进行赋值 tab = Node[] = null ,结果为真,之后的不再运行
    if ((tab = table) == null || (n = tab.length) == 0){
        // n = (tab = resize()).length 解释:n 赋值为 tab 数组并让 tab 等于 resize() 方法返回数组,(用来检查容量)结果返回的是默认长度的数组 16 长度
        n = (tab = resize()).length; // 16
        // 数组的长度 - 1 & 存储对象的哈希值,用来确定存储的位置
        // 判断数组的索引上是不是空的
        if ((p = tab[i = (n - 1) & hash]) == null) // 如果得到 nul == null if 成立
             // i 就是数组索引 给它赋值新的节点对象,传递计算的哈希值和要存储的对象
             tab[i] = newNode(hash, key, value, null);
        else{
            // 如果走 else 则证明数组的索引不是空,要存储的对象,已经有了
            // if 用来判断已经存在的对象,和要存储对象的哈希值和 equals() 方法
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                // 遍历该索引下的链表,和每个元素比较 hashCode 和 equals
                e = p;
                // 之后的代码,先不解释
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        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) {
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
        }
    }
}
 // resize() 方法中的一部分源码 数组扩容翻一倍
 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; 

10. 哈希表面试问题

JDK7 版本和 JDK8 版本的哈希表的区别:

  1. JDK7 中没有转红黑树,而 JDK8 新加入了转成红黑树
  2. JDK7 元素采用头插法,JDK8 元素采用尾插法
    (哈希表存储对象的过程中,在桶中 JDK7 是往下挤已存储的元素,而 JDK8 则是继续在原来的元素之后存储)
  • JDK8 转成红黑树:
    • 转成树的两个参数需要的条件:
      • 当一个数组中存储的 (链表长度 >= 8) 时转树
      • 并且数组的整体长度超过 64
    • 树转回链表:
      • (链表长度 <= 6) 时转回链表

四、红黑树

红黑树( Red-Black-Tree )

红黑树的发展:从二叉树到自然平衡二叉树到红黑树

  • 二叉树,本质就是链表
    • 查询速度快
    • 每个一个节点,只有两个子节点,左和右(特点:小的往左跑,大的往右跑)
    • 但是树容易长偏

之后出现了:

  • 自然平衡二叉树
    • 二叉树的基础上,改进,保证树是平衡的

之后又出现了:

  • 红黑树
    • 每个节点有颜色,要么红,要么是黑
    • 根节点必须是黑色
    • 叶子节点必须是黑色
    • 内存中没有颜色所以用变量表示颜色,true 表示黑色,false 表示红色

红黑树运行状况具象表示:红黑树结构

1. TreeSet 集合使用

TreeSet 集合,底层是红黑树结构,依赖于 TreeMap 的实现

红黑树特点:

  1. 查找速度快,线程不安全
  2. 可以对存储到红黑树的元素进行自动排序,按照元素的自然顺序 abcd… 字典顺序
   public static void treeSetString(){
       Set<String> set = new TreeSet<>();
       // 存储元素
       set.add("abcd");
       set.add("ccdd");
       set.add("z");
       set.add("wasd");
       set.add("bbaa");
       System.out.println("set = " + set);
   }

2. TreeSet 存储自定义对象

/**
 * TreeSet 集合存储 Student 对象
 */
public static void treeSetStudent(){
    Set<Student> set = new TreeSet<Student>();
    set.add(new Student("a",10));
    set.add(new Student("b",20));
    System.out.println("set = " + set);
}

如此操作程序就会出现异常(运行异常),出现类型的转换异常 ClassCastException

  1. 异常原因:Student 类不能进行类型的转换,因为有一个接口没有实现 java.lang.Comparable(这个接口叫做对象的自然顺序)
  2. 解决办法:只有这个类实现接口 Comparable,这个类就具有了自然顺序

解决办法:
第一种方法:

  • 使 Student 类具有自然顺序
    • 实现接口 Comparable,重写方法 compareTo()
// 利用之前的 Student 类
public class Student implements Comparable<Student> {
    /**
     * 重写方法compareTo
     * 返回 int 类型
     * 参数:要参与比较的对象
     * this 对象和 student 对象
     *
     * 红黑树,后来的对象是 this,原有的对象是参数
     */
    public int compareTo(Student student){
        return this.age - student.age;
    }

    private int age;
    private String name;

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

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

    @Override
    // 重写的 equals() 方法 ,idea自动生成
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;

        if (age != student.age) return false;
        return name != null ? name.equals(student.name) : student.name == null;
    }

    @Override
    // 重写的 hashCode() 的方法,idea自动生成
    public int hashCode() {
        int result = age;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        return result;
    }

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

存储并测试:

    public static void treeSetStudent() {
        Set<Student> set = new TreeSet<Student>();
        set.add(new Student("a",10));
        set.add(new Student("b",11));
        set.add(new Student("c",12));
        System.out.println("set = " + set);
    }

    public static void main(String[] args) {
        treeSetStudent();
    }

第二种方法:(两种方法并存的时候,比较器优先)

  • 自定义比较器
    • java.util.Comparator 接口
import java.util.Comparator;
/**
 * 自定义的比较器
 * 实现接口,重写方法
 */
public class MyCom implements Comparator<Student> {
    @Override
    /**
     * TreeSet集合自己调用方法
     * 传递参数
     * Student o1, Student o2
     * o1 是后来的对象
     * o2 是已经有的对象
     */
    public int compare(Student o1, Student o2) {
        return o1.getAge() - o2.getAge();
    }
}
    public static void treeSetStudent() {
        /**
         * TreeSet 集合存储 Student 对象
         * 自定义的比较器,比较器对象传递到集合的构造方法
         */
        Set<Student> set = new TreeSet<Student>(new MyCom());
        set.add(new Student("a",10));
        set.add(new Student("b",11));
        set.add(new Student("c",12));
        System.out.println("set = " + set);
    }

五、Collections 工具类

  • java.util.Collection 集合的顶级接口
  • java.util.Collections 操作集合的工具类
    • 工具类的方法全部静态方法,类名直接调用
    • 主要是操作 Collection 系列的单列集合,少部分功能可以操作 Map 集合(双列集合)
/**
 *  集合操作的工具类:Collections
 *  工具类有组方法:synchronized 开头
 *  	操作过程:类名调用传递集合,返回集合
 *  	功能作用:传递的集合,返回后,变成了线程安全的集合
 */
public class CollectionsTest {
    public static void main(String[] args) {
    	binarySearch();
    	shuffle();
    	sort01();
        sort02();
    }
    
    // 1.集合的二分查找
    public static void binarySearch(){
        List<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(5);
        list.add(9);
        list.add(15);
        list.add(20);
        list.add(25);
        int index = Collections.binarySearch(list, 15);
        System.out.println(index);
    }
    
    // 2.集合元素的随机交换位置
    public static void shuffle(){
        List<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(15);
        list.add(5);
        list.add(20);
        list.add(9);
        list.add(25);
        System.out.println("list = " + list);
        Collections.shuffle(list);
        System.out.println("list = " + list);
    }
    
    // 3.集合元素的排序
    public static void sort01(){
        List<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(15);
        list.add(5);
        list.add(20);
        list.add(9);
        list.add(25);
        System.out.println("list = " + list);
        Collections.sort(list);
        System.out.println("list = " + list);
    }
    
    // 4.集合元素的排序,逆序
    public static void sort02(){
        List<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(15);
        list.add(5);
        list.add(20);
        list.add(9);
        list.add(25);
        System.out.println("list = " + list);
        // Collections.reverseOrder() 用来逆转自然顺序
        Collections.sort(list,Collections.reverseOrder());
        System.out.println("list = " + list);
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值