java容器详细解析

前言:在java开发中我们肯定会大量的使用集合,在这里我将总结常见的集合类,每个集合类的优点和缺点,以便我们能更好的使用集合。下面我用一幅图来表示

其中淡绿色的表示接口,红色的表示我们经常使用的类。

1:基本概念

Java容器类类库的用途是保存对象,可以将其分为2个概念。

1.1:Collection

一个独立元素的序列,这些元素都服从一条或多条规则。其中List必须按照插入的顺序保存元素、Set不能有重复的元素、Queue按照排队规则来确定对象的产生顺序(通常也是和插入顺序相同)

1.2:Map

一组成对的值键对对象,允许用键来查找值。ArrayList允许我们用数字来查找值,它是将数字和对象联系在一起。而Map允许我们使用一个对象来查找某个对象,它也被称为关联数组。或者叫做字典。

2:List

List承诺可以将元素维护在特定的序列中。List接口在Collection的基础上加入了大量的方法,使得可以在List中间可以插入和移除元素。下面主要介绍2种List

2.1:基本的ArrayList

它的优点在于随机访问元素快,但是在中间插入和移除比较慢

那么现在我们就一起来看看为什么ArrayList随机访问快,而插入移除比较慢。先说关于ArrayList的初始化。

ArrayList有三种方式进行初始化如下

private transient Object[] elementData;
public ArrayList() {
        this(10);
    }
  public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }
 public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        size = elementData.length;
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    }

我们可以看出ArrayList其实就是采用的是数组(默认是长度为10的数组)。所有ArrayList在读取的时候是具有和数组一样的效率,它的时间复杂度为1。

插入尾部就是elementData[size++] = e;当然中间会进行扩容。现在主要说插入中间为什么相对来说比较慢源码如下

 public void add(int index, E element) {
        rangeCheckForAdd(index);//验证(可以不考虑)

        ensureCapacityInternal(size + 1);  // Increments modCount!!(超过当前数组长度进行扩容)
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);(核心代码)
        elementData[index] = element;
        size++;
    }

System.arraycopy(elementData, index, elementData, index + 1)第一个参数是源数组,源数组起始位置,目标数组,目标数组起始位置,复制数组元素数目。那么这个意思就是从index索性处每个元素向后移动一位,最后把索引为index空出来,并将element赋值给它。这样一来我们并不知道要插入哪个位置,所以会进行匹配那么它的时间赋值度就为n。

2.2:LinkedList

它是通过代价较低在List中间进行插入和移除,提供了优化的顺序访问,但是在随机访问方面相对较慢。但是他的特性功能要比ArrayList强大的多。支持Queue和Stack

ListedList采用的是链式存储。链式存储就会定一个节点Node。包括三部分前驱节点、后继节点以及data值。所以存储存储的时候他的物理地址不一定是连续的。

我们看下它的中间插入实现

从代码我们可以看出先获取插入索引元素的前驱节点,然后把这个元素作为后继节点,然后在创建新的节点,而新的节点前驱节点和获取前驱节点相同,而后继节点则等于要移动的这个元素。所以这里是不需要循环的,从而在插入和删除的时候效率比较高。

我们在来看看查询(我们可以分析出它的效率要比ArrayList低了不少)

3:Set

Set也是一个集合,但是他的特点是不可以有重复的对象,所以Set最常用的就是测试归属性,很容易的询问出某个对象是否存在Set中。并且Set是具有和Collection完全一样的接口,没有额外的功能,只是表现的行为不同。

3.1:HashSet

HashSet查询速度比较快,但是存储的元素是随机的并没有排序,下面我写一段程序看一下

public static void main(String[] args){
        /**
         * 没有顺序可循,这是因为hashset采用的是散列(处于速度考虑)
         */
        Random random=new Random(47);
        Set<Integer> intset=new HashSet<Integer>();
        for (int i=0;i<10000;i++){
            intset.add(random.nextInt(30));
        }
        System.out.print(intset);
    }

3.2:TreeSet

TreeSet是将元素存储红-黑树结构中,所以存储的结果是有顺序的(所以如果你想要自己存储的集合有顺序那么选择TreeSet)

public static void main(String[] args){
        Random random=new Random(47);
        Set<Integer> intset=new TreeSet<Integer>();
        for (int i=0;i<10000;i++){
            intset.add(random.nextInt(30));
        }
        System.out.print(intset);
    }

关于LinkedHashSet后面再说。

4:Queue

Queue是队列,队列是典型的先进先出的容器,就是从容器的一端放入元素,从另一端取出,并且元素放入容器的顺序和取出的顺序是相同的。LinkedList提供了对Queue的实现,LinkedList向上转型为Queue。其中Queue有offer、peek、element、pool、remove等方法

offer是将元素插入队尾,返回false表示添加失败。peek和element都将在不移除的情况下返回对头,但是peek在对头为null的时候返回null,而element会抛出NoSuchElementException异常。poll和remove方法将移除并返回对头,但是poll在队列为null,而remove会抛出NoSuchElementException异常,以下是例子

  public static void main(String[] args){
        Queue<Integer> queue=new LinkedList<Integer>();
        Random rand=new Random();
        for (int i=0;i<10;i++){
            queue.offer(rand.nextInt(i+10));
        }
        printQ(queue);
        Queue<Character> qc=new LinkedList<Character>();
        for (char c:"HelloWorld".toCharArray()){
            qc.offer(c);
        }
        System.out.println(qc.peek());
        printQ(qc);
        List<String> mystrings=new LinkedList<String>();
        mystrings.add("1");
        mystrings.get(0);
        Set<String> a=new HashSet<String>();
        Set<String> set=new HashSet<String>();
        set.add("1");
    }
    public static void printQ(Queue queue){
        while (queue.peek

从上面的输出的结果我们可以看出结果并不是一个顺序的,没有规则的,这个时候如果想让队列按照规则输出那么这个时候我们就要考虑优先级了,这个时候我们就应该使用PriorityQueue,这个时候如果在调用offer方法插入一个对象的时候,这个对象就会按照优先级在对列中进行排序,默认的情况是自然排序,当然我们可以通过Comparator来修改这个顺序(在下一篇讲解)。PriorityQueue可以确保当你调用peek、pool、remove方法时,获取的元素将是对列中优先级最高的元素。ok我们再次通过代码查看

 public static void main(String[] args) {
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<Integer>();
        Random rand = new Random();
        for (int i = 0; i < 10; i++) {
            priorityQueue.offer(rand.nextInt(i + 10));
        }
        QueueDemo.printQ(priorityQueue);
        List<Integer>ints= Arrays.asList(25,22,20,18,14,9,3,1,1,2,3,9,14,18,21,23,25);
        priorityQueue=new PriorityQueue<Integer>(ints);
        QueueDemo.printQ(priorityQueue);
    }

从输出可以看到,重复是允许的,最小值拥有最高优先级(如果是String,空格也可以算作值,并且比字母具有更高的优先级)如果你想消除重复,可以采用Set进行存储,然后把Set作为priorityQueue对象的初始值即可。

5:Map

Map在实际开发中使用非常广,特别是HashMap,想象一下我们要保存一个对象中某些元素的值,如果我们在创建一个对象显得有点麻烦,这个时候我们就可以用上map了,HashMap采用是散列函数所以查询的效率是比较高的,如果我们需要一个有序的我们就可以考虑使用TreeMap。这里主要介绍一下HashMap的方法,大家注意HashMap的键可以是null,而且键值不可以重复,如果重复了以后就会对第一个进行键值进行覆盖。

put进行添加值键对,containsKey验证主要是否存在、containsValue验证值是否存在、keySet获取所有的键集合、values获取所有值集合、entrySet获取键值对。

 public static void main(String[] args){
        //Map<String,String> pets=new HashMap<String, String>();
        Map<String,String> pets=new TreeMap<String, String>();
        pets.put("1","张三");
        pets.put("2","李四");
        pets.put("3","王五");
        if (pets.containsKey("1")){
            System.out.println("已存在键1");
        }
        if (pets.containsValue("张三")){
            System.out.println("已存在值张三");
        }
        Set<String> sets=pets.keySet();
        Set<Map.Entry<String , String>> entrySet= pets.entrySet();
        Collection<String> values=  pets.values();
        for (String value:values){
            System.out.println(value+";");
        }
       for (String key:sets){
           System.out.print(key+";");
       }
        for (Map.Entry entry:entrySet){
            System.out.println("键:"+entry.getKey());
            System.out.println("值:"+entry.getValue());
        }
    }

6:Iterator和Foreach

现在foreach语法主要作用于数组,但是他也可以应用于所有的Collection对象。Collection之所以能够使用foreach是由于继承了Iterator这个接口。下面我写段代码供大家查看

public class IteratorClass {
    public Iterator<String> iterator(){
      return new Itr();
    }
    private class Itr implements Iterator<String>{
        protected String[] words=("Hello Java").split(" ");
        private int index=0;
        public boolean hasNext() {
            return index<words.length;
        }
        public String next() {
            return words[index++];
        }
        public void remove() {
        }
    }
}

 Iterator iterators=new IteratorClass().iterator();
        for (Iterator it=iterator;iterators.hasNext();) {
            System.out.println(iterators.next());
        }
        while (iterators.hasNext()){
            System.out.println(iterators.next());
        }

从中我们可以看出foreach循环最终是转换成 for (Iterator it=iterator;iterators.hasNext();)只不过jdk帮我们隐藏我们无法查看。下面我们在来分析一个问题,关于List删除问题。我们大多肯定使用过for循环或者foreach循环去删除,但是结果很明显会出现错误,那么现在我们一起分析为啥会出现错误。

1:使用for循环删除(出现错误分析)

2:foreach循环删除(错误分析)

从上面我们得知foreach最终是会转成Iterator的所以它首先会通过next来获取元素,我们看代码

请看for循环删除那段代码,没删除一次modCount会++,所以第二次在次删除的时候modCount由于增加和expectedModCount不等所以无法获取元素也就无法删除。

3:正确的删除方式

采用迭代器代码如下

 Iterator<String> iterator=userList.iterator();
        while (iterator.hasNext()){
            iterator.next();
            iterator.remove();
        }

请记住一定要加上iterator.next();这是因为在源码中有一个lastRed,通过它来记录是否是最后一个元素,如果不加上iterator.next()那么lastRed=-1,在删除验证的时候有这么一段代码if (lastRet < 0)throw new IllegalStateException();所以就会抛出异常。

7:Collections和Arrays

这里只介绍2个常用的Collections.addAll和Arrays.asList

addAll:

asList采用的是数组

可以看出最终转换成ArrayList。

8:总结

1):数组是将数字和对象联系起来,它保存明确的对象,查询对象时候不需要对查询结果进行转换,它可以是多维的,可以保存基本类型的数据,但是数组一旦生成,其容量不能改变。所以数组是不可以直接删除和添加元素。

2):Collection保存单一的元素,而Map保存相关联的值键对,有了Java泛型,可以指定容器存放对象类型,不会将错误类型的对象放在容器中,取元素时候也不需要转型。而且Collection和Map都可以自动调整其尺寸。容器不可以持有基本类型。

3):像数组一样,List也建立数字索性和对象的关联,因此,数组和List都是排好序的容器,List可以自动扩容

4):如果需要大量的随机访问就要使用ArrayList,如果要经常从中间插入和删除就要使用LinkedList。

5):各种Queue和Stack由LinkedList支持

6):Map是一种将对象(而非数字)与对象相关联的设计。HashMap用于快速访问,TreeMap保持键始终处于排序状态,所以不如HashMap快,而LinkedHashMap保持元素插入的顺序,但是也通过散列提供了快速访问的能力

7):Set不接受重复的元素,HashSet提供最快的访问能力,TreeSet保持元素排序状态,LinkedHashSet以插入顺序保存元素。

时间: 2024-10-25 19:27:26

java容器详细解析的相关文章

Java当中的堆与栈详细解析

总结第一句话:Java语言使用内存的时候,栈内存主要保存以下内容:基本数据类型和对象的引用,而堆内存存储对象,栈内存的速度要快于堆内存.总结成一句话就是:引用在栈而对象在堆. Java疯狂讲义的一段对话作为开场白. 一个问题:为什么有栈内存和堆内存之分? 答:当一个方法执行时,每个方法都会简历自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁.因此,所有在方法中创建一个对象时,这个对象将被保存到运行时数据区中,以便利用(因为对象的创建成

java类生命周期详细解析

(一)详解java类的生命周期 引言 最近有位细心的朋友在阅读笔者的文章时,对java类的生命周期问题有一些疑惑,笔者打开百度搜了一下相关的问题,看到网上的资料很少有把这个问题讲明白的,主要是因为目前国内java方面的教材大多只是告诉你“怎样做”,但至于“为什么这样做”却不多说,所以造成大家在基础和原理方面的知识比较匮乏,所以笔者今天就斗胆来讲一下这个问题,权当抛砖引玉,希望对在这个问题上有疑惑的朋友有所帮助,文中有说的不对的地方,也希望各路高手前来指正. 首先来了解一下jvm(java虚拟机)

Java注解全面解析(转)

1.基本语法 注解定义看起来很像接口的定义.事实上,与其他任何接口一样,注解也将会编译成class文件. @Target(ElementType.Method) @Retention(RetentionPolicy.RUNTIME) public @interface Test {} 除了@符号以外,@Test的定义很像一个空的接口.定义注解时,需要一些元注解(meta-annotation),如@Target和@Retention @Target用来定义注解将应用于什么地方(如一个方法或者一个

【Java心得总结五】Java容器中——Collection

在[Java心得总结五]Java容器上——容器初探这篇博文中,我对Java容器类库从一个整体的偏向于宏观的角度初步认识了Java容器类库.而在这篇博文中,我想着重对容器类库中的Collection容器做一个着重的探索与总结. Collection:一个独立元素的序列,这些元素都服从一条或多条规则.(注:Collection其实就是将一组数据对象按照一维线性的方式组织起来)List必须按照插入的顺序保存元素,而set不能有重复元素.Queue按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序

【Java心得总结七】Java容器下——Map

我将容器类库自己平时编程及看书的感受总结成了三篇博文,前两篇分别是:[Java心得总结五]Java容器上——容器初探和[Java心得总结六]Java容器中——Collection,第一篇从宏观整体的角度对Java中强大的容器类库做了一个简单总结而第二篇专门针对容器类库中的Collection部分进行了总结.这篇博文将对容器类库中的Map部分进行一个整理总结. 一.初识Map Map:一组成对的“键值对”对象,允许你使用键来查找值.(注:Map其实是将键与值形成的二元组按照一维线性的方式组织起来,

深入Java源码解析容器类List、Set、Map

1 常用容器继承关系图先上一张网上的继承关系图个人觉得有些地方不是很准确,比如Iterator不是容器,只是一个操作遍历集合的方法接口,所以不应该放在里面.并且Map不应该继承自Collection.所以自己整理了一个常用继承关系图如下如上图所示,接下去会自顶向下解释重要的接口和实现类.2 Collection和Map在Java容器中一共定义了2种集合, 顶层接口分别是Collection和Map.但是这2个接口都不能直接被实现使用,分别代表两种不同类型的容器.简单来看,Collection代表

2019年 Java 面试题解析

作者:Zz_maker https://www.cnblogs.com/Zz-maker/p/11193930.html 包含的模块: 本文分为十九个模块,分别是: Java 基础.容器.多线程.反射.对象拷贝.Java Web .异常.网络.设计模式.Spring/Spring MVC.Spring Boot/Spring Cloud.Hibernate.MyBatis.RabbitMQ.Kafka.Zookeeper.MySQL.Redis.JVM如下图所示: 共包含 208 道面试题,本文

【转】UML中的几种关系详细解析

UML图中类之间的关系:依赖,泛化,关联,聚合,组合,实现 类与类图 1) 类(Class)封装了数据和行为,是面向对象的重要组成部分,它是具有相同属性.操作.关系的对象集合的总称. 2) 在系统中,每个类具有一定的职责,职责指的是类所担任的任务,即类要完成什么样的功能,要承担什么样的义务.一个类可以有多种职责,设计得好的类一般只有一种职责,在定义类的时候,将类的职责分解成为类的属性和操作(即方法). 3) 类的属性即类的数据职责,类的操作即类的行为职责 一.依赖关系(Dependence) 依

Java 容器在实际项目中的应用

前言:在java开发中我们离不开集合数组等,在java中有个专有名词:"容器" ,下面会结合Thinking in Java的知识和实际开发中业务场景讲述一下容器在Web项目中的用法.可结合图片代码了解Java中的容器 备注 :这个地方 ,参考于朝向远方的博客Java容器详解,既然前人总结的这么好,我就直接拿来用,在这里更注重在实际开发中的例子,感谢那些总结的前辈们,辛苦了. 简单的数组例子 Thinking in Java 中并没有把数组归为Java的容器,实际上数组的确不是Java