集合是java中用来存储数据的容器
白话说,程序中如int类型、String类型等Object类的基本数据类型,以及我们自定义的类所生成的对象,
都是我们所需的数据,而数据一多,我们就要对其做处理。
之前我们可以将其存到数组中,但还是有很多不方便之处,如空间、删除、增加等问题
而集合就是聚集了多种不同的存储数据的容器,并将一些处理数据的方法,封装起来
这样我们在处理数据的时候便十分便捷。
集合框架包括接口、实现类、相关数据结构与算法。
在java中,一些较复杂的结构和算法,前人已经写好,我们只需调用即可,当然我们也只有懂底层代码,才可以走的更远。不妨点击了解一下数据结构介绍和实现
接口: 表示集合的抽象数据类型。接口允许我们操作集合时不必关注具体实现,从而达到“多态”。在面向对象编程语言中,接口通常用来形成规范。
实现类: 实现了接口内部的方法,除此还有自身独有的方法
点击了解数据结构介绍和实现:
简介中部分来自
算法在一个实现了某个集合框架中的接口的对象身上完成某种有用的计算的方法,例如查找、排序等。
这些算法通常是多态的,因为相同的方法可以在同一个接口被多个类实现时有不同的表现。
事实上,算法是可复用的函数。它减少了程序设计的辛劳。
集合框架通过提供有用的数据结构和算法使你能集中注意力于你的程序的重要部分上,
而不是为了让程序能正常运转而将注意力于低层设计上。
通过这些在无关API之间的简易的互用性,使你免除了为改编对象或转换代码以便联合这些API而去写大量的代码。
它提高了程序速度和质量。
特点: 如开头说,对象封装了数据,对象有时也是我们所需的数据,数据量一多,也需要存储。集合就用来处理数据量比较多时。
可以根据情况决定,使用数组还是集合。当然有些集合底层用的是数组结构。
容量自增长;
提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量;
允许不同 API 之间的互操作,API之间可以来回传递集合;(目前还没具体了解过API)
可以方便地扩展或改写集合,提高代码复用性和可操作性。
通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。
Iterator是集合里面的一个接口,在java.util;
包下,
该接口以下三个方法经常使用
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
hasNext()是寻找下一个元素是否存在,如果存在就返回true
E next()可以看到它带有泛型,它的含义是返回当前指向的元素
而利用这两个方法,我们就可以迭代出一个集合的所有元素了
Iterator it=set1.iterator();
while (it.hasNext()){
System.out.println(it.next());
}
但要注意的是,每用一次.next(),之前一定要使用.hasnext()确认下个元素是否存在,不然会发生NoSuchElementException异常
关于default void remove()
,只要每次使用了next,都可以进行remove进行删除一次、。
而对应的集合的remove只能使用一次,或者说不能使用集合的方法对元素进行处理。可参考这篇推文
有序性——存进去什么顺序,取出来也什么顺序
可重复性——不多说
有下标——Lsit集合都有下标,从0开始,以1递增(其中双向链表以.get(i)获取元素值)
List集合可插入多个null。
常见的实现类是ArrayList——底层为数组,LinkedList——底层是双向链表,Vector——底层数组(线程安全,但效率低)
底层实现: 数组
同步性和效率: 不同步,非线程安全,检索效率高,但增删效率低(末尾增删效率也高)
默认容量: 10private static final int DEFAULT_CAPACITY = 10;
当然new一个对象的时候,也可以自定义容量。
在JDK1.7直接创建一个初始容量为10的数组;
在JDK1.8一开始创建的是初始容量为0的数组,当添加第一个元素之后,数组的容量才变为10
扩容机制: 1.5倍
因为接口List实现类ArrayList底层是数组,所以其检索效率较高,但是增删效率在非末尾处效率较低。
方法:
list.size();
list.lastIndexOf(Object obj); 获取List集合中某一对象出现的最后位置
list.add() 按顺序 在末尾加对象
list.add(index,obj) 在index位置 加入obj 后面的往后移
list.get(index) 获取第index的数据
list.contains(obj)
list.remove(index/obj) 移除某位置 或某对象
list.iterator()
list.isEmpty()
list.idexOf(obj) 获取位置
list.toArray() 转化为Object[] 数组
底层实现: 双向链表
同步性和效率: 不同步,非线程安全,随机增删效率高,但是检索效率使用.get(i)对某一位置进行检索还可以,因为在每次add元素的时候都会size++,但如果要定位检索效率就低。
容量: 链表没有初始容量和扩容问题,在空间上利用率高,要多少就利用多少,但是它也因此内存空间不连续。
所以比较适合随机位置增、删。但是其基于链表实现,所以在定位时需要线性扫描,效率比较低。
方法:
主要多了一个前后遍历数组的功能
ListIterator<Integer> ite=l.listIterator(index);
向后遍历 ite.hasNext ite.next
向前遍历 ite.hasPrevious() ite.previous()
list.size();
list.lastIndexOf(Object obj); 获取List集合中某一对象出现的最后位置
list.add() 按顺序 在末尾加对象
list.add(index,obj) 在index位置 加入obj 后面的往后移
list.get(index) 获取第index的数据
list.contains(obj)
list.remove(index/obj) 移除某位置 或某对象
list.iterator()
list.isEmpty()
list.idexOf(obj) 获取位置
list.toArray() 转化为Object[] 数组
底层代码:
底层实现: 数组
同步性和效率: 同步,线程安全,效率低
特点: 和数组一样
容量: 10
扩容: 2倍
Vector现在用的比较少了主要原因是效率低下,比如
出现原因:
适合在预先不知数组大小内容, 需要频繁的增删改查 需要考虑向量类。
import java.util.Vector;
public class vectortest {
public static void main(String[] args) {
Vector v=new Vector();
for (int i = 0; i < 10000; i++) {
v.add(i);
}
}
}
后面不够的时候 复制两倍,那占了不必要的空间,所以相对用ArrayList效率更快,而且现在有其他方法解决ArrayList效率低的问题。(虽然目前没有学)
此外Vector是同步的,属于强同步类,在应用中,程序员也可以实现同步。
Vector还有一个子类Stack。
栈,先进后出原则,队列,先进先出原则
可看之前写的文章
public class Stack<E> extends Vector<E>
Stack<Integer> stack=new Stack<>();
stack.push(obj);
stack.pop();
stack.peek();
stack.empty();
stack.size();
栈继承于Vector
队列的创建
Queue<Integer> queue=new LinkedList<>(); 单向队列 只能尾加 头删
Deque<Integer> queu=new LinkedList<>(); 双向队列 头尾都可加删
Deque接口有三种实现方式,分别是LinkedList、ArrayDeque、LinkedBlockingDeque,
其中LinkedList是最常用的。
方法:
queu.add(); 队尾插入元素 空间不够 抛出异常
queue.offer(); 队尾插入元素 空间不够返回null
queu.poll(); 删除第一个元素, 没有时返回为null
queue.remove() 删除第一个元素
queue.offerLast(); 尾巴加
queu.offerFirst(); 头加
queu.pollLast(); 尾巴删
queu.pollFirst(); 尾巴加
无序性——无序,但是对于继承接口SortedSet集合中,因为底层采用的是TreeMap(自平衡二叉树)
不可重复性——同一内存空间只能存在一个,即同一内存空间,有再多的引用也只是一个东西
无下标——自平衡二叉树和散列表的数据结构,所以对于具体的元素来说,没有下标。
常见的实现类是HashSet——底层为HashMap(哈希表),TreeSet——底层是TreeMap(自平衡二叉树)。
底层实现:
HashSet底层实际上是实现了一个HashMap
而HashMap实际上是数组+链表+红黑树
当链表个数超过八,就变为红黑树。
public HashSet() {
map = new HashMap<>();
}
//对于HashSet,我们只需添加key部分的值,其value都是一个常数
//首先先通过哈希函数将我们的值转化为hashcode,然后再将其转化为数组索引值(因为算法原因,不同的hashcode可能转化为索引值相同)
//然后再根据索引值,找到散列表的table部分对应的数组部分,
//先看该部分是否已经有元素
//若没有 直接加入
//若有,先调用equals进行比较,如果不同则加到最后,如果相同就不相加
//注意的是,为避免单链表过长,到达八个且table到达64时会将其转化为红黑树
//所以自定义类 要重写equals和hashcode方法。
关于hashcode
假设我们要添加一个字符串元素
Set s2=new HashSet();
s2.add("abc");
底层代码其实现的是调用HashMap的一个构造器,在add部分 实际上用的额HashMap的一个put方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
再进入底层HashMap的put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
这里可以看到一个hash(key)
,是HashMap的计算一个hashcode的方法
可见 是先进入添加元素的的一个hashCode()的方法,即我们需要存储时需要重写hashCode(),得到哈希值,但是需要对其进行无符号右移十六位之后才是我们所需的hash值,防止冲突。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//无符号右移十六位 防止冲突
}
如上所述 加的是字符串,那进入String看看,String有没有重写这个方法(Object都重写了)
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
效率: 增删效率高,只能存储一个null;
容量: 16
扩容: 2
HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
HashSet如何检查重复?HashSet是如何保证数据不可重复的?
向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。
HashSet 中的add ()方法会使用HashMap 的put()方法。
顺序: 往HashSet中存入元素的时候,底层是HashMap,存入到key中,且经过哈希函数转化为HashCode,存放在固定的位置,所以遍历取出的时候 位置是固定的。
HashSet小练习:
题目:定义一个Employee类,其private元素name,salary,Mydata(其中Mydata是一个类,含有出生年月信息),要求:当姓名和出生年月日相同,被认为是 同一个人。将员工加入HashSet中。
package com.hdu.hashSet;
import com.sun.xml.internal.ws.api.model.wsdl.WSDLOutput;
import org.w3c.dom.ls.LSOutput;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
public class HashSetTest02 {
public static void main(String[] args) {
Mydata<Integer> mydata01=new Mydata<>(1997,03,15);
Mydata<Integer> mydata02=new Mydata<>(1997,06,29);
Employee02<String,Double,Mydata> employee=new Employee02<>("张三",16880.0,mydata01);
Employee02<String,Double,Mydata> employee01=new Employee02<>("李四",18990.6,mydata02);
Employee02<String,Double,Mydata> employee02=new Employee02<>("张三",24000.0,mydata02);
Employee02<String,Double,Mydata> employee03=new Employee02<>("张三",36000.0,mydata01);
HashSet<Employee02> set=new HashSet<>();
System.out.println("employee添加,返回"+set.add(employee));
System.out.println("employee01添加,返回"+set.add(employee01));
System.out.println("employee02添加,返回"+set.add(employee02));
System.out.println("employee03添加,返回"+set.add(employee03));
Iterator<Employee02> iterator=set.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
class Employee02<N,S,D>{
private N name;
private S salary;
private D birthday;
public Employee02(N name, S salary, D birthday) {
this.name = name;
this.salary = salary;
this.birthday = birthday;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Employee02)) return false;
Employee02<?, ?, ?> that = (Employee02<?, ?, ?>) o;
return Objects.equals(name, that.name) &&
Objects.equals(birthday, that.birthday);
}
@Override
public int hashCode() {
return Objects.hash(name, birthday);
}
@Override
public String toString() {
return "员工信息:"+"\n姓名:"+name+",薪水:"+salary+","+birthday;
}
}
class Mydata<I>{
private I year,month,day;
public Mydata(I year, I month, I day) {
this.year = year;
this.month = month;
this.day = day;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Mydata)) return false;
Mydata<?> mydata = (Mydata<?>) o;
return Objects.equals(year, mydata.year) &&
Objects.equals(month, mydata.month) &&
Objects.equals(day, mydata.day);
}
@Override
public int hashCode() {
return Objects.hash(year, month, day);
}
@Override
public String toString() {
return "出生年月:"+year+" "+month+" "+day;
}
}
关键在于你做比较的hashCode和equals重写,看你重写哪几项。
它实际上是在HashMap的基础上添加了一个头尾指针,这样不同下标下的元素可以连起来,且是按照存入的顺序连接起来。
LinkedHashMap详解
且迭代方式类似 用Map.Entry 或者迭代器都可。
由于继承了Set接口,所以它的特点也是无序不可重复的
但是在SortedSet接口下的实现类,因为底层用的是TreeMap,可以进行自动排序,因此它是有序的
底层实现: TreeMap
其自定义类存储的时候要重写compare方法
详解java中TreeMap和TreeSet排序
未进行仔细研究
栈,先进后出原则,队列,先进先出原则
可看之前写的文章
public class Stack<E> extends Vector<E>
栈继承于Vector
HashMap里存放的是k-v键值对:
根据k的哈希值来寻找数组索引,v相当于和k绑在一起,通过k可以找到v值
当k相同时,我们再次存放元素,v值会被替换
k不可以重复,v值key重复。
哈希值: 通过重写hashcode方法(有些类如Object会重写)得到初步的hashCode,再进行无符号右移16位,防止哈希冲突
1首次添加元素: 先将哈希table进行扩容,初始值为16,增长因子为0.75,每次扩2倍
然后将哈希值-键key-值value-空指针加入对应的table[i]
2非首次添加元素: 先判断后面加入的键值key的哈希值,是否存在,
2.1如果此时哈希值不存在,那么加入新的table[j]
2.2如存在则判断键值对是否已经存在(通过判断内存空间引用和equals),
2.2.1如果存在 即重复,则跳出循环,
2.2.2如果不存在,则单链表形式加入 但此时加入要考虑,如果链表上有重复的 也不加入跳出循环;
如果链表上没有加入(但此时要考虑 链表个数是否大于等于8,否则考虑树化)
树化条件:某个单链表个数大于等8,且table容量大于等于64
如若只是单链表到达8,后面也是先将容量进行扩容
2.2.3如果此时table某一索引上已经树化,则直接在红黑树的基础上添加。
哈希表扩容的要求:初始化后加入的第一个元素时;元素个数大于等于总表的0.75倍时扩容;某个单链表个数大于等于8且table容量小于64时。
putVal方法详解
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;//初始化table容量16, 默认加载因子为0.75,即到了12,就扩容,2倍一括。
if ((p = tab[i = (n - 1) & hash]) == null)//做与运算,得到table对应位置的下标
//并判断该数组是不是为空
//如果空 则直接加入结点
//如果不为空,说明要生成一个链表,
//在该数组后面继续加
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//同一索引值位置的hash值一样,
//且加入的元素是同一个引用或者内存空间相同
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
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);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//当总数为8(包括table索引上),就将其转化为红黑树(红黑树还有一个条件是数组个数大于等于64)
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;
}
存放的也是键值对K-V形式;
Hashtable的kv都不能为null
hashtable是同步的 属于线程安全(HashMap不同步 线程不安全)有关键字 synchronized
初始容量是11,加载因子为0.75即在8那会进行扩容,但扩容是*2+1 int newCapacity = (oldCapacity << 1) + 1;
继承于Hashtable 并且实现了Map接口 是线程安全
存储形式也是用k-v键值对,而且只支持Object类
被称为属性类
关于IO流那会用到这个。
待学习
SortedMap存入的键是无序且不可重复的,存入之后,通过比较强进行排序,实际上是一个红黑树。
TreeMap原理实现及常用方法
比较器看我之前写的这篇
目前还没设计Stream的学习所以没有进行Map和List之间的转化,待更新
import java.util.*;
import java.util.stream.Collectors;
/**
* 集合之间的转化
* 数组转List
* List转数组
*/
public class collTocoll {
public static void main(String[] args) {
Integer[] arr1={1,2,3,4,2,2};//int是基本数据类型 对应Integer是int的封装类
List list=collTocoll.arrToList(arr1);
//尝试将ArrayList与LinkedList进行相互转化 但二者之间没有直接联系 所以不能强制转化
LinkedList linkedList=new LinkedList(list);//public LinkedList(Collection extends E> c) { ...}
System.out.println("通过构造方法将ArrayList与LinkedList进行相互转化:"+linkedList);//也可以反过来。
//将List转数组
Object[] listToarr=list.toArray();
System.out.println("List转数组:"+Arrays.toString(listToarr));
Object[] linklistToarr=linkedList.toArray();
System.out.println("LinkedList转数组:"+Arrays.toString(linklistToarr));
//Set和List之间也可以通过new一个构造方法直接转
Set set=new HashSet(linkedList);
System.out.println("List转Set:"+set);
}
public static <T> List arrToList(T[] arr){//方法加泛型
List list=Arrays.asList(arr);
System.out.println("数组转ArrayList:"+list);//ArrayList 底层实现是数组 所以其可以相互转化
return list;
}
}
java8中的Stream用法详解
Collections是一个针对集合操作的工具类,可以改变集合的排序方法,寻找最值,交换、复制、替换等功能。
Java中Collections类详细用法
Java集合框架不看后悔系列
Java集合框架总结
Java集合容器面试题(2020最新版)
java集合超详解
彻底理解HashMap及LinkedHashMap
待学习:红黑树、散列表、Stream