JAVA中级教程 - 第三讲 集合框架(下)

244 阅读6分钟

JAVA中级教程

学习JAVA网站 : how2j

第三讲 关系与区别

3.10 ArrayList Vs HashSet

  • 是否有顺序

ArrayList: 有顺序 HashSet: 无顺序

HashSet的具体顺序,既不是按照插入顺序,也不是按照hashcode的顺序。关于hashcode有专门的章节讲解: hashcode 原理。换句话说,同样是插入0-9到HashSet中, 在JVM的不同版本中,看到的顺序都是不一样的。 所以在开发的时候,不能依赖于某种臆测的顺序,这个顺序本身是不稳定的

  • 能否重复

List中的数据可以重复 Set中的数据不能够重复 重复判断标准是:首先看hashcode是否相同

如果hashcode不同,则认为是不同数据 如果hashcode相同,再比较equals,如果equals相同,则是相同数据,否则是不同数据 更多关系hashcode,请参考hashcode原理

		ArrayList<Integer> numberList =new ArrayList<Integer>();
        //List中的数据可以重复
        System.out.println("----------List----------");
        System.out.println("向List 中插入 9 9");
        numberList.add(9);
        numberList.add(9);
        System.out.println("List 中出现两个9:");
        System.out.println(numberList);
        System.out.println("----------Set----------");
        HashSet<Integer> numberSet =new HashSet<Integer>();
        System.out.println("向Set 中插入9 9");
        //Set中的数据不能重复
        numberSet.add(9);
        numberSet.add(9);
        System.out.println("Set 中只会保留一个9:");
        System.out.println(numberSet);

3.11 ArrayList Vs LinkedList

  • rrayList和LinkedList的区别

ArrayList 插入,删除数据慢 LinkedList, 插入,删除数据快 ArrayList是顺序结构,所以定位很快,指哪找哪。 就像电影院位置一样,有了电影票,一下就找到位置了。 LinkedList 是链表结构,就像手里的一串佛珠,要找出第99个佛珠,必须得一个一个的数过去,所以定位慢

image.png

  • 插入数据时间对比
package collection;
 
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
 
public class TestCollection {
    public static void main(String[] args) {
        List<Integer> l;
        l = new ArrayList<>();
        insertFirst(l, "ArrayList");
 
        l = new LinkedList<>();
        insertFirst(l, "LinkedList");
 
    }
 
    private static void insertFirst(List<Integer> l, String type) {
        int total = 1000 * 100;
        final int number = 5;
        long start = System.currentTimeMillis();
        for (int i = 0; i < total; i++) {
            l.add(0, number);
        }
        long end = System.currentTimeMillis();
        System.out.printf("在%s 最前面插入%d条数据,总共耗时 %d 毫秒 %n", type, total, end - start);
    }
 
}

3609ms - 32ms

  • 定位数据时间比较
package collection;
 
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
 
public class TestCollection {
    public static void main(String[] args) {
        List<Integer> l;
        l = new ArrayList<>();
        modify(l, "ArrayList");
 
        l = new LinkedList<>();
        modify(l, "LinkedList");
 
    }
 
    private static void modify(List<Integer> l, String type) {
        int total = 100 * 1000;
        int index = total/2;
        final int number = 5;
        //初始化
        for (int i = 0; i < total; i++) {
            l.add(number);
        }
         
        long start = System.currentTimeMillis();
 
        for (int i = 0; i < total; i++) {
             int n = l.get(index);
             n++;
             l.set(index, n);
        }
        long end = System.currentTimeMillis();
        System.out.printf("%s总长度是%d,定位到第%d个数据,取出来,加1,再放回去%n 重复%d遍,总共耗时 %d 毫秒 %n", type,total, index,total, end - start);
        System.out.println();
    }
 
}

0ms - 27281ms

  • 练习-在中间插入数据

在List的中间位置,插入数据,比较ArrayList快,还是LinkedList快,并解释为什么?

答:数组更快 因为定位到最后一个元素更快并且插入不需要移动。

3.12 HashMap Vs HashTable

  • HashMap和Hashtable的区别

    HashMap和Hashtable都实现了Map接口,都是键值对保存数据的方式 区别1: HashMap可以存放 null Hashtable不能存放null 区别2: HashMap不是线程安全的类 Hashtable是线程安全的类

鉴于目前学习的进度,不对线程安全做展开,在线程章节会详细讲解

package collection;
 
import java.util.HashMap;
import java.util.Hashtable;
 
public class TestCollection {
    public static void main(String[] args) {
         
        //HashMap和Hashtable都实现了Map接口,都是键值对保存数据的方式
         
        HashMap<String,String> hashMap = new HashMap<String,String>();
        //HashMap可以用null作key,作value
        hashMap.put(null, "123");
        hashMap.put("123", null);
         
        Hashtable<String,String> hashtable = new Hashtable<String,String>();
        //Hashtable不能用null作key,不能用null作value
        hashtable.put(null, "123");
        hashtable.put("123", null);
 
    }
}
  • 练习-反转key和value

使用如下键值对,初始化一个HashMap: adc - 物理英雄 apc - 魔法英雄 t - 坦克

对这个HashMap进行反转,key变成value,value变成key

提示: keySet()可以获取所有的key, values()可以获取所有的value

package com.company;

import java.util.HashMap;

public class TestCollection {
    public static void main(String[] args) {
        HashMap<String,String> map = new HashMap<String,String>();
        map.put("adc", "物理英雄") ;
        map.put("apc", "魔法英雄") ;
        map.put("t", "坦克") ;

        HashMap<String,String> newmap = new HashMap<String,String>();
        System.out.println(map.keySet());
        System.out.println(map.values());
        System.out.println();

        for (String key : map.keySet()){
            newmap.put(map.get(key),key);
        }
        System.out.println(map);
        System.out.println(newmap);
    }
}

3.13 几种Set

  • HashSet LinkedHashSet TreeSet

HashSet: 无序 LinkedHashSet: 按照插入顺序 TreeSet: 从小到大排序

package collection;
  
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.TreeSet;
  
public class TestCollection {
    public static void main(String[] args) {
        HashSet<Integer> numberSet1 =new HashSet<Integer>();
        //HashSet中的数据不是按照插入顺序存放
        numberSet1.add(88);
        numberSet1.add(8);
        numberSet1.add(888);
          
        System.out.println(numberSet1);
          
        LinkedHashSet<Integer> numberSet2 =new LinkedHashSet<Integer>();
        //LinkedHashSet中的数据是按照插入顺序存放
        numberSet2.add(88);
        numberSet2.add(8);
        numberSet2.add(888);
          
        System.out.println(numberSet2);
        TreeSet<Integer> numberSet3 =new TreeSet<Integer>();
        //TreeSet 中的数据是进行了排序的
        numberSet3.add(88);
        numberSet3.add(8);
        numberSet3.add(888);
          
        System.out.println(numberSet3);
          
    }
}
  • 练习-既不重复,又有顺序

利用LinkedHashSet的既不重复,又有顺序的特性,把Math.PI中的数字,按照出现顺序打印出来,相同数字,只出现一次

public static void main(String[] args) {
        char[] bb = String.valueOf(Math.PI).toCharArray();
        System.out.println(bb);
        LinkedHashSet link = new LinkedHashSet();
        for (char p:bb){
            if(p != '.'){
                link.add(p);
            }
        }
        System.out.println(link);
    }

第三讲 其他

3.14 hashcode原理

  • List查找的低效率

假设在List中存放着无重复名称,没有顺序的2000000个Hero 要把名字叫做“hero 1000000”的对象找出来 List的做法是对每一个进行挨个遍历,直到找到名字叫做“hero 1000000”的英雄。 最差的情况下,需要遍历和比较2000000次,才能找到对应的英雄。 测试逻辑:

  1. 初始化2000000个对象到ArrayList中
  2. 打乱容器中的数据顺序
  3. 进行10次查询,统计每一次消耗的时间

不同计算机的配置情况下,所花的时间是有区别的。 在本机上,花掉的时间大概是600毫秒左右

public class TestCollection {
    public static void main(String[] args) {
        List<Hero> heros = new ArrayList<Hero>();
            
        for (int j = 0; j < 2000000; j++) {
            Hero h = new Hero("Hero " + j);
            heros.add(h);
        }
            
        // 进行10次查找,观察大体的平均值
        for (int i = 0; i < 10; i++) {
            // 打乱heros中元素的顺序
            Collections.shuffle(heros);
             
            long start = System.currentTimeMillis();
     
            String target = "Hero 1000000";
     
            for (Hero hero : heros) {
                if (hero.name.equals(target)) {
                    System.out.println("找到了 hero!" );
                    break;
                }
            }
            long end = System.currentTimeMillis();
            long elapsed = end - start;
            System.out.println("一共花了:" + elapsed + " 毫秒");
        }
             
    }
}
  • HashMap的性能表现

使用HashMap 做同样的查找

  1. 初始化2000000个对象到HashMap中。
  2. 进行10次查询
  3. 统计每一次的查询消耗的时间

可以观察到,几乎不花时间,花费的时间在1毫秒以内

public class TestCollection {
    public static void main(String[] args) {
          
        HashMap<String,Hero> heroMap = new HashMap<String,Hero>();
        for (int j = 0; j < 2000000; j++) {
            Hero h = new Hero("Hero " + j);
            heroMap.put(h.name, h);
        }
        System.out.println("数据准备完成");
  
        for (int i = 0; i < 10; i++) {
            long start = System.currentTimeMillis();
              
            //查找名字是Hero 1000000的对象
            Hero target = heroMap.get("Hero 1000000");
            System.out.println("找到了 hero!" + target.name);
              
            long end = System.currentTimeMillis();
            long elapsed = end - start;
            System.out.println("一共花了:" + elapsed + " 毫秒");
        }
  
    }
}
  • HashMap原理与字典

在展开HashMap原理的讲解之前,首先回忆一下大家初中和高中使用的汉英字典。

比如要找一个单词对应的中文意思,假设单词是Lengendary,首先在目录找到Lengendary在第 555页。

然后,翻到第555页,这页不只一个单词,但是量已经很少了,逐一比较,很快就定位目标单词Lengendary。

555相当于就是Lengendary对应的hashcode

  • 分析HashMap性能卓越的原因

-----hashcode概念-----

所有的对象,都有一个对应的hashcode(散列值)

比如字符串“gareen”对应的是1001 (实际上不是,这里是方便理解,假设的值) 比如字符串“temoo”对应的是1004 比如字符串“db”对应的是1008 比如字符串“annie”对应的也是1008

-----保存数据-----

准备一个数组,其长度是2000,并且设定特殊的hashcode算法,使得所有字符串对应的hashcode,都会落在0-1999之间

要存放名字是"gareen"的英雄,就把该英雄和名称组成一个键值对,存放在数组的1001这个位置上 要存放名字是"temoo"的英雄,就把该英雄存放在数组的1004这个位置上 要存放名字是"db"的英雄,就把该英雄存放在数组的1008这个位置上 要存放名字是"annie"的英雄,然而 "annie"的hashcode 1008对应的位置已经有db英雄了,那么就在这里创建一个链表,接在db英雄后面存放annie

-----查找数据-----

比如要查找gareen,首先计算"gareen"的hashcode是1001,根据1001这个下标,到数组中进行定位,(根据数组下标进行定位,是非常快速的) 发现1001这个位置就只有一个英雄,那么该英雄就是gareen. 比如要查找annie,首先计算"annie"的hashcode是1008,根据1008这个下标,到数组中进行定位,发现1008这个位置有两个英雄,那么就对两个英雄的名字进行逐一比较(equals),因为此时需要比较的量就已经少很多了,很快也就可以找出目标英雄 这就是使用hashmap进行查询,非常快原理。

这是一种用空间换时间的思维方式

image.png

  • HashSet判断是否重复

HashSet的数据是不能重复的,相同数据不能保存在一起,到底如何判断是否是重复的呢? 根据HashSet和HashMap的关系,我们了解到因为HashSet没有自身的实现,而是里面封装了一个HashMap,所以本质上就是判断HashMap的key是否重复。

再通过上一步的学习,key是否重复,是由两个步骤判断的: hashcode是否一样 如果hashcode不一样,就是在不同的坑里,一定是不重复的 如果hashcode一样,就是在同一个坑里,还需要进行equals比较 如果equals一样,则是重复数据 如果equals不一样,则是不同数据。

练习-自定义字符串的hashcode

image.png

package com.company;

import java.util.HashMap;

public class TestCollection {
    public static int hashcode(String str){
        char []ch = str.toCharArray();
        int code = 0;
        for (int i = 0;i < ch.length;i++){
            code += (int)ch[i];
        }
        code *= 23;
        code = Math.abs(code)%2000;
        return code;
    }

    public static void main(String[] args) {
        String str = "tjyy";
        int code = hashcode(str);
        System.out.println(code);
    }
}

练习-自定义MyHashMap

image.png

IHashMap.java

package collection;
public interface IHashMap {
    public void put(String key,Object object);
    public Object get(String key);
}

Entry.java

package collection;
 
//键值对
package collection;
 
//键值对
public class Entry {
    public Entry(Object key, Object value) {
        super();
        this.key = key;
        this.value = value;
    }
    public Object key;
    public Object value;
    @Override
    public String toString() {
        return "[key=" + key + ", value=" + value + "]";
    }
}

完成后的代码:

public class Entry implements IHashMap{
    public Entry(Object key, Object value) {
        super();
        this.key = key;
        this.value = value;
    }
    public Object key;
    public Object value;
    @Override
    public String toString() {
        return "[key=" + key + ", value=" + value + "]";
    }

    public static int hashcode(String str){
        char []ch = str.toCharArray();
        int code = 0;
        for (int i = 0;i < ch.length;i++){
            code += (int)ch[i];
        }
        code *= 23;
        code = Math.abs(code)%2000;
        return code;
    }

    // 长度是2000的对象数组
    @SuppressWarnings("unchecked")
    LinkedList<Entry>[] Olist = new LinkedList[2000];

    @Override
    public void put(String key,Object object){
        // TODO 自动生成的方法存根
        int code = hashcode(key);
        if (Olist[code] == null){
            // 在作用域中,没有任何Entry的外层实例可访问   Entry.super;
            Entry e  = new Entry(key,object);
            Olist[code] = new LinkedList<Entry>();
            Olist[code].add(e);
        }
        else {
            Entry e = new Entry(key, object);
            Olist[code].addLast(e);
        }
    }

    @Override
    public Object get(String key){
        // TODO 自动生成的方法存根
        int code = hashcode(key);
        Object v = null;
        if (Olist[code] == null){
            return null;
        }
        else{
            for (Entry o :Olist[code]){
                if (o.key.equals(key)){
                    v = o.value;
                    break;
                }
                else return null;
            }
            return v;
        }
    }
}

练习-内容查找性能比较

image.png

MyHashMap map = new MyHashMap();

3.15 比较器

  • Comparator

假设Hero有三个属性 name,hp,damage 一个集合中放存放10个Hero,通过Collections.sort对这10个进行排序 那么到底是hp小的放前面?还是damage小的放前面?Collections.sort也无法确定 所以要指定到底按照哪种属性进行排序 这里就需要提供一个Comparator给定如何进行两个对象之间的大小比较

Hero.java

package charactor;
  
public class Hero  {
    public String name;
    public float hp;
  
    public int damage;
  
    public Hero() {
  
    }
  
    public Hero(String name) {
 
        this.name = name;
    }
  
    public String toString() {
        return "Hero [name=" + name + ", hp=" + hp + ", damage=" + damage + "]\r\n";
    }
 
    public Hero(String name, int hp, int damage) {
        this.name = name;
        this.hp = hp;
        this.damage = damage;
    }
  
}

TestCollection.java

package com.company;

import java.util.*;

public class TestCollection {
    public static void main(String[] args) {
        Random r = new Random();
        List<Hero> heros = new ArrayList<Hero>();

        for (int i = 0;i < 10;i++){
            //通过随机值实例化hero的hp和damage
            heros.add(new Hero("hero " + i,r.nextInt(100),r.nextInt(100)));
        }
        System.out.println("初始化后的集合:");
        System.out.println(heros);

        //直接调用sort会出现编译错误,因为Hero有各种属性
        //到底按照哪种属性进行比较,Collections也不知道,不确定,所以没法排
        //Collections.sort(heros);

        //引入Comparator,指定比较的算法
        Comparator<Hero> c = new Comparator<Hero>() {
            @Override
            public int compare(Hero h1, Hero h2) {
                //按照hp进行排序
                if(h1.hp>=h2.hp)
                    return 1;  //正数表示h1比h2要大
                else
                    return -1;
            }
        };

        Collections.sort(heros,c);
        System.out.println("按照血量排序后的集合:");
        System.out.println(heros);
    }
}

image.png

  • Comparable

使Hero类实现Comparable接口 在类里面提供比较算法 Collections.sort就有足够的信息进行排序了,也无需额外提供比较器Comparator 注: 如果返回-1, 就表示当前的更小,否则就是更大.

return 1的情况排序,前面的比较项排序靠后。

Hero.java

package com.company;

import java.io.Serializable;
public class Hero  implements Comparable<Hero>{
    public String name;
    public float hp;
    public int damage;

    public Hero(){}

    public Hero(String name) {
        this.name =name;
    }

    //初始化name,hp,damage的构造方法
    public Hero(String name,float hp, int damage) {
        this.name =name;
        this.hp = hp;
        this.damage = damage;
    }

    @Override
    public String toString() {
        return "Hero [name=" + name + ", hp=" + hp + ", damage=" + damage + "]\r\n";
    }

    @Override
    public int compareTo(Hero anotherHero){
        if (damage < anotherHero.damage)
            return 1;
        else
            return -1;
    }
}
public class TestCollection {
    public static void main(String[] args) {
        Random r =new Random();
        List<Hero> heros = new ArrayList<Hero>();
          
        for (int i = 0; i < 10; i++) {
            //通过随机值实例化hero的hp和damage
            heros.add(new Hero("hero "+ i, r.nextInt(100), r.nextInt(100)));
        }
          
        System.out.println("初始化后的集合");
        System.out.println(heros);
          
        //Hero类实现了接口Comparable,即自带比较信息。
        //Collections直接进行排序,无需额外的Comparator
        Collections.sort(heros);
        System.out.println("按照伤害高低排序后的集合");
        System.out.println(heros);
          
    }
}

image.png

  • 练习-自定义顺序的TreeSet

默认情况下,TreeSet中的数据是从小到大排序的,不过TreeSet的构造方法支持传入一个Comparator

public TreeSet(Comparator comparator) 

通过这个构造方法创建一个TreeSet,使得其中的的数字是倒排序的

     @Override
            public int compare(Integer o1, Integer o2) {
                if (o1 >= o2) {
                    return -1;
                }
                return 1;
            }
        };

        TreeSet<Integer> ts = new TreeSet<>(c);

3.16 聚合操作

  • 聚合操作

JDK8之后,引入了对集合的聚合操作,可以非常容易的遍历筛选比较集合中的元素。

像这样:

        String name =heros
            .stream()
            .sorted((h1,h2)->h1.hp>h2.hp?-1:1)
            .skip(2)
            .map(h->h.getName())
            .findFirst()
            .get();

但是要用好聚合,必须先掌握Lambda表达式,聚合的章节讲放在Lambda与聚合操作部分详细讲解

package lambda;
 
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;

import charactor.Hero;
 
public class TestAggregate {
    public static void main(String[] args) {
        Random r = new Random();
        List<Hero> heros = new ArrayList<Hero>();
        for (int i = 0; i < 10; i++) {
            heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
        }

        System.out.println("初始化集合后的数据 (最后一个数据重复):");
        System.out.println(heros);
        
        //传统方式
        Collections.sort(heros,new Comparator<Hero>() {
			@Override
			public int compare(Hero o1, Hero o2) {
				return (int) (o2.hp-o1.hp);
			}
		});
        
        Hero hero = heros.get(2);
        System.out.println("通过传统方式找出来的hp第三高的英雄名称是:" + hero.name);
        
        //聚合方式
        String name =heros
        	.stream()
        	.sorted((h1,h2)->h1.hp>h2.hp?-1:1)
        	.skip(2)
        	.map(h->h.getName())
        	.findFirst()
        	.get();

        System.out.println("通过聚合操作找出来的hp第三高的英雄名称是:" + name);
        
    }
}