Collection的分类
Collection作为单列集合,主要可以分为List和Set两大类,
List添加的元素是有序、可重复、有索引,分为ArrayList和LinkedList
Set添加的元素是无序、不重复、无索引,分为TreeSet和HashSet,
HashSet无序、不重复、无索引,其下又有LinkedHashSet,有序、不重复、无索引。
TreeSet按照大小默认升序排序、不重复、无索引。
List接口具有的特点
List接口的特点:有序(存入和取出顺序一致) / 可重复(可以存储重复元) / 有索引(方法)
List集合的所有的实现类基于底层的结构都具有这三个特点。
List接口就是继承了Collection接口的子接口。
在继承了Collection接口的方法的基础上还拓展了一些新的方法。
List体系中的实现类都具有的特点:可重复,有序,有索引。
List集合操作索引的特有方法
//public void add(int index,E e) : 添加元素到指定索引(原有内容向后挪动一位)
//int index参数的范围:0到集合长度合法 不满足要求非法(注意索引越界问题)
strList.add(1, "王小花");//在李狗蛋前面添加王小花
System.out.println("add : " + strList);
//public E remove(int index) : 删除指定索引的元素(原有内容向前挪动一位)
//int index参数的范围:0到集合长度-1 (注意索引越界问题)
String removeElement = strList.remove(0);//删除张二狗
System.out.println("removeElement : " + removeElement);
System.out.println("remove : " + strList);
//public E set(int index,E e) : 将指定索引的元素替换为参数,并返回被替换的元素
//int index参数的范围:0到集合长度-1 (注意索引越界问题)
String replaceElement = strList.set(0, "张美丽");//将王小花替换为张美丽
System.out.println("replaceElement : " + replaceElement);
System.out.println("set : " + strList);
//public E get(int index) : 获取指定索引的元素
//int index参数的范围:0到集合长度-1 (注意索引越界问题)
System.out.println("0索引元素是 : " + strList.get(0));
System.out.println("1索引元素是 : " + strList.get(1));
System.out.println("2索引元素是 : " + strList.get(2));
List集合的四种遍历方式
迭代器遍历
Iterator<String> it = nameList.iterator();
while (it.hasNext()) {
String element = it.next();
System.out.println("迭代器 : " + element);
}
增强For循环
for (String element : nameList) {
System.out.println("增强For : " + element);
}
Lambda表达式(函数式)
nameList.forEach(element -> System.out.println("Lambda : " + element));
普通For循环
for (int i = 0; i < nameList.size(); i++) {
String element = nameList.get(i);
System.out.println("普通For : " + element);
}
数据结构的概述
数据结构就是存储和组织数据的方式。
合适的场景选择合适的数据结构可以让代码运行的效率更高。数据结构和算法是搭配使用的!
数据结构:材料 / 算法:菜谱 组织数据的方式不同会导致不同结构的特点不一样。
ArrayList底层的数据结构与特点
ArrayList底层基于数组(数组的特点:长度不可变,内存连续)
特点:查询元素快 不需要寻址过程 / 增删元素慢 元素前移/后移/数组整体扩容
数组创建扩容的底层原理分析
1.无参构造创建ArrayList
public ArrayList() {
//将DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给elementData的数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
结论:当new ArrayList的时候,底层实际保存数据的elementData数组就是一个长度为0的空数组。
2.第一次add元素到ArrayList的时候
public boolean add(E e) {
add(e, elementData, size); //e:添加的元素 elementData:长度为0的数组 size:0
return true;
}
private void add(E e, Object[] elementData, int s) {
//如果下一个要添加的元素的可用索引和底层保存数据的数组长度相同
if (s == elementData.length)
//调用grow()方法将返回值赋值给elementData
elementData = grow();
elementData[s] = e; //将元素放到数组的下一个可用索引中
size = s + 1; //可用索引+1
}
private Object[] grow() {
return grow(size + 1); //size:1
}
private Object[] grow(int minCapacity) { //minCapacity:1
//将当前elementData数组的长度赋值给oldCapacity的变量 oldCapacity:0
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,minCapacity - oldCapacity, oldCapacity >> 1);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
//创建一个新的Object[]数组赋值给elementData后返回
//Math.max(DEFAULT_CAPACITY, minCapacity):10
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
一个ArrayList集合每次存储满了扩容1.5倍
ArrayList的最佳实践
前提:无论使用ArrayList的哪个构造最终都可以保存任意个数据! (自动扩容)
如果可以分析出该集合预计要添加的元素数量(不需要很精准),可以尝试使用ArrayList的有参构造!
public ArrayList<>(int initCapacity) : 创建一个指定长度的集合(底层数组默认的长度)
//分析:预计集合要添加100个元素左右
ArrayList<String> strList = new ArrayList<>(100);
//如果上来就开辟好了指定长度的空间(省去扩容过程【代码的性能提高】)
LinkedList底层的数据结构与特点
Linked底层基于双链表
单向链表
特点:查询慢(查询任何一个节点都要从头结点依次寻址) / 增删快(增删元素不需要影响整体)
双向链表/双头链表
特点:查询慢(可以根据要查询的索引选择从头获取还是从尾获取[比单链表快]) / 增删快(增删元素不需要影响整体)
占用空间大(大空间换小时间)
LinkedList的特有操作头尾方法
LinkedList针对于操作特别重要的头结点/尾结点有一些特有方法
//LinkedList特有的操作头尾结点-添加
numberList.addFirst(1); //比add(0,1)效率要高!
numberList.addLast(5); //比add(5)效率要高!
System.out.println("add : " + numberList);
//LinkedList特有的操作头尾结点-获取
System.out.println("get : " + numberList.getFirst()); //比get(0)效率要高
System.out.println("get : " + numberList.getLast()); //比get(长度-1)效率要高
//▲LinkedList的特有的操作头尾结点-删除
Integer firstRemoveElement = numberList.removeFirst();
Integer lastRemoveElement = numberList.removeLast();
System.out.println("remove : " + firstRemoveElement + " remove : " + lastRemoveElement);
System.out.println("remove : " + numberList);
当要操作头尾结点的时候建议使用特有的方法进行操作,效率比单纯的add/get/remove要高很多!
底层逻辑
/**
* 手写双链表。(拓展内容)
*/
public class MyLinkedList<E> {
private int size; // 元素个数
private Node<E> first;
private Node<E> last;
public boolean add(E e){
Node<E> l = last;
// 开始新增一个结点对象封装这个数据,加入到双链表中去。
Node<E> newNode = new Node<>(e, l, null);
last = newNode;
if(l == null){
// 这是第一次添加结点。
first = newNode;
}else {
// 不是第一次添加结点,让添加前的最后一个结点的下一个地址,指向新结点,。
l.next = newNode;
}
size++;
return true;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
// 先把头结点地址交给一个临时变量
Node<E> head = first;
while (head != null) {
sb.append(head.item).append(head.next == null ? "" : ", ");
// 把head结点轮到下一个结点。
head = head.next;
}
sb.append("]");
return sb.toString();
}
private static class Node<E>{
E item;
Node<E> pre; // 上个结点地址
Node<E> next;
public Node(E item, Node<E> pre, Node<E> next) {
this.item = item;
this.pre = pre;
this.next = next;
}
}
}
栈结构与队列结构的特点
ArrayList底层的数组和LinkedList底层的链表是(物理结构) 代码已经提供好了。直接new int[] 、new Node。
有一些情况下可以基于物理结构去做一些模拟其他结构的操作。这种结构称之为逻辑结构。
队列Queue逻辑结构
队伍是一个一端开口另一端也开口的结构,遵守的数据的进出原则(先进先出 FIFO) 消息队列MQ
public class MyQueue<E> {
//基于维护一个LinkedList来保存数据
private LinkedList<E> dataList = new LinkedList<>();
//对外提供功能-1:存数据的功能
public void add(E e) {
dataList.addLast(e);
}
//对外提供功能-2:取数据的功能(满足队列的特性)
public E get() {
return dataList.removeFirst(); //删除头结点并将头结点数据返回
}
}
栈Stack逻辑结构
栈结构是一个一端开口一端封闭的结构,遵守的数据的进出规则(先进后出 FILO) 栈内存(方法)
public class MyStack<E> {
//基于维护一个LinkedList来保存数据
private LinkedList<E> dataList = new LinkedList<>();
//对外提供功能-1:存数据的功能
public void add(E e) {
dataList.addLast(e);
}
//对外提供功能-2:取数据的功能(满足栈的特性)
public E get() {
return dataList.removeLast(); //删除尾结点并返回
}
}
List接口实现类的特点总结与适用场景
ArrayList:数组:查询快/增删慢(没有特有方法)
LinkedList:双向链表:查询慢/增删快(有一些针对于头尾的特有方法)
实际上在数据量小的情况下,用哪个都无所谓!大部分情况下都是使用ArrayList。
但是不能一味的使用ArrayList完成所有的场景(频繁增删不怎么查询:LinkedList)(频繁查询不怎么增删:ArrayList)
Set集合的概述与特点
Set接口在继承自Collection接口的基础上没有新增特有方法。Collection接口有哪些Set集合的实现类就可以用哪些。
Set接口的特点:不重复(无论是哪一种Set实现类都不保存重复元素) / 无索引(Set接口并没有拓展方法)
为什么Set接口不拓展索引相关的方法,是因为底层实现类的数据结构并非是线性结构。
重点:具体每一个实现类底层的数据结构是什么/有什么特点/什么场景应该进行使用!
HashSet集合的概述与特点
HashSet集合是Set接口的最常用的一个实现类。
底层基于哈希表数据结构,基于哈希表可以保证元素唯一!
由于Set接口并没有拓展任何方法,Set接口的实现类能够调用的就是实现自Collection接口的方法。
public static void main(String[] args) {
//创建HashSet的集合对象
HashSet<String> stringSet = new HashSet<>();
//由于HashSet能够使用的方法基本上都是实现自Collection接口的没有特有方法
stringSet.add("张二狗");
stringSet.add("李狗蛋");
stringSet.add("刘铁柱");
stringSet.add("刘铁柱"); //如果可以保证元素唯一(添加重复内容不存)
//基于Lambda表达式遍历HashSet集合
stringSet.forEach(System.out::println);
}
HashSet保证元素唯一的方式
对齐认知:我们认为什么样的元素是重复的?
Person("张二狗",23) / Person("张二狗",23) => 地址不一样,内容一样[重复对象]
HashSet底层是哈希表,哈希表是一个增删改查都非常快的数据结构,最大的特点保证元素的唯一!
哈希表是基于哈希值内容完成去重的,哈希值是基于添加的元素调用方法计算出来的!
Object类的中public native int hashCode方法计算出来的,每一个对象都可以调用该方法进行计算!
Dog dog1 = new Dog("大黄", 3);
Dog dog2 = new Dog("花花", 2);
Dog dog3 = new Dog("笨笨", 1);
Dog dog4 = new Dog("大黄", 3);
//内容和dog1是一模一样
//计算4个Dog对象的哈希值 -> 基于直接调用继承自Object类的hashCode方法计算!
System.out.println("dog1的哈希值是:" + dog1.hashCode()); //990368553
System.out.println("dog2的哈希值是:" + dog2.hashCode()); //1828972342
System.out.println("dog3的哈希值是:" + dog3.hashCode()); //1452126962
System.out.println("dog4的哈希值是:" + dog4.hashCode()); //931919113
没有基于hashCode方法基于地址值生成哈希值
两个对象内容相同,哈希值不一定相同! 【哈希碰撞】哈希表无法保证元素唯一!
基于重写的hashCode方法生成哈希值(内容)
两个对象内容相同,哈希值一定相同! 【计算的逻辑和用于计算的内容一模一样】 哈希表可以基于元素内容保证唯一!
HashSet保存自定义元素,则一定要重写hashCode和equals方法!
HashSet的特点与适用场景
HashSet具有Set体系中的默认特点
1.不重复 【哈希表:前提(重写hashCode和equals方法)】
2.没有索引
3.按照哈希表的排列规则排序(有些时候可能看上去有序,有些时候可能看上去无序)
HashSet适用于保证元素唯一的场景! 【增删改查速度快】
LinkedHashSet集合的概述与特点
LinkedHashSet集合是HashSet集合的子类。
底层的数据结构是哈希表+链表。当哈希表添加元素的时候额外再向链表中添加一份(由链表记录元素的添加顺序)
LinkedHashSet具有Set体系中的默认特点
1.不重复 【哈希表:前提(重写hashCode和equals方法)】
2.没有索引
3.保证元素的存取有序
LinkedHashSet适用于保证元素唯一的并且存取有序场景! 【增删改查速度快】 没有HashSet快,并且会额外占用内存!
LinkedHashSet保证元素唯一的方式
底层的数据结构是哈希表+链表。当哈希表添加元素的时候额外再向链表中添加一份(由链表记录元素的添加顺序)
TreeSet集合的概述与特点
TreeSet是Set集合的一个实现类,底层是红黑树!
按照指定的排序规则去重元素并且排列元素在集合中的顺序!
TreeSet适用于保证元素唯一并且按照指定规则排序的场景!
TreeSet集合的构造方法
TreeSet集合保证元素唯一的方式
当比较器接口的比较方法返回了0,则认为本次添加的元素重复!
TreeSet<Dog> dogTreeSet = new TreeSet<>((o1, o2) -> o2.getAge() - o1.getAge());
Dog dog1 = new Dog("大黄", 3);
Dog dog2 = new Dog("花花", 12);
Dog dog3 = new Dog("笨笨", 7);
Dog dog4 = new Dog("大黄", 3);
//指定完毕规则之后,当基于add添加元素会默认按照排序完成排序(当排序规则返回的结果是0)
dogTreeSet.add(dog1);
dogTreeSet.add(dog2);
dogTreeSet.add(dog3);
dogTreeSet.add(dog4);
当添加dog4的时候与dog1的age进行对比,发现结果为0,则认为dog4重复元素。
TreeSet集合的极端问题解决方案
极端问题:用于比较的两个对象核心比较项相同,但是其他属性不同(TreeSet只关心核心比较项),会导致有一些数据被错误的认为是重复的!
TreeSet<Dog> dogTreeSet = new TreeSet<>(new Comparator<Dog>() {
@Override
public int compare(Dog o1, Dog o2) {
//核心比较条件
int result = o2.getAge() - o1.getAge();
//如果核心比较条件的不是0(不重复) 但如果核心比较的结果是0(本次比较的两个元素内容是否一样)
if (result == 0)
result = o1.equals(o2) ? 0 : 1;
return result;
}
});
单列集合的工具类Collections
Collections是单列集合的工具类,里面提供了一些更加方便的操作单列集合的静态方法。
//public static <T> boolean addAll(Collection<T> c,T... elements) : 向参数一的单列集合中添加多个元素
Collections.addAll(strList, "张二狗", "李狗蛋", "刘铁柱");
System.out.println("strList : " + strList);
//public static void shuffle(List<?> list) : 打乱参数集合的元素顺序(随机性 每次打乱结果都不一样)
Collections.shuffle(strList);
System.out.println("shuffle : " + strList);
单列集合的工具类Collections-两种排序方案
Java中的排序规则:自定义排序
只要是基于Comparator接口声明排序规则的都叫做自定义排序 灵活性是最高的!
//public static <T> void sort(List list,Comparator c) : 基于自定义排序规则完成集合内容排序 ▲建议
Collections.sort(dogList, (o1, o2) -> o1.getAge() - o2.getAge());
dogList.forEach(System.out::println);
Java中的排序规则:自然排序
用于排序的对象自身包含了排序规则,不需要额外声明排序规则,就是自然排序。 灵活性不高!
如何让对象自身包含排序规则:
1.实现自然排序接口Comparable,该接口是个泛型接口,在实现该接口的时候需要声明泛型的具体类型。
具体类型的功能就是用于声明和哪种类型的元素进行比较。
public class Dog implements Comparable<Dog> {
@Override
public int compareTo(Dog o) {
//是this和o进行比较,逻辑和之前一样。
return o.getAge() - this.getAge();
}
}
2.在调用Collections.sort的时候可以直接传递集合,不需要额外声明排序规则。
//public static <T> void sort(List list) : 基于元素包含排序规则完成集合内容排序
Collections.sort(dogList);
如果一个类自身包含了排序规则的前提下,依然是可以再手动给出自定义规则 【自定义排序的优先级更高】