java的集合类库分为两大模块,分别是Collection和Map.其中Collection是一切单值存储数据结构的集合,Map是一切键值对存储数据结构的集合.下面是我简单构造的组织结构图.

集合



Collection

常用的方法有:add(), remove(), clear(), contains(), isEmpty(), size()和toArray().


 List


有序存储的可扩容数组结构


  ArrayList

  LinkedList

  Vector

 Set


无序存储不容忍重复的数据结构


  HashSet

  TreeSet

  LinkedHashSet

Map


1.public v put(k key, v value)


2.public v remove(Object k)


3.public v get(Object k):获取指定键对应的值


4.public Set<k> keySet():获取Map集合中所有的键存储到Set集合中


5.public Set<Map, Entry<k, v>>

EntrySet():获取到Map集合的所有的键值对对象的集合(Set集合)


6.public boolean containKey(Object key):判断该集合中是否有此键


 HashMap

 LinkedHashMap

 TreeMap

这篇文章重点总结一下以下几个知识点:

1.ArrayList和Vector和LinkedList有什么区别,各自的优劣势?

在此引用以下这篇优秀的博客作为回答.我认为已经讲的我没法再多说什么了.
arrayList和vector的区别

2.List和Set的区别,各自的优劣势?

List和Set都是Collection下的子接口,List是以数组和链表作为基本数据结构,它允许存储重复的元素,并且元素有序.而Set是以HashMap的存储数据结构,即哈希表作为它的存储数据结构的,它的底层是基于HashMap的方法实现的.这个数据结构就复杂一些,是数组+链表+二叉树的存储结构.这样的设计使得Set存储数据变得无序,并且不可以存取重复的元素.所谓的重复,一般而言是指同一个对象,最直接的就是同一个地址.但是这个要求太高了,我们理解的重复要求没那么高,特别是我们需要存储自定义的对象的时候,只要两个对象的所有属性都相同,我们就认为是同一个对象.但如果这个时候不对equals和hashCode方法(Object就有的一个方法)进行重写,那么Set就不会认为这两个是同一个对象,还是能够存进去的.可以看以下栗子.
我们希望定义一个HashSet容器,用来存储人这个对象.这个对象有两个特征分别是名字和年龄,如果我们不重写equals()方法的话,我们输入两个特征完全相同的人,看这个容器是否能够存进去.

package HashSetDemo;

import java.util.Objects;

public class Person{
    private String name;
    private int age;

    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 boolean equals(Object o) {
//        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);
//    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
package HashSetDemo;

import java.util.HashSet;
import java.util.Set;

public class HashSetDemo {
    public static void main(String[] args) {
       Set<Person> all = new HashSet<Person>();
       all.add(new Person("张三", 15));
       all.add(new Person("李四", 24));
       all.add(new Person("李四", 24));
       all.add(new Person("王五", 12));

        System.out.println(all);
    }
}

输出结果如下:

javacv AVFrame 保存图片_javacv AVFrame 保存图片


我们发现这两个李四被同时输出了,这就说明了在存入对象的时候set并不认为这两个是同一个对象,这就是因为没有重写equals的缘故.

看一下equals的源码,==比较的就是二者的地址是否相同

javacv AVFrame 保存图片_java_02


所以我们需要重写equals方法,使得java认为两个对象只要各个属性都相同就是同一个对象.按住Alt + Insert,选择equals() and hashCode(),把所有的属性都选中再点击OK.就能够自动重写这个equals()方法了.

@Override
    public boolean equals(Object o) {
        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);
    }

添上这段代码到上面以后,就能够去掉所谓的重复对象了.

这两种接口很难评价孰优孰劣,或者根本不存在优劣问题,要根据具体的应用场景进行选择.如果你比较重视元素存储的顺序,希望做到随时可以查询,那么List是一个不错的选择.比如你想对公司的员工进行一个工资的统计,那我们自然希望同一个部门的员工的信息都能在一起做个比较.这个只作为一次性的统计报告,所以修改删除意义不大,因为基本上两三年后这个报告就失去了其本身的意义,是即时性的查询报告.如果你更关心元素的唯一性,不考虑元素的存储顺序,比如你要对人员进行身份信息登记,那么身份证号便是唯一的,你的工作重心仅在于前面录入身份的工作量,而查询的机会不多,即使查询性能低下,也不会影响你的工作,那么使用Set存储应该是个不错的选择.对Set而言,查询能力是伴随着数据量的增加而越好的,但它毕竟是以链表和树作为基本数据存储结构,还是会比数组要差些,但是录入,删除都非常的方便.

3.我想了解Set的几个实现类的联系和区别,它们的特点都是怎么做到的?

首先,Set它下面也分为三个实现类,分别是HashSet, TreeSet和HashLinkedSet.从Hash这里就可以看出它们跟Hash表的关系了.HashSet是根据对象的哈希值来确定对象的存储位置,它不允许重复的元素,TreeSet基本和HashSet一样,但是TreeSet会对元素进行自动排序,排序规则如果不重写就是按照字典序排序.LinkedHashSet和HashSet的区别在于LinkedHashSet是采用的双向链表+哈希表,而后者是单向链表+哈希表,采用双向链表的好处就是可以随时查询遍历,LinkedHashSet和HashSet在表现方式上的唯一区别就在于LinkedHashSet能够保证存入顺序和遍历顺序一致,也就是保持顺序,至于为什么能够如此我还不是很清楚.可能要去LinkedHashMap源码那里去研究了,但应该有人总结过.在此先留个坑,以后补.

面试官:从源码分析一下TreeSet(基于jdk1.8)

3.HashSet底层存储实现:

HashSet底层是通过HashMap的方法实现的.所以研究HashSet的存储实际上就是在研究HashMap的存储.

下面这张图我在很多地方都看到过,不得不说总结的非常好,让人一看就明白.

javacv AVFrame 保存图片_javacv AVFrame 保存图片_03


结合这张图再打开hashSet的源码,理解起来会比较容易.

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

首先我们了解以下这个哈希表结构.
一般来说,我们的哈希表的默认长度是16,负载因子为0.75.它的意思是如果有16 * 0.75 = 12个哈希表的位置被占有,就会自动扩容1.5倍(似乎).而且这种扩容机制因情况而异,记不太清楚了.任何一个对象根据它的hashCode()方法获得一个hash码,这个hash码对哈希表长度求余,就会得到1 ~ 哈希表长度-1的一个数,对应到哈希表的位置,每个位置我们也称作哈希桶.但是容易出现两个对象的存在位置相同的情况,这种情况下我们就要用到链表结构了,它会在哈希桶这个位置后面接上一个链表结点,此时这个哈希桶就可以看作存储链表结点的一维数组.
但是如果相同位置的元素越来越多,链表查询效率又不是很给力,这个时候链表结构就会自动转型成为红黑树结构.红黑树的性质自查.它是一种特殊的二叉搜索树,本质上的特征就是这个二叉搜索树采用了二分法去遍历查找元素,而且为了避免在一个结点的左,右子树深度太高,红黑树结构就会保证了当一个方向的结点深度太高时就会进行旋转,以保证无论哪个子树的最大深度都不会超过另一个子数的最大深度+1.这个过程也不复杂,建议看一下这篇文档.了解一下二叉搜索树的旋转.再配合B站黑马程序员的解说会更香哦.如果数据还是太多,超过了桶的容量,就会对桶进行扩容.
4.关于TreeSet的自动排序机制是怎么做到的?
TreeSet可以实现对所存入的元素进行自动排序,而且这个排序是可以根据用户的意愿设计的.其实它的内部是所存的对象实现了Comparable接口,才可以.换言之,如果你自定义的对象没有实现comparable接口,那么TreeSet是不能够对对象进行排序的.comparable和comparator接口都能够实现排序的机制,区别在于comparable只能复写一次compareTo()方法,而comparator可以复写多次compare()方法.这对于想要实现一个对象的多种排序机制来说是不错的.所以我们可以这么说,comparator是好于comparable的.
关于 TreeSet 的排序实现,如果是集合中对象是自定义的或者说其他系统定义的类没有实现 Comparable 接口,则不能实现 TreeSet 的排序,会报类型转换(转向 Comparable 接口)错误。 换句话说要添加到 TreeSet 集合中的对象的类型必须实现了 Comparable 接口。 不过 TreeSet 的集合因为借用了 Comparable 接口,同时可以去除重复值,而 HashSet 虽然是 Set 接口子类,但是对于没有复写 Object 的 equals 和 hashCode 方法的对象,加入了 HashSet 集合中也是不能去掉重复值的.
可以看一下这个栗子:
同样是之前的Person对象,只不过多了一个实现Comparable接口,否则TreeSet打印会报错.

package TreeSetDemo;

import java.util.Objects;

public class Person implements Comparable<Person>{
    private String name;
    private int age;

    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 boolean equals(Object o) {
        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);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(Person o) {
        if(this.age > o.age){
            return 1;
        }else if(this.age < o.age){
            return -1;
        }else{
            return 0;
        }
    }
}

再在main方法里存入这几个元素,并且输出这个treeset看一下输出的结果.

package TreeSetDemo;

import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

public class TreeSetDemo {
    public static void main(String[] args) {
       Set<Person> all = new TreeSet<>();
       all.add(new Person("张三", 15));
       all.add(new Person("李四", 24));
       all.add(new Person("赵六", 24));
       all.add(new Person("王五", 12));


       System.out.println(all);
    }
}

他的结果是:

javacv AVFrame 保存图片_数据结构_04


可以看到,虽然年龄的确是按照我们重写的compareTo()方法排序了,但是却少输出了一个对象赵六,这是因为java把具有和李四相同年龄的赵六认为是同一个对象了.TreeSet集合不会存储经compareTo()方法返回值为0的元素.也就是TreeSet认为是相等的元素.但我们心里知道只是年龄相等的对象不能认为就是相等的对象.所以,这里需要重写一下compareTo()方法,使得对于字符串对象继续排序.

@Override
    public int compareTo(Person o) {
        if(this.age > o.age){
            return 1;
        }else if(this.age < o.age){
            return -1;
        }else{
            return this.name.compareTo(o.name);
        }
    }

将重写后的compareTo()方法写入Person类中,再重新执行,就得到了我们想要的结果.

javacv AVFrame 保存图片_java_05

未完待续.
另外我在后台上传了本篇文章相关的此文档,还有自己手绘的思维导图,这里显示不出来,可以通过我的主页上传的资源下载.