1. Collection接口常用方法

  2. 使用Iterator遍历Collection

  3. 迭代器Iterator的执行原理

  4. Iterator遍历集合的两种错误写法

  5. Iterator迭代器remove()的使用

  6. foreach循环遍历集合或数组

  7. List接口常用实现类的对比

  8. ArrayList源码分析

  9. LinkedList源码分析

  10. Vector源码分析

  11. List接口中的常用方法测试

  12. List遍历及方法总结

  13. List的一个面试题

  14. Set接口实现类的对比

  15. Set的无序性和不可重复性的理解

  16. HashSet中元素的添加过程

  17. 关于hashCode()和equals()的重写

  18. LinkedHashSet的使用

  19. TreeSet的自然排序

  20. TreeSet的定制排序

 

1,Collection接口常用方法

    Person类

package com.atguigu.java;

import java.util.Objects;

public class Person {

    private String name;
    private int age;

    public Person() {

    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    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 String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        System.out.println("Person equals()...");
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                Objects.equals(name, person.name);
    }
}
// 向Collection接口的实现类的对象中添加数据obj时,要求obj的类要重写equals()
// 如包装类和String等自己重写了,而如果是自定义类,则要手动重写
@Test
public void test1() {
    Collection coll = new ArrayList();
    coll.add(123);
    coll.add(456);
    coll.add(new String("Tom"));
    coll.add(false);
    coll.add(new Person("Jerry", 20));

    // contains(Object obj):判断obj是否在当前集合中
    System.out.println(coll.contains(123)); // true
    System.out.println(coll.contains(new String("Tom"))); // true,显然比较的是对象的内容,即调用equals()
    System.out.println(coll.contains(new Person("Jerry", 20))); // 如果Person没有重写equals(),则调用Object的equals()返回false
                                                // 首先将123作为Person的equals()的形参进行比较,依次往后比较,比较了5次,输出了5行“Person equals()...”
                                                // 比较集合中最后一个元素时返回true表示找到

    // containsAll(Collection coll):判断一个集合中的所有元素是否在另一个集合中
    Collection coll1 = Arrays.asList(123, false);
    System.out.println(coll.containsAll(coll1)); // true
}
@Test
public void test2() {
    Collection coll = new ArrayList();
    coll.add(123);
    coll.add(456);
    coll.add(new String("Tom"));
    coll.add(false);
    coll.add(new Person("Jerry", 20));

    // remove():移除集合中的元素,先要判断集合中是否有这个元素,也就是调用equals(),有的话就移除并返回true,否则返回false
    coll.remove(123);
    System.out.println(coll); // [456, Tom, false, Person{name='Jerry', age=20}]
    coll.remove(new Person("Jerry", 20)); // Person重写了equals(),所以才能移除
    System.out.println(coll); // [456, Tom, false]

    // removeAll():从一个集合中移除另一个集合中的所有元素
    Collection coll1 = Arrays.asList(456, "Tome");
    coll.removeAll(coll1);
    System.out.println(coll); // [Tom, false],“Tome”不在coll中,所以只移除了456
}
@Test
public void test3() {
    Collection coll = new ArrayList();
    coll.add(123);
    coll.add(456);
    coll.add(new String("Tom"));
    coll.add(false);
    coll.add(new Person("Jerry", 20));

    // retainAll(Collection coll):获取一个集合与另一个集合的交集
    Collection coll1 = Arrays.asList(123, 456, 789);
    coll.retainAll(coll1);
    System.out.println(coll); // 输出了3行Person equals()...,[123, 456],集合coll变为交集。由于List可重复
                            // ,所以对于123,会比较集合中的每个元素,也就调用equals()

    // equals(Object obj):比较两个集合是否相等,调用集合中每个元素的equals()来比较
    Collection coll2 = new ArrayList();
    coll2.add(456);
    coll2.add(123);
    System.out.println(coll.equals(coll2)); // false,因为ArrayList是有序的
}
@Test
public void test4() {
    Collection coll = new ArrayList();
    coll.add(123);
    coll.add(456);
    coll.add(new String("Tom"));
    coll.add(false);
    coll.add(new Person("Jerry", 20));

    // hashCode():返回当前对象哈希值
    System.out.println(coll.hashCode());

    // 集合——>数组:toArray()
    Object[] arr = coll.toArray();
    for(int i = 0 ; i <arr.length ; i++) {
        System.out.println(arr[i]);
    }
    // 数组——>集合
    List list = Arrays.asList(new String[]{"AA", "BB", "CC"});
    System.out.println(list);
}

 

2,使用Iterator遍历Collection

day23-集合_集合

@Test
public void test1() {
    Collection coll = new ArrayList();
    coll.add(123);
    coll.add(456);
    coll.add(new String("Tom"));
    coll.add(false);
    coll.add(new Person("Jerry", 20));

    Iterator iterator = coll.iterator(); // 返回一个迭代器对象
    while(iterator.hasNext()) { // 遍历迭代器中的每个元素
        System.out.println(iterator.next()); // 如果已经输出了最后一个元素,继续调用next()就会抛出NoSuchElementException异常
    }
}

 

3,迭代器Iterator的执行原理

    以上面为例,当返回一个迭代器对象后,就有一个指针指向了集合第一个元素的前一个位置。hasNext()判断下一个位置是否还有元素,调用next()会使指针下移并返回该位置的元素。这里集合只有一个,它存储了元素,迭代器只作为一个遍历集合的结构,本身不存储元素
 

4,Iterator遍历集合的两种错误写法

Iterator iterator = coll.iterator();
while((iterator.next()) != null) { // 调用一次next()指针就会下移一位
    System.out.println(iterator.next());
}

while(coll.iterator().hasNext()) { // 每次返回的都是一个新的迭代器对象,指针指向的是集合第一个元素的前一个位置
    System.out.println(coll.iterator.next());
}

 

5,Iterator迭代器remove()的使用

    可以在遍历集合元素时移除集合中的元素,但不同于集合直接调用remove()

@Test
public void test2() {
    Collection coll = new ArrayList();
    coll.add(123);
    coll.add(456);
    coll.add(new String("Tom"));
    coll.add(false);
    coll.add(new Person("Jerry", 20));

    Iterator iterator = coll.iterator();
    while(iterator.hasNext()) {
        Object obj = iterator.next();
        if("Tom".equals(obj)) {
            iterator.remove(); // 当指针指向集合中“Tom”这个元素时,通过迭代器的remove()移除集合中的这个元素
                            // 如果没有调用过next()或是调用next()后又调用了remove(),又再次调用remove(),
                            // 这些情况都是指针没有指向正确的元素,会报IllegalStateException异常
        }
    }
    iterator = coll.iterator();
    while(iterator.hasNext()) {
        System.out.println(iterator.next()); // 没有“Tom”
    }
}

 

6,foreach循环遍历集合或数组

day23-集合_集合_02

@Test
public void test1() {
    Collection coll = new ArrayList();
    coll.add(123);
    coll.add(456);
    coll.add(new String("Tom"));
    coll.add(false);
    coll.add(new Person("Jerry", 20));

    // for(集合元素类型 局部变量 : 集合对象
    for(Object obj : coll) { // 从集合coll中依次取元素赋值给obj,实际是通过迭代器(hasNext()和next())完成的
        System.out.println(obj);
    }
}

@Test
public void test2() {
    int[] arr = new int[]{1,2,3,4,5,6};
    for(int i : arr) {
        System.out.println(i);
    }
}

@Test
public void test3() {
    String[] arr = new String[]{"MM", "MM", "MM"};
    // 普通for循环赋值,数组每个元素变为GG
//        for(int i = 0 ; i < arr.length ; i++) {
//            arr[i] = "GG";
//        }
    // foreach的方式,将数组的每个元素依次赋值给变量s,并不是改变数组元素arr[i]
    for(String s : arr) {
        s = "GG";
    }
    for(int i = 0 ; i < arr.length ; i++) {
        System.out.println(arr[i]); // MM MM MM
    }
}

 

7,List接口常用实现类的对比

day23-集合_集合_03

    面试题:ArrayList、LinkedList、Vector三者的异同
    JDK1.0时就有Vector,JDK1.2时出现List接口和它的两个实现类ArrayList、LinkedList,又将Vector加到List下
        相同点:都实现了List接口,存储的元素都是有序可重复的
        不同点:ArrayList作为List接口主要使用的实现类,因为它是线程不安全的,执行效率较高,底层使用Object[]的数组elementDate存储集合元素;而Vector是线程安全,效率低,所以不常用,底层使用Object[]的数组存储集合元素。LinkedList比较常用,底层使用双向链表存储集合元素,所以与使用数组的ArrayList相比,执行频繁的插入、删除操作时效率要高

 

8,ArrayList源码分析

    在JDK7中,ArrayList list = new ArrayList();调用空参构造器,接着调用另一个构造器创建了长度为10的Object类型数组elementDate

day23-集合_集合_04

    执行add操作时,先比较集合当中的元素个数(size())+1是否比数组长度大,如果是小则将元素添加进数组,否则进行扩容,一般情况下原来容量的1.5(右移一位相当于除以2)倍就是新容量的大小,接着将原来数组的元素复制到新数组

day23-集合_集合_05

    一般情况下使用ArrayList list = new ArrayList(20);的方式创建指定长度的集合
 
    JDK8中,ArrayList list = new ArrayList();调用空参构造器将数组elementDate设置为一个常量{},即一个为空的数组。执行第一次add操作时,先执行下面的方法进行判断,传入的参数是元素个数+1为1,if条件满足,minCapacity的值为默认值10

day23-集合_集合_06

    if满足,执行扩容操作,grow()就是上面的方法,最终通过Arrays.copyOf()创建了一个长度为10的数组

day23-集合_集合_07

    总结7和8中的ArrayList:区别在于new ArrayList()时调用空参构造器,7中直接创建了长度为10的数组,而8中并没有,等到第一次add操作时才创建长度为10的数组,后续的添加和扩容二者都一样。后者延迟了数组的创建,可以节省内存
 

9,LinkedList源码分析

    LinkedList list = new LinkedList();创建集合对象后,这个对象有两个Node类型的属性first和last,分别指向链表的头结点和尾结点,默认值为null。该集合中的一个元素就是一个Node对象,每个Node对象都有两个Node类型的属性,分别用来指向前一个Node对象和后一个Node对象,这样就构成了双向链表

day23-集合_集合_08

    当执行add操作时,调用linkLast方法,第一次add,创建了第一个结点(Node对象),该结点前后指向null,first和last都指向它;之后的add操作,创建一个新的结点,新结点向前指向原来链表的最后一个结点,向后指向null,first还是指向链表的头结点,last指向新结点,新结点变为尾结点,然后将原来链表的尾结点向后指向的null变为指向现在的尾结点。每次add都会使size+1

day23-集合_集合_09

 

10,Vector源码分析

    在JDK7和8中调用空参构造器都是创建长度为10的数组,和ArrayList不同的是扩容方式,它会扩容到原来的2倍。就算是涉及到线程安全的问题,还是会以别的方式使用ArrayList
 

11,List接口中的常用方法测试

    Collection接口中的常用方法已经测试过,这里是作为有序可重复的List接口中的方法

@Test
public void test1() {
    ArrayList list = new ArrayList();
    list.add(123);
    list.add(456);
    list.add("AA");
    list.add(new Person("Tom", 12));
    list.add(456);

    System.out.println(list); // [123, 456, AA, Person{name='Tom', age=12}, 456]

    // add(int index, Object ele):在索引为index的位置插入元素,对于ArrayList,是将index及其后所有元素向后移一位完成的
    list.add(1, "BB");
    System.out.println(list); // [123, BB, 456, AA, Person{name='Tom', age=12}, 456]
    // addAll(int index, Collection eles):从索引位置添加另一个集合中的所有元素
    List list1 = Arrays.asList(1, 2, 3);
    list.addAll(list1);
    // list.add(list1); // 这样就是将集合list1作为一个元素添加到list中
    System.out.println(list.size()); // 9

    // 获取指定位置元素
    System.out.println(list.get(0)); // 123

    // indexOf(Object obj):返回obj在集合中首次出现的位置,如果没有则返回-1
    int index = list.indexOf(456);
    System.out.println(index); // 2
    // 一个元素在集合中最后一次出现的位置,没有则返回-1
    System.out.println(list.lastIndexOf(456)); // 5

    // remove(int index):和Collection中的remove构成重载,在List中既可以通过元素内容删,也可以通过索引删
    Object obj = list.remove(0); // 数组中索引之后的每个元素向前移一位
    System.out.println(obj); // 123
    System.out.println(list); // [BB, 456, AA, Person{name='Tom', age=12}, 456, 1, 2, 3]

    // set(int index, Object ele):设置指定索引位置的元素
    list.set(1, "CC");
    System.out.println(list); // [BB, CC, AA, Person{name='Tom', age=12}, 456, 1, 2, 3]

    // subList(int fromIndex, int toIndex):返回从fromIndex到toIndex索引位置的左闭右开区间的子集合
    List subList = list.subList(0, 3);
    System.out.println(subList); // [BB, CC, AA],没有对list做修改
}

 

12,List遍历及方法总结

@Test
public void test2() {
    ArrayList list = new ArrayList();
    list.add(123);
    list.add(456);
    list.add("AA");

    // 方式一:Iterator迭代器
    Iterator iterator = list.iterator();
    while(iterator.hasNext()) {
        System.out.println(iterator.next());
    }
    // 方式二:foreach语句
    for(Object obj : list) {
        System.out.println(obj);
    }
    //方式三:for循环
    for(int i = 0 ; i < list.size() ; i++){
        System.out.println(list.get(i));
    }
}
    相比于数组,开发中更多使用List,很多的需求都在List中写好了
    增:add(Object obj)
    删:remove(int index) / remove(Object obj)
    改:set(int index, Object ele)
    查:get(int index)
    插:add(int index, Object ele)
    长度:size()
    遍历:Iterator迭代器,foreach语句,普通的循环
 

13,List的一个面试题

@Test
public void testListRemove() {
    List list = new ArrayList();
    list.add(1);
    list.add(2);
    list.add(3);
    updateList(list);
    System.out.println(list); // 1, 2
}
private void updateList(List list) {
    list.remove(2); // 2被当做索引,调用的是形参为int的remove方法
    // list.remove(new Integer(2)); // 此时调用的是形参为Object类型的remove方法,移除的就是元素2
}

 

14,Set接口实现类的对比

day23-集合_集合_10

day23-集合_集合_11

day23-集合_集合_12

    Set存储无序、不可重复的元素。HashSet作为Set接口主要使用的实现类,线程不安全,可以存储null值;LinkedHashSet作为HashSet的子类,可以按照元素添加的顺序遍历。TreeSet则可以按照指定元素的属性进行排序
 

15,Set的无序性和不可重复性的理解

    以HashSet为例,底层使用数组存储元素,无序性指的是添加元素时并不是按照数组的索引从小到大顺序添加的,而是根据元素的哈希值决定存储到数组的哪个位置;不可重复性指的是添加的元素按照equals()的判断,相同的元素只能存储一个

// Set接口中没有额外定义新的方法,即使用的都是Collection中的方法
@Test
public void test1() {
    Set set = new HashSet();
    set.add(456);
    set.add(123);
    set.add(123);
    set.add("AA");
    set.add("CC");
    set.add(new Person("Tom", 12));
    set.add(new Person("Tom", 12)); // 此时Person没有重写equals()或是重写了equals()
    set.add(129);

    Iterator iterator = set.iterator();
    while(iterator.hasNext()) {
        System.out.println(iterator.next()); // 输出顺序不是添加的顺序,但不论输出几次,顺序都不变,无序性不等于随机性
                                        // 会输出一个123、两个Person,这说明HashSet认为前者是相同的元素,而后者是两
                                        // 个不同的元素,即使后者重写了equals(),且Person中equals()的输出语句没有执行
                                        // ,说明根本就没有执行equals()。而只有当Person重写equals()和hashCode()后,
                                        // equals()才会被执行,也正是通过equals()判断添加的两个Person对象是相同的,
                                        // 此时只有输出一个Person
    }
}

 

16,HashSet中元素的添加过程

    当调用空参构造器创建一个HashSet时,在JDK7中创建了一个长度为16的数组,在8中则是等到第一次add操作时才创建,这和ArrayList相同
    添加一个元素时,调用该对象的hashCode()得到一个哈希值,先大概认为该值唯一的标识了这个对象,如果两个对象的属性一样,则二者的哈希值也一样,但两个对象哈希值一样不意味着两个对象相同(可能是两个类拥有相同的属性),需要equals()判断
    该哈希值通过散列函数决定了该对象在数组中的位置,如果这个位置没有元素就放在这个位置,对于不同位置的元素,它们一定不是重复的
    后续添加元素时,一定会出现要存放的位置已经有元素的情况(毕竟数组长度有限),此时要存放的元素和已存放的元素的哈希值可能相等也可能不相等,不相等则认为是不同的元素,这时就要以链表的方式存储该元素,在JDK7中要存放的元素放在数组中,指向原来的元素,8中由已存放的元素指向要存放的元素;哈希值相等时两个元素也不一定相同,此时通过equals()才能判断两个元素是否相同,不相同则需要以链表的方式添加到数组中
    这个过程也正说明了Set的无序性和不可重复性
    HashSet的底层使用数组+链表的结构;new HashSet实际上是new了一个HashMap

day23-集合_集合_13

 

17,关于hashCode()和equals()的重写

    当一个类没有重写hashCode()而又调用该方法时,调用的是Object的hashCode(),它相当于返回一个随机数,所以在上面当Person类没有重写该方法时,HashSet认为两个相同属性的Person对象是不同的,因为它们的哈希值不同
    重写hashCode()后,String重写过hashCode(),它保证两个相同的字符串返回的哈希值相同,所以当两个Person对象name、age属性相同时下面Person类中的hashCode()返回相同的哈希值。乘上的倍数31是有讲究的,既不能太大也不能太小,如果只是返回result+age,则更可能出现两个属性不同的Person对象算出的哈希值相同,此时就会通过equals()比较,最后用链表存放,而实际上希望链表越少越好,优先能存放到数组的其他位置是更好的

@Override
public int hashCode() {
    int result = name != null ? name.hashCode() : 0;
    result = 31 * result + age;
    return result;
}
    向Set中添加的元素,它的类一定要重写hashCode()和equals(),且这两个方法尽可能保持一致,即如果两个对象equal()判断为相等,则它们的哈希值要一样,若不相等,则哈希值要不一样;两个对象哈希值一样,则equals()判断要相等,若不一样,则equals()判断要不相等。为保证这一点,这两个方法中用到的属性尽量都一样,直接生成的这两个重写方法一般就保证了这一点
 

18,LinkedHashSet的使用

    LinkedHashSet与HashSet基本无差,二者在添加元素时都是通过哈希值和散列函数决定元素存放在数组中的位置,唯一区别是,LinkedHashSet在添加元素时,在这个元素中额外维护了两个引用,一个指向在它之前添加的那个元素,一个指向在它之后添加的一个元素,所以它才能按照元素的添加顺序遍历,且对于频繁的遍历操作,它的效率高于HashSet

@Test
public void test2() {
    Set set = new LinkedHashSet();
    set.add(456);
    set.add(123);
    set.add(123);
    set.add("AA");
    set.add("CC");
    set.add(new Person("Tom", 12));
    set.add(new Person("Tom", 12));
    set.add(129);

    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next()); // 输出的顺序就是添加元素的顺序,这并不是说明LinkedHashSet是有序的
    }
}

 

19,TreeSet的自然排序

// 定义Person类的比较规则,一个Person对象的name属性大于另一个,则这个对象大于另一个对象;当name属性相等时,比较age
@Override
public int compareTo(Object o) {
    if(o instanceof Person) {
        Person person = (Person)o;
        // return this.name.compareTo(person.name);
        int compare = this.name.compareTo(person.name);
        if(compare != 0) {
            return compare;
        }else {
            return Integer.compare(this.age, person.age);
        }
    }else {
        throw new RuntimeException("输入的类型有误");
    }
}
@Test
    public void test1() {
        TreeSet set = new TreeSet();

        // 添加元素操作报错,TreeSet可以按照指定元素的属性进行排序,它不能添加不同类的对象
//        set.add(123);
//        set.add(456);
//        set.add("AA");
//        set.add(new Person("Tom", 12));
        // 举例
//        set.add(34);
//        set.add(-34);
//        set.add(43);
//        set.add(11);
//        set.add(8);
        // Person类实现了Comparable接口
        set.add(new Person("Tom", 12));
        set.add(new Person("Jerry", 32));
        set.add(new Person("Jim", 2));
        set.add(new Person("Mike", 65));
        set.add(new Person("Jack", 33));
        set.add(new Person("Jack", 56)); // 当compareTo()中没有比较age属性时,这个元素没有添加,因为TreeSet通过compareTo()判断它和上一个元素是相同的
                                                // ,HashSet本质上通过equal()判断元素是否相同,这是TreeSet和HashSet的区别。当添加age属性的比较后,TreeSet就认为
                                                // 这是两个不同的元素

        Iterator iterator = set.iterator();
        while(iterator.hasNext()) {
            System.out.println(iterator.next()); // 按照元素定义的比较规则进行排序,比如Integer类型是从小到大的顺序输出
        }
    }

 

20,TreeSet的定制排序

@Test
public void test2() {
    Comparator com = new Comparator() {
        // 只按照age属性比较
        @Override
        public int compare(Object o1, Object o2) {
            if(o1 instanceof Person && o2 instanceof Person) {
                Person p1 = (Person)o1;
                Person p2 = (Person)o2;
                return Integer.compare(p1.getAge(), p2.getAge());
            }else {
                throw new RuntimeException("输入类型有误");
            }
        }
    };

    TreeSet set = new TreeSet(com); // 调用的是空参构造器时,按照Person类的compareTo()比较,传入一个Comparator后,按照它的compare()比较
    set.add(new Person("Tom", 12));
    set.add(new Person("Jerry", 32));
    set.add(new Person("Jim", 2));
    set.add(new Person("Mike", 65));
    set.add(new Person("Mary", 56));
    set.add(new Person("Jack", 56)); // 不会添加,因为和上一个元素的age相等

    Iterator iterator = set.iterator();
    while(iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}