Java学习笔记之集合类(二) List 和 Set

Collection有两个常用的子接口:List 和 Set。它们的区别如下:

  1. List:有序(存入和取出的顺序一致),元素可以重复,元素都有索引(角标)。
  2. Set:无序,元素不可以重复。

List

先来说说List,List中的常用方法如下(它们的特点是都可以操作角标):

  1. 添加
    void add(int index, E element)
    boolean addAll(int index, Collection<? extends E> c)

  2. 删除
    E remove(int index)

  3. 修改
    E set(int index, E element)

  4. 获取
    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的几个常见的实现类:

  1. Vector
  2. ArrayList
  3. LinkedList

首先来说说Vector,它可是个元老级别的人物了,在Java集合框架产生之前,Java里面只有一个集合类那就是Vector。Vector是线程同步的,它的内部元素存储结构是数组。由于线程同步,所以不管是增删还是查询速度都很慢。

再看看ArrayList,它不是同步的,相比于Vector,ArrayList速度要快很多。所以在ArrayList出现之后,Vector就被其替代了(因为Vector实在是没什么优点)。跟Vector一样,ArrayList内部存储结构也是数组。

最后是LinkedList,它也不是同步的,速度也比Vector快很多,LinkedList元素存储结构是链表。

既然LinkedList和 ArrayList速度都比Vector快,那么它们两个谁更快呢?其实是各有千秋。由数据结构的知识我们知道,数组结构的数据类型查询的速度很快但是增删元素的速度较慢,而链表结构的数据类型查询速度慢但是增删速度快。那么答案很明显了,我们的容器当需要频繁的增删元素时,就使用ArrayList,如果需要频繁的查询则使用ArrayList。

LinkedList由于是链表结构,所以除了实现List提供的方法之外,它还有自己特有的方法:

  1. void addFirst(E e) //将指定元素插入此列表的开头。
  2. void addLast(E e) // 将指定元素添加到此列表的结尾。
  3. E getFirst() //获取不移除,如果为空则抛异常
  4. E getLast() //同上
  5. E peekFirst() // 获取不移除;如果此列表为空,则返回 null。
  6. E peekLast() //同上
  7. E removeFirst() //获取并移除,如果为空则抛异常
  8. E removeLast() //同上
  9. E pollFirst() // 获取并移除;如果此列表为空,则返回 null
  10. E pollLast() //同上

说完了List,再来说说Set

Set

不同于List,Set中的元素不可以重复,而且是无序的。Set接口中提供的方法跟Collection是相同的,那么就来看看Set中的两个常用容器:

  1. HashSet:内部数据结构是哈希表,不是线程同步的。
  2. 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;
    }
}

这两种比较的方式前者是让元素具有比较功能,后者是让集合具有比较功能。

猜你喜欢

转载自blog.csdn.net/weixin_44965650/article/details/106983613