在码代码的过程中,我们常常需要对大量对象引用管理,为了有效的归类管理,将同类的引用放在一个数据容器中。

容器主要由:Collection与Map两种构成。

一.概述

1.Collection

包含三大类,set、list、queue。思维导图如下所示:

1.1 Set

TreeeSet:使用二叉树的原理对新 add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。

HashSet: HashSet 通过 hashCode 值来确定元素在内存中的位置。一个 hashCode 位置上可以存放多个元素。

LinkedHashSet:是HashSet与LinkedHashMap的结合。底层使用 LinkedHashMap 来保存所有元素,其所有的方法操作上又与 HashSet 相同。

1.2 List

ArrayList:是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。

因此,它适合随机查找和遍历,不适合插入和删除。

Vector:也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问 ArrayList慢。

LinkedList:用链表结构存储数据的,适合动态插入与删除,随机访问、遍历较慢,此外提供了用于操作表头表尾的方法,可当作栈、队列、双向队列使用。

2.Map

相关知识思维导图如下所示:

2.1 HashMap(数组+链表+红黑树)

根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null。HashMap线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。

2.2 HashTable

很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类,并且是线程安全的,任一时间有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分锁。

2.3TreeMap

TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。

如果使用排序的映射,建议使用 TreeMap。

2.4 LinkedHashMap(记录插入顺序)

LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

二.深入理解

下面就几个重要的知识点深入讲解:

1、ArrayList

ArrayList是基于数组实现的,其默认大小为10,但存储的内容超出十个就将考虑扩容。

(1)、扩容

扩容一般发生在添加元素时,有一个ensureCapacityInternal()方法保证容量足够,如果不够,就使用grow()方法进行扩容,扩容后变为原有容量的1.5倍。

public boolean add(E e) {    //元素添加方法ensureCapacityInternal(size + 1);  // 保证有足够的空间elementData[size++] = e;   //直接添加return true;
}

在初次添加元素的时候需要做特别的处理,然后判断是否超过最大容量,判断在现有的size+1的基础上是否需要扩容,然后扩容,拷贝数组添加元素。

private void ensureCapacityInternal(int minCapacity) {
/*如果使用默认的构造参数的话,则选择minCapacity和DEFAULT_CAPACITY中较大的一个来作为最小的初始
容量, 大家可能奇怪,既然判断了初始化这一说,为何还会有传入的minCapacity 和DEFAULT_CAPACITY
比较这一说,是因为ensureCapacityInternal也会被addAll调用
*/if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);}ensureExplicitCapacity(minCapacity);
}
//需要扩容的情况
private void ensureExplicitCapacity(int minCapacity) {modCount++;// overflow-conscious codeif (minCapacity - elementData.length > 0)grow(minCapacity);}

grow()代码实现扩容

private void grow(int minCapacity) {// overflow-conscious codeint 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);//copy the data to new array}

(2)元素删除

需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),所以这就是为什么说ArrayList 删除元素的代价是非常高的。代码就忽略了,较为简单的向前移位操作。

(3)序列化

所谓的JAVA序列化与反序列化,序列化就是将JAVA 对象以一种的形式保持,比如存放到硬盘,或是用于传输。反序列化是序列化的一个逆过程。规定被序列化的对象必须实现java.io.Serializable这个接口,而我们分析的目标ArrayList同样实现了该接口。

但在实际开发工作中我们并不需要所有元素都需要序列化,比如银行卡、密码等信息,同时数组是由transient修饰,默认不序列化,此时就需要对部分元素进行序列化。ArrayList 实现了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。(此处不展开讲、有兴趣可以自行查询相关知识)

(4).Fail-Fast

modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。

在进行序列化或者迭代遍历等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。

出现的场景:

在单线程中:遍历的过程中进行修改(使用Iterator遍历)

多线程中:一个线程修改另外一个线程的遍历的Collection

解决的方法:对对象加锁、使用java.util.concurrent包中的类来代替ArrayList和Hashmap来表达。

(5).线程安全问题

ArrayList没有同步机制,所以是线程不安全的,有以下两种方式对其进行操作使其安全。

a.使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList。

List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);

b.可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。

List

注:CopyOnWriteArrayList()

核心思想:读写分离,写操作在复制数组进行、读操作在原数组进行,互相分离,互不干扰。

写操作需要加锁,防止并发写入时导致写入数据丢失。

写操作结束之后需要把原始数组指向新的复制数组。

public boolean add(E e) {final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();int len = elements.length;Object[] newElements = Arrays.copyOf(elements, len + 1);newElements[len] = e;setArray(newElements);return true;} finally {lock.unlock();}
}final void setArray(Object[] a) {array = a;
}

优点:大大提高了读操作的性能,因此很适合读多写少的应用场景。

缺点:内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;

数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。

结论:CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。

2、Vector

(1)线程安全

实现与ArrayList类似,不过使用Synchronized进行同步。

public synchronized boolean add(E e) {modCount++;ensureCapacityHelper(elementCount + 1);elementData[elementCount++] = e;return true;
}public synchronized E get(int index) {if (index >= elementCount)throw new ArrayIndexOutOfBoundsException(index);return elementData(index);
}

(2)扩容

Vector的构造函数与ArrayList有所不同,Vector 的构造函数可以传入 capacityIncrement 参数,它的作用是在扩容时使容量 capacity 增长 capacityIncrement。如果这个参数的值小于等于0,扩容时每次都令 capacity 为原来的两倍。默认情况下,capacityIncrement的值为0,也就是说默认情况下 Vector 每次扩容时容量都会翻倍。

public Vector(int initialCapacity, int capacityIncrement) {super();if (initialCapacity < 0)throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);this.elementData = new Object[initialCapacity];this.capacityIncrement = capacityIncrement;
}

扩容方法如下

private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length;int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);if (newCapacity - minCapacity < 0)newCapacity = minCapacity;if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);elementData = Arrays.copyOf(elementData, newCapacity);
}

(3).与ArrayList对比

1.Vector是同步的,线程安全,ArrayList不安全。

2.同样是由于同步原因,开销就比 ArrayList 要大,访问速度更慢。

3.Vector 每次扩容请求其大小的 2 倍,ArrayList是1.5倍。

3、LinkedList

基于双向链表实现,使用 Node 存储链表节点信息。

private static class Node<E> {E item;Node<E> next;Node<E> prev;
}transient Node<E> first;
transient Node<E> last;//每个链表存储了 first 和 last 指针

与ArrayList对比,可以归结为数组和链表的区别:

数组支持随机访问,但插入删除的代价很高,需要移动大量元素;

链表不支持随机访问,但插入删除只需要改变指针。

4、HashMap

数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端。

数组:数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费。

 数组是静态分配内存,并且在内存中连续。数组利用下标定位,时间复杂度为O(1)数组插入或删除元素的时间复杂度O(n)数组的特点是:寻址容易,插入和删除困难

链表:链表存储区间离散,占用内存比较宽松。

 链表是动态分配内存,并不连续。链表定位元素时间复杂度O(n)链表插入或删除元素的时间复杂度O(1)链表的特点是:寻址困难,插入和删除容易。

结合二者,哈希表((Hash table)既满足了数据的查找方便,同时不占用太多的内容空间。

(1)HashMap的存储结构:

包含了一个 Entry 类型的数组 table。Entry 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值和散列桶取模运算结果相同的 Entry。

transient Entry[] table;
static class Entry<K,V> implements Map.Entry<K,V> {final K key;V value;Entry<K,V> next;int hash;Entry(int h, K k, V v, Entry<K,V> n) {value = v;next = n;key = k;hash = h;}public final K getKey() {return key;}public final V getValue() {return value;}public final V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}......
}

(2)拉链法解决哈希冲突

HashMap<String, String> map = new HashMap<>();
map.put("K1", "V1");
map.put("K2", "V2");
map.put("K3", "V3");

上面代码为新建hashMap并插入过程,如下图所示;插入到同一个桶,使用头插法进行插入。

确定桶下标:

int hash = hash(key);
int i = indexFor(hash, table.length);

计算哈希值

final int hash(Object k) {int h = hashSeed;if (0 != h && k instanceof String) {return sun.misc.Hashing.stringHash32((String) k);}h ^= k.hashCode();// This function ensures that hashCodes that differ only by// constant multiples at each bit position have a bounded// number of collisions (approximately 8 at default load factor).h ^= (h >>> 20) ^ (h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4);
}public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);
}

确定桶下标的最后一步是将 key 的 hash 值对桶个数取模:hash%capacity,如果能保证 capacity 为 2 的 n 次方(这也就是为什么HashMap扩容每次都是2倍的原因),那么就可以将这个操作转换为位运算。

static int indexFor(int h, int length) {return h & (length-1);//位运算效果与取模操作一致,但性能更高。
}
/*位运算 y      : 10110010
x-1     : 00001111
y&(x-1) : 00000010
*/
//取模: y%x : 00000010

(3)扩容

设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此查找的复杂度为 O(N/M)。

为了让查找的成本降低,应该使 N/M 尽可能小,因此需要保证 M 尽可能大,也就是说 table 要尽可能大。HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。

void addEntry(int hash, K key, V value, int bucketIndex) {Entry<K,V> e = table[bucketIndex];table[bucketIndex] = new Entry<>(hash, key, value, e);if (size++ >= threshold)resize(2 * table.length);
}

扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中,因此这一步是很费时。

void resize(int newCapacity) {Entry[] oldTable = table;int oldCapacity = oldTable.length;if (oldCapacity == MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return;}Entry[] newTable = new Entry[newCapacity];transfer(newTable);table = newTable;threshold = (int)(newCapacity * loadFactor);
}void transfer(Entry[] newTable) {Entry[] src = table;int newCapacity = newTable.length;for (int j = 0; j < src.length; j++) {Entry<K,V> e = src[j];if (e != null) {src[j] = null;do {Entry<K,V> next = e.next;int i = indexFor(e.hash, newCapacity);e.next = newTable[i];newTable[i] = e;e = next;} while (e != null);}}
}

进行扩容时,需要把键值对重新计算桶下标,从而放到对应的桶上,使用 hash%capacity 来确定桶下标,而每次扩容都是2的n次方,简化了重新计算的。

(4)比较:

A.HashMap和HashTable的区别

1.HashTable的方法是同步的,在方法的前面都有synchronized来同步,HashMap未经同步,所以在多线程场合要手动同步。

2.HashTable不允许null值(key和value都不可以) ,HashMap允许null值(key和value都可以)。

3.HashTable有一个contains(Object value)功能和containsValue(Object value)功能一样。

4.HashTable使用Enumeration进行遍历,HashMap使用Iterator进行遍历。

5.HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。

B.HashMap与HashSet的关系:

1、HashSet底层是采用HashMap实现的:

public HashSet() {
map = new HashMap<E,Object>();
}

2、调用HashSet的add方法时,实际上是向HashMap中增加了一行(key-value对),该行的key就是向HashSet增加的那个对象,该行的value就是一个Object类型的常量。

C.Hashtable 和 ConcurrentHashMap 的关系

主要区别就是加锁的粒度以及如何加锁,ConcurrentHashMap 的加锁粒度要比HashTable更细一点。将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

5.ConcurrentHashMap

ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。Segment 继承自 ReentrantLock。具体信息可以参考链接[https://zhuanlan.zhihu.com/p/31614308]讲的很生动,当然,有耐心可以去看看JUC源码。

hello.world:JVM基础知识大整合,面试看这一篇就够了!​zhuanlan.zhihu.com

如果对您有所帮助就点赞吧!最近找工作,欢迎大家一起交流。欢迎收藏点赞!

(文中部分图片源自书本、某大佬博客,侵删)

java怎样将多个list写入txt中并且不覆盖原有数据_java容器,面试必备知识点整理!...相关推荐

  1. 如何将数据写入excel中,而不覆盖原有数据

    之前直接用pandas库,然后to_excel(),结果直接将原始数据直接覆盖,幸亏有备份.(友善提醒,做数据处理之前,先将数据本地备份一份,确认完全没有问题,然后还是备份一份 ,再进行实盘操作,否则 ...

  2. 高级 Java 面试通关知识点整理

    转载自 高级 Java 面试通关知识点整理 1.常用设计模式 单例模式:懒汉式.饿汉式.双重校验锁.静态加载,内部类加载.枚举类加载.保证一个类仅有一个实例,并提供一个访问它的全局访问点. 代理模式: ...

  3. python将结果写入txt文件_python(如何将数据写入本地txt文本文件)

    一.读写txt文件 1.打开txt文件 file_handle=open('1.txt',mode='w') 上述函数参数有(1.文件名,mode模式) mode模式有以下几种: #w 只能操作写入 ...

  4. java 将整型数组内容写入txt文件_Java自学-I/O 字节流

    Java 字节流 InputStream OutputStream InputStream字节输入流 OutputStream字节输出流 用于以字节的形式读取和写入数据 步骤 1 : ASCII码 所 ...

  5. 将文件夹下的所有文件名写入txt中

    在当前文件夹下 shift +鼠标右键 打开终端   输入 tree /f > 1.txt 则将文件夹下的文件名放入1.txt文件中.不包含绝对路径. 要么是写个.bat文件,文件内容为 dir ...

  6. Java 往文件中写数据,新写入的数据总是覆盖原有数据

    参考https://blog.csdn.net/jack_bob/article/details/78430565 第一种 使用FileOutputStream, 在构造FileOutputStrea ...

  7. 将信息写入TXT文本中(java)

    代码: package com.web.framework.bbpp.module.yituhezisone.util;import java.io.File; import java.io.File ...

  8. mysql百万数据写入_mysql 百万级数据查找,并写入txt文件

    public class ConnectMysql { public List getTag(int start,int end){ List intList=new ArrayList(); Con ...

  9. python 写txt 换行_写入txt文本的内容为什么没换行效果?

    PHP把内容写入文件,并每次自动换行 weixin_4089680029712018-07-20 Java文本追加.换行.输出到TXT u01422837514692015-12-15 Android ...

最新文章

  1. 功能:人脉(People Hub)2-群发短信(创建组)
  2. 如何使用Worktile进行敏捷项目开发管理
  3. formdata传参java用对象怎么接受_Java基础4
  4. 《一江春水向东流》——任正非
  5. 高可用高性能分布式文件系统FastDFS实践Java程序
  6. ES6新特性之函数优化-箭头函数
  7. matlab-robtics学习笔记
  8. vs2019 更新安装错误_本月Windows 10累积更新再出BUG:安装时跳出错误代码
  9. php文件统计源码,php统计文件中的代码行数
  10. mysql内置时间函数大全_MySQL 的时间函数 大全
  11. css悬浮在某个span后面,在contenteditable div中的span元素后面设置光标
  12. Android学习笔记之Bitmap位图的缩放
  13. pythonarp攻击_python通过scapy模块进行arp断网攻击
  14. 我想向你们推荐一门最好的python课程——CS61A学习笔记(一)
  15. Z-TEKCE COM控制线驱动
  16. 将pandas中object类型转换为int类型
  17. 苏宁小店上线咖啡业务,成为国内O2O社区便利店的创领者!
  18. 马士兵oracle_,马士兵oracle视频教程学习笔记个人整理
  19. 1.JsDroid命令行调试命令
  20. JAVA使用接口实现类的功能------JAVA入门基础教程

热门文章

  1. openlayers之obj.js提供的功能函数
  2. 2017数学建模b题回顾_12月热门文章和2017年回顾
  3. 改变根深蒂固的流程可以提高客户忠诚度
  4. ssm架构 开源项目_6个开源架构项目签出
  5. JMeter4.0使用笔记 使用Badboy录制脚本,使用代理录制脚本
  6. TypeScript 之泛型
  7. es6 Promise.race()方法
  8. es6 Promise.all()方法
  9. 基于固定坐标与基于参考坐标系得到的机械手的微分运动不同
  10. SLAM Cartographer(10)子图