java容器

说到java容器脑子里必须浮现出下面这张图


Java容器类库的作用就是用来“保存对象”,准确的说是对象的引用,从图中可以看出大概有两种容器:

1,有序存储容器Collection

        Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements)。一些Collection允许相同的元素而另一些不行。一些能排序而另一些不行。Java SDK不提供直接继承自Collection的类,Java SDK提供的类都是继承自Collection的“子接口”如List和Set。  

主要方法:
boolean add(Object o)添加对象到集合
boolean remove(Object o)删除指定的对象
int size()返回当前集合中元素的数量
boolean contains(Object o)查找集合中是否有指定的对象
boolean isEmpty()判断集合是否为空
Iterator iterator()返回一个迭代器
boolean containsAll(Collection c)查找集合中是否有集合c中的元素
boolean addAll(Collection c)将集合c中所有的元素添加给该集合
void clear()删除集合中所有元素
void removeAll(Collection c)从集合中删除c集合中也有的元素
void retainAll(Collection c)从集合中删除集合c中不包含的元素

因为Collection接口继承了Iterable类,所以所有实现Collection的有序存储容器,都有.iterator()方法返回一个循环体iterator,例如:

 

      List<String> list =   new ArrayList<String>();//泛型也主要用在类似于这样的地方
             Iterator<String> it= list.iterator();
             while(it.hasNext()){
                 System.out.println(it.next());
              }
    有时我们会说迭代器Iterator统一了有序容器的访问方式。能有Iterator的地方也都能用更优雅的增强for循环
         List<String> list =   new ArrayList<String>();
               for(String s:list){
                   System.out.println(s);
               }
注: 1.1、java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。 Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。
        1. 2、java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。

       2,键值对存储容器Map

          Map一组成对的“键值对”对象,允许通过键来查找值或者对象
          Map没有继承Collection接口,Map提供key到value的映射。一个Map中不能包含相同的key,每个key只能映射一个 value。Map接口提供3种集合的视图,Map的内容可以被当作一组key集合,一组value集合,或者一组key-value映射。

   主要方法:
boolean equals(Object o)比较对象
boolean remove(Object o)删除一个对象
put(Object key,Object value)添加key和value

             

       2.1有一个经典的例子,检查java的Random类的随机性,如下

         

       Map<Integer, Integer> map = new HashMap<Integer, Integer>();
    	 Random random1 = new Random();
    	 for(int i =0;i<1000;i++){
    		 Integer inte = random1.nextInt(10);
    		 Integer res= map.get(inte);
    		 map.put(inte, res==null?1:res+1);
    	 }
    	System.out.println(map);
输出:
{0=109, 1=110, 2=93, 3=92, 4=90, 5=108, 6=105, 7=106, 8=94, 9=93}

总结

       Java容器实际上只有三种: List, Set,Map;但每种接口都有不同的实现版本.它们的区别可以归纳为由什么在背后支持它们.也就是说,你使用的接口是由什么样的数据结构实现的.
List的选择:
         比如:ArrayList和LinkedList都实现了List接口.因此无论选择哪一个,基本操作都一样.但ArrayList是由数组提供底层支持.而LinkedList是由双向链表实现的.所以,如果要经常向List里插入或删除数据,LinkedList会比较好.否则应该用速度更快的ArrayList。
从源码分析:
一add(Object)方法,在使用add()增加元素时ArrayList会检查底层存储数据的数组是否够长,如不够则需要创建原有1.5倍加1长度的数组,再使用Arrays.copyOf()方法把原由数组数据copy到新数组可以看出有创建新数组,copy数据的动作,比较费时效率不高。
而LinkedList在使用add()增加元素时,因为LinkedList底层是使用双向链表的形式存储数据的,每个节点元素都保存有前面一个和后面一个节点的索引,并含有此节点的值,这样当新增时只需改变一个节点就可以了,所以新增效率高。
所以在使用Add方法比较多的情况下,应该选择使用LinkedList容器。

二get(index)方法,因ArrayList底层是数组支持的,所以支持根据下标随机访问数据,速度非常快,而Linkedlist底层是双向链表的形式,不支持下标随机访问,只能通过next向后或previous向前逐个的查询,速度慢,效率低,
所以在使用get方法比较多的情况下,应该选择使用ArrayList容器。

三remove方法,
 1,在根据索引删除时remove(index)时,arrayList在删除指定索引的元素之后,使用Array.copy新数组,
而LinkedList根据index逐个循环找到指定的元素,再删除掉。
 2,在根据元素删除时remove(Object)时,arrayList是先循环找到元素的索引,在按照上面的根据索引去删除。
而LinkedList是循环找到元素直接删除掉。所以使用remover删除时,总体还是LinkedList效率更高一些。

关于ArrayList的源码,几点比较重要的总结:

    1、注意其三个不同的构造方法。无参构造方法构造的ArrayList的容量默认为10,带有Collection参数的构造方法,将Collection转化为数组赋给ArrayList的实现数组elementData。

    2、注意扩充容量的方法ensureCapacity。ArrayList在每次增加元素(可能是1个,也可能是一组)时,都要调用该方法来确保足够的容量。当容量不足以容纳当前的元素个数时,就设置新的容量为旧的容量的1.5倍加1,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量),而后用Arrays.copyof()方法将元素拷贝到新的数组(详见下面的第3点)。从中可以看出,当容量不够时,每次增加元素,都要将原来的元素拷贝到一个新的数组中,非常之耗时,也因此建议在事先能确定元素数量的情况下,才使用ArrayList,否则建议使用LinkedList。

    3、ArrayList的实现中大量地调用了Arrays.copyof(),Arrays.copyof()是将原来的数组拷贝到新数组,Arrays工具类能有效的操作数组,其他常用的方法有:Arrays.sort(a)升序排序,Arrays.asList(a)将数组转化为list,Arrays.toString(a)转化为字符串。


关于LinkedList的源码,给出几点比较重要的总结:

    1、从源码中很明显可以看出,LinkedList的实现是基于双向循环链表的,且头结点中不存放数据,如下图;


    2、注意两个不同的构造方法。无参构造方法直接建立一个仅包含head节点的空链表,包含Collection的构造方法,先调用无参构造方法建立一个空链表,而后将Collection中的数据加入到链表的尾部后面。

    3、在查找和删除某元素时,源码中都划分为该元素为null和不为null两种情况来处理,LinkedList中允许元素为null。

    4、LinkedList是基于链表实现的,因此不存在容量不足的问题,所以这里没有扩容的方法。

    5、注意源码中的Entry<E> entry(int index)方法。该方法返回双向链表中指定位置处的节点,而链表中是没有下标索引的,要指定位置出的元素,就要遍历该链表,从源码的实现中,我们看到这里有一个加速动作源码中先将index与长度size的一半比较,如果index<size/2,就只从位置0往后遍历到位置index处,而如果index>size/2,就只从位置size往前遍历到位置index处。这样可以减少一部分不必要的遍历,从而提高一定的效率(实际上效率还是很低)。

    6、LinkedList是基于链表实现的,因此插入删除效率高,查找效率低(虽然有一个加速动作)
 
 Set的选择
    Set的使用和list类似,只是set保存的值不会重复,为什么set的值不会重复,从源码可以看出端倪,HashSet底层实现是:
 public HashSet() {
	map = new HashMap<E,Object>();
    }
  public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }


HashSet底层是通过HashMap实现的,并且通过add()方法可以看出添加的值放入到了map的key值,如果重复自然会覆盖。TreeMap底层实现类似,底层是TreeMap
 public TreeSet() {
	this(new TreeMap<E,Object>());
    }

 public boolean add(E e) {
    return m.put(e, PRESENT)==null;
  }
    HashSet总是比TreeSet 性能要好.而后者存在的理由就是它可以维持元素的排序状态.所以,如果需要一个排好序的Set时,才应该用TreeSet。


关于HashMap源码的一些总结:

1、首先要清楚HashMap的存储结构,如下图所示:


    图中,紫色部分即代表哈希表,也称为哈希数组,数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中。

2、HashMap共有四个构造方法。构造方法中提到了两个很重要的参数:初始容量和加载因子。这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中槽的数量(即哈希数组的长度),初始容量是创建哈希表时的容量(从构造函数中可以看出,如果不指明,则默认为16),加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 resize 操作(即扩容)。

    下面说下加载因子,如果加载因子越大,对空间的利用更充分,但是查找效率会降低(链表长度会越来越长);如果加载因子太小,那么表中的数据将过于稀疏(很多空间还没用,就开始扩容了),对空间造成严重浪费。如果我们在构造方法中不指定,则系统默认加载因子为0.75,这是一个比较理想的值,一般情况下我们是无需修改的。

    另外,无论我们指定的容量为多少,构造方法都会将实际容量设为不小于指定容量的2的次方的一个数,且最大值不能超过2的30次方

3、HashMap中key和value都允许为null。

4、要重点分析下HashMap中用的最多的两个方法put和get。先从比较简单的get方法着手,源码如下:

    // 获取key对应的value    
    public V get(Object key) {    
        if (key == null)    
            return getForNullKey();    
        // 获取key的hash值    
        int hash = hash(key.hashCode());    
        // 在“该hash值对应的链表”上查找“键值等于key”的元素    
        for (Entry<K,V> e = table[indexFor(hash, table.length)];    
             e != null;    
             e = e.next) {    
            Object k;    
    /判断key是否相同  
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))    
                return e.value;    
        }  
    没找到则返回null  
        return null;    
    }    
      
    // 获取“key为null”的元素的值    
    // HashMap将“key为null”的元素存储在table[0]位置,但不一定是该链表的第一个位置!    
    private V getForNullKey() {    
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {    
            if (e.key == null)    
                return e.value;    
        }    
        return null;    
    }    
  首先,如果key为null,则直接从哈希表的第一个位置table[0]对应的链表上查找。记住,key为null的键值对永远都放在以table[0]为头结点的链表中,当然不一定是存放在头结点table[0]中。

    如果key不为null,则先求的key的hash值,根据hash值找到在table中的索引,在该索引对应的单链表中查找是否有键值对的key与目标key相等,有就返回对应的value,没有则返回null。

    put方法稍微复杂些,代码如下:

java] view plain copy

      // 将“key-value”添加到HashMap中    
      public V put(K key, V value) {    
          // 若“key为null”,则将该键值对添加到table[0]中。    
          if (key == null)    
              return putForNullKey(value);    
          // 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。    
          int hash = hash(key.hashCode());    
          int i = indexFor(hash, table.length);    
          for (Entry<K,V> e = table[i]; e != null; e = e.next) {    
              Object k;    
              // 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!    
              if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {    
                  V oldValue = e.value;    
                  e.value = value;    
                  e.recordAccess(this);    
                  return oldValue;    
              }    
          }    
      
          // 若“该key”对应的键值对不存在,则将“key-value”添加到table中    
          modCount++;  
    //将key-value添加到table[i]处  
          addEntry(hash, key, value, i);    
          return null;    
      }   
如果key为null,则将其添加到table[0]对应的链表中,putForNullKey的源码如下:
    // putForNullKey()的作用是将“key为null”键值对添加到table[0]位置    
    private V putForNullKey(V value) {    
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {    
            if (e.key == null) {    
                V oldValue = e.value;    
                e.value = value;    
                e.recordAccess(this);    
                return oldValue;    
            }    
        }    
        // 如果没有存在key为null的键值对,则直接题阿见到table[0]处!    
        modCount++;    
        addEntry(0, null, value, 0);    
        return null;    
    }   

如果key不为null,则同样先求出key的hash值,根据hash值得出在table中的索引,而后遍历对应的单链表,如果单链表中存在与目标key相等的键值对,则将新的value覆盖旧的value,比将旧的value返回,如果找不到与目标key相等的键值对,或者该单链表为空,则将该键值对插入到改单链表的头结点位置(每次新插入的节点都是放在头结点的位置),该操作是有addEntry方法实现的,它的源码如下:
    // 新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。    
    void addEntry(int hash, K key, V value, int bucketIndex) {    
        // 保存“bucketIndex”位置的值到“e”中    
        Entry<K,V> e = table[bucketIndex];    
        // 设置“bucketIndex”位置的元素为“新Entry”,    
        // 设置“e”为“新Entry的下一个节点”    
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);    
        // 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小    
        if (size++ >= threshold)    
            resize(2 * table.length);    
    }    
  注意这里倒数第三行的构造方法,将key-value键值对赋给table[bucketIndex],并将其next指向元素e,这便将key-value放到了头结点中,并将之前的头结点接在了它的后面。该方法也说明,每次put键值对的时候,总是将新的该键值对放在table[bucketIndex]处(即头结点处)。

    两外注意最后两行代码,每次加入键值对时,都要判断当前已用的槽的数目是否大于等于阀值(容量*加载因子),如果大于等于,则进行扩容,将容量扩为原来容量的2倍。

5、关于扩容。上面我们看到了扩容的方法,resize方法,它的源码如下:

    // 重新调整HashMap的大小,newCapacity是调整后的单位    
    void resize(int newCapacity) {    
        Entry[] oldTable = table;    
        int oldCapacity = oldTable.length;    
        if (oldCapacity == MAXIMUM_CAPACITY) {    
            threshold = Integer.MAX_VALUE;    
            return;    
        }    
      
        // 新建一个HashMap,将“旧HashMap”的全部元素添加到“新HashMap”中,    
        // 然后,将“新HashMap”赋值给“旧HashMap”。    
        Entry[] newTable = new Entry[newCapacity];    
        transfer(newTable);    
        table = newTable;    
        threshold = (int)(newCapacity * loadFactor);    
    }    
很明显,扩容是一个相当耗时的操作,因为它需要重新计算这些元素在新的数组中的位置并进行复制处理。因此,我们在用HashMap的时,最好能提前预估下HashMap中元素的个数,这样有助于提高HashMap的性能。

6、注意containsKey方法和containsValue方法。前者直接可以通过key的哈希值将搜索范围定位到指定索引对应的链表,而后者要对哈希数组的每个链表进行搜索。

7、我们重点来分析下求hash值和索引值的方法,这两个方法便是HashMap设计的最为核心的部分,二者结合能保证哈希表中的元素尽可能均匀地散列。

    计算哈希值的方法如下:

    static int hash(int h) {  
            h ^= (h >>> 20) ^ (h >>> 12);  
            return h ^ (h >>> 7) ^ (h >>> 4);  
        }  

  它只是一个数学公式,IDK这样设计对hash值的计算,自然有它的好处,至于为什么这样设计,我们这里不去追究,只要明白一点,用的位的操作使hash值的计算效率很高。

    由hash值找到对应索引的方法如下:

    static int indexFor(int h, int length) {  
            return h & (length-1);  
        }  

这个我们要重点说下,我们一般对哈希表的散列很自然地会想到用hash值对length取模(即除法散列法),Hashtable中也是这样实现的,这种方法基本能保证元素在哈希表中散列的比较均匀,但取模会用到除法运算,效率很低,HashMap中则通过h&(length-1)的方法来代替取模,同样实现了均匀的散列,但效率要高很多,这也是HashMap对Hashtable的一个改进。

    接下来,我们分析下为什么哈希表的容量一定要是2的整数次幂。首先,length为2的整数次幂的话,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率;其次,length为2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性,而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间,因此,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。

Map选择:

         同样是通过哈希表实现的 HashTable是线程安全的,很多方法都是synchronized方法,而HashMap不是线程安全的,但其在单线程程序中的性能比HashTable要高。

          TreeMap底层是使用红黑树的数据结构实现的,具有排序的功能,所以一般需要排序时才会使用它,其他情况没有HashMap效率高。


3,数组

创建数组有两种方式:int[] mylist= new int[2],或者int[] fgg = {1,2,3,4,5};
赋值与取值例子

public class TestArray {
   public static void main(String[] args) {
      // 数组大小
      int size = 10;
      // 定义数组
      double[] myList = new double[size];
      myList[0] = 5.6;
      myList[1] = 4.5;
      myList[2] = 3.3;
      myList[3] = 13.2;
      myList[4] = 4.0;
      myList[5] = 34.33;
      myList[6] = 34.0;
      myList[7] = 45.45;
      myList[8] = 99.993;
      myList[9] = 11123;
      // 计算所有元素的总和
      double total = 0;
      for (int i = 0; i < size; i++) {
         total += myList[i];
      }
      System.out.println("总和为: " + total);
   }
}

以上实例输出结果为:

总和为: 11367.373


JDK 1.5 引进了一种新的循环类型,被称为 foreach 循环或者加强型循环,它能在不使用下标的情况下遍历数组。示例

ublic class TestArray {
   public static void main(String[] args) {
      double[] myList = {1.9, 2.9, 3.4, 3.5};
 
      // 打印所有数组元素
      for (double element: myList) {
         System.out.println(element);
      }
   }
}
以上实例编译运行结果如下:
1.9
2.9
3.4
3.5

多维数组

多维数组可以看成是数组的数组,比如二维数组就是一个特殊的一维数组,其每一个元素都是一个一维数组,例如:

String str [ ] [ ] = new String [ 3 ] [ 4 ] ;

1. 直接为每一维分配空间,格式如下:

type arrayName = new typ [ arraylenght1 ] [ arraylenght2 ] ;

type 可以为基本数据类型和复合数据类型,arraylenght1 和 arraylenght2 必须为正整数,arraylenght1 为行数,arraylenght2 为列数。

例如:

int a[][] = new int[2][3];
         int [][] num = new int [3][3]; //定义了三行三列的二维数组  
         num[0][0] = 1; //给第一行第一个元素赋值  
         num[0][1] = 2; //给第一行第二个元素赋值  
         num[0][2] = 3; //给第一行第三个元素赋值  
           
         num[1][0] = 4; //给第二行第一个元素赋值  
         num[1][1] = 5; //给第二行第二个元素赋值  
         num[1][2] = 6; //给第二行第三个元素赋值  
           
         num[2][0] = 7; //给第三行第一个元素赋值  
         num[2][1] = 8; //给第三行第二个元素赋值  
         num[2][2] = 9; //给第三行第三个元素赋值  
         for(int x = 0; x<num.length; x++){  //定位行  
             for(int y = 0; y<num[x].length; y++){  //定位每行的元素个数  
                 System.out.print(num[x][y]);  
             }  
             System.out.println();  
         }    
输出结果为:
123
456
789

2,直接赋值
/定义二维数组  
        int[ ] [ ] arr={{1,2,3},{4,5,6}};  
        //静态初始化  
           
        //打印出二维数组  
          
        for(int i=0;i<arr.length;i++){  
              
            for(int j=0;j<arr[i].length;j++){  
                      
                System.out.print(arr[i][j]+" ");  
                  
            }  
            //输出一列后就回车空格  
            System.out.println();  
              
        }

输出结果:
1 2 3 
4 5 6 

3. 从最高维开始,分别为每一维分配空间,例如:
    String[][] s = new String[2][];
         s[0] = new String[2];
         s[1] = new String[3];
         s[0][0] = new String("Good");
         s[0][1] = new String("Luck");
         s[1][0] = new String("to");
         s[1][1] = new String("you");
         s[1][2] = new String("!");

         
         for(int i=0;i<s.length;i++){  
             for(int j=0;j<s[i].length;j++){  
                 System.out.print(s[i][j]+" ");  
             }  
             //输出一列后就回车空格  
             System.out.println();  
         }  

输出结果:

Good Luck 
to you !
或者是这样的
   //创建和打印不规则二维数组  
        int arr[ ][ ];  
          
        arr=new int[3][];//现在说明为不规则数组  
          
        arr[0]=new int[10];//arr[0]指向另一个一位数组  
        arr[1]=new int[3];  
        arr[2]=new int[4];  
          
        //赋值  
        for(int i=0;i<arr.length;i++){  
              
            for(int j=0;j<arr[i].length;j++){  
                  
                arr[i][j]=j;  
            }  
              
        }  
          
        //输出  
        for(int i=0;i<arr.length;i++){  
              
            for(int j=0;j<arr[i].length;j++){  
                  
                System.out.print(arr[i][j]+" ");  
            }  
            System.out.println();  
        }  
          
        /*输出结果: 
        0 1 2 3 4 5 6 7 8 9  
        0 1 2  
        0 1 2 3  
        *///  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值