Java - 集合框架
集合从概念上来看就是一堆元素放在一起,我们其实可以使用数组来处理这样的情况(操作一堆数据),但是数组存在各种各样的限制,使用起来并不方便,因此才推出更强大的集合处理方式,即集合框架
数组的问题:
容器最基本的功能就是存储数据,取出数据,但是因为实际需求不同,比如是否有序,是否可重复,不同的数据结构,不同的存取效率等等原因,系统提供了一众不同特性的容器类,共同构成了集合框架
框架体系结构:
其中Collection接口支持(单个形式)数据的存储,而Map接口支持对键值对形式的数据的存储
List接口
特点:
-
有序存储
-
可重复存储
-
可存储null
-
提供按照索引的操作方法
-
动态增长(chang)
主要实现类:
-
ArrayList
-
对于列表尾部的插入删除效率更高
-
随机读取快,查找快,随机插入删除慢
-
底层使用数组实现(连续的内存)
ps: 由于其内部使用数组,而数组容量是固定的,这就导致了,添加删除时都会引发数组的重建,以及数据的拷贝,
举个例子:火焰山终于下雨了,刚开始以为只下一点点,所以找了一个杯子装,后来发现装不下了,要换一个盆(创建更大的数组),那么需要先把杯子里的水倒进盆里(数据拷贝)
适用于:读取,查询操作更频繁的场景
-
-
LinkedList
-
内部使用双链表结构(非连续的内存)
-
随机添加删除和顺序添加删除都很快
-
查找慢,随机读取慢
ps:由于使用的链表结构,所以添加删除时仅需要在任意内存保存数据然后将指针添加到上一个数据的结尾即可,所以这添加删除数据时比使用数组的ArrayList快很多 ,但是当需要查找数据时,就需要频繁的移动指针(地址不是连续的),造成了查询速度慢,
举个例子: 你是要找王老板做生意,但是你自己不认识他,于是你就找了刘老板,刘老板也不认识他,于是刘老板找了李老板,还好最后李老板认识王老板,找到了你要找的人,期间不能跨越中间的必须一个一个找下去
适用于:添加,删除操作更频繁的创建
-
列表的排序:
LinkedList,与ArrayList 都能保证存取顺序一致,我们还可以对元素按照从小到大或是从大到小的方式来排序;
排序方式1:
//调用LinkedList与ArrayList对象的sort的方法,并提供比较器
ArrayList<Person> slist = new ArrayList<Person>();
slist.add(new Person("a","c"));
slist.add(new Person("a","b"));
slist.add(new Person("a","a"));
slist.sort(new Comparator<Person>() {
@Override
//该方法返回整型 整数代表大于,负数代表小于 0代表相等
public int compare(Person o1, Person o2) {
int a;
return (a = o1.getName().compareTo(o2.getName())) != 0 ? a : o1.getGender().compareTo(o2.getGender());
}
});
排序方式二:
//1.让需要排序的独享实现Comparable接口
//2.调用Collections提供的sort方法
class Person implements Comparable{
private String name,gender;
public Person(String name, String gender) {
this.name = name;
this.gender = gender;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override//参数为另一个参与比较的对象
public int compareTo(Object o) {
Person p1 = this;
Person p2 = (Person) o;
return p1.getName().compareTo(p2.getName());
}
}
//测试
package com.yh.lesson.collection;
import java.util.*;
public class SortDemo{
public static void main(String[] args) {
ArrayList<Person> slist = new ArrayList<Person>();
slist.add(new Person("a","c"));
slist.add(new Person("a","b"));
slist.add(new Person("a","a"));
Collections.sort(slist);//排序
}
}
当需要降序时只需要将两个比较独享交换位置即可:(两种排序都适用)
@Override
public int compareTo(Object o) {
Person p1 = this;
Person p2 = (Person) o;
return p1.getName().compareTo(p2.getName());//升序
return p2.getName().compareTo(p1.getName());//降序
}
Set接口
主要实现类:
-
HashSet
- 不保证迭代顺序
- 不可存储重复元素
- 可以存储null元素
- 内部使用HashMap(具体参考下面HashMap内容),将所有键值对的值设置为同一个Object对象
可以说HashSet就是对HashMap进行了简单的包装:
真正存储数据的容器:
包装后的添加数据的方法:
iterable接口
iterable叫做可迭代对象
我们知道集合框架提供了很多不同的容器实现类,但是每种容器的内部实现不同,导致我们需要记忆不同的取值方法,这对于使用者来说无疑增加学习成本,所以Java统一了各种容器的取值方式,即通过iterable接口
好处:
-
实现了iterable的类都可以被foreach循环直接使用
接口方法:
public Iterator iterator();//用于返回一个迭代器接口对象
Iterator接口
Iterator叫做迭代器
迭代器用于获取容器中的值
接口方法:
public boolean hasNext(); //判断是否还有下一个
public Integer next();//获取下一个的值
使用案例:
//写一个支持foreach语法的类
import java.util.Iterator;
public class MyTest implements Iterable<Integer> {
private int num = 0;
@Override
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
@Override
public boolean hasNext() {
return num < 11;
}
@Override
public Integer next() {
return num++;
}
};
}
//测试:
public static void main(String[] args) {
for (Integer i:new MyTest()) {
System.out.println(i);
}
}
}
//该类可以迭代得到0-10这11个数字
ps:collection接口继承了iterable,所以所有collection的子类可以被foreach遍历
集合的遍历:
我们可以使用foreach来遍历collection的任何子类对象:
案例:
HashSet<String> set = new HashSet<String>();
set.add("1");
set.add("2");
set.add("3");
ArrayList<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
for (String s: list) {
System.out.println(s);
}
for (String s: set) {
System.out.println(s);
}
需要特别强调的是不允许在迭代期间删除或添加元素,将会引发:java.util.ConcurrentModificationException
异常,意思是不允许并发修改
那么我们如何删除元素呢?
迭代期间添加删除元素
//删除或添加单个元素:
ArrayList<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
for (String s: list) {
list.add("11111");
break;//操作完成后直接break;
}
//删除或添加多个元素方法1
//先保存需要删除的元素,迭代完成在一并删除
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
ArrayList deletes = new ArrayList();
for (Integer i:
list) {
if (i > 1){
deletes.add(i);
}
}
list.removeAll(deletes);
//删除或添加多个元素方法2
//使用lambda表达式 jdk1.8之后可用
list.removeIf(i -> i > 1);
//删除或添加多个元素方法3
//迭代copy的集合,从原始集合删除
for (Integer i:
(ArrayList<Integer>)list.clone()) {
if(i > 1){
list.remove(i);
}
}
//删除或添加多个元素方法4
//使用迭代器
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()){
if (iterator.next() > 1){
iterator.remove();
}
}
Map接口
特点:
-
数据已键值对的形式存储
-
键值对用Entry类对象表示
-
不能包含重复的key
-
键值对一一对应
注意:
-
避免使用可变对象作为key,如果对象的值改变时会引起equals方法的变化,将会导致不可预知的问题,,简单的说,map之所以不存储重复的key是因为,map使用hashCode+equals方法判断是否重复,如果你使用的是可变对象,那么当对象的属性变化时,可能导致判断重复的机制发生错误,例如:本来插入了Person对象,名字为张三和李四,后来你把张三的名字改成李四,那么这时候就误判断这两个对象key是否相同
-
如何判断对象是否相等
- 第一步调用对象的hashCode获得一个整数进行比较,如果相同再调用equals方法进行判断,如果equals也返回true的话则表示两个对象完全相同,这样的对象是不会被存储的
-
综上所述,我们在自定义类中如果进行去重,则应当重写这两个方法(hashCode,equals)
主要实现类:
-
HashMap
-
基于Hash表实现
-
允许使用null作为key或value
-
Map中的Entry(键值对)是无序的
-
遍历方法:
//HashMap,不是Collection的子类所以不能直接foreach
//可用使用下面几种方式:
//方法1
for ( String key: map.keySet()) {
System.out.println(key+" "+map.get(key));
}
//方法2
for (Map.Entry entry:map.entrySet()){
System.out.println(entry.getKey() +" "+entry.getValue());
}
HashMap 数据结构解析:
-
HashMap内部使用hash表(本质是一个数组见图一)
-
HashMap使用hash算法计算得到存放的索引位置,一次来加快查询速度,(比ArrayList还要快)
-
同样的既然本质是数组则少不了扩容和复制数据的问题了,这与ArrayList的缺点是一样的
-
hash值相同不能表示key完全相同,需要调用equals再次确认
-
如果说key的hash值相同但是equals判断不相同,那么使用树结构或者链表来存储这些hash相同的元素,(具体使用哪种根据当前map中元素的数量,超过64个元素则使用树结构)
获取元素方法:
存储元素方法:
存储元素方法补充:
代码在putVal方法的最后,当添加元素后的大小超过阈值时则直接扩容hash表
扩容/转换方法:
总结:
-
HashMap是使用Hash表(本质是数组)来存储数据,
-
当keyHash值相同但是equals判断不同时使用链表(添加快,查询慢)
-
当元素数量超过64时则将链表转为数结构(添加慢,查询快)
最后再来一张框架图,帮助理解:
图中的Listiterator 可以实现倒叙迭代,需要的话咨询查询API