怎么学
- List,Set,Map的基本API都必须会使用
- ArrayList和LinkedList的区别要能够回答上来 -> 应对面试
- 迭代器 -> 概念明白,API会用
- HashSet和TreeSet,先按Set的理解记原理,把步骤理顺,重点是HashSet,红黑树在后面会详细讲
- *弄清楚HashMap存储数据的原理
复习
集合
- 保存批量对象的工具
- 集合中保存的都是对象
- 如果使用集合保存基本类型的数据,实际保存的是包裹类的对象
- 继承关系
- 父接口 - Collection
- add()
- remove()
- size()
- contains()
- 子接口
- Set
- List
- 常用实现类
- List -> ArrayList, LinkedList
- Set -> HashSet,TreeSet
- 父接口 - Collection
泛型
-
使用<>来限定使用的类型
-
常用于集合API,限定某个集合中可以保存的数据的类型
- 向集合中存数据时,会自动进行类型的校验,如果类型不匹配,则编译不通过
- 从集合中获取数据时,自动进行类型的转换
-
为什么用泛型?
- 提高代码的严谨性和规范性
ArrayList和LinkedList的区别
- ArrayList 是基于数组结构保存批量数据的集合工具
- LinkedList 是基于双向链表保存批量数据的集合工具
- ArrayList的优点:
- 基于下标检索元素的效率高 -> 为什么效率高?
- ArrayList的缺点:
- 初始化集合时,会占用指定长度的内存空间,该空间进能够被该集合使用
- 如果底层数组满了,会执行动态扩容,实际是创建一个更长的数组,将原数组的内容复制到新数组中,会造成资源的消耗
- 在非末端添加或删除元素时,需要对其他元素进行位移操作,也会造成资源的消耗
- LinkedList的优点:
- 初始化时,不需要额外占用内存空间,每添加一个元素时,在内存中创建节点保存数据即可,因此也不涉及动态扩容的问题
- 在非末端添加或删除元素时,效率较高 -> 为什么效率高?仅需要修改前节点和后节点的相关引用,不会对集合中其他的节点造成影响
- LinkedList的缺点:
- 内部并不存在下标的概念,基于下标访问一个元素时,其实从头节点或尾节点,逐个查找,找到对应的元素,效率较低
集合应用
集合算法与数据结构
-
无论是Set集合,还是List集合,均提供了多个实现类
-
这些类对于在存放对象数据时,其内部的数据组织形式是完全不同的
-
数据组织的形式,称为数据结构。不同的数据结构,需要通过不同的算法来支持对其访问
-
常用的Set集合实现类包括:
- HashSet:基于哈希(Hash) 算法来存放和管理集合中的数据
- TreeSet:使用红黑树数据结构来存放和管理数据,集合中的数据按其顺序自然排序存放
public class TestSetData {
public static void main(String[] args) {
Set<String> hashSet = new HashSet<String>();
hashSet.add("36");
hashSet.add("20");
hashSet.add("57");
hashSet.add("26");
hashSet.add("78");
hashSet.add("16");
System.out.println("HashSet=" + hashSet);
System.out.println("--------------------------------");
Set<String> treeSet = new TreeSet<String>();
treeSet.add("36");
treeSet.add("20");
treeSet.add("57");
treeSet.add("26");
treeSet.add("78");
treeSet.add("16");
System.out.println("TreeSet=" + treeSet);
}
}
HashSet数据结构
-
在HashSet集合中,有一个用来存放数据对象的内部数组。
- 向集合中添加数据时,HashSet首先获取该数据对象的哈希值
- 然后通过哈希算法将该值换算为数组的下标索引
- int index = hashCode值 & (数组长度-1)
-
然后判断数组该位置是否已经有元素,如有没有,则直接添加到该位置
- 如果数组该位置已经有元素,则调用equals方法,将2个元素进行比较,如果返回true,则不进行添加
- 如果equals方法返回false,则在该位置形成链表结构,新添加的元素放在链首,之前的元素向后放
- 如果数组某个位置的链表长度超过8,则会自动将该链表结构转变成红黑树结构 (jdk1.8)
-
任何对象均有自己的哈希值,该值可通过调用对象的hashCode()方法(继承自Object类)来获得。
- 底层根据HashMap实现数据存储,HashMap底层基于数组+链表实现数据存储,不能保证数据顺序恒久不变。线程不安全的集合。
- 扩容问题:
- 底层数组默认长度为16,装填因子默认为0.75f,当存储元素个数大于数组长度*装填因子,则触发动态扩容机制
- 会创建一个新数组,长度为原数组长度的2倍,然后将原数组中的元素重新散列存储到新数组中
- 底层数组的长度一定是2的幂
- 当加载因子越大,扩容不频繁,导致某个桶(数组中的一个位置)中的节点(链表上的节点)很多,让查询效率降低了。
- 当加载因子越小,扩容频繁,让rehash操作很频繁导致大量的内存没有使用造成浪费。
- 当某个桶中节点个数大于8个时扭转成二叉树(红黑树)来提高查询效率(jdk1.8出现的)
hashCode和equals方法
- hashCode和equals方法是Object类定义的2个方法,为基于hash算法的数据结构提供必要的支持
- 默认情况下,hashCode会返回基于native代码实现的整数值,equals方法会比较2个元素的内存地址是否相等
- 在实际应用中,尤其是保存非包裹类对象时,需要根据现实需求,重写hashCode和equals方法
- 重写hashCode的要求:
- 对同一个对象多次调用hashCode方法,返回值应该一致( must consistently return the same integer)
- 如果2个对象基于equals()方法判断为相同的,那么调用它们的hashCode方法,也必须返回相同的值
- 如果2个对象基于equals()方法判断为不同的,不强制要求调用它们的hashCode方法返回不同的值
- 重写equals的要求:
- 对于非null的对象,调用equals和自己进行比较,应该返回true
- 对于2个非null的对象,调用x.equals(y)和调用y.equals(x)应该返回相同的结果
- 对于3个非null的对象,如果x.equals(y)->true,y.equals(z)->true,那么x.equals(z)也应该是true
- 对2个对象多次调用equals方法,返回的结果应该是一致的
- 对任何非null对象,调用equals(null)应该返回false
- IDE(比如eclipse和idea)都提供了自动重写hashCode和equals方法的支持,其逻辑是使用用户指定的属性值,经过固定的计算,得到一个hashCode值,因此,一个类的2个属性值完全相同的对象,得到的hashCode值是一致的,调用equals比较,也会返回true。
TreeSet数据结构
- TreeSet集合内部使用排序二叉树(红黑树)来存放和管理数据。
- 排序二叉树是一种能将数据按其大小顺序组织在一个树形结构上的数据结构。
- 排序二叉树具有以下特点:
- 树中的每个节点,除存放数据外,还有指向左、右子树节点的指针;
- 每个节点都是通过指针相互连接的,相连指针的关系都是父子关系;
- 所有左子树的节点数值都小于此节点的数值;
- 所有右节点的数值都大于此节点的数值。
- TreeSet内部默认调用对象的compareTo()方法进行大小的比较,因此,要求添加的对象必须实现了Comparable接口,如果没有,则添加时会报错:
java.lang.ClassCastException: XXX cannot be cast to java.lang.Comparable
- 自定义类重写compareTo()方法,根据实际的需求,提供比较大小的逻辑:
- 如果当前对象大于目标对象,返回正数
- 如果当前对象等于目标对象,返回0
- 如果当前对象小于目标对象,返回负数
- 开发者也可以通过声明Comparator接口的实现类,在compare()方法中提供自定义的比较逻辑
- 在创建TreeSet时,需要调用带参构造器,将Comparator接口实现类的对象传入,以提供自定义的比较逻辑
- 这种方式相对于Comparable接口来说,拥有更高的优先级
- 比较:
- Comparable接口可以为一个类提供默认的比较逻辑,适用于该类的所有对象和所有的场景
- 在一些特殊的场景下,我们需要使用与Comparable接口定义的不同的比较逻辑,这时候可以通过Comparator接口实现类的方式,提供差异性的比较逻辑
- 注意!以上2种提供比较逻辑的方式,不止适用于TreeSet,也适用于Java中所有需要提供比较逻辑的API
HashSet和TreeSet的区别
- 如果希望元素可以被自动排序,使用TreeSet
- HashSet拥有更高效的添加速度
List和Set的区别
- List是有序的,Set是无序的:能够保证按照元素的添加顺序去存放元素
- List是可重复的,Set是不可重复的
- 可以调用set集合的addAll()方法,将list集合中所有元素添加到set集合中,会实现“去重”的效果
- 去重操作时,可以优先考虑使用LinkedHashSet,它在传统HashSet之上维护了一个链表结构,用于记录元素的添加顺序,因此,在遍历时,可以保证按照元素的添加顺序返回元素
迭代器
-
通常有两种方式可以遍历访问集合中的元素:
- 使用增强型for循环
- 使用迭代器
-
增强型for循环比较方便,迭代器方式编程相对复杂
-
迭代 — 在集合中检索每个元素的过程。
-
每个集合都会伴有一个 Iterator(迭代器)对象。
-
Iterator接口位于java.util包中
- list.iterator():获取与当前集合对象绑定的迭代器对象
- it.hasNext();
- 当指针指向集合的末尾时,如果再调用it.next()方法,就会抛出
java.util.NoSuchElementException
异常 - 规避这个异常,可以使用it.hasNext()方法,判断指针后方是否有元素
- 当指针指向集合的末尾时,如果再调用it.next()方法,就会抛出
- it.next(); // 指针下移一位,返回对应的元素
// List<Integer> list=Arrays.asList(1,2,3,4,5);
List<Integer> list=new ArrayList();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
// 获取迭代器对象
Iterator<Integer> it = list.iterator();
it.next();
it.remove(); // 删除集合中最近一次返回的数据,必须先调用1次it.next();
// it.remove(); // 不行,一次it.next()之后,只能调用1次it.remove();
for(int i:list){
System.out.println(i);
}
List<Integer> list=new ArrayList();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
// 将迭代器指针移动到集合的末端
ListIterator<Integer> it=list.listIterator(list.size());
while(it.hasPrevious()){ // 从后向前遍历
System.out.println(it.previous());
}
ListIterator<Integer> it2=list.listIterator();
while (it2.hasNext()){
it2.next();
it2.set(5);// 需要先移动指针
}
for(int i:list){
System.out.println(i);
}
ListIterator<Integer> it3=list.listIterator();
while (it3.hasNext()){
it3.next();
it3.add(9);
it3.add(17);
}
System.out.println("--------------");
for(int i:list){
System.out.println(i);
}
映射(Map<K,V>)
- Map类型的集合是映射集合,以键(key)-值(value)对的形式存储对象。
- Map中存储的每个对象元素都有键(key)与之对应,并可通过键来索引和查找对应元素。
- Map中不允许键的重复和从键到值的一对多映射。
- 是否可以允许出现2个不同的key对应1个value?可以
- Map 接口的API位于java.util包中。
- Map 接口提供了以下主要方法:
- entrySet :返回包括所有键值对的集合。
- keySet :返回映射中所有键的集合。
- values :返回映射中所有值的集合。
- get:返回指定键(由参数指定)所对应的值对象
- put:向Map集合中添加一个键值对
遍历Map集合
- 先获取所有的键再获取所有的值
- 先获取所有的键值对再获取键和值
HashMap
- 底层根据数组+链表实现数据存储
- 键是无序的,不能重复,不能保证数据的顺序恒久不变
- 允许存储null值和null键
- 默认初始容量为16,默认加载因子为0.75,默认是再原来的基础上扩容一倍
- 可以通过带参构造器给定底层数组的初始长度,需要是2的n次方
- 异步式线程不安全的映射