再谈Java集合

上次简单地谈了下Java集合的部分内容,这次让我们继续来看下集合的其他知识,经过查看其他资料后,看到一张图可以很好地来表示Java框架,如下:

再谈Java集合_第1张图片
看到这张图,我们可以看到除了Collection,还有Iterator迭代器和Map,接下来,在这里要谈到的就是Iterator迭代器和Map。
1.Iterator迭代器
迭代器是一个实现Iterator或ListIterator接口的对象,完成循环访问集合。
注意:集合在写Collection的iterator()方法时利用内部类提供迭代器的实现,是不能单独存在使用的!
主要包含三种方法:
next()——驱使Iterator依次访问集合中的元素;
hasNext()——功能和next方法类似,但是这个方法会首先判断集合中是否需要元素,如果存在元素,则返回true;
remove()——删除通过next方法迭代出的元素;是用这个方法必须先调用next方法,再调用remove方法;
遍历集合中的元素时,可以使用增强for循环语句:
for(元素类型 e:数组或者集合){
而增强for循环语句在这里要特别注意一点:在使用增强for循环时不能删除元素,如果硬要删除元素,需要自己另外编写迭代器并且只能调用迭代器的remove方法,谈到迭代器的remove方法,在这里为什么要强调是调用迭代器的remove方法呢?实际上,另外在程序的过程中还存在调用元素的迭代器,那么迭代器调用remove方法和元素调用之间有什么区别呢?
以下通过一个例子来说,比如现在有三张100张人民币和一张假的人民币,此时现要通过迭代器来实现查询这张假的人民币,并且删除这张假币,最后再计算总的人民币数量,首先如图:
再谈Java集合_第2张图片
其实在这里要说明Iterator在调用next()方法时首先会是站在第一张100前面,也就是回归,随着调用的过程中,如果是money.remove(),在查询到假的人民币时,会将假的人民币给删除,而在检索后总算出的总的人民币的数量会是300,和实际不符合,但是如果Iterator.remove()会存在一个回归的过程,在检索一张人民币时会Iterator会站在前一张100的位置上,也就是说类似于那个Iterator在检索一张人民币后会退后一步,这样就不会出现那张被删除的人民币没有被计算在内。
2.Map接口——查询表
查询表:专门通过Key来迅速查找对应的value值得一种存储结构。
存储对象:Entry对象(Key-value键值对)
Map接口并未继承Collection接口,根据内部结构的不同,有多种实现类,常用的内部接口有hash表实现的HashMap和内部排序的二叉树TreeMap,其方法有:
put(K key,V value)——用于存放或替换集合中的元素
get(Object key)——查找获取元素
bollean containsKey(Object key)——判断集合中是否包含指定的键
bollean containsValue(Object value)——判断集合中是否包含指定的值
HashCode——根据一个具体对象的内容计算生成的指向该对象的编码,一般用于比较两个集合
在这里又要谈到,equals方法也是具有比较两个对象的内容是否相同的功能,但是euqals方法比较的结果比HashCode方法要准确些,在使用HashCode方法时要注意一下几点:
第一:重写HashCode方法时也要重写equals方法,但是euqals方法比较返回true的对象,而HashCode方法返回值应该是相同的;
第二:hashCode方法返回的数值应该符合hash算法的要求,一般情况下可以使用IDE提供的工具自动生成HashCode方法;
第三:对于字符串而言,HashCode方法比较的两个对象的内容本身相同,那么计算出的HashCode也一定相等的。
例如:

public class MapTest {
/*
 * 测试字符串类型的HashCode
 * 
 */
    @Test
    public void testHashCode(){
        String s="Cecilia";
        String s1=new String("Cecilia");
        System.out.println(s.hashCode());
        System.out.println(s1.hashCode());
        System.out.println(s.equals(s1)?"内容相等":"内容不相等");
        System.out.println(s.hashCode()==s1.hashCode()?"hashCode相等":"hashCode不相等");
        System.out.println(s==s1?"是同一个对象":"不是同一个对象");
    }
    运行结果为:内容相等、hashCode相等、不是同一个对象
HashMap——又称hash表,本质是一个Entry类型的数组,其默认的Capacity的默认值为16:负载因子(集合元素个数/hash表长度)默认值为0.75;此时HashMap的效率高于ArrayList,而一旦超过其默认的容量就需要自动扩容。
    工作原理:通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Factor则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。 
    具体看以下例子:
@Test
    /*
     * 测试HashMap
     * 尽量使用泛型类型
     * 
     */
    public void testHashMap(){
        Map emps=new HashMap(); 
        Random r=new Random();
        for(int i=0;i<10;i++){
            String name="员工"+r.nextInt(10);
            double salary=r.nextInt(10)*1000;
            Emp e=new Emp(name,salary);
//          将对象放入HashMap中:put
            if(emps.get(name)==null){
            emps.put(name,e);
            }else{
                //将指定的key元素取出
                System.out.println(name+"已存在:"+emps.get(name));
            }
        }
        System.out.println(emps);
        }
        class Emp{
    private String name;
    private double salary;
    public Emp(String name, double salary) {
        super();
        this.name = name;
        this.salary = salary;
    }
    public Emp() {
        super();
        // TODO Auto-generated constructor stub
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        long temp;
        temp = Double.doubleToLongBits(salary);
        result = prime * result + (int) (temp ^ (temp >>> 32));
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Emp other = (Emp) obj;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        if (Double.doubleToLongBits(salary) != Double.doubleToLongBits(other.salary))
            return false;
        return true;
    }
    @Override
    public String toString() {
        return "Emp [name=" + name + ", salary=" + salary + "]";
    }

}
    运行结果为:员工3已存在:Emp [name=员工3, salary=3000.0]
            员工4已存在:Emp [name=员工4, salary=7000.0]
            员工4已存在:Emp [name=员工4, salary=7000.0]
            员工0已存在:Emp [name=员工0, salary=1000.0]
            员工4已存在:Emp [name=员工4, salary=7000.0]
            {员工4=Emp [name=员工4, salary=7000.0], 员工3=Emp [name=员工3, salary=3000.0], 员工0=Emp [name=员工0, salary=1000.0], 员工8=Emp [name=员工8, salary=0.0], 员工7=Emp [name=员工7, salary=0.0]}
    上述例子综合地说明了HashMap检索员工信息的过程。
3.线性表的高级应用
    3.1 排序
    Collection中有个sort方法可以实现排序,这个方法默认是按照升序方法来排列的,而这里要注意的是这里的排序只是修改集合,不重新创建集合。但是这里也可以自定义排序的规则,此处就需要用到——Comparable接口,专门用来定义相同的类型的任意两个对象。实际上,在JDK文档中存在CompareTo方法——比如:
public int COmpareTo(Product o){
return this.price1:
        this.price>o.price:1:
                            0;
}

这种方式是按照升序的方式排序的,而如果需要修改排序的方式,则将其整个return返回的乘以-1即可,按照这种原理就可自定义排序的方式。还有另一种类似功能的接口——
Comparator接口实现类必须重写其定义的方式:
int Compare(T o1,T o2);
若o1

    *测试利用Comparator接口实现动态比较逻辑
    *
    */

    public Class ComparatorTest{
        Comparator bc_p=new Comparator(){
        public int compare(Product o1,Product o2){
        if(o1.getCount()!=o2.getCount()){
        return (o1.getCount()1:
        o1.getCount()>o2.getCount():1:
                            0;)*(-1)//销量按照降序排序
            }else{
                return o1.getPrice()1:
                        o1.getPrice()>o2.getPrice():1:
                                0;//单价按照升序排序
                }
        }
    };
    Collections.sort(products,bc_p);
}
3.2 控制元素操作顺序——LinkList&&Queue接口
    LinkList——操作方法有:
    bollean offer(E e)——添加元素至队尾
     E poll()——从队首中删除并返回一个元素
     E peek()——返回队首元素(但不删除)
    Queue——操作方法有: 
      Push()——进栈
      Pop()——出栈
      (遵循先进后出)
4.查询表的高级应用  
  这里还是先小结下查询表的一些操作方法:
   put(K key,V value)——向查询表中放入键值对儿
   get(Object ,key/value)——获取key或value对象,此方法还可以避免已存在的同名的key的检索出来
   4.1 构建HashMap
    例如:
@Test
    /*
     * 构建字符集与两位数对应关系的HashMap
     * @return 返回字符集对应的两位数的查询表
     */
    public Map getCodeMap(){
//      step1:定义Hash表,key是字符类型,value是字符串类型
//      需要26个字母+3个符号+10个数字=39个字符
//      加载因子:75%,需要39/75%=52个
        Map m=new HashMap();
//      step2:先构建26个字母的键值对儿
//      从第2个按键到第9个按键字母都是字母,除第7个和第9个包含4个字母,其他都包含3个字母,
    char c='a';
        for(int i=2;i<=9;i++){
        int k=(i==7||i==9)?4:3;
//      用来循环按键上的每一个字母
        for(int j=1;j<=k;j++){
//          根据i和j拼接出对应的value
            String value=""+i+j;
//          小技巧:将任意类型转换成字符串的方法:在前面加上空字符串
//          将字母c作为key,以及它对应的value,放入Map中
            m.put(c, value);
//          每放入一个字母,就要继续进入下一个字母的运算
//          字母的unicode编码是连续的,字母加1就可以得到下一个
            c++;
//          System.out.println();

        }

    }

//      step2:专门放入3个特殊的字符对应的编码,空格,逗号,句号
        m.put(' ', "11");
        m.put(',', "12");
        m.put('.', "13");
        m.put('#', "14");
//      step3:从0开始,循环放入10个数字对应的两位数字
        for(c='0';c<='9';c++){
//          每个数字的编码就是在数字后加0
            String value=c+"0";
            m.put(c, value);
        }
        System.out.println(m);
        return m;

    }
    4.2 遍历HashMap
    第一:遍历所有的Key
      方法:set keySet()
      例如:针对上个例子构建的HashMap
/*
     * 仅遍历Key
     */
    public void testKeySet(){
        Map m=getCodeMap();
//      仅遍历Key
        Set keyset=m.keySet();
        System.out.println(keyset);
//      问题:所有的key都是乱序的,——Hash表中key的特点
//      解决方法:排序——将集合对象装入TreeSet集合中
        Set sortset=new TreeSet(keyset);
        System.out.println(sortset);
//      将每个元素用单引号括起来
//      解决:遍历set集合,取出每个元素,加单引号
//      迭代器或增强for循环
//      每10个循环一次进行换行
        int i=1;

        for(char c:sortset){
            System.out.println("'"+c+"'|");
            if(i%10==0){
                System.out.println();
            }
        }

    }
第二:遍历所有的Entry对象
方法:set > entrySet()
例如:
/*
     * 测试遍历所有的entry对象
     */
    @Test
    public void Key_ValueTest(){
//      Map hm = new HashMap();
//      hm.put("张三", 23);
//      hm.put("李四", 24);
//      hm.put("王五", 25);
//      hm.put("赵六", 26);
//
        /*Set keySet = hm.keySet();   //获取集合中所有的键
        Iterator it = keySet.iterator();    //获取迭代器
        while(it.hasNext()) {  //判断单列集合中是否有元素
            String key = it.next();  //获取集合中的每一个元素,其实就是双列集合中的键
            Integer value = hm.get(key);   //根据键获取值
            System.out.println(key + "=" + value);//打印键值对
        }*/
//
//      for(String key : hm.keySet()) {
//          //增强for循环迭代双列集合第一种方式
//      System.out.println(key + "=" + hm.get(key));
//      }
//
//  }
        Map hm = new HashMap();
        hm.put("张三", 23);
        hm.put("李四", 24);
        hm.put("王五", 25);
        hm.put("赵六", 26);
        /*Set> entrySet = hm.entrySet(); //获取所有的键值对象的集合
        Iterator> it = entrySet.iterator();//获取迭代器
        while(it.hasNext()) {
            Entry en = it.next();  //获取键值对对象
            String key = en.getKey();    //根据键值对对象获取键
            Integer value = en.getValue();  //根据键值对对象获取值
            System.out.println(key + "=" + value);
        }*/

        for(Entry en : hm.entrySet()) {
         System.out.println(en.getKey() + "=" + en.getValue());
        }   
}
 第三:遍历所有的value对象(不常用)
 4.3 键找值方法
  思路:  1.获取所有的键集合;
         2.遍历键集合,获取到每一个键;
         3.根据键找到值。
 4.4 Entry对象找键和值方法
   思路:  1.获取所有的entry对象集合;
          2.遍历Entry对象的集合,获取到每一个Entry对象;
          3.根据Entry找到对象的Key和Value。
  例如:根据加密后的密文进行解密
@Test
    /*
     * 测试使用Hash表加密
     */
    public void testEnCode(){
        Map m=getCodeMap();
        String msg="no zuo no die";
//      从字符串中依次取出每个字符for+charAt()
//      将得到的字符放入查询表中,找到对应的两位编码m.get();
//      每找到一个两位编码,就拼接到一个StringBuilder:sb.add();
        StringBuilder sb=new StringBuilder(msg.length()*2);
//      获得固定长度的字符串
        for(int i=0;i<=msg.length();i++){
            String value=m.get(msg.charAt(i));
//          如果找不到对应的字符,则不拼接
            if(value!=null){
                sb.append(value);
            }else{
                System.out.println("找不到对应的字符!");
                break;
            }
        }
//      只有sb的长度等于msg的长度才能正常打印密文
        if(sb.length()==msg.length()*2){
            System.out.println("原文为:"+msg);
            System.out.println("密文为:"+sb);
        }

    }
/*
     * 通过查询到的键值对进行对密文解码
     * 根据value找到对应的键
     */
    @Test
    public void testDecode(){
        Map mp=getCodeMap();
//      step1:先获得所有的键值对
        Set> entrySet=mp.entrySet();
//      强调:使用Entry类型,必须导入import.util.Map.Entry
        String msg="";
//      解码:循环从密文中每次取出2个字符;使用内部的嵌套循环遍历entrySet,知道发现匹配的value,就将value对应的key拼接到
        StringBuilder sb=new StringBuilder(msg.length()/2);
        for(int i=0;i2);
            for(Entry entry:entrySet){
                if(sub.equals(entry.getValue())){
                    sb.append(entry.getKey());
//                  一旦找打匹配的key,就不要再继续寻找
                }
            }
            if(sb.length()==msg.length()/2){
                System.out.println("解码成功!"+sb.toString())
            }else{
                System.out.println("解码错误!");
            }
        }
    }
}
5.LinkedListHashMap——使用Map接口的Hash表和链表的实现
与hashMap不同之处在于:
LinkedHashMap维护着一个循环链表,定义着迭代顺序,如果在Map中重新定义已有的key,那么其位置不变,只是将value替换!!!!!    
6.数组和集合之间的转换
  6.1  集合转换成数组
  集合____ T[] toArray(T[] a)____数组
  注意:如果存放的数组容量足够大,则不会产生新的数组,如果存放的容量小,不足以装得下集合中的元素,则会创建新数组,并且会抛弃原数组,返回新数组;
  其完美做法: String[] mybasket=l.toArray(new String[l,size])
 6.2  数组转换成集合
 数组____static ListasList(T) ____集合
 注意:转换后的集合对象和原数组对象其实共享同一个数组存储空间,任意一方修改都会影响对象; 而且转换后的集合对象不能将其进行增删改查操作!!!!
 ??问题:如果想要对数组转换成的集合进行操作,该如何做???
 step1:先建一个ArrayList对象;
 List L=new ArrayList();
 step2:使用集合的addAll方法批量的将集合中的元素拷贝到新的集合中,这样就可以对新集合进行随意操作,不会受数组的限制。
 l.addAll(Array.asList(arr));
 7.一些小问题:
   7.1什么是Iterator?
    一些集合类提供了内容遍历的功能,通过java.util.Iterator接口。这些接口允许遍历对象的集合。依次操作每个元素对象。当使用Iterators时,在获得Iterator的时候包含一个集合快照。通常在遍历一个Iterator的时候不建议修改集合本省。
 7.2 Iterator与ListIterator有什么区别?
    Iterator:只能正向遍历集合,适用于获取移除元素。ListIerator:继承Iterator,可以双向列表的遍历,同样支持元素的修改。
 7.3 什么是HaspMap和Map?
Map是接口,Java 集合框架中一部分,用于存储键值对,HashMap是用哈希算法实现Map的类。
7.4 HashMap与HashTable有什么区别?对比Hashtable VS HashMap?
两者都是用key-value方式获取数据。Hashtable是原始集合类之一(也称作遗留类)。HashMap作为新集合框架的一部分在Java2的1.2版本中加入。它们之间有一下区别
● HashMap和Hashtable大致是等同的,除了非同步和空值(HashMap允许null值作为key和value,而Hashtable不可以)。
● HashMap没法保证映射的顺序一直不变,但是作为HashMap的子类LinkedHashMap,如果想要预知的顺序迭代(默认按照插入顺序),你可以很轻易的置换为HashMap,如果使用Hashtable就没那么容易了。
● HashMap不是同步的,而Hashtable是同步的。
● 迭代HashMap采用快速失败机制,而Hashtable不是,所以这是设计的考虑点。
7.5 在Hashtable上下文中同步是什么意思?
同步意味着在一个时间点只能有一个线程可以修改哈希表,任何线程在执行hashtable的更新操作前需要获取对象锁,其他线程等待锁的释放。
7.6 什么叫做快速失败特性?
从高级别层次来说快速失败是一个系统或软件对于其故障做出的响应。一个快速失败系统设计用来即时报告可能会导致失败的任何故障情况,它通常用来停止正常的操作而不是尝试继续做可能有缺陷的工作。当有问题发生时,快速失败系统即时可见地发错错误告警。在Java中,快速失败与iterators有关。如果一个iterator在集合对象上创建了,其它线程欲“结构化”的修改该集合对象,并发修改异常 (ConcurrentModificationException) 抛出。
7.7 怎样使Hashmap同步?
HashMap可以通过Map m = Collections.synchronizedMap(hashMap)来达到同步的效果。
7.8 什么时候使用Hashtable,什么时候使用HashMap?
基本的不同点是Hashtable同步HashMap不是的,所以无论什么时候有多个线程访问相同实例的可能时,就应该使用Hashtable,反之使用HashMap。非线程安全的数据结构能带来更好的性能。
如果在将来有一种可能—你需要按顺序获得键值对的方案时,HashMap是一个很好的选择,因为有HashMap的一个子类LinkedHashMap。所以如果你想可预测的按顺序迭代(默认按插入的顺序),你可以很方便用LinkedHashMap替换HashMap。反观要是使用的Hashtable就没那么简单了。同时如果有多个线程访问HashMap,Collections.synchronizedMap()可以代替,总的来说HashMap更灵活。

7.9 为什么Vector类认为是废弃的或者是非官方地不推荐使用?或者说为什么我们应该一直使用ArrayList而不是Vector?
你应该使用ArrayList而不是Vector是因为默认情况下你是非同步访问的,Vector同步了每个方法,你几乎从不要那样做,通常有想要同步的是整个操作序列。同步单个的操作也不安全(如果你迭代一个Vector,你还是要加锁,以避免其它线程在同一时刻改变集合).而且效率更慢。当然同样有锁的开销即使你不需要,这是个很糟糕的方法在默认情况下同步访问。你可以一直使用Collections.sychronizedList来装饰一个集合。
事实上Vector结合了“可变数组”的集合和同步每个操作的实现。这是另外一个设计上的缺陷。Vector还有些遗留的方法在枚举和元素获取的方法,这些方法不同于List接口,如果这些方法在代码中程序员更趋向于想用它。尽管枚举速度更快,但是他们不能检查如果集合在迭代的时候修改了,这样将导致问题。尽管以上诸多原因,oracle也从没宣称过要废弃Vector.

你可能感兴趣的:(Java修炼之道)