介绍
常见的集合框架底层设计大量的数据结构
1.数组
2.链表
3.树
4.队列
集合框架组成部分
Collection (存放单列数据)
List接口 存放数据可以允许重复的, 有序
ArrayList 底层基于数组数据结构实现
LinkedList 底层基于链表数据结构实现
Set接口 不允许存入重复数据 Set集合对数据做去重
HashSet 不允许存入重复数据 底层基于Map集合实现(HashMap)
Map (存放多列数据, 无序)
HashMap 底层 基于数组+链表实现(JDK1.7) (JDK1.8之后)基于数组+链表+红黑树实现
HashTable 与上一致,但是是线程安全的
迭代器
Iterator
使用
import java.util.*;
public class Test1 {
public static void main(String[] args) {
Collection<String> collection = new ArrayList<>();
collection.add("1");
collection.add("2");
collection.add("3");
//获取迭代器对象, list、set都可以
Iterator<String> iterator = collection.iterator();
while (iterator.hasNext()) { //判断是否还能获取到元素
System.out.println(iterator.next()); //获取元素并输出
}
}
}
手写
import java.util.List;
public class MyIterator<T> {
private List<T> list;
public MyIterator(List<T> list){
this.list = list;
}
private Integer count = 0;
public T next(){
if (list == null) {
throw new RuntimeException("list is null");
}
if (count >= list.size()) {
throw new RuntimeException("无法继续向下获取元素啦!");
}
return list.get(count++);
}
public boolean hasNext(){
if (list == null) {
throw new RuntimeException("list is null");
}
return count != list.size();
}
}
ListIterator
使用
import java.util.*;
public class Test2 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
//获取ListIterator对象
ListIterator<String> listIterator = list.listIterator();
while (listIterator.hasNext()) { //从头到位遍历
System.out.println(listIterator.next()); //计数器+1
}
//如果我们想实现从尾到头遍历, 必须先从头到尾进行一次遍历 -- 底层计数器+1
while (listIterator.hasPrevious()) { //从尾到头遍历
System.out.println(listIterator.previous()); //计数器-1
}
}
}
泛型
泛型原理: 底层 使用擦除机制
在编译阶段限制传递的类型
<T> <E> <K> <V> 常见的泛型,或者其他的字母都可以当做泛型
泛型类
public class Test<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
泛型方法
public class Test {
public <T> T show(T t) {
return t;
}
}
泛型接口
public interface Test<T> {
T show(T t);
}
public class TestImpl<T> implements Test<T>{
@Override
public T show(T t) {
return t;
}
}
public interface Test<T> {
<M> T show(T t, M m); //M的地方写T的话,参数中的T就不会用接口上的泛型T
}
public class TestImpl<T> implements Test<T> {
@Override
public <M> T show(T t, M m) {
return t;
}
}
擦除机制
import java.util.*;
public class Test4 {
public static void main(String[] args) {
//泛型原理: 底层 使用擦除机制
//在编译阶段限制传递的类型
List<String> strList = new ArrayList<>();
strList.add("1");
// strList.add(1); //编译报错
//说明: 将一个List集合泛型赋值给一个没有使用到泛型List集合 直接去除泛型--擦除机制
List list = strList;
list.add(1); //成功
/**
* 泛型是在编译阶段限制传递的类型 在运行阶段都是擦除 底层class
* 文件在运行 里面是没有 泛型
* 如何证明?
* java源代码 -> 编译成class
* xjad 工具 反编译(百度有)
* .class文件反编译成.java源代码文件
* 反编译后会发现, 泛型全部消失了,用到的地方全部变成了Object类型
*/
}
}
类型通配符
<?>
import java.util.*;
public class Test {
/**
* List<?> 只能用于接收 ?--可以接收所有的泛型 不能够用于添加
* 是可以做get操作 获取到类型是Object类型
*
* @param list
*/
public void printList(List<?> list) {
// list.add(""); //编译报错
Object o = list.get(0);
List<?> arrayList = new ArrayList<>();
// arrayList.add(""); //与上同理, 会报错
Iterator<?> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
上下限
import java.util.*;
public class Test {
/**
* 类型通配符上限
* List<? extends Test> 上限是Test类
* Test类和它的子类都可以接收
*
* @param list
*/
public void printList(List<? extends Test> list) {
}
/**
* 类型通配符下限
* List<? super Test> 下限是Test类
* Test类和它的父类都可以接收
*
* @param list
*/
public void printList2(List<? super Test> list) {
}
}
ArrayList
时间复杂度
o(1): 基于数组的下标index查询, 只需要查询一次
o(n): n--循环的次数, 从头查询到尾部, 例如: 根据元素查询 将整个数组遍历, 从头遍历到尾部
优缺点
最开始底层默认初始化数组的容量是10
底层基于数组实现, 查询效率是非常高, 增删效率是非常低 —- 错误,误区: 不是查询效率比较高
底层基于数组实现, 根据index下标查询效率非常高, 时间复杂度为o(1), 底层增删效率是非常低
如果根据元素值查询, 时间复杂度为o(n), 效率是非常的低
新增: 如果底层数组容量不够的情况下, 就会触发动态扩容机制, 效率非常低的
删除: 将删除后面的元素向前移动一位, 效率也是非常低
修改: 如果是根据index下标位置修改效率是非常高--时间复杂度为o(1)
如果是根据元素值修改, 效率是非常低, 时间复杂度为o(n)
竞争对手(Vector)
相同点:
ArrayList 和 Vector 默认初始化容量=10
底层都是基于数组实现
List接口下的子类
不同点:
ArrayList 线程不安全 Vector 线程是安全的
ArrayList 每次扩容是原来容量的1.5倍
Vector 每次扩容是原来容量的2倍, 但是可以自己设置扩容的容量
ArrayList 懒加载的形式初始化容量 Vector 直接通过构造函数初始化数组容量=10
总结
Arraylist底层是基于数组实现
add方法如何实现?
1.判断集合容量是否装的下
2.如果装不下则扩容是以1.5倍将原来数组的容量拷贝到新的数组中
Get方法如何实现?
直接提供了根据index下标查询, 效率非常高
Remove方法如何实现的呢?
查找到删除对应的index 下标位置+1 到最后index 元素值向前移动一位
链表数据结构
分类
单向链表
双向链表
环形链表
手写单向链表
public class Node<E> {
private E v;
private Node<E> next;
public static void main(String[] args) {
Node<String> node3 = new Node<>();
node3.v = "c";
Node<String> node2 = new Node<>();
node2.v = "b";
node2.next = node3;
Node<String> node1 = new Node<>();
node1.v = "a";
node1.next = node2;
showNode(node1);
}
//根据对该链表, 从头便利到尾部
public static void showNode(Node<?> node) {
Node<?> cuNode = node;
while (cuNode != null) {
System.out.println(cuNode.v);
cuNode = cuNode.next;
}
}
}
LinkedList
概述
1.LinkedList是双向链表实现的list
2.LinkedList是非线程安全的
3.LinkedList元素允许为null, 允许重复
4.LinkedList是基于链表实现的, 因此插入删除效率高, 查找效率低 (根据下标来删除, 效率还是非常低的)
5.LinkedList是基于链表实现的, 因此不存在容量不足的问题, 所以没有扩容的方法
6.LinkedList还实现了栈和队列的操作方法, 因此也可以作为栈、队列和双端队列来使用
手写简易版LinkedList
public class MyLinkedList<E> {
private Node<E> first; //第一个节点
private Node<E> last; //链表的最后一个节点
private int size; //LinkedList 存放的元素个数
//LinkedList底层是基于链表实现的
private static class Node<E> {
private E item; //当前节点的值
private Node<E> prev; //上一个节点
private Node<E> next; //下一个节点
/**
* @param prev 当前节点的上一个节点
* @param item 当前节点
* @param next 当前节点的下一个节点
*/
public Node(Node<E> prev, E item, Node<E> next) {
this.item = item;
this.prev = prev;
this.next = next;
}
}
public void add(E e) {
Node<E> l = last;
Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null) {
first = newNode;
} else {
l.next = newNode;
}
size++;
}
//折半查询
private Node<E> node(int index) {
if (index < (size >> 1)) {
//查询链表中间值的 左边
Node<E> f = first;
for (int i = 0; i < index; i++) {
f = f.next;
}
return f;
} else {
//查询链表中间值的 右边
Node<E> l = last;
for (int i = size - 1; i > index; i--) {
l = l.prev;
}
return l;
}
}
//根据下标查找元素
//时间复杂度为 o(n), 效率非常低
public E get(int index) {
//下标如果越界, 需要报错
return node(index).item;
}
//根据下标删除
//需要先根据index 查询对应的node节点, 时间复杂度已经是 o(n) ,效率很低
public E remove(int index) {
return unlink(node(index));
}
private E unlink(Node<E> node) {
// 删除链表效率非常高
E item = node.item; //获取删除节点元素值
Node<E> prev = node.prev; //删除节点的上一个节点
Node<E> next = node.next; //删除节点的下一个节点
//如果删除的节点, 上一个节点为空
if (prev == null) {
//删除的该头节点是头节点
first = next;
} else {
//删除节点上一个节点的.next = 删除节点的下一个节点
prev.next = next;
node.prev = null;
}
if (next == null) {
//删除的该头节点是尾结点
last = prev;
} else {
//将删除节点下一个节点的上一个节点指向 删除节点的上一个节点
next.prev = prev;
node.next = null;
}
size--;
return item;
}
}
Map
Map集合中, key是不允许重复的, 但是value值是可以重复的, 如果key存在,则直接修改value的值
Map集合是散列存放数据, 所以会存在散列的问题, 遍历顺序与存储顺序不一致 --例如: HashMap是无序的 而LinkedHashMap是有序的HashMap集合
HashSet
特点
HashSet 基于HashMap来实现的, 采用HashMap集合的key来存放的, 存放数据是散列没有顺序的, 是一个不允许有重复元素的集合
HashSet 允许有null值, 因为HashMap也是允许存放key值是为null的
HashSet 是无序的, 即不会记录插入的顺序
HashSet 存入对象, 去除重复数据, 需要重写对象元素对应的equals和hashCode的方法, 比较两个对象的成员属性值是否是一样的
手写简易版HashSet
import java.util.HashMap;
public class MyHashSet<E> {
private HashMap<E, Object> map;
private static final Object PRESENT = new Object();
public MyHashSet() {
map = new HashMap<>();
}
public void add(E e) {
map.put(e, PRESENT);
}
@Override
public String toString() {
return "MyHashSet{" +
"map=" + map +
'}';
}
}
HashCode
equals
equals作用: 误区 equals 就是在比较两个对象的值是否相等
equals属于 object 父类中 默认的情况下 在比较两个对象的内存地址是否相同
String 类, 重写了父类中华的 equals 比较两个字符串值是否相同
如果想能够实现比较两个对象中成员属性是否相等 需要重写
介绍
hashCode 属于 object 父类中, java虚拟机提供给每个对象生成一个 hashCode值, 整数类型(int类型), hashcode方法返回该对象的哈希码值, 支持该方法是为哈希表提供一些优点, 例如, java.util.Hashtable提供的哈希表
1.如果equals方法比较两个对象相等, 则HashCode值也一定相等
2.但是两个对象的HashCode值相等, 不代表使用equals比较也相等
3.如果两个对象的HashCode值相等, 但是值不同 专业术语: Hash冲突问题
4.如果equals方法比较两个对象相等, 则HashCode值也一定相等, 但是两个对象的HashCode值相等, 不代表使用equals比较也相等
比如:String str = "a";
Integer i = 97;
str.hashCode(); //hashCode = 97
i.hashCode(); //hashCode = 97
5.HashCode的常规协定是: 在Java应用程序执行期间, 在同一对象上多次调用hashCode方法时, 必须一致地返回相同的整数
6.hashCode的存在主要是用于查找的快捷性, 如Hashtable, HashMap等, hashCode是用来在散列存储结构中确定对象的存储地址的
7.如果类中重写了equals方法必须要重写HashCode方法
HashMap
介绍
HashMap存储区数据, 是无序的, 底层采用散列机制存放数据, key值可以为null值, 也可以为我们自定义的对象, null值会放在数组index=0的位置 (HashTable是不可以存放null值的)
如果我们的HashMap key存放我们自定义的对象, 需要重写自定义对象中的equals和hashCode方法
jdk1.7 采取数组 + 链表的形式
jdk1.8 采用数组 + 链表 + 红黑树
(数组,数组中每个Node值都相当于一个链表,然后用HashCode%数组长度,进行存放,然后有散列冲突情况下,就放到该位置元素链表的下一个Node节点)
手写简易版HashMap
public class MyHashMap<K, V> {
private Object[] objects = new Object[10000];
private class Entry<K, V> {
K k;
V v;
Integer hash;
Entry<K, V> next;
public Entry(K k, V v, Integer hash) {
this.k = k;
this.v = v;
this.hash = hash;
}
}
public void put(K k, V v) {
int hash = k.hashCode();
int index = hash % objects.length;
Entry<K, V> entry = (Entry<K, V>) objects[index];
while (entry != null) {
if (entry.k.equals(k)) {
entry.v = v;
return;
} else if (entry.next == null) {
entry.next = new Entry<>(k, v, hash);
return;
}
entry = entry.next;
}
objects[index] = new Entry<>(k, v, hash);
}
public V get(K k) {
int hash = k.hashCode();
int index = hash % objects.length;
Entry<K, V> entry = (Entry<K, V>) objects[index];
while (entry != null) {
if (entry.k.equals(k)) return entry.v;
entry = entry.next;
}
return null;
}
}
LinkedHashMap
说明
LinkedHashMap 与 HashMap集合用法都是相同的
唯一区别:
LinkedHashMap 是有序的HashMap集合
HashMap 是无序的LinkedHashMap 中, 每一次存放元素时, 会对每一个entry对象做一个双向链表的形式连接起来, 保证存入元素的有序, 遍历的时候直接遍历entry双向链表就可以了
LinkedHashSet
说明
LinkedHashSet是Set集合的一个实现, 具有set集合不重复的特点, 与HashSet集合不同的是可以保证元素顺序性, 也就是遍历顺序和插入顺序是一致的
LinkedHashSet底层是基于LinkedHashMap实现
手写简易版LinkedHashSet
import java.util.LinkedHashMap;
public class MyLinkedHashSet<E> {
private LinkedHashMap<E, Object> linkedHashMap;
private static final Object PRESENT = new Object();
public MyLinkedHashSet() {
linkedHashMap = new LinkedHashMap<>();
}
public void add(E e) {
linkedHashMap.put(e, PRESENT);
}
@Override
public String toString() {
return "MyLinkedHashSet{" +
"linkedHashMap=" + linkedHashMap +
'}';
}
}
工具类
Collections
Collections是单列集合操作的工具类, Collection单列集合
Collections.sort(list); //将指定的列表升序排序
Collections.reverse(list); //反转指定列表中的元素
Collections.shuffle(list); //使用默认的随机源随机排序指定的列表
Collections 不可以操作Map接口下的子类