本文我们重点介绍 集合体系结构、Collection、List、ArrayList、LinkedList、Set、TreeSet、HashSet、LinkedHashSet。Map、HashMap相关部分可查看 Java集合【超详细】2 – Map、可变参数、Collections类。
所有的集合类和集合接口都在java.util包下;
在内存中申请一块空间用来存储数据,在Java中集合就是替换掉定长数组的一种引用数据类型。
相同点
都是容器,可以存储多个数据
不同点


常见的数据结构:栈、队列、数组、链表、二叉树、二叉查找树、平衡二叉树、红黑树
每种数据结构长什么样子?如何添加数据?如何删除数据?

二叉树的特点
二叉树结构图

二叉查找树的特点
二叉查找树结构图

二叉查找树和二叉树对比结构图

二叉查找树添加节点规则

平衡二叉树的特点
平衡二叉树旋转
旋转触发时机
左旋


右旋
就是将根节点的左侧往右拉,左子节点变成了新的父节点,并把多余的右子节点出让,给已经降级根节点当左子节点


平衡二叉树和二叉查找树对比结构图

平衡二叉树旋转的四种情况
左左
左左: 当根节点左子树的左子树有节点插入,导致二叉树不平衡
如何旋转: 直接对整体进行右旋即可

左右
左右: 当根节点左子树的右子树有节点插入,导致二叉树不平衡
如何旋转: 先在左子树对应的节点位置进行左旋,在对整体进行右旋

右右
右右: 当根节点右子树的右子树有节点插入,导致二叉树不平衡
如何旋转: 直接对整体进行左旋即可

右左
右左:当根节点右子树的左子树有节点插入,导致二叉树不平衡
如何旋转: 先在右子树对应的节点位置进行右旋,在对整体进行左旋

红黑树(Red Black Tree):也是一种自平衡的二叉搜索树(BST),之前叫做平衡二叉B树(Symmetric Binary B-Tree)。
红黑树的时间复杂度:查找、添加、删除都是O(logn)
红黑树的特点
红黑树的红黑规则有哪些(在添加或删除节点的时候,如果不符合这些性质会发生旋转,以达到所有的性质)
每一个节点或是红色的,或者是黑色的
根节点必须是黑色
如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点,每个叶节点(Nil)是黑色的空节点
如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连的情况)
对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点

红黑树添加节点的默认颜色

红黑树添加节点后如何保持红黑规则
泛型的介绍
泛型是JDK5中引入的特性,它提供了编译时类型安全检测机制
泛型的好处
泛型的定义格式
泛型的细节
伪泛型:存入集合内部是Object,基本数据类型无法转Object




红色的是接口,蓝色的是实现类;List的有序 指的是存和取的顺序是一样的

Collection集合概述
创建Collection集合的对象
Collection集合常用方法。(由于是单例集合的顶层接口)它的功能是全部单列集合都可以继承使用的。
| 方法名 | 说明 |
|---|---|
| boolean add(E e) | 添加元素。细节:如果往List系列集合中添加数据,方法永远返回true;如果往Set系列集合中添加数据,若当前添加元素不存在 方法返回true 表示添加成功,若当前要添加的元素已存在 方法返回false 表示添加失败 |
| boolean remove(Object o) | 从集合中移除指定的元素 |
| boolean removeIf(Object o) | 根据条件进行移除 |
| void clear() | 清空集合中的元素 |
| boolean contains(Object o) | 判断集合中是否存在指定的元素。底层是依赖equals判断是否存在,所以如果集合中存储的是自定义对象 若想通过contains方法来判断是否包含,在javabean中一定要重写equals方法 |
| boolean isEmpty() | 判断集合是否为空 |
| int size() | 集合的长度,也就是集合中元素的个数 |
contails方法在底层依赖equals方法判断两对象是否一致。【自定义Javabean时,重写equals方法 —— 如果存的是自定义对象,没有重写equals方法,那么默认调用Object类中的equals方法进行判断,而Object类中equals,依赖地址值进行判断】
String底层也是依赖equal判断是否相等,只不过在字符串里面 java已重写了equals方法
共有三种方法遍历Collection集合:迭代器遍历、增强for、lambda表达式。
迭代器介绍
Collection集合获取迭代器
| 方法名称 | 说明 |
|---|---|
| Iterator iterator() | 返回该集合的迭代器对象(可视为指针),默认指向当前集合的0索引 |
| 方法名称 | 说明 |
|---|---|
| boolean hasNext() | 判断当前位置是否有元素,有元素返回true,没有元素返回false |
| E next() | 获取当前位置的元素,将迭代器对象移向下一个索引位置(获取元素,移动指针) |
Collection集合的遍历
public class IteratorDemo1 {
public static void main(String[] args) {
//创建集合对象
Collection<String> c = new ArrayList<>();
//添加元素
c.add("money");
c.add("study");
c.add("honor");
c.add("happy");
//Iterator iterator():返回此集合中元素的迭代器,通过集合的iterator()方法得到
Iterator<String> it = c.iterator();
//用while循环改进元素的判断和获取
while (it.hasNext()) {
String s = it.next();
System.out.println(s);
}
}
}
迭代器中删除的方法
void remove(): 删除迭代器对象当前指向的元素
public class IteratorDemo2 {
public static void main(String[] args) {
Collection<String> c = new ArrayList<>();
c.add("money");
c.add("study");
c.add("honor");
c.add("happy");
Iterator<String> it = c.iterator();
while (it.hasNext()) {
String s = it.next();
if ("study".equals(s)) {
//报错java.util.ConcurrentModificationException
// c.remove(s);
//使用迭代器删除。指向谁,那么此时就删除谁.
it.remove();
}
}
System.out.println(c);
}
}
迭代器总结
1.迭代器在遍历集合的时候是不需要依赖索引的
2.迭代器需要掌握三个方法:
Iterator<String> it = list.iterator();
while(it.hasNext()){
String str = it.next();
System.out.println(str);
}
3.迭代器的四个细节:
如果当前位置没有元素,还要强行获取,会报NoSuchElementException
迭代器遍历完毕,指针不会复位
循环中只能用一次next方法
迭代器遍历时,不能用集合的方法进行增加或者删除
public class IteratorDemo3 {
public static void main(String[] args) {
Collection<String> c = new ArrayList<>();
c.add("money");
c.add("study");
c.add("honor");
c.add("happy");
//获取迭代器对象。迭代器就好比是一个箭头,默认指向集合的0索引处
Iterator<String> it = c.iterator();
while (it.hasNext()) {
String s = it.next();
System.out.println(s);
}
//当上面循环结束之后,迭代器的指针已经指向了最后没有元素的位置
//System.out.println(it.next());//报错NoSuchElementException
//迭代器遍历完毕,指针不会复位
System.out.println(it.hasNext()); //false
//如果我们要继续第二次遍历集合,只能再次获取一个新的迭代器对象
Iterator<String> it2 = c.iterator();
while (it2.hasNext()) {
String str = it2.next();
System.out.println(str);
}
}
}
idea中快捷键 集合名字.for
for (元素的数据类型 变量名 : 数组或者集合) {
//已经将当前遍历到的元素封装到变量中了,直接使用变量即可
}
for (String s : list) {
System.out.println(s);
}
public class IteratorFor {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("money");
list.add("study");
list.add("honor");
list.add("happy");
//1,数据类型一定是集合或者数组中元素的类型
//2,str仅仅是一个变量名而已,在循环的过程中,依次表示集合或者数组中的每一个元素
//3,list就是要遍历的集合或者数组
for (String str : list) {
System.out.println(str);
}
}
}
如果当前位置没有元素,还要强行获取,会报NoSuchElementException
迭代器遍历完毕,指针不会复位
循环中只能用一次next方法
迭代器遍历时,不能用集合的方法进行增加或者删除。如果我实在要删除:那么可以用迭代器提供的remove方法进行删除;如果我要添加,暂时没有办法(只是暂时)

得益于JDK8开始的新技术Lambda表达式,提供了一种更简单、更直接的遍历集合的方式。利用forEach方法,再结合lambda表达式的方式进行遍历
| 方法名称 | 说明 |
|---|---|
| default void forEach(Consumer super T> action) | 结合lambda遍历集合 |
public class IteratorLambda {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("money");
list.add("study");
list.add("honor");
list.add("happy");
//法一:利用匿名内部类的形式
//底层原理:其实也会自己遍历集合,依次得到每一个元素。把得到的每一个元素,传递给下面的accept方法,s依次表示集合中的每一个数据
list.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
System.out.println("~~~~~~~~");
//法二:lambda表达式
list.forEach( s -> System.out.println(s));
}
}

List集合添加的元素:有序、可重复、有索引
Set集合添加的元素:无序、不重复、无索引
List集合的概述
List集合的特点
Collection与List区别
| 方法名 | 描述 |
|---|---|
| void add(int index,E element) | 在此集合中的指定位置插入指定的元素。原来索引上的元素会依次往后移 |
| E remove(int index) | 删除指定索引处的元素,返回被删除的元素。细节——在调用方法的时候,如果方法出现了重载现象 优先调用,实参跟形参类型一致的那个方法 |
| E set(int index,E element) | 修改指定索引处的元素,返回被修改的元素 |
| E get(int index) | 返回指定索引处的元素 |
public class ListFunction {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("money");
list.add("study");
list.add("honor");
list.add("happy");
method1(list);
method2(list);
method3(list);
}
// add, set
private static void method1(List<String> list) {
//void add(int index,E element) 在此集合中的指定位置插入指定的元素
//原来位置上的元素往后挪一个索引
list.add(1, "health");
System.out.println("使用add方法后: " + list);
//E set(int index,E element) 修改指定索引处的元素,返回被修改的元素
String setStr = list.set(0, "wealth");
System.out.println("被修改的元素: " + setStr);
System.out.println("使用set方法后: " + list);
}
// remove
private static void method2(List<String> list) {
//在List集合中有两个删除的方法
//第一个 删除指定的元素,返回值表示当前元素是否删除成功(继承自Collection的remove方法)
//第二个 删除指定索引的元素,返回值表示实际删除的元素
String s = list.remove(1);
System.out.println("实际删除的元素: " + s);
System.out.println("使用remove方法后: " + list);
}
// get
private static void method3(List<String> list) {
String s = list.get(0);
System.out.println("get(0)获取的元素: " + s);
}
}
代码示例
public class ListTraverse {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("money");
list.add("study");
list.add("honor");
list.add("happy");
//1.迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String str = it.next();
System.out.println(str);
}
System.out.println("~~~~~~~~~");
//2.增强for
//下面的变量s,其实就是一个第三方的变量而已。在循环的过程中,依次表示集合中的每一个元素
for (String s : list) {
System.out.println(s);
}
System.out.println("~~~~~~~~~");
//3.lambda表达式
//forEach方法的底层其实就是一个循环遍历,依次得到集合中的每一个元素,并把每一个元素传递给下面的accept方法。accept方法的形参s,依次表示集合中的每一个元素
list.forEach( s -> System.out.println(s));
System.out.println("~~~~~~~~~");
//4.普通for循环(因为List集合存在索引)
//size方法跟get方法还有循环结合的方式,利用索引获取到集合中的每一个元素
for (int i = 0; i < list.size(); i++) {
String s = list.get(i); //i:依次表示集合中的每一个索引
System.out.println(s);
}
System.out.println("~~~~~~~~~");
//5.列表迭代器
//获取一个列表迭代器的对象,里面的指针默认也是指向0索引的。
//额外添加了一个方法:在遍历的过程中,可以添加元素。不能用集合的add方法添加、删除
ListIterator<String> it1 = list.listIterator();
while (it1.hasNext()) {
String str1 = it1.next();
if ("study".equals(str1)) {
it1.add("learn");
}
}
System.out.println(list);
}
}
List集合的索引从0开始
List系列集合中的两个删除的方法:直接删除元素、通过索引进行删除
public class ListDelete {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
//此时删除的是1这个元素,还是1索引上的元素?为什么?
//删除1索引上的元素。因为在调用方法的时候,如果方法出现了重载现象,优先调用 实参跟形参类型一致的那个方法
//此时remove方法是不会自动装箱的(结论,本质如上)
// list.remove(1);
// System.out.println(list); //[1, 3]
//如果就是想删除1 可以通过索引。
// list.remove(0);
// System.out.println(list); //[2, 3]
//手动装箱,手动把基本数据类型的1,变成Integer类型
Integer i = Integer.valueOf(1);
list.remove(i);
System.out.println(list); //[2, 3]
}
}
参考JDK API中文文档
ArrayList集合
底层是数组结构实现,查询快、增删慢
LinkedList集合
底层是链表结构实现,查询慢、增删快

private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}
//真正存放元素的数组
transient Object[] elementData; // non-private to simplify nested class access
private int size;
一定要记住ArrayList中的transient Object[] elementData,该elementData是真正存放元素的容器,可见ArrayList是基于数组实现的。
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
Object[] elementData 是ArrayList真正存放数据的数组。
ArrayList支持默认大小构造,和空构造,当空构造的时候存放数据的Object[] elementData是一个空数组{}。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//采用右移运算,就是原来的一般,所以是扩容1.5倍。比如10的二进制是1010,右移后变成101就是5
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
注意ArrayList中有一个modCount的属性,表示该实例修改的次数。(所有集合中都有modCount这样一个记录修改次数的属性),每次增改添加都会增加一次该ArrayList修改次数,而上边的add(E e)方法是将新元素添加到list尾部。
数组copy
Java 是无法自己分配空间的,是底层C和C++的实现。以 C 为例,我们知道 C 中数组是一个指向首部的指针,比如我们 C 语言对数组进行分配内存。Java 就是通过 arraycopy 这个 native 方法实现的数组的复制。
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
p = (int *)malloc(len*sizeof(int));
这样的好处何在呢?Java里内存是由jvm管理的,而数组是分配的连续内存,而arrayList不一定是连续内存,当然jvm会帮我们做这样的事,jvm会有内部的优化,会在后续的例子中结合问题来说明。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
/**
* Reconstitute the ArrayList instance from a stream (that is,
* deserialize it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
在序列化方法writeObject()方法中可以看到,先用默认写方法,然后将size写出,最后遍历写出elementData,因为该变量是transient修饰的,所有进行手动写出,这样它也会被序列化了。那是不是多此一举呢?
protected transient int modCount = 0;
当然不是,其中有一个关键的modCount, 该变量是记录list修改的次数的,当写入完之后如果发现修改次数和开始序列化前不一致就会抛出异常,序列化失败。这样就保证了序列化过程中是未经修改的数据,保证了序列化安全。(java集合中都是这样实现)

可见LinkedList既是List接口的实现也是Queue的实现(Deque),故其和ArrayList相比LinkedList支持的功能更多,其可视作队列来使用,当然下文中不强调其队列的实现。
transient int size = 0;
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
//Node源码
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;
}
}
LinkedList由一个头节点和一个尾节点组成,分别指向链表的头部和尾部。
LinkedList中Node由当前值item,和指向上一个节点prev和指向下个节点next的指针组成。并且只含有一个构造方法,按照(prev, item, next)这样的参数顺序构造。
数据结构中链表的头插法linkFirst和尾插法linkLast
/**
* Links e as first element.
*/
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
/**
* Links e as last element.
*/
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++;
}
按照下标获取某一个节点:get方法,获取第index个节点。
public E get(int index) {
checkElementIndex(index);
return node(index).item; //node(index)方法是怎么实现的呢?判断index是更靠近头部还是尾部,靠近哪段从哪段遍历获取值。
}
Node<E> node(int index) {
// assert isElementIndex(index);
//判断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;
}
}
查询索引修改方法,先找到对应节点,将新的值替换掉老的值。
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
这个也是为什么ArrayList随机访问比LinkedList快的原因**,LinkedList要遍历找到该位置才能进行修改,而ArrayList是内部数组操作会更快。
public boolean add(E e) {
linkLast(e);
return true;
}
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
| 方法名 | 说明 |
|---|---|
| public void addFirst(E e) | 在该列表开头插入指定的元素 |
| public void addLast(E e) | 将指定的元素追加到此列表的末尾 |
| public E getFirst() | 返回此列表中的第一个元素 |
| public E getLast() | 返回此列表中的最后一个元素 |
| public E removeFirst() | 从此列表中删除并返回第一个元素 |
| public E removeLast() | 从此列表中删除并返回最后一个元素 |
public class MyLinkedListDemo1 {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.add("money");
list.add("study");
list.add("honor");
list.add("happy");
method1(list);
}
private static void method1(LinkedList<String> list) {
list.addFirst("excellence");
System.out.println("使用addFirst方法后: " + list);
list.addLast("health");
System.out.println("使用addLast方法后: " + list);
System.out.println("getFirst、getLast: " + list.getFirst() + ", " + list.getLast());
String first = list.removeFirst();
String last = list.removeLast();
System.out.println("removeFirst、removeLast: " + first + ", " + last);
System.out.println(list);
}
}
核心步骤:
创建ArrayList对象的时候,他在底层先创建了一个长度为0的数组。
数组名字:elementDate,定义变量size。
size这个变量有两层含义:
①:元素的个数,也就是集合的长度
②:下一个元素的存入位置
添加元素,添加完毕后,size++
扩容时机一:
扩容时机二:
一次性添加多个数据,扩容1.5倍不够,怎么办呀?
如果一次添加多个元素,1.5倍放不下,那么新创建数组的长度以实际为准。
举个例子:
在一开始,如果默认的长度为10的数组已经装满了,在装满的情况下,我一次性要添加100个数据很显然,10扩容1.5倍,变成15,还是不够,
怎么办?
此时新数组的长度,就以实际情况为准,就是110
具体分析过程可以参见视频讲解。
添加一个元素时的扩容:

添加多个元素时的扩容:

底层是双向链表结构
核心步骤如下:
具体分析过程可以参见视频讲解。

迭代器遍历相关的三个方法:
Iterator iterator() :获取一个迭代器对象
boolean hasNext() :判断当前指向的位置是否有元素
E next() :获取当前指向的元素并移动指针

java中的Set接口和Colletion是完全一样的定义。
public interface Set<E> extends Collection<E> {
// Query Operations
int size();
boolean isEmpty();
boolean contains(Object o);
Object[] toArray();
<T> T[] toArray(T[] a);
// Modification Operations
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean retainAll(Collection<?> c);
boolean removeAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();
//此处和Collection接口有区别
Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT);
}
}
存储字符串并遍历
public class MySet1 {
public static void main(String[] args) {
Set<String> set = new TreeSet<>();
set.add("money");
set.add("study");
set.add("honor");
set.add("happy");
set.add("study");
// for (int i = 0; i < set.size(); i++) {
// //Set集合是没有索引的,所以不能使用通过索引获取元素的方法
// }
//遍历集合
Iterator<String> it = set.iterator();
while (it.hasNext()) {
String s = it.next();
System.out.println(s);
}
System.out.println("~~~~~~~~");
for (String s : set) {
System.out.println(s);
}
System.out.println("~~~~~~~~");
set.forEach(s -> System.out.println(s));
}
}
存储Integer类型的整数并遍历
public class TreeSetDemo01 {
public static void main(String[] args) {
TreeSet<Integer> ts = new TreeSet<>();
ts.add(20);
ts.add(10);
ts.add(40);
ts.add(50);
ts.add(30);
for (Integer t : ts) {
System.out.println(t);
}
System.out.println(ts); //[10, 20, 30, 40, 50]
}
}
注意输出顺序 10 20 30 40 50
案例需求
实现步骤
代码实现
学生类
//不加implements Comparable【加implements Comparable 需重写compareTo(Student o)方法】 报错ClassCastException: com.ywj.collection.MySet.Student cannot be cast to java.lang.Comparable
public class Student implements Comparable<Student>{
private String name;
private int age;
public Student() {
}
public Student(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 "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(Student o) {
//按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序
//主要判断条件: 按照年龄从小到大排序
int result = this.age - o.age;
//次要判断条件: 年龄相同时,按照姓名的字母顺序排序
result = result==0 ? this.name.compareTo(o.getName()) : result;
return result;
}
}
测试
public class MyTreeSet2 {
public static void main(String[] args) {
TreeSet<Student> ts = new TreeSet<>();
Student s1 = new Student("zhangsan",28);
Student s2 = new Student("lisi",27);
Student s3 = new Student("wangwu",29);
Student s4 = new Student("zhaoliu",28);
Student s5 = new Student("qianqi",30);
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
ts.add(s5);
for (Student student : ts) {
System.out.println(student);
}
}
}
输出
Student{name=‘lisi’, age=27}
Student{name=‘zhangsan’, age=28}
Student{name=‘zhaoliu’, age=28}
Student{name=‘wangwu’, age=29}
Student{name=‘qianqi’, age=30}
案例需求
实现步骤
代码实现
老师类
public class Teacher {
private String name;
private int age;
public Teacher() {
}
public Teacher(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 "Teacher{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
测试类
public class TeacherTreeSet {
public static void main(String[] args) {
//创建集合对象
TreeSet<Teacher> ts = new TreeSet<>(new Comparator<Teacher>() {
@Override
public int compare(Teacher o1, Teacher o2) {
//o1表示现在要存入的那个元素
//o2表示已经存入到集合中的元素
//主要条件
int result = o1.getAge() - o2.getAge();
result = result == 0 ? o1.getName().compareTo(o2.getName()) : result;
return result;
}
});
//创建老师对象
Teacher t1 = new Teacher("zhangsan",23);
Teacher t2 = new Teacher("lisi",22);
Teacher t3 = new Teacher("wangwu",24);
Teacher t4 = new Teacher("zhaoliu",24);
//把老师添加到集合
ts.add(t1);
ts.add(t2);
ts.add(t3);
ts.add(t4);
//遍历集合
for (Teacher teacher : ts) {
System.out.println(teacher);
}
}
}
输出结果
Teacher{name=‘lisi’, age=22}
Teacher{name=‘zhangsan’, age=23}
Teacher{name=‘wangwu’, age=24}
Teacher{name=‘zhaoliu’, age=24}
是一种Hash实现的集合,使用的底层结构是HashMap。其特点如下:

public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
//...
public Iterator<E> iterator() {
return map.keySet().iterator();
}
public int size() {
return map.size();
}
public boolean contains(Object o) {
return map.containsKey(o);
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
public void clear() {
map.clear();
}
}
可以看到HashSet内部其实是一个HashMap。
可见HashSet的add方法,插入的值会作为HashMap的key,所以是HashMap保证了不重复。map的put方法新增一个原来不存在的值会返回null,如果原来存在的话会返回原来存在的值。关于HashMap是如何实现的,见后续!
public class HashSetDemo1 {
public static void main(String[] args) {
//创建集合对象
HashSet<String> set = new HashSet<String>();
//添加元素
set.add("money");
set.add("study");
set.add("honor");
set.add("happy");
//不包含重复元素的集合
set.add("study");
//遍历
for (String s : set) {
System.out.println(s);
}
//输出 [study, money, honor, happy]
System.out.println(set);
}
}
Set集合的去重原理使用的是哈希值。
哈希值简介
是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值
如何获取哈希值
Object类中的public int hashCode():返回对象的哈希码值
哈希值的特点
JDK1.8以前
数组 + 链表

JDK1.8以后
节点个数少于等于8个
数组 + 链表
节点个数多于8个
数组 + 红黑树

案例需求
代码实现
学生类
public class Student {
private String name;
private int age;
public Student() {
}
public Student(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;
}
//是否重写equals、hashCode方法,HashSet的值有所不同
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
测试类
public class HashSetDemo02 {
public static void main(String[] args) {
//创建HashSet集合对象
HashSet<Student> hs = new HashSet<>();
Student s1 = new Student("zhangsan",28);
Student s2 = new Student("lisi",27);
Student s3 = new Student("wangwu",29);
Student s4 = new Student("zhaoliu",28);
Student s5 = new Student("zhangsan",28);
//把学生添加到集合
hs.add(s1);
hs.add(s2);
hs.add(s3);
hs.add(s4);
hs.add(s5);
System.out.println(hs);
//遍历集合(增强for)
for (Student s : hs) {
System.out.println(s.getName() + "," + s.getAge());
}
}
}
如果不重写Student类的equals和hashCod方法,名字为"zhangsan"的对象不同,输出如下:

重写Student类的equals和hashCod方法,能保证"zhangsan"对象的唯一性,输出如下:

HashSet集合存储自定义类型元素,要想实现元素的唯一,要求必须重写hashCode方法和equals方法
LinkedHashSet用的也比较少,其也是基于Set的实现。

public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
private static final long serialVersionUID = -2851667679971038690L;
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
public LinkedHashSet() {
super(16, .75f, true);
}
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
@Override
public Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
}
}
其操作方法和HashSet完全一样,那么二者区别是什么呢?1.首先LinkedHashSet是HashSet的子类;2.LinkedHashSet中用于存储值的实现LinkedHashMap,而HashSet使用的是HashMap。LinkedHashSet中调用的父类构造器,可以看到其实列是一个LinkedHashMap。
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
参考黑马程序员相关视频与笔记、【查漏补缺】Java 集合详解!