Java集合类全解

 

 

java的对数组的复制操作有两种,深度复制和浅复制

1:深度复制是指复制数组的索引和数组的内容。修改目标值不会影响原数组的值。

2:浅复制是指复制数组的索引,修改目标的值会影响数组的值。

char知识点补充:

char储存的是字。一个汉字代表两个字节。每个char字在unicode码表上都可以找到。unicode码表上(一个字对应一个数字,数字也就是二进制)。

System:

System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length):

arrayCopy对数组的复制操作,如果原数组储存的是基本类型,那就是深度复制,如果储存的是对象,那就是浅复制。

src表示源数组,srcPos表示源数组要复制的起始位置,desc表示目标数组,length表示要复制的长度。

它是线程不安全的。他是高效的,因为它对数组的复制是从内存中直接复制的,没有寻址这个操作。

String:

底层用的是char数组,通过System.arrayCopy()来进行String的各种操作。

equals():根据char[]数组的每一个char是否相同来判断true or false;

index(String str):判断传入参数的第一个char,然后遍历this。当char中和this的char[]相同时,再使用类似于equals的逻辑来判断str是否存在this中。如果不存在,继续遍历剩下char[]的值。

startwith():逻辑跟equals差不多。

concat(): 例如 String str = ”abc“; str.concat("dbf");首先复制abc的作为目标char数组。然后dbf作为源数组。然后将原数组的值复制到目标数组的后面。也就是个重组的概念。

compareTo():这个只有当两个字符串相等的时候比较才有意义,会返回0。因为这个方法时根据unicode码表去判断的,中文字排在前的就大。所以如果不重写,没有比较意义。

contains():该方法跟indexof逻辑是一样的。

charAt(int index):根据索引获取char[]数组的索引值。

format():待定

getBytes():待定

split(String regex):

subString(int beginIndex):将char数组从beginIndex索引后的值重新复制到目标数组。

toLowerCase():

toUpperCase():

ArrayList:

默认容量为10个。底层用的是Object[]数组(elementData)

add(E e):先看容量是否足够。(size>elementData.length),如果大于,创建一个新的数组,容量为原数组的1.5倍。然后将原来的数组复制到新数组中。

remove(int index):校验删除的索引是否越界。其实就是两个相同的数组之间的复制。例如

1,2,3,4}.{1,2,3,4}。删除索引2。那就保持索引2前面的数据不变。将索引2后面的数据复制到目标数组中。注意:删除的索引越靠前,复制的数组越大。

get(int index):也就是数组中获取索引值,没什么好说的。

set(int index, E element):也就是将数组的索引值替换一下而已,没什么好说的。

总结:add和remove都要移动数据,也就是重新复制数组。其实add的代价也不是很大。假如能把数组直接转成ArrayList。ArrayList的容量足够大,也就不会发生数组复制的现象。也就没什么代价。当然,当数组越大,发生数组复制的现象会越少,所以问题不大。remove的话每执行一次,都会发生数组复制,所以性能很低。

LinkedList:

底层用的是双链表结构(Node)。

add(E e):先判断该容器是否有值,有的话,取最后一个Node。将传入的元素赋值给原最后一个Node的next对象上(Node)。将原最后一个节点(Node)赋值给刚传入的元素节点的prev上。如果没有。容器就将传入的元素作为第一个first Node。

remove(int index) :首先将容器的size>>1(除以2)。当要索引的值小于size>>1。从链表左边开始遍历。当索引的值大于size>>1时,从右边开始遍历。然后得到要删除的Node节点。

根据要删除的Node节点获取上一个节点和下一个节点。将要删除的Node节点next和prev都

设置为Null。然后判断上一个节点是否为空(为空证明删除的是双链表的开头)。那就将下一个节点设置为双链表的first节点。判断下一个节点是否为空(为空证明删除的是双链表的结尾)。那就将上一个节点设置为双链表的last节点。主要first节点的prev和last节点的next都要设置为null。

get(int index):先判断索引是否越界。然后根据index遍历到该节点,获取到该节点的值。遍历规则还是看size>>1。

set(int index, E element):其实就是跟remove一样先遍历到该节点。然后将索引对应的Node节点的值修改为element。

总结:remove、get、set、都需要通过遍历链表来执行相关操作。当要操作的节点处在中间时,这三个操作都很耗性能。但是当要操作的节点在开头和结尾的不远处时。linkedList的性能极高。因为它不需要移动数据,只需要修改对应的节点。

特点:linkedList有一个first节点和last节点。作用是为了后面的操作提供入口。linkedList没有容量的概念。

记住,LinkedList没有容量这个概念。在内存允许的情况下,可以容纳内存允许的容量

vector:

这个容器实际跟ArrayList容器一样。不同的是它的所有方法都加了Synchronized关键词。

ArrayList和LinkedList的性能比较:

添加操作:ArrayList胜出,原因:虽然ArrayList在添加时会有数组复制操作,但是次数少。而LinkedList每次都要创建一个Node对象。1:2(3)

随机删除操作:ArrayList胜出。

随机插入操作:ArrayList胜出。

遍历:ArrayList胜出

如果是往容器的最开头插入,数据在3千万左右,LinkedList的效率比较快。其他操作为什么是ArrayList快主要原因还是因为LinkedList需要遍历链表。

ArrayList主要性能开销在数组复制上,LinkedList的性能开销主要在搜索Node节点和创建Node节点上。所以容器选择上一般选择ArrayList

HashMap:

异或^:当两个值之间相减是偶数,则输出该值,否则相加为奇数

&:可以暂时理解为两值之间取最小。负数不算。

默认容量是16。有一个静态匿名类Node(K,V,Hash。和一个Node(自己)),实现了Map.entry。

HashMap下有两个对象。一个Node<K,V>[]数组对象,一个Set<Map.Entry<K,V>>对象

每个Node中有一个Node,形成了单链表结构,主要是为了因为Key哈希算法后还是定位到相同的索引位置。

HashMap有三个匿名类KeySet、Values、entrySet、其实都是在操作HashMap中的Node数组。

put(K k, V v):首先对K进行hashCode运算得到唯一的Int。首先声明一个16容量的Node数组。

当得到的hash在Node[]中索引,如果该索引的值为null,就为其创建Node节点。如果不为null,则遍历Node节点,直到Node的next为null,再为其创建Node节点。当key相同,且hash相同。就修改其值。

HashMap如何确认key是否相同。首先判断Key的hash是否相同,如果相同,再判断Key的equals或者Key的内存地址是否相同。如果都一致,代表是相同的Key

remove():前面做的判断跟Put一样,也是判断Key是否是同一个,所在的索引是否一样,如果一样,遍历单链表,找到具体的Node对象,如果Node对象是第一个的话,就直接将Node节点设置为null。如果不是,将Node节点对应的上个节点的next设置为Node节点的next

get():找到key所在的Node[]数组的索引位置。然后遍历单链表,直到找到这个Node节点。

values():也是一样的到底,遍历Node[]数组,并且包含Node下的单链表

TreeMap:

是红黑树结构。有排序的功能

TreeMap下有Entry[K,V] root根节点。

Entry的结构如下:

K key;

V value;

Entry<K,V> left;

Entry<K,V> right;

Entry<K,V> parent;

boolean color = BLACK;

put(K key, V value):先进行二叉查找树(遍历该树,每个节点和传入的key比较,传入的值大走右,小于走左,相等时直接插入(修改),相等时直接返回)。找到传入Key的父节点后,创建传入key节点。根据两者之间的值选择将新创建的Entry节点插入在父节点的左或右。

为传入节点染红色。如果传入节点的父节点不是红色,就不进行操作。如果是红色,就进行染色和旋转。

记住,如果要进行旋转和染色,该树必定是三层以上。为什么呢?

旋转和染色的条件是 x.parent.color == RED。因为root是黑色,所以2层的时候是不需要进行旋转和染色的。

现在开始讲传入节点叫now节点

当now节点的父节点的父节点的左节点等于now节点的父节点时,也就是now节点在root的左树上:

获取now节点的父节点的父节点的右节点。

如果右节点为红色:

将now节点的父节点染成黑色。将右节点染成黑色。将now节点的父节点的父节点染成红色。将now的父节点的父节点赋值给now节点

如果右节点为黑色:

当now节点是父节点的右节点时,将父节点赋值给now节点,进行左旋。旋转后,将新的now的父节点染色成黑色,将新的now父节点的父节点染色成红色。进行右旋转。

当now节点是父节点的左节点时,将now的父节点染色成黑色,将now的父节点的父节点染色成红色,进行右旋转。

当now节点的父节点的父节点的左节点不等于now节点的父节点时,也就是now节点在root的右树上:

获取now节点的父节点的父节点的左节点。

如果左节点是红色:

将now节点的父节点染色为黑色。将左节点染色为黑色。将now的父节点的父节点染色为红色。将Now的父节点的父节点赋值给now节点。

如果左节点是黑色:

当now节点是父节点的左节点时,父节点赋值给now节点,进行右旋转。旋转后,将新的now的父节点染色成黑色,将新的now父节点的父节点染色成红色。进行左旋转。

当now节点是父节点的右节点时,将now的父节点染色成黑色,将now的父节点的父节点染色成红色,进行左旋转

get():都是通过遍历整个树,然后树节点跟传入的节点key比较,当传入的值大时走右,小时走左。相等时返回该entry。

remove():都是通过遍历整个树,然后树节点跟传入的节点key比较,当传入的值大时走右,小时走左。相等时删除该entry。

得出:root节点是黑色。当传入的entry对象的父节点为红色或者是要进行旋转的entry的父节点为红色时。并且都不为null且都不为root时,就要进行旋转和染色。

HashSet:

有个HashMap对象,所以也是一个Node[]数组和单链表完成。

它的增删查改都是在操作HashMap,所以没什么好讲的。它的遍历其实就是利用Map.keySet来完成

TreeSet:

有个TreeMap对象,所以也是一个红黑树

HashSet和TreeSet、HashMap和TreeMap的区别?

hashSet和HashMap能存Null、无排序。(原因是null能通过hash函数得到具体的数组的索引)

TreeSet和TreeMap不能存Null,,它要将key进行比较,而key有默认的比较器(comparator)。会出现null.方法操作,就会抛出异常,如果你重新实现比较器,那也可以存Null值。因为底层是红黑树,所以它是能排序的。

ConcurrentHashMap:

有两个被volatile修饰的Node(table)数组,Node(nextTable)数组

 

java的安全集合类,如果不是通过synchronized来实现同步机制,就是利用volatile和CAS(Unsafe类)来实现乐观锁。达到数据的原子性。

PriorityQueue:

底层是数组,是一个完全二叉树。公式有下,假如当前节点的索引为n。那么就有当前节点的父节点索引为(n-1)/2。当前节点的左节点索引为(n*2) + 1。当前节点的右节点索引为(n*2) + 1 + 1

add(E e):判断当前要传入的e跟e的父节点哪个大,假如当前e的值大,那么就将e和e的父节点替换。一直循环,直到e的值比e的父节点的值小为止(此时该树还没有排序好)。在添加阶段,保证了root节点的值是最小的。左右节点大小不一,但是父节点一定比子节点都小的特点。

poll():在这个阶段,先返回root节点,然后将root节点设置为最后一个节点的值,root的左右子节点比较大小,小的和root节点替换。一直循环,直到父节点比子节点都小为止。

总结:正是因为add和poll的配合,导致每次获取都是最小的值。

如果要实现最大堆的话,只需要往构造函数添加Comparator就可以了

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值