java常用分类如下:
Collection
集合主要是两组(单列集合,双列集合)
Collection集合框架结构
Map集合框架结构
Collection类的主要方法
collection包含如下方法
这里将常用的演示出来
package 集合概述.Collection;
import java.util.ArrayList;
import java.util.List;
public class CollectionMethod {
public static void main(String[] args) {
List list=new ArrayList<>();
// add:添加单个元素
list.add("jack");
list.add(10);//自动装箱为Integer 对象的形式存储的 list.add(new Integer(10) )
list.add(true);
// remove: 删出指定元素
list.remove(0); // 删除第一个元素
list.remove("jack"); //指定删除某个元素
// contains:查找某个元素是否存在
System.out.println(list.contains("jack"));// T
// size:获取元素的个数
System.out.println(list.size());
// isEmpty:判断是否为空
System.out.println(list.isEmpty());
// clear:清空
list.clear();
// addAll:添加多个元素 将list2添加进去
List list2=new ArrayList();
list2.add("红楼梦");
list2.add("三国");
list.addAll(list2);
// containsAll:查看多个元素是否都存在
System.out.println(list.containsAll(list2));//T
// removeAll:删除多个元素
list.removeAll(list2);
}
}
List
List的三种遍历方式
- 迭代器方式
- 增强for
- 使用普通for
List排序练习
ArrayList
在添加前会先使用Integer valueOf 的方法将int类型的数据进行自动装箱
确定容量够不够
elementData.length指的是当前容量,比较的是:如果当前容量小于grow里面的容量,就扩容
minCapacity 最小需要的容量
使用有参构造器的ArrayList
Vector
vector与ArrayLis的比较
底层结构 | 版本 | 线程安全(同步)效率 | 扩容倍数 | |
ArrayList | 可变数组 | jdk1.2 | 不安全,效率高 | 如果有参构造1.5倍 如果是无参 1.第一次10 2.从第二次开始按照1.5倍扩容 |
Vector | 可变数组 | jdk1.0 | 如果是无惨,默认10,满后,就按2倍扩容 如果指定大小,则每次直接按2倍扩容 |
针对以下代码执行流程,分析源码
package 集合概述.Vector;
import java.util.Vector;
@SuppressWarnings({"All"})
public class Vector_ {
public static void main(String[] args) {
//无参构造器
Vector vector=new Vector();
for (int i = 0; i < 10; i++) {
vector.add(i);
}
}
}
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); 就是 扩容两倍这段代码结果等同于 newCapacity = oldCapacity + 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
双向链表模拟
模拟一个双向链表进行简单操作
package 集合概述.LinkedList;
/**
* @Description: 简单 模拟简单的双向链表
* @Param:
* @return:
* @Author: 熊辉
* @Date: 2022/7/9
*/
public class LinkedList01 {
public static void main(String[] args) {
//模拟一个简答的双向链表
Node jack=new Node("Jack");
Node tom = new Node("Tom");
Node xh = new Node("xh");
//连接三个节点,形成双向链表
// jack -> tom -> xh
jack.next=tom;
tom.next=xh;
// xh -> tom -> jack
xh.pre=tom;
tom.pre=jack;
Node first=jack; //让first引用指向jack,就是双向链表的头节点
Node last=xh; //让last引用指向xh,就是双向链表的尾节点
System.out.println("===从头到尾进行遍历====");
//从头到尾进行遍历
while(true){
if (first==null){
break;
}
//输出first的信息
System.out.println(first);
first=first.next;
}
System.out.println("===从尾到到进行遍历====");
//从尾到头进行遍历
while(true){
if (last==null){
break;
}
//输出first的信息
System.out.println(last);
last=last.pre;
}
//演示链表的添加对象/数据
//要求在,是在tom和xh中间,插入一个对象 smith
// 1. 先创建一个 Node 节点 ,名字就是 smith
Node smith = new Node("smith");
//下面就把smith加入到双向链表
smith.pre=tom;
smith.next=xh;
tom.next=smith;
xh.pre=smith;
//重置下first,让frist再次指向第一个人
first=jack;
System.out.println("===从头到尾进行遍历====");
//从头到尾进行遍历
while(true){
if (first==null){
break;
}
//输出first的信息
System.out.println(first);
first=first.next;
}
// 让last重新指向最后一个节点
last=xh;
System.out.println("===从尾到到进行遍历====");
//从尾到头进行遍历
while(true){
if (last==null){
break;
}
//输出first的信息
System.out.println(last);
last=last.pre;
}
}
}
//定义一个Node类,Node对象 ,表示双向链表的一个节点
class Node{
public Object item; //真正存放数据的地方
public Node next; //指向下一个节点
public Node pre; //指向前一个节点
public Node(Object name){
this.item=name;
}
public String toString(){
return "Node name="+item;
}
}
针对如下代码运行的源码分析
package 集合概述.LinkedList;
import java.util.LinkedList;
@SuppressWarnings({"all"})
public class LinkedListCRUD {
public static void main(String[] args) {
LinkedList linkedList = new LinkedList();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
System.out.println("linkedList ="+linkedList);
linkedList.remove(); //默认删除的是第一个节点
System.out.println("linkedList ="+linkedList);
}
}
添加的执行流程
1. LinkedList linkedList = new LinkedList();
public LinkedList() {}
2. 这时 linkedList 的属性 first =null last =null
3. 执行add方法 添加
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最优使用迭代器遍历,通过节点去遍历,当使用for循环时,get(i)获取某一元素时都需要对整个List重新遍历,迭代器遍历效率最高
执行删除操作的流程
linkedList.remove(); //默认删除的是第一个节点
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;
}
List集合的选择
set
set接口基本介绍
- 无序(添加和取出顺序不一致),没有索引
- 不允许重复元素,所以最多包含一个null
Set 接口和常用方法
set 接口的遍历方式
set接口对象 没有索引没有get方法 不能通过索引来获取因此没有传统的for的遍历方式
1.迭代器遍历
2.增强for循环遍历(增强for的底层就是迭代器)
package 集合概述.set;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
@SuppressWarnings({"all"})
public class SetMethod {
public static void main(String[] args) {
//1. 以set接口的实现类 HashSet 来讲解set 接口的方法
Set set=new HashSet();
set.add("John");
set.add("lucy");
set.add("John");
set.add("jack");
set.add(null);
set.add(null);
System.out.println(set);
//遍历:
//1.方式一:使用迭代器
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
//方式二:增强for(增强for的底层就是迭代器)
for (Object o : set) {
System.out.println(o);
}
}
}
HashSet
构造器走的源码
1. Set set=new HashSet(); 构造器走的是HashMap
public HashSet() {
map = new HashMap<>();
}
2. HashSet可以存放空值但是只能有一个空值(即元素不可以重复)
set.add(null);
set.add(null);
//HashSet不能添加相同的元素
Set set=new HashSet();
set.add("lucy"); //添加成功
set.add("lucy"); //失败
set.add(new Dog("Tom")); //添加成功
set.add(new Dog("Tom")); //添加成功 new 出来的是两个不一样的对象
System.out.println(set);
class Dog{
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
'}';
}
}
可以看到两个两个Tom都被添加进去了,lucy只能添加一个符合set集合不可添加重复元素的性质
那么我们再来看一道经典的面试题
set.add(new String("xh")); //OK
set.add(new String("xh")); //添加失败
看完这个我们又会觉得和我们之前的结论产生冲突 new String("xh") 是两个不同的对象为啥会添加失败呢
先看结论再来分析过程
注意注意:这里说的table的大小指的是 当我们向HashSet增加一个元素, -> Node -> 加入Table,就算是增加了一个元素
下面是hashset存放元素的过程
hashset是基于hashmap实现的
public class HashSetSource {
public static void main(String[] args) {
HashSet hashSet=new HashSet();
hashSet.add("java");
hashSet.add("php");
hashSet.add("java");
System.out.println(hashSet);
}
}
针对如上代码分析其执行流程:
1.执行HashSet()
public HashSet() {
map = new HashMap<>();
}
2.执行add()方法
public boolean add(E e) { // e="java"
return map.put(e, PRESENT)==null; // PRESENT 就是一个无意义的静态对象用来占位的 private static final Object 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 指向的Ndoe 节点 的key 是同一个对象
// (2) p指向的Node结点的 key的equals() 和准备加入的key比较后相同 即 (1)是先比较是否是同一个对象,(2)是在不是同一个对象的前提下比较两个对象的值是否相等 ==判断地址是否相同(判断是否是同一个对象),equals判断内容是否相同(判断两个对象的属性是否相同)
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(tab, hash);方法 对当前这个链表进行树化(转成红黑树) 注意: 在转成红黑树时,要进行判断,如果该table数组的大小 <64 则先扩容,不树化
// (2) 依次比较的过程中,如果有相同的情况,就直接 break
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 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)
if (++size > threshold)
resize(); //扩容
afterNodeInsertion(evict);
return null;
}
LinkedHashSet
TreeSet
TreeSet treeSet = new TreeSet();
treeSet.add("bbb");
treeSet.add("aaa");
treeSet.add("ccc");
System.out.println(treeSet);
1. new TreeSet()构造器默认执行的是TreeMap
public TreeSet() {
this(new TreeMap<E,Object>());
}
Map
map接口常用方法
Map遍历方式
HashMap
常见面试问题
HashMap运行机制
HashMap map=new HashMap();
map.put("java",10);
map.put("php",10);
map.put("java",20);
System.out.println(map);
1. 执行构造器 new HashMap() 初始化加载因子 loadfactor=0.75 HashMap$Node[] table=null
2. 执行put方法 会调用hash方法计算key的值 (h = key.hashCode()) ^ (h >>> 16);
public V put(K key, V value) { k="java" value=10
return putVal(hash(key), key, value, false, true);
}
3.执行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数组为null或者length=0,就扩容到16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//计算元素下标:将 hash 值和数组长度-1 进行与操作,保证结果不会超过 table 数组范围 取出hash值对应的table的索引位置的Node,如果为null,就直接把加入的k-v,创建成一个Node,加入到该位置即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k; //辅助变量
if (p.hash == hash && //如果table表的索引位置的key的Hash值相同和新的key的hash值相同 并 满足(table现有结点的key和准备添加的key是同一个对象 || equals 返回真) 就认为不能加入新的k-V
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode) //如果当前table已有的Node是红黑树,就按照红黑树的方式处理
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果找到的结点的后面是链表就循环比较
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//如果整个链表,没有和他相同的,就加到该链表的最后
p.next = newNode(hash, key, value, null);
//加入后,判断当前链表的个数,是否已经到达8个,到8个后就调用 treeifyBin方法进行红黑树的树化,
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && //再循环比较过程中,发现有相同的就不添加了,break,就只是替换value
((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; //key 相同 将 value进行替换
afterNodeAccess(e);
return oldValue;
}
}
++modCount; //每增加一个Node就size++
if (++size > threshold[12-24-48]) //如果size大于临界值 就扩容
resize();
afterNodeInsertion(evict);
return null;
}
5. 关于树化(转成红黑树)
//如果table表为空,或者大小还没有到64,就暂时不树化,而是进行扩容
//否则才会真正的树化-> 剪枝
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
HashTable