【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 版本的哈希表的区别:
- JDK7 中没有转红黑树,而 JDK8 新加入了转成红黑树
- JDK7 元素采用头插法,JDK8 元素采用尾插法
(哈希表存储对象的过程中,在桶中 JDK7 是往下挤已存储的元素,而 JDK8 则是继续在原来的元素之后存储)
- JDK8 转成红黑树:
- 转成树的两个参数需要的条件:
- 当一个数组中存储的 (链表长度 >= 8) 时转树
- 并且数组的整体长度超过 64
- 树转回链表:
- (链表长度 <= 6) 时转回链表
- 转成树的两个参数需要的条件:
四、红黑树
红黑树( Red-Black-Tree )
红黑树的发展:从二叉树到自然平衡二叉树到红黑树
- 二叉树,本质就是链表
- 查询速度快
- 每个一个节点,只有两个子节点,左和右(特点:小的往左跑,大的往右跑)
- 但是树容易长偏
之后出现了:
- 自然平衡二叉树
- 二叉树的基础上,改进,保证树是平衡的
之后又出现了:
- 红黑树
- 每个节点有颜色,要么红,要么是黑
- 根节点必须是黑色
- 叶子节点必须是黑色
- 内存中没有颜色所以用变量表示颜色,true 表示黑色,false 表示红色
红黑树运行状况具象表示:红黑树结构
1. TreeSet 集合使用
TreeSet 集合,底层是红黑树结构,依赖于 TreeMap 的实现
红黑树特点:
- 查找速度快,线程不安全
- 可以对存储到红黑树的元素进行自动排序,按照元素的自然顺序 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
- 异常原因:Student 类不能进行类型的转换,因为有一个接口没有实现 java.lang.Comparable(这个接口叫做对象的自然顺序)
- 解决办法:只有这个类实现接口 Comparable,这个类就具有了自然顺序
解决办法:
第一种方法:
- 使 Student 类具有自然顺序
- 实现接口 Comparable,重写方法
compareTo()
- 实现接口 Comparable,重写方法
// 利用之前的 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);
}
}