Java 笔记 20
Collection
List
一、 List接口的实现类们:
- 1、Vector:动态数组
- 物理结构:数组
- 2、ArrayList:动态数组
- 物理结构:数组
- 3、Stack:栈,它是Vector的子类
- 物理结构:数组
- 4、LinkedList:双向链表
- 物理结构:链表
- 二、Vector和ArrayList的区别?(同时也是面试题)
- Vector:最早版本的动态数组(旧版),线程安全的(有线程同步的),不够后扩容为原来的2倍,初始容量:10,
-
Vector支持的遍历集合的方式有:(1)foreach(2)Iterator(3)支持旧版的Enumeration迭代器
- ArrayList:相对Vector来说新一点(新版),线程不安全的(没有线程同步的),不够后扩容为原来的1.5倍,初始容量:10,
-
ArrayList支持的遍历集合的方式有:(1)foreach(2)Iterator
- 2倍:造成空间浪费的可能性比较大
- 1.5倍:造成扩容的次数增大
- Vector和ArrayList的使用时,为了比较空间浪费,和扩容次数太多,如果能够预估大概的元素个数,那么可以用
- ArrayList(int initialCapacity)和Vector(int initialCapacity)直接初始化为一定容量的数组。
- StringBuffer和StringBuilder类似
- 三、Stack:栈
- 先进后出(FILO)或后进先出(LIFO:Last in first out)
- Stack是Vector的子类,比Vector多了几个方法,它的后进先出的特征,就是通过调用这几个方法实现的。
- (1)Object peek() :访问当前栈顶元素,但是不拿走栈顶元素
- (2)Object pop():弹出栈顶元素
- (3)Object push(Object item) :把元素压入栈顶,等价于add(item)
-
这里为了更形象化,单独设计了一个push。
- 四、LinkedList:双向链表(双向链表、队列、栈、双端队列、单向链表等各种数据结构的效果)
- 内部有一个结点的类型:
- class Node{
- Object data;
- Node previous;
- Node next;
- }
- class LinkedList{
- Node first;//记录第一个结点的地址
- Node last;//记录最后一个结点的地址
- }
- 空链表:if(first == null && last ==null)
- 有一个结点: first == last
- 第一个结点: first.previous = null
- 最后一个结点:last.next == null
- LinkedList可以被当做双向链表、栈、队列、双端队列等数据结构使用。
- 如何体现双向链表?
- (1)E getFirst()
- (2)E getLast()
- (3)boolean offerFirst(E e) :添加的第一个
- (4)boolean offerLast(E e) :添加到最后一个
- (5)int indexOf(Object o) :从first开始找
- (6)int lastIndexOf(Object o) :从last开始找
- (7) E get(int index)
-
先判断index是靠前还是靠后
- 如何体现栈?
- E peek()
- E pop()
- void push(E e)
- 如何体现队列?实现了Queue接口
- 队列:先进先出(FIFO)
- 抛出异常 返回特殊值
插入 add(e) offer(e)
移除 remove() poll()
检查 element() peek() - 如何体现双端队列?JDK1.6才支持,实现了Deque(double ended queue(双端队列)的缩写)
- 下表总结了上述 12 种方法:
第一个元素(头部) 最后一个元素(尾部)
抛出异常 特殊值 抛出异常 特殊值
插入 addFirst(e) offerFirst(e) addLast(e) offerLast(e)
移除 removeFirst() pollFirst() removeLast() pollLast()
检查 getFirst() peekFirst() getLast() peekLast()
ArrayList
- ArrayList动态数组:源码跟踪
- (1)new ArrayList():
- JDK1.8版本:发现内部初始化为了一个长度为0的空数组 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
- JDK1.7版本:也是初始化为长度为0的空数组 EMPTY_ELEMENTDATA;
- JDK1.6版本:初始化为长度为10的数组
- 为什么要初始化为空数组呢?
- 因为开发中,很多时候创建了ArrayList的对象,但是没有装元素,这个时候的话,如果初始化为10的数组,就浪费空间了。
- (2)add(Object e)
*JDK1.8 第一次添加元素,扩容为长度为10的数组
*JDK1.8 如果不够了,再扩容为1.5倍
Set
Collection是根接口,没有提供任何直接实现的。它有一些更具体的子接口,例如:List和Set。
*
- 一、Set接口
- 1、Set系列的集合的元素是不能重复的。
- 2、Set的实现类们:HashSet、TreeSet、LinkedHashSet
- Set:如果按照元素的存储顺序来说,有一些是可以保证的,有一些是不能保证的。唯有LinkedHashSet可以保证元素添加的顺序。
-
如果按照元素的大小顺序来说,有一些是可以保证的,有一些是不能保证的。唯有TreeSet可以保证元素的大小顺序。
- 其中HashSet:既不能保证添加顺序,也不能保证大小顺序。是完全无序的。
- List:有序的,可重复的
-
有序的:前后顺序,可能是[下标]索引顺序,也可以能是链表的next,prev的引用顺序。
- 3、Set接口没有增加方法,都是从Collection接口中继承的。
- 4、HashSet和LinkedHashSet
- HashSet:完全无序
- LinkedHashSet:添加顺序
- LinkedHashSet是HashSet的子类,比HashSet多维护了添加的顺序。
- 当你既想要实现集合的元素的不可重复性,又想要保证元素的添加顺序,就选择使用LinkedHashSet。否则就用List系列或HashSet。
- LinkedHashSet干的事多了,效率低了。
- 5、HashSet和TreeSet
- HashSet:完全无序
- TreeSet:大小顺序
- 当你需要元素不可重复,又要给元素排大小时,就用TreeSet。
- 要用到TreeSet,一定要用java.lang.Comparable或java.util.Comparator
- 6、如何保证元素不可重复的?
- 换句话说,如何判断两个元素是重复的呢?
- HashSet和LinkedHashSet:
-
①先比较hash值,如果hash值不一样,说明一定不相同,
-
②如果hash值一样,再调用equals方法比较
- TreeSet:
-
按照元素的大小来决定是否相同元素。
public class TestSet {
@Test
public void test12(){
//存储到TreeSet的元素不会用它的hashCode和equals
//认为大小“相等”就是相同的元素
TreeSet set = new TreeSet(new Comparator(){
@Override
public int compare(Object o1, Object o2) {
Student s1 = (Student) o1;
Student s2 = (Student) o2;
return s1.getId() - s2.getId();
}});
Student s1 = new Student(3,"张三");
Student s2 = new Student(1,"李四");
Student s3 = new Student(2,"王五");
set.add(s1);
set.add(s2);
set.add(s3);
System.out.println(set);
}
@Test
public void test11(){
//存储到HashSet的元素会用它的hashCode和equals
HashSet set = new HashSet();
Student s1 = new Student(3,"张三");
Student s2 = new Student(1,"李四");
Student s3 = new Student(2,"王五");
set.add(s1);
set.add(s2);
set.add(s3);
System.out.println(set);
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
System.out.println(s3.hashCode());
}
@Test
public void test10(){
//用匿名内部类实现了Comparator接口
TreeSet set = new TreeSet(new Comparator(){
@Override
public int compare(Object o1, Object o2) {
Student s1 = (Student) o1;
Student s2 = (Student) o2;
return s1.getId() - s2.getId();
}});
set.add(new Student(3,"张三"));
set.add(new Student(1,"李四"));
set.add(new Student(2,"王五"));
System.out.println(set);
}
@Test
public void test9(){
TreeSet set = new TreeSet();
//这里因为Object类型没有实现了java.lang.Comparable接口,所以不可以添加到TreeSet中
set.add(new Object());
}
@Test
public void test8(){
TreeSet set = new TreeSet();
//这里因为String类型实现了java.lang.Comparable接口,所以可以添加到TreeSet中
set.add("hello");
set.add("world");
set.add("java");
set.add("chailinyan");
}
//顺序:TreeSet大小顺序
@Test
public void test7(){
TreeSet set = new TreeSet();
set.add("hello");
set.add("world");
set.add("java");
set.add("chailinyan");
System.out.println(set);//[chailinyan, hello, java, world]
}
//顺序:LinkedHashSet添加顺序
@Test
public void test6(){
LinkedHashSet set = new LinkedHashSet();
set.add("hello");
set.add("world");
set.add("java");
set.add("chailinyan");
System.out.println(set);//[hello, world, java, chailinyan]
}
//顺序:HashSet无序
@Test
public void test5(){
HashSet set = new HashSet();
set.add("hello");
set.add("world");
set.add("java");
set.add("chailinyan");
System.out.println(set);//[world, java, chailinyan, hello]
}
//顺序:HashSet无序
@Test
public void test4(){
HashSet set = new HashSet();
set.add("张三");
set.add("李四");
set.add("王五");
set.add("柴林燕");
System.out.println(set);//[李四, 柴林燕, 张三, 王五]
}
//不可重复
@Test
public void test2(){
LinkedHashSet set = new LinkedHashSet();
set.add("张三");
set.add("张三");
set.add("李四");
System.out.println(set);
}
//不可重复
@Test
public void test3(){
TreeSet set = new TreeSet();
set.add("张三");
set.add("张三");
set.add("李四");
System.out.println(set);
}
//不可重复
@Test
public void test1(){
HashSet set = new HashSet();
set.add("张三");
set.add("张三");
set.add("李四");
System.out.println(set);
}
}
class Student{
private int id;
private String name;
public Student(int id, String name) {
super();
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Student [id=" + id + ", name=" + name + "]";
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int hashCode() {
System.out.println("Student的hashCode方法");
/*final int prime = 31;
int result = 1;
result = prime * result + id;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;*/
return 1111;
}
@Override
public boolean equals(Object obj) {
System.out.println("Student的equals方法");
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Student other = (Student) obj;
if (id != other.id)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
- Set的底层实现是什么?
- 1、HashSet : HashMap
- 2、TreeSet : TreeMap
- 3、LinkedHashSet: LinkedHashMap
- Set添加元素时用add(元素),而Map添加元素put(key,value)。
- 发现添加到Set中的元素,是作为底层的Map的key,那么value它们选用了一个Object类型的常量对象PRESENT。
- 所有的HashSet共用同一个PRESENT对象。
- 所有的TreeSet共用同一个PRESENT对象。
- 所有的LinkedHashSet共用同一个PRESENT对象。
Map
1、集合分为两大家族:
- (1)Collection:一组对象,单身party
- List和Set
- (2)Map:一组键值对,映射关系,情侣party
- 2、Map接口
- (1)API
- put(Object key, Object value)
- putAll(Map m):将另一个m中是所有的映射关系都添加到当前map中
- boolean containsKey(Object key) :是否包含某个key
- boolean containsValue(Object value) :是否包含某个value
- boolean isEmpty():是否为空
- Object get(Object key):根据key获取它的value
- void clear() :清空所有
- Object remove(Object key) :根据key删除一对映射关系,并且返回其中的value
- int size()
- 遍历map:
- Map接口没有继承java.lang.Iterable接口,所以不支持直接使用foreach循环进行遍历。
- Map接口中也没有提供Iterator iterator()方法返回迭代器对象。
- (1)Set keySet()
- 获取所有的key,然后遍历它们
- 这里所有的key组成了一个Set集合,因为它们不可重复
- (2)Collection values()
- 获取所有的value,然后遍历它们
- 这里所有的value组成了一个Collection系列的集合,可能重复,也可能不重复
- (3)Set entrySet()
- 获取所有的映射关系,然后遍历它们。此时把一对映射关系(key,value)看成一个整体,是Entry类型的对象。
- 因为key不可重复,那么所有的组合也就唯一了,所以所有的映射关系也是set集合
- (2)说明:任意的引用数据类型都可以作为key和value。
- 虽然key也可以是任意类型的对象,但是习惯上,或者说我们遇到的最多的key的类型是:String和Integer。
- 因为String和Integer比较简洁,而且对象不可变。
一、Map接口的实现类们:
- 1、HashMap
- 2、Hashtable
- 3、TreeMap
- 4、LinkedHashMap
- 5、Properties
- 二、HashMap和Hashtable 哈希表
- Hashtable:旧版。线程安全的。它的key和value不能为null。
- HashMap:相对Hashtable它来说新一点。线程不安全。它允许key和value为null值。
- StringBuffer和StringBuilder
- Vector和ArrayList
- Hashtable和HashMap
- 三、HashMap和LinkedHashMap
- LinkedHashMap是HashMap的子类,比HashMap多维护了映射关系的添加顺序。
- HashMap:无序的。
- LinkedHashMap:可以记录添加顺序。
- LinkedHashMap比HashMap要做的事多,效率低。只在需要维护顺序时再使用它。
- 四、HashMap和TreeMap
- HashMap:无序的。
- TreeMap:按照key排大小顺序。
- 五、Properties
- Properties是Hashtable的子类,不允许key和value是null,并且它的key和value的类型都是String。
- 通常用于存储配置属性。
- 而且为了可读性更好,还增加了两个方法:
- setProperty(key,value)
- String getProperty(key)
- 六、所有的map的key不能重复,如何实现不重复?
- HashMap、Hashtable、LinkedHashMap、Properties:依据key的hashCode和equals方法
- TreeMap:依据key的大小,认为大小相等的两个key就是重复的
- 如果key重复了,那么后面的value会替换原来的value。
- TreeMap要让key排大小,要么key类型本身实现了java.lang.Comparable接口,要么在创建TreeMap时,指定一个java.util.Comparator接口的实现类对象。
Map的底层实现是什么?
- 1、哈希表系列:
-
数组 + 链表
-
数组 + 链表/红黑树
- 2、TreeMap:红黑树
- HashMap的底层实现:
-
JDK1.7以及之前:数组 + 链表
-
JDK1.8以及之后:数组 + 链表/红黑树
- 数组的优点:访问速度快,因为可以根据下标直接定位到某个元素
- 链表的优点:不需要元素是挨着存储,不需要连续空间,在添加和删除元素时不需要移动元素,只需要修改前后元素的引用关系就可以。
-
HashMap:会根据key的hashCode-->公式/算法-->[index]
-
因为不同的hashCode值,可能得到的[index]是相同的,那么此时就冲突了,那么只能把[index]的多个映射关系用链表连接起来
- 二叉树的优点:查找的速度比链表快
-
旧版的HashMap,如果key的hashCode算出了[index]相同的话(我们称为冲突)都在一个table[index]下面,
-
如果严重的话,会导致[index]下面的链表很长,就会导致查询速度减慢。当链表长到一定程度时,就需要把链表变为二叉树,以提高我们查询速度。
public V put(K key, V value) {
if (table == EMPTY_TABLE) {//判断table数组是否是空数组
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);//根据key的hashCode用异或,无符号右移等各种运算,得到了一个int类型的hash值
因为我们下面要用hash值来算[index],它的设计者认为用户重写的hashCode可能不够散列。
int i = indexFor(hash, table.length); // hash & table.length-1
/*
table数组的长度一定是2的n次方
table.length-1 的二进制是前面都是0,后面都是连续的1
hash & table.length -1 做按位与运算的结果一定是 在[0,table.length-1]范围内
*/
//查找table[i]下面的链表中是否有映射关系的key是和我重复的,如果有重复的,就用新value替换旧value
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//当元素的总个数达到阈值 && 并新的映射关系要添加的table[index]下面不是空的
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//数组扩容为原来的2倍
hash = (null != key) ? hash(key) : 0;//重新算hash
bucketIndex = indexFor(hash, table.length);//index也重新算
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
//把table[index]下面原来的Entry连接到新的Entry的next中
size++;
}
一、JDK1.6的HashMap:数组+链表
- (1)new HashMap()
- table数组初始化为了一个长度为16的空数组,threshold=12
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;//0.75
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);//12
table = new Entry[DEFAULT_INITIAL_CAPACITY];//16
} - 二、几个常量与变量的介绍
- 1、DEFAULT_INITIAL_CAPACITY:16
- 2、DEFAULT_LOAD_FACTOR:0.75 默认加载因子
- 3、threshold:阈值/临界值 数组需要考虑扩容的阈值
-
threshold = capacity(容量,数组的长度) * load factor(加载因子,默认是0.75)
-
例如: threshold = 16 * 0.75 当数组大概3/4满的时候就考虑扩容
- 思考:load factor设置为0.9和0.1有什么区别?
-
0.1:扩容太频繁
-
0.9:会导致table[index]下面的链表会很长,查询速度就低
- 三、JDK1.7的HashMap:数组+链表
- 1、new HashMap()
- table数组初始化为了一个长度为0的空数组
- 2、 put()
- 步骤:
- (1)发现数组table是空数组后,会把数组初始化为长度为16的Entry类型的数组,并且把threshold计算为12
- 这里如果你手动指定了数组的capacity,那么如果这个capacity不是2的n次方,会自动纠正为2的n次方
- 为什么要纠正为2的次方?
- ①后面算index = hash & table.length-1,这样才能保证[0,table.length-1]范围内
- ②2的次方,根据它的散列算法,可以保证比较均匀的分散在它的数组的各个位置
- (2)如果key是null,特殊对待,key为null的映射关系的hash值为0,index也为0
- (3)hash = hash(key)
- 为了干扰我们key的hashCode值
- (4)index = hash & table.length-1
- (5)先判断table[index]下面是否有映射关系的key是和我新添加的映射关系的key有重复的,如果有,就用新的value替换旧的value,就结束了
- (6)如果没有重复的,决定添加新的映射关系
- ①看是否需要扩容
- 扩容的条件:A:size达到阈值threshold B:table[index]下面已经有映射关系,即不为空
- 如果扩容了,会重新计算hash和index
- ②把新的映射关系new为一个Entry的对象,放到table[index]中,原来table[index]的映射关系作为新的映射关系的next连接起来。
- Entry相当于是一个结点类型。是一个单向链表的结点类型。
- class Entry{
-
int hash;
-
Object key;
-
Object value;
-
Entry next;
- }
JDK1.8的HashMap的底层实现:数组+链表/红黑树 - 几个常量和变量:
- (1)DEFAULT_INITIAL_CAPACITY:默认的初始容量 16
- (2)MAXIMUM_CAPACITY:最大容量 1 << 30
- (3)DEFAULT_LOAD_FACTOR:默认加载因子 0.75
- (4)TREEIFY_THRESHOLD:默认树化阈值8,当链表的长度达到这个值后,要考虑树化
- (5)UNTREEIFY_THRESHOLD:默认反树化阈值6,当树中的结点的个数达到这个阈值后,要考虑变为链表
- (6)MIN_TREEIFY_CAPACITY:最小树化容量64
-
当单个的链表的结点个数达到8,并且table的长度达到64,才会树化。
-
当单个的链表的结点个数达到8,但是table的长度未达到64,会先扩容
- (7)Node<K,V>[] table:数组
- (8)size:记录有效映射关系的对数,也是Entry对象的个数
- (9)int threshold:阈值,当size达到阈值时,考虑扩容
- (10)double loadFactor:加载因子,影响扩容的频率
- 1、new HashMap()
- public HashMap() {
-
}//加载因子赋值为0.75 this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted 其他字段都是默认值 //threshold是0 //table=null //size = 0
- 2、put(key,value)
- (1)如果第一次添加时
- 把table初始化为长度为16的数组,threshold = 12
- (2)如果不是第一次添加
- ①会考虑是否key有重复,那么就替换value
- ②如果table[i]下面不是树,统计table[i]的结点的个数,添加之前达到7个,考虑树化
-
当单个的链表的结点个数添加之前达到7,并且table的长度达到64,才会树化。
-
当单个的链表的结点个数添加之前达到7,table的长度未达到64,先扩容。
- ③table[i]下面已经是树,单独处理,直接把新的映射关系连接到树的叶子结点
- ④添加后,size达到threshold,还要扩容
- 一旦扩容,就会调整所有映射关系的位置