一.MAP
赋值:
HashMap<String,Object> hm = new HashMap(); HashMap<String,Object> hmCopy = new HashMap(); hm.put("123", 123); System.out.println(hm.get("123")); hmCopy = hm; hmCopy.remove("123"); System.out.println(hm.get("123")); 输出结果:123 null
注:如果再次put相同的键名,则map里的value会被覆盖
//map拷贝putAll方法: HashMap<String,Object> hm = new HashMap(); HashMap<String,Object> hmCopy = new HashMap(); hm.put("123", 123); System.out.println(hm.get("123")); hmCopy.putAll(hm); hmCopy.remove("123"); System.out.println(hm.get("123")); 输出结果:123 123
map对象深拷贝:
List<Integer> list = new ArrayList<Integer>(); list.add(100); list.add(200); HashMap<String,Object> map = new HashMap<String,Object>(); map.put("basic", 100);//放基本类型数据 map.put("list", list);//放对象 HashMap<String,Object> mapNew = new HashMap<String,Object>(); mapNew.putAll(map); System.out.println("----数据展示-----"); System.out.println(map); System.out.println(mapNew); System.out.println("----更改基本类型数据-----"); map.put("basic", 200); System.out.println(map); System.out.println(mapNew); System.out.println("----更改引用类型数据-----"); list.add(300); System.out.println(map); System.out.println(mapNew); System.out.println("----使用序列化进行深拷贝-----"); mapNew = CloneUtils.clone(map); list.add(400); System.out.println(map); System.out.println(mapNew); 输出结果: ----数据展示----- {basic=100, list=[100, 200]} {basic=100, list=[100, 200]} ----更改基本类型数据----- {basic=200, list=[100, 200]} {basic=100, list=[100, 200]} ----更改引用类型数据----- {basic=200, list=[100, 200, 300]} {basic=100, list=[100, 200, 300]} ----使用序列化进行深拷贝----- {basic=200, list=[100, 200, 300, 400]} {list=[100, 200, 300], basic=200}
最上面的两条是原始数据,使用了putAll方法拷贝了一个新的mapNew对象,
中间两条,是修改map对象的基本数据类型的时候,并没有影响到mapNew对象。
但是看倒数第二组,更改引用数据类型的时候,发现mapNew的值也变化了,所以putAll并没有对map产生深拷贝。
最后面是使用序列化的方式,发现,更改引用类型的数据的时候,mapNew对象并没有发生变化,所以产生了深拷贝。
上述的工具类,可以实现对象的深拷贝,不仅限于HashMap,前提是实现了Serlizeable接口。
//附克隆方法: public static <T extends Serializable> T clone(T obj) { T cloneObj = null; try { // 写入字节流 ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream obs = new ObjectOutputStream(out); obs.writeObject(obj); obs.close(); // 分配内存,写入原始对象,生成新对象 ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray()); ObjectInputStream ois = new ObjectInputStream(ios); // 返回生成的新对象 cloneObj = (T) ois.readObject(); ois.close(); } catch (Exception e) { e.printStackTrace(); } return cloneObj; }
二.linkedList
1、简介
LinkedList类声明如下:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
可以发现 LinkedList继承了 AbstractSequentialList抽象类,而不是像 ArrayList和 Vector那样实现 AbstractList,实际上,java类库中只有 LinkedList继承了这个抽象类,正如其名,它提供了对序列的连续访问的抽象:
LinkedList的底层是 Deque双向链表,实现了 Deque接口,而 Deque接口继承于 Queue接口,因此,在java中,如果要实现队列,一般都使用 LinkedList来实现。
Node结点
Node节点 一共有三个属性:item代表节点值,prev代表节点的前一个节点,next代表节点的后一个节点。
LinkedList中关于链表结点的定义如下:
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; } }
每个结点都有一个前驱和后继结点,并且在 LinkedList中也定义了两个变量分别指向链表中的第一个和最后一个结点:
transient Node<E> first; transient Node<E> last;
2、添加(add)操作
public boolean add(E e) { linkLast(e); return true; } void linkLast(E e) { final Node<E> l = last;//链表最后一个节点 final Node<E> newNode = new Node<>(l, e, null);//建立节点对象,前一个(perv属性)是last,后一个(next属性)是null,中间item数据是输入的参数e last = newNode; if (l == null) first = newNode; //如果最后一个节点 为null表示链表为空,则添加数据时将新加的数据作为第一个节点 else l.next = newNode; //如果链表不为空则将最后一个链表的next属性指向新添加的节点 size++; //链表长度自增1 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; } }
总结:
LInkedList添加操作时每个新添加的对象都会被放到新建的Node对象中,Node对象中包含加入的对象和前后指针属性(pred和next,pred和next其实都是Node对象,直接存放的就是当前对象的前一个节点对象和后一个节点对象,最后一个及节点的next值为null),然后将原来最后一个last节点的next指针指向新加入的对象,即可
addAll操作:
public boolean addAll(int index, Collection<? extends E> c) { checkPositionIndex(index);//判断index是否越界,越界则抛出异常 Object[] a = c.toArray(); int numNew = a.length;//要插入的集合的长度 if (numNew == 0) return false; Node<E> pred, succ;//声明pred和succ两个Node对象,用于标识要插入元素的前一个节点和最后一个节点 if (index == size) { //如果size等于原数组长度则表示在结尾添加 succ = null; pred = last; } else { succ = node(index);//index位置上的Node对象 pred = succ.prev; } for (Object o : a) { //遍历要插入的集合 @SuppressWarnings("unchecked") E e = (E) o; Node<E> newNode = new Node<>(pred, e, null); if (pred == null) first = newNode; //如果要插入的位置的前一个节点为null表示是第一个节点,则直接将newNode赋给第一个节点 else pred.next = newNode; //将要插入的集合元素节点对象赋给此位置原节点对象的前一个对象的后一个,即更改前一个节点对象的next指针指到新插入的节点上 pred = newNode;//更改指向后将新节点对象赋给pred作为下次循环中新插入节点的前一个对象节点,依次循环 } //此时pred代表集合元素的插入完后的最后一个节点对象 if (succ == null) { //结尾添加的话在添加完集合元素后将最后一个集合的节点对象pred作为last last = pred; } else { pred.next = succ;//将集合元素的最后一个节点对象的next指针指向原index位置上的Node对象 succ.prev = pred;//将原index位置上的pred指针对象指向集合的最后一个对象 } size += numNew; modCount++; return true; } Node<E> node(int index) { if (index < (size >> 1)) { //判断index是否小于size的一半,如果小于则从头遍历节点,否则从结尾遍历节点 Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; //从first第一个节点开始,依次将后一个节点赋给x return x; //返回index位置上的Node对象,下同理 } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
总结:
LinkedList在某个位置插入元素是通过将原位置节点的前一个节点的后一个指针指向新插入的元素,然后将原位置的节点的前一个指针指向新元素来实现的,相当于插入元素后后面的元素后移了,但是不是像ArrayList那样将所有插入位置后面的元素都后移,此处只是改变其前后节点的指向
3、删除操作
public E remove(int index) { checkElementIndex(index); return unlink(node(index)); } E unlink(Node<E> x) { final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; if (prev == null) { first = next; } else { prev.next = next;//将传入的节点的下一个节点对象赋给其之前前一个节点的下一个节点对象,即将传入的节点对象跳过 x.prev = null;//将传入对象的前一个节点对象置空使其前一个指针不指向任何元素 } if (next == null) { last = prev;//如果next为null表示是最后一个元素,直接将pred节点赋给last } else { next.prev = prev; x.next = null;//将传入节点的后一个节点置空,使其后一个节点指针不指向任何元素 } x.item = null;//将传入的节点对象上的对象置空,也就是整个Node对象中的属性都为空了,后面会被GC回收 size--; modCount++; return element; }
总结:
LinkedList删除操作是通过将index位置上的前一个节点的next执行index位置的后一个节点,同时将后一个节点的pred指向前一个节点,然后将index位置上的Node节点对象属性都置空来实现的,置空后的Node对象会被GC垃圾回收期回收掉。
4、修改操作
public E set(int index, E element) { checkElementIndex(index);//检查索引越界情况 Node<E> x = node(index);//获取index位置上的节点对象 E oldVal = x.item; x.item = element;//将新插入的元素直接赋给此位置上节点对象的item属性即可 return oldVal; }
总结:
LinkedList中的实际数据保存在节点对象的item属性中
5、查询操作
public E get(int index) { checkElementIndex(index);//检查索引越界情况 return node(index).item;//返回该index位置上的节点对象的item属性,即该位置上的实际值 }
总结:
插叙操作是通过node方法实现的,而node方法是通过先判断index位置是在整个链表的前一半还是后一半来遍历前一半或者后一半元素来获取index位置上的元素
三.ArrayList
add操作:
private transient Object[] elementData; private static final int DEFAULT_CAPACITY = 10; public ArrayList() { super(); this.elementData = EMPTY_ELEMENTDATA; } public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; } public boolean add(E e) { ensureCapacityInternal(size + 1); // 添加之前检查数组容量,size+1表示接下来要有的容量 elementData[size++] = e;//size初始默认值是0,每添加一个元素自增1,故size表示实际的数组长度 return true; } private void ensureCapacityInternal(int minCapacity) { if (elementData == EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);//如果elementData 为空集合,即则将传入的minCapacity和默认值10比较,选出较大的 } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++; if (minCapacity - elementData.length > 0) //新建对象时如果指定大小则elementData.length为指定的大小,否则elementData.length=0;此处minCapacity 表示需要的数组长度,elementData.length表示扩容后的数组长度,如果是的数组长度 grow(minCapacity);//扩容方法 } private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1);//>> : 右移运算符,num >> 1,相当于num除以2,此处扩容按照原容量大小的一般扩容 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity);//将原有元素拷贝到扩容后的新数组中 }
总结:elementData 是ArrayList的中间数组,是实际数据的存放数组,size是数组实际的长度,elementData.length都是大于或者等于size的。
声明ArrayList数组时如果指定长度后向数组中添加元素,如果长度超出了ArrayList中间数组的长度(elementData.length)则会增加数组容量(新建一个更长的数组),增加原长度的一半容量,然后将原数据拷贝到新数组中
如果声明ArrayList数组时没有指定长度,则默认是空数组,在添加元素的时候,判断如果当前集合(中间数组)为空,则将默认长度10赋给数组,即添加第一个元素后的数组容量实际是10
删除(remove)操作:
remove(int index)删除指定位置上的元素
public E remove(int index) { rangeCheck(index);//检查索引是否越界 modCount++; E oldValue = elementData(index);//要移除位置上的元素 int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index,numMoved);//从指定源数组中复制一个数组,复制从指定的位置开始,到目标数组的指定位置结束。即将elementData数组 index+1位置上及后面的元素都复制到elementData中,从index位置接收复制元素,接收numMoved个元素 elementData[--size] = null; // clear to let GC do its work 即将最后一个元素置null回收 return oldValue;//返回被移除的元素 }
总结:删除指定位置上的元素是将该数组中该位置以后的元素都往前移动一位,最后一位置null被回收
remove(Object o)删除此列表中首次出现的指定元素
public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } //此方法和remove类似 private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work }
总结:移除时先判断是否是null,如果是要单独操作,因为null没有equals方法,通过逐个比较元素来判断要溢出的元素的位置,再通过类似于remove的方法移除元素
修改/设置(set)操作
set(int index, E element)用指定的元素替代此列表中指定位置上的元素。 public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; }
总结:直接将指定位置上的元素赋给要赋的值
查询/获取(get)操作:
public E get(int index) { rangeCheck(index); return elementData(index); }
总结:直接返回指定位置上的数组元素
LinkedList和ArrayList的大致区别:
1、ArrayList继承于 AbstractList, LinkedList继承于 AbstractSequentialList;
2、ArrayList基于数组, LinkedList基于双向链表,对于随机访问, ArrayList比较占优势,LinkedList插入、删除元素比较快,如果只要调整指针的指向那么时间复杂度是O(1),但是如果针对特定位置需要遍历时,时间复杂度是O(n),也就是LinkedList在随机访问元素的话比较慢;
3、LinkedList没有实现自己的 Iterator,但是有 ListIterator和 DescendingIterator;
4、LinkedList需要更多的内存,因为 ArrayList的每个索引的位置是实际的数据,而 LinkedList中的每个节点中存储的是实际的数据和前后节点的位置;
5、ArrayList 和 LinkedList都是非同步的集合。
6、和ArrayList一样,LinkedList也是非线程安全的,只有在单线程下才可以使用。为了防止非同步访问,可以采用如下方式创建LinkedList:List list= Collections.synchronizedList(new LinkedList());
7、LinkedList基于双向链表实现,元素都可以为null。