Java基础_容器(第一章Collection)

目录

1:什么是容器

1.1:Java容器的结构图

1.2:Iterable可迭代接口

2:Collection接口

3:List接口 

3.1ArrayList(有序集合,可以重复,可变数组)

3.11ArrayList特点

3.12ArrayList数据结构和特点分析

3.13ArrayList线程安全代码验证

3.2:LinkedList(有序集合,可重复,双链表)

3.21:LinkedList特点

3.22:LinkedList数据结构和特点分析

3.23:ArrayList线程安全代码验证

4:set接口

4.1:HashSet

4.11:HashSet特点

4.12:HashSet数据结构和特点分析

4.13:HashSet线程安全

4.2:TreeSet

4.21:TreeSet特点

4.22:TreeSet数据结构和特点分析

4.23:TreeSet线程安全


1:什么是容器

        Java中一切皆对象,那么程序运行的过程中,程序会产生各种各样的对象引用数据,这些对象数据既有基本数据类型,也有引用数据类型和我们自己创建的JavaBean,数组Array对于每一种语言,都是基本和重要的数据容器,都是数组却有他的局限性,例如类型简单,长度固定等缺点,难以支持这些复杂些数据,所有Java引入了容器概念,容器是对于Array的扩展。支持各种各样的数据类型。同时容器在Java中也存在各种各样的类型,有不同的优缺点,我们了解这些容器的结构,优缺点,才能够熟练运用,写出正确高效的程序。

1.1:Java容器的结构图

其中淡绿色的表示接口红色的表示我们经常使用的类。 

1.2:Iterable可迭代接口

我们先看一下Iterable接口,这个接口中的迭代器方法和forEach方法如下,

我们进入迭代器接口,看到里边有三个方法,分别对应迭代器的迭代方法,在collection中继承了可迭代接口,

2:Collection接口

collection接口是这些容器的父类接口,该父类接口方法很多,定义了这些不同容器的一些基本方法。供不同的容器继承实现。

collection接口方法如下:这个接口方法是一些通用的方法。

Collection接口定义的方法
返回值方法名(参数类型 参数)描述
int size()容器中对象的数目
booleanisEmpty()是否为空
voidclear()清空
booleancontains(Object element)是不是包含element对象
booleanadd(Object element)添加element的对象
booleanremove(Object element)移除element对象
Iteratoriterator()返回一个Iterator对象,用于遍历容器中的对象
booleancontainsAll(Collection c)是否包含c容器中的所有对象
booleanaddAll(Collection c)把c容器中的所有对象添加到容器中
booleanremoveAll(Collection c)从容器中移除C容器中存在的所有对象
booleanretainAll(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线程安全

线程不安全

具体讲解件下一章

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值