读书小记——JAVA数据容器总结

数组:

数组是指将相同类型的元素按照一定顺序进行排列的集合,数组的存储是一块固定长度的连续的存储空间。
eg:当我们在声明一个数组时是需要指定其长度的

int[] data = new int[10];

数组的存储是连续的,因此只要知道第一个元素的存储位置,对应的就能知道后面所有元素的位置,
随机访问一个元素的时间复杂度是O(1)。
数组删除/增加一个元素,需要移动删除/增加位置之后的所有元素,时间复杂度是O(n)。


链表:

链表是一种非连续、非顺序的结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,链表由一系列结点组成。
链表的存储位置是无法确定的,通过一个节点的指针指向才能确定另一个节点的存储位置,因此随机访问一个元素的
效率是比较低的,需要遍历才能确定各个节点的存储位置,时间复杂度是O(n),但是相比较与数组,链表的删除/插入
的效率是比较高的,它不需要移动链表内的元素,只需要改变删除/插入左右两边元素的指针的指向,时间复杂度是O(1)。
常见的链表有:单向链表/双向链表/循环链表

//eg:java实现的类似链表结构的节点类
class Node<E> {
    E element; //当前节点元素
    Node previous; //上一个节点
    Node next; //下一个节点

    public Node(E element, Node previous, Node next) {
        this.element = element;
        this.previous = previous;
        this.next = next;
    }
}

栈:

数据遵循后进先出原则(LIFO)
简单分析下Java中Stack类。

/*
 * Stack继承自Vector类,因此也是基于数组的操作,
 * 如何实现后进先出的呢?
 * 很简单,Stack数据的操作主要分为入栈和出栈,当控制数据的进入和弹出都只能从一个方向操作时,即实现了后进先出
 * eg:依次放入abcd:a<-----b<------c<-------d(从右边依次放入)
 * 此时若从左取出则不符合后进先出,只能从同一个方向取出操作(即右边),则取出顺序为(dcba)。
 * 下面看下java中Stack的入栈和出栈是不是这么一回事?
*/
//入栈操作
public E push(E item) {
        addElement(item);//执行了添加一个元素的方法

        return item;
    }
public synchronized void addElement(E obj) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);//数组扩容方法,这里不展开描述,在ArrayList再做说明
        elementData[elementCount++] = obj;//扩容后将数据添加到当前数组的后面
    }
//出栈操作
public synchronized E pop() {
    E       obj;
    int     len = size();

    obj = peek();//获取到数组最末尾的元素的引用
    removeElementAt(len - 1);//将数组最末尾的元素清除

    return obj;
}
public synchronized E peek() {
    int     len = size();

    if (len == 0)
        throw new EmptyStackException();
    return elementAt(len - 1);
}

队列

数据遵循先进先出原则(FIFO)
数据从一个方向进入,另外一个方向移除。通过上边栈操作稍微改动就能实现。这里不再展开分析


Collection集合通用接口定义
Collections集合常见操作的工具类
List:
存放有序(和添加的顺序一致),可重复的元素。
ArrayList:
底层是通过数组来实现的,但是不需要考虑数组长度越界,当元素超出固定长度后,会进行容量的扩充。非线程安全

//eg:在增加元素时,数组容量进行了扩张
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    private void ensureCapacityInternal(int minCapacity) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
            }
            ensureExplicitCapacity(minCapacity);
    }
    private void ensureExplicitCapacity(int minCapacity) {
            modCount++;

            // overflow-conscious code
            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:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

LinkeList:
底层是通过链表的实现,非线程安全


Set:
存放不可重复的元素,放入Set的类必须实现equals和hashCode方法(比较两个对象是否是同一个对象的方法)
HashSet:无序,不可重复对象。不保证元素的添加顺序,底层采用 哈希表算法,查询效率高。判断两个元素是否相等,equals() 方法返回 true,hashCode() 值相等。
即要求存入 HashSet 中的元素要覆盖 equals() 方法和 hashCode()方法
TreeSet:有序,不可重复对象,添加自定义对象的时候,必须要实现 Comparable 接口,并要覆盖 compareTo(Object obj) 方法来自定义比较规则


Map:
Map存储的是两个集合之间映射关系,以key/value的形式形成映射可看成一条数据,key不允许重复,value可重复
HashMap:采用哈希算法,key不可重复,所以key对象需重写equals和hashCode方法。
HashMap的原理分析:采用数组+链表的方式实现(散列表)。
利用数组存放哈希表的关键字key,通过哈希函数就能够快速得到其存储的位置,但是为了处理hashcode冲突,即对某个元素
通过哈希函数获得存储地址,发现该位置已经被占用。
解决办法,采用链表,当发现某个位置被占用时,看是否是同一个元素,如果是,直接将元素存入该位置,如果不是,则将该元素散列(创建新的地址存放),然后关联到链表的next。
散列表:散列表用数组加链表实现。数组的每一列称之为桶,根据数据的hashcode与桶的长度取余,得到的余数就是该元素所在桶的索引,如果当前桶的位置还没有其他的元素,则直接将该元素插入到该位置,如果该位置已经存在了其他元素(散列冲突),比较两个元素是否一致,如果不一致则将该元素添加到该桶的链表的头部。
TreeMap:同理,红黑树实现有序,key必须要实现 Comparable 接口,并要覆盖 compareTo(Object obj) 方法来自定义比较规则。


HashMap是非线程安全的,
HashTable是线程安全的,通过查看源码的实现可以看到,对其的操作方法上都加上了同步锁(Synchronized),不推荐使用,使用多线程目的是实现多个任务的并行执行,添加同步锁之后,数据的访问因为持有了锁,需要锁的获取和释放,线程对数据的访问是串行的,并且还增加了锁的获取释放的消耗。
Java中的免锁集合:
CopyOnWriteArrayList、CopyOnWriteArraySet、ConCurrentHashMap。

  1. CopyOnWriteArrayList:
    采用读写分离的思想 ,当对List进行修改操作的时候,先复制出一份,然后修改复制出来的List,这时候如果有其他线程读取List的话读取到的是旧的数据,当修改操作完成后会将该对象的引用指向修改完后的数据,这时候读取数据将读取到新指向的数据,当然,修改操作必须是加锁的,否则会copy多个副本出来。下面我们通过源码来证实上面的说法是否正确:
public boolean add(E e) {
        synchronized (lock) {
        //可以看到此处加了同步锁
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);//复制新的Array,同时容量扩充了1
            newElements[len] = e;//复制出的Array执行了增加的操作
            setArray(newElements);//将对象的引用指向新的Array
            return true;
        }
    } 

知道了原理之后,可以可自己手动试试写一个CopyOnWriteMap.
存在的问题:

  1. 内存占用,增加了内存负荷,频繁GC,导致相应响应的时间变长。
  2. 数据一致性:该List只能保证数据最终的一致性,如果需要数据修改后立马能够读取到修改后的数据不建议使用。

2.ConCurrentHashMap:
分段加锁,减小并发冲突。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值