集合框架体系
写在前面的话:由于最近在准备面试,刚好复习到集合,就写一个博客记录一下
集合的分类
单列集合
单列集合直接存储的对象,实则存储的也是对象的引用,也就是对象的地址.
双列集合
双列集合是通过key-value 对数据进行存储的,我们想要获取集合中的元素必须通过key进行获取
集合体系图
要学习集合我们必须了解集合的体系,它都是有哪些类,有哪些接口,哪些方法,可以为我们做什么?优点是啥?缺点又在哪?网上有很多这种集合体系图我就不在这画了,同学们可以 点击打开集合框架查看.
开始学习
Iterable
-
通过我们对集合框架的了解,我们发现所有单列集合都继承自==Iterable==接口,在Idea中查看源代码得知Iterable接口中定义两个方法如下:
/* *返回一个迭代器 */ Iterator<T> iterator(); /* *遍历整个集合 传递一个消费者接口 */ default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } }
-
第一个返回一个迭代器 很好理解,我们可以通过迭代器对集合进行遍历,这也是Collection下的集合可以通过迭代器进行遍历,而map下的集合不可以的原因.
-
第二个forEach方法 需要传递一个消费者接口, 然后会对整个集合进行遍历.
具体使用方法如下:
List vector = new Vector<Integer>(); vector.add(1); vector.add(2); vector.add(3); /* *这里我通过了lambda表达式写的 lambda是Java8新增加的一个特性 */ vector.forEach((i) ->{ System.out.println(i); });
-
写在后面的话:
现在程序员必须要会的 :
1: lambda表达式
2:链式编程
3:函数式接口
4:stream流式计算
传送门放在下面
流式计算
lambda
ps: 大家可以看看B站狂神老师的其他课程,质量很高还是免费的 —> 传送门
-
collection
-
collection接口下定义了对集合进行操作的CURD方法,
-
add(Object o) 往集合中添加一个元素,因为是Object类型所以添加的元素必须是引用类型,这个也是我们设计包装类的作用之一,虽然我们再添加元素时直接是写一个基本数据类型,但是在JDK1.5时添加了自动拆装箱的概念,所以我们这里写的基本数据类型其实是一个对象.
jdk1.5前list.add(new Integer(1)) JDk1.5之后list.add(1);
-
讨论一下包装类的优缺点
- 优点:
- 首先设置包装类可以更好的满足Java面向对象的性质,而基本数据类类型只是Java中的关键字不具备面向对象的性质
- 其次在很多时候我们用到的是包装类而不是基本数据类型,例如上述的集合中需要存入的对象,我们就必须要用到包装类.
- 缺点: 每个包装类都是一个对象,都需要在堆上开辟一个空间,比较浪费空间
- 优点:
List
List是collection下的一个**有序,可重复**的集合接口.
常见的实现类有如下:
-
ArrayList
-
ArrayList是基于**可变数组**实现的,这里的可变数组的概念是指当集合装满元素时,会创建一个新的数组将元素复制过去,从而达到宏观上可以扩容的效果.这么做不好的地方就是,如果经常扩容会造成资源的浪费,所以我们在进行创建ArrayList时尽量指定其大小。
/* *创建集合时指定其大小 */ new ArrayList<>(16);
数组的扩容
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; // >>位运算符,表示向右移动一位相当于除以二 也就是说ArrayList是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); }
ArrayList在JDK7是初始容量为10,在1.8是初始容量为0只有在添加元素时才会指定默认大小为10
源码如下:
/* *JDK7 */ public ArrayList() { this(10); } public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; /* *JDK8 */public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
-
ArrayList使用场景
由于ArrayList底层是数组 地址空间是连续的所以我们通过get(i)方法获取元素时时间复杂度为O(1) 但是如果我们对其进行添加元素时,需要大量的移动元素所以时间复杂度为O(n),所以ArrayList使用的场景应该是经常从集合里面取出数据,而不经常对其元素进行增删.
-
-
LinkedList
- LinkedList底层是通过**双向链表**实现的,所以我们通过get(i)进行获取元素时需要遍历链表,源码如下:
Node<E> node(int index) { // assert isElementIndex(index); if (index < (size >> 1)) { Node<E> x = first; //通过遍历查找到第i个元素 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; } }
虽然LinkedList获取元素需要遍历,但是如果获取first节点和last节点则很快,因为LinkedList有两个属性分别用来存放这两个节点
//第一个节点 transient Node<E> first; //获取第一个节点的方法 public E getFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return f.item; } //最后一个节点 transient Node<E> last; //获取最后一个节点的方法 public E getLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return l.item; }
但是其增减元素比较快,因为只需要维护两个节点之间的关系,
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++; }
- 最后因为LinkedList因为实现了Deque接口,所以LinkedList可以当做双端队列来使用
-
总结:
- ArrayList底层是通过可变数组LInkedList底层是通过可变数组
- 由于底层数据结构的关系,所以ArrayList适用于查询比较多场景,而LinkedList适用于增删比较多的场景
- 由于LinkedList实现了Deque接口所以可以当做双端队列来使用
Set
-
HashSet
-
HashSet是基于哈希表实现的,哈希表是一种特殊的数据结构,是基于数组加链表实现的,所以它查询和增删都比较快,HashSet是无序,不可重复的。
-
因为HashSet是唯一的,为了保证唯一性我们需要重写实体类的hashCode()和equals()方法.
测试如下:
Person类
private int age; private String name; private int grade; private String address; public Person(int age, String name, int grade, String address) { this.age = age; this.name = name; this.grade = grade; this.address = address; } 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; } public int getGrade() { return grade; } public void setGrade(int grade) { this.grade = grade; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Person person = (Person) o; return age == person.age && grade == person.grade && Objects.equals(name, person.name) && Objects.equals(address, person.address); } @Override public int hashCode() { return Objects.hash(age, name, grade, address); } @Override public String toString() { return "Person{" + "age=" + age + ", name='" + name + '\'' + ", grade=" + grade + ", address='" + address + '\'' + '}'; }
没有重写hashCode()和equals()
public static void main(String[] args) { Person p1 = new Person(10 , "jie" , 89 , "金水区"); Person p2 = new Person(20 , "guo" , 90 , "新郑市"); Person p3 = new Person(19 , "zhang" , 100 , "二七区"); //p4和p5完全一样 Person p4 = new Person(18 , "jie" , 80 , "金水区"); Person p5 = new Person(18 , "jie" , 80 , "金水区"); HashSet<Person> hashSet = new HashSet<>(); hashSet.add(p1); hashSet.add(p2); hashSet.add(p3); hashSet.add(p4); hashSet.add(p5); hashSet.forEach(i -> System.out.println(i)); } /* Person{age=19, name='zhang', grade=100, address='二七区'} Person{age=18, name='jie', grade=80, address='金水区'} Person{age=20, name='guo', grade=90, address='新郑市'} Person{age=10, name='jie', grade=89, address='金水区'} Person{age=18, name='jie', grade=80, address='金水区'} */
重写了hashCode()和equals()
Person{age=10, name='jie', grade=89, address='金水区'} Person{age=18, name='jie', grade=80, address='金水区'} Person{age=19, name='zhang', grade=100, address='二七区'} Person{age=20, name='guo', grade=90, address='新郑市'}
-
为什么重写了equals()和hashCode()的方法两个对象hashSet就不会存储相同的了,这就涉及到hash表的存储过程了,接下来我们就来讨论一下hash表的存储过程
-
hash表的结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hcegw0Qj-1628963775638)(E:\我的桌面\hash表.png)]
-
第一步:
通过hashCode计算出哈希值
-
第二步
通过哈希值计算出在哈希表中的存储位置,也就是索引的位置,y=k(x)=x%数组长度
-
第三步
通过equals比较元素是否重复 解决了添加元素时冲突的问题
-
因为在添加元素时会先通过hash值进行比较确定在数组中的位置,接下来在通过equals进行比较去确定链表中的位置,所以相同的对象一定会有相同的哈希值和equals比较为true。拥有相同哈希值的两个对象却不一定拥有相同的equals
-
-
-
TreeSet
-
TreeSet是基于红黑树实现的,红黑树是平衡二叉树,他是有顺序的,基本数据类型它会对其进行自动排序,而引用数据类型可以通过外部排序器和内部排序器进行设定其排序规则,优先外部排序器再是内部排序器
比较规则:
如果返回的值是正数则表明前面的对象大于后面的
如果返回的值是负数则表明前面的对象小于后面的
如果返回的值是零则表明前面的对象等于后面的
外部排序器:
public class PersonComparator implements Comparator<Person> { @Override public int compare(Person o1, Person o2) { if (o1.getAge() == o2.getAge()) return 0; return (o1.getAge() > o2.getAge()) ? 1:-1; } }
内部排序器:
public class Person implements Comparable<Person> { private int age; private String name; private int grade; private String address; @Override public int compareTo(Person p) { return (this.getAge()>p.getAge()) ? 1:-1; } }
-
注意事项
String类可以直接进行比较,因为String类内部已经实现了Comparable接口
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { public int compareTo(String anotherString) { int len1 = value.length; int len2 = anotherString.value.length; int lim = Math.min(len1, len2); char v1[] = value; char v2[] = anotherString.value; int k = 0; while (k < lim) { char c1 = v1[k]; char c2 = v2[k]; if (c1 != c2) { return c1 - c2; } k++; } return len1 - len2; } }
小结一下:
-
当时用HashSet时实体类要重写hashCode和equals方法,才可以保证唯一性
-
TreeSet对于基本数据类型和String可以自动进行比较 , 对于引用数据类型可以通过内部比较器或者外部比较器设置比较规则进行比较,优先外部比较器其次内部比较器
-
Queue
从集合图中我们发现Queue的实现类只有一个LinkedList,LinkedList的特性在上面已经说过了,其中对头尾进行操作的特性就是队列的特性.其次Queue还有其他的实现类。例如在JUC包下的ArrayBlockingQueue等
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Glw19c0-1628963775640)(C:\Users\45423\AppData\Roaming\Typora\typora-user-images\image-20210815002045302.png)]
这些类也都实现了Queue接口,它们有四组不同的api对象线程池四种不同的拒绝策略,在以后的多线程中我会详细的讨论它们不同Api的区别.
map
首先map是一个双列集合,是通过key-value 进行存储的
map提供了判断key或者value是否存在的方法
//判断key是否存在
boolean containsKey(Object key);
//判断value是否存在
boolean containsValue(Object value);
map提供的通过key获取value的方法
V get(Object key);
添加元素的方法
V put(K key, V value);
添加一个map到集合中的方法
void putAll(Map<? extends K, ? extends V> m);
还有很多其他方法这里就不一一列举额大家可以通过API进行查看
下面我们来讨论讨论集合的四种遍历方式:
集合内部基本数据如下:
public static void main(String[] args) {
Map<String , String> map = new HashMap<>();
map.put("1" , "1");
} map.put("2" , "2");
- 方法一:
通过map提供的方法keySet();该方法会将map的key封装成一个Set集合进行返回,使用如下:
Set<String> set = map.keySet();
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()){
String key = iterator.next();
System.out.println("key"+"--->"+key+"value"+"--->"+map.get(key));
}
/*
测试结果如下:
key--->1value--->1
key--->2value--->2
*/
-
方法二:
通过map提供的values()方法,该方法会将value封装成一个collection集合进行返回,使用如下:
Collection<String> values = map.values();
values.forEach(i -> {
System.out.println(i);
});
/*
测试结果如下:
1
2
*/
- 方法三:
通过map提供的entrySet(),该方法会将每一个key和value封装成一个map存放到Set集合中,使用如下:
Set<Map.Entry<String, String>> entrySet = map.entrySet();
Iterator<Map.Entry<String, String>> iterator = entrySet.iterator();
while (iterator.hasNext()){
Map.Entry<String, String> next = iterator.next();
System.out.println("key---->"+next.getKey()+"value----->"+next.getValue());
}
测试结果如下:
key---->1value----->1
key---->2value----->2
*/
-
方法四:
最后我在map的最下面发现了forEach(BiConsumer<? super K, ? super V> action)方法,内心还是很震惊得.
里面和Collection的forEach()差不多,不过里面参数变成了两个,使用如下:
map.forEach((key , value) -> { System.out.println("key--->"+key+"value--->"+value); }); /* 测试结果如下: key--->1value--->1 key--->2value--->2 */
这几种遍历方式各位仁者见仁智者见智,自己可以选择一种自己比较喜欢的方式,我个人还是更倾向于使用forEach().
HashMap
说到HashMap就不得不提到JDK1.7和JDK1.8的差别,HashMap从1.7到1.8变化还是挺多的,我们从底层结构开始看。
- 首先HashMap在JDK1.7的时候使用的是数组+链表进行存储的,数组+链表这种结构是不是听着很熟悉,这不就是哈希表吗,其实HashSet在创建的时候直接就是调用了HashMap,HashSet就是HashMap的key.
public HashSet() {
map = new HashMap<>();
}
HashSet的add()方法直接就是调用的map的put方法
public boolean add(E e) {
//PRESENT 常量
return map.put(e, PRESENT)==null;
}
而HashMap在JDK1.8最大的变化就是引入了红黑树,引入红黑树的作用就是当链表过长时为了保持高效率将链表进行树化转换为红黑树。
- 为什么HashMap的初始容量是16 负载因子是0.75,二倍扩容呢?
这个问题我暂时还没搞明白,这个文章写的还可以大家可以看看.传送门
3.HashMap的put过程
- 通过key通过哈希算法与与运算计算出在数组中的位置
- 如果当前位置为空则将key和value封装成Node节点(1.7是Entry节点)存储起来
- 如果当前位置不为空
- 如果是1.7,则将整个链表进行遍历,如果存在key则直接进行覆盖,如果不存在则通过头插法插入
- 如果是1.8 则首先判断是红黑树还是链表
- 如果是链表的话,则将整个链表进行遍历,如果存在key则直接进行覆盖,如果不存在则通过尾插法插入,插入完成后,会查看当前链表的节点个数,如果大于8,则转换为红黑树
- 如果是红黑树的话,将key和value封装成一个红黑树节点插入到红黑树中,这个过程会遍历整个树,如果key存在的话,则更新value,如果不存在则直接进行插入
- 插入完成后,判断是否需要扩容.
总结
第一次写博客,写的不完美,希望这篇博客能够给你带来帮助!!!