集合
1.Collection
boolean add(E e):添加元素
boolean remove(Object o):从集合中移除指定元素
void clear():清空集合中的元素
boolean contains(Object o):判断集合中是否存在指定元素
boolean isEmpty():判断集合是否为空
int size():集合中元素个数
1.1 迭代器iterator
迭代:Collection几何元素的通用获取方法。在取元素之前先要判断集合中是否有元素,如果有,就把这个元素取出来,继续再判断;一直把集合中所有元素全部取出。
常用方法:
public E next():返回迭代的下一个元素
public boolean hasNext():如果有元素可以迭代则返回true
使用步骤:
1.使用集合中的方法iterator()获取迭代器的实现类对象,使用iterator接口接收
2.使用iterator接口中的方法hasNext()判断 是否还有下一个元素
3.使用iterator接口中的方法next()取出下一个元素
public class Demo01 {
public static void main(String[] args) {
Collection<String> c=new ArrayList<>();
c.add("Kobe");
c.add("James");
c.add("Jordan");
c.add("Ray");
Iterator<String> it=c.iterator(); //使用集合中的方法iterator()获取迭代器的实现类对象,
// 使用Iterator接口接收
while(it.hasNext()){
//遍历集合,用hasNext()判断是否有下一个元素
System.out.println(it.next()); //用next()取出下一个元素
}
}
}
增强for循环:
for(元素的数据类型 变量:Collection集合or数组){
}
用于遍历Collection和数组,通常只进行遍历,不要再遍历的过程中对集合元素进行增删操作
public class Demo02 {
public static void main(String[] args) {
int[] array={
24,23,10,8};
for(int a:array){
System.out.println(a);
}
}
}
1.2 泛型
泛型是一种未知的数据类型,当我们不知道用什么数据类型时,可以使用泛型
创建集合对象不使用泛型:
好处:默认类型是Object类型,可以存储任意类型的数据
坏处:不安全,容易引发异常
使用泛型:
好处:
避免了类型转换的麻烦
把运行期异常,提升到了编译期
public class GenericClass<E> {
private E name;
public E getName() {
return name;
}
public void setName(E name) {
this.name = name;
}
}
public class Demo03 {
public static void main(String[] args) {
GenericClass gc=new GenericClass();
gc.setName("泛型");
}
}
泛型用于方法:
修饰符 <泛型> 返回值类型 方法名称(参数列表){}
public class GenericMethod {
public <M> void method01(M m){
System.out.println(m);
}
}
泛型用于接口:
第一种方式:
public interface GenericInterface <I>{
void method(I i);
}
public class GenericInterfaceImpl implements GenericInterface<String>{
@Override
public void method(String s) {
System.out.println(s);
}
}
第二种方式:
接口使用什么泛型,实现类就使用什么泛型
public class GenericInterfaceImpl<I> implements GenericInterface<I>{
@Override
public void method(I i) {
System.out.println(i);
}
}
public class demo04 {
public static void main(String[] args) {
GenericInterfaceImpl impl=new GenericInterfaceImpl();
impl.method("hello");
}
}
泛型通配符:
public class demo04 {
public static void main(String[] args) {
ArrayList<Integer> list1=new ArrayList<>();
list1.add(24);
list1.add(23);
ArrayList<String> list2=new ArrayList<>();
list2.add("Kobe");
list2.add("James");
method(list1);
method(list2);
}
public static void method(ArrayList<?> list){
Iterator<?> iterator = list.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
}
}
泛型上限限定:? extends E 代表使用的泛型只能E类型的子类/本身
泛型下限限定:? super E 代表使用的泛型只能是E类型的父类/本身
2. ArrayList
2.1 底层数据结构
ArrayList集合介绍
List 接口的可调整大小的数组实现。线程同步不安全。其本质为对象动态数组。
数组:一旦初始化长度就不可以发生改变
数组结构介绍
增删慢:每次删除元素,都需要更改数组长度、拷贝以及移动元素位置。
查询快:由于数组在内存中是一块连续空间,因此可以根据地址+索引的方式快速获取对应位置上的元素。
2.2 常用方法
public boolean add(E e) :将指定的元素追加到此列表的末尾。
public void add(int index,E element):将指定的元素,添加到该集合中的指定位置上
public E get(int index):返回集合中指定位置的元素
public E remove(int index):移除列表中指定位置的元素,返回的是移除的元素
public E set(int index,E element):用指定元素替换集合中指定位置的元素,返回值事更新前的元素
public void clear():清空集合所有数据
public boolean contains(Object o):判断集合是否包含指定元素
public class demo05 {
public static void main(String[] args) {
List<Integer> list=new ArrayList<>();
list.add(23);
list.add(24);
list.add(10);
list.add(8);
list.add(2,35);
System.out.println(list); //[23, 24, 35, 10, 8]
System.out.println(list.get(2)); //35
list.remove(2);
System.out.println(list); //[23, 24, 10, 8]
list.set(2,30);
System.out.println(list); //[23, 24, 30, 8]
}
}
public boolean addAll(Collection<?extends E> c):按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。
public boolean addAll(i nt index,Collection<? extends E> c):将指定集合中的所有元素插入到此列表中,从指定的位置开始。
2.3 注意事项
ArrayList是如何扩容的?
第一次扩容10
以后每次都是原容量的1.5倍
ArrayList插入删除一定比LinkedList慢吗?
1.数组删除元素确实要比链表慢,慢在需要创建新数组,还有比较麻烦的数据拷贝,但是在ArrayList底层不是每次删除元素都需要扩容,因此在这个方面相对于链表来说数组的性能更好
2.LinkedList删除元素之所以效率并不高,其原理在于底层先需要对整个集合进行折半的动作,然后又需要对集合进行遍历一次,这些操作导致效率变低
ArrayList 和 LinkList区别?
ArrayList
基于动态数组的数据结构
对于随机访问的get和set,ArrayList要优于LinkedList
对于随机操作的add和remove,ArrayList不一定比LinkedList慢 (ArrayList底层由于是动态数组,因此
并不是每次add和remove的时候都需要创建新数组)
LinkedList
基于链表的数据结构
对于顺序操作,LinkedList不一定比ArrayList慢
对于随机操作,LinkedList效率明显较低
3. LinkedList
3.1 底层数据结构
LinkedList底层的数据结构是一个双向链表。
3.2 常用方法
public void addFirst(E e):将指定元素插入列表头
public void addLast(E e):将指定元素插入列表尾
public E getFirtst():返回第一个元素
public E getLast():返回最后一个元素
public E removeFirst():移除并返回列表第一个元素
public E removeLast():移除并返回列表最后一个元素
public E pop():从此列表所表示的堆栈处弹出一个元素
public void push(E e):将元素推入此列表所标识的堆栈
public boolean isEmpty():如果列表不包含元素,则返回true
3.3 注意事项
LinkedList是无容量限制的;
LinkedList是非线程安全的;
LinkedList是基于双向链表实现的,当数据顺序无关的情况下,选择ArrayList还是LinkedList要从各动作的执行效率综合考虑。
4. Map
4.1 底层数据结构
Map<K,V>:K代表所维护的键的类型,V代表映射值得类型
Collection中的集合,元素是孤立存在的,向集合中存储元素采用一个个元素的存储方式
Map中的集合,元素是成对存在的。每个元素由键和值组成,通过键可以找到对应的值
Collection中的集合成为单列集合,Map中的集合称为双列集合
Map中键不可以重复,值可以重复,每个键只对应一个值
在JDK1.8之前,HashMap里用于存储单个键值对即key-value的是静态内部类Entry对象,其在HashMap中是以位桶和链表相结合的方式(拉链法)组织的;在JDK1.8之前的HashMap数据存储结构中,如果位桶数量不扩容,随着数据的增多链表长度会越来越长,查找的时候要逐一equals对比,速度会慢很多。因此JDK 1.8在解决哈希冲突时增加了红黑树方法,当冲突节点数量较小的时候,还是使用原始链表结构存储,当数量较大(>8)的时候,则将链表转化为红黑树存储。存储单个键值对的对象由原来的Entry变为了Node。
4.2 常用方法
public V put(K key,V value):把指定的键与指定的值添加到Map集合中,key不重复,返回null;key重复,会使用新的value替换map中重复的value,返回被替换的value值
public V remove(Object key):把指定的键所对应的键值对元素在Map集合中删除,返回被删除的值;如果key不存在,返回空。
public V get(Object key):根据指定的键,在Map集合中获取对应的值
boolean containsKey(Object key):判断集合中是否包含指定的键
public Set keySet():获取Map集合中所有的键,存储到Set集合中
public Set<Map.Entry<K,V> entrySet()>:获取到集合中所有的键值对对象的集合
public class Demo {
public static void main(String[] args) {
Map<String,Integer> map=new HashMap<>();
map.put("Kobe",24);
map.put("James",23);
map.put("Jordan",23);
map.put("Curry",30);
System.out.println(map);
//{Curry=30, Kobe=24, James=23, Jordan=23}
int value1=map.remove("James"); //23
int value2=map.get("Kobe"); //24
boolean value3=map.containsKey("Jordan"); //true
}
}
遍历
Set keySet()
public class Demo {
public static void main(String[] args) {
Map<String,Integer> map=new HashMap<>();
map.put("Kobe",24);
map.put("James",23);
map.put("Jordan",23);
map.put("Curry",30);
//使用Map集合中的keySet(),把Map集合中所有的key取出来,存储到一个Set集合中
Set<String> set=map.keySet();
//遍历Set集合
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
String key = iterator.next();
//通过Map集合中的方法get(key),通过key找到value值
int value=map.get(key);
System.out.println(key+":"+value);
}
}
}
entrySet()
public class Demo {
public static void main(String[] args) {
Map<String,Integer> map=new HashMap<>();
map.put("Kobe",24);
map.put("James",23);
map.put("Jordan",23);
map.put("Curry",30);
//使用Map集合中的entrySet(),把Map集合中多个Entry对象取出来,存储到一个Set集合中
Set<Map.Entry<String,Integer>> set=map.entrySet();
//遍历Set集合,获取每一个Entry对象
Iterator<Map.Entry<String, Integer>> it = set.iterator();
while (it.hasNext()) {
Map.Entry<String, Integer> entry = it.next();
//使用Entry对象中的方法获取键与值
String key=entry.getKey();
int value=entry.getValue();
System.out.println(key+":"+value);
}
}
}
4.3 注意事项
1.HashMap存取无序
2.HashMap键与值都可以为null,但是键位置只能是一个null
3.键位置是唯一的,底层的数据结构控制键
4.jdk1.8之前数据结构是:链表+数组;jdk1.8之后是:链表+数组+红黑树
5.阈值>8并且数组长度大于64,才将链表转换为红黑树,变为红黑树是为了高效的查询
4.4 面试题
1.面试题:HashMap中hash函数是怎么实现的?还有哪些hash函数的实现方式?
对于key的hashCode做hash操作,无符号右移16位然后做异或运算。
还有平方取中法,伪随机数法和取余数法。这三种效率都比较低。而无符号右移16位异或运算效率是最高的。至于底层是如何计算的我们下面看源码时给大家讲解。
2.面试题:当两个对象的hashCode相等时会怎么样?
会产生哈希碰撞,若key值内容相同则替换旧的value.不然连接到链表后面,链表长度超过阈值8就转换为红黑树存储。
3.面试题:何时发生哈希碰撞和什么是哈希碰撞,如何解决哈希碰撞?
只要两个元素的key计算的哈希码值相同就会发生哈希碰撞。jdk8前使用链表解决哈希碰撞。jdk8之后使用链表+红黑树解决哈希碰撞。
4.面试题:如果两个键的hashcode相同,如何存储键值对?
hashcode相同,通过equals比较内容是否相同。
相同:则新的value覆盖之前的value
不相同:则将新的键值对添加到哈希表中
5.在不断的添加数据的过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。
6.通过上述描述,当位于一个链表中的元素较多,即hash值相等但是内容不相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度(阀值)超过 8 时且当前数组的长度 > 64时,将链表转换为红黑树,这样大大减少了查找时间。jdk8在哈希表中引入红黑树的原因只是为了查找效率更高。
简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。如下图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4x0NHA7p-1610453975882)(img/哈希表.png)]
但是这样的话问题来了,传统hashMap的缺点,1.8为什么引入红黑树?这样结构的话不是更麻烦了吗,为何阈值大于8换成红黑树?
JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。 当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。
5. LinkedHashMap
5.1 底层数据结构
LinkedHashMap 继承了 HashMap。LinkedHashMap内部维护了一个双向链表,能保证元素按插入的顺序访问,也能以访问顺序访问,可以用来实现LRU缓存策略。
LinkedHashMap可以看成是 LinkedList + HashMap。
虽然LinkedHashMap增加了时间和空间上的开销,但是它通过维护一个额外的双向链表保证了迭代顺序。特别地,该迭代顺序可以是插入顺序,也可以是访问顺序。因此,根据链表中元素的顺序可以将LinkedHashMap分为:保持插入顺序的LinkedHashMap和保持访问顺序的LinkedHashMap,其中LinkedHashMap的默认实现是按插入顺序排序的。
5.2 常用方法
属性
//双向链接的头结点,最久的
transient LinkedHashMap.Entry<K,V> head;
//双向链接的尾结点,最新的
transient LinkedHashMap.Entry<K,V> tail;
//true表示最近最少使用次序(LRU),false表示插入顺序
final boolean accessOrder;
构造方法
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
public LinkedHashMap() {
super();
accessOrder = false;
}
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
可以看到,LinkedHashMap的构造方法都是默认调用了父类的构造方法,并且几乎都是把属性accessOrder 赋值为false,除了第五个将其作为参数初始化,也就是说,默认情况下,LinkedHashMap创建对象都是采用插入顺序的方式来维持键值对的次序的。
LRU算法的实现
public class Test {
public static void main(String[] args) {
Map map = new LinkedHashMap<Integer,Integer>(20,0.75f,true);
for (int i = 1; i<=5;i++){
map.put(i,i);
}
System.out.println("正常输出=="+map.toString());
map.get(3);
System.out.println("读取元素=="+map.toString());
map.put(6,6);
System.out.println("插入元素=="+map.toString());
}
}
//输出结果
正常输出=={
1=1, 2=2, 3=3, 4=4, 5=5}
读取元素=={
1=1, 2=2, 4=4, 5=5, 3=3}
插入元素=={
1=1, 2=2, 4=4, 5=5, 3=3, 6=6}
可以看到,当初始化的实例时传入值为 true 的 accessOrder 时,不管是插入元素还是读取元素,都是将最近用到的元素放到最后,这是因为 在put 和 get方法中都做了特定的处理。
5.3 注意事项
(1)LinkedHashMap继承自HashMap,具有HashMap的所有特性;
(2)LinkedHashMap内部维护了一个双向链表存储所有的元素;
(3)如果accessOrder为false,则可以按插入元素的顺序遍历元素;
(4)如果accessOrder为true,则可以按访问元素的顺序遍历元素;
(5)LinkedHashMap的实现非常精妙,很多方法都是在HashMap中留的钩子(Hook),直接实现这些Hook就可以实现对应的功能了,并不需要再重写put()等方法;
(6)默认的LinkedHashMap并不会移除旧元素,如果需要移除旧元素,则需要重写removeEldestEntry()方法设定移除策略;
(7)LinkedHashMap可以用来实现LRU缓存淘汰策略
6. TreeMap
6.1 底层数据结构
TreeMap是一个双列集合,底层由红黑树结构构成。元素中键不能重复,元素会按照大小顺序排序
6.2 常用方法
put():添加
get():获取
remove():移除
6.3 注意事项
TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。
TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。
TreeMap 实现了Cloneable接口,意味着它能被克隆。
TreeMap 实现了java.io.Serializable接口,意味着它支持序列化。
TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的
Comparator 进行排序,具体取决于使用的构造方法。
TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。
另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。
7. Queue
7.1 底层数据结构
队列是一种特殊的线性表,它只允许在表的前端进行删除操作,而在表的后端进行插入操作。
LinkedList类实现了Queue接口,因此我们可以把LinkedList当成Queue来用。
7.2 常用方法
void add(Object o):将指定元素加入此队列的尾部。
Object element():获取队列头部元素,但不删除该元素。
boolean offer(Object o):将指定元素加入此队列的尾部。当使用有容量限制的队列时,此方法通常比add(Object e)方法更好。
Object peek():获取队列尾部元素,但不删除该元素。如果队列为空,则返回null。
Object poll():获取队列头部元素,并删除该元素。如果队列为空,则返回null。
Object remove():获取队列的头部元素,并删除该元素
8. Stack
8.1 底层数据结构
栈是Vector的一个子类,它实现了一个标准的后进先出的数据结构。
堆栈只定义了默认构造函数,用来创建一个空栈。 堆栈除了包括由Vector定义的所有方法,也定义了自己的一些方法。
8.2 常用方法
boolean empty():测试堆栈是否为空。
Object peek( ) :查看堆栈顶部的对象,但不从堆栈中移除它。
Object pop( ) :移除堆栈顶部的对象,并作为此函数的值返回该对象。
Object push(Object element) :把项压入堆栈顶部。
int search(Object element) :返回对象在堆栈中的位置,以 1 为基数。