集合 - - Collection接口(List接口、Set接口)
导读
- 集合,进行多对象的统一存储,更贴近Java的面向对象的语法框架,根据不同应用场景的需求,对集合进行了三种分类:List(动态数组)、Set(高中的”集合“)、Map(高中的”函数“),并提供了增删改查、数组集合互转、遍历等公用方法
- List接口:主要是ArrayList、LinkedList两个实现类,并掌握其底层添加数据的原理,以及针对List集合中数据特有的有序性,提供了针对索引值操作操作的系列方法。
- Set接口:主要是HashSet、LinkedSet两个实现类,其底层添加数据原理与HashMap有关,需要参HashMap的源码分析,针对Set集合中数据的无序、不可重复性,所有的对象性数据的对应类必须重写equals方法和hashCode方法
引用
-
集合(Collection)是方便对多个对象的统一存储,以前学过的储存结构式是数组(Array)
① 共同点:都是对多个数据进行存储的结构,简称:Java容器
注 :此处的存储是指内存层面的存储,不涉及到永久化存储
② 数组的特点
一旦定义,其长度就确定了
一旦定义,其元素类型就确定了③ 数组的弊端
初始化后,长度不可改变
提供的方法有限,不便于增删,效率不高
获取数组中实际元素个数的需求,数组没有现成的属性或方法使用
数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,无法满足,故引进集合④ 集合的优势
可以动态的存储数量不等、类型不同的多个对象,还可以保存具有映射关系的关联数组
封装一个类,类中提供方法,再用这个类的对象去装数据,去调用方法,呈现其很有特色的一些需求(增删改查),这就是对集合框架的一个很正常的需求 -
集合框架
什么容器干什么事,故集合中会有各种不同的容器类型(接口类型)
① Collection接口:单列集合,用来存储一个一个的对象
List:存储有序、可重复的数据 - - - > " 动态 "数组
| - - - ArrayList、LinkedList、Vector
Set: 存储无序、不可重复的数据 - - - > 高中讲的“ 集合 ”
| - - - HashSet、LinkedHashSet、TreeSet
② Map接口:双列集合,用来保存一对(具有映射关系“ key - value 对”) 一对的数据 - - - > 高中函数:y = f(x)
| - - - HashMap、LinkedHashMap
| - - -TreeMap
| - - -Hashtable、Properties
-
现实应用(JSON)
一、Collection接口中的API
- List接口和Set接口都实现了Collection接口,所以Collection接口中的方法就会通用
- 向Collection接口实现类的对象中添加数据obj时,要求obj所在类要重写equals( ) 方法
- jdk8新特性StreamAPI对于集合数据的操作 详见 Java8新特性 - - Stream类
-
add():为集合添加元素
@Test public void test(){ Collection coll = new ArrayList(); // 1.添加字符串 coll.add("AA"); coll.add(new String("Tom")); // 2.添加基本类型数据 coll.add(123); coll.add(false); // 3.添加自定义类:Person类重写了toString和equals方法 coll.add(new Person("Jerry",20)); // 输出为集合形式[AA, Tom, 123, false, {Jerry,20}] System.out.println(coll); }
-
size():返回该集合中的元素个数
// 5 System.out.println(coll.size());
-
addAll(Collection c):将集合c中的元素全部添加到调用集合中
// 1.创建集合coll2 collection coll2 = new ArrayList(); coll2.add("BB"); coll2.add(123); // 2.将集合coll2添加到集合coll中 coll.addAll(coll2); // 输出结果为:[AA, 123, Tom, false, {Jerry,20},BB, 123] System.out.println(coll);
-
clear():清空集合中的元素,为[ ],但不是null
// 清空集合中的元素,输出为:[] coll.clear(); System.out.println(coll);
-
contains(Object obj) 判断集合中是否含有obj
// 判断集合中是否包含某元素,判断结果为:true boolean contain = coll.contains(123); System.out.println(contain);
对于自定义类,如果没有重写equals()方法,则 contains方法返回 false
/* 1.输出结果为true, 因为Person类中重写了equals方法 2.contain方法会在调用obj对象时,判断其所在类是否重写了equals方法 */ boolean contain1 = coll.contains(new Person("Jerry",20)); System.out.println(contain1);
-
containsAll(Collection c):coll2集合中的元素是否都包含在coll集合中
// 1.新建集合coll3 Collection coll3 = Arrays.asList(123,"AA"); // 2.判断集合coll中是否包含集合coll3的所有元素,输出结果为true System.out.println(coll.containsAll(coll2));
-
remove(Object o) 移除集合中的元素
// 将元素123从集合coll中移除 coll.remove(123); // 移除元素后的集合coll:[AA, Tom, false, {Jerry,20}] System.out.println(coll);
-
removeAll(Collection c) 移除集合中包含有集合 c 中的元素,差集操作
// 从集合coll中移除集合coll3中的所有元素 coll.removeAll(coll3); // 移除后的集合coll:[Tom, false, {Jerry,20}] System.out.println(coll);
-
retainAll(Collection c) 交集:将交集运算的结果返回
// 新建集合coll4 Collection coll4 = Arrays.asList("123",new Person("Jerry",20)); // 从集合coll中找到与集合coll4共有的元素,并移除其它元素 coll.retainAll(coll4); // 交集运算后的coll只包含一个共有元素:[{Jerry,20}] System.out.println(coll);
-
equals(Collection c):比较两个集合是否相等,元素相同并且顺序相同
// 新建集合 Collection coll5 = Arrays.asList("AA",123,new String("Tom"),false,new Person("Jerry",20)); // false,此时的coll只剩一个元素,如果是原先状态,则输出true System.out.println(coll.equals(coll5));
-
hashCode() 返回该集合的哈希值
// 1031980562 System.out.println(coll.hashCode());
-
toArray() 集合 - - > 数组
@Test public void test2(){ // 1.新建集合coll6 Collection coll = new ArrayList(); coll.add("AA"); coll.add(123); coll.add(new String("Tom")); coll.add(false); coll.add(new Person("Jerry",20)); // 2.将集合coll6转变为数组格式 Object[] arr = coll6.toArray(); // 将数组arr用增强for循环遍历输出:AA 123 Tom false {Jerry,20} for (Object o : arr){ System.out.println(o); }
-
Arrays.asList() 数组 - - > 集合
// 1.新建int型数组(基本数据类型数组) int[] nums1 = new int[]{123, 345}; // 1.1 将数组转换为集合 List list1 = Arrays.asList(nums1); // 1.2此中的new int[]{123, 345}相当于是集合中一个元素,输出为地址值[[I@3d82c5f3] System.out.println(list1); // 2.新建Integer型数组(包装类数据型数组) Integer[] nums2 = new Integer[]{123, 345}; // 1.1 将数组转换为集合 List list2 = Arrays.asList(nums2); // 1.2 输出结果为[123,456] System.out.println(list2); // 3.新建String型数组 String[] nums3 = new String[]{"123"," 345"}; // 1.1 将数组转换为集合 List list3 = Arrays.asList(nums3); // 1.2 输出结果为[123,456] System.out.println(list3);
-
基于Iterator接口的集合遍历
-
① 迭代器:提供一种方法访问一个容器(container)对象中各个元素,而又不需要暴露该对象的细节。
-
② 迭代器模式,为容器而生
-
③ Iterator对象称为迭代器,设计模式的一种,主要用于遍历Collection集合中的元素
-
④ Collection接口继承了java.lang.Iterable接口,该接口中有一个**iterator()**方法,用以返回一个实现了Iterator接口的对象
-
⑤ Iterator仅用于遍历集合,Iterator本身并不提供承装对象的能力。如果需要创建Itetator对象,则必须有一个被迭代的集合
-
⑥ 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前
-
⑦ 常用方法
thod summary hasNext() 判断集合的iterator对象中的是否还有元素 next() 返回集合中的下一个元素 remove() 可以在遍历集合的过程中,删除某些符合要求的元素 -
⑧代码1【hasNext()、next()】
@Test public void iteratorTest(){ // 1.创建集合coll Collection coll = new ArrayList(); coll.add("AA"); coll.add(123); coll.add(new String("Tom")); coll.add(false); coll.add(new Person("Jerry",20)); // 2.用iterator方法返回一个Iteretor对象 Iterator iterator = coll.iterator(); // 3.调用hasNext方法判断该集合中是否有元素 while (iterator.hasNext()){ // 4.调用next方法返回集合的下一个元素 System.out.println(iterator.next()); } }
-
代码2:【remove()】
@Test public void iteratorTest1(){ Collection coll = new ArrayList(); coll.add("AA"); coll.add(123); coll.add(new String("Tom")); coll.add(false); coll.add(new Person("Jerry",20)); Iterator iterator1 = coll.iterator(); while (iterator1.hasNext()){ // 1.在遍历元素的过程中,将符合条件的元素删除 Object obj = iterator1.next(); if ("Tom".equals(obj)){ iterator1.remove(); } } // 2.再次遍历集合,要新建Iterator对象,将其游标重新定位到集合的第一元素之前 Iterator iterator2 = coll.iterator(); // 输出:AA 123 false {Jerry,20} while (iterator2.hasNext()){ System.out.print(iterator2.next() + "\t"); } }
-
-
foreach循环遍历(增强for循环)
内部调用的还是Interator迭代器(可用Debug查看)
-
代码示例
@Test public void foreachTest(){ // 新建集合coll Collection coll = new ArrayList(); coll.add("AA"); coll.add(123); coll.add(new String("Tom")); coll.add(false); coll.add(new Person("Jerry",20)); // 1.增强for循环对集合遍历 for (Object obj : coll){ System.out.println(obj); } // 2.增强for循环对普通数组遍历 int[] arr = new int[]{1,2,3,4,5}; for (int i : arr){ System.out.println(i); } }
-
关于foreach的笔试题
// 笔试题 @Test public void test1() { // 新建数组str String[] str = new String[]{"MM","MM","MM"}; // 1.用增强for循环为数组中元素赋值,赋值的是变量 s ,所以原数组值不会变 for (String s : str){ s = "GG"; } for (String t: str){ // 输出结果为:"MM","MM","MM" System.out.println(t); } // 2.用普通for循环为数组中元素赋值,赋值的就是数组元素,所以值会变 for (int i = 0; i < str.length; i++) { str[i] = "GG"; } for (String r : str){ // 输出为:"GG","GG","GG" System.out.println(r); } }
-
(一) List接口
-
特点:
① 存储有序、可重复的数据
② “动态”数组,可动态调节长度,替换原有的数组 -
接口中的实现类
① ArrayList: 作为List接口的主要实现类,线程不安全、效率高;底层使用Object[ ] elementData存储
② LinkedList:对于频繁的插入、删除操作,效率高;底层使用双向链表存储
③ Vector: 是List接口的旧实现类,线程安全,效率低;底层使用Object[ ] elementData存储 -
ArrayList类的源码分析
-
jdk7的情况下:底层创建了长度是10的Object数组
// ArrayList中的源码 // 1.空参构造器,底层创建了长度是10的Object数组:Object[] elementData public ArrayList() { this = (10); } // 2.add方法 public boolean add(E e) { // size:指elementData数组中已经添加的元素的个数 ensureCapacityInternal(size + 1); // transient Object[] elementData;是一个已经定义的Object空数组属性 elementData[size++] = e; return true; } // 3.判断是否要扩容 private void ensureExplicitCapacity(int minCapacity) { modCount++; // if (minCapacity - elementData.length > 0) grow(minCapacity); } // 4.扩容方法 private void grow(int minCapacity) { // elementData数组中元素的个数作为旧的容量 int oldCapacity = elementData.length; // 新的容量为旧的容量的1.5倍。>>1表示除以2 int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果新容量仍然不满足最小容量需求,则新容量变为最小容量需求 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 如果新容量比类内定义的常量属性MAX_ARRAY_SIZE还大 if (newCapacity - MAX_ARRAY_SIZE > 0) // 则把整型的最大值赋给新容量。(如果超过整型最大值,就会抛异常) newCapacity = hugeCapacity(minCapacity); // 把原先elementData数组拷贝到扩容后的新的elementData数组中 elementData = Arrays.copyOf(elementData, newCapacity); } // ArrayList空参构造器, ArrayList list = new ArrayList(); // elementData[0] = new Integer(123) list.add(123); ··· /* 如果此次的添加导致底层elementData数组容量不够,则扩容。 默认情况下,扩容为原来容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中 */ list.add(11); // 结论:建议开发中使用带参的构造器,减少因不断扩容带来的硬件开销 ArrayList list = new ArrayList(int capacity)
-
jdk8中的变化:底层创建了空的Object数组,首次调用add方法时才会扩容为10
// ArrayList中的源码 // 1.空参构造器,底层创建了空的Object数组:Object[] elementData public ArrayList() { // static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } // 2.add方法 public boolean add(E e) { // size:指elementData数组中已经添加的元素的个数 ensureCapacityInternal(size + 1); // transient Object[] elementData;是一个已经定义的Object空数组属性 elementData[size++] = e; return true; } // 3.确定是否要扩容。 private void ensureCapacityInternal(int minCapacity) { // minCapacity = size + 1 ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } // 4.初次添加元素,就给elementData设置容量为10 private static int calculateCapacity(Object[] elementData, int minCapacity) { // 如果elementData数组为空,就返回默认容量10和添加元素个数+1之间的较大值 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // DEFAULT_CAPACITY = 10; return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } // 5.判断是否要继续扩容 private void ensureExplicitCapacity(int minCapacity) { modCount++; if (minCapacity - elementData.length > 0) grow(minCapacity); } // 6.扩容方法 private void grow(int minCapacity) { // elementData数组中元素的个数作为旧的容量 int oldCapacity = elementData.length; // 新的容量为旧的容量的1.5倍。>>1表示除以2 int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果新容量仍然不满足最小容量需求,则新容量变为最小容量需求 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 如果新容量比类内定义的常量属性MAX_ARRAY_SIZE还大 if (newCapacity - MAX_ARRAY_SIZE > 0) // 则把整型的最大值赋给新容量。(如果超过整型最大值,就会抛异常) newCapacity = hugeCapacity(minCapacity); // 把原先elementData数组拷贝到扩容后的新的elementData数组中 elementData = Arrays.copyOf(elementData, newCapacity); } // 底层Object elementData初始化为{},并没有创建长度 ArrayList list = new ArrayList(); // 首次调用add(),底层才创建长度为10的数组,并将数据123添加到elementData[0]中 list.add(123); // 后续的添加和扩容操作与jdk7无异 ···
-
小结:
- jdk7中的ArrayList的对象的创建类似于单例模式中的饿汉式,而jdk8中的ArrayList对象的创建类似于单例模式中的懒汉式,延迟了数组的创建,节省内存。
- 建议开发中使用带参的构造器,减少因不断扩容带来的硬件开销
-
-
LinkedList的源码分析
// LinkedList源码 private static class Node<E> { E item; Node<E> next; Node<E> prev; // Node定义:体现了LinkedList双向链表的说法 Node(Node<E>) prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } } // 内部声明了Node类型的first和last属性,默认值为null LinkedList list = new LinkedList(); // 将123封装到Node中,创建了Node对象 list.add(123);
-
List接口中的方法
method summary void add( int index , Object ele ) 在index位置插入ele元素 boolean addAll( int index , Collection eles) 从index位置上将eles中的所有元素添加进来 Object get( int index ) 获取指定位置上的元素 int indexOf( Object obj ) 返回obj在集合中首次出现的位置,如不存在,返回-1 int lastIndexOf( Object obj ) 返回obj在集合中最后出现的位置 Object remove( int index ) 移除指定位置上的元素,并返回该元素 Object set( int index , Object ele ) 将指定位置上的元素设定为ele List subList( int formIndex , int toIndex ) 返回从fromIndex到toIndex位置左闭右开的子集合 @Test public void listTest() { // 新建ArrayList集合list1 ArrayList list1 = new ArrayList(); list1.add(123); list1.add(456); list1.add("AA"); list1.add(new Person("Tom",21)); list1.add(456); // 集合list1的原本输出:[123, 456, AA, {Tom,21}, 456] System.out.println(list1); // 1.在集合索引0处插入元素 list1.add(0,"first"); // 输出结果为:[first, 123, 456, AA, {Tom,21}, 456] System.out.println(list1); // 2.1 新建List集合list2 List list2 = Arrays.asList(11, 22, 33); // 2.2 在索引值3处插入该集合中的所有元素 list1.addAll(3,list2); // 输出结果为:[first, 123, 456, 11, 22, 33, AA, {Tom,21}, 456] System.out.println(list1); // 3.得到集合索引值为0的元素 Object obj = list1.get(0); // 输出为:123 System.out.println(obj); // 4.返回元素456在集合中首次出现的索引值 int index = list1.indexOf(456); // 输出为:1 System.out.println(index); // 5.返回元素456在集合中最后一次出现的索引值 int index1 = list1.lastIndexOf(456); // 输出为:3 System.out.println(index1); // 6.删除索引值0处的元素,并将该元素返回 Object obj1 = list1.remove(0); // 删除元素作为返回值返回:first System.out.println(obj1); // 删除后的集合输出:[123, 456, 11, 22, 33, AA, {Tom,21}, 456] System.out.println(list1); // 7.设置索引1处的元素为789 Object obj2 = list1.set(1,789); // [123, 789, 11, 22, 33, AA, {Tom,21}, 456] System.out.println(list1); // 8.将索引值[2,4)范围内的元素组成一个新的集合并返回 List list3 = list1.subList(2, 4); // 新集合输出:[11, 22] System.out.println(list3); }
(二) Set接口
-
特点:
存储无序的、不可重复的数据 - - - > 高中时的“集合”
① **无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据元数据的哈希值决定排序的,第一个存储的数据可能不在数组的第一位,但是每个数据的存储位置时不变的
② ***不可重复:***保证添加的元素按照equals( )判断时,不能返回true。即:相同的元素只能添加一个
③ 添加的对象元素对应的类必须重写equals方法 -
Set接口的框架
① HashSet:作为Set接口的主要实现类,线程不安全,可以存储null值
| - - - LinkedHashSet:作为HashSet的子类,遍历内部数据时,可以按照添加的顺序遍历,因为增加了双向链表,常用于频繁遍历的集合
② TreeSet:使用对象排序接口,可以按照添加对象的指定属性,进行排序
-
添加元素的过程:以HashSet为例
-
① 我们向HashSet中添加元素a,首先调用元素a所在类的hashCode( )方法,计算元素a的哈希值
-
② 此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即:索引位置),判断数组在该位置上是否已经有其它元素:
-
如果此位置没有其它元素,则元素a添加成功。 - - - > 情况1
-
如果此位置有其它元素b(或以链表形式存在的多个元素),则比较元素a和元素b的hash值:
| - - - 如果hash值不相同,则元素 a 添加成功。 - - - > 情况2
| - - - 如果hash值相同,进而需要调用元素 a 所在类的equals( ) 方法:
| - - - equals( ) 返回false,则元素 a 添加成功 - - - > 情况3
| - - - equals( ) 返回true, 则元素 a 添加失败
说明:
-
底层也是数组,初始容量为16,超过容量,就会扩大容量为原来的2倍,结构为:数组 + 链表
-
对于添加成功的情况2和情况3:虽然hash值不一样,但是对应的底层数组的索引位置可能是一样的,故元素a与已经存在指定索引位置上数据以链表的方式存储
-
集合中的两个数据必须是hash值和equals值都相同,才能判定这两个元素是同一个数据
-
jdk7:元素 a 放到数组中,指向原来的元素
jdk8:原来的元素在数组中,指向元素 a
-
-
-
HashSet源码分析
-
源码分析图(与HashMap源码分析图结合去看)
-
程序示例
// HashSet测试类 public class HashSetTest { public static void main(String[] args) { // 新建一个HashSet集合 HashSet hashSet = new HashSet(); hashSet.add(456); hashSet.add(456); hashSet.add(new Customer("Tom",24)); hashSet.add(new Customer("Tom",24)); hashSet.add("A"); Iterator iterator = hashSet.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } } } // 作为HashSet集合中对象数据对应的类,需要重写equals方法和hashCode方法 class Customer { private String name; private int age; public Customer() {} public Customer(String name, int age) { this.name = name; this.age = age; } public void setName(String name) { this.name = name; } public String getName() { return name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "{" + "name='" + name + '\'' + ", age=" + age + '}'; } /* 1、下面重写的equals方法和hashCode方法是基于 IntelliJ Default 2、hashCode中的31的由来 ① 选择系数的时候,要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的冲突就越少,查 找起来效率也会提高。 ② 31 只占用5bits,相乘造成数据溢出的概率较小 ③ 31 可以由i*31 == (i<<5)-1 来表示,现在很多虚拟机中都有做相关优化,提高算法效率 ④ 31 是一个素数,素数作用就是如果我用一个数字乘以这个素数,那么最终出来的结果只能被素数本 身和被乘数还有1整除,减少冲突 */ @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof Customer)) { return false; } Customer customer = (Customer) o; if (getAge() != customer.getAge()) { return false; } return getName().equals(customer.getName()); } @Override public int hashCode() { int result = getName().hashCode(); result = 31 * result + getAge(); return result; }
/* 这里调用的java.util.Objects.equals and hashCode(java 7+)中的重写方法 这里要区分Objects和Object ① Object中的hashCode方法的源码是C程序:public native int hashCode(); ② Objects继承于Object,其hash方法调用了Arrays的hashCode() */ @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof Customer)) { return false; } Customer customer = (Customer) o; return getAge() == customer.getAge() && Objects.equals(getName(), customer.getName()); } @Override public int hashCode() { return Objects.hash(getName(), getAge()); }
-
-
LinkedHashSet存储
-
TreeSet
① 向TreeSet中添加的数据,要求是相同类的对象,且对象对应的类必须排序接口之一,因为涉及到红黑树排序
② 两种排序方式:
自然排序 (实现Comparable接口)
比较两个对象是否相同的标准为compareTo( ) 返回0,不再是equals( )
定制排序(实现Comparator接口)
比较两个对象是否相同的标准为compare( ) 返回0,不再是equals( )
代码测试
@Test public void treeSetTest() { TreeSet set = new TreeSet(); set.add(new Customer("Lily",36)); set.add(new Customer("Alla",18)); set.add(new Customer("Susan",19)); set.add(new Customer("Susan",25)); set.add(new Customer("Petty",79)); set.add(new Customer("Joda",59)); Iterator iterator = set.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } } class Custeomer implements Comparable{ @Override public int compareTo(Object o) { if (o instanceof Customer) { Customer c = (Customer)o; int result = this.name.compareTo(c.name); if (result != 0) { return result; } if (result == 0) { return Integer.compare(this.age,c.age); } } throw new RuntimeException("输入的数据不正确"); } }
-
强调:
- Set接口中没有额外定义新的方法,使用的都是Collection中声明过的方法
- 要求:向Set中添加数据,其所在的类一定要重写hashCode( ) 和 equals( ) 方法,实现对象相等规则。即:相等的对象必须具有相等的散列码,对象中用作 equals方法比较的 Field 都必须用来计算 hashCode。