概述
-
面向对象语言对事物的提现是以对象的形式,为了方便对多个对象的操作,就要对对象进行储存。但JAVA当中的存储对象方面的数组存在弊端,针对此问题Java提供了另一种容器(集合),可以动态的把多个对象的引用放入容器。
-
Java数组的特点:
- 初始化长度确定
- 需要指定元素类型
- 对于添加、删除、插入数据等操作,非常不便,同时效率不高。
- 有序、可重复,对于无序、不重复的需求,无法满足。
-
集合优点:
- 解决数组的弊端
Collection 接口
-
描述: 单列集合,用来储存对象
-
子类(实现类):
-
List(接口): 有序的,可重复的数据
- ArrayList、LinkedList、Vector
-
Set(接口): 无序的,不可重复的数据
- HashSet、LinkedHashSet、TreeSet
-
常用方法
int size(); // 返回集合长度
boolean isEmpty(); // 判断集合是否为空
boolean contains(Object obj); // 当前集合是否包含obj
boolean containsAll(Collection<?> c); // 判断c中的所有元素是否都在当前集合中
Object[] toArray(); // 将集合转换为数组对象
boolean add(E e); // 向集合中添加新对象
boolean addAll(Collection<? extends E> c); // 添加另一个集合
void clear(); // 删除集合中所有的元素
boolean remove(Object o); // 从集合中删除指定的对象
boolean removeAll(Collection<?> c); // 从当前集合中删除与c集合相同的所有元素(差集)
boolean retainAll(Collection<?> c); // 从当前集合中找出找到c集合相同的元素(交集)
boolean equals(Object o); // 判断集合是否相等(如何有序的话,顺序也必须一致)
int hashCode(); // 得到当前对象的hashCode值
Iterator<E> iterator(); // 返回Iterator实例,用于集合遍历
注意事项
contains(Object obj)方法:
contains(Object obj)是否包含对象obj,判断方式取决于obj对象是否重写equals()方法,如果重写,则比较的为内容,如果没有重写,则比较的是地址值
集合当中如果需要遍历当中删除:iterator(迭代器)的remove方法。在删除之前需要先调用iterator.next()方法,将指针下移才能使用remove方法不然会抛异常
面试题
@Test
public void test3 () {
int[] a = {1,2,3,4,5,6,7,8,9};
for (int i = 0; i < a.length; i++) {
a[i] = 0;
}
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]); // {000000000}
}
}
@Test
public void test4() {
int[] a = {1,2,3,4,5,6,7,8,9};
for (int i : a) {
i = 1;
}
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]);{123456789}
}
}
for循环会改变原有的数组。
forEach则不会。
List接口
概述:有序的,可重复的数据。可用于替换原有的数组
经典面试题:
ArrayList、LinkedList、Vector三者的异同?
-
同: 三个类都是实现了List接口,存储数据的特点相同: 储存有序的、可重复的数据
-
异:
-
ArrayList: 线程不安全,执行效率高;底层使用Object[]存储
-
LinkedList: 对于频繁的插入、删除操作,使用此类的效率比ArrayList高;底层使用双向链表储存
-
Vector: 线程安全,执行效率低;底层使用Object[]存储
-
常用方法:
List接口继承自Collection,Collection的方法都有,这里就不重复介绍了
void add (int index,Object obj); // 在index位置插入元素 obj
boolean addAll(int index,Collection eles); //从index位置开始将eles中的所有元素添加进来
Object get(int index); // 获取指定index位置的元素
int indexOf(Object obj); // 返回obj在当前集合中首次出现的位置
int lastIndexOf(Object obj); // 返回ovj在当前集合中最后一次出现的位置
Object remove(int index); // 移除指定index位置的元素,并返此元素
Object set(int index,Object ele); // 设置指定index位置的元素为ele
List subList(int formIndex,int toIndex); // 返回从fromIndex到toIndex位置的子集合
ArrayList
底层使用Object[]存储
源码:
jdk7:
构造器:
// 初始化一个空数组,长度为10的Object[] elementData
public ArrayList () {
this (10);
}
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity:" + initialCapacity);
this.elementData = new Object[initialCapacity];
}
主要方法:
public boolean add (E e) {
ensureCapacotyInternal(size + 1); // 判断长度是否够用
elementData[size++] = e;
return true;
}
private void ensureCapacotyInternal (int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0) { // 长度比较,现有长度是否够用
grow(minCapacity)
}
}
private void grow (int minCapacity) {
int oldCapacity = elementData.length; // 记录本身的长度
int newCapacity = oldCapacity + (oldCapacity >> 1); //新的长度(默认情况下扩容为1.5倍)
if (newCapacity - minCapacity < 0)
// 如果长度还是不够,那么扩容长度直接为需求的长度
newCapacity = minCapacity
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 如果长度达到了预设的最大值
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity); // 复制
}
/**
* 如果长度大于预定最大值则返回整型数的最大值
**/
private static int hugeCapacity (int minCapacity) {
if (minCapacity < 0)
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ? Interger.MAX_VALUE : MAX_ARRAY_SIZE;
}
总结:
ArrayList list = new ArrayList();
底层创建默认长度为10的Object[]数组ElementData。
list.add(“test”);
如果本次的添加导致底层elementData数组容量不够,则扩容。
默认情况下,扩容为原来容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。
结论: 建议开发中如果能够确定长度,推荐使用带参的构造器可提高效率:
public ArrayList(int initialCapacity) {
}
jdk 8
- 构造器:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
初始化时,不再给长度只申明一个空的数组。
-
add方法:
public boolean add (E e) { ensureCapacotyInternal(size + 1); // 判断长度是否够用 elementData[size++] = e; return true; } private void ensureCapacotyInternal (int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA ) { minCapacity = Math.max (DEFAULT_CAPACITY,minCapacity); } ensureExplicitCapacity(minCapacity); } 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); //新的长度(默认情况下扩容为1.5倍) if (newCapacity - minCapacity < 0) // 如果长度还是不够,那么扩容长度直接为需求的长度 newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) // 如果长度达到了预设的最大值 newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); // 复制 }
总结
ArrayList list = new ArrayList();
底层并没有创建为十长度的Object[],elementData初始化为{}
list.add(“test”)
当一次调用add方法时,底层才创建了长度10的数组。
其余操作与7无异。
总结:
- jdk7中的ArrayList初始化类似于单例模式饿汉式,初始化则给长度。
- jdk8中的ArrayList初始化类似于单例模式中懒汉式,初始化并没有给定长度,而是当调用的添加时才会给定长度
面试题
@Test
public void test(){
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
updateList(list);
System.out.println(list);
}
public void updateList(List list) {
list.remove(2);
}
结果为:[1,2]
此题考查对于list.remove()方法,放入数字参数时,是按照下标还是按照值来删除。
在添加时List会自动装箱,但删除的时候直接输入数字却不会自动装箱,需要我们手动来进行。
LinkedList
LinkedList存储结构为链式,一个数据划分为三部分:
- 上一个的值 (如果是第一个则为null)
- 本数组的值 (即我们储存的值)
- 下一个的值
源码:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;
/**
* Pointer to first node.
*/
transient Node<E> first;
/**
* Pointer to last node.
*/
transient Node<E> last;
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) {
final Node<E> l = last; // 当前的最后一个元素
final Node<E> newNode = new Node<>(l, e, null); // 参数(上一个元素,需要添加的元素,下一个元素),由于为添加方法,所以最后一部分为null。
last = newNode; // 最后一个元素等于当前的元素
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
总结
LinkedList list = new LinkedList();
内部声明了Node类型的first和last属性,默认值为null
list.add(123); // 将123封装到Node中,创建了Node对象
Node定义为: 体现了双向列表的说法
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next; // 上一个
this.prev = prev; // 下一个
}
Vector
通过构造器创建时,创建了长度为10的数组。扩容长度为原来的2倍。
Set接口
特点:
-
无序:不等于随机性。存储数据在底层数据中的顺序并非按照数组索引的先后顺序,而是按照数据的哈希值决定。
-
不可重复:保证添加的元素按照equals()判断时,返回值不为true
子类:
-
HashSet:
- 线程不安全
- 初始化长度为16
- 可以存储null值
- 子类LinkedHashSet
方法与Collection接口一致
添加过程:
首先调用添加元素的hashCode方法得到hash值,通过某种算法计算出在HashSet底层数组中的存放位置。
判断数组此位置是否有元素,如果此位置上没有元素,则元素直接添加成功。
如果有其它元素(或以链表形式存在的多个元素),则比较元素与其他元素的hash值:
如果Hash值不相同,则直接添加
如果hash值相同,equals()方法,如果都为false,则添加成功,如果一个为true,则添加失败
注意: 向Set中添加的数据,需要重写equals与hashCode方法。
面试题
去重复
public static void main(String[] args){
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(4);
Set set = new HashSet();
set.addAll(list);
list = new ArrayList(set);
}
利用了set当中的不可重复性来达到目的,如果去除的是自定义类,那么要求自定义类当中重写hashCode与equals方法
@Test
public void test() {
HashSet set = new HashSet():
Person p1 = new Person(1001,"AA");
Person p2 = new Person(1002,"BB");
set.add(p1);
set.add(p2);
System.out.println(set); // [{1001,"AA"},{1002,"BB"}]
p1.name = "CC";
set.remove(p1);
System.out.println(p1); // [{1001,"CC"},{1002,"BB"}]
set.add(new Person(1001,"CC")); // [{1001,"CC"},{1002,"BB"},{1001,"CC"}]
set.add(new Person(1001,"AA")); // [{1001,"CC"},{1002,"BB"},{1001,"CC"},{1001,"AA"}]
}
解释
当p1.name修改为CC时,只是将p1.name修改为CC
当remove方法被执行时,那么set会重新计算hashCode值,而set集合根据对象的hashCode值来确定位置.
那么remove方法实际上寻找的对象为{10001,“CC”}这个对象算出来的hashCode值位置。而我们set集合当中现有的{10001,“CC”}的hashCode值为{1001,“AA”}算出来的地址值,所以会找不到这个地址值。
add(new Person(1001,“CC”);由于当前集合中的{10001,“CC”}位置是由{1001,“AA”}的hashCode值得到的,与{10001,“CC”}的hashCode值很大概率会不同,那么得到的位置也会不同,所以直接放入。
set.add(new Person(1001,“AA”)); 由于当前集合中的{10001,“CC”}位置是由{1001,“AA”}的hashCode值得到的,那么两个元素的hashCode值相同,调用equlas比较内容,显然是false,所以可以插入
-
LinkedHashSet
优点: 与LinkedList一致,对于频繁的插入、删除操作效率高于HashSet。底层采用双向链表结构。
TreeSet
特点:
-
可以按照添加对象的指定属性,进行排序
-
向TreeSet中添加的数据,要求是相同类的对象
-
TreeSet判断是否重复不再使用equals、hashCode方法,而是ComparaTo方法的返回值,如果返回0(两个对象某个值相同)那么则认为这个对象相同。
-
排序方式可以使用自然排序与定制排序
Map
特点:
-
双列数据,储存key-value对的数据
-
key不可重复,使用Set储存所有的key
-
value无序的、可重复,使用Collection储存所有的value
-
底层使用Entry对象存储key-value,Entry也是无序的、不可重复的
HashMap
特点:
- Map主要实现类
- 线程不安全,效率高
- 可以储存null的key与value
- 底层:
- 数组+链表 jdk7
- 数组+链表+红黑树 jdk8
- key要求所在的类要重写equals()和hashCode(),value所在类要重写equals()
主要方法:
V put(K key, V value); // 将指定key-value 添加到(或修改)当前map对象中
void putAll(Map<? extends K, ? extends V> m); // 将m中所有的key-value对,并返回value
V remove(Object key); // 删除指定key的key-value对,并返回value
void clear(); // 清空当前map中的所有数据
V get(Object key); // 获取指定key对应的value
boolean containsKey(Object key); // 是否包含指定的key
boolean containsValue(Object value); // 是否包含指定的value
Set<K> keySet(); // 返回所有key构成的Set集合
Collection<V> values(); // 返回所有value构成的 Collection集合
Set<Map.Entry<K, V>> entrySet(); // 返回所有key-value对构成的Set集合
面试题:
- HashMap的底层实现原理
- jdk7:
-
在实例化以后,底层创建了长度是16的一维数组Entry[] table
-
调用key1所在类的hashCode方法计算key1哈希值,此哈希值经过某种算法计算以后,得到Entry数组中的存放位置
- 如果此位置上的数据为空,此时的key1-value1添加成功
- 如果此位置上的数据不为空(此位置存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个的哈希值:
- 如果key1的哈希值与已经存在的哈希值都不相同,此时key1-value1添加成功
- 如果keu1的哈希值和已经存在的某一个数据的哈希值相同,继续比较:
- 调用key1所在类的equals()方法,比较:
- 如果equals返回false:则添加成功
- 如果equals返回true:使用value1替换相同key的value值
- 调用key1所在类的equals()方法,比较:
-
如超出临界值(且存放的位置为空时)默认扩容为原来的2倍,并将原有的数据复制
-
HashMap map = new HashMap();
map.put(key1,value1);
-
jdk8:
不同:
- new HashMap():底层没有创建一个长度为16的数组
- 底层的数组是: Node[],而非Entry[]
- 首次调用put()方法,底层创建长度为16的数组
- jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树
- 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度>64时,此时此索引位置上的所有数据改为使用红黑树存储
负载因子(加载因子)的大小,对HashMap有什么影响?
- 负载因子的大小决定了HashMap的数据密度。
- 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
- 负载因子越小,就越容易出发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。经常扩容也会影响性能,建议初始化预设大一点的空间。
- 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7-0.75,此时平均检索长度接近与常数
源码分析:
HashMap源码中的重要常量:
名称 | 含义 |
---|---|
DEFAULT_INITIAL_CAPACITY | HashMap的默认容量,16 |
MAXIMUM_CAPACITY | HashMap的最大支持容量,2^30 |
DEFAULT_LOAD_FACTOR | HashMap的默认加载因子,0.75 |
TREEIFY_THRESHOLD | Bucket中链表长度大于该默认值,转换为红黑树 |
UNTREEIFY_THRESHOLD | Bucket中红黑树储存的Node小于该默认值,转化为链表 |
MIN_TREEIFY_CAPACITY | 桶中的Node被树化时最小的hash表容量:64(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。) |
table | 存储元素的数组,总是2的n次幂 |
entrySet | 存储具体元素的集 |
size | HashMap中存储的键值对的数量 |
modCount | HashMap扩容和结构改变的次数 |
threshold | 扩容的临界值,=容量 * 填充因子:16 * 0.75 |
loadFactor | 填充因子 |
jdk7:
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The next size value at which to resize (capacity * load factor).
* @serial
*/
// If table == EMPTY_TABLE then this is the initial capacity at which the
// table will be created when inflated.
int threshold;
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)Math.min(capacity * loadFactor,MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
public V put(K key,V value) {
if (key == null)
return putForNullkey(value);
int hash = hash(key);
int i = indexFor(hash,table.length);
for(Entry[K,V] e = table[i];e != null; e = e.next){
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash,key,value,i);
return null;
}
jdk8:
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
public V put(K key,V value){
return putVal(hash(key),key,value,false,true);
}
static final int hash (Object key) {
int h ;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
}
-
HashMap和Hashtable的异同
-
CurrentHashMap与Hashtable的异同
LinkedHashMap
特点:
-
在遍历Map元素时,可以按照添加的顺序实现遍历
- 在原有的HashMap底层结构基础上,添加了一对指针,指向前一个与后一个
-
对于频繁的遍历操作,此类执行效率高于HashMap
-
底层实现原理
- LinkedHashMap底层使用的结构与HashMap相同,因为LinkedHashMap区别就在于:LinkedHashMap内部提供了Entry,替换HashMap中的Node.
TreeMap
特点:
- 按照添加的key-value对进行排序
- 按照key进行自然排序或定制排序
- 底层使用红黑树
HashTable
特点:
- 线程安全的,效率低
- 不能储存null的key和value
Properties
特点:
- 常用于处理配置文件
- key和value都是String类型
Collections
操作Collection和Map的工具类
面试题:
Collections与Collection的区别
- Collections是Collection和Map的工具类
- Collections是类,Collection是接口
常用方法:
public static void reverse(List<?> list) //反转list中元素的顺序
public static void shuffle(List<?> list) // 对list集合元素进行随机排序
public static <T extends Comparable<? super T>> void sort(List<T> list) // 根据元素的自然排序对指定list集合元素按升序排序
public static <T> void sort(List<T> list, Comparator<? super T> c) // 根据指定的Comparator产生的顺序对list集合进行定制排序
public static void swap(List<?> list, int i, int j) // 将指定list集合中的i处元素和j处元素进行交换
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) // 根据元素的自然顺序,返回给定集合中的最大元素
public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp) // 根据Comparator指定的顺序,返回给定集合中的最大值。
public static <T extends Object & Comparable<? super T>> T min(Collection<? extends T> coll) // 根据元素的自然顺序,返回给定集合中的最小元素
public static <T> T min(Collection<? extends T> coll, Comparator<? super T> comp) // 根据Comparator指定的顺序,返回给定集合中的最小值
public static int frequency(Collection<?> c, Object o) // 返回指定集合中指定元素的出现次数
public static <T> void copy(List<? super T> dest, List<? extends T> src) // 将src的内容复制到dest中
public static <T> boolean replaceAll(List<T> list, T oldVal, T newVal)
package com.pyw;
import org.junit.Test;
import java.lang.reflect.Array;
import java.util.*;
public class CollectionsTest {
@Test
public void test(){
List<Integer> list = new ArrayList();
list.add(123);
list.add(456);
list.add(789);
list.add(753);
list.add(498);
list.add(785);
list.add(642);
Collections.reverse(list);
// System.out.println(list); // [642, 785, 498, 753, 789, 456, 123]
// Collections.shuffle(list);
// System.out.println(list); // [456, 498, 642, 785, 123, 789, 753] 由于随机所以每次都会不一样
// Collections.sort(list);
// System.out.println(list); // [123, 456, 498, 642, 753, 785, 789] 由于类当中实现了Comparable接口默认从小到大
Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return -Integer.compare(o1,o2);
}
});
// System.out.println(list); // [789, 785, 753, 642, 498, 456, 123] 取反按照从大到小
List list2= new ArrayList<>();
list2 = Arrays.asList(new Object[list.size()]);
Collections.copy(list2,list);
System.out.println(list2); // [789, 785, 753, 642, 498, 456, 123]
}
}
Collections常用方法:同步控制
Collections.SynchronizedXXX(XX); // 返回一个线程安全的list或Map集合