Collection
Collection中的元素一个一个存放。Collection是顶层接口
1、List
底层都是数组实现:
优点:查询速度快
缺点:删除、插入速度慢
特点:数据可重复
【1】ArrayList :线程不安全,效率高
----ArrayList的源代码。
JDK1.7:
底层ElementData数组,直接容量初始化为10,
扩容时,数组长度变为原来的1.5倍:newLength = oldLength + (oldLength >> 1);
问题:要是没有数据,浪费内存。
JDK1.8:
底层数组:在调用参数为空构造方法的时,底层数组为[]
调用add方法后,底层数组才重新赋值为新数组
新数组长度为10----->节省了内存,在添加元素后才创建长度为10的数组。扩容还是1.5倍
【2】Vector: ---->扩容方法不一样,两倍 ,线程安全,效率低,被淘汰。
【3】LinkedList
泛型
【1】泛型如果不指定,就会被擦除,泛型对应的类型为Object。
【2】推荐指定泛型
【3】泛型类构造器不可以带泛型
public class TestGeneric<A,B,C>{
//构造器,再这里带泛型这种写法是错误的
public TestGeneric<A,B,C>(){
}
}
【4】不同泛型的引用类型不可以相互赋值
【5】继承情况分两种,如下:
public class Father<E>{
}
//1、第一种,父类指定泛型
public class Child extends Father<String>{
}
//2、第二种,父类不指定泛型,那么子类也会成为一个泛型类,具体类型可以在创建子类对象的时候确定。
public class Child<E> extends Father<E>{
}
【6】泛型类中的静态方法不能使用泛型。
泛型具体类型是在创建对象的时候才确定的,而静态方法会优先与对象创建被加载到内存中。
编译的时候就不会通过。
【7】不能通过泛型直接创建数组,但是可以创建引用。
public class Test<E>{
public void a(){
E[] e = new E[10];//这样,编译器不会通过
//解决方案
E[] e = (E[])new Object[10];
}
}
【8】泛型方法
泛型方法对应的那个泛型参数类型,和当前所在的这个类是否是泛型类,泛型是什么,无关
public class Father<E>{
//不是泛型方法
//这个方法不能是静态方法
public void a(E e){
}
//是泛型方法
//T的类型在调用方法时确定
//泛型方法可以是静态方法,具体泛型在调用时确定
public <T> void b(T t){
}
}
【9】A和B是子类父类的关系。但是List<“A”>,List<“B”>不存在继承关系,是并列关系。这里的泛型限制了你编译时的操作,List的底层还是Object。
【10】泛型受限
public class Student extends Person {
public static void main(String[] args) {
List<Object> list1 = new ArrayList<>();
List<Person> list2 = new ArrayList<>();
List<Student> list3 = new ArrayList<>();
/**
* 泛型的上限:
* List<? extends Person> 是List<Person>的父类,也是List<Person的子类>的父类</>
父类引用可以指向子类对象
*/
List<? extends Person> list = new ArrayList<>();
list = list1;//会报错,Object类是Person的父类
list = list2;
list = list3;
/**
* 泛型的下限:
* List<? super Person> 是List<Person>的父类,是List<Person父类>的父类
*/
List<? super Person> list4 = new ArrayList<>();
list4 = list1;
list4 = list2;
list4 = list3;//会报错,因为List<Student>不是其子类
}
}
JAVA序列化:
【1】Java中对象的序列化指的是将对象转换成以字节序列的形式来表示。
【2】一个序列化后的对象可以被写到数据库或文件中,也可用于网络传输。
【3】序列化后的最终目的是为了反序列化,恢复成原先的Java对象。
【4】Java中transient关键字的作用,简单地说,就是让某些被修饰的成员属性变量不被序列化。
为什么要不被序列化呢,主要是为了节省存储空间。
JAVA通配符
public class Test{
public static void main(String[] args){
C c = new C();
//使用通配符可以传递任何你想传递的类型的数组
c.add(new List<Integer>());
c.add(new List<String>());
c.add(new List<Object>());
}
}
class C{
public void add(List<?> list){
//内部遍历的时候,使用Object,不能使用?
for(Object o : list)
}
}
Collection总结
迭代器Iterator
具体实现:
|| 增强for循环底层也是通过迭代器实现的。
|| 并发修改异常 ConcurrentModificationException。使用迭代器时产生。
public class Test{
public static void main(String[] args){
ArrayList<String> list = new ArrayList<>();
list.add("aa");
list.add("bb");
list.add("cc");
list.add("dd");
Iterator it = list.iterator();
while(it.hasNext()){
if("cc".equals(it.next)){
list.add("xx");//此处发生错误
}
}
}
}
产生错误原因:迭代器和list同时操作集合
解决方案:让一个人做。
引入:ListIterator
public class Test{
public static void main(String[] args){
ArrayList<String> list = new ArrayList<>();
list.add("aa");
list.add("bb");
list.add("cc");
list.add("dd");
Iterator it = list.ListIterator();//此处修改
while(it.hasNext()){
if("cc".equals(it.next)){
it.add("xx");//此处发生错误
}
}
}
}
2、Set(唯一 无序【与List相比】)
---->方法与Collection一致,但没有关于索引的方法,
遍历方法:
1、增强for循环
2、Iterator
【1】HashSet
--HashSet底层简要原理图:
- 首先是待存放数组。
- 然后根据HashCode方法,计算出相应的Hash值。
- 根据Hash值加相应的匹配方法计算在数组中的下标。
- 必须重写equals方法,才能判断是否为重复的值,才会不重复写入。
- 下标相等的数组元素(不包含上面equals相等的元素),通过链表形式链接出去
- Hash表 = 数组 + 链表(底层实现)。
HashSet源码分析
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
//其实也是用HashMap实现的,只不过value值,都是他给你new的一个Object
private transient HashMap<E,Object> map;
// Dummy(虚拟) value to associate with
// an Object in the backing Map
private static final Object PRESENT = new Object();
//无参构造
public HashSet() {
map = new HashMap<>();
}
//有参构造,参数为初始容量和装填因子
public HashSet(int initialCapacity, float loadFactor) {
//后续,同HashMap
map = new HashMap<>(initialCapacity, loadFactor);
}
}
内部比较器与外部比较器
public class Student implements Comparable<Student>{
public String name;
public int age;
public double height;
//实现Comparable接口
@Override
public int compareTo(Student o) {
//比较年龄
//return this.age - o.getAge();
//比较身高
/**
* 调用了Double的CompareTo方法,直接相减的话,无法得到整数,
* 强制类型转换也会有问题
**/
/**
*return
*((Double)this.height).compareTo((Double)o.getHeight());
**/
//比较姓名
return this.name.compareTo(o.getName());
}
}
//外部比较器,优点,利用多态,可扩展性增强
class BiJiaoQi1 implements Comparator<Student>{
//比较年龄
@Override
public int compare(Student o1, Student o2) {
return o1.getAge() - o2.getAge();
}
}
class BiJiaoQi2 implements Comparator<Student>{
//比较名字
@Override
public int compare(Student o1, Student o2) {
return o1.getName().compareTo(o2.getAge());
}
}
class Test{
public static void main(String[] args){
Student s1 = new Student("zhangsan",18,160.6);
Student s2 = new Student("Lisi",19,180.5);
Comparator bj1 = new BiJiaoQi1();
bj1.compare(s1,s2);
Comparator bj2 = new BiJiaoQi2();//利用多态
bj2.compare(s1,s2);
}
}
NOTE:LinkedHashSet可以按照输入顺序输出,通过链表链接上了。
【2】TreeSet
-
还是满足唯一,无序(没有按照输入顺序进行输出)。
-
有序。按照升序进行遍历输出,所以必须实现比较器才行。
-
比较器一是实现唯一,二是实现升序。
public class Test01 {
public static void main(String[] args) {
//利用内部比较器,Student实现了Comparable接口
TreeSet<Student> set = new TreeSet<>();
set.add(new Student("cili",18,176.8));
set.add(new Student("alili",17,170.2));
set.add(new Student("blili",15,176.3));
set.add(new Student("clili",20,176.5));
set.add(new Student("clili",21,174.0));
set.add(new Student("flili",19,180));
System.out.println(set.size());
System.out.println(set);
//利用外部比较器,Student没有实现Comparable接口
Comparator bj1 = new BiJiaoQi1();
//传入外部比较器即可。
TreeSet<Student> set = new TreeSet<>(bj1);
}
}
输出结果:
– 观察输出结果可以明显发现,clili并没有存储两次,size为5(因为Student的内部比较器比较的是名字)
– 排序结果也为按名字升序排序
Map
-
Hashtable :jdk1.0开始
- 效率低,线程安全,key不可以存入null值,会报空指针异常。
- 方法与HashMap一致
-
HashMap:jdk1.2开始
- 无序,唯一,按照Key对应
- 集合数据对应的类型必须实现hashCode()和equals()方法
- 效率高,线程不安全,key可以存入null值,null也遵循唯一
HashMap源码:jdk1.8
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//重要属性
//默认容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子 | 装填因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//元素集合
transient Set<Map.Entry<K,V>> entrySet;
//元素个数
transient int size;
//修改次数
transient int modCount;
//临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
//装填因子
final float loadFactor;
//
/// 构造
//
//空构造器
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//带参构造
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;
初始化threshold大小
this.threshold = tableSizeFor(initialCapacity);
}
/**
>>> 无符号右移
| 按位或
下面这个简化得有点厉害,就是个语法糖。
表达的意思是:
找到第一个比入参大的2的高次幂。所以最后要n+1
eg:传入65
n = 65--->1000001
**/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1; //1000001 | 0100000 = 1100001
n |= n >>> 2; //1100001 | 0011000 = 1111001
n |= n >>> 4; //1111001 | 0000111 = 1111111
n |= n >>> 8; //1111111 | 0000000 = 1111111
n |= n >>> 16;//.... n = 1111----->不用再继续,会超出MAXMUM_CAPACITY
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
//最后得出n = 128
}
/**
* Implements Map.put and related methods.
*
* @param hash 得出hash值
* @param key 存储的key值
* @param value 存储的value值
* @param onlyIfAbsent 如果设置为true,则不会替换,否则会替换
* @param evict 如果设置为false,则table处于创建态
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;//table
Node<K,V> p;
//n为map集合的容量大小
int n, i;
//如果数组为空,Resize();
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//判断table[i]是否为null,如果是,直接插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//key是否存在,存在的话直接覆盖value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//hash值不相等,即key不相等;为红黑树结点
//否则,tab[i](也就是p)是否为TreeNode
//是的话,直接在红黑树中插入结点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {//这个else一定会进入
//否则,遍历链表
for (int binCount = 0; ; ++binCount) {
//如果不存在,在尾部插入新结点
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//链表长度是否大于8,大于8转成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
// 相等,跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表下·
p = e;
}
}
// 表示在桶中找到key值、hash值与插入元素相等的结点
if (e != null) {
V oldValue = e.value; //记录e.value
if (!onlyIfAbsent || oldValue == null)
e.value = value;//替换掉旧值
// 访问后回调
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
++modCount;
if (++size > threshold)//容量增加后是否超过阈值,若超过,则扩容
resize();
//插入后回调
afterNodeInsertion(evict);
return null;
}
}
put方法
JDK1.8中,HashMap采用数组+链表+红黑树实现
当链表长度超过阈值(8)时,将链表转换为红黑树
HashMap的数据存储实现原理(同样来自上篇博客,他写得很好)
流程:
1. 根据key计算得到key.hash = (h = k.hashCode()) ^ (h >>> 16);
2. 根据key.hash计算得到桶数组的索引index = key.hash & (table.length - 1),这样就找到该key的存放位置了:
① 如果该位置没有数据,用该数据新生成一个节点保存新数据,返回null;
② 如果该位置有数据是一个红黑树,那么执行相应的插入 / 更新操作;
③ 如果该位置有数据是一个链表,分两种情况一是该链表没有这个节点,另一个是该链表上有这个节点,注意这里判断的依据是key.hash是否一样:
如果该链表没有这个节点,那么采用尾插法新增节点保存新数据,返回null;如果该链表已经有这个节点了,那么找到该节点并更新新数据,返回老数据。
经典面试题
(1)装填因子/加载因子为什么为0.75?
如果装填因子设置为1:空间利用率得到了很大的满足,但很容易产生碰撞,产生链表,导致查询效率低
如果装填因子设置太小,比如0.5:碰撞概率低,扩容,产生链表的几率低,查询效率高,但空间利用率太低
所以取了一个折中的值:0.75
(2)主数组的长度为什么是2^n?
原因1:使得 hash&(length)等效于 hash%length----->取余操作的效率没有位运算高
原因2:降低hash冲突
- TreeMap
- 唯一,有序(按照一定顺序输出,不是指和输入顺序一致)
-还是要有内部比较类或者外部比较类 - 二叉搜索树
- 红黑树
- 唯一,有序(按照一定顺序输出,不是指和输入顺序一致)
TreeMap底层原理简图:
TreeMap源码分析:
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
//外部比较器
private final Comparator<? super K> comparator;
//根结点
private transient Entry<K,V> root;
//结点数量
private transient int size = 0;
//修改次数
private transient int modCount = 0;
//空构造器
//将comparator初始化为null,这种时候,Tree的key类型的类必须实现内部比较器
public TreeMap() {
comparator = null;
}
//使用外部比较器
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
/**
* 查找。
**/
//根据键值获取value
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
//查找方法的具体实现
final Entry<K,V> getEntry(Object key) {
//提供外部比较器的情况
if (comparator != null)
return getEntryUsingComparator(key);
//以下为不提供外部比较器的情况
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
//从根开始
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key); //与当前结点的键值比较大小
//比他小,查找左子树
if (cmp < 0)
p = p.left;
//比他大,查找右子树
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;//查找失败,返回null
}
//使用外部比较器进行查找,大同小异
final Entry<K,V> getEntryUsingComparator(Object key) {
@SuppressWarnings("unchecked")
K k = (K) key;
Comparator<? super K> cpr = comparator;
if (cpr != null) {
Entry<K,V> p = root;
while (p != null) {
int cmp = cpr.compare(k, p.key);//使用外部比较器比较
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
}
return null;
}
/**
* 插入。
**/
public V put(K key, V value) {
Entry<K,V> t = root;//根
if (t == null) {//根为空,将新插入结点设置为根
compare(key, key);
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;//返回null,因为之前该key位置没有元素
}
//根不为空的情况
int cmp;
Entry<K,V> parent;//父节点
Comparator<? super K> cpr = comparator;
//外部比较器的情况
if (cpr != null) {
do {
parent = t;//当前key值与父节点比较,大于进入右子树,小于进入左子树
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);//找到key值一样的,覆盖
} while (t != null);
}
//使用内部比较器的情况
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);//t.setValue的返回值是oldValue
} while (t != null);
}
//如果没有key值一致的
//新创建一个结点Entry,插入搜索到的父节点
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);//插入后进行红黑树的调整
size++;
modCount++;
return null;//新结点,直接返回null
}
}
Put流程
[1]判断根是否为空,为空将新结点置为根节点。
[2]不为空的情况下,分为使用内部比较器还是外部比较器,都从根节点出发进行比较
- 外部比较器使用comparator
- 内部比较器,key的类型所属类需要实现内部比较器
[3]如果现存结点的key值与新插入的key值一致,则覆盖,返回旧值。
[4]如果不存在,根据二叉搜索的结果,将新插入的值创建Entry,设置为叶节点。
红黑树的调整:
NOTE:叔叔为父节点的兄弟结点,我们将空域也看作一个结点,爷结点为父节点的父节点。
1、插入的为根节点,直接染黑
2、否则染为红色,插入
若破坏了红黑树的特性,需要染色
- 若叔叔结点为红色
- 叔结点,父节点,爷结点染色(都变为与原来相反的颜色)---->爷结点变为新结点(也就是把爷结点当作新插入的结点),重复上述步骤
- 若叔叔结点为黑色
- LL旋转,旋转完,将父和爷染色【以父结点为旋转中心】
- RR旋转,------------------
- LR旋转,旋转完,将新节点与爷结点染色【先以父结点为旋转中心,再以新结点为旋转中心】
- RL旋转,-------------------------
调整过程图示:
红黑树源码解析见此博客