1.1目录
1.为什么使用集合?
2.集合架构有哪些?
3.List集合
4.ArrayList集合
5.LinkedList集合
6.Set集合
7.HashSet集合
8.TreeSet集合
1.2为什么使用集合?
1.我们原来学习过数组,但是数组有缺点(一旦声明之后,长度就不可变了)同时,声明数组时的数据类型也决定了该数组存储的数据的类型;而且,数组存储的数据是有序的、可重复的,特点单一。 但是集合提高了数据存储的灵活性,Java 集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据。
2.我们是否可以定义一个长度改变的容器。---当然可以。
3.手撕可变长度的容器。
package com.qy151.test2;
import java.util.Arrays;
/**
* @unthor : YSH
* @date : 20:17 2022/4/15
*/
public class MyArray {
private Object[] arr;//声明一个Object类型的数组
private int size;//表示数组的下标 因为他们是类成员变量 在创建对象时都有默认值。
public MyArray(){//无参构造函数
this(3);//本类中其他的构造函数 this本类的对象。 如果在构造方法中this()表示调用本类的其他构造函数
}
public MyArray(int initSize){//有参构造函数--表示数组的长度
if (initSize<0){//长度不合法
throw new RuntimeException("抱歉,数组长度有误,请重新定义数组长度");
}
arr=new Object[initSize];
}
//把元素o放入数组arr
public void addData(Object o){
//判断你的数组是否已满
if(size>=arr.length){
//扩容--(1)容器的长度变长 (2)把原来容器中的元素复制到新的容器中
Object[] newArr = Arrays.copyOf(arr, size * 2);
arr=newArr;
}
arr[size]=o;
size++;
}
//根据下标获取数组中的元素。
public Object getData(int index){
if(index>=size){
throw new ArrayIndexOutOfBoundsException("下标越界");
}
Object o = arr[index];
return o;
}
}
我们自己可以手写一个可变的容器,那么别人也可以手写可变的容器。
java官网 基于数组 根据不同的数据结构 创建了多个类 而这些类统称 为集合框架。
以后 我们在说集合框架时 就表示多个类。
1.3集合的架构
1.4 List集合-ArrayList
1.4.0 ArrayList的特点
- 实现了List接口
- 可以动态扩容(我们只管存,长度不够,底层会自动的扩容)
- 通过下标可以快速访问数据
- 查找快,插入删除慢
- ArrayList底层是数组,对数组做了封装
- 可以存储任意类型的数据,包括null
- 数据按照存储次序排列
- 数据可以重复
- 多线程访问时不安全
1.4.1 创建集合对象
//创建一个集合对象,如果没有指定集合容器的长度默认为10
List list = new ArrayList();
List list = new ArrayList(15);
1.4.2 添加的操作
//添加操作
list.add("java01");
list.add("java02");
list.add("java03");
list.add(66.66);
list.add(new Date());
list.add("迪迦来了");
//下标为2的位置添加集合元素,并把后面的元素进行移位
list.add(2,"我是迪迦");
//打印一个对象时默认使用的为toString()
System.out.println(list);
1.4.3 删除的操作
//删除操作
list.remove(2);//移除下标为2的元素
System.out.println(list);
//list.clear();//清空集合中的元素.
//System.out.println(list);
1.4.4修改的操作
//修改操作
//修改下标为1的集合元素的值
list.set(1,"刘德华");
System.out.println(list);
1.4.5查询的操作
List list1 = new ArrayList();
list1.add("java01");
list1.add("java02");
System.out.println(list1);
//查询的方法
//根据下标获取元素
Object o = list1.get(1);
System.out.println(o);
//获取集合元素中的个数
Object o1 = list1.size();
System.out.println(o1);
//判断元素是否在集合中
boolean f = list1.contains("java05");
System.out.println(f);
//查询元素在集合中第一次出现的位置
int index = list1.indexOf("java01");
System.out.println(index);
//遍历集合中的元素 for循环
for (int i=0 ;i<list1.size();i++){
Object ol = list1.get(i);
System.out.println(ol);
}
System.out.println("====");
//for each 遍历集合中的元素
for (Object o2 : list1){
System.out.println(o2);
}
}
}
1.4.6 ArrayList底层源码
从构造方法来入手。new ArrayList(22) 底层声明了一个Object类型的数组 名字elementData
Object[] elementData
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) { //大于0
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) { //等于初始化为一个空数组
this.elementData = EMPTY_ELEMENTDATA;
} else { //抛出一个异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
==========add("java01")======E理解为Object类型================
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 扩容
elementData[size++] = e; //把元素赋值给数组的相应位置
return true;
}
==========indexOf("java02") 判断元素在集合中第一次的位置=============
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i])) //和数组中的每个元素内容进行比对
return i; //返回元素在集合中位置
}
return -1;
}
===========size() 请求数组的长度======================
public int size() {
return size;
}
============contain("java05")判断元素是否在集合中==============
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
===============get(1) 获取指定位置的元素========
public E get(int index) {
rangeCheck(index); //判断指定的位置是否合法
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
============toString() 为什么不打印对象的引用地址
[java01, java02, java03, java02]因为重写了Object里面的toString方法。
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
通过对ArrayList方法的底层代码分析:底层就是对数组的操作。
ArrayList的底层就是基于数组实现的。
1.5 LinkedList
它是一个链表结构。具有List的特征,底层以链表结构实现,可以进行头尾元素的添加删除。
1.5.1 添加操作
//添加
linkedList.add("喜羊羊");//追加到尾部
linkedList.addFirst("美羊羊");//追加到头部
linkedList.addLast("懒羊羊");//追加到尾部
linkedList.add(2,6666);//在指定的位置添加元素
System.out.println(linkedList);
linkedList.addFirst("沸羊羊");//追加到头部
linkedList.addLast("暖羊羊");//追加到尾部
System.out.println(linkedList);
1.5.2 删除操作
//删除操作
linkedList.removeFirst();//移除头部元素
System.out.println(linkedList);
linkedList.remove(2);//移除指定位置的元素
System.out.println(linkedList);
linkedList.removeLast();//移除尾部元素
System.out.println(linkedList);
//linkedList.clear();//清空元素
//System.out.println(linkedList);
1.5.3 修改操作
//修改操作
linkedList.set(1,"灰太狼");//修改指定位置的元素
System.out.println(linkedList);
1.5.4查询操作
//查询操作
int size = linkedList.size();//求长度
boolean empty = linkedList.isEmpty();//查询是否为空
boolean b = linkedList.contains("java01");//判断元素是否在集合中
Object o = linkedList.get(1);//根据下标获取指定位置的元素
Object first = linkedList.getFirst();//获取第一个元素
System.out.println(first);
Object last = linkedList.getLast();//获取最后一个元素
System.out.println(last);
1.5.5 LInkedList的底层源码
1.凡是查询源码,我们都是从类对的构造方法入手:
/**
* Constructs an empty list.
*/
public LinkedList() {
}
该类的构造方法内是空的,没有任何的代码。但是该类中有三个属性。
transient int size = 0; //索引
transient Node<E> first ; //第一个元素对象
transient Node<E> last ; //最后一个元素对象
================ add的源码=====E:理解为Object类型==========================。
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
//上一个节点 数据 下一个节点
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
==================Node的源码 内部类=======================================
private static class Node<E> { //<E>泛型--object
E item; //数据
Node<E> next; //下一个节点
Node<E> prev; //上一个节点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
==================== get(1)-----获取元素========================
public E get(int index) {
checkElementIndex(index); //检查index下标是否正确。
return node(index).item; //李四Node对象
}
========================node(index)=============================
Node<E> node(int index) {
//>> 位运算二进制运算 ----- size >> 1 一半的意思size/2
if (index < (size >> 1)) { //前半部分
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else { //后半部分
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
1.6Set集合
1.6.1Set集合(接口)的特点
- Set接口是无序的
- Set接口中的数据不允许重复
- Set接口无法通过下标访问数据
- 查找慢,插入删除快(底层数据结构是哈希表和红黑树)
- Set集合使用equals()和hashCode()方法实现元素去重
1.7 HashSet集合
1.7.1HashSet的特点
- HashSet是Set接口的实现类
- 线程不安全
1.7.2创建HashSet对象
public class Test {
public static void main(String[] args) {
//如果没有指定容器的大小 默认为16 负载因子为0.75
HashSet hashSet = new HashSet(16);//初始容器的大小
//loadFactor:-->0.7f 表示负载因子 当前空间使用70%时 要求扩容
HashSet hashSet1 = new HashSet(16,0.7f);
}
}
1.7.3添加操作
//添加操作
hashSet.add("周杰伦");
hashSet.add("刘德华");
hashSet.add("陈奕迅");
hashSet.add("许嵩");
hashSet.add("周杰伦");
System.out.println(hashSet);
HashSet set2=new HashSet();
set2.add("刘德华");
set2.add("张学友");
set2.add("黎明");
hashSet.addAll(set2); //把set2中的每个元素添加到hashset中
System.out.println(hashSet); //元素不能重复 而且无序
运行结果:
1.7.4删除操作
//删除操作
hashSet.remove("黎明");//删除元素"黎明"
//hashSet.clear();//清空容器集合
System.out.println(hashSet);
运行结果:
1.7.5查询操作
//查询操作
boolean empty = hashSet.isEmpty();//判断是否为空
System.out.println(empty);
boolean b = hashSet.contains("刘德华");//判断元素是否在集合中
System.out.println(b);
运行结果:
1.7.6遍历
foreach遍历:
//foreach遍历
for (Object o : hashSet){
System.out.println(o);
}
迭代器遍历:
迭代器(iterator)有时又称游标(cursor)是程序设计的软件设计模式,可在容器对象(container,例如链表或数组)上遍访的接口,设计人员无需关心容器对象的内存分配的实现细节。
HashSet类中没有提供根据集合索引获取索引对应的值的⽅法,
因此遍历HashSet时需要使⽤Iterator迭代器。Iterator的主要⽅法如下
返回类型 | 方法 | 描述 |
boolean | hasNext() | 如果有元素可迭代 |
Object | next() | 返回迭代的下⼀个元素 |
//迭代器遍历
Iterator iterator = hashSet.iterator();//创建迭代器对象 有序:有下标
while (iterator.hasNext()) {//判断是否指针能够移动
Object next = iterator.next();//指针移动并获取当前的元素
System.out.println(next);
运行结果:
1.7.7HashSet的源码
从构造函数说起:
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
在创建一个HashSet的对象时,底层创建的是HashMap。我们说hashset的底层原理时,我们就在后HashMap的原理就行。 讲HashMap时给大家说原理。
1.8TreeSet集合
1.8.1TreeSet的特点
TreeSet中的方法和HashSet中的方法一模一样 只是他们的实现不一样。
TreeSet 基于TreeMap 实现。TreeSet可以实现有序集合,但是有序性需要通过比较器实现。
- 有序
- 不重复
- 添加、删除、判断元素存在性效率比较高
- 线程不安全
1.8.2TreeSet对元素进行排序的方式:
1) 如果是基本数据类型和String类型,无需其它操作,可以直接进行排序。
2) 对象类型元素排序,需要实现Comparable接口,并覆盖其compareTo方法。
3) 自己定义实现了Comparator接口的排序类,并将其传给TreeSet,实现自定义的排序规则。
1.8.2存储String类型
TreeSet treeSet=new TreeSet();
treeSet.add("java05");
treeSet.add("java03");
treeSet.add("java04");
treeSet.add("java01");
treeSet.add("java02");
treeSet.add("java04");
System.out.println(treeSet);
1.8.3存储一个对象类型
public class Test1 {
public static void main(String[] args) {
TreeSet treeSet=new TreeSet(); //TreeSet不允许重复元素
treeSet.add(new Student("喜羊羊",10));
treeSet.add(new Student("美羊羊",11));
treeSet.add(new Student("慢羊羊",66));
treeSet.add(new Student("灰太狼",25));
System.out.println(treeSet);
}
}
通过运行我们发现出现如下的错误:
发现: TreeSet中的元素必须实现Comparable接口 方可放入TreeSet
解决办法有两个:
第一个: 让你的类实现Comparable接口
package com.qy151.test3;
import java.util.TreeSet;
/**
* @unthor : YSH
* @date : 17:09 2022/4/17
*/
public class Test1 {
public static void main(String[] args) {
TreeSet treeSet=new TreeSet(); //TreeSet不允许重复元素
treeSet.add(new Student("喜羊羊",10));
treeSet.add(new Student("美羊羊",11));
treeSet.add(new Student("慢羊羊",66));
treeSet.add(new Student("灰太狼",25));
System.out.println(treeSet);
}
}
class Student implements Comparable{
private String name;
private Integer age;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public Student(String name, Integer age) {
this.name = name;
this.age = age;
}
public Student() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
//排序:---返回如果大于0 表示当前元素比o大 如果返回-1 当前添加的元素比o小 返回0表示相同元素。
@Override
public int compareTo(Object o) {
Student student= (Student) o;
System.out.println(this+"===================>"+o);
if(this.age>student.age){
return 1;
}
if(this.age<student.age){
return -1;
}
return 0;
}
}
第二种: 在创建TreeSet时指定排序的对象。
我们之前 创建过TreeSet对象。
TreeSet treeSet=new TreeSet(); 但是在创建对象时 并没有为其指定排序得规则,那么就要求该集合得元素有排序规则。 如果元素得类已经创建完成,不能修改该类得源码,这时我们又想把该类得对象放入得TreeSet容器中。 这时就需要你在创建TreeSet时指定排序得规则。
public class Test {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet(new MyComparator());//为TreeSet指定了排序规则
treeSet.add(new Student("张三",18));
treeSet.add(new Student("李四",17));
treeSet.add(new Student("王五",20));
treeSet.add(new Student("赵六",19));
System.out.println(treeSet);
}
}
public class MyComparator implements Comparator {//需要重写接口中的抽象方法
@Override
public int compare(Object o1, Object o2) {
System.out.println("=====调用了compare方法=====");
//先判断两个要比较的数据是否是相同类型的
if ((o1 instanceof Student) && (o2 instanceof Student)) ;
{
Student s1 = (Student) o1;
Student s2 = (Student) o2;
System.out.println(o1 + "======" + o2);
if (s1.getAge() > s2.getAge()) {
return 1;
} else if (s1.getAge() < s2.getAge()) {
return -1;
} else {
return 0;
}
}
}
}
总结:
TreeSet : 1.保证数据逻辑有序 2.不重复
实现有序的两种方式
a. 自定义类型实现Comparable接口,实现里面的compareTo方法
b. 自定义一个比较器对象实现Comparator接口,实现里面的commpare方法
1.9Map接口及其实现类
1.9.1Map接口特点
- 以键值对方式存储数据(Collection是单值集合)
- 键不能重复,键重复时,后面的数据会覆盖前面的数据
- 可以存储null
- 键值对数据无序
map中得每个元素属于键值对模式。 如果往map中添加元素时 需要添加key 和 value. 它也属于一个接口,该接口常见得实现类有: HashMap.
1.9.2如何创建Map对象
//默认初始化大小为16 负载因子为0.75
Map map=new HashMap();
//初始化大小
Map map2=new HashMap(16);
//初始化大小 负载因子
Map map3=new HashMap(16,0.78f);
1.9.3HashMap实现类
HashMap实现了Map接口,拥有Map接口的基本特点。HashMap线程不安全,效率高。HashMap的底层是由哈希表、链表加红黑树构成的。
1.9.4添加操作
public class MapTest {
public static void main(String[] args) {
//默认初始化大小为16 负载因子为0.75
Map map = new HashMap();
//添加操作 key:name/age 相当于属性 value:相当于属性值
map.put("name","张三");
map.put("age",18);
map.put("sex","男");
map.put("name","李四");//因为key不能重复,所以后者会把前者覆盖
Map m1 = new HashMap();
m1.put("k1","v1");
m1.put("k2","v2");
map.putAll(m1);//把m1中的每个元素添加到map中
map.putIfAbsent("age1",20);//如果指定得到的key存在,则不放入map中,如果不存在则放入map中
System.out.println(map);
}
}
运行结果:
1.9.5删除操作
//删除操作
map.remove("age1");//根据指定的key移除元素
map.remove("sex","男");//根据指定的key,value移除元素
//map.clear();//清空map容器
System.out.println(map);
1.9.6修改操作
//修改操作
map.replace("age",20);//修改元素的值
System.out.println(map);
运行结果:
1.9.7查询操作
public class MapTest1 {
public static void main(String[] args) {
Map map = new HashMap();
map.put("k1","v1");
map.put("k4","v4");
map.put("k2","v2");
map.put("k3","v3");
//查询操作
boolean k2 = map.containsKey("k2");//判断map是否存在指定的key
System.out.println(k2);
Object k3 = map.get("k3");//根据指定的key获取相应的value值
System.out.println(k3);
Set keys = map.keySet();//返回该map中所有得key
System.out.println(keys);
//遍历map.
for(Object k:keys){
Object value= map.get(k);//
System.out.println(k+"================>"+value);
}
}
}
运行结果:
1.9.8HashMap的底层原理
JDK1.7 和 JDK1.8他们是有区别得。
JDK1.7使用得数据结构: 数组+链表 而且链表插入模式为头部插入(造成死循环)。
jdk1.8使用得数据结构: 数组+链表+红黑树 而且链表得插入模式为尾部插入。
从构造函数入口:
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
总结:
HashMap的put()和get()的实现
1) map.put(key,value)实现原理
第一步:首先将k,v封装到Node对象当中(节点)。
第二步:它的底层会调用K的hashCode()方法得出hash值。
第三步:通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equals。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。
2) map.get(key) 实现原理
第一步:先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。
第二步:通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。重点理解如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着参数K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。
JDK1.8 HashMap原理
Hashmap得原理,存储元素使用得put(key,value),根据key得hash计算出相应得哈希值,根据相应得算法求出该元素在数组中得位置, 如果求出得哈希值相同,则称为哈希冲突,会根据equals来判断元素是否一致,如果equals不同,则存入单向链表上, 如果哈希碰撞得个数超过8个,则把链表转换为红黑二叉树。
JDK1.7和JDK1.8 HashMap得区别。
JDK1.7使用得数据结构: 数组+链表 而且链表插入模式为头部插入(造成死循环)。
jdk1.8使用得数据结构: 数组+链表+红黑树 而且链表得插入模式为尾部插入。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//如果key得hash值相同,判断key得equals是否相同,替换原来得元素
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 判断链表得长度是否超过8个
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 把链表转换为红黑树结构
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
集合到此结束,谢谢大家浏览!!!!!