面试必问的集合-1.1:Set、List

ArrayList 排序

ArrayList 是一种 List 实现,它的内部用一个动态数组来存储元素,

因此 ArrayList 能够在添加和移除元素的时候进行动态的扩展和缩减。

java中实现对list的自定义排序主要通过两种方式

1)让需要进行排序的对象的类实现Comparable接口,重写compareTo(T o)方法,在其中定义排序规则,那么就可以直接调用Collections.sort()来排序对象数组

public class Student implements Comparable{

  @Override

  public int compareTo(Object o) {

  }

}

2)实现比较器接口Comparator,重写compare方法,直接当做参数传进sort中

Collections.sort(list, new Comparator<Student>() {
      @Override
      public int compare(Student o1, Student o2) {
        if(o1.getAge() >= o2.getAge()) {
          return 1;
        }
        else {
          return -1;
        }
      }
    });

ArrayList和LinkedList的区别

ArrayList和Vector使用了数组的实现,可以认为ArrayList或者Vector封装了对内部数组的操作,比如向数组中添加,删除,插入新的元素或者数据的扩展和重定向。

LinkedList使用了循环双向链表数据结构。

与基于数组ArrayList相比,这是两种截然不同的实现技术,这也决定了它们将适用于完全不同的工作场景。

LinkedList是个双向链表,它同样可以被当作栈、队列或双端队列来使用。

LinkedList链表由一系列表项连接而成。一个表项总是包含3个部分:元素内容,前驱表和后驱表,如图所示:

我们知道,通常情况下,ArrayList和LinkedList的区别有以下几点:

1. ArrayList是实现了基于动态数组数据结构,而LinkedList基于链表的数据结构;

2. 对于随机访问getsetArrayList要优于LinkedList,因为LinkedList要移动指针;

3. 对于添加和删除操作add和remove,一般大家都会说LinkedList要比ArrayList快,因为ArrayList要移动数据。

 

Arraylist:底层是基于动态数组,根据下标随机访问数组元素的效率高,向数组尾部添加元素的效率高;

但是,删除数组中的数据以及向数组中间添加数据效率低,因为需要移动数组。

例如最坏的情况是删除第一个数组元素,则需要将第2至第n个数组元素各向前移动一位。

而之所以称为动态数组,是因为Arraylist在数组元素超过其容量大,Arraylist可以进行扩容(针对JDK1.8  数组扩容后的容量是扩容前的1.5倍),

 

Linkedlist基于链表的动态数组,数据添加删除效率高,只需要改变指针指向即可,但是访问数据的平均效率低,需要对链表进行遍历。

 

总结:

1、对于随机访问get和set,ArrayList觉对优于LinkedList,因为LinkedList要移动指针。 

                 对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。

2、各自效率问题:

 

Set集合

HashSet

HashSet其实就是基于HashMap实现, 因此,熟悉了HashMap, 再来看HashSet的源码,会觉得极其简单。

没错,HashSet就是通过HashMap保存数据, HashSet的值就是HashMap的key。

下面还是直接看源码吧:

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;

    //HashMap ? 没错,HashSet就是通过HashMap保存数据, HashSet的值就是HashMap的key
    private transient HashMap<E,Object> map;
    
    //HashMap 为<key, value>的键值对, 既然HashSet的值就是HashMap的key, 那么HashMap的值呢,当然就是这个PRESENT啦
    private static final Object PRESENT = new Object();
    
    //下面这一系列的构造方法都是创建HashMap, 之前已经介绍过HashMap, 这儿就不再详说了
    public HashSet() {
        map = new HashMap<>();
    }

    //将一个已知的collection转换为HashSet
    public HashSet(Collection<? extends E> c) {
        //这儿的HashMap的参数为什么这么写?
        //上次介绍HashMap的时候可知,如果没有指定HashMap的capacity, 那么默认的就是16
        //根据 threshold = capacity * loadFactor, 可以计算出 capacity
        //Math.max((int) (c.size()/.75f) + 1, 16) 这个意思就是capacity如果没超过16, 那么就直接使用默认的16
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        //将已知的collection转换为HashSet的方法
        //addAll方法是HashSet的父类AbstractCollection的方法,为了便于阅读,会将代码粘贴在下面
        addAll(c);
    }

    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }


    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }


    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
    
    //addAll方法是HashSet的父类AbstractCollection的方法
    public boolean addAll(Collection<? extends E> c) {
        boolean modified = false;
        for (E e : c)
            //此处的add方法由HashSet重写实现
            if (add(e))
                modified = true;
        return modified;
    }
    
    //HashSet的核心方法来了, 没错,就这么简单
    public boolean add(E e) {
        //应证了上面所说的key为HashSet的值
        return map.put(e, PRESENT)==null;
    }
    
    //剩下这些方法都是跟Map相关的了,只要熟悉了HashMap, 那就太简单了,就不说了
    public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }

    public void clear() {
        map.clear();
    }
    
}

1. HashSet基于HashMap实现, 以HashSet的值作为HashMap的一个key, 以一个Object对象常量作为HashMap的值。

2. 根据HashMap的特性,可以推敲出:HashSet允许拥有1个为null的值, HashSet的值不可重复。

3. 在创建HashSet的时候,如果合适,最好指定其内部HashMap的 capacity和loadFactory的值, 至于原因,在介绍HashMap的时候,提到过。

如果没有指定HashMap的capacity, 那么默认的就是16

OK, 讲完HashSet之后,我觉得是时候提一下这个问题了: 可能在大家初学java的时候,老师或者书上都推荐大家在重写对象equals的时候,最好重写一下hashCode方法,还记得吧? 为什么要这么做? 给大家演示一下,你就能明白了,下面看一个小demo:

先定义一个Person类:

public class Person {

    //身份证
    private String idCard;
    
    private String name;
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public String getIdCard() {
        return idCard;
    }

    public void setIdCard(String idCard) {
        this.idCard = idCard;
    }

    //重写equals方法(规则是:idCard一致,则认为是同一个人)
    @Override
    public boolean equals(Object obj) {
        if(obj == this) {
            return true;
        }
        if(!(obj instanceof Person)) {
            return false;
        }
        Person others = (Person) obj;
        if(others.getIdCard().equals(idCard)) {
            return true;
        }
        return false;
    }
}

 

然后,写一个测试类,用HashSet去添加Person实例:

public class Test {

    public static void main(String[] args) {
        
        Person p1 = new Person();
        p1.setIdCard("1234567890");
        
        Person p2 = new Person();
        p2.setIdCard("1234567890");
        
        Set<Person> hashSet = new HashSet<Person>();
        hashSet.add(p1);
        hashSet.add(p2);
        
        System.out.println(hashSet.size());
        
    }
    
}

我们知道HashSet的元素不可重复,因此,在以上测试代码中,p1 p2对象是equals的,我们本来希望HashSet只保存其中一个对象, 但是,事与愿违,输出的结果却是2 说明hashSet把这两个对象都保存了。这是为什么呢? 我们结合一下HashMap来看吧, 首先,由于我们没有重写PersonhashCode方法,会导致p1 p2hash值不一致,这时, HashMap会把hash不一致的元素放在不同的位置, 因此就产生了两个对象。那么,怎么改善? 当然是重写hashCode方法了。下面,我们在Person类中,重写hashCode方法:

    @Override
    public int hashCode() {
        return this.idCard.hashCode() * 11;
    }

这时候,我们再用上面的测试类测试,发现输出为1。OK,终于和我们的想法一致了。

这就是为什么强烈推荐在重写equals方法的时候,同时重写hashCode方法的原因之一。

 

TreeSet

TreeMap 的实现就是红黑树数据结构,也就说是一棵自平衡的排序二叉树,这样就可以保证当需要快速检索指定节点。

TreeSet 和 TreeMap 的关系

为了让大家了解 TreeMap 和 TreeSet 之间的关系,下面先看 TreeSet 类的部分源代码:

public class TreeSet<E> extends AbstractSet<E> 
    implements NavigableSet<E>, Cloneable, java.io.Serializable 
 { 
    // 使用 NavigableMap 的 key 来保存 Set 集合的元素
    private transient NavigableMap<E,Object> m; 
    // 使用一个 PRESENT 作为 Map 集合的所有 value。
    private static final Object PRESENT = new Object(); 
    // 包访问权限的构造器,以指定的 NavigableMap 对象创建 Set 集合
    TreeSet(NavigableMap<E,Object> m) 
    { 
        this.m = m; 
    } 
    public TreeSet()                                      // ①
    { 
        // 以自然排序方式创建一个新的 TreeMap,
        // 根据该 TreeSet 创建一个 TreeSet,
        // 使用该 TreeMap 的 key 来保存 Set 集合的元素
        this(new TreeMap<E,Object>()); 
    } 
    public TreeSet(Comparator<? super E> comparator)     // ②
    { 
        // 以定制排序方式创建一个新的 TreeMap,
        // 根据该 TreeSet 创建一个 TreeSet,
        // 使用该 TreeMap 的 key 来保存 Set 集合的元素
        this(new TreeMap<E,Object>(comparator)); 
    } 
    public TreeSet(Collection<? extends E> c) 
    { 
        // 调用①号构造器创建一个 TreeSet,底层以 TreeMap 保存集合元素
        this(); 
        // 向 TreeSet 中添加 Collection 集合 c 里的所有元素
        addAll(c); 
    } 
    public TreeSet(SortedSet<E> s) 
    { 
        // 调用②号构造器创建一个 TreeSet,底层以 TreeMap 保存集合元素
        this(s.comparator()); 
        // 向 TreeSet 中添加 SortedSet 集合 s 里的所有元素
        addAll(s); 
    } 
    //TreeSet 的其他方法都只是直接调用 TreeMap 的方法来提供实现
    ... 
    public boolean addAll(Collection<? extends E> c) 
    { 
        if (m.size() == 0 && c.size() > 0 && 
            c instanceof SortedSet && 
            m instanceof TreeMap) 
        { 
            // 把 c 集合强制转换为 SortedSet 集合
            SortedSet<? extends E> set = (SortedSet<? extends E>) c; 
            // 把 m 集合强制转换为 TreeMap 集合
            TreeMap<E,Object> map = (TreeMap<E, Object>) m; 
            Comparator<? super E> cc = (Comparator<? super E>) set.comparator(); 
            Comparator<? super E> mc = map.comparator(); 
            // 如果 cc 和 mc 两个 Comparator 相等
            if (cc == mc || (cc != null && cc.equals(mc))) 
            { 
                // 把 Collection 中所有元素添加成 TreeMap 集合的 key 
                map.addAllForTreeSet(set, PRESENT); 
                return true; 
            } 
        } 
        // 直接调用父类的 addAll() 方法来实现
        return super.addAll(c); 
    } 
    ... 
 } 

从上面代码可以看出,TreeSet 的 ① 号、② 号构造器的都是新建一个 TreeMap 作为实际存储 Set 元素的容器,

而另外 2 个构造器则分别依赖于 ① 号和 ② 号构造器,由此可见,TreeSet 底层实际使用的存储容器就是 TreeMap

 

与 HashSet 完全类似的是,TreeSet 里绝大部分方法都是直接调用 TreeMap 的方法来实现的,这一点读者可以自行参阅 TreeSet 的源代码,此处就不再给出了。

 

对于 TreeMap 而言,它采用一种被称为“红黑树”的排序二叉树来保存 Map 中每个 Entry —— 每个 Entry 都被当成“红黑树”的一个节点对待。例如对于如下程序而言:

public class TreeMapTest 
 { 
    public static void main(String[] args) 
    { 
        TreeMap<String , Double> map = new TreeMap<String , Double>(); 
        map.put("ccc" , 89.0); 
        map.put("aaa" , 80.0); 
        map.put("zzz" , 80.0); 
        map.put("bbb" , 89.0); 
        System.out.println(map); 
    } 
 } 

当程序执行 map.put("ccc" , 89.0); 时,系统将直接把 "ccc"-89.0 这个 Entry 放入 Map 中,这个 Entry 就是该“红黑树”的根节点。接着程序执行 map.put("aaa" , 80.0); 时,程序会将 "aaa"-80.0 作为新节点添加到已有的红黑树中。

以后每向 TreeMap 中放入一个 key-value 对,系统都需要将该 Entry 当成一个新节点,添加成已有红黑树中,通过这种方式就可保证 TreeMap 中所有 key 总是由小到大地排列。例如我们输出上面程序,将看到如下结果(所有 key 由小到大地排列):

 {aaa=80.0, bbb=89.0, ccc=89.0, zzz=80.0}

 

TreeSet和HashSet的区别

HashSet是通过HashMap实现的,TreeSet是通过TreeMap实现的,只不过Set用的只是Map的key。

 

HashSet无序

TreeSet有序

二者里边不能有重复的对象

 

HashMap与HashSet的区别

先了解一下HashMap跟HashSet

HashSet:

HashSet实现了Set接口,它不允许集合中出现重复元素。当我们提到HashSet时,第一件事就是在将对象存储在

HashSet之前,要确保重写hashCode()方法和equals()方法,这样才能比较对象的值是否相等,确保集合中没有

储存相同的对象。如果不重写上述两个方法,那么将使用下面方法默认实现:

public boolean add(Object obj)方法用在Set添加元素时,如果元素值重复时返回 "false",如果添加成功则返回"true"

 

HashMap:

HashMap实现了Map接口,Map接口对键值对进行映射。Map中不允许出现重复的键(Key)。Map接口有两个基本的实现

TreeMap和HashMap。TreeMap保存了对象的排列次序,而HashMap不能。HashMap可以有空的键值对(Key(null)-Value(null))

HashMap是非线程安全的(非Synchronize),要想实现线程安全,那么需要调用collections类的静态方法synchronizeMap()实现。

public Object put(Object Key,Object value)方法用来将元素添加到map中。

 

HashMap相对于HashSet较快,因为它是使用唯一的键获取对象

这句话我觉得有问题。因为hashset的hash判断跟hashmap是完全一样的,其底层就是hashmap,只是自己仅仅存一个key,不用像hashmap一样还要存value。慢是是因为hashset是对hashmap的封装吧?

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值