数组与集合

数组与集合

数组与集合区别

  • 数组是大小固定的,并且同一个数组只能存放类型一样的数据(基本类型/引用类型),而JAVA集合可以存储和操作数目不固定的一组数据。 所有的JAVA集合都位于 java.util包中, JAVA集合只能存放引用类型的的数据,不能存放基本数据类型。

  • 数组与集合的区别(参考文章:《Thinking In Algorithm》03.数据结构之数组):

    • 有人想有可以自动扩展的数组,所以有了List ;有的人想有没有重复的数组,所以有了set ;有人想有自动排序的组数,所以有了TreeSet,TreeList,Tree** -》而几乎有有的集合都是基于数组来实现的. 因为集合是对数组做的封装,所以,数组永远比任何一个集合要快
    • 但任何一个集合,比数组提供的功能要多:元素类型不声明;动态大小;数字没法只读,集合提供了ReadOnly方法可以返回只读

java中的数组(即Array)

java所有“存储及随机访问一连串对象”的做法,array是最有效率的一种.

  • 效率高,但容量固定且无法动态改变。
    array还有一个缺点是,无法判断其中实际存有多少元素,length只是告诉我们array的容量

例如:声明数组

    String [] arr;
    int arr1[];
    String[] array=new String[5];
    int score[]=new int[3];

也可以直接赋值:

    int[] m = { 1, 2, 3 };
    String[] strings = { "aaa", "bbb" };
  • Java中有一个Arrays类,专门用来操作数组array。
    方法 作用
    equals() 比较两个array是否相等。array拥有相同元素个数,且所有对应元素两两相等。
    fill() 将值填入array中。
    sort() 用来对array进行排序。
    binarySearch() 在排好序的array中寻找元素。
    System.arraycopy() array的复制。

示例

    int[] arr3=new int[5];
    Arrays.fill(arr3, 10);  //将数组全部填充10
    //遍历输出
    for (int i = 0; i < arr3.length; i++) {
        System.out.println(arr3[i]);
    }
    int[] arr4 = {3, 7, 2, 1, 9};
    Arrays.sort(arr4);         //.sort(int[] a)   放入数组名字排序
    for (int i = 0; i < arr4.length; i++) {
        System.out.println(arr4[i]);
    }

java中的集合

若撰写程序时不知道究竟需要多少对象,需要在空间不足时自动扩增容量,则需要使用容器类库,array不适用。所以就要用到集合

  • 集合分类:Collection:List、Set || Map:HashMap、HashTable

  • 集合 List Set Map的区别
    Java中的集合包括三大类,它们是Set、List和Map,它们都处于java.util包中,Set、List和Map都是接口,它们有各自的实现类。Set的实现类主要有HashSet和TreeSet,List的实现类主要有ArrayList,Map的实现类主要有HashMap和TreeMap。

    • List中的对象按照索引位置排序,可以有重复对象,允许按照对象在集合中的索引位置检索对象,如通过list.get(i)方式来获得List集合中的元素
    • Set中的对象不按特定方式排序,并且没有重复对象。但它的有些实现类能对集合中的对象按特定方式排序,例如TreeSet类,它可以按照默认排序,也可以通过实现java.util.Comparator来自定义排序方式。
    • Map中的每一个元素包含一个键对象和值对象,它们成对出现。键对象不能重复,值对象可以重复。
  • 总结

    • Collection 和 Map 的区别:容器内每个为之所存储的元素个数不同。Collection类型者,每个位置只有一个元素。Map类型者,持有 key-value pair,像个小型数据库

    • 各自旗下的子类关系:

      • Collection

        • List:将以特定次序存储元素。所以取出来的顺序可能和放入顺序不同
          ArrayList / LinkedList / Vector,Stack继承自Vector(其中Vector和Stack是线程安全的)
        • Set : 不能含有重复的元素
          HashSet / TreeSet
        • Collection集合也有一个专门的操作类Collections
      • Map(Hashtable 既不允许 null 键也不允许 null 值但 HashMap 允许任意数量的 null 值和最多一个 null 键。另外HashTable是线程安全的)

        • HashMap
        • HashTable
        • TreeMap
  • 集合的范例及使用

定义student对象,主要属性如下:

    public class Student {
        // 成员变量
        private String name;
        private int age;
        private int id;
        public Student(String name, int age, int id) {
            this.name = name;
            this.age = age;
            this.id = id;
        }
        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 int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
    }

集合使用

    public class Step1TestMain {
        public static void main(String[] args) {
            // 创建5个学生对象,并赋值。
            Student s1 = new Student("小明", 27);
            Student s2 = new Student("小红", 30);
            Student s3 = new Student("小强", 30);
            Student s4 = new Student("旺财", 12);
            Student s5 = new Student("张三", 35);
            //List测试,主要是ArrayList
            List<Student> list = new ArrayList<Student>();
            list.add(s1);
            list.add(s2);
            list.add(s3);
            list.add(s4);
            list.add(s5);
            //s1再次添加,list是不判断重复的      
            list.add(s1);
            //List两种遍历方式,输出一致
            for (Student student : list) {
                System.out.println(student.toString());
            }
            //遍历方式2
            for (int i=0; i< list.size(); i++) {
                System.out.println(list.get(i).toString());
            }
            //Set测试,主要是HashSet
            Set<Student> set = new HashSet<Student>();
            set.add(s1);
            set.add(s2);
            set.add(s3);
            set.add(s4);
            set.add(s5);
            set.add(s1);
            System.out.println("set不包含重复:"+ set.size());
            //HashMap测试,每次输出结果都不一致,内部是无序的
            Map<String, Student> map = new HashMap<String, Student>();
            map.put(s1.getName(), s1);
            map.put(s2.getName(), s2);
            map.put(s3.getName(), s3);
            for (Entry<String, Student> entry : map.entrySet()) {
                System.out.println(entry.getValue().toString());
            }
        }
    }

集合类的详细说明

List中删除元素

【答案解析】
推荐采用iterator进行遍历,发现符合条件的用iterator.remove进行删除,如下:

    Iterator<String> it = list.iterator();
    while(it.hasNext()){
        String x = it.next();
        if(x.equals("del")){
            it.remove();
        }
    }

这种方式可以正常的循环及删除。但要注意的是,使用iterator的remove方法,如果用list的remove方法同样会报上面提到的ConcurrentModificationException错误。

如果只删除一个元素的话也可以for循环然后找到要删除的元素,调用list.remove(obj)的方式进行删除,如下,一般建议只用iterator的方式:

    //方式1:for循环遍历list
    for(int i=0;i<list.size();i++){
        if(list.get(i).equals("del"))
            list.remove(i);
    }
    //这种方式的问题在于,删除某个元素后,list的大小发生了变化,而你的索引也在变化,所以会导致你在遍历的时候漏掉某些元素。比如当你删除第1个元素后,继续根据索引访问第2个元素时,因为删除的关系后面的元素都往前移动了一位,所以实际访问的是第3个元素。因此,这种方式可以用在删除特定的一个元素时使用,但不适合循环删除多个元素时使用。
    //方式2:增强for循环
    for(String x:list){
        if(x.equals("del"))
            list.remove(x);
    }
    //这种方式的问题在于,删除元素后继续循环会报错误信息ConcurrentModificationException,因为元素在使用的时候发生了并发的修改,导致异常抛出。但是删除完毕马上使用break跳出,则不会触发报错。
    //方式1和方式2只是用来扩充思路,实际一般不采用

HashMap遍历

结合安全性和效率一般使用迭代器 EntrySet方法(KeySet需要再次遍历HashMap,效率低)

	HashMap<Integer, String> map=new HashMap<>();
        map.put(1,"赵");
        map.put(2,"钱");
        map.put(3,"孙");
        map.put(4,"李");
        // 迭代器 EntrySet
        Iterator<Map.Entry<Integer, String>>iterator=map.entrySet().iterator();
        while(iterator.hasNext()){
        Map.Entry<Integer, String> entry=iterator.next();
        System.out.println(entry.getKey()+":"+entry.getValue());
        }
        // 迭代器 KeySet
        Iterator<Integer> iterator2=map.keySet().iterator();
        while(iterator.hasNext()){
        Integer key=iterator2.next();
        System.out.println(key+":"+map.get(key));
        }
        // ForEach EntrySet
        for(Map.Entry<Integer, String> entry:map.entrySet()){
        System.out.println(entry.getKey()+":"+entry.getValue());
        }
        // ForEach KeySet
        for(Integer key:map.keySet()){
        System.out.println(key+":"+map.get(key));
        }
        // lambda
        map.forEach(
        (key,value)->{
        System.out.println(key+":"+value);
        });
        // stream
        map.entrySet().stream()
        .forEach(
        (entry)->{
        System.out.println(entry.getKey()+":"+entry.getValue());
        });

Collection和Collections区别

【参考答案】
java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。

  • Collection
    • List
      • LinkedList
      • ArrayList
      • Vector
        • Stack
    • Set

java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。

List排序

Java中List排序的3种方法!

利用Collections类的 java.util.Collections.sort(java.util.List, java.util.Comparator) 方法,自定义比较器对象对指定对象进行排序

        ArrayList list=new ArrayList();
        list.add(new Student(1,"石一歌",22));
        list.add(new Student(2,"茉小彪",23));
        list.add(new Student(3,"小林",24));
        Collections.sort(
        list,
        (Comparator<Student>)(s1,s2)->{
        // 排序规则:按照汉字拼音首字母排序
        Comparator<Object> com=Collator.getInstance(Locale.CHINA);
        return com.compare(s1.getName(),s2.getName());
        });

        list.forEach(x->System.out.println(x.toString()));

通过实现Comparable接口重写compareTo方法来实现list的排序

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

  .....

  @Override
  public int compareTo(Student o) {
    Comparator<Object> com = Collator.getInstance(Locale.CHINA);
    return com.compare(this.getName(), o.getName());
  }
}
        ArrayList list=new ArrayList();
        list.add(new Student(1,"石一歌",22));
        list.add(new Student(2,"茉小彪",23));
        list.add(new Student(3,"小林",24));
        Collections.sort(list);
        list.forEach(x->System.out.println(x.toString()));

集合的内部源码

HashMap实现原理

  • 关键点:

    • 使用“链地址法”解决哈希冲突,内部使用数组(Entry数组)实现,每个位置是包含一个key-value键值对的Entry基本组成单元,放入元素的过程主要有两步

    • put

      • HashMap 通过 key 的 hashCode 经过 hash() 处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置
        • 如果当前位置无元素的话直接放在当前位置
        • 如果当前位置有元素的话,通过key的equals方法进行判断,如果返回true的话直接更新当前位置,如果false的话需遍历链表,存在即覆盖,否则新增,此时链表时间复杂度为O(n)
    • get

      • HashMap 通过 key 的 hashCode 经过 hash() 处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置
        • 如果该位置无链表的话直接返回
        • 如果该位置有链表的话需遍历链表,然后通过key对象的equals方法逐一比对查找
  • HashMap 的长度为什么是 2 的幂次方

    在指定容量的情况下,容量为不小于指定容量的2的幂数,元素超过阈值会扩容到原来的两倍

    默认容量16,阈值0.75

    • 数组计算下标方法 key 的 hashCode 经过 hash() 处理过后得到的值对数组长度取余,即 hash%length
    • 在length=2^n 的前提下,hash%length可转化为hash&(length-1),以此来提高效率和减少冲突
  • 100个元素不扩容,初始设置元素为多少

    • 100/0.75=133,128<133<256
    • 设置元素范围[129,256]
  • HashMap整体结果如下图:

  • 155300385071512
    【备注】jdk1.8起,如果链表的key数量不超过8仍然使用链表,如果超过了8个将链表转换为红黑树,用来解决链表比较长的时候查找性能下降的问题。

HashSet实现原理

HashSet是一个没有重复元素的集合,HashSet其实是由HashMap实现的,HashMap中保存的是键值对,然而我们只能向HashSet中添加key,原因在于HashSet的Value其实都是同一个对象,这是HashSet添加元素的方法,可以看到辅助实现HashSet的map中的value其实都是Object类的同一个对象。
如下:

    private static final Object PRESENT = new Object();
    public boolean add(E e) {
    	return map.put(e, PRESENT)==null;
    }

TreeMap实现原理

内部使用红黑树来进行排序的,每个元素是Entry的结构,如下:

static final class Entry<K,V> implements Map.Entry<K,V> {
       K key;
       V value;
       // 左孩子节点
       Entry<K,V> left = null;
       // 右孩子节点
       Entry<K,V> right = null;
       // 父节点
       Entry<K,V> parent;
       // 红黑树用来表示节点颜色的属性,默认为黑色
       boolean color = BLACK;
}

为了维护数据的唯一性。在存入数据的时候,如果在构造方法中传递了Comparator对象,那么就会以Comparator对象的方法进行比较。否则,则使用Comparable的 compareTo(T o)方法来比较。

具体的实现是:调用比较方法,返回-1 的时候,添加到左子树,返回1 的时候 添加到 右子树。返回0 有相同数据 不添加该元素!
155300389351114
创建时传入迭代器代码如下:

        // 比较器
        private final Comparator<? super K> comparator;
        // 红黑树根节点
        private transient Entry<K,V> root = null;
    	//默认构造方法,comparator为空,代表使用key的自然顺序来维持TreeMap的顺序,这里要求key必须实现Comparable接口
        public TreeMap() {
            comparator = null;
        }
    	//用指定的比较器构造一个TreeMap
        public TreeMap(Comparator<? super K> comparator) {
            this.comparator = comparator;
        }
    	// 构造一个指定map的TreeMap,同样比较器comparator为空,使用key的自然顺序排序
        public TreeMap(Map<? extends K, ? extends V> m) {
            comparator = null;
            putAll(m);
        }

添加元素的时候,如果comparator不为空会调用compare方法;否则会调用key的compareTo方法,这要求key必须实现Comparable接口
代码如下:

    //传入的比较迭代器
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        ...
        cmp = cpr.compare(key, t.key);
        ...
    } else {
        if (key == null)
            throw new NullPointerException();
        Comparable<? super K> k = (Comparable<? super K>) key;
        这段代码的意思是:
        1. K implements Comparable is ok
        2. That if any super class of K has implemented  Comparable is OK too
        ...
         cmp = k.compareTo(t.key);
        ...
    }

TreeSet实现原理

TreeSet也是没有重复的排序集合,TreeSet也是由TreeMap实现的,这个跟HashSet的实现方式是类似的。

ConCurrentHashMap实现原理:

ConcurrentHashMap和Hashtable主要区别就是围绕着锁的粒度以及如何锁
58adc9e7b4725349c149a
ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁当前需要用到的桶。试想,原来只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制,之后会提到),并发性的提升是显而易见的。
更令人惊讶的是ConcurrentHashMap的读取并发,因为在读取的大多数时候都没有用到锁定,所以读取操作几乎是完全的并发操作,而写操作锁定的粒度又非常细,比起之前又更加快速(这一点在桶更多时表现得更明显些)。只有在求size等操作时才需要锁定整个表。

【了解】:在迭代时,ConcurrentHashMap使用了不同于传统集合的快速失败迭代器的另一种迭代方式,我们称为弱一致迭代器。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。

ArrayList实现原理

ArrayList基于数组实现,

  • ArrayList 实际上是通过一个数组去保存数据的。当我们构造ArrayList时;若使用默认构造函数,则ArrayList的默认容量大小是10。
  • 当ArrayList容量不足以容纳全部元素时,ArrayList会重新设置容量:新的容量=“(原始容量x3)/2 + 1”。
  • ArrayList的克隆函数,即是将全部元素克隆到一个数组中。
  • ArrayList实现java.io.Serializable的方式。当写入到输出流时,先写入“容量”,再依次写入“每一个元素”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。

LinkedList实现原理

LinkedList基于双向链表实现

  • LinkedList 实际上是通过双向链表去实现的。
    它包含一个非常重要的内部类:Entry。Entry是双向链表节点所对应的数据结构,它包括的属性有:当前节点所包含的值,上一个节点,下一个节点。
  • 从LinkedList的实现方式中可以发现,它不存在LinkedList容量不足的问题。
  • LinkedList的克隆函数,即是将全部元素克隆到一个新的LinkedList对象中。

155300392321460

Vector实现原理

Vector是ArrayList的线程安全版本,vector是线程(Thread)同步(Synchronized)的,此种实现方式(即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性)需要很高的花费,因此,访问它比访问ArrayList慢

Stack实现原理

Stack是java实现的栈,Stack继承于Vector,意味着Vector拥有的属性和功能,Stack都拥有, Stack也是线程安全的。

equals()相等的两个对象,hashcode是否相同,为什么

【参考答案】:
不对,理解完集合类的内部实现后对这个将非常明确。
如果两个对象x和y满足x.equals(y) == true,它们的哈希码(hash code)应当相同。Java对于eqauls方法和hashCode方法是这样规定的:

  • 如果两个对象相同(equals方法返回true),那么它们的hashCode值一定要相同;
  • 如果两个对象的hashCode相同,它们并不一定相同。当然,你未必要按照要求去做,但是如果你违背了上述原则就会发现在使用容器时,相同的对象可以出现在Set集合中,同时增加新元素的效率会大大下降(对于使用哈希存储的系统,如果哈希码频繁的冲突将会造成存取性能急剧下降)。
posted @ 2022-06-07 21:20  Faetbwac  阅读(67)  评论(0编辑  收藏  举报