Java中的Set、Map、List等数据结构的存取以及基本使用

一、首先来看他们之间的关系

 

 

二、Set的集合

2.1 Set的概述

set就类似于一个箱子,在"箱子"里面可以存放各种对象,这些对象就组成了集合

Set的特点:无序不重复

2.2 HashSet类

HashSet是Java集合Set的一个实现类,Set是一个接口,其实现类除HashSet之外,还有TreeSet,并继承了Collection,HashSet集合很常用;HashSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能。底层数据结构是哈希表

哈希表
一个元素为链表的数组,综合了数组与链表的优点。

HashSet具有以下特点:

  • 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也可能发生变化;
  • HashSet不是同步的;
  • 集合元素值可以是null;

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{}

HashSet构造

HashSet有几个重载的构造方法

private transient HashMap<E,Object> map;
//默认构造器
public HashSet() {
    map = new HashMap<>();
}
//将传入的集合添加到HashSet的构造器
public HashSet(Collection<? extends E> c) {
    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}
//明确初始容量和装载因子的构造器
public HashSet(int initialCapacity, float loadFactor) {
    map = new HashMap<>(initialCapacity, loadFactor);
}
//仅明确初始容量的构造器(装载因子默认0.75)
public HashSet(int initialCapacity) {
    map = new HashMap<>(initialCapacity);
}

从上面的源码可以看出来,HashSet的底层依赖于HashMap来实现,就相当于HashSet是一个外包,负责接活,然后把活扔给HashMap来做;

 

HashSet的add方法

HashSet的add方法时通过HashMap的put方法实现的,不过HashMap是key-value键值对,而HashSet是集合;那么HashSet是怎么存储的呢?

private static final Object PRESENT = new Object();

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

通过观看源码可以发现,HashSet添加的元素是存放在HashMap的key位置上,而value取了默认常量PRESENT,是一个空对象。

内部存储机制

当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet中的存储位置。如果有两个元素通过equals方法比较true,但它们的hashCode方法返回的值不相等,HashSet将会把它们存储在不同位置,依然可以添加成功。
也就是说。HashSet集合判断两个元素的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode方法返回值也相等。

靠元素重写hashCode方法和equals方法来判断两个元素是否相等,如果相等则覆盖原来的元素,依此来确保元素的唯一性

实例:
没有重写hashCode和equals方法

创建一个Student

public class Student {
    private String name;

    private int 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;
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
创建一个TestOfSet进行测试
public class TestOfSet {

    public static void main(String[] args) {
        String a1 = new String("星期一");
        String a2 = new String("星期二");
        String a3 = new String("星期一");
        String a4 = new String("星期三");
        String a5 = new String("星期四");
        String a6 = new String("星期五");
        Set<String> setString = new HashSet<> ();
        setString.add(a1);
        setString.add(a2);
        setString.add(a3);
        setString.add(a4);
        setString.add(a5);
        setString.add(a6);


        Iterator it = setString.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }

        System.out.println("============第二种输出String=========");
        for (String s : setString) {
            System.out.println(s);
        }

        Student s1 = new Student("露娜", 12);

        Student s2 = new Student("赵云", 18);

        Student s3 = new Student("露娜", 16);

        Student s4 = new Student("小莫", 3);

        Student s5 = new Student("5号", 8);

        Student s6 = new Student("小莫",3);


        Set<Student> h1 = new HashSet<>();
        h1.add(s1);
        h1.add(s2);
        h1.add(s3);
        h1.add(s4);
        h1.add(s5);
        h1.add(s6);


        for (Student s : h1) {
            System.out.println("姓名:"+s.getName()+"=====年龄:"+s.getAge());
        }

    }
}

输出结果:

从结果可以看出,Student里面的name,跟age是相同的,但是Set并没有帮我们执行去重的操作,这是为什么呢?

从上面的结果可以看出来,如果传进来的是String的话,Set会帮自动去重,如果是一个自定义对象的话,就没有去重的操作;

为什么呢?

看一下String的源码

 equals方法

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
         //进行值比较,逐一比较字符
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

String的hashcode()的方法

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

因为String重写了equals方法,比较的是值,所以值一样的话,那么hashCode的结果也是一样的,因为HashSet是根据hashCode来存储的,equals为true的话,那么就覆盖掉了原来的值,所以String值的话,就可以去重;

 

那么我们来重写一下Student类的equals()方法跟hashCode()方法

//判断两个对象是否相等,是否存在,里面的参数是否相等
    @Override
    public boolean equals(Object obj) {
        //首先判断两个对象是否相等,也就是,是否属于同一个对象,如果属于,则返回true
        if (this == obj) return true;
        //比较的对象是否为空,或者对象的构造是否存在,如果不存在,则返回false
        if (obj == null || getClass() != obj.getClass()) return false;

        //将对象转化成Student类进行比较
        Student student = (Student) obj;
        //对象存在,比较里面的参数是否相等
        //参数相等,则返回true
        return age == student.age &&
                Objects.equals(name, student.name);
    }

    //返回对象的name值和age值
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

接着运行看一下结果:

如果需要把某个类的对象保存到HashSet集合中,重写这个类的equals方法和hashCode方法时,
应尽量保证两个对象通过equals发那个法比较返回true时,他们的hashCode方法返回值也相等。

2.3 TreeSet类

TreeSet是SortedSet接口的实现类,TreeSet可以确保集合元素处于排序状态。

内部存储机制
TreeSet内部实现的是红黑树,默认整形排序为从小到大。

TreeSet底层依赖于TreeMap(TreeMap的数据结构是二叉树,红黑树)、

根据红黑树的性质:

1. 每个结点不是红色就是黑色

2. 根节点是黑色的 

3. 如果一个节点是红色的,则它的两个孩子结点是黑色的 

每条路径上的黑色节点的数量必须是一样的

例子:存储下列元素、

21,19,24,23,18,25,20,19,25

在程序上写代码

public class TestTreeSet {

    public static void main(String[] args) {
        //添加21,19,24,23,18,25,20,19,25
        TreeSet<Integer> treeSet = new TreeSet<>();
        treeSet.add(21);
        treeSet.add(19);
        treeSet.add(24);
        treeSet.add(23);
        treeSet.add(18);
        treeSet.add(25);
        treeSet.add(20);
        treeSet.add(19);
        treeSet.add(25);
        System.out.println(treeSet);
        for(Integer i : treeSet) {
            System.out.println("输出:"+i);
        }
    }
}

输出结果:

可以看到,输出已经是去重,排序都有了

public class TestTreeSet {

    public static void main(String[] args) {
        //添加21,19,24,23,18,25,20,19,25
        TreeSet<Integer> treeSet = new TreeSet<>();
        treeSet.add(21);
        treeSet.add(19);
        treeSet.add(24);
        treeSet.add(23);
        treeSet.add(18);
        treeSet.add(25);
        treeSet.add(20);
        treeSet.add(19);
        treeSet.add(25);
        System.out.println(treeSet);
        System.out.println("集合中的第一个元素:"+treeSet.first());//集合中的第一个元素:
        System.out.println("集合中的最后一个元素:"+treeSet.last());//集合中的最后一个元素:
        System.out.println("集合小于20的子集,不包含20:"+treeSet.headSet(20));//集合小于20的子集,不包含20
        System.out.println("集合大于等于20的子集:"+treeSet.tailSet(20));//集合大于等于20的子集:
        System.out.println("集合中大于等于18,小于21的子集:"+treeSet.subSet(18,21));//集合中大于等于18,小于21的子集:[2]
    }
}

 

使用二叉树存储数据的时候:

      第一次存储数据没有树根,就创建了一个树根,然后把这个元素赋值给了树根。

      第二次存储的时候,首先应该和根节点进行比较

              ①当前存储元素的值大于根节点存储元素的值,那么就把当前的元素存储在树根的右边

              ②如果小于的话,就存储在树根的左边

              ③如果相等,则不存储(达到去重的效果)

取出元素:从根节点开始依次从左、中、右开始;

保证元素的唯一性,是靠compareTo的方法返回值确定,如果是0则不存储,保证了元素了唯一性;

从前面来看,TreeSet帮我们自动排序了数字,从小到大来排序;

问题:对于数字,能不能降序排序呢?如果是一个对象,又是怎么排序呢?

 

TreeSet支持两种排序方法:自然排序定制排序,在默认情况下,采用的是自然排序

自然排序

TreeSet会调用集合元素的compareTo(Objec obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列,这就是自然排序。

Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类必须实现该方法,实现接口的类就可以比较大小了。当调用一个一个对象调用该方法与另一个对象进行比较时,obj1.compareTo(obj2)如果返回0表示两个对象相等;如果返回正整数则表明obj1大于obj2,如果是负整数则相反。

案例:
实现存储学生类的集合,排序方式,按年龄大小,如果年龄相等,则按name字符串长度,如果长度相等则比较字符。如果name和age都相等则视为同一对象。

首先让Student类继承一个Comparable,重写实现compareTo的方法

/**
 * @description:
 * @author: WEN
 * @create: 2020-08-14
 **/

public class Student implements Comparable<Student>{
    private String name;

    private int 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;
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }



    //判断两个对象是否相等,是否存在,里面的参数是否相等
    @Override
    public boolean equals(Object obj) {
        //首先判断两个对象是否相等,也就是,是否属于同一个对象,如果属于,则返回true
        if (this == obj) return true;
        //比较的对象是否为空,或者对象的构造是否存在,如果不存在,则返回false
        if (obj == null || getClass() != obj.getClass()) return false;

        //将对象转化成Student类进行比较
        Student student = (Student) obj;
        //对象存在,比较里面的参数是否相等
        //参数相等,则返回true
        return age == student.age &&
                Objects.equals(name, student.name);
    }

    //返回对象的name值和age值
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(Student o) {
        //比较age
        int num = this.age - o.age;
        //如果age相等则比较name长度 ,num = 0的话,就执行num1 = this.name.length() - o.name.length()
        int num1 = num == 0 ? this.name.length() - o.name.length(): num;
        //如果两者相等则比较name字符串
        int num2 = num1 == 0 ? this.name.compareTo(o.name) : num1;
        //返回最终的比较
        return num2;
    }
}

 

写一个demo类进行测试一下

public class TreeSetDemo {
    public static void main(String[] args) {
        TreeSet<Student> tree = new TreeSet<>();

        //向集合中添加元素
        Student s1 = new Student("露娜", 12);

        Student s2 = new Student("赵云", 18);

        Student s3 = new Student("露娜", 16);

        Student s4 = new Student("小莫", 3);

        Student s5 = new Student("5号", 8);

        Student s6 = new Student("小莫",3);
        tree.add(s1);
        tree.add(s2);
        tree.add(s3);
        tree.add(s4);
        tree.add(s5);
        tree.add(s6);

        //遍历
        for (Student s : tree) {
            System.out.println("name:"+s.getName()+"====age:"+s.getAge());
        }

    }
}

 

输出结果:

对于修改数字的排序,可以在定义的时候这样修改

将默认的升序,改成降序排序

public class TestTreeSet {

    public static void main(String[] args) {
        //添加21,19,24,23,18,25,20,19,25
        TreeSet<Integer> treeSet = new TreeSet<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return -(o1-o2);
            }
        });
        treeSet.add(21);
        treeSet.add(19);
        treeSet.add(24);
        treeSet.add(23);
        treeSet.add(18);
        treeSet.add(25);
        treeSet.add(20);
        treeSet.add(19);
        treeSet.add(25);
        System.out.println(treeSet);
        System.out.println("集合中的第一个元素:"+treeSet.first());//集合中的第一个元素:
        System.out.println("集合中的最后一个元素:"+treeSet.last());//集合中的最后一个元素:
        System.out.println("集合小于20的子集,不包含20:"+treeSet.headSet(20));//集合小于20的子集,不包含20
        System.out.println("集合大于等于20的子集:"+treeSet.tailSet(20));//集合大于等于20的子集:
    }
}

输出结果:

使用Lambda表达式来实现

public class TestTreeSet2 {
    public static void main(String[] args) {
        //添加21,19,24,23,18,25,20,19,25
        //使用Lambda表达式来实现
        TreeSet<Integer> treeSet = new TreeSet<>((a,b) -> -(a-b));
        treeSet.add(21);
        treeSet.add(19);
        treeSet.add(24);
        treeSet.add(23);
        treeSet.add(18);
        treeSet.add(25);
        treeSet.add(20);
        treeSet.add(19);
        treeSet.add(25);
        System.out.println(treeSet);
    }
}

    输出结果:

 

三 、Map集合

3.1 map的概述

map集合用于保存具有映射关系的数据,map集合保存着两组值,一组用于保存map的key,一组用于保存map的value;

所有的key不能重复,没有顺序,key跟value要一一对应,key数据组相当于一个set集合;

map集合的作用
和查字典类似,通过key找到对应的value,通过页数找到对应的信息。用学生类来说,key相当于学号,value对应name,age,sex等信息。用这种对应关系方便查找。

Map和Set的关系

可以说关系是很密切了,虽然Map中存放的时键值对,Set中存放的是单个对象,但如果把value看做key的附庸,key在哪里,value就在哪里,这样就可以像对待Set一样来对待Map了。事实上,Map提供了一个Entry内部类来封装key-value对,再计算Entry存储时则只考虑Entry封装的key。

如果把Map集合里的所有value放在一起来看,它们又类似于一个List,元素可以重复,每个元素可以根据索引来找,只是Map中的索引不再是整数值,而是以另一个对象作为索引。

Map中的常用方法:

  • void clear():删除该Map对象中所有键值对;
  • boolean containsKey(Object key):查询Map中是否包含指定的key值;
  • boolean containsValue(Object value):查询Map中是否包含一个或多个value;
  • Set entrySet():返回map中包含的键值对所组成的Set集合,每个集合都是Map.Entry对象。
  • Object get():返回指定key对应的value,如果不包含key则返回null;
  • boolean isEmpty():查询该Map是否为空;
  • Set keySet():返回Map中所有key组成的集合;
  • Collection values():返回该Map里所有value组成的Collection。
  • Object put(Object key,Object value):添加一个键值对,如果集合中的key重复,则覆盖原来的键值对;
  • void putAll(Map m):将Map中的键值对复制到本Map中;
  • Object remove(Object key):删除指定的key对应的键值对,并返回被删除键值对的value,如果不存在,则返回null;
  • boolean remove(Object key,Object value):删除指定键值对,删除成功返回true;
  • int size():返回该Map里的键值对个数;

内部类Entry

Map中包括一个内部类Entry,该类封装一个键值对,常用方法:

  • Object getKey():返回该Entry里包含的key值;
  • Object getvalue():返回该Entry里包含的value值;
  • Object setValue(V value):设置该Entry里包含的value值,并设置新的value值。

例子:

创建一个Student

/**
 * @description:
 * @author: WEN
 * @create: 2020-08-15
 **/
public class Student {

    //学号
    private String sno;

    //姓名
    private String name;

    //年龄
    private int age;

    public String getSno() {
        return sno;
    }

    public void setSno(String sno) {
        this.sno = sno;
    }

    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;
    }

    public Student(String sno, String name, int age) {
        this.sno = sno;
        this.name = name;
        this.age = age;
    }
}

 

创建一个demo测试一下,用学号作为map的key,将Student对象作为value来存取

public class MapDemo {
    public static void main(String[] args) {
        //定义一个Hashmap
        Student s1 = new Student("003","张五",5);
        Student s2 = new Student("004","张六",6);
        Student s3 = new Student("001","张三",3);
        Student s4 = new Student("002","张四",4);
        Student s5 = new Student("005","张七",7);

        //定义一个Hashmap,用学号来作为key,value来存放一个对象
        HashMap<String,Student> hashMap = new HashMap<>();
        hashMap.put(s1.getSno(),s1);
        hashMap.put(s2.getSno(),s2);
        hashMap.put(s3.getSno(),s3);
        hashMap.put(s4.getSno(),s4);
        hashMap.put(s5.getSno(),s5);

        //利用set集合来获取map集合的key,可以运用map的keySet()方法
        Set<String> keys = hashMap.keySet();

        //循环输出
        for (String key : keys) {
            Student student = hashMap.get(key);
            System.out.println("学号:"+student.getSno()+"====姓名:"+student.getName()+"====age:"+student.getAge());
        }
    }
}

输出结果:

可以看到上面输出是按学号顺序来的,map本身是无序的,只不过set集合将学号进行了升序排序,最后输出按学号顺序来输出

 

再来看一下下面这段代码

public class MapDemo {
    public static void main(String[] args) {
        //定义一个Hashmap
        Student s1 = new Student("003","张五",5);
        Student s2 = new Student("004","张六",6);
        Student s3 = new Student("001","张三",3);
        Student s4 = new Student("002","张四",4);
        Student s6 = new Student("002","张四",6);
        Student s5 = new Student("005","张七",7);
        Student s7 = new Student("006","张八",9);
        Student s8 = new Student("007","张八",9);

        //定义一个Hashmap,用学号来作为key,value来存放一个对象
        HashMap<String,Student> hashMap = new HashMap<>();
        hashMap.put(s1.getSno(),s1);
        hashMap.put(s2.getSno(),s2);
        hashMap.put(s3.getSno(),s3);
        hashMap.put(s4.getSno(),s4);
        hashMap.put(s5.getSno(),s5);
        hashMap.put(s6.getSno(),s6);
        hashMap.put(s7.getSno(),s7);
        hashMap.put(s8.getSno(),s8);
        //利用set集合来获取map集合的key,可以运用map的keySet()方法
        Set<String> keys = hashMap.keySet();

        //循环输出
        for (String key : keys) {
            Student student = hashMap.get(key);
            System.out.println("学号:"+student.getSno()+"====姓名:"+student.getName()+"====age:"+student.getAge());
        }
        System.out.println(hashMap.get("002").getAge());
    }
}

输出结果:

表 1 Map接口的常用方法
方法名称说明
V get(Object key)返回 Map 集合中指定键对象所对应的值。V 表示值的数据类型
V put(K key, V value)向 Map 集合中添加键-值对,返回 key 以前对应的 value,如果没有, 则返回 null
V remove(Object key)从 Map 集合中删除 key 对应的键-值对,返回 key 对应的 value,如果没有,则返回null
Set entrySet()返回 Map 集合中所有键-值对的 Set 集合,此 Set 集合中元素的数据类型为 Map.Entry
Set keySet()返回 Map 集合中所有键对象的 Set 集合

 

public class MapDemo {
    public static void main(String[] args) {
        //定义一个Hashmap
        Student s1 = new Student("003","张五",5);
        Student s2 = new Student("004","张六",6);
        Student s3 = new Student("001","张三",3);
        Student s4 = new Student("002","张四",4);
        Student s6 = new Student("002","张四",6);
        Student s5 = new Student("005","张七",7);
        Student s7 = new Student("006","张八",9);
        Student s8 = new Student("007","张八",9);

        //定义一个Hashmap,用学号来作为key,value来存放一个对象
        HashMap<String,Student> hashMap = new HashMap<>();
        hashMap.put(s1.getSno(),s1);
        hashMap.put(s2.getSno(),s2);
        hashMap.put(s3.getSno(),s3);
        hashMap.put(s4.getSno(),s4);
        hashMap.put(s5.getSno(),s5);
        hashMap.put(s6.getSno(),s6);
        hashMap.put(s7.getSno(),s7);
        hashMap.put(s8.getSno(),s8);
        //利用set集合来获取map集合的key,可以运用map的keySet()方法
        Set<String> keys = hashMap.keySet();

        Set<Map.Entry<String, Student>> entrySet = hashMap.entrySet();
        for (Map.Entry<String, Student> entry : entrySet) {
            System.out.println("key:"+entry.getKey()+"----姓名:"+entry.getValue().getName());
        }

        //查看map中是否存在某个key <boolean containsKey(Object key)>
        if(hashMap.containsKey("003")){
            System.out.println("存在003学号的key");
        }else {
            System.out.println("不存在003学号的key");
        }

        if(hashMap.containsKey("008")){
            System.out.println("存在学号为008的key");
        }else {
            System.out.println("不存在学号为008的key");
        }


        Map<String,Student> hashmap2 = new HashMap<>();
        hashmap2.put(s1.getSno(),s8);
        Student student2 = hashmap2.get(s1.getSno());
        System.out.println("测试学号:"+s1.getSno()+"22学号:"+student2.getSno()+"====姓名:"+student2.getName()+"===age:"+student2.getAge());
        //循环输出
        for (String key : keys) {
            Student student = hashMap.get(key);
            System.out.println("学号:"+student.getSno()+"====姓名:"+student.getName()+"====age:"+student.getAge());
        }
        System.out.println("年龄:"+hashMap.get("002").getAge());
    }
}

输出结果:

 

 

3.2 hashmap的数据结构

    3.2.1 HashMap的底层数据结构       

  • JDK1.7及之前:数组+链表
  • JDK1.8:数组+链表+红黑树

首先抛出几个面试常见的问题:

    1:为什么数组的长度为2的次幂

    2:为什么hashmap的加载因子要设置成0.75

    3:为什么链表的长度要大于等于8时转成了红黑树

以前计算index用下面这条:

index = hashCode % 数组长度

为什么这样,要用取模运算来计算index,也就是存放value数组的下标?

答:因为根据取模计算出来的index能够更加均匀分布

有什么问题?

在实际中,往往不会产生这样理想的状况,有时候,计算出来的index值都是一样的,index值一样时就会产生hash冲突。

hash冲突那怎么解决?

这时候,产生了第二中数据结构 --- 链表,产生冲突的元素会在该索引处以链表的形式保存。但是这时候就出现了另外一个问题,如果冲突过多,链表过长,就会影响查询效率。

怎么解决链表过长,影响查询效率的问题?

这时候,红黑树就出现了,红黑树是一颗接近平衡的二叉树,他的查询时间复杂度为O(logn),在多元素下,查询效率远要比链表的查询效率高。当链表长度大于等于8时,就会转成红黑树。

为什么链表长度大于等于8时就转成红黑树,其他不行吗?

这里需要提到一个概率论中的泊松分布,的因为链表长度大于等于8时转成红黑树正式遵循泊松分布

再来查看一下hashmap源码对泊松分布的描述

从源码描述可以看出,当个数到达8的时候,这概率已经是非常小了,所以当链表个数不多的时候,就没必要转成红黑树了,因为个数不多,转成红黑树,效率提高不了多少,并且转换过程中也会耗资源。

 

为什么数组的长度默认为16,或者是2的次幂呢!是10不行吗?

前面讲到为了使算出来的尽量分布均匀,使用index = hashCode(key) % 数组长度,这条公式固然可以算出均匀分布,但是存在一个问题,就是效率低下。这时候有人就想到了位运算,如何进行位运算呢?

就是: index = hashCode(key) & (数组长度 - 1)

 

下面我们以值为“book”的Key来演示整个过程:

1.计算book的hashcode,结果为十进制的3029737,二进制的1011100011101011101001。

2.假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111。

3.把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以index=9。可以说,Hash算法最终得到的index结果,完全取决于Key的Hashcode值的最后几位。

这样的方式有什么好处呢?为什么长度必须是16或者2的幂?比如HashMap长度是10会怎么样?

这样做不但效果上等同于取模,而且还大大提高了性能。至于为什么采用16,我们可以试试长度是10会出现什么问题。

假设HashMap的长度是10,重复刚才的运算步骤:

单独看这个结果,表面上并没有问题。我们再来尝试一个新的HashCode  10 1110 0011 1010 1110 1011 :

让我们再换一个HashCode 10 1110 0011 1010 1110 1111 试试  :

这上面的例子可以看出,虽然HashCode的倒数第二第三位从0变成了1,但是运算的结果都是1001。也就是说,当HashMap长度为10的时候,有些index结果的出现几率会更大,而有些index结果永远不会出现(比如0111)!

这样,显然不符合Hash算法均匀分布的原则。

反观长度16或者其他2的幂,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。

 

为什么加载因子是0.75,设成0.5或者1不行吗?

加载因子如果定的太大,比如1,这就意味着数组的每个空位都需要填满,即达到理想状态,不产生链表,但实际是不可能达到这种理想状态,如果一直等数组填满才扩容,虽然达到了最大的数组空间利用率,但会产生大量的哈希碰撞,同时产生更多的链表,显然不符合我们的需求。

但如果设置的过小,比如0.5,这样一来保证了数组空间很充足,减少了哈希碰撞,这种情况下查询效率很高,但消耗了大量空间。

因此,我们就需要在时间和空间上做一个折中,选择最合适的负载因子以保证最优化,取到了0.75

 

hashmap扩容是怎么扩的?

扩容是是新建了一个HashMap的底层数组,而后调用transfer方法,将就HashMap的全部元素添加到新的HashMap中(要重新计算元素在新的数组中的索引位置)。 很明显,扩容是一个相当耗时的操作,因为它需要重新计算这些元素在新的数组中的位置并进行复制处理。因此,我们在用HashMap的时,最好能提前预估下HashMap中元素的个数,这样有助于提高HashMap的性能。

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

3.3 hashmap和hashtable的区别

 

继承的父类不同

Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。

 

线程安全性不同

javadoc中关于hashmap的一段描述如下:此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。

Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省情况下是非Synchronize的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用HashMap时就必须要自己增加同步处理。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:

      Map m = Collections.synchronizedMap(new HashMap(...));

      Hashtable 线程安全很好理解,因为它每个方法中都加入了Synchronize

为什么hashmap是线程不安全的?

HashMap底层是一个Entry数组,当发生hash冲突的时候,hashmap是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。

下面来分析一下多线程访问:

(1)在hashmap做put操作的时候会调用下面方法:

// 新增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);      
    }

 

在hashmap做put操作的时候会调用到以上的方法。现在假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失

(2)删除键值对的方法

<span style="font-size: 18px;">      </span>// 删除“键为key”的元素      
    final Entry<K,V> removeEntryForKey(Object key) {      
        // 获取哈希值。若key为null,则哈希值为0;否则调用hash()进行计算      
        int hash = (key == null) ? 0 : hash(key.hashCode());      
        int i = indexFor(hash, table.length);      
        Entry<K,V> prev = table[i];      
        Entry<K,V> e = prev;      
     
        // 删除链表中“键为key”的元素      
        // 本质是“删除单向链表中的节点”      
        while (e != null) {      
            Entry<K,V> next = e.next;      
            Object k;      
            if (e.hash == hash &&      
                ((k = e.key) == key || (key != null && key.equals(k)))) {      
                modCount++;      
                size--;      
                if (prev == e)      
                    table[i] = next;      
                else     
                    prev.next = next;      
                e.recordRemoval(this);      
                return e;      
            }      
            prev = e;      
            e = next;      
        }      
     
        return e;      
    }  

(3)addEntry中当加入新的键值对后对总数量超过门限值的时候,会调用一个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);      
    } 

这个操作会新生成一个新的容量的数组,然后对原数组的所有键值对重新进行计算和写入新的数组,之后指向新生成的数组。

      当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。

 

是否提供contains方法?

HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解。

Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。

我们看一下Hashtable的ContainsKey方法和ContainsValue的源码:

public boolean containsValue(Object value) {      
     return contains(value);      
 } 
// 判断Hashtable是否包含“值(value)”      
 public synchronized boolean contains(Object value) {      
     //注意,Hashtable中的value不能是null,      
     // 若是null的话,抛出异常!      
     if (value == null) {      
         throw new NullPointerException();      
     }      
    
     // 从后向前遍历table数组中的元素(Entry)      
     // 对于每个Entry(单向链表),逐个遍历,判断节点的值是否等于value      
     Entry tab[] = table;      
     for (int i = tab.length ; i-- > 0 ;) {      
         for (Entry<K,V> e = tab[i] ; e != null ; e = e.next) {      
             if (e.value.equals(value)) {      
                 return true;      
             }      
         }      
     }      
     return false;      
 }  
// 判断Hashtable是否包含key      
 public synchronized boolean containsKey(Object key) {      
     Entry tab[] = table;      
/计算hash值,直接用key的hashCode代替    
     int hash = key.hashCode();        
     // 计算在数组中的索引值     
     int index = (hash & 0x7FFFFFFF) % tab.length;      
     // 找到“key对应的Entry(链表)”,然后在链表中找出“哈希值”和“键值”与key都相等的元素      
     for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {      
         if ((e.hash == hash) && e.key.equals(key)) {      
             return true;      
         }      
     }      
     return false;      
 }  

      下面我们看一下HashMap的ContainsKey方法和ContainsValue的源码:

// HashMap是否包含key      
    public boolean containsKey(Object key) {      
        return getEntry(key) != null;      
    }  
// 返回“键为key”的键值对      
    final Entry<K,V> getEntry(Object key) {      
        // 获取哈希值      
        // HashMap将“key为null”的元素存储在table[0]位置,“key不为null”的则调用hash()计算哈希值      
        int hash = (key == null) ? 0 : hash(key.hashCode());      
        // 在“该hash值对应的链表”上查找“键值等于key”的元素      
        for (Entry<K,V> e = table[indexFor(hash, table.length)];      
             e != null;      
             e = e.next) {      
            Object k;      
            if (e.hash == hash &&      
                ((k = e.key) == key || (key != null && key.equals(k))))      
                return e;      
        }      
        return null;      
    }  

 

// 是否包含“值为value”的元素      
    public boolean containsValue(Object value) {      
    // 若“value为null”,则调用containsNullValue()查找      
    if (value == null)      
            return containsNullValue();      
     
    // 若“value不为null”,则查找HashMap中是否有值为value的节点。      
    Entry[] tab = table;      
        for (int i = 0; i < tab.length ; i++)      
            for (Entry e = tab[i] ; e != null ; e = e.next)      
                if (value.equals(e.value))      
                    return true;      
    return false;      
    }  

 

Key和value是否允许null值

其中key和value都是对象,并且不能包含重复key,但可以包含重复的value。

通过上面的ContainsKey方法和ContainsValue的源码我们可以很明显的看出:

Hashtable中,key和value都不允许出现null值。但是如果在Hashtable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常,这是JDK的规范规定的。
HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。

两个遍历方式上的内部实现上不同

 

 Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。

hash值不同

     哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。

      hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值。

      Hashtable计算hash值,直接用key的hashCode(),而HashMap重新计算了key的hash值,Hashtable在求hash值对应的位置索引时,用取模运算,而HashMap在求位置索引时,则用与运算,且这里一般先用hash&0x7FFFFFFF后,再对length取模,&0x7FFFFFFF的目的是为了将负的hash值转化为正值,因为hash值有可能为负数,而&0x7FFFFFFF后,只有符号外改变,而后面的位都不变。

 

内部实现使用的数组初始化和扩容方式不同

 HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。
      Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。

      Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。

 

 

四、List集合

4.1 list集合的一些特性

1.List接口继承Collection接口,实现了List接口的类称为List集合。

2.在List集合中允许出现重复的元素,所有元素以线性方式进行存储,可以通过索引来访问集合中指定的元素。List集合的元素的存储顺序和取出顺序一致。

3.List不但继承了Collection接口中的全部方法,还增加了一些根据元素位置索引来操作集合的特有方法。

注:集合中不能定义为基本数据类型如(int、char、float……),应该定义为包装的数据类型(Integer、String...)

ArrayList集合和LinkedList集合的区别:

1、ArrayList集合底层是数组,而且是Object [] 类型;而LinkedList集合底层是链表。

2、ArrayList集合查询数据很快,但是增删数据很慢;LinkedList集合增删数据很快。但是查询数据很慢。

ist集合中常用方法:

add(Object object):向集合中添加数据

get(int index):获取集合中指定的索引位置的元素数值

size():获取集合的长度

isEmpty():判断集合是否为空

contains(Object object);//判断结合中是否含有指定的这个元素

set(int index, Object object):更改集合中指定索引位置的元素数值

toArray():将集合转换为数组

remove(int index):删除集合中指定索引位置的元素数值

clear():清空集合元素数值,谨慎使用

 

问题:在list中随机插入100个1-100的数,用set对这些数进行去重,最后按从小到大顺序输出来。

public class TestListAndTreeset {

    //定义一个函数,将list进行去重跟排序的操作
    public static List removeDulByTreeSet(List<Integer> list){
        TreeSet treeSet = new TreeSet(list);
        list.clear();
        list.addAll(treeSet);
        return list;
    }
    public static void main(String[] args) {

        List<Integer> list = new ArrayList();

        Random random = new Random();
        while(list.size()<100){
            int s = random.nextInt(101);
            list.add(s);
        }
        list = removeDulByTreeSet(list);

        for (int i = 0; i < list.size(); i++) {
            System.out.print(list.get(i)+" ");
        }
        System.out.println();
        System.out.println("总数:"+list.size());


    }

}

 

 

4.2 ArrayList集合

1.ArrayList是List接口的一个实现类,它是程序中最常见的一种集合类;

2.在ArrayList内部封装了一个数组对象,初始长度缺省为10,当存入的元素超过数组长度时,ArrayList会在
内存中分配一个更大的数组来重新容纳这些元素,因此可以将ArrayList集合看作一个长度可变的数组;

3.ArrayList集合类的大部分方法都是从父类Collection和List继承过来的,其中add()方法和get()方法用于
实现元素的添加和读取。

4.ArrayList集合的内部使用一个数组来保存元素。在删除元素时,会将被删除元素之后的元素都向前移一个位
置以填补空位;而在用add(intindex, Object element)方法添加元素时,是把元素插入index指向的位置,
先把该位置的元素以及后续元素都向后移一个位置,如果超出数组的容量,会创建更大的新数组。因为增删元
素会导致大量的内存操作,所以效率低,但ArrayList集合允许通过索引随机的访问元素,查询效率高。

5.集合和数组一样,索引的取值范围是从0开始,到size-1为止(size是集合的长度),不能超出此范围,否则
会引发异常。add(Objecto)方法是把元素添加到集合的尾部,而add(intindex, Object o)是把元素添加到由
索引index指定的位置。

代码测试,新建一个ListDemo测试一下

/**
 * @description:
 * @author: WEN
 * @create: 2020-08-16
 **/

public class ListDemo1 {
    public static void main(String[] args) {
        ArrayList alist = new ArrayList();

        //添加各种类型的元素到list集合
        alist.add(11);
        alist.add(22);
        alist.add(55);
        alist.add(33);
        alist.add(5.00);
        alist.add(6.00);
        alist.add(3.0);
        alist.add("字符串1");
        alist.add("字符串4");
        alist.add("字符串1");
        alist.add("字符串2");
        alist.add('A');
        alist.add('B');

        //获取集合的长度
        int length = alist.size();
        System.out.println("集合长度是:"+length);

        //判断集合是否为空
        boolean flag =  alist.isEmpty();
        
        //判断结合中是否还有55这个数据
        boolean flag2 =  alist.contains(55);

        //集合是否为空:false
        System.out.println("集合是否为空:"+flag);

        //集合中是否含有数字55:true
        System.out.println("集合中是否含有数字55:"+flag2);

        //获取集合下标为7的元素值
        Object obj = alist.get(7);
        System.out.println("集合下标为7的元素值:"+obj);

        //更改集合下标为4的元素的值,将其修改为:张无忌
        alist.set(4, "张无忌");

        Object[] obj2 = alist.toArray();
        System.out.println("数组obj2的数值是:");
        for (Object object : obj2) {
            System.out.println("obj2输出:"+object);
        }
        
        //删除集合下标为3的元素数值
        alist.remove(3);

        // alist.clear()  清空集合,谨慎使用
        System.out.println("alist集合的数值是:");
        for (Object object : alist) {
            System.out.println(object);
        }
    }
}

输出结果:

集合长度是:13
集合是否为空:false
集合中是否含有数字55:true
集合下标为7的元素值:字符串1
数组obj2的数值是:
obj2输出:11
obj2输出:22
obj2输出:55
obj2输出:33
obj2输出:张无忌
obj2输出:6.0
obj2输出:3.0
obj2输出:字符串1
obj2输出:字符串4
obj2输出:字符串1
obj2输出:字符串2
obj2输出:A
obj2输出:B
alist集合的数值是:
11
22
55
张无忌
6.0
3.0
字符串1
字符串4
字符串1
字符串2
A
B

 

给ArrayList指定类型

public class ListDemo2 {
    public static void main(String[] args) {
        ArrayList<String> alist = new ArrayList<>();

        //向集合alist中添加数据
        alist.add("赵云");
        alist.add("张飞");
        alist.add("亚索");
        alist.add("张君宝");
        alist.add("张敏");
        alist.add("周星驰");

        //获取集合的长度
        int length = alist.size();
        System.out.println("集合长度是:"+length);

        //判断集合是否为空
        boolean flag =  alist.isEmpty();

        //判断结合中是否还有55这个数据
        boolean flag2 =  alist.contains(55);
        //集合是否为空:false
        System.out.println("集合是否为空:"+flag);
        //集合中是否含有数字55:false
        System.out.println("集合中是否含有数字55:"+flag2);

        //获取集合下标为4的元素值
        Object obj = alist.get(4);
        //集合下标为4的元素值:字符串5
        System.out.println("集合下标为4的元素值:"+obj);

        Object[] obj2 = alist.toArray();

        for(Object object : obj2) {
            System.out.println("obj2输出:"+object);
        }

        alist.remove(2);
        alist.set(3, "我是超人");

        System.out.println("alist集合的数值是:");
        for (Object object : alist) {
            System.out.println(object);
        }
    }
}

输出结果:

集合长度是:6
集合是否为空:false
集合中是否含有数字55:false
集合下标为4的元素值:张敏
obj2输出:赵云
obj2输出:张飞
obj2输出:亚索
obj2输出:张君宝
obj2输出:张敏
obj2输出:周星驰
alist集合的数值是:
赵云
张飞
张君宝
我是超人
周星驰

4.3数组和list集合之间的转换

 

1、List的toArray()方法用于将集合转换成数组,但实际上改方法是在Collection中定义的,所以所有的集合都具备这个功能,

     其有两个方法:Object[] toArray()  和   T<T> []  toArray(T[] a)第二个方法是比较常用的 ,我们可以传入一个指定类型的数组,

    该数据的元素类型应与集合的元素类型一致,返回值则是转换后的数组,该数组会保存集合中的所有元素。

eg:

                   List<String> list = new ArrayList<String>();

                   list.add("a");

                   list.add("b");

                   list.add("c");

                   String[] strArr = list.toArray(new String[] {});

                   System.out.println(Arrays.toString(strArr)); // [a, b, c]

2、List将数组转换成

                ListString[] strArr = { "a", "b", "c" };

                List<String> list = Arrays.asList(strArr);

                System.out.println(list); // [a, b, c]

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: ListMapSetJava三个常用的集合接口,用于存储和操作数据。下面是它们在存取元素时的异同点: 1. 存放方式: - List是有序的集合,可以存放重复的元素,使用索引进行访问。 - Map是键值对的集合,每个元素都包含一个键和对应的值,键是唯一的。 - Set是无序的集合,不允许重复的元素。 2. 存取效率: - List使用索引进行访问,根据索引可以快速获取元素,但是在插入或删除元素时,需要移动其他元素来维护索引,效率较低。 - Map使用键值对的方式存储元素,根据键可以快速查找到对应的值,获取元素的效率比List高,插入和删除元素时也有较高的效率。 - Set使用哈希算法来存储元素,根据元素的哈希值来确定存放位置,查找和插入元素的效率都很高。 3. 元素的唯一性: - List允许存放重复的元素。 - Map的键是唯一的,值可以重复。 - Set不允许存放重复的元素,如果插入重复元素,会被自动去重。 4. 遍历方式: - List可以使用for循环、迭代器或者增强型for循环进行遍历。 - Map可以通过键遍历,也可以通过值遍历,还可以通过Entry遍历键值对。 - Set可以使用迭代器或者增强型for循环进行遍历。 需要注意的是,这些是它们的一般性特点,具体实现类的表现可能会有差异。 ### 回答2: ListMapSetJava集合框架常用的三个接口,用于存储和操作数据元素。它们各自有不同的特点和用途。 1. List(列表)是一个有序的集合,允许存储重复元素。常见的实现类有ArrayList和LinkedListList接口的元素是按照它们的插入顺序进行排序的。可以根据索引访问和修改元素。List适合于需要按顺序访问元素,而且可能包含重复元素的场景。 2. Map(映射)是一种键值对的集合,每个键唯一地映射到一个值。常见的实现类有HashMap和TreeMapMap接口提供了根据键查找对应值的功能,并且可以根据需要插入、删除或更新键值对。Map适合于需要通过键快速查找对应值的场景。 3. Set(集合)是一个不允许存储重复元素的集合。常见的实现类有HashSet和TreeSetSet接口的元素没有特定的顺序,且会自动去除重复元素。可以添加、删除和查询元素。Set适合于需要保持元素唯一性的场景。 在存取元素时,这三个接口有以下异同点: 1. 共同点: - 都可以用来存储元素,提供了添加、删除和查询元素的方法。 - 都支持迭代遍历元素,可以使用for-each循环或迭代器来访问集合的元素。 2. 不同点: - ListSet接口允许存储重复元素,而Map接口的键必须唯一。 - List接口的元素是有序的,可以根据索引访问和修改元素;而SetMap接口的元素没有特定的顺序。 - List接口提供了根据索引进行元素的插入和删除操作;而SetMap接口提供了根据元素值进行插入和删除操作。 - Map接口需要同时存储键和对应的值,通过键可以快速定位到值;而ListSet接口只需要存储元素值。 总结:ListMapSet三个接口在存取元素时的异同点主要在于允许存储重复元素与否、元素是否有序以及访问和操作元素的方式。具体使用时需要根据实际需求选择合适的接口。 ### 回答3: ListMapSetJava 常用的集合接口,用于存储和操作一组元素。以下为它们在存取元素时的异同点: 1. 异同点: - List 是有序的集合,可以包含重复元素,元素以插入的顺序排列;而 Map 是键值对的集合,元素是无序的,每个元素都有一个唯一的键;Set 是无序的集合,不能包含重复元素。 - ListSet 都继承自 Collection 接口,而 Map 则是独立的接口。 - List 使用索引访问元素,可以通过元素的索引值来定位和访问特定元素;Map 使用键来访问元素,每个元素都有一个键值对;Set 不允许使用索引或键来访问元素,只能通过迭代来获取元素。 - List 可以使用迭代器进行遍历;Map 可以通过迭代 Map 的键或值进行遍历;Set 可以通过迭代器或增强型 for 循环进行遍历。 2. 存取元素时的共同点: - 都支持添加、删除、修改和遍历元素。 - 都可以使用迭代器进行元素的迭代操作。 - 都可以使用增强型的 for 循环来遍历元素。 - 都不是线程安全的,如果在多线程环境下使用,需要保证线程的同步。 总之,ListMapSet 这三个接口在存取元素时的差异主要体现在集合的存储方式、元素的排序与去重规则以及访问元素的方式上。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值