集合、数组都是对多个数据进行存储操作的结构,简称Java容器。
说明:此时的存储,主要是指的是内存层面的存储,不涉及到持久化的存储。
一旦初始化后,其长度就确定了。
数组一旦定义好,其元素类型也就确定了。我们只能操作指定类型的数据。
String[] arr = new String[4];//数组类型和长度确定 String[] arr2 = new String[]{"123", "AA", "cc"};//数组类型和长度确定 int[] arr3; Object[] arr4;①一旦初始化后,其长度就不可修改
②数组中提供的方法非常有限,对于添加、删除、插入数据等操作,非常不便,同时效率不高。
③获取数组中实际元素的个数的需求,数组中没有现成的属性或方法可用。
④数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。
解决数组存储数据方面的弊端
/----Collection接口:单列集合,用来存储一个一个的对象
/----list接口:存储有序的、可重复的数据。 —>“动态的数组”
/----ArrayList、LinkedList、Vector
/----Set接口:存储无序的、不可重复的数据 —>高中的集合
/----HashSet、LinkedHashSet、TreeSet
add(Object obj)添加元素
addAll(Collection coll)将列表coll中的元素全部添加到当前列表
size()当前列表内的元素个数
isEmpty()判断当前列表是否为空
clear()清空当前列表
remove(Object obj) 删除元素
removeAll(Collection col)
removeIf
contains(Object obj)判断是否包含元素obj,返回boolean
containsAll(Collection coll)判断当前集合是否包含集合coll,返回boolean
retainAll(Collection coll),取当前集合和coll的交集,赋给当前集合,有返回值,返回值的类型为boolean
equals()判断两个集合是否相等
hasCode(),返回当前集合的哈希值
toArray(),列表转数组,返回一个Objcet类型的数组
iterator(),返回一个当前集合的迭代器
使用迭代器Iterator
Iterator iterator = coll.iterator(); //遍历集合 while(iterator.hasNext()){ System.out.println(iterator.next()); }使用foreach循环
//快捷键时iter Object[] objects = coll1.toArray(); for (Object o : objects) { System.out.println(o); }迭代器中remove()的使用
如果还没调用next()或在上一次调用next方法之后,已经调用了remove方法,再调用remove都会报IllegalStateException迭代器中定义的remove方法不同于Collection集合直接调用remove。 @Test public void test1{ Collection coll = new ArrayList(); coll.add(123); coll.add(456); coll.add(new Person("Jerry",20)); coll.add(new String("Tom")); coll.add(false); //删除集合中"Tom" Iterator iterator = coll.iterator(); while(iterator.hasNext()){ Object obj = iterator.next(); if("Tom".equals(obj)){ //调用iterator的remove() iterator.remove(); } } //遍历集合 iterator = coll.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); } }jdk 5.0 新特性 – 增强for循环(foreach循环)
遍历集合举例:
@Test public void test1(){ Collection coll = new ArrayList(); coll.add(123); coll.add(456); coll.add(new Person("Jerry",20)); coll.add(new String("Tom")); coll.add(false); //for(集合元素的类型 局部变量 : 集合对象) for(Object obj : coll){ System.out.println(obj); } }说明
内部仍然时调用了迭代器
遍历数组举例:
@Test public void test2(){ int[] arr = new int[]{1, 2, 3, 4, 5}; //for(数组元素的类型 局部变量:数组对象) for(int i : arr){ System.out.println(i); } }存储有序的、可重复的数据
增:add(Object obj)
删:remove(Object obj) / remove(int index)
改:set(int index, Object obj)
查:get(int index)
插:add(int index, Object obj)
长度:size()
遍历:①Iterator迭代器方式
②增强for循环
③普通for循环
增加:add(Object obj) | add(int index, Object obj) | addAll(Collection coll) | addAll(int index, Collection coll)
注意:add(int index, Object obj),index下标索引值可以=size;但是不能间隔一个去插入
List list = new ArrayList(); list.add("java"); list.add(123); list.add("123L"); list.add(3, "hadoop");//这可以 list.add(8, "spark");//IndexOutOfBoundsException删除:remove
修改
/----Collection接口:单列集合,用来存储一个一个的对象
/----List接口:存储有序的、可重复的数据。–>”动态数组“,替换原来的数组
/----ArrayList:作为List接口的主要实现类;线程不安全,效率高;底层使用Object[] elementData存储
/----LinkedList:对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储
jdk 7 的情况下
ArrayList list = new ArrayList();//底层创建了长度为10的Object[] 数组elementData;
list.add(123);//elementData[0] = new Integer(123);
…
list.add(11);//如果此次的添加导致底层的elementData数组容量不够,则扩容。
默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。结论:建议开发中使用带参数的构造器:
ArrayList list = new ArrayList(int capacity)
jdk 8 的情况下
ArrayList list = new ArrayList(); //底层Object[] elementData初始化为{}。并没有创建长度为10的数组。
list.add(123);//第一次调用add()时,底层才创建了长度为10的数组,并将数据123添加到elementData[0]
…
后续的添加和扩容操作与jdk 7 无异
结论:jdk 7的ArrayList的对象的创建相当于类似单例的饿汉式,而jdk 8的ArrayList的对象的创建相当于懒汉式,延迟了数组的创建,节省内存。
//ArrayList的源码分析 @Test public void testArrayList(){ List list = new ArrayList(); //增add(Object obj) list.add("java"); list.add(123); list.add(33); list.add(14423); list.add("123L"); list.add("123L"); list.add("123L"); list.add(5, "hadoop"); list.add("123L"); list.add("123L"); list.add("123L"); System.out.println(list); } /* ArrayList里的属性: this.elementData,是ArrayList的实例化对象的数组。 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//jdk1.8以后,默认初始的数组为空。在第一次添加的时候才扩容为10。 1. 初始化,new ArrayList(); public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;// } 2. 第一次添加的时候: public boolean add(E e) { ensureCapacityInternal(size + 1);//2.1,minCapacity = size + 1 //1 elementData[size++] = e; return true; } 2.1 private void ensureCapacityInternal(int minCapacity) { //第一次添加的时候elementData==DEFAULT...都是空数组 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //最小容量 = max(10, 1) = 10 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity);2.2 } 2.2 private void ensureExplicitCapacity(int minCapacity) { modCount++;//操作数++ //如果最小容量 > 数组长度 ---> 10 > 0 if (minCapacity - elementData.length > 0) grow(minCapacity);//2.3 扩容机制 } 2.3 private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0)//第一次添加的时候 newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0)//数组容量特别大的时候 newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity);//数组扩容 } */LinkedList list = new LinkedList();内部声明了Node类型的first和last属性,属性值为null。
List.add(123);//将123封装到Node中,创建了Node对象。
其中,Node定义为:体现了LinkedList的双向链表的说话 //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的源码分析 @Test public void testLinkedList(){ List list = new LinkedList(); //增add(Object obj) list.add("hadoop"); list.add(1024); list.add("spark"); System.out.println(list); } /* 1. new LinkedList(),实例化LinkedList对象 public LinkedList() {//空参构造器 } 1.1 LinkedList的属性 transient int size = 0; transient Node<E> first;//first和last都是Node类型的节点。LinkedList中只保存了头结点和尾节点的两个属性;还有size节点数量。 transient Node<E> last; 2. add()添加操作。 public boolean add(E e) { linkLast(e);//2.1 添加元素 return true; } 2.1 void linkLast(E e) { final Node<E> l = last;//定义一个辅助节点,保存last节点; final Node<E> newNode = new Node<>(l, e, null);//声明一个newNode节点。 last = newNode;//将链表中的last=newNode if (l == null)//第一次添加的时候l = null,first = newNode first = newNode; else//不是第一次添加的时候,l.next = newNode l.next = newNode; size++;//每添加一个size++ modCount++;//每添加一个操作数++ } */jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。
在扩容方面,默认扩容为原来的数组长度的2倍。
//Vector的源码分析 @Test public void testVector(){ List list = new Vector(); list.add("hadoop"); for (int i = 0; i < 9; i++) { list.add("i" + i); } list.add(123); System.out.println(list); } /* 源码: 1. new Vector(),创建Vector对象 public Vector() { this(10);//默认初始容量为10; } 2. add(),添加元素 public synchronized boolean add(E e) { modCount++;//操作数++ ensureCapacityHelper(elementCount + 1);// 2.1判断是否需要扩容 elementData[elementCount++] = e;//添加元素到数组 return true; } 2.1 private void ensureCapacityHelper(int minCapacity) { if (minCapacity - elementData.length > 0) grow(minCapacity);//扩容操作,第一次添加不需要扩容 } 3. 添加到第11个时,需要扩容,扩容为原容量的两倍 private void grow(int minCapacity) { int 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); } 4. 另外需要说明的是,Vector的大部分方法是synchronize的,所以线程安全。 */添加的对象,所在的类要重写equals()方法
[面试题]
面试题:ArrayList、LinkedList、Vector者的异同?同:三个类都是实现了List接口,存储数据的特点相同:存储序的、可重复的数据不同:见上(第3部分+第4部分)无序的、不可重复的元素。
以HashSet为例说明:
无序性:不等于随机性。存储的数据在底层数组中并非照数据索引的顺序添加,而是根据数据的哈希值决定的。不可重复性:保证添加的元素照equals()判断时,不能返回true。即相同的元素只能添加一个。我们向HashSet中添加元素a,首先会调用元素a所在类的hashCode()方法,计算元素a的哈希值。
此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为索引位置)
判断数组位置上是否已经有元素:
3.1 如果此位置上没有其他元素,则元素a添加成功。---->情况1
3.2 如果此位置上有其他元素b(或以链表的形式存在多个元素),则比较元素a与元素b的hash值:
3.2.1 如果hash值不相同,则元素a添加成功。---->情况2
3.2.2 如果hash值相同,进而需要调用元素a所在类的equals()方法:
3.2.2.1 equals()返回true,元素a添加失败
3.2.2.2 equals()返回false,元素a添加成功。---->情况3
对于jdk 7 和jdk 8 添加成功的情况2和情况3而言:
元素a与已经存在指定索引位置上的数据以链表的方式存储
jdk7 : 元素a放到数组中,指向原来的元素。
jdk8: 原来的元素在数组中,指向元素a
总结:七上八下
HashSet底层:数组+链表的结构
Set接口中没有额外定义新的方法,使用的都是Collection中声明的方法。
/----Collection接口:单列集合,用来存储一个一个的对象
/----Set接口:存储无序的、不可重复的数据—>高中讲的集合
/----HashSet:作为Set接口的主要实现类;线程不安全;可以存储null值;
/----LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据。
/----TreeSet:可以按照添加对象的指定属性,进行排序。
HashSet/LinkedHashSet
要求:向Set(主要指:HashSet、LinkedHashSet)中添加的数据,其所在的类一定要重写hashCode()和equals()
要求:重写的hashCode()和equals()尽可能保持一直;相等的对象必须具有相等的散列码
重写两个方法的小技巧:对象中用作equals()方法比较的Field,都应该用来计算hashCode值。TreeSet:
自然排序中,比较两个对象是否相同的标准为:compareTo()返回0,不再是equals()定制排序中,比较两个对象是否相同的标准为:compare()返回0,不再是equals()Map - 双列集合框架
/----Map:双列数据,存储key-value对的数据。—> 类似高中的函数:y = f(x)
/----HashMap:作为Map的主要实现类:线程不安全的,效率高;可以存储null的key和value
HashMap底层使用数组+链表(jdk7及以前);数组+链表+红黑树(jdk8);
/----LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。
原因:在原来的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,此类执行效率高于HashMap。
/----TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序。
TreeMap底层使用红黑树结构
/----Hashtable:作为古老的实现类:线程安全的,效率低,不能存储null的key和value
/----Properties:常用来处理匹配文件。key和value都是String类型。
【面试题】
HashMap的底层实现原理?HashMap和Hashtable的异同?CurrentHashMap与Hashtable的异同?(暂时不讲)Map中的key:无序的、不可重复的,使用Set存储的key
key所在的类要重写equals()和hashCode() (以HashMap为例)
Map中的value:无序的、可重复的,使用Collection存储value
value所在的类要重写equals()
Map中的entry:无序的、可重复的,使用Set存储entry
一个键值对:key-value 构成一个Entry对象
添加:put(Object key, Object value)
putAll(Map map)
删除:remove(Object key)
remove(Key k, Value v)
修改:replace(Key k, newValue)
replace(Key k, oldValue, newValue)
查询:get(Object key)
containsKey(Key k)
containsValue(Value v)
遍历:keySet() —> 遍历key值,使用iterator或foreach
values() —> 遍历value值,使用iterator或foreach
entrySet() —> 遍历entry
HashMap map = new HashMap():
在实例化以后,底层创建了长度是**16**的一维数组Entery[] table
…可能执行过多次put…
map.put(key1, value1);
首先调用key1所在类的hashCode()计算key1的哈希值,此时经过某种算法计算后,得到在Entry数组中的存放位置。
2.1 如果此位置上的数据为空,此时的key1-value1添加成功。 —> 情况1
2.2 如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据)(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
2.2.1 如果key1的哈希值与已经存在的数据的哈希值不都相同,此时key1-value1添加成功。 —> 情况2
2.2.2 如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法,比较:
2.2.2.1 如果equals()返回false:此时key1-value1添加成功。
2.2.2.2 如果equals()返回true:使用value1替换value2
补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。在不断的添加过程中,会涉及到扩容的问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量16的两倍,将原来的数据复制过来。DEFAULT_INITIAL_CAPACITY:HashMap的默认容量,16
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子,0.75
threshold:扩容的临界值=容量×填充因子,16×75 => 12
TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树,8
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64
LinkedHashMap底层使用的结构与HashMap相同,因为LinkedHashMap继承于HashMap。
区别在于:LinkedHashMap内部提供了Entry,替换HashMap中的Node。