集合——Collection
1、集合介绍
集合、数组都是对多个数据进行存储操作的结构,简称 Java 容器
此时的存储主要指的是内存层面的存储,不涉及到持久化的存储(.txt,.jpg,.avi)
- 数组存储多个数据的特点:
- 一旦初始化,长度就确定了数组
- 一旦定义好,其元素的类型就确定了,只能操作指定类型的数据
- 比如:
String[] arr
,int[] arr
,Object[] arr
- 数组在存储多个数据方面的缺点
- 一旦初始化,长度就不可修改
- 数组中提供的方法非常有限,对于添加、删除、插入等操作,不便,效率不高
- 获取数组中实际元素的个数(null为空,属于无实际元素),没有现成的属性或方法可用
- 数组存储数据的特点:有序、可重复。所以不能满足无需、不可重复的数据需求
集合与数组的区别:
Java 集合可以分为 Collection
和 Map
两种体系
Collection
:单列数据。定义了存取一组对象的方法的集合
—>List
:元素有序、可重复的集合 ——“动态”数组
--------------->ArrayList、 LinkedList、Vector
—> Set
:元素无序、不可重复的集合 —— 高中的集合
--------------->HashSet、LinkedHashSet、TreeSet
Map
:双列数据。保存具有映射关系 " key – value " 的集合 —— 高中的函数
—>HashMap、LinkedHashMap、TreeMap、Hashtable、Properties
" key – value ":二者的关系相当于是 f(x) = y
多个 key 可以指向同一个 value
但是不可能有多个 value 指向同一个 key
Collection 的继承树
Map 的继承树
2、Collection常用方法
Collection
的定义以ArrayList
为例,创建的是有序、可重复的集合
集合Collection
中方法执行后,更改的都是当前的对象,具有可变性
add(Object obj)
:将元素添加至当前集合中
addAll(Collection c)
:将集合c中的元素添加至当前集合中
public void Test01(){
//ArrayList创建有序的、可重复的集合
Collection co = new ArrayList();
//add(Object obj):添加元素至集合中
co.add(123);
co.add("xixi");
co.add(new String("草莓"));
co.add(new Person("Tom",18));
co.add(456);
//addAll(Collection c):将集合c中的元素添加至当前集合中
Collection c1 = Arrays.asList(222,333);
co.addAll(c1);
System.out.println(co);
//[123, xixi, 草莓, Person{name='Tom', age=18}, 456, 222, 333]
//size():获取集合中的元素个数
System.out.println(co.size()); //7
}
size()
:获取集合中的元素个数
isEmpty()
:判断当前集合的元素个数是否为 0
clear()
:清空当前集合的元素,而非为null
System.out.println(co.size());
co.clear();
System.out.println(co.isEmpty());
contain(Object obj)
:判断当前集合是否包含obj,返回一个boolean值
containAll(Collection c)
:判断集合c中的所有元素是否都存在当前集合中
会调用方法参数所在类的 equals() 方法
方法执行后,更改的都是当前的对象,具有可变性
public void Test01(){
Collection co = new ArrayList();
co.add(123);
co.add("xixi");
co.add(new String("草莓"));
co.add(new Person("Tom",18));
co.add(456);
//contain(Object obj):判断当前集合是否包含obj,返回一个boolean值
// System.out.println(co.contains(new String("草莓"))); //true
//此处未重写equals()方法,就返回false
System.out.println(co.contains(new Person("Tom", 18))); //重写了equals(),返回true
//containAll(Collection c):判断集合c中的所有元素是否都存在当前集合中
Collection c = Arrays.asList(123,4);
System.out.println(co.containsAll(c)); //false
}
执行结果如下
remove(Object obj)
:判断是否移除obj成功,返回一个boolean值
会调用方法参数所在类的 equals() 方法
方法执行后,更改的都是当前的对象,具有可变性
public void Test02(){
Collection co = new ArrayList();
co.add(123);
co.add("xixi");
co.add(new String("草莓"));
co.add(new Person("Tom",18));
co.add(456);
//remove(Object obj):判断是否移除obj成功(会先判断是否包含有这个obj,若没有这个成员,返回false)
//这里还是通过集合元素中的equals()方法来判断是否有obj
System.out.println(co.remove(new Person("Tom",18))); //true
System.out.println(co); //[123, xixi, 草莓, 456]
System.out.println(co.remove(new String("草莓"))); //true
System.out.println(co); //[123, xixi, 456]
}
执行结果如下
removeAll(Collection c)
:判断是否将集合c中的共有的元素移除成功
retainAll(Collection c)
:判断是否将集合c中的共有的元素放进当前集合
会调用方法参数所在类的 equals() 方法
方法执行后,更改的都是当前的对象,具有可变性
public void Test03(){
Collection co = new ArrayList();
co.add(123);
co.add("xixi");
co.add(new String("草莓"));
co.add(new Person("Tom",18));
co.add(456);
//removeAll(Collection c):判断是否将集合c中的共有的元素移除成功
//这里还是通过集合元素中的equals()方法来判断是否有obj
System.out.println(co); //[123, xixi, 草莓, Person{name='Tom', age=18}, 456]
Collection c = Arrays.asList(111,123);
System.out.println(co.removeAll(c)); //true
System.out.println(co); //[xixi, 草莓, Person{name='Tom', age=18}, 456]
//retainAll(Collection c):判断是否将集合c中的共有的元素放进当前集合
System.out.println(co); //[xixi, 草莓, Person{name='Tom', age=18}, 456]
Collection c1 = Arrays.asList(new Person("Tom",18),new Person("Jerry",17));
System.out.println(co.retainAll(c1)); //true
System.out.println(co); //[Person{name='Tom', age=18}]
}
执行结果如下
equals(Object obj)
:要想返回true,就要当前对象与参数对象的元素都相同
hashCode()
:返回当前对象的哈希值
System.out.println(co.equals(c1));
System.out.println(co.hashCode());
toArray()
: 集合 ----> 数组
Arrays.asList()
: 数组 ----> 集合
@org.junit.Test
public void Test04(){
Collection co = new ArrayList();
co.add(123);
co.add("xixi");
co.add(new String("草莓"));
co.add(new Person("Tom",18));
co.add(456);
//转为数组
Object[] o = co.toArray();
//遍历数组
for (Object o1 : o) {
System.out.println(o1);
}
//数组转为集合
Arrays.asList(new String[]{"aa","bb","cc"});
Arrays.asList("aa","bb","cc");
}
注意:
在数组转为集合,调用Arrays.asList()
的时候,有个特殊情况需要注意
注意当向Arrays
的静态方法asList()
中传入int类型的一维数组时候,会有特殊情况,这时会将 int型的一维数组看成一个元素
具体情况见以下代码
public void Test06(){
//数组转为集合,调用Arrays的静态方法asList()
List<String> ll = Arrays.asList("aa", "bb", "cc");
//注意当向Arrays的静态方法asList()中传入int类型的一维数组时候,会有特殊情况
//这种情况会将 int型的一维数组看成一个元素
List ll1 = Arrays.asList(new int[]{1,2,3});
System.out.println(ll1); //[[I@2c8d66b2]
System.out.println(ll1.size()); // 1
//可以写成以下形式
List ll2 = Arrays.asList(1,2,3);
System.out.println(ll2); //[1, 2, 3]
System.out.println(ll2.size()); // 3
List ll3 = Arrays.asList(new Integer[]{1,2,3});
System.out.println(ll3); //[1, 2, 3]
System.out.println(ll3.size()); // 3
}
iterator()
:返回Iterator接口的实例,可用于遍历集合
其实就是对于当前的集合,创建一个指针,默认游标在第一个元素之前
当集合对象每次调用iterator()
方法,都会得到一个全新的从头开始的指针,默认游标在第一个元素之前
结论:
向Collection
接口的实现类的对象添加数据obj时,要求obj所在类要重写equals()
方法
3、迭代器 Iterator
3.1、迭代器遍历
使用迭代器进行遍历集合
会用到两个方法
Iterator i = c.iterator();
创建迭代器对象
i.hasNext()
返回一个boolean值,判断指针下一个元素是否存在
i.next()
先指针下移,再进行输出
@org.junit.Test
public void Test01(){
Collection c = new ArrayList();
c.add(123);
c.add("xixi");
c.add(new Person("Tom",19));
c.add(456);
//使用迭代器遍历集合
//调用方法,返回迭代器对象
Iterator i = c.iterator();
while (i.hasNext()){
System.out.println(i.next());
}
//此时迭代器的指针已经指向最后一个元素
//再接着指针下移会报错
//抛出异常:java.util.NoSuchElementException
//System.out.println(i.next());
}
也可以进行循环遍历
Iterator i = c.iterator();
for (int j = 0; j < c.size(); j++) {
System.out.println(i.next());
}
迭代器的执行原理
此时注意两种错误的写法:
第一种情况
当判断i.next()
方法,指针下移,拿到了第一个元素不是空,再调用i.next()
方法输出了第二个元素;
再次判断i.next()
方法,指针下移,拿到了第三个元素不是空,再调用i.next()
方法输出了第四个元素;
再次判断i.next()
方法,指针下移,拿第五个元素,没拿到,抛异常
public void Test02(){
Collection c = new ArrayList();
c.add(123);
c.add("xixi");
c.add(new Person("Tom",19));
c.add(456);
//第一种
Iterator i = c.iterator();
while (i.next() != null){
System.out.println(i.next());
}
}
执行结果如下
第二种情况
当调用c.iterator()
,会得到一个新的迭代器指针,指向第一个元素之前
然后调用hasNext()
方法,判断了第一个元素存在,执行循环体
循环体中调用c.iterator()
会得到一个新的迭代器指针指向第一个元素之前
调用next()
方法,指针下移拿到了第一个元素,并输出
下一步再判断,调用c.iterator()
,得到一个新的迭代器指针
然后调用hasNext()
方法,判断了第一个元素存在,执行循环体
循环体中调用c.iterator()
会得到一个新的迭代器指针指向第一个元素之前
调用next()
方法,指针下移拿到了第一个元素,并输出
。。。
成为了死循环
//第二种
while (c.iterator().hasNext()) {
System.out.println(c.iterator().next());
}
执行结果如下
3.2、迭代器remove()
迭代器中的remove()
方法,移除的也是当前集合 中的元素,更改的也是当前集合
@org.junit.Test
public void Test02(){
Collection c = new ArrayList();
c.add(123);
c.add("xixi");
c.add(new Person("Tom",19));
c.add(456);
Iterator i = c.iterator();
while (i.hasNext()){
Object o = i.next();
if("xixi".equals(o)){
i.remove();
}
}
System.out.println(c);
}
执行结果如下
注意:
如果还未调用next()
方法就调用remove()
方法,会抛异常
或者调用了一次remove()
方法,再调用一次remove()
方法,会抛异常java.lang.IllegalStateException
比如在第一步,hasNext()
方法判断了第一个元素存在,但是指针还在第一个元素之前,此时调用remove()
方法,没有东西可以拿来移除
比如已经移除了元素之后,再次调用remove()
方法,没有东西可以拿来移除
Iterator i = c.iterator();
while (i.hasNext()){
i.remove();
}
执行结果如下
3.3、新特性for- each
底层其实就是调用迭代器Iterator
来实现的
//for(集合类型 自定义的局部变量:集合对象){循环体}
for (Object o : c) {
System.out.println(o);
}
执行原理:
在后面的集合c
中去一个元素,然后赋值
给局部变量Object o
执行循环体,输出变量o
此处也可以遍历数组
//for(数组类型 局部变量:数组对象){循环体}
int[] i = new int[]{1,2,3,4,5};
for (int i1 : i) {
System.out.println(i1);
}
面试题:
判断以下代码执行结果
String[] arr = new String[]{"MM","MM","MM"};
for (int i = 0; i < arr.length; i++) {
arr[i] = "GG";
}
//遍历一下
for (String o : arr) {
System.out.println(o);
}
执行结果如下
String[] arr1 = new String[]{"MM","MM","MM"};
for (String o : arr1) {
o = "GG";
}
//遍历一下
for (String o : arr1) {
System.out.println(o);
}
执行结果如下
第二种 foreach 循环使用局部变量来赋值操作,所以涉及到 String 的不可变性
第一种是直接操作数组
4、List 接口
List
集合类中元素是有序的、可重复的
面试题:
指出ArrayList
、LinkedList
、Vector
三者的异同点:
- 同:三个类都是实现了
List
接口,存储数据的特点相同:有序的、可重复的 - 异:
①ArrayList
:List 接口的主要实现类,jdk 1.2存在。线程不安全,效率高。底层使用Object[] elementData
存储。扩容是底层扩1.5倍
②LinkedList
:jdk 1.2存在。底层使用双线链表存储,对于频繁的插入、删除操作,效率更高
③Vector
:List 接口的古老实现类,jdk 1.0存在。线程安全,效率低。底层使用Object[] elementData
存储。扩容是底层扩2倍
比如StringBuffer
等的底层使用char[] value
存储
双线链表存储、数组存储 区别:
- 使用双线链表存储的数据方便插入、删除等,但不方便查询
- 使用数组存储的数据方便查询,但不方便进行插入、删除等
双线链表存储、数组存储 原理:
- 以下是双向链表的图片演示 在这种情况下, 若需要在第三个位置插入数据,只需要修改第二个和第四个数据的前后引用链接,无需动其他的
- 在数组存储数据的情况下,若想在第三个位置插入数据,就需要其后的数据都往后移动一位,给插入的数据腾空。若需要删除数据,就需要后面的数据都往前移动,填补删除的数据的空格
双向链表结构图解
4.1、ArrayList底层源码分析
在JDK 7情况下(类似于饿汉式,先造好)
ArrayList
没有赋值的时候底层创建了一个长度为10的Object数组
ArrayList aa1 = new ArrayList();
——//Object[] elementData = new Object[10];
aa1.append(123);
——//elementData[0] = new Integer(123);
扩容问题:
如果要添加的数据底层数组放不下,那就需要扩容底层的数组,默认情况下,扩容为原来容量的1.5倍,同时将原有数组中元素复制到新的数组中
对于此种情况,建议使用ArrayList(int initialCapacity)创建数组。指定长度
在JDK 8情况下(类似于懒汉式,先不造)
ArrayList
没有赋值的时候底层没有创建数组,没有实例化数组(节省空间内存)
ArrayList aa1 = new ArrayList();
——//Object[] elementData = {};
当第一次添加数据的时候,创建长度为10的Object数组,然后添加
aa1.append(123);
——//elementData = new Object[10]; elementData[0] = new Integer(123);
后续与JDK 7情况下一样
4.2、LinkedList底层源码分析
LinkedList
LinkedList aa1 = new LinkedList();
——内部声明了Node
类型的first
和last
属性,默认值为null
当添加数据的时候
aa1.append(123);
——其实就是最后一个元素的后链接给了新加的元素的前空,新加的元素的前链接给了最后一个元素的后空。(双向指向)新加的元素的后空为null,新加的元素作为整个集合的最后一个元素
Node
定义源码如下:体现了LinkedList
的双向链表的说法
private static class Node<E> {
E item;
LinkedList.Node<E> next;
LinkedList.Node<E> prev;
Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
this.prev = prev;
this.item = element;
this.next = next;
}
}
add
操作的源码如下:
void linkLast(E e) {
LinkedList.Node<E> l = this.last;
LinkedList.Node<E> newNode = new LinkedList.Node(l, e, (LinkedList.Node)null);
this.last = newNode;
if (l == null) {
this.first = newNode;
} else {
l.next = newNode;
}
++this.size;
++this.modCount;
}
4.3、Vector底层源码分析
Vector
没有赋值的时候底层创建了一个长度为10的Object数组
Vector aa1 = new Vector();
——//Object[] elementData = new Object[10];
扩容问题:
默认情况下,扩容为原来容量的2倍,线程是安全的
4.4、List接口的常用方法
因为List
是有序的、可重复的,底层带有索引,所以现在的方法里面包括对索引的操作
add(int i , Object obj)
:在索引 i 处添加插入数据 obj
addAll(int i , Collection c)
:在索引 i 处添加插入集合 c
ArrayList a1 = new ArrayList();
a1.add(123);
a1.add(4);
System.out.println(co.size()); //2
ArrayList a2 = new ArrayList();
a2.add(2);
a2.add(3);
//注意以下情况
//a1.addAll(a2);
//System.out.println(co.size()); //4
a1.add(a2);
System.out.println(co.size()); //3
Object get(int i)
:获取索引 i 处的元素
Object set(int i,Object obj)
:将指定索引位置的元素替换为 obj
int indexOf(Object obj)
:返回元素 obj 在集合中首次出现的索引,若没有返回-1
int lastindexOf(Object obj)
:返回元素 obj 在集合中末次出现的索引,若没有返回-1
Object remove(int i)
:移除指定索引 i 处的元素,并返回此元素输出。注意此方法是重载了集合中的remove方法,两个都存在,看你要用哪个
默认里面传入的是索引值
public void Test02(){
ArrayList a = new ArrayList();
a.add(1);
a.add(2);
a.add(3);
m(a);
System.out.println(a); //[1, 2]
}
private static void m(List l){
l.remove(2); //这里默认是索引
//若想要删除的是元素,可封装成对象
//l.remove(new Integer(2));
}
List subList(int i,int j)
:返回截取指定索引位置的集合,左闭右开
总结常用方法:
增:add(Object obj)
删:Object remove(int i)
、boolean remove(Object obj)
改:Object set(int i,Object obj)
查:Object get(int i)
插:add(int i , Object obj)
长度:size()
遍历:迭代器、for-each循环、普通循环
普通循环遍历
for (int i = 0; i < aa1.size(); i++) {
System.out.println(aa1.get(i));
}
5、Set 接口
Set
集合类中元素是无序的、不可重复的
HashSet
:Set 接口的主要实现类。线程不安全,效率高。可以存储 null 值(add
方法可以添加null值)LinkedHashSet
:是 HashSet 的子类。
在遍历元素的时候,可以按照添加的顺序来遍历输出。
对于频繁的遍历操作,LinkedHashSet
效率高于HashSet
TreeSet
:可以按照添加的对象的指定属性,来排序。不可以存储null值
Set
接口中没有定义新的方法,能使用的方法只有Collection
中的方法
结论:
向Set
中添加数据时,要求其所在类要重写hashSet()
方法和equals()
方法
重写的hashSet()
方法和equals()
方法要尽可能保持一致,相等的对象必须有相等的散列码(无序性就是按照散列码找位置)
.
小技巧:对象中作equals()
方法比较的属性,都应该来在hashSet()
方法中参与计算哈希值
5.1、HashSet
理解Set
集合类中的无序和不可重复,以HashSet
为例
1. 无序性:
不等于随机性。不管遍历多少次,都会是按照一个顺序输出的
而这个顺序就是根据元素的哈希值决定的。
根据哈希值通过散列排序得到散列码,散列码就是对应数组的位置,所以元素的位置是根据哈希值决定的
public class Test {
@org.junit.Test
public void Test01() {
HashSet h = new HashSet();
h.add(123);
h.add("xixi");
h.add(888);
h.add(new Student("Tom",18)); //注意需要重写toString()方法
//遍历
Iterator iterator = h.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
class Student{
private String name;
private int age;
public Student() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
执行结果如下
2. 不可重复性:
先根据元素的哈希值判断是否重复,若哈希值一样,再根据元素所在类的equals()
方法判断是否重复
所以建议在给
HashSet
添加数据obj的时候,要求obj所在类要重写hashCode()
方法和equals()
方法
向
Collection
接口的实现类的对象添加数据obj时,要求obj所在类要重写equals()
方法
在没有重写hashCode()
方法之前,添加两个new Student("Tom",18)
,遍历输出也会是两个,因为父类Object
中的hashCode()
方法就是生成随机数,所以每个对象数据的哈希值都不同
代码如下
public class Test {
@org.junit.Test
public void Test01() {
HashSet h = new HashSet();
h.add(123);
h.add("xixi");
h.add(888);
h.add(new Student("Tom",18)); //注意需要重写toString()方法
h.add(new Student("Tom",18));
//遍历
Iterator iterator = h.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
执行结果如下
3. 添加元素的过程:
元素的位置是由数据的哈希值决定的,是根据数据obj的哈希值通过计算得出位置在哪里
我们向HashSet
中添加元素a,首先调用a所在类的hashCode()
方法,计算元素a的哈希值,此哈希值通过某种计算得到HashSet
底层数组的存放位置(即为索引位置),判断数组此位置上是否已经有元素
——若位置上没有元素,则元素a添加成功 -------> 情况①
——若位置上有其他元素b(或以链表形式存在多个元素),则比较元素a、b的哈希值
————若哈希值不同,则元素a添加成功 -------> 情况②
————若哈希值相同,则需要调用元素a所在类的equals()
方法
————————equals()
方法返回true,则属于重复元素,添加失败
————————equals()
方法返回false,则元素a添加成功 -------> 情况③
注意:
对于情况②和情况③,元素a 与 已经存在指定位置上的数据以链表的方式存储
JDK 7 中,元素a放入数组中,指向原来的数据
JDK 8 中,原来的数据放在数组中,指向元素a
总结:七上八下
5.2、LinkedHashSet
是HashSet
的子类,所以存储数据与HashSet
区别不大,也是无序的、不可重复的。
区别不过是在添加元素的时候,在原有HashSet
的基础上添加了链表,每个元素有两个引用,记录此元素的前一个数据和后一个数据
优点:对于频繁的遍历操作,LinkedHashSet
效率高于HashSet
5.3、TreeSet
- 可以按照添加的对象的指定属性,来排序
- 向
TreeSet
中添加数据,要求是相同类的对象 TreeSet
和TreeMap
采用红黑树的存储结构,是有序的,查询速度比List快- 注意:当
TreeSet
调用remove
之类的方法时,不是用equals
来判断相同元素,而还是用Compare
比较
以基本数据类型 int 为例
以下情况会抛异常:java.lang.ClassCastException
class java.lang.Integer cannot be cast to class java.lang.String
// @org.junit.Test
// public void Test02(){
// TreeSet t = new TreeSet();
// t.add(1);
// t.add(2);
// t.add("xixi");
// }
对于自定义类而言,若TreeSet
中数据都是自定义类Student
,则类Student
中需要实现Comparable
接口,重写compareTo(Object obj)
方法,否则报错。
如下所示,抛异常:java.lang.ClassCastException
class Student cannot be cast to class java.lang.Comparable
@org.junit.Test
public void Test02(){
TreeSet t = new TreeSet();
t.add(new Student("Jerry",17));
t.add(new Student("Tom",20));
t.add(new Student("Ammiy",15));
t.add(new Student("Cancy",80));
t.add(new Student("Ben",25));
}
!
对于TreeSet
有两种排序方式:
- 自然排序
- 实现
Comparable
接口,重写compareTo(Object o)
方法 - 自然排序中,比较两个对象是否重复的标准:compareTo()返回0,(不再是根据equals()方法)
- 实现
- 定制排序
- 定义
Comparator
对象,作为参数传入TreeSet
有参构造器 - 比较两个对象是否重复的标准:compareTo()返回0,(不再是根据equals()方法)
- 定义
自然排序:
类Student
中重写compareTo(Object o)
方法,按照姓名从小到大排序
@Override
public int compareTo(Object o) {
if(o instanceof Student){
Student s = (Student)o;
return this.name.compareTo(s.name);
}
throw new RuntimeException("传入数据类型不一致");
}
然后遍历输出
@org.junit.Test
public void Test02(){
TreeSet t = new TreeSet();
t.add(new Student("Jerry",17));
t.add(new Student("Tom",20));
t.add(new Student("Ammiy",15));
t.add(new Student("Cancy",80));
t.add(new Student("Ben",25));
Iterator i = t.iterator();
while (i.hasNext()){
System.out.println(i.next());
}
}
执行结果如下
注意:
在TreeSet
中体现不可重复性是根据参与排序方法的元素的重复决定的
比如在以下代码中,传入参数
public void Test02(){
TreeSet t = new TreeSet();
t.add(new Student("Jerry",17));
t.add(new Student("Tom",20));
t.add(new Student("Ammiy",15));
t.add(new Student("Cancy",80));
t.add(new Student("Ben",25));
t.add(new Student("Cancy",99999));
Iterator i = t.iterator();
while (i.hasNext()){
System.out.println(i.next());
}
}
new Student("Cancy",99999)
和new Student("Cancy",80)
只有一个元素name
相同,同时,也只有这一个元素name
参与了compareTo(Object o)
方法的重写
@Override
public int compareTo(Object o) {
if(o instanceof Student){
Student s = (Student)o;
return this.name.compareTo(s.name);
}
throw new RuntimeException("传入数据类型不一致");
}
遍历输出结果如下
而假如此时两个元素都参与了compareTo(Object o)
方法的重写,则会根据两个元素是否重复来决定不可重复性
//先按照姓名从小到大排序,再按照年龄从小到大排序
@Override
public int compareTo(Object o) {
if(o instanceof Student){
Student s = (Student)o;
int i = this.name.compareTo(s.name);
if (i == 0){
return Integer.compare(this.age,s.age);
}
return i;
}
throw new RuntimeException("传入数据类型不一致");
}
遍历输出结果如下
定制排序:
在这种排序下,传入的数据所在类也可以不实现Comparable
接口和重写compareTo(Object o)
方法。这样也不会报错
假如实现Comparable
接口和重写compareTo(Object o)
方法。也只会根据定制排序来实现排序
比如以下代码中,Student
类中未实现Comparable
接口,直接使用定制排序
@org.junit.Test
public void Test03(){
Comparator c = new Comparator() {
//按照名字从大到小排序
@Override
public int compare(Object o1, Object o2) {
if (o1 instanceof Student && o2 instanceof Student){
Student s1 = (Student)o1;
Student s2 = (Student)o2;
return -s1.getName().compareTo(s2.getName());
}
throw new RuntimeException("传入数据类型不一致");
}
};
TreeSet t = new TreeSet(c);
t.add(new Student("Jerry",17));
t.add(new Student("Tom",20));
t.add(new Student("Ammiy",15));
t.add(new Student("Cancy",80));
t.add(new Student("Ben",25));
Iterator i = t.iterator();
while (i.hasNext()){
System.out.println(i.next());
}
}
执行结果如下
5.4、Set接口面试题
练习:
在List
内取出重复的数字值
操作:将ArrayList
集合放入HashSet
集合里面
public static List dd(List l){
HashSet h = new HashSet();
h.addAll(l);
return new ArrayList(h);
}
public void Test01(){
List aa = new ArrayList();
aa.add(new Integer(1));
aa.add(new Integer(1));
aa.add(new Integer(2));
aa.add(new Integer(2));
aa.add(new Integer(3));
aa.add(new Integer(3));
List aa1 = dd(aa);
for (Object o : aa1) {
System.out.println(o);
}
}
问题二:
问以下程序的输出结果(类Person已经重写了hashSet()
方法和equals()
方法)
public void Test01(){
HashSet h = new HashSet();
Person p1 = new Person("AA",18);
Person p2 = new Person("BB",20);
h.add(p1);
h.add(p2);
p1.name("CC");
System.out.println(h);
h.remove(p1);
System.out.println(h);
h.add(new Person("CC",18));
System.out.println(h);
h.add(new Person("AA",18));
System.out.println(h);
}
在执行h.add(p1);
、h.add(p2);
的时候,此时HashSet
集合中就根据二者的属性计算出,得出位置1A、2B
然后p1.name("CC");
,输出的结果就是"CC",18、“BB”,20
执行h.remove(p1);
,因为HashSet
集合是根据哈希值找位置,然后来找元素。所以根据此时的p1,即"CC",18计算得到位置1C,此时发现这个位置上没有元素,就删除失败。所以输出的结果还是"CC",18(1A位置)、“BB”,20(1B位置)
执行h.add(new Person("CC",18))
,此时HashSet
集合中就根据属性计算出,得出位置1C,这个位置是空的,就直接在这个位置放数据。所以输出的结果是"CC",18(1A位置)、“BB”,20(1B位置)、“CC”,18(1C位置)
执行h.add(new Person("AA",18));
,此时HashSet
集合中就根据属性计算出,得出位置1A,这个位置有数据"CC",18(1A位置),然后比较二者的哈希值,发现哈希值不同,就以链表的方式将两个数据放在一个位置上。所以输出的结果是"CC",18(1A位置)、“AA”,18(1A位置)、“BB”,20(1B位置)、“CC”,18(1C位置)
6、小结
- 集合大家庭简介
Collection
:单列数据List
:元素有序、可重复的集合
ArrayList
、LinkedList
、Vector
Set
:元素无序、不可重复的集合
HashSet
、LinkedHashSet
、TreeSet
Map
:双列数据
HashMap
、LinkedHashMap
、TreeMap
、Hashtable
、Properties
Collection
常用方法
修改的都是当前集合本身
建议添加的元素所在类都重写equals()
方法add(Object obj)
:将元素添加至当前集合中addAll(Collection c)
:将集合c中的元素添加至当前集合中size()
:获取集合中的元素个数isEmpty()
:判断当前集合的元素个数是否为 0clear()
:清空当前集合的元素,而非为nullcontain(Object obj)
:判断当前集合是否包含obj,返回一个boolean值containAll(Collection c)
:判断集合c中的所有元素是否都存在当前集合中remove(Object obj)
:判断是否移除obj成功,返回一个boolean值removeAll(Collection c)
:判断是否将集合c中的共有的元素移除成功retainAll(Collection c)
:判断是否将集合c中的共有的元素放进当前集equals(Object obj)
:要想返回true,就要当前对象与参数对象的元素都相同hashCode()
:返回当前对象的哈希值toArray()
: 集合 ----> 数组Arrays.asList()
: 数组 ----> 集合iterator()
:返回Iterator接口的实例,可用于遍历集合
- 迭代器
i.hasNext()
返回一个boolean值,判断指针下一个元素是否存在i.next()
先指针下移,再进行输出i.remove()
迭代器中的remove()
方法,移除的也是当前集合 中的元素,更改的也是当前集合for(集合类型 自定义的局部变量:集合对象){循环体}
- 注意:
当集合对象每次调用iterator()
方法,都会得到一个全新的从头开始的指针,默认游标在第一个元素之前
List
接口ArrayList
、LinkedList
、Vector
三者的异同点- 同:三个类都是实现了
List
接口,存储数据的特点相同:有序的、可重复的 - 异:
①ArrayList
:List 接口的主要实现类,jdk 1.2存在。线程不安全,效率高。底层使用Object[] elementData
存储(扩容1.5倍)
②LinkedList
:jdk 1.2存在。底层使用双线链表存储,对于频繁的插入、删除操作,效率更高
③Vector
:List 接口的古老实现类,jdk 1.0存在。线程安全,效率低。底层使用Object[] elementData
存储(扩容2倍)
- 同:三个类都是实现了
- 常用方法
add(int i , Object obj)
:在索引 i 处添加插入数据 objaddAll(int i , Collection c)
:在索引 i 处添加插入集合 cObject get(int i)
:获取索引 i 处的元素Object set(int i,Object obj)
:将指定索引位置的元素替换为 objint indexOf(Object obj)
:返回元素 obj 在集合中首次出现的索引,若没有返回-1int lastindexOf(Object obj)
:返回元素 obj 在集合中末次出现的索引,若没有返回-1Object remove(int i)
:移除指定索引 i 处的元素,并返回此元素输出。默认里面传入的是索引值List subList(int i,int j)
:返回截取指定索引位置的集合,左闭右开
Set
接口HashSet
:
① Set 接口的主要实现类。线程不安全,效率高
② 可以存储 null 值
③ 元素所在类需要重写hashSet()
方法和equals()
方法LinkedHashSet
:
① 是HashSet
的子类
② 在遍历元素的时候,可以按照添加的顺序来遍历输出。
③ 对于频繁的遍历操作,LinkedHashSet
效率高于HashSet
④ 元素所在类需要重写hashSet()
方法和equals()
方法TreeSet
:
① 只可以放同一个类型的对象
② 采用红黑树的存储结构,是有序的,查询速度比List快
③ 比较两个对象是否重复的标准:compareTo()返回0,(不再是根据equals()方法)
④ 元素所在类需要有 Java 比较器