Collection有两个常用的子接口:List 和 Set。它们的区别如下:
- List:有序(存入和取出的顺序一致),元素可以重复,元素都有索引(角标)。
- Set:无序,元素不可以重复。
List
先来说说List,List中的常用方法如下(它们的特点是都可以操作角标):
-
添加
void add(int index, E element)
boolean addAll(int index, Collection<? extends E> c) -
删除
E remove(int index) -
修改
E set(int index, E element) -
获取
E get(int index)
int indexOf(Object o) //返回此列表中第一次出现的指定元素的索引;如果此列表不包含该元素,则返回 -1。
int lastIndexOf(Object o) //返回此列表中最后出现的指定元素的索引;如果列表不包含此元素,则返回 -1。
List subList(int fromIndex, int toIndex) //返回列表中指定的 fromIndex(包括 )和 toIndex(不包括)之间的部分视图
接下来举一个初学者可能会犯错误的例子:
图片中的代码首先创建了一个ArrayList之后在其中添加了3个元素,之后开始迭代获取其中的字符串,如果取出来了str2就往里面插入一个字符串。那么为什么会产生异常呢?
我们知道,每次调用add方法,容器的size就会加1。其实问题出在第13行,第13行获取到了ArrayList的迭代器,这时候迭代器只知道容器里面有3个元素。那么在第二次循环中判断获取到了str2,则会在容器中又加入了一个元素,但是迭代器并不知道容器多了一个元素,所以产生异常。
换一种说法,上面的例子我们同时通过迭代器和集合对象(调用了add方法)对容器进行了操作,所以产生异常。根据这个解释,我们想不产生异常,要么只用迭代器操作容器,要么只用集合对象操作容器,但是Iterator并没有提供添加元素的方法,那么怎么解决这个问题呢?
在List这个接口中定义了一个方法ListIterator listIterator(int index) ,该方法返回了一个ListIterator对象,它是一个列表迭代器(List独有的),来看一下它里面提供的方法:
可以看到,这哥们儿是真牛逼阿,除了有add和set方法之外,还能反向遍历集合中的元素(hasPrevious和privious方法),那么上面的代码可以改成这样:
List<String> list = new ArrayList<>();
list.add("str1");
list.add("str2");
list.add("str3");
ListIterator<String> it = list.listIterator();
while(it.hasNext()) {
String str = it.next();
if(str.equals("str2")) {
it.add("插入");
} else {
System.out.println(str);
}
}
System.out.println(list);
这只是正向遍历,再来个反向遍历:
List<String> list = new ArrayList<>();
list.add("str1");
list.add("str2");
list.add("str3");
ListIterator<String> it = list.listIterator();
while(it.hasNext()) {
String str = it.next();
if(str.equals("str2")) {
it.add("插入");
}
}
System.out.println(list);
while(it.hasPrevious()) {
String str = it.previous();
if(str.equals("str2")) {
it.add("插入");
}
}
System.out.println(list);
说完了List这个接口常用方法,接着再来看看List的几个常见的实现类:
- Vector
- ArrayList
- LinkedList
首先来说说Vector,它可是个元老级别的人物了,在Java集合框架产生之前,Java里面只有一个集合类那就是Vector。Vector是线程同步的,它的内部元素存储结构是数组。由于线程同步,所以不管是增删还是查询速度都很慢。
再看看ArrayList,它不是同步的,相比于Vector,ArrayList速度要快很多。所以在ArrayList出现之后,Vector就被其替代了(因为Vector实在是没什么优点)。跟Vector一样,ArrayList内部存储结构也是数组。
最后是LinkedList,它也不是同步的,速度也比Vector快很多,LinkedList元素存储结构是链表。
既然LinkedList和 ArrayList速度都比Vector快,那么它们两个谁更快呢?其实是各有千秋。由数据结构的知识我们知道,数组结构的数据类型查询的速度很快但是增删元素的速度较慢,而链表结构的数据类型查询速度慢但是增删速度快。那么答案很明显了,我们的容器当需要频繁的增删元素时,就使用ArrayList,如果需要频繁的查询则使用ArrayList。
LinkedList由于是链表结构,所以除了实现List提供的方法之外,它还有自己特有的方法:
- void addFirst(E e) //将指定元素插入此列表的开头。
- void addLast(E e) // 将指定元素添加到此列表的结尾。
- E getFirst() //获取不移除,如果为空则抛异常
- E getLast() //同上
- E peekFirst() // 获取不移除;如果此列表为空,则返回 null。
- E peekLast() //同上
- E removeFirst() //获取并移除,如果为空则抛异常
- E removeLast() //同上
- E pollFirst() // 获取并移除;如果此列表为空,则返回 null
- E pollLast() //同上
说完了List,再来说说Set
Set
不同于List,Set中的元素不可以重复,而且是无序的。Set接口中提供的方法跟Collection是相同的,那么就来看看Set中的两个常用容器:
- HashSet:内部数据结构是哈希表,不是线程同步的。
- TreeSet:内部结构是二叉树,可以对容器中的元素进行排序,是不同步的
先来说说HashSet,先举个例子验证它的无序性。
可以看到,迭代顺序跟存储顺序是不相同的,实际上Set内部是有自己的存储算法的。
再来验证一下Set的唯一性:
可以看到,不管存了几个str4都没用,最后只输出了一个str4.
在上面的代码中,我们只用了String来举例验证Set的唯一性,那么如果换成我们自定义的类型,结果又会如何呢?关于这部分内容,请参考我的另一篇文章:HashSet容器的重复性问题
那么HashSet就说到这里,我们再来说一下HashSet的一个子类:LinkedHashSet。为什么要引入LinkedHashSet呢?因为HashSet是无序的,而LinkedHashSet虽然也是Set,但是它是有序的(比较特殊),LinkedHashSet的内部通过双向链表和哈希表来实现了唯一性和有序性。
我们来验证一下:
可以看到,输出的字符串有序了(跟添加进来的顺序一致称为有序)。
接着来看TreeSet,它是一个有比较功能的Set容器,既然是Set容器,那么元素就是唯一的,TreeSet判断元素是否重复的依据就是根据比较方法返回值是否为0,为零则重复(不存)。要想让元素具有比较功能,就必须让元素的类实现Comparable接口并覆盖compareTo方法,代码如下:
public class Test {
public static void main(String args[]){
TreeSet<Person> ts = new TreeSet<>();
ts.add(new Person(40, "刘能"));
ts.add(new Person(45, "谢大脚"));
ts.add(new Person(32, "宋晓锋"));
ts.add(new Person(40, "赵四"));
Iterator<Person> it = ts.iterator();
while(it.hasNext()){
Person p = it.next();
System.out.println(p.getName() + p.getAge());
}
}
}
class Person implements Comparable<Person> {
String name;
int age;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
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;
}
@Override
public int compareTo(Person p) {
int temp = age - p.getAge();
//返回值大于零说明当前对象比p大,等于零说明相等,小于零说明比p小
return temp==0? name.compareTo(p.getName()) : temp; //优先比较年龄,再比较姓名
}
}
运行结果如下:
在年龄相同的情况下,按照姓名进行排序。
那么考虑这样一种情况,如果我们要添加的元素是别人写的类(不能让你轻易改动),那么就可以自定义一个比较器实现Comparator接口并覆盖compare方法,并在创建TreeSet的时候将比较器对象作为参数传入构造方法中:
public class Test {
public static void main(String args[]){
TreeSet<Person> ts = new TreeSet<>(new MyComparator());
ts.add(new Person(40, "刘能"));
ts.add(new Person(45, "谢大脚"));
ts.add(new Person(32, "宋晓锋"));
ts.add(new Person(40, "赵四"));
Iterator<Person> it = ts.iterator();
while(it.hasNext()){
Person p = it.next();
System.out.println(p.getName() + p.getAge());
}
}
}
class Person {
String name;
int age;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
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;
}
}
class MyComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
int temp = p1.getAge() - p2.getAge();
return temp==0? p1.getName().compareTo(p2.getName()) : temp;
}
}
这两种比较的方式前者是让元素具有比较功能,后者是让集合具有比较功能。