这篇文章主要讲的是java的Collection接口派生的两个子接口List和Set。List的主要特征是:有序、带有索引、允许重复,Set的主要特征是不允许重复,其中HashSet不能保证存取的顺序。List的实现类包括ArrayList, LinkedList, Vector,Set的实现类包括hashSet, LinkedHashSet, TreeSet.
*本文是最近学习到的知识的记录以及分享,算不上原创。
*参考文献见文末。
这篇文章主要讲的是java的Collection接口派生的两个子接口List和Set。
目录
Collection框架
List接口
Set接口
1.Collection框架
首先我们综合性地看一下java的Collection接口的框架,如下图:
*图中绿色表示接口,白色表示类。
List接口和Set接口是Collection接口派生的主要的两个子接口。
2.List接口
List的主要特征
(1)有序(ordered):元素的存取是有序的,保证了取出的元素的顺序与输入的元素顺序保持一致。
例如:
1 import java.util.ArrayList;
2 import java.util.LinkedList;
3 import java.util.Vector;
4
5 public class CollectionDemo {
6 public static void main(String[] args) {
7 //ArrayList
8 ArrayList<String> list=new ArrayList<String>();
9 list.add("11");
10 list.add("22");
11 list.add("33");
12 for(int i=0;i<list.size();++i){
13 System.out.print(list.get(i)+" "); //output: 11 22 33
14 }
15 System.out.println("");
16 //LinkedList
17 LinkedList<String> list2=new LinkedList<String>();
18 list2.add("11");
19 list2.add("22");
20 list2.add("33");
21 for(int i=0;i<list2.size();++i){
22 System.out.print(list.get(i)+" "); //output: 11 22 33
23 }
24 System.out.println("");
25 //Vector
26 Vector<String> list3=new Vector<String>();
27 list3.add("11");
28 list3.add("22");
29 list3.add("33");
30 for(int i=0;i<list3.size();++i){
31 System.out.print(list.get(i)+" "); //output: 11 22 33
32 }
33 System.out.println("");
34 }
35 }
List元素存取有序
(2)索引(index):允许用户根据索引对元素进行精准定位并进行查询、插入、删除等操作。
*所以,对List的遍历,不仅可以通过Iterator,还可以通过索引(index)。
(3)允许重复:允许多个重复的元素存在。
List的主要方法
https://docs.oracle.com/javase/7/docs/api/java/util/List.html
在提到List接口的各种实现类之前,首先我们回顾一下数据结构中数组和链表的各自的特色。
数组易于对元素的查询、遍历,但对元素的增删操作比较繁琐;链表能够方便地对元素进行增删,但不利于元素的查询、遍历。
2.1 ArrayList
ArrayList元素存储的数据结构是数组结构。ArrayList相当于动态数组,既保持了数组查询快速的优点,又不像数组那样对元素的增删慢,所以是最常用的集合。
ArrayList的Clone()
ArrayList的clone()属于浅拷贝。
浅拷贝与深拷贝的问题,这与引用对象的存储方式有关系。浅拷贝简单地说,就是把复制一个指向该对象的箭头给你,深拷贝简单地说,就是复制一个对象给你。
当ArrayList中的元素为基本数据类型时,可以说不存在浅拷贝与深拷贝的问题。
例如:
import java.util.ArrayList;
public class ListDemo3 {
public static void main(String[] args) {
ArrayList<Integer> a1=new ArrayList<Integer>();
a1.add(1);
a1.add(2);
a1.add(3);
//基本数据类型: byte short int long float double boolean char
ArrayList<Integer> a2=(ArrayList<Integer>) a1.clone();
System.out.println(a1); //[1, 2, 3]
System.out.println(a2); //[1, 2, 3]
a1.set(0, 10);
a2.remove(2);
System.out.println(a1); //[10, 2, 3]
System.out.println(a2); //[1, 2]
}
}
当ArrayList中的元素为引用数据类型时,要意识到浅拷贝与深拷贝的问题。
例如:
import java.util.ArrayList;
public class ListDemo3 {
public static void main(String[] args) {
ArrayList<Student> b1=new ArrayList<Student>();
Student s1=new Student(001,"zhangsan",22);
Student s2=new Student(002,"lisi",21);
Student s3=new Student(003,"wangwu",18);
b1.add(s1);
b1.add(s2);
b1.add(s3);
//基本数据类型: byte short int long float double boolean char
//引用数据类型:接口interface, 类class, 数组
ArrayList<Student> b2=(ArrayList<Student>) b1.clone();
System.out.println(b1);
System.out.println(b2);
/*
* [Student [id=1, name=zhangsan, age=22], Student [id=2, name=lisi, age=21], Student [id=3, name=wangwu, age=18]]
* [Student [id=1, name=zhangsan, age=22], Student [id=2, name=lisi, age=21], Student [id=3, name=wangwu, age=18]]
*/
b2.remove(2);
System.out.println(b1);
System.out.println(b2);
/*
* [Student [id=1, name=zhangsan, age=22], Student [id=2, name=lisi, age=21], Student [id=3, name=wangwu, age=18]]
* [Student [id=1, name=zhangsan, age=22], Student [id=2, name=lisi, age=21]]
*/
b1.get(0).setName("wangmazi");
System.out.println(b1);
System.out.println(b2);
/*
* [Student [id=1, name=wangmazi, age=22], Student [id=2, name=lisi, age=21], Student [id=3, name=wangwu, age=18]]
* [Student [id=1, name=wangmazi, age=22], Student [id=2, name=lisi, age=21]]
*/
}
}
第一步:创建ArrayList对象b1
第二步:clone()
从下图中,可以看到左边的箭头和右边的箭头,箭头左边是地址,箭头右边是指向的对象。我们可以发现虽然b1和b2的地址不同,但指向相同的对象。如果仅仅改变左边的箭头,如改变b1的箭头则不会影响到b2,但如果改变了右边的箭头,如改变了s1的箭头,就会同时对b1, b2造成影响。
Arraylist的遍历
ArrayList有三种遍历方式。
例如:
import java.util.ArrayList;
import java.util.Iterator;
public class ListDemo2 {
public static void main(String[] args) {
ArrayList<String> list=new ArrayList<String>();
list.add("11");
list.add("22");
list.add("33");
//第一种遍历方式:Iterator
Iterator<String> it=list.iterator();
while(it.hasNext()){
System.out.print(it.next()+" "); //11 22 33
}
System.out.println("");
//第二种遍历方式
for(int i=0;i<list.size();++i){
System.out.print(list.get(i)+" "); //11 22 33
}
System.out.println("");
//第三种遍历方式
for(String s:list){
System.out.print(s+" "); //11 22 33
}
System.out.println("");
}
}
结果显示,这三种遍历方法中,第二种(使用索引index)的效率最高,第一种(使用Iterator)的效率最低。
ArrayList的toArray(T[] contents)
Arraylist提供了两个将ArrayList转换为数组的方法:
Object[] toArray()
<T> T[] toArray(T[] contents)
由于toArray()返回的类型是Object[],如果进行强制类型转换会造成java.lang.ClassCastException
。因此调用toArray()容易出错,更建议使用toArray(T[] contents)。
例如:
import java.util.ArrayList;
import java.util.Arrays;
public class ListDemo2 {
public static void main(String[] args) {
ArrayList<String> list=new ArrayList<String>();
list.add("11");
list.add("22");
list.add("33");
//方法1
String[] str=new String[list.size()];
str=list.toArray(str);
System.out.println(Arrays.toString(str)); //[11, 22, 33]
//方法2
String[] str2=(String[])list.toArray(new String[0]);
System.out.println(Arrays.toString(str2)); //[11, 22, 33]
}
}
2.2 LinkedList
LinkedList元素存储的数据结构是链表结构。LinkedList能够方便地对元素进行增删。
LinkedList提供了一些方法,来方便对首尾元素的操作。
LinkedList还可以作为堆栈、队列的结构使用,所以提供了一些和堆栈、队列相关的方法。
例如:
1 import java.util.LinkedList;
2 public class ListDemo1 {
3 public static void main(String[] args) {
4 LinkedList<String> list=new LinkedList<String>();
5 list.add("11");
6 list.add("22");
7 list.add("33");
8 while(!list.isEmpty()){
9 System.out.print(list.pop()+" "); //output: 11 22 33
10 }
11 System.out.println("");
12 }
13 }
LinkedList的pop()
*注意元素存取的顺序,保持着先进先出的顺序。
2.3 Vector
Vector元素存储的数据结构是数组结构。Vector与ArrayList类似,Vector提供的Enumeration与ArrayList提供的Iterator类似,二者在功能上可以说的上是重复的。
例如:
1 import java.util.ArrayList;
2 import java.util.Enumeration;
3 import java.util.Iterator;
4 import java.util.Vector;
5
6 public class ListDemo1 {
7 public static void main(String[] args) {
8 //ArrayList和Iterator
9 ArrayList<String> list=new ArrayList<String>();
10 list.add("11");
11 list.add("22");
12 list.add("33");
13 Iterator<String> it=list.iterator();
14 while(it.hasNext()){
15 System.out.print(it.next()+" ");
16 }
17 System.out.println("");
18 //Vector和Enumeration
19 Vector<String> list2=new Vector<String>();
20 list2.add("11");
21 list2.add("22");
22 list2.add("33");
23 Enumeration<String> en=list2.elements();
24 while(en.hasMoreElements()){
25 System.out.print(en.nextElement()+" ");
26 }
27 System.out.println("");
28 }
29 }
ArrayList和Iterator vs.Vector和Enumration
3.Set接口
Set的主要特征
(1)不允许重复:元素不允许重复。Set在存储元素时会通过hashCode()和equals()来保证元素的唯一性。
Set如何保证元素的唯一性
Set在存储元素时,通过hashCode()和equals()来保证元素的唯一性。
事实上,当存储一个新的元素时,仅仅通过equals()来逐一判断新元素是否与集合中已有的元素是否重合,这种方法也是可行的,那为什么还需要hashCode()呢。因为当Set中元素数量很多时,通过equals()逐一判断并不是一个高效率的方法,所以同时通过hashCode()和equals()进行判断可以提高判断的效率。
首先,我们回忆一下什么是hashcode。
hashCode()是Object的类,每个对象都具有hashCode值。不同的对象可能会有相同的hashCode值,但hashCode值不相同的两个对象肯定不同。
我们可以用映射的概念来理解对象与hashCode之间的关系,对象(value)与hashCode(key)构成了多对一的映射。
当每次存储新的元素时,首先通过hashCode()获得新元素的hashCode,判断是否与已有元素的hashCode相同。如果没有,将新元素加入到集合中。如果有,再通过equals()判断元素是否相同,如果相同,则不添加该元素,如果不同,则把该元素加到集合中。
3.1 HashSet
HashSet元素存储的结构是哈希表。
hashSet除了不允许重复元素外,还不能保证元素存取的顺序。
例如:
1 import java.util.HashSet;
2 import java.util.Iterator;
3
4 public class setDemo1 {
5 public static void main(String[] args) {
6 HashSet<String> hash=new HashSet<String>();
7 hash.add("11");
8 hash.add("22");
9 hash.add("33");
10 Iterator<String> it=hash.iterator();
11 while(it.hasNext()){
12 System.out.print(it.next()+" ");
13 }
14 System.out.println(""); //output: 22 33 11
15 }
16 }
不能保证存取顺序
hashCode()和equals()的重写
就像我们刚才提到的,hashSet是通过hashCode()和equals()来保证元素的唯一性。JaveAPI中的每个类(如String, Integer类)都能获得hashCode和equals的比较方法,所以这类元素可以直接用hashSet存储,但是用户自定义的类,也需要先重写hashCode()和equals(),之后才能用hashSet存储该类元素。
*Object的equals()与==功能相同,判断的是地址是否相同。所以想要判断内容是否相同,必须要重写equals(),比如String类。
例如:
//用户自定义类
public class Student {
private int id;
private String name;
private int age;
//构造方法
public Student() {
// TODO Auto-generated constructor stub
}
public Student(int id,String name,int age){
this.id=id;
this.name=name;
this.age=age;
}
//getter and setter
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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;
}
//overwrite hashCode() and equals()
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + id;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Student other = (Student) obj;
if (age != other.age)
return false;
if (id != other.id)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
//overwrite toString()
@Override
public String toString() {
return "Student [id=" + id + ", name=" + name + ", age=" + age + "]";
}
}
//hashSet分别存储JavaAPI类和用户自定义类
import java.util.HashSet;
import java.util.Iterator;
public class setDemo1 {
public static void main(String[] args) {
//用HashSet存储JavaAPI类,如String、Integer等
HashSet<String> hash=new HashSet<String>();
hash.add("11");
hash.add("22");
hash.add("33");
hash.add("22");
Iterator<String> it=hash.iterator();
while(it.hasNext()){
System.out.print(it.next()+" ");
}
/*
* 不允许重复元素
* 不保证存取顺序
*/
System.out.println(""); //output: 22 33 11
//用HashSet存储用户自定义类
//首先在用户自定义类中需要重写hashCode()和equals()
HashSet<Student> hash2=new HashSet<Student>();
Student s1=new Student(001,"zhangsan",22);
Student s2=new Student(002,"lisi",21);
Student s3=new Student(003,"wangwu",18);
hash2.add(s1);
hash2.add(s2);
hash2.add(s3);
hash2.add(s1);
Iterator<Student> it2=hash2.iterator();
while(it2.hasNext()){
System.out.println(it2.next()+" ");
}
/*
* output:
* Student [id=3, name=wangwu, age=18]
* Student [id=1, name=zhangsan, age=22]
* Student [id=2, name=lisi, age=21]
*/
}
}
View Code
3.2 LinkedHashSet
LinkedHashSet的元素的存储结构是链表和哈希表。
LinkedHashSet保证了元素存取的顺序。
*LinkedHashSet遵循先进先出的顺序
例如:
1 import java.util.Iterator;
2 import java.util.LinkedHashSet;
3
4 public class setDemo2 {
5 public static void main(String[] args) {
6 LinkedHashSet<String> lset=new LinkedHashSet<String>();
7 lset.add("11");
8 lset.add("22");
9 lset.add("33");
10 Iterator<String> it=lset.iterator();
11 while(it.hasNext()){
12 System.out.print(it.next()+" ");
13 }
14 System.out.println(""); //output: 11 22 33
15 }
16 }
3.3 TreeSet
TreeSet可以保证元素存取的顺序。
参考文献
https://docs.oracle.com/javase/7/docs/api/java/util/List.html