java集合

java集合

1.集合概述

不管是哪一种数据结构,其实本质上都是容器来着,就是用来装对象的。因此,要搞清楚两点:(1)如何存储(2)存储特点

数组

(1)存储结构

  • 逻辑结构:线性的
  • 物理结构:顺序的存储结构
  • 申请内存:一次申请一大段连续的空间,一旦申请到了,内存就固定了。
  • 存储特点:所有数据存储在这个连续的空间中,数组中的每一个元素都是一个具体的数据(或对象),所有数据都紧密排布,不能有间隔。
    (2)存储操作
    查询:每一个元素都有一个数值下标,可以通过下标瞬间定位到某个元素
    增加:先使用total变量辅助记录实际存储元素个数从尾部增加
    从其他位置插入:先把index位置开始所有元素后移,然后数组名[index]=新元素
    删除:先把index后面的元素前移,然后数组名[total–]=null
    改:直接数组名[index]=新元素
    (3)优缺点
    优点:按索引查询效率高
    缺点:添加/删除效率低,因为都涉及到移动元素;无法直接获取有效元素的个数。

集合

为了可以满足用户数据更多种的逻辑关系,而设计的一系列的不同于数组的可变的聚合的抽象数据类型.(大致分为三种:list,set,map)
在这里插入图片描述
在这里插入图片描述

Collection 集合

1、添加元素
(1)add(Object obj):添加元素对象到当前集合中
(2)addAll(Collection other):添加other集合中的所有元素对象到当前集合中,即this = this ∪ other
2、删除元素
(1) boolean remove(Object obj) :从当前集合中删除第一个找到的与obj对象equals返回true的元素。
(2)boolean removeAll(Collection coll):从当前集合中删除所有与coll集合中相同的元素。即this = this - this ∩ coll
3、判断元素
(1)boolean isEmpty():判断当前集合是否为空集合。
(2)boolean contains(Object obj):判断当前集合中是否存在一个与obj对象equals返回true的元素。
(3)boolean containsAll(Collection c):判断c集合中的元素是否在当前集合中都存在。即c集合是否是当前集合的“子集”。
4、其他
(1)int size():获取当前集合中实际存储的元素个数
(2)boolean retainAll(Collection coll):当前集合仅保留与c集合中的元素相同的元素,即当前集合中仅保留两个集合的交集,即this = this ∩ coll;
(3) Object[] toArray():返回包含当前集合中所有元素的数组

向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals()
containsAll(Collection coll1):判断形参collI中的所有元素是否都存在于当前集合中。

5.代码
(1)添加元素

package com.atguigu.part01.collection;

import java.util.ArrayList;
import java.util.Collection;

import org.junit.Test;

@SuppressWarnings("all")
public class TestCollection {
	@Test
	public void test1(){
		Collection coll = new ArrayList();//ArrayList是Collection的子接口List的实现类
		coll.add("张三");
		coll.add("李四");
		coll.add("王五");
		coll.add("张三");
		
		System.out.println("coll集合元素的个数:" + coll.size());
	}
}

在这里插入图片描述
(2)删除元素

public void test3(){
		Collection coll = new ArrayList();
		coll.add("张三");
		coll.add("李四");
		coll.add("王五");
		coll.add("张三");
		
		System.out.println("coll集合元素的个数:" + coll.size());
		coll.remove("张三");
		System.out.println("coll集合元素的个数:" + coll.size());
	}

Collection集合的遍历

(1)foreach循环遍历
Java 5时Collection接口继承了java.lang.Iterable接口,因此Collection系列的集合就可以直接使用foreach循环遍历。格式:

for(元素的类型  迭代变量 : 数组/集合名称){
	  //每一次循环迭代变量依次代表集合中的一个元素
}
public void test6(){
		Collection coll = new ArrayList();
		coll.add(1);
	coll.add(2);
		coll.add(3);
		coll.add(4);

		for(Object obj : coll){
		System.out.println(obj);
		}
		//foreach循环4次,obj每一次代表一个元素
	}

(2)Iterator迭代器遍历
因为Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象,该对象可以用于迭代集合中的元素。
Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建 Iterator 对象,则必须有一个被迭代的集合。
集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素位置。
常用方法:
(1)boolean hasNext():如果仍有元素可以迭代,则返回 true
(2)Object next():返回迭代的下一个元素。
(3)void remove():从迭代器指向的 collection 中移除迭代器返回的最后一个元素(可选操作)。
注意:
在调用it.next()方法之前必须要调用it.hasNext()进行检测。若不调用,且下一条记录无效,直接调用it.next()会抛出NoSuchElementException异常。
如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,再调用remove都会报IllegalStateException

(3)ListIterator迭代器
List 集合额外提供了一个 listIterator() 方法,该方法返回一个 ListIterator 对象, ListIterator 接口继承了 Iterator 接口,提供了专门操作 List 的方法:
void add():通过迭代器添加元素到对应集合
void set(Object obj):通过迭代器替换正迭代的元素
void remove():通过迭代器删除刚迭代的元素
boolean hasPrevious():如果以逆向遍历列表,往前是否还有元素。
Object previous():返回列表中的前一个元素。
int previousIndex():返回列表中的前一个元素的索引

boolean hasNext()
Object next()
int nextIndex()

	@Test
	public void test7(){
		Collection c = new ArrayList();
		c.add(new Student(1,"张三"));
		c.add(new Student(2,"李四"));
		c.add(new Student(3,"王五"));
		c.add(new Student(4,"赵六"));
		c.add(new Student(5,"钱七"));
		
		Iterator iterator = c.iterator();
		while(iterator.hasNext()){
			Student next = (Student) iterator.next();
			//例如:要删除学号为1的学生对象
			if(next.getId()==1){
				iterator.remove();
			}
		}
	}

List集合

List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。
1、添加元素
void add(int index, Object ele)
boolean addAll(int index, Collection eles)
2、获取元素
Object get(int index)
List subList(int fromIndex, int toIndex)
3、获取元素索引
int indexOf(Object obj)
int lastIndexOf(Object obj)
4、删除和替换元素
Object remove(int index)
Object set(int index, Object ele)
5.List接口的实现类
Vector 类:动态数组
ArrayList类:动态数组
Stack类:堆栈
LinkedList:双向链表,从JDK1.6之后又实现了双端队列

6.ArrayList与Vector区别
(1)它们的底层物理结构都是数组,我们称为动态数组
(2)数组的初始化容量,如果在构建ArrayList与Vector的集合对象时,没有显式指定初始化容量,那么Vector初始化为10,而ArrayList在JDK1.6时也是10,而之后的版本初始化为默认的空数组,如果ArrayList一开始初始化为默认的空数组,那么添加第一个元素时,扩容为默认大小为10的数组。
(3)之后当容量不够时,ArrayList扩容增加原来的50%,Vector扩容增加原来的1倍或按照用户指定的capacityIncrement增长。
(4)ArrayList线程不安全,效率高,Vector线程安全,效率低。
Vector因为版本古老,支持Enumeration 迭代器。但是该迭代器不支持快速失败。而Iterator和ListIterator迭代器支持快速失败。如果在迭代器创建后的任意时间从结构上修改了向量(通过迭代器自身的 remove 或 add 方法之外的任何其他方式),则迭代器将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就完全失败,而不是冒着在将来不确定的时间任意发生不确定行为的风险。
(5)ArrayList的源码分析:

ArrayList List = new ArrayList();//底层创建了长度是10的Object[]数组elementData
List.add(123);//elementData[0] = new Integer(123);
List.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。
默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。
结论:建议开发中使用带参的构造器: ArrayList list = new ArrayList(int capacity)
jdk 8中ArrayList的变化:
ArrayList list = new ArrayList();//底层0bject[] elementData初始化为{}.并没有创建
list.add(123);//第一次调用add()时,底层才创建了长度为10的数组,并将数据123添加到elemenData中。
源码为:

1.初始化
transient Object[] elementData;  //实际存储元素的数组

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
    //初始化为一个默认的空数组
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
2. 添加元素
private static final int DEFAULT_CAPACITY = 10;//默认容量

public boolean add(E e) {
    //确保当前数组的容量是够得
        ensureCapacityInternal(size + 1);  // Increments modCount!!

        //将新元素添加到[size++]的位置
        elementData[size++] = e;
        return true;
}
private void ensureCapacityInternal(int minCapacity) {
    //如果是第一次添加
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //扩容为默认容量大小:10
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        
        //每一次添加都要判断是否需要扩容
        ensureExplicitCapacity(minCapacity);
}
3.扩容
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 如果需要扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
}

    private void grow(int minCapacity) {
        // 先获取当前数组的容量
        int oldCapacity = elementData.length;

        //新容量为当前容量 + 当前容量的一半
        int newCapacity = oldCapacity + (oldCapacity >> 1);

        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);

        // 拷贝原数组中的元素至新数组,并返回新数组的引用
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
ArrayList物理结构是数组,决定了它的存储特点是:需要开辟连续的存储空间来存储元素,当存储容量不够时,需要扩容,增加容量为原来的1.5倍

7.LinkedList类
除了实现 List 接口外,LinkedList 类还为在列表的开头及结尾 get、remove 和 insert 元素提供了统一的命名方法。这些操作允许将链接列表用作堆栈、队列或双端队列。

(1)JDK1.6之后LinkedList实现了Deque接口。双端队列也可用作 LIFO(后进先出)堆栈。如果要使用堆栈结构的集合,可以考虑使用LinkedList,而不是Stack。
堆栈方法 等效Deque方法
push(e) addFirst(e)
pop() removeFirst()
peek() peekFirst()



public static void main(String[] args) {
		LinkedList list = new LinkedList();
		//入栈
		list.addFirst(1);
		list.addFirst(2);
		list.addFirst(3);
		
		//出栈: LIFO(后进先出)
		System.out.println(list.removeFirst());//3
		System.out.println(list.removeFirst());//2
		System.out.println(list.removeFirst());//1
		//栈空了,会报异常java.util.NoSuchElementException
		System.out.println(list.removeFirst());
	}

用作队列时,将得到 FIFO(先进先出)行为。将元素添加到双端队列的末尾,从双端队列的开头移除元素。
Queue 方法 等效 Deque 方法
add(e) addLast(e)
offer(e) offerLast(e)
remove() removeFirst()
poll() pollFirst()
element() getFirst()
peek() peekFirst()
(2)Linkedlist 源码分析:
Linkedlist list = new LinkedList(); 内部声明了Node类型的first和ast属性,默认值为null
list.add(123);//将123封装到Node中,创建了Node对象。
其中,Node定义为:体现了LinkedList的双向链表的说法

 transient Node<E> first;//指向链表的第一个结点

transient Node<E> last;//指向链表的最后一个结点
   
   //LinkedList中有一个内部类Node类型 
   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;
        }
    }
    public boolean add(E e) {
    //默认链接到链表末尾
        linkLast(e);
        return true;
}
void linkLast(E e) {
    //用l记录当前链表的最后一个结点对象
        final Node<E> l = last;

        //创建一个新结点对象,并且指定当前新结点的前一个结点为l
        final Node<E> newNode = new Node<>(l, e, null);
//当前新结点就变成了链表的最后一个结点
        last = newNode;

        if (l == null)
//如果当前链表是空的,那么新结点对象,同时也是链表的第一个结点
            first = newNode;
else
            //如果当前链表不是空的,那么最后一个结点的next就指向当前新结点
            l.next = newNode;

       //元素个数增加
        size++;

       //修改次数增加
        modCount++;
}

Set集合

Set接口:存储无序的、不可重复的数据
HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值
----LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序进行遍历
TreeSet:可以按照添加对象的指定属性,进行排序。

(1)HashSet
以HashSet为例说明:
1.无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值进行判断
2.不可重复性:保证添加的元素按照equals()判断时,不能返回true 即:相同的元素只能添加一个
添加元素的过程:以HashSet为例:
我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置),判断数组此位置上是否已经有元素,如果此位置上没有其他元素,则元素a添加成功。如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值:如果hash值不相同,则元素a添加成功,如果hash值相同,进而需要调用元素a所在类的equlas()方法.(equals()返回true,元素a添加失败。equals()返回false,则元素a添加成功。)
结论:
向Set中添加的数据,其所在的类一定要重写hashCode()和equals()
重写的hashCode()和equals()尽可能保持一致性:相等的对象必须具有相等的散列码

(2)LinkedHashSet
LinkedHashSet作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据
优点:对于频繁的遍历操作,LinkedHashSet效率高于Hashset
(3)TreeSet
1.向Treeset中添加的数据,要求是相同类的对象。
2.两种排序方式:自然排序和定制排序
3.自然排序中,比较两个对象是否相同的标准为:compareTo()返回e.不再是equals()
补充
关于 TreeSet 的排序实现,如果是集合中对象是自定义的或者说其他系统定义的类没有实现 Comparable 接口,则不能实现 TreeSet 的排序,会报类型转换(转向 Comparable 接口)错误。 换句话说要添加到 TreeSet 集合中的对象的类型必须实现了 Comparable 接口。 不过 TreeSet 的集合因为借用了 Comparable 接口,同时可以去除重复值,而 HashSet 虽然是 Set 接口子类,但是对于没有复写 Object 的 equals 和 hashCode 方法的对象,加入了 HashSet 集合中也是不能去掉重复值的。

Map

双列数据,储存key-value对的数据
HashMap:作为Map的主要实现类;线程不安全的,效率高;存储null的key和value
|----LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,此类执行效率高于HashMap
TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序,底层使用红黑树
Hashtable:作为古老的实现类;线程安全的,效率低;不能存储nulL的key和vaLue
|----Properties:常用来处理配置文件。key和value都是string类型
HashMap的底层:数组+链表(jdk7及之前),数组+链表+红黑树(jdk 8)
Map结构的理解:
Map中的key:无序的、不可重复的,使用set存储所有的key。key所在的类要重写equals()和hashcode()
Map中的value:无序的、可重复的,使用Collection存储所有的value。value所在的类要重写equals()

一个键值对:key-vaLue构成了一个Entry对象。
Map中的entry:无序的、不可重复的,使用Set存储所有的entry
HashMap 底层实现原理:
HashMap的底层实现原理,jdk7为例说明:
HashMap map = new HashMap() 在实例化以后,底层创建了长度是16的一维数组Entry[] table
…可能已经执行过多次put…
map.put( key1,value1) 首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。如果此位置上的数据为空,此时的key1-vaLue1添加成功。----情况1
如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据。如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2
如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)的方法
如果equals()返回false:此时key1-vaLue1添加成功。----情况3
如果equals()返回true:使用value1替换vaLue2
补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。

在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。

  1. new HashMap():底层没有创建一个长度为16的数组
  2. jdk8底层的数组是:Node[].而非Entry[]
  3. 首次调用put()方法时,底层创建长度为16的数组
  4. jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。当数组的某一个索引位置上的元素以链表形式存在的数据个数>8且当前数组的长度>64时,此时此索引位置上的所有数据改为使用红黑树存储。
    Properties 类是 Hashtable 的子类,Properties 可保存在流中或从流中加载。属性列表中每个键及其对应值都是一个字符串。
    在这里插入图片描述
    在这里插入图片描述
源码:(ctrl +点击查看源码,查看这个类的所有方法为ctrl+f12

resize 扩容代码:
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
				//扩容为原来的2倍
                newThr = oldThr << 1; // double threshold
        }
   //......此处省略其他代码
}
(2)
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);
        //....省略大量代码
		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) {
					//遍历每个链表节点 是否与此时的添加的hash 和key相等
                    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;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值