集合就是可以动态存放多个对象的容器
数组在存储数据方面的弊端:
数组长度不可变,不便于扩展
集合提供了多种方法操作集合,这是数组不具备的
数组存储数据类型单一
集合的使用场景
- Collection 接口是 List、Set 和 Queue 接口的父接口
- 在 Java5 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 JDK 5.0 增加了泛型以后,Java 集合可以记住容器中对象的数据类型。
注意:向Collection接口的实现类的对象中添加obj,建议要重写该obj类的equals方法!!(否则会遇到大麻烦)toString方法也要重写
添加
add(Object obj)
addAll(Collection coll)
获取有效元素的个数:int size()
清空集合:void clear()
是否是空集合:boolean isEmpty()
是否包含某个元素:
boolean contains(Object obj)
:是通过元素的equals方法来判断是否是同一个对象boolean containsAll(Collection c)
:也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较。
删除:
boolean remove(Object obj)
:通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素boolean removeAll(Collection coll)
:通过调用equals方法,取当前集合的差集(删除交集)
取两个集合的交集:boolean retainAll(Collection c)
:把交集的结果存在当前集合
集合是否相等:boolean equals(Object obj)
,object若不是Collection,一定返回false
转成对象数组:Object[] toArray()
获取集合对象的哈希值:hashCode()
遍历:iterator()
:返回迭代器对象,用于集合遍历
asList方法
//拓展:数组 --->集合:调用Arrays类的静态方法asList()
List<String> list = Arrays.asList(new String[]{"AA", "BB", "CC"});
System.out.println(list);
List arr1 = Arrays.asList(new int[]{123, 456});
System.out.println(arr1.size());//1,认为只有一个元素--arr1的元素类型为数组,解决办法看下面:
List arr2 = Arrays.asList(new Integer[]{123, 456});
System.out.println(arr2.size());//2
说说下面输出结果:
@Test
public void test1(){
//用ArrayList来展示Collection接口的方法
Collection coll = new ArrayList();
//add()的用法
coll.add(123);
coll.add(456);
coll.add(new Person("Jerry",20));
coll.add(new String("Tom"));
coll.add(false);
//contains(Object obj):判断当前集合中是否包含obj
//我们在判断时会调用obj对象所在类的equals()。
System.out.println(coll.contains(123));//此处调用Integer重写的equals()方法
System.out.println(coll.contains(new String("Tom")));//此处调用String重写的equals()方法
System.out.println(coll.contains(new Person("Jerry",20)));//调用Person中的equals()方法,没有重写就相当于比较地址值,此处重写的equals再第一行加入System.out.println("Person equals()....");
//containsAll(Collection coll1):判断形参coll1中的所有元素是否都存在于当前集合中。
Collection coll1 = Arrays.asList(123,4567);
System.out.println(coll.containsAll(coll1));
}
输出结果:
思考为什么输出了三个Person equals()…(提示信息:Person类中重写equals()方法中,只要调用一次equals方法,就输出一次Person equals()…)
而输出三次Person equals()…是由于分别和123,456和new Person(“Jerry”,20)比对三次导致的
Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素
java.lang.Iterable
接口,该接口有一个iterator()
方法,那么所有实现了Collection接口的集合类都有一个iterator()
方法,用以返回一个实现了Iterator接口的对象在调用
it.next()
方法之前必须要调用it.hasNext()
进行检测。若不调用,且下一条记录无效,直接调用it.next()
会抛出NoSuchElementException
异常。
hasNext()+Next()
://hasNext():Returns true if the iteration has more elements.
while(iterator.hasNext()){
//next():Returns the next element in the iteration
System.out.println(iterator.next());
}
remove()
方法//测试Iterator中的remove()
//如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,
//再调用remove都会报IllegalStateException。
@Test
public void test3(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new Person("Jerry",20));
coll.add(new String("Tom"));
coll.add(false);
//删除集合中"Tom"
Iterator iterator = coll.iterator();
while (iterator.hasNext()){
Object obj = iterator.next();
if("Tom".equals(obj)){
iterator.remove();
}
}
//遍历集合
iterator = coll.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
Iterator可以删除集合的元素,但是是遍历过程中通过迭代器对象的remove方法,不是集合对象的remove方法。
如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,再调用remove都会报IllegalStateException。
foreach也叫增强for循环,用于遍历集合或数组
练习:判断输出结果:
public class ForTest {
public static void main(String[] args) {
String[] str = new String[5];
for (String myStr : str) {
myStr = "atguigu";
System.out.println(myStr);
}
System.out.println(Arrays.toString(str));
}
}
三种List的实现类分析:
- ArrayList:作为List接口的主要实现类;线程不安全的,效率高;底层使用Object[] elementData存储
- LinkedList:对于频繁的插入、删除操作,使用此类效率高;底层使用双向链表存储。线程不安全
- Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储
List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。
void add(int index, Object ele)
:在index位置插入ele元素
boolean addAll(int index, Collection eles)
:从index位置开始将eles中的所有元素添加进来
Object get(int index)
:获取指定index位置的元素
上面Iterator中讲述了两种遍历Collection的方法,对于List可以结合size()方法和get()方法直接用for循环进行遍历
int indexOf(Object obj)
:返回obj在集合中首次出现的位置
int lastIndexOf(Object obj)
:返回obj在当前集合中末次出现的位置
Object remove(int index)
:移除指定index位置的元素,并返回此元素(重载Collection接口的remove,Collection中的根据对象删除任然能使用)
Object set(int index, Object ele)
:设置指定index位置的元素为ele
List subList(int fromIndex, int toIndex)
:返回从fromIndex到toIndex位置的子集合
简单来记忆就是增,删,改,查,插,长度,遍历
ArrayList是动态数组
ArrayList的JDK1.8之前与之后的实现区别?
JDK1.7:初始状态创建长度为10的数组,空间不足则扩容为原来的1.5倍并将数据迁移
- ArrayList list = new ArrayList();//底层创建了长度是10的Object[]数组elementData
- list.add(123);//elementData[0] = new Integer(123);
- list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。
默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。
结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)
JDK1.8:首次创建长度为0的数组,首次添加元素,创建长度为10的数组,后续扩容也是1.5倍并数据迁移
- ArrayList list = new ArrayList();//底层Object[] elementData初始化为{}.并没有创建长度为10的数组、
- list.add(123);//第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData[0]------后续的添加和扩容操作与jdk 7 无异。
小结:jdk7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk8中的ArrayList的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。
Arrays.asList(…) 方法返回的 List 集合,既不是 ArrayList 实例,也不是Vector 实例。Arrays.asList(…) 返回值是一个固定长度的 List 集合
频繁插入删除操作适合使用LinkedList。底层是双向链表存储
相较于与ArrayList新增方法:头插,尾插,头节点,尾节点,头删,尾删
- void addFirst(Object obj)
- void addLast(Object obj)
- Object getFirst()
- Object getLast()
- Object removeFirst()
- Object removeLast()
LinkedList:底层为双向链表,定义了Node类型的first和last,用于记录首末元素。同时,定义内部类Node,作为LinkedList中保存数据的基本结构。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;
}
}
Vector大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。
ArrayList是用的最多的。但LinkedList插入删除效率高。Vector使用的很少
新增方法:
请问ArrayList/LinkedList/Vector的异同?谈谈你的理解?ArrayList底层是什么?扩容机制?Vector和ArrayList的最大区别?
- ArrayList和LinkedList的异同二者都线程不安全,相对线程安全的Vector,执行效率高。此外,ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。
- ArrayList和Vector的区别Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制(使用工具类,将ArrayList变为线程安全的)。Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。Vector还有一个子类Stack。
程序题:remove调用的是Collection中的方法还是List中的方法:
@Test
public void testListRemove() {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
updateList(list);
System.out.println(list);//
}
private static void updateList(List list) {
list.remove(2);
}
本题考查的是到底条用的remove是Collection中的方法,还是List中的方法。Collection中的remove是按equals方法匹配,List中的remove则是按照下标匹配
调用方法从当前类匹配,没有匹配方法才会到父类中找。所以List中的remove匹配上就调用了呗,就不会去父类中找了
若想要调用Collection的方法,那么就直接传进Integer的参数即可
Set接口是Collection的子接口,set接口没有提供额外的方法
List接口有序可重复,而Set接口无序不重复。Set完全相当于高中数学中集合的概念。
无序性:不等于随机性。所谓无序性指的是存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的。
不可重复性:保证添加的元素按照equals()判断时,不能返回true.即:相同的元素只能添加一个。
HashSet是Set接口的实现类,用的很多。HashSet 不是线程安全的,集合元素可以是 null
HashSet 集合判断两个元素相等的标准: hashCode() 方法和equals() 方法返回值均相等才判断两个对象相等。
对于存放在Set容器中的对象,对应的类一定要重写equals()
和hashCode(Object obj)
方法,以实现对象相等规则。
当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据 hashCode 值,通过某种散列函数决定该对象在 HashSet 底层数组中的存储位置。(这个散列函数会与底层数组的长度相计算得到在数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布,该散列函数设计的越好)
对于添加成功的情况2和情况3而言:元素a与已经存在指定索引位置上数据以链表的方式存储。七上八下
底层也是数组,初始容量为16,当如果使用率超过0.75,(16*0.75=12)就会扩大容量为原来的2倍。(16扩容为32,依次为64,128…等)
其实多数情况下就自动生成,没有猪b会自己写
重写hashCode()方法的基本原则
- 同一对象多次调用 hashCode()方法应该返回相同的值。
- 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值也应相等。
- 对象中用作 equals() 方法比较的字段,都应该用来计算 hashCode 值。
重写equals()方法的基本原则
重写equals方法的时候一般都需要同时复写hashCode方法。通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。
当自定义的类有自己特定的”逻辑相等“的概念,就需要重写equals方法。由于相等的对象必须要有相等的散列码(哈希别称)。如果没有重写hashCode(),就调用Object中的hashCode()。根据Object.hashCode()方法,它们仅仅是两个对象。显然违反了“相等的对象必须具有相等的散列码”。所以equals和hashCode往往是一起重写的
Eclipse/IDEA工具里hashCode()的重写
以Eclipse/IDEA为例,在自定义类中可以调用工具自动重写equals和hashCode。
问题:为什么用Eclipse/IDEA复写hashCode方法,有31这个数字?
- 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)
- 并且31只占用5bits,相乘造成数据溢出的概率较小。
- 31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)
- 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除!(减少冲突)
LinkedHashSet 是 HashSet 的子类
TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态(所以TreeSet中元素必须是相同类型)。
TreeSet底层使用红黑树结构存储数据
新增的方法如下: (了解)
- Comparator comparator()
- Object first()
- Object last()
- Object lower(Object e)
- Object higher(Object e)
- SortedSet subSet(fromElement, toElement)
- SortedSet headSet(toElement)
- SortedSet tailSet(fromElement)
TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序
TreeSet和后面要讲的TreeMap采用红黑树的存储结构
自然排序:TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列
如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable 接口。(回忆之前HashSet,若将对象添加到HashSet中必须重写HashCode和equals)
实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过compareTo(Object obj) 方法的返回值来比较大小。
Comparable 的典型实现:(回忆常用类)
- BigDecimal、BigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小进行比较
- Character:按字符的 unicode值来进行比较
- Boolean:true 对应的包装类实例大于 false 对应的包装类实例
- String:按字符串中字符的 unicode 值进行比较
- Date、Time:后边的时间、日期比前面的时间、日期大(因为时间对应的时到1970年1月1日0时0分0秒的毫秒数,数字大自然就大)
向 TreeSet 中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较。
因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类的对象。
对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(Object obj) 方法比较返回值。不同于Set的其他接口使用equals比较是否相等
当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 方法时,应保证该方法与 compareTo(Object obj) 方法有一致的结果:如果两个对象通过equals() 方法比较返回 true,则通过 compareTo(Object obj) 方法比较应返回 0。否则,让人难以理解。(一般不会出问题的)
可以发现List和Set都建议重写equals方法,List的contain方法会调用,set本身不重复也会调用。相应的toString方法也最好重写。特殊的HashSet需要重写HashCode方法用于比较是否相等
TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。
定制排序,通过Comparator接口来实现。需要重写compare(T o1,T o2)方法。
更具体的可以参考常用类中的比较器的讲解
hashCode()
和equals()
方法(Set使用hashCode和equals来保证无序不重复)HashMap
、TreeMap
、LinkedHashMap
和Properties
。其中,HashMap是 Map 接口使用频率最高的实现类Map的实现类的结构:
|----Map:双列数据,存储key-value对的数据 ---类似于高中的函数:y = f(x)
|----HashMap:作为Map的主要实现类;线程不安全的,效率高;存储null的key和value
|----LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。
原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。
对于频繁的遍历操作,此类执行效率高于HashMap。
|----TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序
底层使用红黑树
|----Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
|----Properties:常用来处理配置文件。key和value都是String类型
Object put(Object key,Object value)
:将指定key-value添加到(或若重复则修改)当前map对象中(Collection中是add)
void putAll(Map m)
:将m中的所有key-value对存放到当前map中
Object remove(Object key)
:移除指定key的key-value对,并返回value
如果不存在该键值对,返回null
void clear()
:清空当前map中的所有数据(内部数组元素置null)
Object get(Object key)
:获取指定key对应的value
boolean containsKey(Object key)
:是否包含指定的key
boolean containsValue(Object value)
:是否包含指定的value
int size()
:返回map中key-value对的个数(键值对个数)
boolean isEmpty()
:判断当前map是否为空(内部用isEmpty来判断是否为空)
boolean equals(Object obj)
:判断当前map和参数对象obj是否相等
Set keySet()
:返回所有key构成的Set集合
Collection values()
:返回所有value构成的Collection集合
values和keySet的遍历顺序是一一对应的
案例:map集合中的
value
为List
类型,如何将map
的value
放入List
类型变量中//返回 map 中存放的所有 T 对象 public List<T> list(){ //错误的: //Collection
values = map.values(); //return (List) values; //正确的: ArrayList<T> list = new ArrayList<>(); Collection<T> values = map.values(); for(T t : values){ list.add(t); } return list; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
回顾类型转换,用父类引用指向子类实例,这个是多态。父类类型转为子类类型,这个是强制类型转换,但是强制类型转换要求instanceof为true,可以说这个强制类型转换只是还原其本身的面目,而非基本数据类型理解的那种强制类型转换
这里value本身就是Collection,不能强制类型转换为List
Set entrySet()
:返回所有key-value对构成的Set集合(集合原色为kv键值对)
集合为Emtry类型,在Emtry内部有相应的方法获得相应的key和value
遍历Map
@Test
public void test5(){
Map map = new HashMap();
map.put("AA",123);
map.put(45,1234);
map.put("BB",56);
//遍历所有的key集:keySet()
Set set = map.keySet();
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
System.out.println();
//遍历所有的value集:values()
Collection values = map.values();
for(Object obj : values){
System.out.println(obj);
}
System.out.println();
//遍历所有的key-value
//方式一:entrySet()
Set<Map.Emtry<String,Integer>> entrySet = map.entrySet();
Iterator<Map.Emtry<String,Integer>> iterator1 = entrySet.iterator();
while (iterator1.hasNext()){
Object obj = iterator1.next();
//entrySet集合中的元素都是entry
Map.Entry entry = (Map.Entry) obj;//Emtry是接口!!
System.out.println(entry.getKey() + "---->" + entry.getValue());
}
System.out.println();
//方式二:
Set keySet = map.keySet();
Iterator iterator2 = keySet.iterator();
while(iterator2.hasNext()){
Object key = iterator2.next();
Object value = map.get(key);
System.out.println(key + "=====" + value);
}
}
总结:增删改查长度遍历,插?插p无序插der
/*
* 添加:put(Object key,Object value)
* 删除:remove(Object key)
* 修改:put(Object key,Object value)
* 查询:get(Object key)
* 长度:size()
* 遍历:keySet() / values() / entrySet()
*/
类似HashSet,HashSet将存储的值映射到底层数组。HashMap将键值对的键映射到底层数组,存储键值对,过程类似HashSet。
JDK7及以前—数组加链表
JDK8-数组加链表加红黑树(开始存储链表,超过一定长度转换为红黑树提高查询效率,小于一定长度转回链表)
DEFAULT_INITIAL_CAPACITY
: HashMap的默认容量,16
MAXIMUM_CAPACITY
: HashMap的最大支持容量 230
DEFAULT_LOAD_FACTOR
:HashMap的默认加载因子
TREEIFY_THRESHOLD
:Bucket中链表长度大于该默认值,转化为红黑树
UNTREEIFY_THRESHOLD
:Bucket中红黑树存储的Node小于该默认值,转化为链表
MIN_TREEIFY_CAPACITY
:桶中的Node被树化时最小的hash表容量。(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作,这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。)
table
:存储元素的数组,总是2的n次幂
entrySet
:存储具体元素的集
size
:HashMap中存储的键值对的数量
modCount
:HashMap扩容和结构改变的次数。
threshold
:扩容的临界值,=容量*填充因子
loadFactor
:填充因子
HashMap的内部存储结构其实是 数组和链表的结合 ** 。当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry类型的数组table**,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。(数组加链表)
而且新添加的元素作为链表的head。(七上八下)
//默认构造器,底层调用双参数的构造器
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);
}
//设置初始容量的Hashmap,底层调用双参数的构造器,值得注意容量只会是2的整数倍。指定容量为15,那么容量就会是16
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//双参数构造器,第一个参数指代容量,第二个参数指代加载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
这里以put为例讲解
向HashMap中添加entry(key1,value1),需要首先计算key1的哈希值(根据key1所在类的hashCode()计算得到,此哈希值经过处理以后(散列映射),得到在底层Entry[]数组中要存储的位置i。
**如果equals()返回false:此时key1-value1添加成功。----情况3**
**如果equals()返回true:使用value1替换value2。**
不同于HashSet中的key值相等则添加失败,HashMap若key相等则替换
//put源码
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);//将null添加到Hashmap
int hash = hash(key);//计算哈希值
int i = indexFor(hash, table.length);//计算Hashmap存放位置
//
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果相等,就将value值取代
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);//内部调用resize存在扩容,后一部分有讲
return null;
}
值得关注为什么put有一个引用类型变量,这是因为在hashset中的add方法调用的就是hashmap的put方法,判断put方法的返回值是否等于空,来保证hashset的值不重复。
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高(桶bucket存放的链表变长),因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算(扩容后,数据正确存放位置发生变化,取模)
其在新数组中的位置,并放进去,这就是resize
。
那么HashMap什么时候进行扩容呢?
当HashMap中的元素个数超过 length*loadFactor
时(数组总大小length,不是数组中有效数据的个数size) , 就会进行数组 扩容,loadFactor 的默认值 (DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16*0.75=12
(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32
,即扩大一倍(源码是移位运算),然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能
HashMap的内部存储结构其实是 数组+链表+树的结合 。当实例化一个HashMap时,会初始化initialCapacity和loadFactor,在put第一对映射关系时,系统会创建一个长度为initialCapacity的Node数组(Entry另有用途了用到LinkedHashMap中去了),这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链。
也可能是一个一个TreeNode对象(红黑树存储),每一个TreeNode对象可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。
那么HashMap什么时候进行扩容和树形化呢?
当HashMap中的元素个数超过
length*loadFactor
时 (数组总大小length,不是数组中个数size), 就会进行数组扩容 , loadFactor 的默认值(DEFAULT_LOAD_FACTOR)为0.75
.这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过
16* 0.75=12
(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
总结:JDK1.8相较于之前的变化:
面试题:负载因子值的大小,对HashMap有什么影响
首先,让我们看看 HashMap 是如何存储键值对的。HashMap
内部使用 Node
类型来维护键值对:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
...
}
如我们所见,键声明有 final 关键字。因此,我们不能直接修改key值。
但是我们可以使用remove()然后put(),以达到修改键的目的。
假设我们在 HashMap
中有键值对 K1 -> V
,我们希望修改为K2 -> V
。不能直接将K1修改为K2。但是我们也可以删除 K1 -> V
,并添加 K2 -> V
,达到一样的效果。操作如下:
Map<String, Integer> playerMap = new HashMap<>();
playerMap.put("Kai", 42);
playerMap.put("Amanda", 88);
playerMap.put("Tom", 200);
// 用Eric替换Kai
playerMap.put("Eric", playerMap.remove("Kai"));
assertFalse(playerMap.containsKey("Kai"));
assertTrue(playerMap.containsKey("Eric"));
assertEquals((Integer)42, (Integer)playerMap.get("Eric"));
尽管我们的问题已经解决了,但还有一个潜在的问题。我们知道 HashMap
的键是一个 final 变量。所以,我们不能重新分配变量。但是我们可以修改一个 final对象的值。在我们的 playerMap 示例中,键是 String。我们不能改变它的值,因为字符串是不可变的。但是如果它是一个可变对象,我们可以通过修改键来解决问题吗?
首先,我们不应该在 Java 的 HashMap 中使用一个可变对象作为键,因为这可能导致潜在的问题和意外的行为。
这是因为 HashMap
中的键对象用于计算一个哈希码,该哈希码决定了相应的值将被存储在哪个桶中。如果键是可变的并且在被用作 HashMap
中的键之后被更改,哈希码也可以更改。结果,我们将无法正确检索与键关联的值,因为它将位于错误的桶中。
接下来,让我们通过一个例子来理解它。
首先,创建一个只有一个属性的Player类:
class Player {
private String name;
public Player(String name) {
this.name = name;
}
// 省略了getter和setter方法
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Player)) {
return false;
}
Player player = (Player) o;
return name.equals(player.name);
}
@Override
public int hashCode() {
return name.hashCode();
}
}
如我们所见,Player
类在 name
属性上有一个 setter
,所以它是可变的。此外,hashCode()
方法使用 name
属性来计算哈希码。这意味着更改 Player
对象的名字可以使它具有不同的哈希码。
接下来,让我们创建一个 map,并在其中放入一些条目,使用 Player
对象作为键:
Map<Player, Integer> myMap = new HashMap<>();
Player kai = new Player("Kai");
Player tom = new Player("Tom");
Player amanda = new Player("Amanda");
myMap.put(kai, 42);
myMap.put(amanda, 88);
myMap.put(tom, 200);
assertTrue(myMap.containsKey(kai));
接下来,让我们将玩家 kai 的名字从 “Kai” 更改为 “Eric”,然后验证我们是否可以得到预期的结果:
// 将Kai的名字更改为Eric
kai.setName("Eric");
assertEquals("Eric", kai.getName());
Player eric = new Player("Eric");
assertEquals(eric, kai);
// 现在,map中既不包含Kai也不包含Eric:
assertFalse(myMap.containsKey(kai));
assertFalse(myMap.containsKey(eric));
如上面的测试所示,更改 kai 的名字为 “Eric” 后,我们无法再使用 kai 或 eric 检索 “Eric” -> 42 的 Entry。然而,对象 Player(“Eric”) 存在于 map 中作为一个键(因为它不在它该在的位置mother fucker):
// 虽然Player("Eric")存在:
long ericCount = myMap.keySet()
.stream()
.filter(player -> player.getName()
.equals("Eric"))
.count();
assertEquals(1, ericCount);
要理解为什么会这样,我们首先需要了解 HashMap
是如何工作的。
HashMap
维护一个内部哈希表来存储添加到 map 中的键的哈希码。一个哈希码引用一个 map 条目。当我们检索一个条目时,例如通过使用 get(key)方法,HashMap
计算给定键对象的哈希码,并在哈希表中查找哈希码。
在上面的例子中,我们将 kai(“Kai”) 放入 map 中。所以,哈希码是基于字符串“Kai”计算的。HashMap
存储了结果,让我们说 “hash-kai”,在哈希表中。后来,我们将 kai(“Kai”) 更改为 kai(“Eric”)。当我们试图通过 kai(“Eric”) 检索条目时,HashMap计算“hash-eric”作为哈希码。然后,它在哈希表中查找它。当然,它找不到它。
不难想象,如果我们在一个真正的应用程序中这样做,这种意外行为的根本原因将很难找到。
因此,我们不应该在 HashMap
中使用可变对象作为键。更进一步,我们永远不应该修改键。
画图加深理解:Kai、Tom、Amanda 分别 put 之后,效果大致如下:
在这里插入图片描述
其中 Eric 未真实 put,紫色这里只是模拟如果 put 之后,一个虚拟的位置(即 如果有 Eric 这个 Key put 之后应该在哪里)。
因为 Player 是可变的,那么直接 name 设置为 Eric 会导致 Map 中原本为 Kai 的对象的 name 改为了 Eric (注意此时该对象位置未发生变化)。
总结:在该部分中,我们学习了
remove()
然后put()
方法来替换HashMap
中的一个键。此外,我们通过一个例子讨论了为什么我们应该避免在HashMap
中使用可变对象作为键,以及为什么我们永远不应该修改HashMap
中的键。(只有出现可变对象的时候才会有修改键的说法,因为final)
- LinkedHashMap是HashMap的子类
- 再HashMap存储结构的基础上使用了一对双向链表来记录添加元素的顺序
- 与LinkedHashSet类似,LinkedHashMap可以维护Map的迭代顺序(迭代顺序与Key-Value对的插入顺序一致)
HashMap中的内部类:Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
LinkedHashMap中的内部类:Entry
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态。
TreeSet底层使用红黑树结构存储数据
TreeMap 的 Key 的排序:
- 自然排序:TreeMap 的 Key类 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
- 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接
TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。
Properties pros = new Properties();
pros.load(new FileInputStream("jdbc.properties"));
String user = pros.getProperty("user");
System.out.println(user);
- Collections 是一个操作 Set、List 和 Map 等集合的工具类
- Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
reverse(List)
:反转 List 中元素的顺序shuffle(List)
:对 List 集合元素进行随机排序sort(List)
:根据元素的自然顺序对指定 List 集合元素按升序排序sort(List,Comparator)
:根据指定的 Comparator 产生的顺序对 List 集合元素进行排序swap(List,int,int)
:将指定 list 集合中的 i 处元素和 j 处元素进行交换Object max(Collection)
:根据元素的自然顺序,返回给定集合中的最大元素Object max(Collection,Comparator)
:根据 Comparator 指定的顺序,返回给定集合中的最大元素Object min(Collection)
Object min(Collection,Comparator)
int frequecy(Collection,Object)
:返回指定集合中指定元素的出现次数void copy(List dest,List src)
:将src中的内容复制到dest中boolean replaceAll(List list,Object oldVal,Object newVal)
:使用新值替换 List 对象的所有旧值这里着重看copy的写法,copy的list双方需要确保size()的大小一致
@Test
public void test2(){
List list = new ArrayList();
list.add(123);
list.add(43);
list.add(765);
list.add(-97);
list.add(0);
//报异常:IndexOutOfBoundsException("Source does not fit in dest")
//List dest = new ArrayList();
//Collections.copy(dest,list);
//正确的:
List dest = Arrays.asList(new Object[list.size()]);
System.out.println(dest.size());//list.size();
Collections.copy(dest,list);
System.out.println(dest);
}
Collections同步控制
Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题
线程安全(效率)+是否添加null+提供方法的变化
public static void main(String[] args) {
Integer[] datas = {1,2,3,4,5};
List<Integer> list = Arrays.asList(datas);
list.add(5);
System.out.println(list.size());
}
运行异常,不允许添加元素(具体看下)
首先Arrays.asList()可不简单,得少用,是这道题让我深入认识这个函数,来看看输出结果?
public static void main(String[] args) {
Integer[] datas = {1,2,3,4,5};
List<Integer> list = Arrays.asList(datas);
list.add(5);
System.out.println(list.size());
}
结果:运行异常,不允许修改集合。要是答错了,下面的知识可得看看了!!程序员的素养课来啦!
Arrays.asList()
作用是将数组转化为集合,但它返回的集合并非如你所愿。先来看看《阿里巴巴Java开发手册》的讲述:
使用工具类
Arrays.asList()
把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear
等方法会抛出UnsupportedOperationException
异常。(调用的add/remove其实是AbstractList
的方法,该方法就是抛出异常。后面会讲)说明:
asList
的返回对象是一个Arrays 内部类java.util.Arrays.ArrayList
(我们常用的式java.util.ArrayList
),该类并没有实现集合的修改方法。Arrays.asList()
体现的是适配器模式,只是转换接口,后台的数据仍是数组。
在解决以上问题前,先看看Arrays.asList()的使用吧
int[] data = new int[]{1,2,3,4,5};
List list = Arrays.asList(data);
System.out.println(list.size());
输出结果是1,也就是说数组转化为List,是将整个数组作为一个元素!看看 Arrays.asList()
的函数如何定义就能理解了:
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
可以 asList()
看到使用了可变长泛型来定义,泛型的实例化必须是类,而基本数据类型不支持泛型化。案例中没有规定泛型,于是 asList
将数组作为泛型的实例,解决办法就是使用包装类,如下:
@Test
public void test(){
Integer[] data = new Integer[]{1,2,3,4,5};
List<Integer> list = Arrays.asList(data);
System.out.println(list.size());
}
输出结果:5
public static void main(String[] args) {
Integer[] datas = {1,2,3,4,5};
List<Integer> list = Arrays.asList(datas);
list.add(5);
System.out.println(list.size());
}//运行结果:抛出异常!
原因: Arrays.asList()
返回的并非是 java.util.Arrays.ArrayList
。不同于 java.util.ArrayList
,java.util.Arrays.ArrayList
类内部的put,add等增删改查方法方法体都是直接抛出异常。下面详细说明:
通过调试发现, Arrays.asList()
返回的并非是 java.util.ArrayList
而是java.util.Arrays.ArrayList
!(看红框框处)
java.util.Arrays.ArrayList
是工具类Arrays内部的静态类:
public class Arrays {
//省略其他方法
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
//here
private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable{
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
@Override
public int size() {
return a.length;
}
}
}
Arrays.ArrayList
是工具类 Arrays 的一个内部静态类,该静态类继承了 AbstractList
,对应类java.util.AbstractList
,内部的大部分方法都是抛出异常(只要是修改集合的都是抛出异常)–
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
//略
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
//略
}
但是java.util.Arrays.ArrayList
重写了父类的getset方法,所以由工具类Arrays.asList()
把数组转换成的集合,除了set操作,不允许其他修改集合的操作
而 ArrayList直接实现了List 接口,实现了List所有方法,真正实现了增删等操作。
总结:使用工具类
Arrays.asList()
把数组转换成集合时,不能使用其修改集合相关的方法(可以set),它的add/remove/clear
方法会抛出UnsupportedOperationException
异常。
看看此题输出?
@Test
public void test() {
String[] arr = {"欢迎","关注","Java"};
List list = Arrays.asList(arr);
arr[1] = "爱上";
list.set(2,"我");
System.out.println(Arrays.toString(arr));
System.out.println(list.toString());
}
结果:
啊?都一样,震惊一百年啊哥们?
回到刚才的asList的源码:
//Arrays中asList
public class Arrays {
private final E[] a;
//省略其他方法
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
//ArrayList<>(a);
ArrayList(E[] array) {
a = Objects.requireNonNull(array);//这里的a时内部定义的泛型数组
}
//requireNonNull()
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
}
可以看到这是直接将数组引用返回了啊,所以数组和转化后的集合实际上公用一个数组,本质依旧数组,改了其中一个,数据都会变化。你要是不知道,数据异常的事情你是抠破脑袋也想不明白的呀!
总结asList的坑:
- 基本数据类型数组转化集合只有一个元素(基本数据类型不支持泛型化)
- 数组转化为集合,该集合不能修改,除了set方法(抛出异常)
- 数据转化为集合,本质依旧是数组,公用一块空间(内部直接引用赋值)
方法一:遍历添加(较low)
Integer intArray[] = {1, 2, 3};
ArrayList<Integer> aList = new ArrayList<>();
for (Integer i: intArray){
aList.add(i);
}
方法二:用Collections工具类中方法(addAll后面的参数为可变泛型,也就是不支持基本数据类型)
List<String> list = new ArrayList();
Collections.addAll(list, "welcome", "to", "china");
System.out.println(list);
此法效率高?too young too simple!
addAll()
方法的实现就是用的上面遍历的方式。
方法三:将Arrays.asList返回的集合作为ArrayList的构造参数
ArrayList arrayList = new ArrayList<>(Arrays.asList("welcome", "to", "china"));
没有方法可以直接让基本数据类型的数组转化为集合,只能一个个添加,或者使用包装类。