前言:
1.jvm、jre、jdk的区别
JVM:java虚拟机,用于保证java的跨平台特性
JRE:java的运行环境,包括jvm+java的核心类库
JDK:Java的开发工具,包括jre+开发工具
java数据类型分为基本数据类型和引用数据类型
(1) 基本数据类型包括 boolean(布尔型)、float(单精度浮点型)、char(字符型)、byte(字节型)、short(短整型)、int(整型)、long(长整型)和 double (双精度浮点型)共 8 种,详见表 1 所示。
类型名称 | 关键字 | 占用内存 | 取值范围 |
---|---|---|---|
字节型 | byte | 1 字节 | -128~127 |
短整型 | short | 2 字节 | -32768~32767 |
整型 | int | 4 字节 | -2147483648~2147483647 |
长整型 | long | 8 字节 | -9223372036854775808L~9223372036854775807L |
单精度浮点型 | float | 4 字节 | +/-3.4E+38F(6~7 个有效位) |
双精度浮点型 | double | 8 字节 | +/-1.8E+308 (15 个有效位) |
字符型 | char | 2 字节 | ISO 单一字符集 |
布尔型 | boolean | 1 字节 | true 或 false |
所有的基本数据类型的大小(所占用的字节数)都已明确规定,在各种不同的平台上保持不变,这一特性有助于提高 Java 程序的可移植性。
(2) 引用数据类型建立在基本数据类型的基础上,包括数组、类和接口。引用数据类型是由用户自定义,用来限制其他数据的类型。
第一章、集合
集合的定义:
数据存储的容器
集合和数组的区别
- 数组是固定长度的;集合可变长度的。
- 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
- 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。
集合框架结构
1.Iterator接口
首先是Iterator接口,这是用于遍历集合里面存储数据的接口,在Iterator接口中定义了三个方法:
修饰与类型 | 方法与描述 |
---|---|
boolean | hasNext() 如果仍有元素可以迭代,则返回true。 |
E | next() 返回迭代的下一个元素。 |
void | remove() 从迭代器指向的 collection 中移除迭代器返回的最后一个元素(可选操作)。 |
//含remove的使用
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
Iterator<String> iterator= list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
iterator.remove();
}
System.out.println(list.size());
使用Iterator迭代器进行删除集合元素,则不会出现并发修改异常。 什么是并发修改异常?
因为:在执行remove操作时,同样先执行checkForComodification(),然后会执行ArrayList的remove()方法,该方法会将modCount值加1,这里我们将expectedModCount=modCount,使之保持统一。
注意,源码部分不是this.elementData,因为外部类的对象是不能调用内部类的,所以只能用类名.this来区别内部类和外部类的属性。
首先我们看一下它的几个成员变量:
cursor:表示下一个要访问的元素的索引,从next()方法的具体实现就可看出
lastRet:表示上一个访问的元素的索引
expectedModCount:表示对ArrayList修改次数的期望值,它的初始值为modCount。
modCount是AbstractList类中的一个成员变量
//普通迭代器会报并发修改异常
//modCount ++ 导致!= expectedModCount 出现异常
//add一次之后会导致modCount++
public static void exc(){
//创建List集合
List<String> list = new ArrayList<String>();
//给集合中添加元素
list.add("pig");
list.add("dog");
list.add("cat");
list.add("chiken");
//迭代集合,当有元素为"cat"时,集合加入新元素"mark"
Iterator<String> it = list.iterator();
while(it.hasNext()){
String str = it.next();
//判断取出的元素是否是"cat",是就添加一个新元素
if("cat".equals(str)){
list.add("mark");// 该操作会导致程序出错
}
}
//打印容器中的元素
System.out.println(list);
}
1.异常解释
-
ConcurrentModificationException:当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常。
-
产生的原因:
迭代器是依赖于集合而存在的,在判断成功后,集合的中新添加了元素,而迭代器却不知道,所以就报错了,这个错叫并发修改异常。
简单描述就是:迭代器遍历元素的时候,通过集合是不能修改元素的。 -
如何解决呢?
A:迭代器迭代元素,迭代器修改元素
B:集合遍历元素,集合修改元素(普通for循环)1.ListIterator接口
ListIterator是一个功能更加强大的迭代器, 它继承于Iterator接口,只能用于各种List类型的访问。可以通过调用listIterator()方法产生一个指向List开始处的ListIterator, 还可以调用listIterator(n)方法创建一个一开始就指向列表索引为n的元素处的ListIterator。
特点
- 允许我们向前、向后两个方向遍历 List;
- 在遍历时修改 List 的元素;
- 遍历时获取迭代器当前游标所在位置。
常用API
修饰与类型 方法与描述 void
add(E e)
将指定的元素插入到列表 (可选操作)。boolean
hasNext()
如果此列表迭代器在前进方向还有更多的元素时,返回true
。boolean
hasPrevious()
如果此列表迭代器在相反方向还有更多的元素时,返回true
。E
next()
返回列表中的下一个元素和光标的位置向后推进。int
nextIndex()
返回调用next()
后返回的元素索引。E
previous()
返回列表中的上一个元素和光标的位置向前移动。int
previousIndex()
返回调用previous()
后返回的元素索引 。void
remove()
删除列表中调用next()
或previous()
的返回最后一个元素。void
set(E e)
用指定元素替换列表中调用next()
或previous()
的返回最后一个元素。
//避免并发修改异常
List<String> list = new ArrayList<String>();
list.add("hello");
list.add("world");
list.add("java");
//获取列表迭代器
//这里的列表迭代器(ListIterator)add方法和前面介绍过的迭代器add方法不同的是每进行一次都会把实际修改值modCount重新赋值给预期修改值expectedModCount
ListIterator<String> li = list.listIterator();
while (li.hasNext()){
String s = li.next();
if (s.equals("world")){
li.add("javaee");
}
}
System.out.println(list);
2.Collection接口
所有集合类都位于java.util包下。Java的集合类主要由两个接口派生而出:Collection和Map,Collection和Map是Java集合框架的根接口,这两个接口又包含了一些子接口或实现类。
- Collection一次存一个元素,是单列集合;
- Map一次存一对元素,是双列集合。Map存储的一对元素:键–值,键(key)与值(value)间有对应(映射)关系。
- Collection集合主要有List和Set两大接口
- List:有序(元素存入集合的顺序和取出的顺序一致),元素都有索引。元素可以重复。
- Set:无序(存入和取出顺序有可能不一致),不可以存储重复元素。必须保证元素唯一性。
单列集合(Collection)继承关系图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GV3vxGRC-1605684069963)(C:\Users\pzh\AppData\Roaming\Typora\typora-user-images\image-20201117105314981.png)]
2.1list接口
list集合的方法
list的主要实现类
ArrayList的介绍
底层实现:数组。ArrayList是可以动态增长和缩减的索引序列,它是基于数组实现的List类。
该类封装了一个动态再分配的Object[]数组,每一个类对象都有一个capacity属性,表示它们所封装的Object[]数组的长度,当向ArrayList中添加元素时,该属性值会自动增加。
线程安全问题:ArrayList是线程不安全的,当多条线程访问同一个ArrayList集合时,程序需要手动保证该集合的同步性,而Vector则是线程安全的。
ArrayList的部分源码解析
1.先看构造方法
private static final long serialVersionUID = 8683452581122892189L;
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
/**
* Constructs an empty list with an initial capacity of ten.
*/
//this.elementData首先给定一个空的数组
//transient Object[] elementData;
//private static final Object[] EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//有参构造方法
/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
2.分析部分方法
add方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
size默认值为0
调用ensureCapacityInternal(size+1)方法 size+1=1
//minCapacity = 1
//第一次进来 elementData为空数组
//this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
调用ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));方法
//minCapacity = 1
//第一次进来 elementData为空数组
//this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//第一次进来判断一定相等
//max(10,1)
//这里表示默认长度为10 private static final int DEFAULT_CAPACITY = 10;
//返回10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
最后调用ensureExplicitCapacity方法
//第一次传入的值为10
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
//判断是否要扩容
//10 - 数组的长度要大于0
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
*减去8的目的是为了提早发现扩容问题
*/
//扩容机制
private void grow(int minCapacity) {
// overflow-conscious code
//获取之前数组的唱的
int oldCapacity = elementData.length;
//进行位运算 扩展到原来长度的一半 10 + 5
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果扩展后还是小于插入数组的长度 则newCapacity = minCapacity;
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果扩容后的数据大于MAX_ARRAY_SIZE 调用hugeCapacity(minCapacity)
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
private static int hugeCapacity(int minCapacity) {
//如果minCapacity小于0 则抛出内存溢出异常
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
//最后调用Arrays.copyOf(elementData, newCapacity)方法 并赋值给elementData
elementData = Arrays.copyOf(elementData, newCapacity);
}
//传入起始数组 新数组的长度
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
//定义一个copy数组
//进行判断
//第一步就是:创建指定长度的某种类型的数组。
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
//调用本地方法
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
public static Object newInstance(Class<?> componentType, int length)
throws NegativeArraySizeException {
return newArray(componentType, length);
}
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
ArrayList的部分源码解析二
//将元素添加到制定位置
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
//如果传入的值的索引大于数组本身的大小或者小于0则会报越界异常
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
引入外部知识点:Lambda表达式及函数式接口介绍
Lambda表达式是java8中最重要的新功能之一。使用Lambda表达式可以替代只有一个抽象函数接口的实现,告别匿名内部类,代码看起来更简洁易懂。Lambda表达式同时还提升了对集合、框架的迭代、遍历、过滤数据的操作。
Lambda表达式使用场景
任何有函数式接口的地方
函数式接口:只有一个抽象方法的接口,排除Object中的方法,default,static修饰的方法
函数式接口的定义:
1.定义只有一个抽象方法的接口
2.在接口上加注解@FunctionalInterface
@FunctionalInterface
public interface LambdaDemo {
int add();
//不包括Object中的方法
public int hashCode();
//不包括default修饰的方法
public default boolean increment(){
return true;
}
//不包括static修饰的方法
static void mul(){};
}
jdk1.8中定义Lambda表达式
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
Lambda表达式详解
Lambda表达式是一个对象,是一个函数式接口的示例
Lambda表达式语法
语法 :
函数式接口中定义的方法中的参数->实现函数式接口的具体内容
ArrayList集合安全问题
思考在多线程环境下ArrayList是否线程安全?
示例代码:
public void duoThread(){
List<String> list = new ArrayList<>();
for (int i = 0; i < 20; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
以上代码会出现并发修改异常 为什么会出现?
一个线程还没来得及添加数据完成 另外一个线程就已经将数据添加进去了
如何解决?有哪些解决方法?
解决方法1:
使用Vector集合
原因:通过分析Vector的add方法
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
加入了synchronized 导致同一时间只能有一个线程进行访问
解决方法二:
使用Collections工具类
Collections.synchronizedList(new ArrayList<>());
RandomAccess接口是一个标记接口,用以标记实现的List几个具备快速随机访问的能力,随机访问List中的任何一个元素。
ArrayList实现了RandomAccess接口,而LinkedList没有
用于判断该list属于哪种
第一种for循环最快速 第二种Itertor迭代器最快速
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
解决方法三:
使用java.util.Concurrent中的
final transient ReentrantLock lock = new ReentrantLock();
transient:被这个修饰的字段的生命周期仅存于调用者的内存而不会写到磁盘里持久化
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
总结:ArrayList是一个动态数组 查询快 增删慢 不同步,非线程安全,允许插入空值 初始容量为10 扩容机制 。。。。。。
LinkedList
LinkedList的底层实现是双向链表
/**
* Constructs an empty list.
*/
public LinkedList() {
}
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
分析带参构造函数
1.addAll()方法源码分析
//第一次添加时 size=0
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
//index = 0
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);
//因为属于Collection类以及他的实现子类 则可以调用相对应的toArray方法 用一个Object的数组接收
Object[] a = c.toArray();
int numNew = a.length;
//如果数组的长度为0则插入失败
if (numNew == 0)
return false;
//定义两个节点类型
Node<E> pred, succ;
//如果index == 0 连接
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
//遍历object数组
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
//新建一个节点
Node<E> newNode = new Node<>(pred, e, null);
//当插入的是第一个节点时 pred == null
//first指向第一个节点
//当遍历到第2个节点时
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;
}
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
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;
}
}
Node<E> node(int index) {
// assert isElementIndex(index);
//如果索引位置靠链表前半部分,从头开始遍历
if (index < (size >> 1)) {
Node<E> x = first;
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;
}
}
add()方法
public void add(int index, E element) {
//检查是否越界
checkPositionIndex(index);
//插入位置与当前数量相同 说明是尾部插入
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
//从尾部插入
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++;
}
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
检索操作:
get
//获取到头结点的值
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
//获取到尾部节点的值
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
//获取到制定位置的值
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
其余方法自己分析即可
2.2Map接口
Map 是一种把键对象和值对象映射的集合,它的每一个元素都包含一对键对象和值对象。 Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。
Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap
HashMap原理分析:
底层实现:数组+链表+红黑树(JDK1.8增加了红黑树部分)
红黑树介绍:https://blog.csdn.net/v_july_v/article/details/6105630
我们在一般情况下,都会使用无参构造方法创建 HashMap。但当我们对时间和空间复杂度有要求的时候,使用默认值有时可能达不到我们的要求,这个时候我们就需要手动调参。在 HashMap 构造方法中,可供我们调整的参数有两个,一个是初始容量 initialCapacity,另一个负载因子 loadFactor。
//默认数组容量大小:16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//数组最大容量大小:2 的 30 次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//使用树而不是链表的计数阈值,将元素添加到至少这么多的节点的链表中时,链表将转换为树
static final int TREEIFY_THRESHOLD = 8;
//可以进行树化的最小数组容量
static final int MIN_TREEIFY_CAPACITY = 64;
//存储键值对元素的数组,分配后,长度始终是 2 的幂(哈希桶数组)
transient Node<K,V>[] table;
//此映射中包含的键-值映射数,即当前数组中的元素数量
transient int size;
//主要用于记录HashMap内部结构发生变化的次数。
transient int modCount;
//哈希表所能容纳的最大键值对个数,下次扩容的临界值,size>=threshold 数组就会扩容
int threshold;
//负载因子
final float loadFactor;
构造方法:
//构造一个带指定初始容量和加载因子的空 HashMap。
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);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
//构造一个空的hashMap 默认容量为16
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
initialCapacity
初始容量
- 官方要求我们要输入一个2的N次幂的值,比如说2、4、8、16等等这些,但是我们忽然一个不小心,输入了一个20怎么办?没关系,虚拟机会根据你输入的值,找一个离20最近的2的N次幂的值,比如说16离他最近,就取16为初始容量。
loadFactor
负载因子
- 默认值是0.75。负载因子表示一个散列表的空间的使用程度,有这样一个公式:
initailCapacity*loadFactor = HashMap
的容量。 - 所以负载因子越大则散列表的装填程度越高,也就是能容纳更多的元素,元素多了,链表大了,所以此时索引效率就会降低。反之,负载因子越小则链表中的数据量就越稀疏,此时会对空间造成烂费,但是此时索引效率高
分析hashmap的put方法
图片来自于美团技术团队
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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是否为空 或者 table的长度是否为0
//如果为空 或者长度为 0 则
if ((tab = table) == null || (n = tab.length) == 0)
//开始扩容
//返回一个newTab
//n=16
n = (tab = resize()).length;
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 &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
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);
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;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
static final int hash(Object key) {
int h;
// h = key.hashCode() 为第一步 取hashCode值
// h ^ (h >>> 16) 为第二步 高位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这里的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算。
对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用方法一所计算得到的Hash码值总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,模运算的消耗还是比较大的,在HashMap中是这样做的:调用方法二来计算该对象应该保存在table数组的哪个索引处。
这个方法非常巧妙,它通过h & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。
final Node<K,V>[] resize() {
//定义一个节点数组
Node<K,V>[] oldTab = table;
//第一次插入 oldTab == null -> oldCap =0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//oldThr = 0
int oldThr = threshold;
//new Cap new Thr = 0
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//第一次无参构造方法 添加
//newCap = 16
newCap = DEFAULT_INITIAL_CAPACITY;
//newThr = 16 * 0.75 = 12
//扩容了8+4 = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//threshold=12
threshold = newThr;
//新建一个大小为16的节点数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//复制给table
table = newTab;
//第一次插入 oldTab为空
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
HashTable
Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
LinkedHashMap
LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
底层实现:HashMap+双向链表
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);
}
}
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
treeMap
TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
ArrayList、LinkedList、Vector 的区别
ArrayList | LinkedList | Vector | |
---|---|---|---|
底层实现 | 数组 | 双向链表 | 数组 |
同步性及效率 | 不同步,非线程安全,效率高,支持随机访问 | 不同步,非线程安全,效率高 | 同步,线程安全,效率低 |
特点 | 查询快,增删慢 | 查询慢,增删快 | 查询快,增删慢 |
默认容量 | 10 | / | 10 |
扩容机制 | int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5 倍 | / | 2 倍 |
2.3Set接口
hashSet
hashSet无序
private transient HashMap<E,Object> map;
//从总可以开出map的值为一个Object对象 不变
//保证不重复依靠他的键 通过hash运算
private static final Object PRESENT = new Object();
HashSet如何检查重复
当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。
hashCode()与equals()的相关规定:
- 如果两个对象相等,则hashcode一定也是相同的
- 两个对象相等,equals方法返回true
- 两个对象有相同的hashcode值,它们也不一定是相等的
- 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
无参构造:
public HashSet() {
map = new HashMap<>();
}
底层实现:HashMap
LinkedHashSet
treeset
HashSet、TreeSet、LinkedHashSet的区别
HashSet | TreeSet | LinkedHashSet | |
---|---|---|---|
底层实现 | HashMap | 红黑树 | LinkedHashMap |
重复性 | 不允许重复 | 不允许重复 | 不允许重复 |
有无序 | 无序 | 有序,支持两种排序方式,自然排序和定制排序,其中自然排序为默认的排序方式。 | 有序,以元素插入的顺序来维护集合的链接表 |
时间复杂度 | add(),remove(),contains()方法的时间复杂度是O(1) | add(),remove(),contains()方法的时间复杂度是O(logn) | LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet,时间复杂度是 O(1)。 |
同步性 | 不同步,线程不安全 | 不同步,线程不安全 | 不同步,线程不安全 |
null值 | 允许null值 | 不支持null值,会抛出 java.lang.NullPointerException 异常。因为TreeSet应用 compareTo() 方法于各个元素来比较他们,当比较null值时会抛出 NullPointerException异常。 | 允许null值 |
比较 | equals() | compareTo() | equals() |
集合工具类Collections
Collections:集合工具类,方便对集合的操作。这个类不需要创建对象,内部提供的都是静态方法。
数组工具类 Arrays
用于操作数组对象的工具类,里面都是静态方法。