集合
数组的不足:
- 创建时长度必须指定,而且指定了不能更改
- 保存的必须为同一类型的元素
- 使用数组增加删除比较麻烦(数组扩容要新创建一个数组)
集合:
- 可以动态保存任意多个对象,使用方便
- 提供了一系列方便的操作对象的方法:add、remove、set、get
- 使用集合添加删除新元素的代码简洁
集合的框架体系
Java的集合类很多,主要分为两大类:
- 单列集合
- 双列集合
Collection接口有两个重要的子接口 List Set,他们的实现子类都是单列集合
Map接口实现的子类是双列集合,存放的K-V
Collection接口和常用方法
-
Collection接口实现类的特点
- collection实现子类可以存放多个元素,每个元素可以是Object
- 有些Collection的实现类,可以存放重复的元素,有些不可以
- 有些Collection的实现类,有些是有序的(List),有些不是有序(Set)
- Collection接口没有直接的实现子类,是通过它的子接口Set 和 List来实现的
-
Collection接口和常用方法
List list = new ArrayList(); //1、add 添加单个元素 list.add("jack"); list.add(10); //list.add(new Integer(10)) list.add(true); //2、remove 删除指定元素 list.remove(0);//删除第一个元素 list.remove(true);//指定删除某个元素 //3、contains:查找元素是否存在 System.out.println(list.contains("jack"));//T //4、size:获取元素个数 System.out.println(list.size());//2 //5、isEmpty:判断是否为空 System.out.println(list.isEmpty());//F //6、clear:清空 list.clear(); System.out.println("list=" + list); //7、addAll:添加多个元素,形参是集合 ArrayList list2 = new ArrayList(); list2.add("红楼梦"); list2.add("三国演义"); list.addAll(list2); System.out.println("list=" + list); //8、containsAll:查找多个元素是否都存在 System.out.println(list.containsAll(list2));//T //9、removeAll: 删除多个元素,形参是集合 list.add("聊斋"); list.removeAll(list2); //list直接输出是[元素1,元素2,...]
-
Collection 接口遍历元素方式 1-使用 Iterator(迭代器)
-
Iterator对象称为迭代器,主要用于遍历 Collection集合中的元素。
-
所有实现了Collection接口的集合类都有一个iterator)方法,用以返回一个实现了lterator接口的对象,即可以返回一个迭代器。
-
Iterator的结构
-
lterator仅用于遍历集合,Iterator本身并不存放对象。
-
lterator iterator = coll.iterator();//得到一个集合的迭代器 hasNext():判断是否还有下一个元素while(iterator.hasNext()){ //next0作用:1.下移2.将下移以后集合位置上的元素返回 System.out.println(iterator.next(); } //在调用iterator.next()方法之前必须要调用iterator.hasNext()进行检测。若不调用,且下一条记录无效,直接调用it.next()会抛出NoSuchElementException异常。 //itit 是while的快捷键 // Ctrl + j 可以查看所有的快捷键 Collection col = new ArrayList(); col.add(new Book("三国演义", "罗贯中", 10.1)); col.add(new Book("小李飞刀", "古龙", 5.1)); col.add(new Book("红楼梦", "曹雪芹", 34.6)); //System.out.println("col=" + col); //现在老师希望能够遍历 col 集合 //1. 先得到 col 对应的 迭代器 Iterator iterator = col.iterator(); //2. 使用 while 循环遍历 // while (iterator.hasNext()) {//判断是否还有数据 // //返回下一个元素, 类型是 Object // Object obj = iterator.next(); // System.out.println("obj=" + obj); // } //老师教大家一个快捷键, 快速生成 while => itit //显示所有的快捷键的的快捷键 ctrl + j while (iterator.hasNext()) { Object obj = iterator.next(); System.out.println("obj=" + obj); } // 3. 当退出 while 循环后 , 这时 iterator 迭代器, 指向最后的元素 // iterator.next();//NoSuchElementException //4. 如果希望再次遍历, 需要重置我们的迭代器 iterator = col.iterator(); System.out.println("===第二次遍历==="); while (iterator.hasNext()) { Object obj = iterator.next(); System.out.println("obj=" + obj); }
-
-
Collection 接口遍历对象方式 2-for 循环增强
增强for循环,可以代替iterator迭代器,特点:增强for就是简化版的iterator,本质一样。只能用于遍历集合或数组。
基本语法
for(元素类型元素名:集合名或数组名){访问元素
}
/* 增强for循环底层还是调用的迭代器,可以理解为简化版本的迭代器 快捷方式:I 或者 iter */
List接口和常用方法
-
List接口是 Collection接口的子接口
-
List集合类中元素有序(即添加顺序和取出顺序一致)、且可重复
-
List集合中的每个元素都有其对应的顺序索引,即支持索引。
// 索引是从 0 开始的 System.out.println(list.get(3));//第四个
-
List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
-
-
方法
//在 index = 1 的位置插入一个对象,不写索引默认插入在最后 list.add(1, "韩顺平"); //boolean addAll(int index, Collection eles):从 index 位置开始将 eles 中的所有元素添加进来 List list2 = new ArrayList(); list2.add("jack"); list2.add("tom"); list.addAll(1, list2); // Object get(int index):获取指定 index 位置的元素 // int indexOf(Object obj):返回 obj 在集合中首次出现的位置 System.out.println(list.indexOf("tom"));//2 // int lastIndexOf(Object obj):返回 obj 在当前集合中末次出现的位置 list.add("韩顺平"); System.out.println("list=" + list); System.out.println(list.lastIndexOf("韩顺平")); // Object remove(int index):移除指定 index 位置的元素, 并返回此元素 list.remove(0); System.out.println("list=" + list); // Object set(int index, Object ele):设置指定 index 位置的元素为 ele , 相当于是替换.索引必须存在 list.set(1, "玛丽"); System.out.println("list=" + list); // List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex 位置的子集合 // 注意返回的子集合 fromIndex <= subList < toIndex List returnlist = list.subList(0, 2); //前闭后开 System.out.println("returnlist=" + returnlist);
-
List的三种遍历方式:
- 迭代器
- 增强for
- 普通for循环
//List 接口的实现子类 Vector LinkedList //List list = new ArrayList(); //List list = new Vector(); //List list = new LinkedList(); / //遍历 //1. 迭代器 Iterator iterator = list.iterator(); while (iterator.hasNext()) { Object obj = iterator.next(); System.out.println(obj); } System.out.println("=====增强 for====="); //2. 增强 for for (Object o : list) { System.out.println("o=" + o); } System.out.println("=====普通 for===="); //3. 使用普通 for for (int i = 0; i < list.size(); i++) { System.out.println("对象=" + list.get(i)); }
ArrayList
-
ArrayList注意事项
- permits all elements可以放任意元素, including null , ArrayList可以加入null,并且可以放多个
- ArrayList是由数组来实现数据存储的(看源码)
- ArrayList基本等同于Vector,除了**ArrayList是线程不安全(执行效率高))**看源码,没有synchronize修饰,不互斥.在多线程情况下,不建议使用ArrayList
-
ArrayList 的底层操作机制源码分析(重点, 难点.)
- ArrayList中维护了一个Object类型的数组elementData. [debug看源码]
transient Object[] elementData;//transient表示瞬间,短暂的,表示该属性不会被序列化
-
当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍。
-
如果使用的是指定大小的构造器,则初始elementData容量为指定大小,**如果需要扩容,则直接扩容elementData为1.5倍。**一开始指定大小为1,扩容1.5倍还是1,此时会将扩容后的1和所需的数组长度做比较,选择较大的
源码:
//老韩解读源码 //注意, 注意, 注意, Idea 默认情况下, Debug 显示的数据是简化后的, 如果希望看到完整的数据需要做设置. //使用无参构造器创建 ArrayList 对象 //ArrayList list = new ArrayList(); ArrayList list = new ArrayList(8); //使用 for 给 list 集合添加 1-10 数据 for (int i = 1; i <= 10; i++) { list.add(i); } //使用 for 给 list 集合添加 11-15 数据 for (int i = 11; i <= 15; i++) { list.add(i); } list.add(100); list.add(200); list.add(null);
Vector底层结构和源码剖析
- Vector类的定义说明
public class vector extends AbstractList
implements List,RandomAccess,cloneable,Serializable
-
Vector底层也是一个对象数组
protected Object[] elementData;
-
Vector是线程同步的,即线程安全, Vector类的操作方法带有synchronized
public synchronized E get(int index){i
if (index >= elementCount)
throw new ArraylndexOutOfBoundsException(index);return elementData(index);
- 在开发中,需要线程同步安全时,考虑使用Vector
Vector和ArrayList的比较:
//无参构造器
//有参数的构造
Vector vector = new Vector(8);
for (int i = 0; i < 10; i++) {
vector.add(i);
} v
ector.add(100);
System.out.println("vector=" + vector);
//老韩解读源码
//1. new Vector() 底层
/*
public Vector() {
this(10);
}
补充: 如果是 Vector vector = new Vector(8);
走的方法:
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
2. vector.add(i)
2.1 //下面这个方法就添加数据到 vector 集合
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
2.2 //确定是否需要扩容 条件 : minCapacity - elementData.length>0
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
2.3 //如果 需要的数组大小 不够用, 就扩容 , 扩容的算法
//newCapacity = oldCapacity + ((capacityIncrement > 0) ?
// capacityIncrement : oldCapacity);
//就是扩容两倍.
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
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);
}
*/
Linkedlist底层结构
-
LinkedList的全面说明
- LinkedList实现了双向链表和双端队列特点
- 可以添加任意元素(元素可以重复),包括null
- 线程不安全,没有实现同步
-
LinkedList的底层操作机制
- LinkedList底层维护了一个双向链表
- LinkedList中维护了两个属性first和last分别指向首节点和尾节点
- 每个节点(Node对象),里面又维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向后一个节点。最终实现双向链表
- 所以LinkedList的元素的添加和删除,不是通过数组完成的,相对来说效率较高。
LinkedList linkedList = new LinkedList(); linkedList.add(1); linkedList.add(2); linkedList.add(3); System.out.println("linkedList=" + linkedList); //演示一个删除结点的 linkedList.remove(); // 这里默认删除的是第一个结点 //linkedList.remove(2); System.out.println("linkedList=" + linkedList); //修改某个结点对象 linkedList.set(1, 999); System.out.println("linkedList=" + linkedList); //得到某个结点对象 //get(1) 是得到双向链表的第二个对象 Object o = linkedList.get(1); System.out.println(o);//999 //因为 LinkedList 是 实现了 List 接口, 三种遍历方式 System.out.println("===LinkeList 遍历迭代器===="); Iterator iterator = linkedList.iterator(); while (iterator.hasNext()) { Object next = iterator.next(); System.out.println("next=" + next); } System.out.println("===LinkeList 遍历增强 for===="); for (Object o1 : linkedList) { System.out.println("o1=" + o1); } System.out.println("===LinkeList 遍历普通 for===="); for (int i = 0; i < linkedList.size(); i++) { System.out.println(linkedList.get(i)); } //老韩源码阅读. /* 1. LinkedList linkedList = new LinkedList(); public LinkedList() {} 2. 这时 linkeList 的属性 first = null last = null 3. 执行 添加 public boolean add(E e) { linkLast(e); //在尾部执行添加方法 return true; } 4.将新的结点, 加入到双向链表的最后 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.remove(); // 这里默认删除的是第一个结点 //也可以remove(index) remove(Object obj) 1. 执行 removeFirst public E remove() { return removeFirst(); } 2. 执行 public E removeFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return unlinkFirst(f); } 3. 执行 unlinkFirst, 将 f 指向的双向链表的第一个结点拿掉 private E unlinkFirst(Node<E> f) { // assert f == first && f != null; final E element = f.item; final Node<E> next = f.next; f.item = null; f.next = null; // help GC first = next; if (next == null) last = null; else next.prev = null; size--; modCount++; return element; } */
-
ArrayList和LinkedList比较
Set接口和常用方法
-
Set接口基本介绍
- 无序(添加和取出的顺序不一致),没有索引
- 不允许重复元素,所以最多包含一个null
- JDK API中Set接口的实现类有:HashSet TreeSet 等
- 是Collection接口的子接口,方法、遍历方式一样
-
Set接口的常用方法
和Collection接口一样
-
Set接口的遍历方式
和Collection接口的遍历方式一样
- 迭代器
- 增强for
- 不能使用索引方式(也就是不能用普通for)
-
//老韩解读 //1. 以 Set 接口的实现类 HashSet 来讲解 Set 接口的方法 //2. set 接口的实现类的对象(Set 接口对象), 不能存放重复的元素, 可以添加一个 null //3. set 接口对象存放数据是无序(即添加的顺序和取出的顺序不一致) //4. 注意: 取出的顺序的顺序虽然不是添加的顺序, 但是他是固定的. Set set = new HashSet(); set.add("john"); set.add("lucy"); set.add("john");//重复 set.add("jack"); set.add("hsp"); set.add("mary"); set.add(null);// set.add(null);//再次添加 null for(int i = 0; i <10;i ++) { System.out.println("set=" + set); }//取多少次都一样 //遍历 //方式 1: 使用迭代器 System.out.println("=====使用迭代器===="); Iterator iterator = set.iterator(); while (iterator.hasNext()) { Object obj = iterator.next(); System.out.println("obj=" + obj); } set.remove(null);//删除null //方式 2: 增强 for 增强for的底层就是迭代器 System.out.println("=====增强 for===="); for (Object o : set) { System.out.println("o=" + o); } //set 接口对象, 不能通过索引来获取
Set接口实现类-HashSet
-
基本介绍
-
HashSet实现了Set接口
-
HashSet实际上是HashMap
public Hashset { map = new HashMap<>();//源码 }
-
可以存放null值,但是只能有一个null
-
HashSet不保证元素是有序的,取决于hash后,再确定索引的结果(即,不保证存放元素的顺序和取出顺序一致)
-
不能有重复元素/对象
//老韩解读 //1. 构造器走的源码 /* public HashSet() { map = new HashMap<>(); } 2. HashSet 可以存放 null ,但是只能有一个 null,即元素不能重复 */ Set hashSet = new HashSet(); hashSet.add(null); hashSet.add(null); System.out.println("hashSet=" + hashSet);
HashSet set = new HashSet(); //说明 //1. 在执行 add 方法后, 会返回一个 boolean 值 //2. 如果添加成功, 返回 true, 否则返回 false //3. 可以通过 remove 指定删除哪个对象 System.out.println(set.add("john"));//T System.out.println(set.add("lucy"));//T System.out.println(set.add("john"));//F System.out.println(set.add("jack"));//T System.out.println(set.add("Rose"));//T set.remove("john"); System.out.println("set=" + set);//3 个 // set = new HashSet(); System.out.println("set=" + set);//0 //4 Hashset 不能添加相同的元素/数据? set.add("lucy");//添加成功 set.add("lucy");//加入不了 set.add(new Dog("tom"));//OK set.add(new Dog("tom"));//Ok System.out.println("set=" + set); //看源码, 做分析, 先给小伙伴留一个坑, 以后讲完源码, 你就了然 //去看他的源码, 即 add 到底发生了什么?=> 底层机制. set.add(new String("hsp"));//ok set.add(new String("hsp"));//加入不了. System.out.println("set=" + set);
-
-
HashSet底层机制
HashSet的底层是HashMap,HashMap的底层是(数组+链表+红黑树)
- HashSet底层是HashMap
- 添加一个元素时,先得到hash值-会转成->索引值
- 找到存储数据表table,看这个索引位置是否已经存放的有元素
- 如果没有,直接加入
- 如果有,调用equals比较,如果相同,就放弃添加,如果不相同,则添加到最后(String重写了equals方法,所以两个new的对象的哈希值是一样的)
- 在Java8中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
public class HashSetSource { public static void main(String[] args) { HashSet hashSet = new HashSet(); hashSet.add("java");//到此位置, 第 1 次 add 分析完毕. hashSet.add("php");//到此位置, 第 2 次 add 分析完毕 hashSet.add("java"); System.out.println("set=" + hashSet); /* 老韩对 HashSet 的源码解读 1. 执行 HashSet() public HashSet() { map = new HashMap<>(); } 2. 执行 add() public boolean add(E e) { //e = "java" return map.put(e, PRESENT)==null; //(static) PRESENT = new Object(); } 3.执行 put() , 该方法会执行 hash(key) 得到 key 对应的 hash 值 算法 h = key.hashCode()) ^ (h >>> 16) 按位抑或 public V put(K key, V value){ //key = "java" value = PRESENT 共享 return putVal(hash(key), key, value, false, true); } 4.执行 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; //定义了辅助变量 //table 就是 HashMap 的一个属性, 类型是 Node[] //if 语句表示如果当前 table 是 null, 或者 大小=0 //就是第一次扩容, 到 16 个空间. if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //(1)根据 key, 得到 hash 去计算该 key 应该存放到 table 表的哪个索引位置 //并把这个位置的对象, 赋给 p //(2)判断 p 是否为 null //(2.1) 如果 p 为 null, 表示还没有存放元素, 就创建一个 Node (key="java",value=PRESENT) //(2.2) 就放在该位置 tab[i] = newNode(hash, key, value, null) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //一个开发技巧提示: 在需要局部变量(辅助变量)时候, 在创建 Node<K,V> e; K k; // //如果当前索引位置对应的链表的第一个元素和准备添加的 key 的 hash 值一样 //并且满足 下面两个条件之一: //(1) 准备加入的 key 和 p 指向的 Node 结点的 key 是同一个对象 //(2) p 指向的 Node 结点的 key 的 equals() 和准备加入的 key 比较后相同 就不能加入(所以String重写了equals方法,即使new了两个对象,也是一样的) if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //再判断 p 是不是一颗红黑树, //如果是一颗红黑树, 就调用 putTreeVal , 来进行添加 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {//如果 table 对应索引位置, 已经是一个链表, 就使用 for 循环比较 //(1) 依次和该链表的每一个元素比较后, 都不相同, 则加入到该链表的最后 // 注意在把元素添加到链表后, 立即判断 该链表是否已经达到 8 个结点 // , 就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树) // 注意, 在转成红黑树时, 要进行判断, 判断条件 // if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64)) // resize(); // 如果上面条件成立, 先 table 扩容. // 只有上面条件不成立时, 才进行转成红黑树 //(2) 依次和该链表的每一个元素比较过程中, 如果有相同情况,就直接 break for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD(8) - 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; //size 就是我们每加入一个结点 Node(k,v,h,next), size++ if (++size > threshold) resize();//扩容 afterNodeInsertion(evict); return null; } */
注:equals可以重写
HashSet的扩容和红黑树机制
- HashSet底层是HashMap.第一次添加时,table数组扩容到16,临界值(threshold)是16 * 加载因子(loadFactor)是0.75 = 12
- 如果table 数组使用到了临界值12(总共所有的元素),就会扩容到16 * 2=32,新的临界值就是32 * 0.75 =24,依次类推
- 如果一条链表的元素个数达到8,增加一个就扩容一次,直到扩容到64,开始树化
- 在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制