Java集合


集合就是许多数据(啥类型的都有)存储在同一个“容器”中,那个“容器”好像就叫做集合,哈哈哈。
从数据库中提取出来数据,在后端存入一个集合,然后前端调用这个集合,然后一股脑的将该集合中的数据加载到页面上,让用户能看到数据。应该是这么回事。

一、Collection接口

Collection接口是List接口和Set接口的父接口,所以该接口中封装的方法,子接口可以照常使用。

以下方法都是基础的方法,详细的请查看API

    public static void main(String[] args) {
        /*
        Collection接口的常用方法:
        增加:add(E e) addAll(Collection<? extends E> c)
        删除:clear() remove(Object o)
        查看:iterator() size()
        判断:contains(Object o)  equals(Object o) isEmpty()
         */
        //创建对象:接口不能创建对象,利用实现类创建对象:该创建方法为多态的一种使用方法
        Collection col = new ArrayList();
        //调用方法:
        //集合有一个特点:只能存放引用数据类型的数据,不能是基本数据类型
        //基本数据类型自动装箱,对应包装类。int--->Integer
        col.add(18);   //向集合中添加数据
         //将这些数据转换成包装类,然后调用Arrays类中的asList方法转换为List集合
        List list = Arrays.asList(new Integer[]{11, 15, 3, 7, 1});  
        col.addAll(list);//将另一个集合添加入col中
        //col.clear();清空集合
        System.out.println("集合中元素的数量为:"+col.size());
        System.out.println("集合是否为空:"+col.isEmpty());    //返回值为布尔类型,true或false
        boolean isRemove = col.remove(15);      //删除值为15的元素
        System.out.println("集合中数据是否被删除:"+isRemove);  //返回值为布尔类型,true或false
        Collection col2 = new ArrayList();
        col2.add(18);
        col2.add(12);
        Collection col3 = new ArrayList();
        col3.add(18);
        col3.add(12);
        System.out.println(col2.equals(col3));  //调用equals方法比较两个集合中的元素是否相等,返回true或false
        System.out.println(col2==col3);//地址一定不相等  false
        System.out.println("是否包含元素:"+col3.contains(117));   //contain是否包含此元素,返回true或false  
    }

1.List接口(继承于Collection接口)(特点:不唯一,有序的)

因为List接口为有序的集合,所以在该集合中增加的方法都是一些与索引相关的方法。
增加:add(int index, E element)
删除:remove(int index) remove(Object o)
修改:set(int index, E element)
查看:get(int index)

List集合的遍历:

 //方式1:普通for循环:
        System.out.println("---------------------");
        for(int i = 0;i<list.size();i++){
            System.out.println(list.get(i));
        }
        //方式2:增强for循环:
        System.out.println("---------------------");
        for(Object obj:list){
            System.out.println(obj);
        }
        //方式3:迭代器:
        System.out.println("---------------------");
        //Iterator为迭代器,后续会介绍
        Iterator it = list.iterator();   
        while(it.hasNext()){
            System.out.println(it.next());

(1)、ArrayList类(底层为数组存储)

因为JDK1.7跟JDK1.8版本的ArrayList是不一样的,所以我会分别列举:
1.7版本:当在调用构造器时,数组初始化的长度为10,然后当数据超出初始化时,会自动扩容为原来的1,5倍,然后再将老数组指向新数组,在返回新数组。
1.8版本:当在调用构造器时,底层数组为null,只有在调用add方法以后底层的数组才重新复制新的数组,长度为10,也是扩容1.5倍。优点:节省内存
ArrayList的线程不安全,但是效率高
数组的优点:查询速度快,但是可以重复。
数组的缺点:删除,增加元素效率低

(2)、Vector类(已淘汰,底层为数组存储)

该类底层也是数组,但是该类扩容2倍,线程安全,效率低。

(3)、LinkeList类(底层为双向链表存储)

请添加图片描述
LinkedList的源码:

public class LinkedList<E>{//E是一个泛型,具体的类型要在实例化的时候才会最终确定
        transient int size = 0;//集合中元素的数量
        //Node的内部类
        private static class Node<E> {
        E item;//当前元素
        Node<E> next;//指向下一个元素地址
        Node<E> prev;//上一个元素地址
        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
        transient Node<E> first;//链表的首节点
        transient Node<E> last;//链表的尾节点
        //空构造器:
        public LinkedList() {
    }
        //添加元素操作:
        public boolean add(E e) {
        linkLast(e);
        return true;
    }
        void linkLast(E e) {//添加的元素e
        final Node<E> l = last;//将链表中的last节点给l 如果是第一个元素的话 l为null
                //将元素封装为一个Node具体的对象:
        final Node<E> newNode = new Node<>(l, e, null);
                //将链表的last节点指向新的创建的对象:
        last = newNode;
                
        if (l == null)//如果添加的是第一个节点
            first = newNode;//将链表的first节点指向为新节点
        else//如果添加的不是第一个节点 
            l.next = newNode;//将l的下一个指向为新的节点
        size++;//集合中元素数量加1操作
        modCount++;
    }
        //获取集合中元素数量
        public int size() {
        return size;
    }
        //通过索引得到元素:
        public E get(int index) {
        checkElementIndex(index);//健壮性考虑
        return node(index).item;
    }
        
    Node<E> node(int index) {
        //如果index在链表的前半段,那么从前往后找
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {//如果index在链表的后半段,那么从后往前找
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
}

2.Set接口(继承于Collection接口)(特点:唯一,无序的)

该接口因为是无序的(而非随机,而是根据底层的算法来进行排序),所以没有与索引相关方法。
遍历的方法:使用Iterator迭代器迭代;增强for循环遍历。

(1)、HashSet类(底层为哈希表(数组+链表)存储)

该类下还有一个实现类为:LinkedHashSet实现类。
该类的特点为:唯一,而且有序的(按照输入的顺序输出)。

该类底层是HashMap类。

(2)、TreeSet类(底层为二叉树存储)

该类的特点:唯一,有序的(是按照升序的排序进行输出),无序(没有按照输入的方式进行输出)。
底层原理:实现内部比较器(compareTo)或者外部比较器(compare)

在这里插入图片描述

当调用TreeSet的空构造器时,底层创建了一个TreeMap。

二、Map接口

Map接口的常用方法:

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
        增加:put(K key, V value)
        删除:clear() remove(Object key)
        修改:
        查看:entrySet() get(Object key) keySet() size() values()
        判断:containsKey(Object key) containsValue(Object value)
            equals(Object o) isEmpty()
         */
        //创建一个Map集合:无序,唯一
        Map<String,Integer> map = new HashMap<>();
        System.out.println(map.put("lili", 10101010));
        map.put("nana",12345234);
        map.put("feifei",34563465);
        System.out.println(map.put("lili", 34565677));
        map.put("mingming",12323);
        /*map.clear();清空*/
        /*map.remove("feifei");移除*/
        System.out.println(map.size());
        System.out.println(map);
        System.out.println(map.containsKey("lili"));
        System.out.println(map.containsValue(12323));
        Map<String,Integer> map2 = new HashMap<>();
        System.out.println(map2.put("lili", 10101010));
        map2.put("nana",12345234);
        map2.put("feifei",34563465);
        System.out.println(map2.put("lili", 34565677));
        map2.put("mingming2",12323);
        System.out.println(map==map2);
        System.out.println(map.equals(map2));//equals进行了重写,比较的是集合中的值是否一致
        System.out.println("判断是否为空:"+map.isEmpty());
        System.out.println(map.get("nana"));
        System.out.println("-----------------------------------");
        //keySet()对集合中的key进行遍历查看:
        Set<String> set = map.keySet();
        for(String s:set){
            System.out.println(s);
        }
        System.out.println("-----------------------------------");
        //values()对集合中的value进行遍历查看:
        Collection<Integer> values = map.values();
        for(Integer i:values){
            System.out.println(i);
        }
        System.out.println("-----------------------------------");
        //get(Object key) keySet()
        Set<String> set2 = map.keySet();
        for(String s:set2){
            System.out.println(map.get(s));
        }
        System.out.println("-----------------------------------");
        //entrySet()
        Set<Map.Entry<String, Integer>> entries = map.entrySet();
        for(Map.Entry<String, Integer> e:entries){
            System.out.println(e.getKey()+"----"+e.getValue());
        }
    }
}

1、HashMap类(底层为哈希表(数组+链表)存储)

HashMap实现类的特点为:无序,但唯一。
该实现类按照key值进行排序总结,因底层key是按照哈希表的结构进行排序。

在这里插入图片描述
所以当放入这个集合的数据对应的类时,必须重写HashCode方法(将key进行哈希运算)和equals方法(判断维不唯一)。

HashMap源码详解:

public class HashMap<K,V>
    extends AbstractMap<K,V> //【1】继承的AbstractMap中,已经实现了Map接口,这个地方属于重复使用
    implements Map<K,V>, Cloneable, Serializable{
    static final int DEFAULT_INITIAL_CAPACITY = 16;//哈希表主数组的默认长度
        //定义了一个float类型的变量,以后作为:默认的装填因子,加载因子是表示Hsah表中元素的填满的程度
        //太大容易引起哈西冲突,太小容易浪费  0.75是经过大量运算后得到的最好值
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
        transient Entry<K,V>[] table;//主数组,每个元素为Entry类型
        transient int size;
        int threshold;//数组扩容的界限值,门槛值   16*0.75=12 
        final float loadFactor;//用来接收装填因子的变量
        
        //【4】查看构造器:内部相当于:this(16,0.75f);调用了当前类中的带参构造器
        public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
        //【5】本类中带参数构造器:--》作用给一些数值进行初始化的!
        public HashMap(int initialCapacity, float loadFactor) {
        //【6】给capacity赋值,capacity的值一定是 大于你传进来的initialCapacity 的 最小的 2的倍数
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;
                //【7】给loadFactor赋值,将装填因子0.75赋值给loadFactor
        this.loadFactor = loadFactor;
                //【8】数组扩容的界限值,门槛值
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
                
                //【9】给table数组赋值,初始化数组长度为16
        table = new Entry[capacity];
                   
    }
        //【10】调用put方法:
        public V put(K key, V value) {
                //【11】对空值的判断
        if (key == null)
            return putForNullKey(value);
                //【12】调用hash方法,获取哈希码
        int hash = hash(key);
                //【14】得到key对应在数组中的位置
        int i = indexFor(hash, table.length);
                //【16】如果你放入的元素,在主数组那个位置上没有值,e==null  那么下面这个循环不走
                //当在同一个位置上放入元素的时候
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
                        //哈希值一样  并且  equals相比一样   
                        //(k = e.key) == key  如果是一个对象就不用比较equals了
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
                //【17】走addEntry添加这个节点的方法:
        addEntry(hash, key, value, i);
        return null;
    }
        
        //【13】hash方法返回这个key对应的哈希值,内部进行二次散列,为了尽量保证不同的key得到不同的哈希码!
        final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }
                //k.hashCode()函数调用的是key键值类型自带的哈希函数,
                //由于不同的对象其hashCode()有可能相同,所以需对hashCode()再次哈希,以降低相同率。
        h ^= k.hashCode();
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
                /*
                接下来的一串与运算和异或运算,称之为“扰动函数”,
                扰动的核心思想在于使计算出来的值在保留原有相关特性的基础上,
                增加其值的不确定性,从而降低冲突的概率。
                不同的版本实现的方式不一样,但其根本思想是一致的。
                往右移动的目的,就是为了将h的高位利用起来,减少哈西冲突
                */
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
        //【15】返回int类型数组的坐标
        static int indexFor(int h, int length) {
                //其实这个算法就是取模运算:h%length,取模效率不如位运算
        return h & (length-1);
    }
        //【18】调用addEntry
        void addEntry(int hash, K key, V value, int bucketIndex) {
                //【25】size的大小  大于 16*0.75=12的时候,比如你放入的是第13个,这第13个你打算放在没有元素的位置上的时候
        if ((size >= threshold) && (null != table[bucketIndex])) {
                        //【26】主数组扩容为2倍
            resize(2 * table.length);
                        //【30】重新调整当前元素的hash码
            hash = (null != key) ? hash(key) : 0;
                        //【31】重新计算元素位置
            bucketIndex = indexFor(hash, table.length);
        }
                //【19】将hash,key,value,bucketIndex位置  封装为一个Entry对象:
        createEntry(hash, key, value, bucketIndex);
    }
        //【20】
        void createEntry(int hash, K key, V value, int bucketIndex) {
                //【21】获取bucketIndex位置上的元素给e
        Entry<K,V> e = table[bucketIndex];
                //【22】然后将hash, key, value封装为一个对象,然后将下一个元素的指向为e (链表的头插法)
                //【23】将新的Entry放在table[bucketIndex]的位置上
        table[bucketIndex] = new Entry<>(hash, key, value, e);
                //【24】集合中加入一个元素 size+1
        size++;
    }
    //【27】
        void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
                //【28】创建长度为newCapacity的数组
        Entry[] newTable = new Entry[newCapacity];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
                //【28.5】转让方法:将老数组中的东西都重新放入新数组中
        transfer(newTable, rehash);
                //【29】老数组替换为新数组
        table = newTable;
                //【29.5】重新计算
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
        //【28.6】
        void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                                //【28.7】将哈希值,和新的数组容量传进去,重新计算key在新数组中的位置
                int i = indexFor(e.hash, newCapacity);
                                //【28.8】头插法
                e.next = newTable[i];//获取链表上元素给e.next
                newTable[i] = e;//然后将e放在i位置 
                e = next;//e再指向下一个节点继续遍历
            }
        }
    }
}
HashMap效率高,但线程不安全。key可以存入null值,并且key的null值也遵循唯一的特点。
在JDK1.2之前有个Hashtable实现类,但是它的效率低,线程安全,并且key不可以存入null值。

2、TreeMap类(底层为二叉树存储)

TreeMAp底层为二叉树,所以遵照二叉树的特点,放入集合的key的数据对应的类型内部一定要实现比较器(内部或外部比较器)。

补充泛型的相关知识

泛型就相当于标签
形式:<>
集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在JDK1.5之前只能把元素类型设计为Object,
JDK1.5之 后使用泛型来解决。因为这个时候除了元素的类型不确定,其他的部分是确定的,例如关于这个元素如何保存,如何管理等是确定的,因此此时把元素的类型设计成一个参数,这个类型参数叫做泛型。
Collection,List,ArrayList中的就是类型参数,就是泛型。
示例代码:

import java.util.ArrayList;
public class Test01 {
    //这是main方法,程序的入口
    public static void main(String[] args) {
        //创建一个ArrayList集合,向这个集合中存入学生的成绩:
        //加入泛型的优点:在编译时期就会对类型进行检查,不是泛型对应的类型就不可以添加入这个集合。
        ArrayList<Integer> al = new ArrayList<Integer>();
        al.add(98);
        al.add(18);
        al.add(39);
        al.add(60);
        al.add(83);
        /*al.add("丽丽");
        al.add(9.8);*/
        //对集合遍历查看:
        /*for(Object obj:al){
            System.out.println(obj);
        }*/
        for(Integer i:al){
            System.out.println(i);
        }
    }
}

· 泛型引用的必须都是引用数据类型,不能是基本数据类型。

·ArrayList<Integer> al = new ArrayList<Integer>();在JDK1.7之后可以写钻石运算符:ArrayList<Integer> al = new ArrayList<>();
`泛型继承关系:当父类指定了泛型,子类可以直接使用;当父类没指定泛型时,那么子类可以在创建子类对象的时候指定泛型。
·泛型类可以定义多个参数。例如:
	public class TestGeneric<A,B,C>{
	A age ; B name ; C sex ; 
	public void a(A m , B n , C x ){
		
		}
	}
·静态方法不能使用类的泛型;也就是有static关键词修饰的方法
·泛型如果没被指定,那么系统就会把他擦除。

泛型方法的含义:没带泛型的方法为泛型方法。
泛型方法对应的参数类型与当前所在的这个类是否为泛型类,是否为泛型无关。
· 泛型方法的定义前边要加上< T >,< T >在调用方法的时候确认。泛型方法可以是静态方法。
示例:

public static <T>  void b(T t){
    }

泛型的通配符<?>的作用:当G< A >和G< B >不存在父子关系时,加入G<?>就会变成他俩的父类。
List<? extends Person>:
就相当于:
List<? extends Person>是List的父类,是List<Person的子类>的父类

List<? super Person>就相当于:List<? super Person>是List的父类,是List<Person的父类>的父类

补充二叉树的比较器compareTo方法的与原理及使用

比较器分为内部比较器compareTo()跟外部比较器compareTo()

String类实现了Comparable接口,这个接口中有一个抽象方法compareTo,String类中重写这个方法即可。
内部比较器的使用逻辑代码里重写compareTo()方法。
外部比较器是创建单独的类来重写compare()方法。

问题1:Iterator迭代器与ListIterator迭代器的区别

Iterator只是单纯的迭代数据。
当程序需要对集合进行迭代加修改双重操作时,就会使用ListIterator。

问题2:iterator(),Iterator,Iterable的关系

请添加图片描述

HashMap中的装填因子是0.75的原因

这时我们得考虑时间跟空间的问题

如果装填因子为1,那么就证明需要等到数组满了时候才能进行数组2倍扩容,这样可以做到最大的空间利用率,但是现实中元素不可能完全的均匀分布,那就很可能发生哈希碰撞,就会产生链表,产生链表就会使查询速度变慢。
所以说当装填因子为1时,能做到最大的空间利用率,但是时间会变慢。

如果装填因子变小,变到0.5时,也就说当元素数量增加到数组容量的一半时就会进行2倍的数组扩容,这样做可以减少哈希碰撞,若不产生链表,那么查询的效率很高,但是会浪费空间。
所以当装填因子为0.5时,查询时间会很快,但是空间会浪费。

所以我们会在空间和时间中取中间值0.75来平衡这个因素。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值