Java集合
- 集合
- ————单值存储————
- Collection
- List
- 相对于 Collection,添加的方法
- ArrayList
- 底层实现是数组:
- 为什么默认是长度为 10?
- 为什么扩容 1.5 倍?
- Vector
- LinkedList
- Iterator 和 ListIterator
- Iterator
- Iteraotr 实现删除
- ListIterator
- Set
- Set集合该怎么获取元素?
- HashSet
- TreeSet
- TreeSet 放置自定义实例
- ————键值对存储————
- 哈希表概念
- 碰撞处理
- 桶
- 散列因子
- HashMap
- HashMap 的存储流程
- HashMap HashTable ConcurrentHashMap 的区别
- TreeMap
- LinkedHashMap
- Map的==注意点==
- 使用自定义 Key 时,要重写 equals 方法和 hashCode 方法
- 自定义 key 存入后,==不能修改==其属性
集合
我这次不讲解用法,要知道怎么用,去翻阅 API 文档就好了,我这里,就稍微品尝一下集合的源码
这里,我先放上集合的所有继承关系图:
————单值存储————
Collection
下面就是 Collection 的源码,所有集合,都要实现 Collection接口
在早期,都是用 Collection 去指向集合实现的,但是因为无法区分 List 和 Set (主要是有取消重复元素的需要),所以,Sun 在开源项目 PetShop 中,开始推荐使用 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);
// Modification Operations
//增加元素
boolean add(E e);
//删除指定元素
boolean remove(Object o);
// Bulk Operations
//判断当前集合是否包含传入集合的所有元素
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);
}
//java8新增的 stream 流操作
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
}
源码的注释中,还给出了 Collection 的所有子类:
* @see Set
* @see List
* @see Map
* @see SortedSet
* @see SortedMap
* @see HashSet
* @see TreeSet
* @see ArrayList
* @see LinkedList
* @see Vector
* @see Collections
* @see Arrays
* @see AbstractCollection
List
继承 Collection 接口,也添加了自己特殊的接口方法
public interface List<E> extends Collection<E> {
//...
}
相对于 Collection,添加的方法
ArrayList
这里,我们就小尝一下 ArrayList 的源码
ArrayList 在未被指定大小的时候,会默认申请一个长度为 10 的空间
如果在 add 的时候,长度不够,会扩容 1.5 倍
ArrayList,是线程不安全的,所以效率比 Vector 高(Vector 线程安全)
底层实现是数组:
我相信,很多人在一开始写代码的时候,都尝试过写自己的工具类吧(我在知道集合之前时,也因为数组的种种问题,尝试过去写自己的工具类)
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
为什么默认是长度为 10?
下面是其无参构造:
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
DEFAULTCAPACITY_EMPTY_ELEMENTDATA默认是一个空数组:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
那为什么还说默认是 10 的空间呢?
那是在 add 中实现的:
add 源码如下:
这里要引伸一个话题,add 的返回值,只会是 true
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true; //只可能返回 true
}
add 前,会先去确保大小:
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
这里,就会去计算,应该分给数组多少空间:
如果长度为默认长度,就分一个 DEFAULT_CAPACITY
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity); //如果长度为默认长度,就分一个 DEFAULT_CAPACITY
}
return minCapacity;
}
DEFAULT_CAPACITY,就是 10:
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
为什么扩容 1.5 倍?
这里,我们从 add 的确保空间的函数开始:
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) {
// overflow-conscious code
int oldCapacity = elementData.length;
//右移一位,就是除以 2,也就是说,最后扩容得到的大小,是原来的 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
Vector
Vector 线程安全,所以效率低于 ArrayList
因为底层实现和 ArrayList 很像,所以,就不再重复讲解了
LinkedList
使用双向链表,增删很快,但是查找较慢
因为其实现的是双向链表,所以,可以把它当栈和队列去使用
Iterator 和 ListIterator
Iterator
Itetrator,在设计模式的迭代器模式中,我已经充分的讲解过了,这里就不再过多赘述
简单来说,Iterator 可以屏蔽遍历时的细节
而且,相比于 ForEach 来说,iterator 可以在遍历的过程中,删除元素
其对应的方法如下:
这里,我来演示一下,Iterator 该怎么实现删除:
Iteraotr 实现删除
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer ele = iterator.next(); //这里,一定要在 iterator 获取以后,才能删除
if (ele.equals(3)) {
iterator.remove();
}
}
System.out.println(list.toString());
ListIterator
ListIterator 只能在 List 集合中使用
相较于 Iterator,ListIterator 还实现了向集合中插入元素的操作
因为是在是不怎么常用,我就不演示了
这里,还是建议各位去看看迭代器模式,这样,就能对 Iterator 有一个更深刻的认识
Set
Set 是一个集合,既然是集合,那么,其中存储的元素,就是无序的,且不能出现重复的元素
Set 在存储可变的元素的时候,一定要十分小心,因为,元素在 Set 中的位置,是根据元素中的属性计算好的,如果元素属性发生变化,但是其位置有没法根据属性变化后的值动态修改,那么,该元素就出现在了它不该出现的地方
Set集合该怎么获取元素?
我们知道,Set 是没有get()
方法的(这也是必然,元素是无序的,没办法通过下标获取)
所以如果我们要获取 Set 中的元素,只能:
- 使用 toArray()方法,将所有元素放到数组中,然后遍历
- 使用迭代器
- 使用 forEach 遍历
HashSet
HashSet,使用的是散列存储
其中的元素顺序,依赖的是 Hash 算法
HashSet,其实就是把 HashMap 给封装了一下:
public HashSet() {
map = new HashMap<>();
}
只使用 HashSet 的 key,把 value 封装了:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
TreeSet
TreeSet 中的元素,是使用二叉树存储的,是按照自然顺序,有序排列的,在说的直白一点,TreeSet 可以帮助我们,给传入的数据进行排序
public class TreeSetDemo {
public static void main(String[] args) {
Set<String> set = new TreeSet<>();
set.add("B");
set.add("C");
set.add("A");
set.add("D");
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
TreeSet 把 ABCD 的顺序排好了:
TreeSet 放置自定义实例
我们在 TreeSet 中,放置自定义的 Person 对象:
public class TreeSetDemo {
public static void main(String[] args) {
Set<Person> set = new TreeSet<>();
Person p1 = new Person("张三", 20);
Person p2 = new Person("李四", 19);
set.add(p1);
set.add(p2);
}
}
class Person {
private String name;
private Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public Person() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
发现报错,显示无法转换为 Comparable 接口:
这是因为,TreeSet 在放置对象的时候,是依照既定规则,去放置的(这也是为什么我们放入的元素,最后是有序的),所以,我们要让自定义类实现 Comparable 接口:
class Person implements Comparable<Person> {
private String name;
private Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public Person() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
//实现 compareTo 方法
@Override
public int compareTo(Person o) {
return this.age-o.getAge();
}
}
这里我们以 age 为比照对象,当 compareTo 返回的是:
- 大于 0:放后
- 等于 0:不放
- 小于 0:放前
这里我们再传入一个 age 为 19,但 name 不一样的对象:
Person p1 = new Person("张三", 20);
Person p2 = new Person("李四", 19);
Person p3 = new Person("麻子", 19);
set.add(p1);
set.add(p2);
set.add(p3);
可以发现,麻子没有被传进去,说明compareTo 起作用了:
————键值对存储————
哈希表概念
碰撞处理
java8中,当碰撞,则将数据以链表的形式,存放在碰撞的元素后面
当这个链表长度大于等于 8 的时候,由链表转换成红黑树
当红黑树元素个数少到 6 的时候,又转换为链表
桶
桶,就是对象数组中,一个个存链表或者红黑树的位置,在上图中标出来了
HashMap 的初始容量是 16,即HashMap 初始有 16 个桶
散列因子
如果将一个对象数组都存满了,而且元素很多,那么,Hash 表的效率,就会很低,为了避免这种情况发生,我们就要在适当的时机扩容。
但是什么时候该扩容呢?这就需要借助散列因子了
HashMap 中,散列因子是 0.75,表示一旦对象数组中,有 75%的桶中存放了内容,就对对象数组进行扩容,这个 0.75,是官方根据大量测试,最后获得的结论
散列因子也可以指定
散列因子越大,空间浪费越少,但是查找效率会降低
散列因子越小,空间浪费越多,但是查找效率会提升
HashMap
碰撞,使用链表+红黑树解决
初始大小是 16(有 16 个桶)
**负载因子(散列因子)**是 0.75
初始容量和散列因子,能影响 HashMap的效率
HashMap 的存储流程
源码分析搞死人了,这里先放张图
这玩意儿的源码,我记得自己在学 c++的时候,写过,那时候可是花费了大力气,红黑树的转换真的是搞死人了
源码分析,等我有心思了,再去搞吧
HashMap HashTable ConcurrentHashMap 的区别
**HashMap:**线程不安全,效率高
**HashTable:**线程安全,效率低
ConcurrentHashMap:采用分段锁机制,保证线程安全,但是效率也比较高
TreeMap
了解 TreeSet,就知道了 TreeMap
TreeMap 中的 key,是按照自然顺序排序的
LinkedHashMap
记录我们的存储顺序
实现是即使用 HasnMap
又通过一个双向链表,实现存储顺序的保存
Map的注意点
首先我们要明确,Map 的 key 值的比较,是依据 HashCode 和 equals 来判断的(equals 要重写,使得其判断标准是属性值)
只有 HashCode 和 equals判断都是相等的,这个 key 才是相等的
使用自定义 Key 时,要重写 equals 方法和 hashCode 方法
我们知道,Object 的equals()
方法,默认是获取对象的内存位置的,我们的自定义 key 对象,需要重写 equals()
,使其比对的方式,是类中的部分属性
我们自定义一个 Book 类:
这里重写了 equals()
方法,使其比较的是属性内容
重写了hashCode()
方法,使得计算 hash 的数据,是来自属性值,而不是内存位置,这样,就可以拿属性内容一样,但是是另外 new 出来的对象,作为 key 值了
class Book {
private String name;
private String desc;
// constructor...
// getter and setter...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return Objects.equals(name, book.name) &&
Objects.equals(desc, book.desc);
}
@Override
public int hashCode() {
return Objects.hash(name, desc);
}
}
我们来写一个测试,将 Book 作为 key 值:
Map<Book, String> map1 = new HashMap<>();
Map<Book, String> map2 = new HashMap<>();
Book book1 = new Book("金苹果", "讲述了种植苹果的心酸历程");
Book book2 = new Book("银苹果", "讲述了种植苹果的心酸历程");
map1.put(book1,"我的第一本书");
map1.put(book2,"我的第二本书");
//如果不重写 hashCode 方法,那是不能拿新 new 出来的对象,作为 key 值的
Book book3 = new Book("金苹果", "讲述了种植苹果的心酸历程");
System.out.println(map1.get(book3));
如果我们删除 Book 类中,重写的hashCode()
方法,看看发生了什么:
class Book {
private String name;
private String desc;
// constructor...
// getter and setter...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return Objects.equals(name, book.name) &&
Objects.equals(desc, book.desc);
}
// @Override
// public int hashCode() {
// return Objects.hash(name, desc);
// }
}
这时候的测试结果,就是找不到:
因为默认的 hashCode()
计算方法,就是按照内存位置计算的,所以新 new 出来的对象,就算属性值一模一样,但是 hashCode 是永远不可能相等的。
自定义 key 存入后,不能修改其属性
如果修改其属性值,那么,其 hashCode 就会发生变化,那就很难被再次找到
这里,我们承接之前的测试,但是,在存入金苹果后,把金苹果的 name 修改:
Map<Book, String> map1 = new HashMap<>();
Map<Book, String> map2 = new HashMap<>();
Book book1 = new Book("金苹果", "讲述了种植苹果的心酸历程");
Book book2 = new Book("银苹果", "讲述了种植苹果的心酸历程");
map1.put(book1,"我的第一本书");
map1.put(book2,"我的第二本书");
/**
* 修改 key 值
*
* 这和时候,属性值变成了: name:铜苹果 desc:讲述了种植苹果的心酸历程
* 但是,hash 值还是按照 name:金苹果 desc:讲述了种植苹果的心酸历程 算出来的
*/
book1.setName("铜苹果");
Book book3 = new Book("金苹果", "讲述了种植苹果的心酸历程");
Book book4 = new Book("铜苹果", "讲述了种植苹果的心酸历程");
System.out.println(map1.get(book3)); //equals 对不上
System.out.println(map1.get(book4)); //hashCode 对不上
测试结果如下,都取不到:
这是因为,修改 key 值,这个时候,属性值变成了: name:铜苹果 desc:讲述了种植苹果的心酸历程
但是,hash 值还是按照 name:金苹果 desc:讲述了种植苹果的心酸历程 算出来的。所以,无论如何,都对应不上。