探索集合框架

今晚和往常一样在论坛上寻找着自己需要的知识,突然看到有人问到当初困扰自己很久的问题,这时我意识到把自己懂得的知识分享给大家和大家一起进步才是一个小菜鸟应该做的!PS:我是一个初中毕业的小菜,可能在描述方面很朴素,但是我会尽最大的努力去描述自己懂的知识。


集合框架

集合框架作为java最开始接触到东西,现在回过头去研究其内部如何运作是非常有必要的!

Iterator

迭代器可以遍历集合框架里面所有的集合是非常重要和方便的,但里面也有很多需要注意的地方,迭代器作为一个接口,从图中可以看到Collection总接口继承该接口也就是说所有的集合都可以用迭代器进行遍历,而该接口中有一个最重要方法Iterator iterator();该方法返回T 类型的元素上进行迭代的迭代器。

可能遇到的异常:

java.util.ConcurrentModificationException。

    public void test(){
        List<String>list=new ArrayList<String>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        Iterator<String> it = list.iterator();
        while(it.hasNext()){
            String value=it.next();
            if("c".equals(value)){
                list.add("error");
            }
            System.out.println(value);
        }
    }

该测试方法会抛出 java.util.ConcurrentModificationException异常;
首先在 AbstractList类中有个属性:protected transient int modCount = 0;
这里我们只讨论modCount属性对于AbstractList类 我们后面再讨论。
modCount这个属性是用记录操作集合的计数器;
以ArrayList为例

添加对象

 private void ensureExplicitCapacity(int minCapacity) {
        //计数器进行了自增
        modCount++;
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

移除对象

public E remove(int index) {
        rangeCheck(index);
        //计数器进行了自增
        modCount++;
        E oldValue = elementData(index);
        int numMoved = size - index - 1;
            if (numMoved > 0){
                System.arraycopy(elementData, index+1,                      elementData, index, numMoved);
            }
            elementData[--size] = null; 
            return oldValue;
}

Iterator的hasNext()和next()方法,Iterator的实现在AbstractList类中;

判断是否有元素方法

        public boolean hasNext() {
        //cursor集合里面下一个对象的下标
            return cursor != size();
        }

获取下一个元素方法

        public E next() {
            //检查该集合被修改没有
            checkForComodification();
            try {
                int i = cursor;
                E next = get(i);
                lastRet = i;
                //集合里面下一个对象的下标进行自增,用于hasNext()方法判断是否还有下一条数据;
                cursor = i + 1;
                return next;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

检查集合是否被修改方法

        final void checkForComodification() {
       //这里发现了modCount的作用了,expectedModCount是调用iterator()方法初始化时获取 modCount的值;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    public Iterator<E> iterator() {
        return new Itr();
    }
    //Itr类片段
     private class Itr implements Iterator<E> {
        int cursor = 0;
        int lastRet = -1;
        //将modCount的值赋值给expectedModCount 
        int expectedModCount = modCount;
        }

到这里大概有个了解吧,当一个集合获取一个迭代器的时候,该集合的modCount的值将会赋值给expectedModCount,当迭代器进行遍历集合的时候会检查expectedModCount的值是否和modCount相等,假如不相等将会抛出异常;所以在迭代的时候修改集合,注意是对集合,不是对集合里面的对象喔,将会抛出异常。

Iterator和foreach的关系:

    public static void test(){
        List<String>list=new ArrayList<String>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        Iterator<String> it = list.iterator();
        //foreach循环
        for(String value:list){
            System.out.println(value);
        }
        //foreach底层实现也是采用迭代器的
        while(it.hasNext()){
            String value =it.next();
            System.out.println(value);
        }       
    }

foreach循环说白了就是对迭代器进行了一次封装,让代码更加简洁更不容易出错,但是它们的remove()方法还是有区别的。

    public static void test(){
        List<String>list=new ArrayList<String>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        Iterator<String> it = list.iterator();
        //foreach
        for(String value:list){
            if(value.equals("a")){              
                list.remove(value);//调用了该方法后,进行下一次迭代  就会抛出ConcurrentModificationException异常
            }
        }
        //Iterator
        System.out.println(list.size()); //长度为4
        while(it.hasNext()){
            String value =it.next();
            if(value.equals("a")){
                it.remove();
            }
        }
        System.out.println(list.size());  //长度为3,说明正确删除掉了。
    }

ArrayList集合 remove()方法源码:

    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);//调用删除方法
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

   private void fastRemove(int index) {
        modCount++;           //操作计数器进行自增
        int numMoved = size - index - 1;
        if (numMoved > 0){
            System.arraycopy(elementData, index+1, elementData, index,numMoved);
        }
        elementData[--size] = null; 
    }

Iterator remove()方法源码

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
                //检查expectedModCount与modCount是否相等
            checkForComodification();

            try {
                //调用删除方法
                AbstractList.this.remove(lastRet);
                if (lastRet < cursor)
                    cursor--;
                lastRet = -1;
                //这里很关键,expectedModCount被重新赋值
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                throw new ConcurrentModificationException();
            }
        }

这里大概明白了把当调ArrayList的remove()时没有对expectedModCount值进行改变的操作,而Iterator的remove()会有一个对expectedModCount赋值的操作。遍历的时候checkForComodification()会检查expectedModCount与modCount是否相等,这里也就是抛出异常的地方。

Collection

1、collection接口继承Iterable接口也就是迭代器接口。
2、collection定义了一些列方法:注意该方法是Iterable接口的喔。
3、简单的来说:Deque接口为队列、List接口可重复的集合、set接口不可重复的集合,对于更加详细的我们接下来一起慢慢做研究。

AbstractCollection

该抽象类实现了Collection接口和Iterable接口里面的方法,是AbstractList(list接口实现抽象类)和AbstractSet(set接口实现抽象类)的父类。

AbstractList子类:ArrayList、LinkedList、Vector 。

ArrayList和LinkedList、Vector 的区别:
1、ArrayList底层是基于数组是线性的,优点:随机访问速度很快。缺点:当插入删除元素时较为费时。
2、LinkedList是基于链表的,优点:插入删除元素时速度快。缺点:随机访问的速度较慢。
3、Vector 和ArrayList几乎一样,但Vector是线程安全,默认的扩容方式是原来的2倍,ArrayList是原来的1.5倍。

ArrayList就是一个动态的数组有下标的概念,当知道第一个元素的位置就知道其他元素的位置了所以随机访问的数据很快,但就因为它的下标导致它每次增加或删除元素后下标都需要挪动位置,ArrayList用作查询最好,ArrayList在多线程环境下需要注意同步;
LinkedList就是一个双向循环链表,内部有个Entry类,里面包括前一个元素和后一个元素的地址,这样增加或删除元素后就不要重写排列速度很快,但随机访问就需要从第一个开始查找速度简直慢爆了;
Vector几乎和ArrayList一样,在多线程环境下建议使用。

ArrayList比较重要的知识

ArrayList默认初始化大小?

// ArrayList带容量大小的构造函数。    
     public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+                                         initialCapacity);
        }
    }

// ArrayList无参构造函数。(jdk1.7)    
    public ArrayList() {
    //DEFAULTCAPACITY_EMPTY_ELEMENTDATA是空数组
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
// ArrayList无参构造函数。(jdk1.7以前不传入容量默认容量为10)
    public ArrayList() {
        this(10);
    }  

超级数组是如何动态变大的?

在arrayList中有两个最为重要的变量:
//存放元素的数组
 transient Object[] elementData;
 //表示元素的个数
  private int size;

添加元素方法如下:

     //调用顺序:1
    public boolean add(E e) {
    //动态添加的秘密在这里面喔!
        ensureCapacityInternal(size + 1);  
        elementData[size++] = e;
        return true;
    }

用元素的个数加一进行容量验证。

    //调用顺序:2
   private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }

当素组为长度为0时,默认长度为DEFAULT_CAPACITY(10)和minCapacity中最大的值。

      //调用顺序:3
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

操作计数器自增(用于迭代器遍历判断使用),minCapacity 容量大于elementData长度将继续容量验证。

     //调用顺序:4
    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        //oldCapacity + (oldCapacity >> 1)可以认为是1.5倍
        //jdk1.7以前采用的是int newCapacity = (oldCapacity * 3)/2 + 1;
        //jdk1.7采用位运算比以前的计算方式更快。
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
      //jdk1.7这里增加了对元素个数的最大个数判断,jdk1.7以前是没有最大值判断的,MAX_ARRAY_SIZE 为int最大值减去8
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
            //最重要的复制元素方法
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

minCapacity容量比elementData长度乘以1.5还要大的话,将minCapacity赋值给newCapacity,继续对newCapacity和MAX_ARRAY_SIZE(int最大值减去8)进行比较,大于MAX_ARRAY_SIZE该值的话,将用minCapacity值去和MAX_ARRAY_SIZE比较如果还大于,就将会得到int最大的值作为容量,然后进行数据拷贝。


    //复制元素方法(  original - 要复制的数组
    //newLength - 要返回的副本的长度
    //newType - 要返回的副本的类)
        public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
    //确定数组类型class,(newType.getComponentType()方法是获取数组类型的Class)   
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
            //调用系统的copy方法
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    } 
    //复制元素最后的方法(该方法被native修饰,是由外部实现无法看到源码)
    //但是根据参数的含义,我们也大概只要这个方法要做什么。
    //src - 源数组。 
    //srcPos - 源数组中的起始位置。 
    //dest - 目标数组。 
    //destPos - 目标数据中的起始位置。 
    //length - 要复制的数组元素的数量。 
     public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos,int length);

添加一堆数据方法。

    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        //只需要注意这里,判断的容量是:原本元素的个数和插入个数相加。
        ensureCapacityInternal(size + numNew);  
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

LinkedList比较重要的知识

什么是线性结构、非线性结构、栈、队列?

线性结构:常用的一种数据结构,线性结构的特点是结构中的元素之间满足线性关系,按这个关系可以把所有元素排成一个线性序列。
非线性结构:指在该类结构中至少存在一个数据元素,它具有两个或者两个以上的前驱或后继。
线性结构包括:数组、栈、队列;非线性结构包括:树、图。   
栈:先进后出,只能在一端进行数据操作,类似于向一个篮子里面扔衣服永远都是先拿最上面的衣服。
队列:先进先出,只能在一端进行增加一端进行删除,类似于排队买票,先排的人先买到票。

LinkedList简单实现

/**
 * LinkedList 简单版本
 * @author orange
 *
 */
public class StackTest3 {
    public int size; //集合个数
    public NodeTest headNode; //第一个节点
    public NodeTest tailNode; //最后一个节点

    //用于存放节点的对象
    class NodeTest{
        Object object; //存放具体的数据
        NodeTest next; //下一个对象
        NodeTest previa;//上一个对象

        NodeTest(NodeTest previa,Object object,NodeTest next){
            this.object=object;
            this.next=next;
            this.previa=previa;
        }
    }

    //添加尾部
    public void addTail(Object object){
        //创建一个节点对象
        NodeTest node=new NodeTest(null,object,null);
        if(isNull()){
            //当链表为空时,头指针和尾指针都为新创建的节点。
            headNode=tailNode=node;
        }else{
            //将新节点设置为最后一个节点的下一个节点。
            tailNode.next=node;
            //将新节点的上一个节点为最后一个节点。
            node.previa=tailNode;
            //尾指针指向新节点。
            tailNode=node;
        }
        //节点个数加1
        size++;
    }
    //添加头部
    public void addHead(Object object){
        //创建一个节点对象
        NodeTest node=new NodeTest(null,object,null);
        if(isNull()){
            //当链表为空时,头指针和尾指针都为新创建的节点。
            headNode=tailNode=node;
        }else{
            //将新节点的下一个节点设置为第一个节点。
            node.next=headNode;
            //将第一个节点的上一个设置为新节点。
            headNode.previa=node;
            //头指针指向新节点。
            headNode=node;
        }
        //节点个数加1
        size++;
    }

    //删除尾部
    public Object deleteTail(){
        if(isNull()){
            //链表为空时,返回null
            return null;
        }
        NodeTest temp = tailNode;
        if(size==1){
            //链表只有一个节点的话,将头尾指针设置为空。
            tailNode=headNode=null;
        }else{
            //将尾指针指向最后一个节点的上一个节点。
            tailNode=temp.previa;
            //取消对最后一个节点的引用
            tailNode.next=null;
        }
        //节点个数减1
        size--;
        return temp;
    }

    //删除头部
    public Object deleteHead(){
        if(isNull()){
            //链表为空时,返回null
            return null;
        }
        NodeTest temp =headNode;
        if(size==1){
            //链表只有一个节点的话,将头尾指针设置为空。
            headNode=tailNode=null;
        }else{
            //头指针指向第二个节点。
            headNode=temp.next;
            //取消对第一个节点的引用
            headNode.previa=null;
        }
        //节点个数减1
        size--;
        return temp;
    }

    //查看头部
    public Object showHead(){
        return headNode.object;
    }


    //查看尾部
    public Object showTtai(){
        return tailNode.object;
    }
    //根据下标获取值
    public Object getindex(int index){
        if(isNull()||index>=size||index<0){
            return null;
        }
        NodeTest value=null;
        if(index<=size/2){//将长度一分为二,根据接近的那一半来开始循环。
            value=headNode;
            int flag=0;
            while(true){  //正序循环
                if(flag==index){
                    break;
                }
                value=value.next;
                flag++;
            }
        }else{
            value=tailNode;
            int flag = size-1;
            while(true){  //倒序循环
                if(flag<=index){
                    break;
                }
                value=value.previa;
                flag--;
            }
        }
        return value.object;
    }
    //清空链表
    public void clear(){
        //将头尾指针长度初始化。
        headNode=tailNode=null;
        size=0;
    }

    //判断链表是否为空
    public boolean isNull(){
        return size==0;
    }   
}

看了数组和链形的实现是不是清晰很多?举个栗子数组就像班上的同学每个人都有一个学号,知道学号就知道是那个同学了;链表还是一个班上的同学,只不过他们是手牵着手的,除了首尾两个同学之外,其他的每个同学都只知道自己上一个和下一个同学是谁。

LinkedList遍历陷阱,千万注意!

/**
 * 对比ArraryList、LinkedList遍历耗时
 * @author orange
 *
 */
public class LinkedListTest {
    public static void main(String[] args) {
        LinkedList<String> linedLinkedListTest=new LinkedList<String>();
        ArrayList<String>arrayList=new ArrayList<String>();

        //添加值
        for(int i=0;i<30000;i++){
            linedLinkedListTest.addLast(i+"");
            arrayList.add(i+"");
        }

        //ArrayList(for循环) 
        String arrayTemp="";
        long arrayTime1 = System.currentTimeMillis();
        for(int i=0;i<arrayList.size();i++){
            arrayTemp=arrayList.get(i);
        }
        long arrayTime2 = System.currentTimeMillis();

        //linkedList(for循环) 
        String linkedTemp1="";
        long linkedTime1 = System.currentTimeMillis();
        for(int i=0;i<linedLinkedListTest.size();i++){
            linkedTemp1=linedLinkedListTest.get(i);
        }
        long linkedTime2 = System.currentTimeMillis();


        //linkedList(迭代器)
        String linkedTemp2="";
        long linkedTime3 = System.currentTimeMillis();
        Iterator<String> it = linedLinkedListTest.iterator();
        while(it.hasNext()){
            linkedTemp2=it.next();
        }
        long linkedTime4 = System.currentTimeMillis();
        //打印遍历耗时
        System.out.println("arrayList(for循环):"+(arrayTime2-arrayTime1));
        System.out.println("LinkedList(for循环):"+(linkedTime2-linkedTime1));
        System.out.println("LinkedList(迭代器):"+(linkedTime4-linkedTime3));
    }
}

遍历耗时如下:
arrayList(for循环):7
LinkedList(for循环):6239
LinkedList(迭代器):4

我们发现LinkedList(for循环)遍历方式简直弱爆了,我们来一探究竟。

    Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) { //得到index是在上半部分还是下半部分
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

通过源码我们知道,其实每次调用get()方法就是一次遍历,虽然if (index < (size >> 1))做了优化,但是实在是无能为力了,一旦linkedList元素过多采用这种方式就完蛋了。
正确的遍历方式应该采用迭代器或者foreach()的方式,foreach()底层也是迭代器,就是个语法糖而已。

vector比较重要的知识

vector特殊的属性和方法

//用于数组扩容的增量值
protected int capacityIncrement;
//数组扩容方法
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //如果增量值在创建的时候设置了,就用增量值,反之就是2倍的扩容。
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

//获取超级数组的长度。
    public synchronized int capacity() {
        return elementData.length;
    }

Set比较重要的知识

hash是什么,Set是什么,它们之间有什么关系?

hash通俗来讲即是任意长度的消息经过hash函数得到一个长度固定的消息摘要,而这个消息摘要理论上是不会重复的
(实际也会发生碰撞,即两个不同的消息经过hash函数得到一样的消息摘要),这个消息摘要可以标识这个消息就像数据表的主键一样。

set接口有两个最终实现类hashSet和treeSet,hashSet无序不重复的集合、treeSet有序不重复的集合。

两个最终实现类的底层几乎全是调用map的方法,可以理解为实现类的底层是map的底层,而map底层中最重要的就是hash,由于最终类主要调用map的方法,set就简单研究而map再去深入研究。

HashSet如何区别重复

重写对象的hashCode()和equals()方法

package ABC;

import java.util.HashSet;

/**
 * hashSet 去重复
 * @author orange
 *
 */
public class Student {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }


    public Student() {
    }
    public Student(String name) {
        this.name = name;
    }
    //重写object类的hashCode()方法,这里方法返回固定的值,这样就只需要比较equals()方法就好了。
    @Override
    public int hashCode() {
        //return super.hashCode();
        return 9527;
    }

    //由于hashCode()方法返回固定值,区别就由equals()方法来做,这里是名字相同就认为是相同的对象。
    @Override
    public boolean equals(Object obj) {
        return this.name.equals(((Student)obj).name);
    }
    @Override
    public String toString() {
        return "Student [name=" + name + "]";
    }

    public static void main(String[] args) {
        Student student1=new Student("abc1");
        Student student2=new Student("abc1");
        Student student3=new Student("abc3");
        HashSet<Student>set=new HashSet<Student>();
        set.add(student1);
        set.add(student2);
        set.add(student3);
        //最后只会有abc1和abc3,名字重复的无法插入。
        for (Student student : set) {
            System.out.println(student);
        }
    }
}

TreeSet如何区别大小

第一种方法:实现Comparable接口

package ABC;
import java.util.TreeSet;
/**
 * treeSet 排序(Comparable)
 * @author orange
 *
 */
public class Person implements Comparable{
    public String name;
    public int age;




    @Override
    public String toString() {
        return "Person [name=" + name + ", age="
                + 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;
    }


    //Comparable接口实现
    @Override
    public int compareTo(Object o) {
        if(this.age>((Person)o).age){
            return 1;
        }else if (this.age<((Person)o).age) {
            return -1;
        }
        //返回0 的话表示相等!
        return 0;
    }

    public static void main(String[] args) {        
        Person p1=new Person("testName1", 2333);
        Person p2=new Person("testName2", 666);
        Person p3=new Person("testName3", 9527);
        Person p4=new Person("testName4", 4399);
        //treeSet排序
        TreeSet treeSet=new TreeSet( );
        treeSet.add(p1);
        treeSet.add(p2);
        treeSet.add(p3);
        treeSet.add(p4);
        for (Object person : treeSet) {
            System.out.println(person);
        }
        //集合排序
        List<Person>list=new ArrayList<Person>();
        list.add(p1);
        list.add(p2);
        list.add(p3);
        list.add(p4);
        //Collections里面有个排序的方法,实现了Comparable接口就可以进行排序   
        Collections.sort(list);

        //数组排序
        Person[]persons=new Person[4];
        persons[0]=p1;
        persons[1]=p2;
        persons[2]=p3;
        persons[3]=p4;
        //Arrays里面有个排序的方法,实现了Comparable接口就可以进行排序
        Arrays.sort(persons);
    }

}

第二种方法:实现Comparator接口

package ABC;
import java.util.Comparator;
import java.util.TreeSet;
/**
 * treeSet 排序(Comparator)
 * @author orange
 *
 */
public class Person2{
    public String name;
    public int age;


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


    public Person2( 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;
    }

    //Comparator接口实现,其实该接口没必要在类中实现由外部实现最好,可以不用对该类进行任何修改也可以排序。
/*  @Override
    public int compare(Object o1, Object o2) {

        Person s1=(Person) o1; 
        Person s2=(Person) o2; 

        if (s1.age>s2.age) {
            return 1;
        }else if (s1.age<s2.age) {
            return -1;
        }
        return 0;
    }*/

    public static void main(String[] args) {
        //在创建TreeSet的时候,实现Comparator接口,这样可以不用关心Person2类里面具体的类容。
        TreeSet treeSet=new TreeSet(new Comparator<Person2>() {
            @Override
            public int compare(Person2 o1, Person2 o2) {
                //模仿源码写个三目运算,看起来是不是清爽很多?
                return o1.age>o2.age ? 1 : o1.age==o2.age ? 0 : -1;
            }
        });
        Person2 p1=new Person2("testName1", 2333);
        Person2 p2=new Person2("testName2", 666);
        Person2 p3=new Person2("testName3", 9527);
        Person2 p4=new Person2("testName4", 4399);
        treeSet.add(p1);
        treeSet.add(p2);
        treeSet.add(p3);
        treeSet.add(p4);
        for (Object person : treeSet) {
            System.out.println(person);
        }
    }

}

Comparable和Comparator的区别

1、Comparable接口必须在对象里面实现,可以使用Arrays和Collections的sort()方法进行排序;Comparator接口可以在类的外部实现,对类0入侵的同时实现排序。
2、如过要对String类进行排序,我们试图改写规则要怎么办String这个类我们没办法修改,这时候用Comparator()接口实现我们自己的排序就非常棒。
3、其实用什么排序看具体的情况而定,千万不要生搬硬套。

Map

Map的键和值是成对出现的,比较突出的运用就是数据字典,说破大天也就是一个对象和对象之间的对应关系。

HashMap、Hashtable、TreeMap的区别

1、HashMap和Hashtable几乎一模一样只是HashMap允许null值null键而Hashtable则是线程安全。
2、TreeMap非线程安全且多了个按照键排序。

HashMap比较重要的知识

HashMap是如何构成的?

我们这里讨论的是在jdk1.6中的Map这个map的数据结构还相对比较简单,后面的版本采用红黑树实现的,红黑树设计到了比较复杂的数据结构以及相对应的算法,在后面我们专门来讨论数据结构和算法,这里我们就用相对简单的来着重理解hashMap的实现原理。
首先放出一张图:

紫色部分是hash数组,绿色部分是链表。
hashMap是基于数组和链表实现的,这种实现方式主要是为了解决hash值的碰撞问题(hash数组里面的下标也就是通过key的hashCode计算得来的hash值,一旦添加的元素过多就有可能发生下标一样的情况)hash数组里面装的是链表的头。这个做法就是分类,按照对象的hash值来分类,这样大大加快了查询速度。

hash数组(紫色)

transient Entry[] table;

链表对象(绿色)

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;//键
        V value;    //值
        Entry<K,V> next;//下一个Entry对象
        final int hash; //键的hash值
    }

HashMap的成员变量

    //hash数组(存放链表的链表头对象)
    transient Entry[] table;
    //hash数组的默认大小(这个值必须为2的N次幂至于为什么我们接下来再讨论)
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    //负载因子的默认大小(size和hash数组长度的比值)
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //map里面具体装了多个键值对
    transient int size;
    //临界点容量值(负载因子*hash数组长度)
    int threshold;
    //map修改的次数(用于迭代遍历时保证数据完整)
    transient volatile int modCount;

put方法到底做了什么?

    public V put(K key, V value) {
    //空键调用空键的put方法(空键在hash表的第一个)
        if (key == null)
            return putForNullKey(value);
        //维护较差的hash值减少过多的碰撞
        int hash = hash(key.hashCode());
        //根据hash值和hash表的长度计算出下标位置
        int i = indexFor(hash, table.length);
        //table[i]链表头,遍历链表找到对应键将值进行替换
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                //该方法没有任何意义是一个没有任何实现的方法
                e.recordAccess(this);
                //将老的值返回
                return oldValue;
            }
        }
        //修改计数器
        modCount++;
        //如果没有重复的键,这里就直接添加一个链表对象。
        //注意每次添加的对象都在链表的第一位。
        addEntry(hash, key, value, i);
        //当添加一个新的键值对返回值是null(这样添加的时候就知道该键在以前有没有对应的值)。
        return null;
    }

HahaMap如何扩容的?

    //添加键值对方法
    void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        //当存储键值对数量大于临界容量开始扩容
        if (size++ >= threshold)
            //扩容的值为2倍(又是2的N次幂)
            resize(2 * table.length);
    }
    //扩容方法
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        //当容量已经为最大值(2的30次方)时,就调节临界值threshold为int的最大值
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        //对原本的数据进行转移(这里超级耗费时间)
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }
   //数据转移方法(由于hash数组长度变了,所有的Entry对象的位置也要变化
   //也就是需要把所有的链表拆掉重新组装成新的链表这也就是耗时的原因)
       void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        //遍历所有的链表头
        for (int j = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if (e != null) {
                src[j] = null;
               //从链表头开始遍历。
                do {
                    Entry<K,V> next = e.next;
                    //重新计算Entry对象的位置
                    int i = indexFor(e.hash, newCapacity); 
                    //将计算出位置的Entry对象放在数组里面也就是链表头                  
                    e.next = newTable[i];                    
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    } 

为什么hash数组的长度要为2的N次幂?

    //原因就在获取下标的方法里
    static int indexFor(int h, int length) {
    /**
    这里换个写法大家可能就懂了h % length,注意当length为2的N次幂时   h % length才和h & (length-1)等价 虽然等价但是采用位运算要比取余快的多(貌似CPU计算除法和取余有点慢)。
   其实我个人觉得想要加快效率就应该在开始创建Map的时候想好负载因子和初始化容量,毕竟最耗时间的是数组的扩容,减少扩容的次数才是关键。*/
        return h & (length-1);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值