1、集合概述
为了方便的对对象进行操作和存储,Java 提供了集合这个工具。前面已经讲过了使用数组来进行数据存储,但是数组有很多弊端,比如不支持动态扩展,一旦声明了数组元素类型就不可再变。
-
数组初始化之后,长度就固定了,不利于拖拽
-
声明了类型之后,就不可再变
-
数组提供的属性和方法少,不利于添加、删除、插入等操作,而且效率较低。同时无法直接获取存储元素的个数
-
数组存储元素是有序的,可重复的。特点单一
为了解决这些缺点,出现了集合。
共同点:
-
都是用来对数据的存储,这里指的是内存的存储,而不是持久化到硬盘的存储。
集合的优势:
-
长度可变
-
提供了添加、删除、插入、查找等便捷高效的方法
-
存储元素可以无序、可以保证不重复
JAVA 的集合主要分为 Collection 和 Map 2种接口。
2、Collection 接口
2.1 Collection 接口 详解
单列数据接口、主要包括2个子接口 List、Set
实线为继承关系,虚线为实现关系。
public interface Collection<E> extends Iterable<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
boolean retainAll(Collection<?> c);
void clear();
// Comparison and hashing
boolean equals(Object o);
int hashCode();
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, 0);
}
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
}
以上为 JDK 8.0 的Collection 源码。就是一个普通接口,继承了 Iterable 接口。
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}
Iterable 接口有一个 iterator 抽象方法,返回值是 Iterator 接口的实现类。
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
Collection 接口继承 Iterable 接口,重写了里面的默认方法 spliterator。 并没有重写 iterator 方法。
Iterator 接口的实现类对象称为迭代器、是设计模式中的一种。迭代器的定义:提供一种方法访问一个容器对象中的各个元素,而不暴露该对象的内部细节。迭代器模式就是为容器而生。
2.1.1 方法详解
Collection 里面有 很多的抽象方法,方便的进行增删改查。
使用 ArrayList 来测试 Collection 接口。因为接口是不能直接new对象的。必须是new实现类。
-
add方法: 添加元素; size方法: 获取元素个数
Collection coll = new ArrayList();
coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱
coll.add(new Date());
System.out.println(coll.size()); // 4
-
addAll 方法: 添加另一个集合的所有元素; isEmpty 方法:判断集合是否没有元素(注意不是判断null)
Collection coll = new ArrayList();
Collection coll2 = new ArrayList();
coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱
coll.add(new Date());
coll2.add("456");
coll2.add("GGG");
// 将 coll2 集合的全部元素添加到 coll中
coll.addAll(coll2);
System.out.println(coll.size()); // 6
// 判断coll2 集合是否没有元素
System.out.println(coll2.()); // false 即不为空,coll2里面有元素。
System.out.println(coll); // [AA, BB, 123, Tue Aug 17 19:40:48 CST 2021, 456, GGG]
-
clear方法:清空集合内所有元素
Collection coll = new ArrayList();
coll.add("AA");
coll.add("BB");
coll.clear();
System.out.println(coll.isEmpty()); // true
-
contains 方法: 判断集合内是否包含某个元素。 内部判断相等是调用equals方法。没有重写就是判断对象的地址。
因此,如果添加自定义类型对象Class类型的,需要重写equals方法。
Collection coll = new ArrayList();
coll.add("123");
coll.add("555");
coll.add(new String("tom"));
coll.add(false);
boolean tom = coll.contains("tom");
System.out.println(tom); // true
boolean tom1 = coll.contains("Tom");
System.out.println(tom1); // false
System.out.println(coll); // [123, 555, tom, false]
-
containsAll 方法:判断集合内的元素是否包含另一个集合的所有元素。同样是调用equals方法进行判断相等的。
Collection coll = new ArrayList();
Collection coll2 = new ArrayList();
Collection coll3 = new ArrayList();
coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱
coll2.add("456");
coll2.add("GGG");
coll2.add("ddd");
coll3.add("456");
coll3.add("GGG");
boolean b = coll.containsAll(coll2);
System.out.println(b); // false
boolean b1 = coll2.containsAll(coll3);
System.out.println(b1); // true
-
remove(Object o) 方法: 移除元素。 内部还是会调用equals方法,只有先判断相等才能移除。
Collection coll = new ArrayList();
Collection coll2 = new ArrayList();
coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱
boolean remove = coll.remove(123);
System.out.println(remove); // true
boolean remove1 = coll.remove(456);
System.out.println(remove1); // false
System.out.println(coll); // [AA, BB]
-
removeAll(Collection<?> c) 方法:移除输入集合内的所有元素。只能移除输入集合与被移除集合的公共元素,只要移除了1个元素就会返回true。一个都没有移除返回false。通用涉及到判断相等,调用equals方法。自定义类需要重写equals方法。
类似于求2个集合的差集,被移除集合只剩下除公共元素外的元素。
Collection coll = new ArrayList();
Collection coll2 = new ArrayList();
coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱
coll2.add(123);
coll2.add(456);
boolean b = coll.removeAll(coll2);
System.out.println(b); // true
System.out.println(coll); // [AA, BB]
-
retain (Collection<?> c)方法: 求 输入集合与 原集合的交集。如果有交集返回true,否则返回false 。当返回值为true时,原计划元素为交集元素; 当返回值为false时,由于没有交集,原计划变成空集。 这个操作还是会调用equals方法。
注意这个操作也是对原集合直接修改的。
Collection coll = new ArrayList();
Collection coll2 = new ArrayList();
coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱
coll2.add(123);
coll2.add(456);
boolean b = coll.retainAll(coll2);
System.out.println(b); // true
System.out.println(coll); // [123]
-
equals(Object o) 方法: 判断输入的对象与原集合是否相等。 参数是object对象,说明可以传任意值。
但是只有传与原集合元素一致的集合时,才会返回true。
注意:这个一致还得看集合是否是有序的,如果是List,集合元素要求顺序和值都相同才会返回true;对于无序的Set,只要求元素相同就会返回true。
Collection coll = new ArrayList();
Collection coll2 = new ArrayList();
coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱
coll2.add(12);
coll2.add(456);
boolean equals = coll.equals(coll2);
System.out.println(equals); // false
-
hashCode 方法 返回当前对象的哈希值。这是是object的方法。所有的对象都有这个方法。
Collection coll = new ArrayList();
coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱
int i = coll.hashCode();
System.out.println(i); // 2094266
-
toArray () 方法 : 集合转换为数组
Collection coll = new ArrayList();
coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱
Object[] arr = coll.toArray();
for (Object o : arr) {
System.out.print(o + "\t");
}
//AA BB 123
数组也可以转换为集合,使用 Arrays.asList() 方法即可
List<String> strings = Arrays.asList(new String[]{"aa", "bb"});
System.out.println(strings);
// [aa, bb]
// 这样写会将 int[] 当成集合的一个元素。
List<int[]> ints = Arrays.asList(new int[]{123, 456});
System.out.println(ints);
// [[I@5c8da962]
List<Integer> integers = Arrays.asList(new Integer[]{123, 456});
System.out.println(integers);
// [123, 456]
List<Integer> list = Arrays.asList(123, 456);
System.out.println(list);
// [123, 456]
2.1.2 迭代与遍历
-
iterator() 方法:用于集合的遍历
这个方法就是继承的是 Iterable 接口的方法 iterator。iterator方法返回值是一个 Iterator 实现类对象。即迭代器对象。
Collection coll = new ArrayList();
coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱
// 得到一个迭代器对象,使用Iterator接口接收Iterator实现类的对象。
Iterator iterator = coll.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
hasNext 和 next 都是 Iterator 接口中定义的方法。
hasNext 方法 就判断一下还有没有元素。
next 方法就是获取下一个元素。
那么这个 Iterator 实现类对象,是如何实现的 这2个方法hasNext 、next 呢?
Iterator iterator = coll.iterator();
System.out.println(iterator.getClass()); // class java.util.ArrayList$Itr
先看一下这个实现类对象究竟是什么:class java.util.ArrayList$Itr 是ArrayList里面的Itr这个类的对象。
下面是 ArrayList类里面的 Itr 内部类的源码:
private class Itr implements Iterator<E> {
int cursor; // 要返回的下一个元素的索引
int lastRet = -1; // 返回的最后一个元素的索引,如果没有,则返回 -1
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
原理就是 内部维护了一个 cursor ,这个cursor 叫做指针、游标、索引 都可以。就是指向下一个元素的索引。
hasNext方法 是判断 cursor 索引是否和容器个数相等。相等就没有下一个,不相等就有。cursor是使用默认初始化为0的
next 方法 会先判断 cursor 与 容器个数 size 的关系。通过验证之后,cursor就会 移动一位 + 1,然后取出cursor+1之前位置的元素返回。
里面还有remove方法。移除元素。这个方法没有参数,内部实现是删除 lastRet 这个索引对应的值。删除这个值还是调用的ArrayList的remove(int index)方法。
Collection coll = new ArrayList();
coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱
Iterator iterator = coll.iterator();
while (iterator.hasNext()){
Object next = iterator.next();
if ( "BB".equals(next)){
// 调用迭代器的remove方法
iterator.remove();
}
}
// 增强for循环遍历。后面会讲到
for (Object o : coll) {
System.out.println(o);
}
// 输出
//AA
//123
这个remove内部实现是依赖于 lastRet,lastRet初始值是-1,所有一开始不能直接调用remove方法。得先调用next方法改变lastRet值。才能调用remove方法。即调用remove方法之前需要调用next方法。
2.1.3 foreach 循环
这个 foreach 是JDK5.0 新增的特性。用于遍历集合、数组
Collection coll = new ArrayList();
coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱
// 增强for循环
for (Object o : coll) {
System.out.print(o + "\t");
}
// AA BB 123
就是上一步写的增强for循环。与普通for循环的区别在于,可以直接的遍历每一个元素,但是也无法直接获取元素的下标了。
格式:
for(集合元素类型 变量名: 集合对象){}
变量名自己取。增强for还可以遍历数组。 那么就是格式就是: for(数组元素类型变量名:数组对象){}
这个增强for循环内部实现原理还是 调用的迭代器。只是形式上更简洁而已。
2.2 List 接口
存储有序、可重复的数据。习惯上叫 List 为动态数组。
public interface List<E> extends Collection<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean addAll(int index, Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
default void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final ListIterator<E> li = this.listIterator();
while (li.hasNext()) {
li.set(operator.apply(li.next()));
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
void clear();
boolean equals(Object o);
int hashCode();
E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index);
int indexOf(Object o);
int lastIndexOf(Object o);
ListIterator<E> listIterator();
ListIterator<E> listIterator(int index);
List<E> subList(int fromIndex, int toIndex);
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.ORDERED);
}
}
List 接口继承了 Collection 接口,Collection中能被继承的抽象方法,非静态方法,默认方法在List接口里面都有。
List 接口有3个实现类。Vector 、ArrayList、LinkList
List的常用方法:以ArrayList为例
-
add 方法: 添加一个元素
ArrayList arr = new ArrayList(10);
arr.add("AA"); // 添加到末尾
arr.add(1, "BB"); // 插入到指定索引
-
addAll 方法:和Collection中的 addAll方法作用一样。
ArrayList arr = new ArrayList(16);
arr.add("AA");
arr.add(1, "BB");
List<Integer> nums = Arrays.asList(1, 2, 3);
arr.addAll(nums); // 添加nums 中每一个元素到末尾
arr.addAll(2, nums); // 将 nums 中每一个元素插入到 索引为2的位置
for (Object o : arr) {
System.out.println(o);
}
-
indexOf () : 返回元素首次出现的位置,没有就返回-1 ;作用与String类中的一样
ArrayList arr = new ArrayList(16);
arr.add("AA");
arr.add(1, "BB");
List<Integer> nums = Arrays.asList(1, 2, 3);
arr.addAll(nums);
int i = arr.indexOf(2);
System.out.println(i); // 3
int i1 = arr.indexOf(5);
System.out.println(i1); // -1
-
lastIndexOf 返回元素最后一次出现的位置,没有就返回-1;作用与String类中的一样
ArrayList arr = new ArrayList(16);
arr.add("AA");
arr.add(1, "BB");
List<Integer> nums = Arrays.asList(1, 2, 3);
arr.addAll(nums);
arr.addAll(1, nums);
System.out.println(arr); // [AA, 1, 2, 3, BB, 1, 2, 3]
int i = arr.lastIndexOf(2);
System.out.println(i); // 6
-
remove 移除元素
remove(int index)、remove(Object o) 有2个重载的方法
ArrayList arr = new ArrayList(16);
arr.add("AA");
arr.add(1, "BB");
List<Integer> nums = Arrays.asList(1, 2, 3);
arr.addAll(nums);
arr.addAll(1, nums);
System.out.println(arr); [AA, 1, 2, 3, BB, 1, 2, 3]
// 输入是数字时,默认调用参数为int index这个方法,返回值是删除的 元素
// 传入索引时,不能超过List的最大下标,否则报错
Object remove = arr.remove(2);
// 是对象是时,调用这个方法,返回值是是否删除成功
boolean remove1 = arr.remove(nums);
System.out.println(remove1); // false
System.out.println(arr); // [AA, 1, 3, BB, 1, 2, 3]
这里面 Object remove = arr.remove(2); 删除的是 索引为2的元素。如果就是要删除 元素为2,要怎么做呢?
我们知道,List里面存储的都是对象,另一个remove重载的方法参数值就是对象。因此传一个包装类对象即可。
boolean b= arr.remove(new Integer(2)); 注意,这个方法返回值是 boolean 类型。
-
set(int index, E element) 方法: 将指定索引元素修改为输入值
ArrayList arr = new ArrayList(16);
arr.add("AA");
arr.add("CC");
arr.add(1, "BB");
arr.set(1,"dd");
System.out.println(arr);
-
subList(int fromIndex, int toIndex)方法: 返回一个子list, 也是左闭右开区间。 会返回新的List对象,不会修改原来的List
ArrayList arr = new ArrayList(16);
arr.add("AA");
arr.add("CC");
arr.add(1, "BB");
List list = arr.subList(0, 1);
System.out.println(arr); //[AA, BB, CC]
System.out.println(list); //[AA]
-
List 的遍历
ArrayList arr = new ArrayList(16);
arr.add("AA");
arr.add("CC");
arr.add(1, "BB");
1、普通for 循环遍历
for (int i = 0; i < arr.size(); i++) {
System.out.println(arr.get(i));
}
2、List继承了Collection接口,使用 iterator() 方法 获取迭代器。使用迭代器进行遍历
Iterator iterator = arr.iterator();
while (iterator.hasNext()){
Object next = iterator.next();
System.out.println(next);
}
3、使用增强for循环进行遍历
for (Object o : arr) {
System.out.println(o);
}
2.2.1 Vector 是List的古老实现类
说它是古老实现类是因为它是JDK1.0出现的,而实现的接口Collection是JDK1.2出现的。出现的很早,现在基本不用。
Vector 是线程安全的,但是执行效率低。底层使用 Object[]
源码过长,不适合全部在这里写出。
public Vector() {
this(10);
}
使用无参构造器会直接默认初始化容量为 10
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 默认扩容为原来的 2倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
其他的使用与ArrayList差不多。
它是线程安全的,但是效率低下。基本不用
即使需要使用线程安全的List,也可以使用Collections类中的 synchronizedList(List<T> list) ,将不安全的ArrayList对象传入,得到线程安全的List对象。
2.2.2 ArrayList 是List的主要实现类
ArrayList 是线程不安全的,效率高。底层使用 Object[] ,源码过长,不适合全部在这里写出。以下为JDK8.0的源码分析
transient Object[] elementData; // ArrayList 底层的数组
无参构造器:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
添加元素
public boolean add(E e) {
// 先确保容量是否足够
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
ensureCapacityInternal会确保容量够,如果不够会进行扩容。通常扩容为原来的1.5倍大小。
ArrayList 里面有一个默认容量 private static final int DEFAULT_CAPACITY = 10; 使用无参构造器第一次添加的数据如果小于10,数组的容量会扩容到默认容量10
// 扩容代码
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 默认扩容为 原来容量+原来容量右移1位 右移1位等于除以2 即1.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 arr = new ArrayList(16);
在JDK7.0中,使用无参构造器创建ArrayList对象时,会直接初始化为一个长度为10的数组。而JDK8使用无参构造创建对象则是一开始是一个空的,第一次添加时直接进行容量确保操作。如果需要的容量小于10,才会变成长度为10的数组。
2.2.3 LinkList 是List的另一种分工的实现类,底层使用双向链表实现。
底层使用 双向链表存储。适合频繁的插入、删除操作,源码过长,不适合全部在这里写出。
transient int size = 0;
// 第一个Node 默认初始化为 null
transient Node<E> first;
// 最后一个Node 默认初始化为 null
transient Node<E> last;
LinkList 有3个典型的属性,size是元素个数。以及 Node类型的first ,Node类型的last
Node 是LinkList的内部类,是linkList的基本存储单位
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;
}
}
Node类里面有 3个属性,1个是item 元素,1个Node类型的next,1个Node类型的prev
next是记录当前节点的下一个节点,prev是记录当前节点的上一个节点
Node就是一个类,linkList的基本存储单位就是一个类。因为每个类实例化的位置都是随机的,所以链表是不连续的。需要用next、prev来记录下一个元素,上一个元素是什么。item存储的就是真正的内容。next、prev也说明这是双向链表,可以往下找,也可以往上找。
无参构造器:啥也不干。
public LinkedList() {
}
-
add方法
public boolean add(E e) {
linkLast(e);
return true;
}
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++;
}
总结:
Vector、ArrayList、LinkList的使用在List接口的方法介绍中以ArrayList为例。其他的2个一样的使用。
2.3 Set 接口
存储无序、不可重复的数据 类似于高中数学中的集合。
public interface Set<E> extends Collection<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
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();
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT);
}
}
以上为Set接口的源码,并没有额外定义新的方法,基本都是继承于Collection 接口的方法
以HashSet实现类为例。
-
add 方法 添加元素
HashSet set = new HashSet();
set.add(123);
set.add(123);
set.add("abc");
set.add(null);
for (Object o : set) {
System.out.print(o + "\t");
}
// null abc 123
可以看到,可以添加null值。遍历输出时不是按照我们添加的顺序。这就是无序,但是无序不代表随机。
而且,虽然我们调用了2次添加 123,但是输出只有1个。说明Set添加的元素不可重复。
1、什么是无序呢? 看看源码
// HashSet 的无参构造实际上是 得到了一个 HashMap 对象
public HashSet() {
map = new HashMap<>();
}
// HashSet 的 add 方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// HashMap 的 add 方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
可以看到,源码里面HashSet的add方法是调用 map对象的put方法。map是 HashMap 类的一个对象。
所以,HashSet 实际上是和HashMap有关的。这里还需要研究HashMap。先直接给出答案,就是添加时的顺序不是按照索引顺序,而是按照一定的算法计算得到。但是一旦添加完成,顺序也就确定了。变量输出也是确定的。
2、什么是不可重复
// HashMap 的 putVal 方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
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) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
上面讲到添加2次123,输出只有1个。 看源码可以知道,add方法是有返回值的,返回值是boolean类型。表示此次添加是否成功。
成功返回true,失败返回false。
那么,是如果判断不可重复的呢?即添加的数据是否与已有数据相等呢? 答案是 调用Object对象的equals方法。任意类有继承Object对象,都有equals方法。equals 默认是调用 == 比较地址。因此,要实现对象之间的判断相等,必须要重写这个equals方法。Set添加的元素才能确保不重复。
问题来了:如果添加 到1000个数据,10000个数据,每添加一个数据都要进行一一比较,效率就太慢了。
HashSet 内部还是采用 数组进行存储数据的。初始大小为16
HashSet采用的是 计算Hash值【调用hashCode方法】 + equals 混合的方法【最终以equals为准】,每一个hash值都对应数组一个位置【注意不同的hash值可能对应同一个位置】。只需要比较位置上是否有元素。没有元素就可以添加。
如果位置上已经有元素了,就需要调用equals方法比较 已有元素与 需要添加的元素是否相同。当hash不同,而且元素也不同时,说明这个元素需要被添加进去,但是这个位置已经有元素了啊。怎么办? 这个位置链上一个链表来存储,JDK7时,新元素位置在上面,JDK8新元素在老元素的下面。即 HashSet 实际上是 数组加上链表的 混合结构。
在第一步 计算hash值时,如果hash值一样,是否说明这2个元素一样呢?不一定。可能计算hash值的算法出现了重复值。因此还需要调用equals方法进行最终判断。如果equals不同,还是要添加的。如果equals也相同,就说明真的是同一个元素。
总结:
1、HashSet 内部采用 数组+链表混合结构存储数据
2、HashSet 为了加快速度,采用 hash 值的计算比较来辅助比较相等。最终确定是否相等的是equals方法。
3、调用equals的时机1:2个不同的 hash 值指向同一个数组位置时,调用equals判断是否真的相同
4、调用equals的时机2: 2个相同的hash值,还需要调用 equals 判断是否真的相同。
3、hashCode 与 equals 方法的重写
Object中的hashCode方法是一个native方法。即调用的是C语言写的。大致的逻辑是产生一个随机数。
先看看idea 给我们生成的重写的hashCode方法
public class Cat extends Animal{
public int age = 14;
public String type = "cat";
// idea 默认模板生成的
@Override
public int hashCode() {
int result = age;
result = 31 * result + (type != null ? type.hashCode() : 0);
return result;
}
重写的原则是 同一个类的对象的属性值相同时,我们希望计算的hash值是相同的,而属性值不同时,计算的hash值是不同的
为什么会出现31这个数字?
加上系数是为了避免一些情况。String类已经重写了hashCode方法,能够实现String相同是,计算的hash值相同。假设A对象的属性 type计算的hash值是 40,B对象计算的hash值是 30,而 A的age = 20,B的age=30,不加系数直接用 下面的式子
@Override
public int hashCode() {
int result = age;
return result + (type != null ? type.hashCode() : 0);
}
就会出现 hash值相同的情况。但是A、B对象属性其实是不同的。当乘以系数31时,放大了age之间的误差,就能尽量避免这种情况发生。
那为什么系数是31? 31=32-1=2^5 -1 因为31可以用 i * 31 == (i << 5)- 1 来表示,很多虚拟机对这里有优化。
31 只占用5个bit,相乘造成数据移除的概率较小。(安全性)
31是一个素数,素数的作用是如果我用一个数字A乘以这个素数,那么最终出来的结果只能被素数本身A以及1来整除。(减少冲突)
equals 方法的重写要尽可能的与hashCode值保持一致性。即如果只需要满足某一个属性相同,就判断对象相同,那么hashCode也应该是当这个属性相同时,计算的hash值是一样的。
保持一致性就是保持 判断对象相同的逻辑涉及到的属性,在计算hash值时,当这些属性一样时,计算出的hash值也要相同。
2.3.1 HashSet 主要实现类
线程不安全的,可以存储null值。
应用:去除List中重复的数字(对象)。
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(2);
list.add(4);
list.add(4);
list.add(5);
// 使用 set 数据结构的特点,不可重复
Set<Integer> set = new HashSet<>();
// addAll方法将传入的集合的全部元素添加到set中
set.addAll(list);
// 将 set转换为ArrayList
ArrayList<Integer> newList = new ArrayList<>(set);
for (Integer i : newList) {
System.out.println(i);
}
这里是去除重复的数字,Integer 类已经重写了equals 和hashCode方法。如果是去除我们自定义的类,这个类需要重写equals和hashCode方法。
练习: 有一个类Cat 已经重写了equals和hashCode方法。分析下面的过程
public class Cat{
public int age;
public String type = "cat";
@Override
public int hashCode() {
int result = age;
result = 31 * result + (type != null ? type.hashCode() : 0);
return result;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Cat)) {
return false;
}
Cat cat = (Cat) o;
if (age != cat.age) {
return false;
}
return Objects.equals(type, cat.type);
}
public Cat(int age, String type) {
this.age = age;
this.type = type;
}
public Cat() {
}
@Override
public String toString() {
return "Cat{" +
"age=" + age +
", type='" + type + '\'' +
'}';
}
}
HashSet set = new HashSet();
Cat c1 = new Cat(15, "AA");
Cat c2 = new Cat(16, "BB");
set.add(c1);
set.add(c2);
System.out.println(set); // [Cat{age=16, type='BB'}, Cat{age=15, type='AA'}]
// 修改c1对象的 type属性
c1.type = "CC";
// set 移除 c1 对象:set寻找元素会先计算hash值,再找位置。会先计算传入的c1对象的hash值,由于type属性改变,hash值也被改变。因此位置与原来不同。remove 的时候发现新位置 没有元素,就认为成功删除。即使有元素,再调用equals方法,发现不一样,也认为删除成功。
set.remove(c1);
System.out.println(set); // [Cat{age=16, type='BB'}, Cat{age=15, type='CC'}]
// 添加之前先计算 hash值。传入的对象的hash值对应的位置没有元素,添加成功。注意最开始添加的是按照属性是15,“AA” 计算的。
set.add(new Cat(15, "CC"));
// 添加的时候计算 hash值,发现已经有一个元素,属性是15,“CC”了,然后调用equals方法比较,发现不同,以链表的形式指向老元素。添加成功。
set.add(new Cat(15, "AA"));
System.out.println(set); // [Cat{age=16, type='BB'}, Cat{age=15, type='CC'}, Cat{age=15, type='CC'}, Cat{age=15, type='AA'}]
2.3.1.1 LinkedHashSet 是 HashSet 的 一个子类
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);
}
}
遍历其内部数据时,可以按照添加的顺序进行遍历。不代表它是有序的。
我们知道无序是指添加元素存储的时候是无序。那么LinkedHashSet 是如何做到遍历时,按照我们添加的顺序呢?就是增加了双向链表
对于频繁的遍历操作,使用 LinkedHashSet 比 HashSet 效率更高。
LinkedHashSet set = new LinkedHashSet();
set.add(123);
set.add(456);
set.add("abc");
set.add(456);
for (Object o : set) {
System.out.print(o + "\t");
}
// 123 456 abc
调用无参构造器时:调用的是父类 HashSet中的构造器
public LinkedHashSet() {
super(16, .75f, true);
}
实际是:得到一个 LinkedHashMap 对象
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
调用 add 方法。看上面的源码可知,LinkedHashSet 类只是定义了4个构造方法,以及重写了Spliterator方法。
调用add方法,实际上是父类 HashSet 中的add方法,最终还是使用map进行添加数据
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
2.3.2 TreeSet
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
private transient NavigableMap<E,Object> m;
private static final Object PRESENT = new Object();
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
public TreeSet() {
this(new TreeMap<E,Object>());
}
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
public TreeSet(Collection<? extends E> c) {
this();
addAll(c);
}
public TreeSet(SortedSet<E> s) {
this(s.comparator());
addAll(s);
}
public Iterator<E> iterator() {
return m.navigableKeySet().iterator();
}
public Iterator<E> descendingIterator() {
return m.descendingKeySet().iterator();
}
public NavigableSet<E> descendingSet() {
return new TreeSet<>(m.descendingMap());
}
public int size() {
return m.size();
}
public boolean isEmpty() {
return m.isEmpty();
}
public boolean contains(Object o) {
return m.containsKey(o);
}
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return m.remove(o)==PRESENT;
}
public void clear() {
m.clear();
}
public boolean addAll(Collection<? extends E> c) {
// Use linear-time version if applicable
if (m.size()==0 && c.size() > 0 &&
c instanceof SortedSet &&
m instanceof TreeMap) {
SortedSet<? extends E> set = (SortedSet<? extends E>) c;
TreeMap<E,Object> map = (TreeMap<E, Object>) m;
Comparator<?> cc = set.comparator();
Comparator<? super E> mc = map.comparator();
if (cc==mc || (cc != null && cc.equals(mc))) {
map.addAllForTreeSet(set, PRESENT);
return true;
}
}
return super.addAll(c);
}
public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
E toElement, boolean toInclusive) {
return new TreeSet<>(m.subMap(fromElement, fromInclusive,
toElement, toInclusive));
}
public NavigableSet<E> headSet(E toElement, boolean inclusive) {
return new TreeSet<>(m.headMap(toElement, inclusive));
}
public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
return new TreeSet<>(m.tailMap(fromElement, inclusive));
}
public SortedSet<E> subSet(E fromElement, E toElement) {
return subSet(fromElement, true, toElement, false);
}
public SortedSet<E> headSet(E toElement) {
return headSet(toElement, false);
}
public SortedSet<E> tailSet(E fromElement) {
return tailSet(fromElement, true);
}
public Comparator<? super E> comparator() {
return m.comparator();
}
public E first() {
return m.firstKey();
}
public E last() {
return m.lastKey();
}
public E lower(E e) {
return m.lowerKey(e);
}
public E floor(E e) {
return m.floorKey(e);
}
public E ceiling(E e) {
return m.ceilingKey(e);
}
public E higher(E e) {
return m.higherKey(e);
}
public E pollFirst() {
Map.Entry<E,?> e = m.pollFirstEntry();
return (e == null) ? null : e.getKey();
}
public E pollLast() {
Map.Entry<E,?> e = m.pollLastEntry();
return (e == null) ? null : e.getKey();
}
@SuppressWarnings("unchecked")
public Object clone() {
TreeSet<E> clone;
try {
clone = (TreeSet<E>) super.clone();
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
clone.m = new TreeMap<>(m);
return clone;
}
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
s.defaultWriteObject();
s.writeObject(m.comparator());
s.writeInt(m.size());
for (E e : m.keySet())
s.writeObject(e);
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
@SuppressWarnings("unchecked")
Comparator<? super E> c = (Comparator<? super E>) s.readObject();
TreeMap<E,Object> tm = new TreeMap<>(c);
m = tm;
int size = s.readInt();
tm.readTreeSet(size, s, PRESENT);
}
public Spliterator<E> spliterator() {
return TreeMap.keySpliteratorFor(m);
}
private static final long serialVersionUID = -2479143000061671589L;
}
TreeSet 使用二叉树存储的,具体一点是红黑二叉树存储的。可以按照添加的对象的指定属性进行排序。
TreeSet 只能存储同类型的对象。这是为了保证 可以按照添加的对象的指定属性进行排序。要是不同类的对象,就无法保证都有某个属性了。
TreeSet set = new TreeSet();
set.add("abc");
set.add("3333");
set.add("abc");
for (Object o : set) {
System.out.print(o + "\t");
}
// 3333 abc
输出是按照从小到大。
当我们传入自己定义的类时,要求必须 实现 Comparable接口。重写compareTo 方法
public class Cat implements Comparable{
public int age;
public String type = "cat";
@Override
public int hashCode() {
int result = age;
result = 31 * result + (type != null ? type.hashCode() : 0);
return result;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Cat)) {
return false;
}
Cat cat = (Cat) o;
if (age != cat.age) {
return false;
}
return Objects.equals(type, cat.type);
}
@Override
public int compareTo(Object o) {
if ( !(o instanceof Cat)){
throw new RuntimeException("必须传入相同类型的对象");
}
Cat cat = (Cat) o;
if (this == cat){
return 0;
}
return Integer.compare(this.age, cat.age);
}
public Cat(int age, String type) {
this.age = age;
this.type = type;
}
public Cat() {
}
@Override
public String toString() {
return "Cat{" +
"age=" + age +
", type='" + type + '\'' +
'}';
}
}
注意,TreeSet 判断相等不再使用equals方法,而是使用Compare 接口中的 compareTo方法。
TreeSet<Cat> set = new TreeSet<>();
set.add(new Cat(15, "ddd"));
set.add(new Cat(10, "ddd"));
set.add(new Cat(12, "ddd"));
set.add(new Cat(50, "ddd"));
for (Cat o : set) {
System.out.println(o);
}
// 输出
Cat{age=10, type='ddd'}
Cat{age=12, type='ddd'}
Cat{age=15, type='ddd'}
Cat{age=50, type='ddd'}
如果不想实现 Comparable 接口,也想用 TreeSet存储,可以使用 Comparator 的匿名实现类对象,初始化TreeSet
Comparator com = new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if ( !(o1 instanceof Cat)){
throw new RuntimeException("必须传入Cat类型的对象");
}
if ( !(o2 instanceof Cat)){
throw new RuntimeException("必须传入Cat类型的对象");
}
Cat cat1 = (Cat) o1;
Cat cat2 = (Cat) o2;
if (cat1 == cat2){
return 0;
}
return Integer.compare(cat1.age, cat2.age);
}
};
TreeSet<Cat> set = new TreeSet<>(com);
set.add(new Cat(15, "ddd"));
set.add(new Cat(10, "ddd"));
set.add(new Cat(12, "ddd"));
set.add(new Cat(50, "ddd"));
其实,就是在 实例化 TreeSet 对象时,告诉它 比较的方法。这样添加的对象就不要求一定实现 Comparable 接口了。
当使用这个方式实例化 TreeSet 对象,即使 添加的对象实现了 Comparable 接口,比较的方法还是 Comparator匿名实现类中定义的方法。同理,比较相等,也是这个 compare方法。而不是 equals方法了。