Java集合

目录

1.Collection

List

ArrayList,实现了List接口,继承自AbstractList类

LinkedList,实现了List接口,继承自AbstractSequentialList

总结ArrayList与LinkedList区别

Set

HashSet,实现了Set接口,继承自AbstractSet类

2.Map

HashMap,继承AbstractMap类,实现Map接口


Java的集合类主要由三个接口派生而出,即Collection,Map接口,Iterator接口。

分别讲讲

1.Collection

collection可以认为是一个顶级接口,也就是父接口,然后它下面又有子接口,为List与Set等,只谈常用的。

List

List<Integer> list = new List<Integer>();//错误,List是一个接口,不可以实例化;

List是继承自Collection的一个子接口,它肯定也重写了Collect类的所有抽象的方法。
List集合代表一个元素是有序的且可以重复的集合,集合中每个元素都有其对应的顺序索引|;
List集合允许添加重复元素,可以通过索弓|来访问指定位置的集合元素。
List接口有两个最为常用的实现类ArrayList和LinkedList。

ArrayList,实现了List接口,继承自AbstractList类

ArrayList 是一个数组队列,相当于 动态数组。与Java中的数组相比,它的容量能动态增长。
ArrayList中的操作线程不安全的!所以,建议在单线程中才使用ArrayList,而在多线程中可以选择CopyOnWriteArrayList。

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

简介add,remove,set方法使用
public class Demo {
    public static void main (String args[]){
        List list = new ArrayList();
        list.add("亚瑟");
        list.add("安琪拉");
        list.add("口红"); 
        System.out.println(list.get(0)); //输出其中一个元素写法
        System.out.println("==================");
        System.out.println("插入前list集合的元素输出顺序"+list);
        list.add(2,"百里"); //在指定位置插入元素,后面的元素都往后移一个元素
        System.out.println("==================");
        System.out.println("插入后list集合的元素输出顺序"+list);
        System.out.println("==================");
        list.remove(2);  //删除指定索引的对象
        System.out.println("删除索引2后的list集合元素输出顺序"+list);
        list.set(2, "口红红"); //在索引为index位置的元素更改为element元素
        System.out.println("删除索引2后的list集合元素输出顺序"+list);
    }
}
/*
 亚瑟
==================
插入前list集合的元素输出顺序[亚瑟, 安琪拉, 口红]
==================
插入后list集合的元素输出顺序[亚瑟, 安琪拉, 百里, 口红]
==================
删除索引2后的list集合元素输出顺序[亚瑟, 安琪拉, 口红]
删除索引2后的list集合元素输出顺序[亚瑟, 安琪拉, 口红红]
*/

List<EsubList(int fromIndex, int toIndex)
返回从索引fromIndex到toIndex的元素集合,包左不包右
public class Demo {
    public static void main (String args[]){
        List list1 = new ArrayList();
        List list2 = new ArrayList();
        list1.add("亚瑟");
        list1.add("安琪拉");
        list1.add("口红"); 
        list2 = list1.subList(0, 1);
        System.out.println(list2); //输出其中一个元素写法
        
    }
}

int indexOf(Object o)方法使用
返回list集合中第一次出现o对象的索引位置,如果list集合中没有o对象,那么就返回-1

public class Demo {
    public static void main (String args[]){
        List list1 = new ArrayList();
        list1.add("亚瑟");
        list1.add("安琪拉");
        list1.add("口红"); 
        int i = list1.indexOf("口红");
        System.out.println("第一次出现 口红 的位置在"+i); 
        int i2 = list1.indexOf("口红红");
        System.out.println("第一次出现 口红红 的位置在"+i2);   //这里由于找不到于是就返回-1
    }
}
/*
 第一次出现 口红 的位置在2
第一次出现 口红红 的位置在-1
*/

boolean addAll(int index, Collection<? extends E> c)
在指定的位置中插入c集合全部的元素,如果集合发生改变,则返回true,否则返回false。
意思就是当插入的集合c没有元素,那么就返回false,如果集合c有元素,插入成功,那么就返回true

public class Demo {
    public static void main (String args[]){
        List list1 = new ArrayList();
        List list2 = new ArrayList();
        List list3 = new ArrayList();  //写一个空集合看看addAll会输出false
        list1.add("亚瑟");
        list1.add("安琪拉");
        list1.add("口红"); 
        list2.add("亚瑟2");
        list2.add("安琪拉2");
        list2.add("口红2");
        boolean b = list1.addAll(list2);
        System.out.println("list2的值全部添加到list1里"+b);
        System.out.println("此时list1数据为"+list1);
        b = list1.addAll(list3);
        System.out.println("往list1里加一个空集合list3则输出"+b);
    }
}
/*
list2的值全部添加到list1里true
此时list1数据为[亚瑟, 安琪拉, 口红, 亚瑟2, 安琪拉2, 口红2]
往list1里加一个空集合list3则输出false
*/

另外ArrayList在初始化的时候指定长度肯定是要比不指定长度的性能好很多, 这样不用重复的申请空间, 复制数组, 销毁老的分配空间了,
性能对比测试如下

public class Demo {

    public static int length = 1048576; //10的20次幂
    public static List<Integer> list1 = new ArrayList<>();
    public static List<Integer> list2 = new ArrayList<>(length);

    public static void addList(int sign) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < length; i++) {
            if (sign == 0) {
                list1.add(sign);
            } else {
                list2.add(sign);
            }
        }
        long end = System.currentTimeMillis();
        System.out.println(sign + " exec time is: " + (end - start)+"毫秒");
    }

    public static void main(String[] args) {
        addList(0);
        addList(1);
    }
}

/*
0 exec time is: 28毫秒
1 exec time is: 19毫秒
*/

LinkedList,实现了List接口,继承自AbstractSequentialList

List<String> list = new LinkedList();
List<Integer> list1 = new LinkedList();

List<String> tempList = new ArrayList<>();
List<String> stringList2 = new LinkedList<>(tempList);  //转化

LinkedList和ArrayList一样是集合List的实现类,虽然较之ArrayList,其使用场景并不多,两者可以相互转化,它拥有的方法与ArrayList差不多一致,因为都是实现了List接口,不写代码示例了。

总结ArrayList与LinkedList区别

类型内部结构插入效率(正常情况)删除效率(正常情况)顺序遍历效率随机遍历效率占用内存序列化
ArrayList数组Object[]
LinkedList双向链表Node

上述的对比都是基于大数据量的情况下,如果只是几个元素或几十个元素,它们之间并没有多大区别。
 

问:插入效率为何说正常情况下ArrayList低,LinkedList高呢?

答:我们清楚ArrayList之所以插入效率低,有两个原因会造成时间的消耗。
 第一,当底层数组空间不足时需要扩容,扩容后需进行数组拷贝
 第二,当不在数组末尾插入数据,那么就需要移动数组元素
知道了其插入效率低的原因后,那么很明显,数据扩容及拷贝只有在数组空间不足时才发生,如果我们正确使用,就像《阿里巴巴Java开发手册》中提到我们在创建集合对象时,就传递参数预先设置好数组大小,那么插入效率是非常高的;而90%的情况下我们在添加元素时都调用的是add(E e),直接在末尾添加元素,很少调用add(int index, E e)在数组中部添加元素,这样其实移动数组元素就很少发生,因此插入效率也很高。


问:删除效率为何说正常情况下ArrayList低,LinkedList高呢?

答:因为删除效率高、低不是绝对的。其实删除操作可以分为两部分。
第一:找到要删除的元素,这个通过索引找,ArrayList的执行效率要远高于LinkedList的执行效率;通过equals找则需要遍历整个集合,ArrayList和LinkedList执行效率基本一致。
第二:删除元素及后续操作,这个如果删除是最后一个元素,执行效率基本一致;如果是删除的中间元素,那么ArrayList需进行数组元素移动,而LinkedList只需搭建起该元素的上一个节点和下一个节点的关系即可,LinkedList执行效率高于ArrayList。

​ 因此,需根据实际情况才可判断实际的执行效率。


问:遍历效率这个问题怎么说?

答:ArrayList通过数组实现,天然可以通过数组下标读取数据,顺序遍历、随机遍历效率都非常高;LinkedList通过双向链表实现,顺序遍历时,可直接通过本节点.next()直接找到相关联的下一个节点,效率很高,而如果LinkedList随机遍历时,首先需判断(传递的索引值与集合长度/2)的大小,来确定接下来是应该从第一个节点开始找还是最后节点开始找,越是靠近集合中部、集合越大,随机遍历执行效率越低。

Set

Set集合类似于一个瓶子,“装进”Set集合中的多个对象之间没有明显的顺序。
Set集合不允许包含相同的元素,如果试图将两个相同的元素加入同一个Set集合中,则添加操作失败, add方法返回false, 且新元素不会被加入其集合中。

HashSet具有以下特点:不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化集合元素值可以是null。

HashSet,实现了Set接口,继承自AbstractSet类

HashSet是Set接口的典型实现,大多数时候使用Set集合时就是使用这个实现类。HashSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能。底层数据结构是哈希表。

HashSet具有以下特点:

  • 储存的是 无序,唯一的对象
  • 由于是 无序 的所以每组数据都 没有索引,很多list可用的方法他都没有,凡是 需要通过索引来进行操作的方法都没有
  • 所以也 不能使用普通for循环来进行遍历,只有加强型for和迭代器 两种遍历方法

内部存储机制

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

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

HashSet的各种方法:

增加
add(null);
删除
remove(news);
对比查找
contains(news);
清空集合
clear();
获取长度
size();

代码示例:

public class Demo {
    public static void main(String[] args) {
        HashSet<String> set = new HashSet<String>();
        set.add("Tom"); // 向set集合中添加元素
        set.add("ABC");
        set.add("Jerry");
 
        //对比查找
        boolean flag = set.contains("Tom");
        System.out.println(flag);

        //迭代器
        Iterator<String> iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
        
        //增强型for
        for(String n : set){
           System.out.println(set);
        }
        
        //remove移除某个元素
        set.remove("Tom");    //删除“Tom”
        System.out.println(set.size());    //检验是否已被删除
        
        //clear() 方法
        set.clear();
        System.out.println(set.isEmpty()); //如果Set不包含元素,则返回 true ,否则返回false
    }
}

2.Map

Map用于保存具有映射关系的数据,Map集合中保存着两组值,一组值用于保存Map中的key,另外一组值保存Map的value。

HashMap,继承AbstractMap类,实现Map接口

它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
从结构实现来讲,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。

        Map map = new HashMap();
        HashMap map2 = new HashMap();
        map.put("name", "张三");
        map2.put("name", "张三");
        System.out.print(map);
        System.out.print(map2);

1.谈一下HashMap的特性?

1.HashMap存储键值对实现快速存取,允许为null。key值不可重复,若key值重复则覆盖。
2.非同步,线程不安全。
3.底层是hash表,不保证有序(比如插入的顺序)

2.谈一下HashMap的底层原理是什么?

HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 

HashMap的数据结构: 在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

当我们往Hashmap中put元素时,首先根据key的hashcode重新计算hash值,根绝hash值得到这个元素在数组中的位置(下标),如果该数组在该位置上已经存放了其他元素,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放入链尾.如果数组中该位置没有元素,就直接将该元素放到数组的该位置上。

需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

3.谈一下hashMap中put是如何实现的?

1.计算关于key的hashcode值(与Key.hashCode的高16位做异或运算)
2.如果散列表为空时,调用resize()初始化散列表
3.如果没有发生碰撞,直接添加元素到散列表中去
4.如果发生了碰撞(hashCode值相同),进行三种判断
     4.1:若key地址相同或者equals后内容相同,则替换旧值
     4.2:如果是红黑树结构,就调用树的插入方法
     4.3:链表结构,循环遍历直到链表中某个节点为空,尾插法进行插入,插入之后判断链表个数是否到达变成红黑树的阙值8;也可以遍历到有节点与插入元素的哈希值和内容相同,进行覆盖。
5.如果桶满了大于阀值,则resize进行扩容

4.谈一下hashMap中什么时候需要进行扩容,扩容resize()又是如何实现的?

调用场景:
1.初始化数组table
2.当数组table的size达到阙值时即++size > load factor * capacity 时,也是在putVal函数中
实现过程:(细讲)
1.通过判断旧数组的容量是否大于0来判断数组是否初始化过
否:进行初始化

  •  判断是否调用无参构造器,
  • 是:使用默认的大小和阙值
  • 否:使用构造函数中初始化的容量,当然这个容量是经过tableSizefor计算后的2的次幂数

是,进行扩容,扩容成两倍(小于最大值的情况下),之后在进行将元素重新进行与运算复制到新的散列表中
概括的讲:扩容需要重新分配一个新数组,新数组是老数组的2倍长,然后遍历整个老结构,把所有的元素挨个重新hash分配到新结构中去。

PS:可见底层数据结构用到了数组,到最后会因为容量问题都需要进行扩容操作

3.工作实践

实际工作中,已经很少直接for循环来操作一个集合了,结合工作记录如下:

场景一:待定

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值