目录
4:set接口
1:什么是容器
Java中一切皆对象,那么程序运行的过程中,程序会产生各种各样的对象引用数据,这些对象数据既有基本数据类型,也有引用数据类型和我们自己创建的JavaBean,数组Array对于每一种语言,都是基本和重要的数据容器,都是数组却有他的局限性,例如类型简单,长度固定等缺点,难以支持这些复杂些数据,所有Java引入了容器概念,容器是对于Array的扩展。支持各种各样的数据类型。同时容器在Java中也存在各种各样的类型,有不同的优缺点,我们了解这些容器的结构,优缺点,才能够熟练运用,写出正确高效的程序。
1.1:Java容器的结构图
其中淡绿色的表示接口,红色的表示我们经常使用的类。
1.2:Iterable可迭代接口
我们先看一下Iterable接口,这个接口中的迭代器方法和forEach方法如下,
我们进入迭代器接口,看到里边有三个方法,分别对应迭代器的迭代方法,在collection中继承了可迭代接口,
2:Collection接口
collection接口是这些容器的父类接口,该父类接口方法很多,定义了这些不同容器的一些基本方法。供不同的容器继承实现。
collection接口方法如下:这个接口方法是一些通用的方法。
Collection接口定义的方法 | ||
返回值 | 方法名(参数类型 参数) | 描述 |
int | size() | 容器中对象的数目 |
boolean | isEmpty() | 是否为空 |
void | clear() | 清空 |
boolean | contains(Object element) | 是不是包含element对象 |
boolean | add(Object element) | 添加element的对象 |
boolean | remove(Object element) | 移除element对象 |
Iterator | iterator() | 返回一个Iterator对象,用于遍历容器中的对象 |
boolean | containsAll(Collection c) | 是否包含c容器中的所有对象 |
boolean | addAll(Collection c) | 把c容器中的所有对象添加到容器中 |
boolean | removeAll(Collection c) | 从容器中移除C容器中存在的所有对象 |
boolean | retainAll(Collection c) | 求当前的集合类与C容器的交集 |
Object[] | toArray() | 把容器中的所有对象转换到对应的数组中 |
3:List接口
list接口继承collection接口,除了父接口的方法,list也拓展了很多自己的方法,list方法如下,扩展了很多自己的数据操作方法,增删改查等方法
boolean add(E e) |
将指定的元素追加到此列表的末尾(可选操作)。 |
void add(int index, E element) |
将指定的元素插入此列表中的指定位置(可选操作)。 |
boolean addAll(Collection<? extends E> c) |
按指定集合的迭代器(可选操作)返回的顺序将指定集合中的所有元素附加到此列表的末尾。 |
boolean addAll(int index, Collection<? extends E> c) |
将指定集合中的所有元素插入到此列表中的指定位置(可选操作)。 |
void clear() |
从此列表中删除所有元素(可选操作)。 |
boolean contains(Object o) |
如果此列表包含指定的元素,则返回 true 。 |
boolean containsAll(Collection<?> c) |
如果此列表包含指定 集合的所有元素,则返回true。 |
boolean equals(Object o) |
将指定的对象与此列表进行比较以获得相等性。 |
E get(int index) |
返回此列表中指定位置的元素。 |
int hashCode() |
返回此列表的哈希码值。 |
int indexOf(Object o) |
返回此列表中指定元素的第一次出现的索引,如果此列表不包含元素,则返回-1。 |
boolean isEmpty() |
如果此列表不包含元素,则返回 true 。 |
Iterator<E> iterator() |
以正确的顺序返回该列表中的元素的迭代器。 |
int lastIndexOf(Object o) |
返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1。 |
ListIterator<E> listIterator() |
返回列表中的列表迭代器(按适当的顺序)。 |
ListIterator<E> listIterator(int index) |
从列表中的指定位置开始,返回列表中的元素(按正确顺序)的列表迭代器。 |
E remove(int index) |
删除该列表中指定位置的元素(可选操作)。 |
boolean remove(Object o) |
从列表中删除指定元素的第一个出现(如果存在)(可选操作)。 |
boolean removeAll(Collection<?> c) |
从此列表中删除包含在指定集合中的所有元素(可选操作)。 |
default void replaceAll(UnaryOperator<E> operator) |
将该列表的每个元素替换为将该运算符应用于该元素的结果。 |
boolean retainAll(Collection<?> c) |
仅保留此列表中包含在指定集合中的元素(可选操作)。 |
E set(int index, E element) |
用指定的元素(可选操作)替换此列表中指定位置的元素。 |
int size() |
返回此列表中的元素数。 |
default void sort(Comparator<? super E> c) |
使用随附的 Comparator排序此列表来比较元素。 |
default Spliterator<E> spliterator() |
在此列表中的元素上创建一个Spliterator 。 |
List<E> subList(int fromIndex, int toIndex) |
返回此列表中指定的 fromIndex (含)和 toIndex之间的视图。 |
Object[] toArray() |
以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。 |
<T> T[] toArray(T[] a) |
以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。 |
3.1ArrayList(有序集合,可以重复,可变数组)
3.11ArrayList特点
ArrayList:
0:由于是连续的内存区域,对内存要求高,并且长度可能存在浪费
1:ArrayList数据是有序和可以重复的
2:是一个可变的数组,数据存储在内存中的连续存储区域,用过下标访问数据,扩容长度是n+0.5n(int 新长度 = 旧长度 + (旧长度 >> 1))
3:查询速度快:无论查询哪一个数据,都是通过下标获取,时间恒定,速度很快。
4:添加和删除速度慢:因为数据可变,每一次添加和删除都会造成数组数据位移,时间和n/2(n是数据总个数)
5:ArrayList是线程不安全的,尽量不要定义全局变量的ArrayList,如果非要使用:第一,使用synchronized关键字;第二,可以用Collections类中的静态方法synchronizedList();对ArrayList进行调用即可。
6:modCount的作用是每次修改都会++。用来记录修改次数
3.12ArrayList数据结构和特点分析
我们是怎么知道ArrayList的这些忒点的呢?如果只是生记硬背,很难全面掌握,我们通过分析数据结构和查看源码,来得出结论
数组是在内存中开辟一段连续的空间,并在此空间存放元素。通过下标访问元素。当某一个元素被删除或者在某一个元素前添加元素的时候,会导致数组后续元素位置整体移动,所有查询快,删除,添加慢
list有add方法, 通过查看源码,看到数组添加的过程中,
主要分两步,
第一步:初始化数组长度为10,并且如果添加长度超过初始化长度的时候,扩展长度,为 newSize=oldSize+oldSize/2(1.5倍的长度新增)
第二步:将数据放入到数组的该下标位置,
就是
//ArrayList是一个object数组
transient Object[] elementData;
//数组长度
private int size;
//添加主方法,不是线程安全的,
public boolean add(E e) {
//第一步:增量验证,设置默认长度是10
ensureCapacityInternal(size + 1);
//第二步:在新的数组下表中赋值给要添加的元素
elementData[size++] = e;//在新的数组下表中赋值给要添加的元素
return true;
}
//设置默认初始化数组的长度是10
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
//模数加一
modCount++;
// 长度溢出的时候,每次扩展是以前程度的一般,
//newSize=oldSize+oldSize/2(1.5倍的长度新增)
//此处采取的位移运算 int newCapacity = oldCapacity + (oldCapacity >> 1);
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//长度验证方法
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//这行代码底层是C语言实现的,创建一个新的长度的数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
remove方法,同样的我们再来看删除方法源码:得出结论删除速度比较快慢。
3.13ArrayList线程安全代码验证
那么线程安全问题呢,通过源码我们看到。添加方法的多线程异常很容易出现在数组扩展的那个环节:
我们启动两个个线程,操作同一个list,就会出现线程异常问题:
线程1代码
public class Thred1 extends Thread{
private ArrayList<Integer> list;
public ArrayList<Integer> getList() {
return list;
}
public void setList(ArrayList<Integer> list) {
this.list = list;
}
@Override
public void run() {
//线程1中添加10个元素
for (int i = 10; i < 20 ; i++) {
list.add(i);
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程2代码
public class Thred2 implements Runnable{
private ArrayList<Integer> list;
public ArrayList<Integer> getList() {
return list;
}
public void setList(ArrayList<Integer> list) {
this.list = list;
}
@Override
public void run() {
//线程2中添加10个元素
for(int i=0;i<10;i++) {
list.add(i);
try {
Thread.sleep(2);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
测试主方法代码:
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
ArrayList<Integer> list = new ArrayList<Integer>();
//继承thread,插入10-20
Thred1 thred1=new Thred1();
thred1.setList(list);
thred1.start();
//实现runable,插入0-10
Thred2 thred2=new Thred2();
thred2.setList(list);
Thread thread=new Thread(thred2);
thread.start();
// 主方法线程休息1秒钟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("数据长度是:"+list.size());
for (int i = 0; i < list.size(); i++) {
System.out.println("第" + (i + 1) + "个元素为:" + list.get(i));
}
}
}
输出结果分析:
null出现在11和16的位置,是因为数组进行了两次了扩容,默认长度是10,第一次扩容长度是15,这两次扩容,在多线程条件下,非常容易出现错误.数组下标越界是出现在扩容之前
1:数组下标越界分析:由此看到add元素时,实际做了两个大的步骤:
判断elementData数组容量是否满足需求
在elementData对应位置上设置值
这样也就出现了第一个导致线程不安全的隐患,在多个线程进行add操作时可能会导致elementData数组越界。具体逻辑如下:
列表大小为9,即size=9
线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。
线程B此时也进入add方法,它获取到size的值也为9,也开始调用ensureCapacityInternal方法。
线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。
线程B也发现需求大小为10,也可以容纳,返回。
线程A开始进行设置值操作, elementData[size++] = e 操作。此时size变为10。
线程B也开始进行设置值操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException.
另外第二步 elementData[size++] = e 设置值的操作同样会导致线程不安全。从这儿可以看出,这步操作也不是一个原子操作,它由如下两步操作构成:
elementData[size] = e;
size = size + 1;
在单线程执行这两条代码时没有任何问题,但是当多线程环境下执行时,可能就会发生一个线程的值覆盖另一个线程添加的值,具体逻辑如下:
2:null值分析:
线程A开始添加一个元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。
接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到size的值依然为0,于是它将B也放在了elementData下标为0的位置上。
线程A开始将size的值增加为1
线程B开始将size的值增加为2
这样线程AB执行完毕后,理想中情况为size为2,elementData下标0的位置为A,下标1的位置为B。而实际情况变成了size为2,elementData下标为0的位置变成了B,下标1的位置上什么都没有。并且后续除非使用set方法修改此位置的值,否则将一直为null,因为size为2,添加元素时会从下标为2的位置上开始。
3.2:LinkedList(有序集合,可重复,双链表)
3.21:LinkedList特点
1:Linkedlist是双向链表结构,在内存中存储不是连续的,对内存的要求低,便于利用碎片化内存。
2:链表的删除和在指定位置新增比数组要快点,但是查询和修改不占优势,需要遍历局部链表,按照二分法遍历
3:链表和数组都在顺序的末尾添加的数据的话,没有大的区别,
4:linkedlist不是线程安全的,
3.22:LinkedList数据结构和特点分析
linked是一种链表结构,并且是双向链表
关于单双向链表的结构如下:
单链表只有next方法,只能按照链表串,逐个遍历;凡是双链表能向前或者向后遍历
咱们查看源代码在双向链表的结构中,每一个数据都有前后置节点和数据体本身,这个指针的length在32位系统中是4字节,在64位系统中是8个字节的空间
在添加方法中:从源码看到,是在链表的末尾一直添加元素
//前置元素
transient Node<E> first;
//后置元素
transient Node<E> last;
//添加链表方法
void linkLast(E e) {
//第一次添加,后置节点为null
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++;
}
//构造方法,创建节点
private static class Node<E> {
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;
}
}
在查询方法中:从代码中而我们知道是按照二分法查询,要么从开始,要么从结束,开始遍历数据。每一次都要遍历一部分链表
删除方法源码分析: 可以看到无论是删除节点的情况,需要二分法找到当前节点,返回断开当前的节点的手脚,把节点前后的数据手脚连接起来。
3.23:ArrayList线程安全代码验证
线程1和线程2的代码如下,同时对一个linked list操作,然后在main方法中读取该集合
package com.thit.linkedlist;
import java.util.LinkedList;
import java.util.List;
//线程1代码
public class Thred1 extends Thread{
private List<String> list;
public List<String> getList() {
return list;
}
public void setList(List<String> list) {
this.list = list;
}
@Override
public void run() {
for(int i=0;i<5;i++) {
try {
Thred1.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
list.add("线程1:"+i);
}
}
}
package com.thit.linkedlist;
import java.util.LinkedList;
import java.util.List;
//线程2代码
public class Thred2 extends Thread{
private List<String> list;
public List<String> getList() {
return list;
}
public void setList(List<String> list) {
this.list = list;
}
public void run() {
for(int i=0;i<5;i++) {
try {
Thred2.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
list.add("线程2:"+i);
}
}
}
main方法读取:
package com.thit.linkedlist;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class Test1 {
public static void main(String[] args) {
// TODO Auto-generated method stub
//线程不安全的
LinkedList<String> list=new LinkedList<String>();
//线程安全的,但是该方法效率很低,使整体加锁的方式
//List<String> list= Collections.synchronizedList(new LinkedList<String>());
Thred1 t1=new Thred1();
t1.setList(list);
t1.start();
Thred2 t2=new Thred2();
t2.setList(list);
t2.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("集合长度是:"+list.size());
for (int i=0;i<list.size();i++) {
System.out.println("输出结果是:"+list.get(i));
}
}
}
读取结果如下:可以看到长度不一定是10
我们针对结果分析,在linked的add方法和remove方法以及其他的方法都存在并发异常,size 和公共属性,存在被多线程读取相同的值,然后覆盖的情况,所以导致希望的长度上是10,但是实际长度是9,所以尽量避免多线程操作 。
4:set接口
4.1:HashSet
4.11:HashSet特点
1:底层实现HashMap,初始化数组长度是2的四次方为16,阀值是16*0.75=12的时候,数组重新扩容
2:hashmap在jdk1.8中是数据+链表+红黑树(当链表长度超过8的时候变成红黑树)
3:数据不可重复,且无序,可以有null
4:将插入数据的hashcode转换为数组下标的位置,无论增加还是查询都是先看看这个位置有没有元素,然后比较元素是否相等
相等的话覆盖
4.12:HashSet数据结构和特点分析
数据结构如下:
4.13:HashSet线程安全
线程不安全,只是使用了hashmap的key和hashcode用来验证数据不能重复。
add代码分析
//第一步:创建了HashMap
public HashSet() {
map = new HashMap<>();
}
//第二步:调用了HashMap方法的put
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
//第三步:调用了HashMap方法的put,只用到我注释的几行代码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
HashMap.Node<K, V>[] tab;
HashMap.Node<K, V> p;
int n, i;
//如果tab==table是null的话 初始化hashMap 只会执行一次
if ((tab = table) == null || (n = tab.length) == 0) {
n = (tab = resize()).length;
}
//查找tab中的位置是否有数据,没有直接放到该位置
if ((p = tab[i = (n - 1) & hash]) == null) {
tab[i] = newNode(hash, key, value, null);
} else {
HashMap.Node<K, V> e;
K k;
//tab的P位置有数据,
//如果传递的数据的hash和该位置的hash相同key也相同,就覆盖数据
//e就是P了。hashset的有效代码只有这么多了
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) {
e = p;
} else if (p instanceof HashMap.TreeNode) {
e = ((HashMap.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
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;
}
4.2:TreeSet
4.21:TreeSet特点
1:底层实现TreeMap,红黑树结构,会出现喜欢转变色操作,维护二叉树平衡性
2:不允许有null
3:数据不可重复,且无序,但是满足红黑树的西安小结构
4.22:TreeSet数据结构和特点分析
底层代码是TreeMap的实现,红黑树结构
4.23:TreeSet线程安全
线程不安全
具体讲解件下一章