精华内容
下载资源
问答
  • html容器标签_HTML容器标签

    千次阅读 2020-08-27 19:57:24
    html容器标签 容器标签 (Container tags) ... HTML提供了一组容器标记。 这些标签可以包含一组未指定的其他标签。 We have: 我们有: article article s...

    html容器标签

    容器标签 (Container tags)

    HTML provides a set of container tags. Those tags can contain an unspecified set of other tags.

    HTML提供了一组容器标记。 这些标签可以包含一组未指定的其他标签。

    We have:

    我们有:

    • article

      article

    • section

      section

    • div

      div

    and it can be confusing to understand the difference between them.

    理解它们之间的差异可能会造成混淆。

    Let’s see when to use each one of them.

    让我们看看何时使用它们中的每一个。

    article (article)

    The article tag identifies a thing that can be independent from other things in a page.

    商品标签标识,可以是独立于页面其他东西事情

    For example a list of blog posts in the homepage.

    例如,主页中的博客文章列表。

    Or a list of links.

    或链接列表。

    <div>
    	<article>
    		<h2>A blog post</h2>
    		<a ...>Read more</a>
    	</article>
    	<article>
    		<h2>Another blog post</h2>
    		<a ...>Read more</a>
    	</article>
    </div>

    We’re not limited to lists: an article can be the main element in a page.

    我们不仅限于列表:文章可以是页面中的主要元素。

    <article>
    	<h2>A blog post</h2>
    	<p>Here is the content...</p>
    </article>

    Inside an article tag we should have a title (h1-h6) and

    内部article标签,我们应该有一个标题( h1 - h6 )和

    section (section)

    Represents a section of a document. Each section has a heading tag (h1-h6), then the section body.

    代表文档的一部分。 每个节都有一个标题标签( h1 - h6 ),然后是节正文

    Example:

    例:

    <section>
    	<h2>A section of the page</h2>
    	<p>...</p>
    	<img ... />
    </section>

    It’s useful to break a long article into different sections.

    将较长的文章分成不同的部分很有用。

    Shouldn’t be used as a generic container element. div is made for this.

    不应用作通用容器元素。 div是为此而设计的。

    div (div)

    div is the generic container element:

    div是通用容器元素:

    <div>
    	...
    </div>

    You often add a class or id attribute to this element, to allow it to be styled using CSS.

    您经常向该元素添加classid属性,以允许使用CSS设置其样式。

    We use div in any place where we need a container but the existing tags are not suited.

    我们在需要容器的任何地方使用div ,但现有标签不合适。

    This tag is used to create the markup that defines the page navigation. Into this we typically add an ul or ol list:

    此标记用于创建定义页面导航的标记。 通常,向其中添加一个ulol列表:

    <nav>
    	<ol>
    		<li><a href="/">Home</a></li>
    		<li><a href="/blog">Blog</a></li>
    	</ol>
    </nav>

    aside (aside)

    The aside tag is used to add a piece of content that is related to the main content.

    aside标签用于添加与主要内容相关的内容。

    A box where to add a quote, for example. Or a sidebar.

    例如,在其中添加引号的框。 或侧边栏。

    Example:

    例:

    <div>
      <p>some text..</p>
      <aside>
        <p>A quote..</p>
      </aside>
      <p>other text...</p>
    </div>

    Using aside is a signal that the things it contains are not part of the regular flow of the section it lives into.

    使用aside表示它包含的内容不是它所在的部分的常规流程的一部分。

    The header tag represents a part of the page that is the introduction. It can for example contain one or more heading tag (h1-h6), the tagline for the article, an image.

    header标记表示页面的一部分,即简介。 例如,它可以包含一个或多个标题标签( h1 - h6 ),商品的标语,图像。

    <article>
      <header>
    	  <h1>Article title</h1>
      </header>
      ...
    </article>

    main (main)

    The main tag represents the main part of a page:

    main标签代表页面的主要部分:

    <body>
      ....
      <main>
        <p>....</p>
      </main>
    </body>

    The footer tag is used to determine the footer of an article, or the footer of the page:

    footer标记用于确定文章的页脚或页面的页脚:

    <article>
     ....
    	<footer>
        <p>Footer notes..</p>
      </footer>
    </article>

    翻译自: https://flaviocopes.com/html-container-tags/

    html容器标签

    展开全文
  • Java集合容器面试题(2020最新版)

    万次阅读 多人点赞 2020-03-01 11:08:34
    文章目录集合容器概述什么是集合集合的特点集合和数组的区别使用集合框架的好处常用的集合类有哪些?List,Set,Map三者的区别?List、Set、Map 是否继承自 Collection 接口?List、Map、Set 三个接口存取元素时,各...

    大家好,我是CSDN的博主ThinkWon,“2020博客之星年度总评选"开始啦,希望大家帮我投票,每天都可以投多票哦,点击下方链接,然后点击"最大”,再点击"投TA一票"就可以啦!
    投票链接:https://bss.csdn.net/m/topic/blog_star2020/detail?username=thinkwon
    在技术的世界里,ThinkWon将一路与你相伴!创作出更多更高质量的文章!2020为努力奋斗的你点赞👍,️新的一年,祝各位大牛牛气冲天,牛年大吉!😊😊

    文章目录

    Java面试总结汇总,整理了包括Java基础知识,集合容器,并发编程,JVM,常用开源框架Spring,MyBatis,数据库,中间件等,包含了作为一个Java工程师在面试中需要用到或者可能用到的绝大部分知识。欢迎大家阅读,本人见识有限,写的博客难免有错误或者疏忽的地方,还望各位大佬指点,在此表示感激不尽。文章持续更新中…

    序号 内容 链接地址
    1 Java基础知识面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/104390612
    2 Java集合容器面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/104588551
    3 Java异常面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/104390689
    4 并发编程面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/104863992
    5 JVM面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/104390752
    6 Spring面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/104397516
    7 Spring MVC面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/104397427
    8 Spring Boot面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/104397299
    9 Spring Cloud面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/104397367
    10 MyBatis面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/101292950
    11 Redis面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/103522351
    12 MySQL数据库面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/104778621
    13 消息中间件MQ与RabbitMQ面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/104588612
    14 Dubbo面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/104390006
    15 Linux面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/104588679
    16 Tomcat面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/104397665
    17 ZooKeeper面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/104397719
    18 Netty面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/104391081
    19 架构设计&分布式&数据结构与算法面试题(2020最新版) https://thinkwon.blog.csdn.net/article/details/105870730

    集合容器概述

    什么是集合

    集合框架:用于存储数据的容器。

    集合框架是为表示和操作集合而规定的一种统一的标准的体系结构。
    任何集合框架都包含三大块内容:对外的接口、接口的实现和对集合运算的算法。

    接口:表示集合的抽象数据类型。接口允许我们操作集合时不必关注具体实现,从而达到“多态”。在面向对象编程语言中,接口通常用来形成规范。

    实现:集合接口的具体实现,是重用性很高的数据结构。

    算法:在一个实现了某个集合框架中的接口的对象身上完成某种有用的计算的方法,例如查找、排序等。这些算法通常是多态的,因为相同的方法可以在同一个接口被多个类实现时有不同的表现。事实上,算法是可复用的函数。
    它减少了程序设计的辛劳。

    集合框架通过提供有用的数据结构和算法使你能集中注意力于你的程序的重要部分上,而不是为了让程序能正常运转而将注意力于低层设计上。
    通过这些在无关API之间的简易的互用性,使你免除了为改编对象或转换代码以便联合这些API而去写大量的代码。 它提高了程序速度和质量。

    集合的特点

    集合的特点主要有如下两点:

    • 对象封装数据,对象多了也需要存储。集合用于存储对象。

    • 对象的个数确定可以使用数组,对象的个数不确定的可以用集合。因为集合是可变长度的。

    集合和数组的区别

    • 数组是固定长度的;集合可变长度的。

    • 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。

    • 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。

    数据结构:就是容器中存储数据的方式。

    对于集合容器,有很多种。因为每一个容器的自身特点不同,其实原理在于每个容器的内部数据结构不同。

    集合容器在不断向上抽取过程中,出现了集合体系。在使用一个体系的原则:参阅顶层内容。建立底层对象。

    使用集合框架的好处

    1. 容量自增长;
    2. 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量;
    3. 允许不同 API 之间的互操作,API之间可以来回传递集合;
    4. 可以方便地扩展或改写集合,提高代码复用性和可操作性。
    5. 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。

    常用的集合类有哪些?

    Map接口和Collection接口是所有集合框架的父接口:

    1. Collection接口的子接口包括:Set接口和List接口
    2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
    3. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
    4. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

    List,Set,Map三者的区别?List、Set、Map 是否继承自 Collection 接口?List、Map、Set 三个接口存取元素时,各有什么特点?

    img

    Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。

    Collection集合主要有List和Set两大接口

    • List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
    • Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

    Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。

    Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap

    集合框架底层数据结构

    Collection

    1. List
    • Arraylist: Object数组
    • Vector: Object数组
    • LinkedList: 双向循环链表
    1. Set
    • HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
    • LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
    • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)

    Map

    • HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
    • LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
    • HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
    • TreeMap: 红黑树(自平衡的排序二叉树)

    哪些集合类是线程安全的?

    • vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。
    • statck:堆栈类,先进后出。
    • hashtable:就比hashmap多了个线程安全。
    • enumeration:枚举,相当于迭代器。

    Java集合的快速失败机制 “fail-fast”?

    是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。

    例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

    原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

    解决办法:

    1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。

    2. 使用CopyOnWriteArrayList来替换ArrayList

    怎么确保一个集合不能被修改?

    可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。

    示例代码如下:

    List<String> list = new ArrayList<>();
    list. add("x");
    Collection<String> clist = Collections. unmodifiableCollection(list);
    clist. add("y"); // 运行时此行报错
    System. out. println(list. size());
    

    Collection接口

    List接口

    迭代器 Iterator 是什么?

    Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。

    Iterator 怎么使用?有什么特点?

    Iterator 使用代码如下:

    List<String> list = new ArrayList<>();
    Iterator<String> it = list. iterator();
    while(it. hasNext()){
      String obj = it. next();
      System. out. println(obj);
    }
    

    Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。

    如何边遍历边移除 Collection 中的元素?

    边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下:

    Iterator<Integer> it = list.iterator();
    while(it.hasNext()){
       *// do something*
       it.remove();
    }
    

    一种最常见的错误代码如下:

    for(Integer i : list){
       list.remove(i)
    }
    

    运行以上错误代码会报 ConcurrentModificationException 异常。这是因为当使用 foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。

    Iterator 和 ListIterator 有什么区别?

    • Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
    • Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
    • ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。

    遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?

    遍历方式有以下几种:

    1. for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。

    2. 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。

    3. foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。

    最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。

    • 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。
    • 如果没有实现该接口,表示不支持 Random Access,如LinkedList。

    推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。

    说一下 ArrayList 的优缺点

    ArrayList的优点如下:

    • ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。
    • ArrayList 在顺序添加一个元素的时候非常方便。

    ArrayList 的缺点如下:

    • 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
    • 插入元素的时候,也需要做一次元素复制操作,缺点同上。

    ArrayList 比较适合顺序添加、随机访问的场景。

    如何实现数组和 List 之间的转换?

    • 数组转 List:使用 Arrays. asList(array) 进行转换。
    • List 转数组:使用 List 自带的 toArray() 方法。

    代码示例:

    // list to array
    List<String> list = new ArrayList<String>();
    list.add("123");
    list.add("456");
    list.toArray();
    
    // array to list
    String[] array = new String[]{"123","456"};
    Arrays.asList(array);
    

    ArrayList 和 LinkedList 的区别是什么?

    • 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
    • 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
    • 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
    • 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
    • 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;

    综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。

    补充:数据结构基础之双向链表

    双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。

    ArrayList 和 Vector 的区别是什么?

    这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合

    • 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
    • 性能:ArrayList 在性能方面要优于 Vector。
    • 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。

    Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。

    Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。

    插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述 ArrayList、Vector、LinkedList 的存储性能和特性?

    ArrayList、LinkedList、Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。

    Vector 中的方法由于加了 synchronized 修饰,因此 Vector 是线程安全容器,但性能上较ArrayList差

    LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以 LinkedList 插入速度较快

    多线程场景下如何使用 ArrayList?

    ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:

    List<String> synchronizedList = Collections.synchronizedList(list);
    synchronizedList.add("aaa");
    synchronizedList.add("bbb");
    
    for (int i = 0; i < synchronizedList.size(); i++) {
        System.out.println(synchronizedList.get(i));
    }
    

    为什么 ArrayList 的 elementData 加上 transient 修饰?

    ArrayList 中的数组定义如下:

    private transient Object[] elementData;
    

    再看一下 ArrayList 的定义:

    public class ArrayList<E> extends AbstractList<E>
         implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    

    可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现:

    private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
        *// Write out element count, and any hidden stuff*
            int expectedModCount = modCount;
        s.defaultWriteObject();
        *// Write out array length*
            s.writeInt(elementData.length);
        *// Write out all elements in the proper order.*
            for (int i=0; i<size; i++)
                s.writeObject(elementData[i]);
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
    }
    

    每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。

    List 和 Set 的区别

    List , Set 都是继承自Collection 接口

    List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。

    Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

    另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。

    Set和List对比

    Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
    List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变

    Set接口

    说一下 HashSet 的实现原理?

    HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。

    HashSet如何检查重复?HashSet是如何保证数据不可重复的?

    向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。
    HashSet 中的add ()方法会使用HashMap 的put()方法。

    HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。

    以下是HashSet 部分源码:

    private static final Object PRESENT = new Object();
    private transient HashMap<E,Object> map;
    
    public HashSet() {
        map = new HashMap<>();
    }
    
    public boolean add(E e) {
        // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
    	return map.put(e, PRESENT)==null;
    }
    

    hashCode()与equals()的相关规定

    1. 如果两个对象相等,则hashcode一定也是相同的
    2. 两个对象相等,对两个equals方法返回true
    3. 两个对象有相同的hashcode值,它们也不一定是相等的
    4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
    5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

    ==与equals的区别

    1. ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同
    2. ==是指对内存地址进行比较 equals()是对字符串的内容进行比较3.==指引用是否相同 equals()指的是值是否相同

    HashSet与HashMap的区别

    HashMap HashSet
    实现了Map接口 实现Set接口
    存储键值对 仅存储对象
    调用put()向map中添加元素 调用add()方法向Set中添加元素
    HashMap使用键(Key)计算Hashcode HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false
    HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 HashSet较HashMap来说比较慢

    Queue

    BlockingQueue是什么?

    Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。

    在 Queue 中 poll()和 remove()有什么区别?

    • 相同点:都是返回第一个元素,并在队列中删除返回的对象。
    • 不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。

    代码示例:

    Queue<String> queue = new LinkedList<String>();
    queue. offer("string"); // add
    System. out. println(queue. poll());
    System. out. println(queue. remove());
    System. out. println(queue. size());
    

    Map接口

    说一下 HashMap 的实现原理?

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

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

    HashMap 基于 Hash 算法实现的

    1. 当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
    2. 存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
    3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
    4. 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。

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

    HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现

    在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。

    JDK1.8之前

    JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

    jdk1.7中HashMap数据结构

    JDK1.8之后

    相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。

    jdk1.8中HashMap数据结构

    JDK1.7 VS JDK1.8 比较

    JDK1.8主要解决或优化了一下问题:

    1. resize 扩容优化
    2. 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
    3. 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。
    不同 JDK 1.7 JDK 1.8
    存储结构 数组 + 链表 数组 + 链表 + 红黑树
    初始化方式 单独函数:inflateTable() 直接集成到了扩容函数resize()
    hash值计算方式 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算
    存放数据的规则 无冲突时,存放数组;冲突时,存放链表 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树
    插入数据方式 头插法(先讲原位置的数据移到后1位,再插入数据到该位置) 尾插法(直接插入到链表尾部/红黑树)
    扩容后存储位置的计算方式 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)

    HashMap的put方法的具体流程?

    当我们put的时候,首先计算 keyhash值,这里调用了 hash方法,hash方法实际是让key.hashCode()key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标index = (table.length - 1) & hash,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。

    putVal方法执行流程图

    putVal方法执行流程图

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
    //实现Map.put和相关方法
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 步骤①:tab为空则创建 
        // table未初始化或者长度为0,进行扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 步骤②:计算index,并对null做处理  
        // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        // 桶中已经存在元素
        else {
            Node<K,V> e; K k;
            // 步骤③:节点key存在,直接覆盖value 
            // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                    // 将第一个元素赋值给e,用e来记录
                    e = p;
            // 步骤④:判断该链为红黑树 
            // hash值不相等,即key不相等;为红黑树结点
            // 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null
            else if (p instanceof TreeNode)
                // 放入树中
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 步骤⑤:该链为链表 
            // 为链表结点
            else {
                // 在链表最末插入结点
                for (int binCount = 0; ; ++binCount) {
                    // 到达链表的尾部
                    
                    //判断该链表尾部指针是不是空的
                    if ((e = p.next) == null) {
                        // 在尾部插入新结点
                        p.next = newNode(hash, key, value, null);
                        //判断链表的长度是否达到转化红黑树的临界值,临界值为8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //链表结构转树形结构
                            treeifyBin(tab, hash);
                        // 跳出循环
                        break;
                    }
                    // 判断链表中结点的key值与插入的元素的key值是否相等
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        // 相等,跳出循环
                        break;
                    // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                    p = e;
                }
            }
            //判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个值
            if (e != null) { 
                // 记录e的value
                V oldValue = e.value;
                // onlyIfAbsent为false或者旧值为null
                if (!onlyIfAbsent || oldValue == null)
                    //用新值替换旧值
                    e.value = value;
                // 访问后回调
                afterNodeAccess(e);
                // 返回旧值
                return oldValue;
            }
        }
        // 结构性修改
        ++modCount;
        // 步骤⑥:超过最大容量就扩容 
        // 实际大小大于阈值则扩容
        if (++size > threshold)
            resize();
        // 插入后回调
        afterNodeInsertion(evict);
        return null;
    }
    

    ①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

    ②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

    ③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

    ④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

    ⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

    ⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

    HashMap的扩容操作是怎么实现的?

    ①.在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;

    ②.每次扩展的时候,都是扩展2倍;

    ③.扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。

    在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;//oldTab指向hash桶数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空
            if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀值
                threshold = Integer.MAX_VALUE;
                return oldTab;//返回
            }//如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold 双倍扩容阀值threshold
        }
        // 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初始化成最小2的n次幂
        // 直接将该值赋给新的容量
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        // 无参构造创建的map,给出默认容量和threshold 16, 16*0.75
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 新的threshold = 新的cap * 0.75
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        // 计算出新的数组长度后赋给当前成员变量table
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//新建hash桶数组
        table = newTab;//将新数组的值复制给旧的hash桶数组
        // 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素重排逻辑,使其均匀的分散
        if (oldTab != null) {
            // 遍历新数组的所有桶下标
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    // 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收
                    oldTab[j] = null;
                    // 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树
                    if (e.next == null)
                        // 用同样的hash映射算法把该元素加入新的数组
                        newTab[e.hash & (newCap - 1)] = e;
                    // 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    // e是链表的头并且e.next!=null,那么处理链表中元素重排
                    else { // preserve order
                        // loHead,loTail 代表扩容后不用变换下标,见注1
                        Node<K,V> loHead = null, loTail = null;
                        // hiHead,hiTail 代表扩容后变换下标,见注1
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        // 遍历链表
                        do {             
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    // 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead
                                    // 代表下标保持不变的链表的头元素
                                    loHead = e;
                                else                                
                                    // loTail.next指向当前e
                                    loTail.next = e;
                                // loTail指向当前的元素e
                                // 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素时,
                                // 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next.....
                                // 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素。
                                loTail = e;                           
                            }
                            else {
                                if (hiTail == null)
                                    // 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
    

    HashMap是怎么解决哈希冲突的?

    答:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知道什么是哈希才行;

    什么是哈希?

    Hash,一般翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数

    所有散列函数都有如下一个基本特性**:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同**。

    什么是哈希冲突?

    当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)

    HashMap的数据结构

    在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突:

    img

    这样我们就可以将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下,但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化

    hash()函数

    上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的hash()函数如下:

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)
    }
    

    这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动)

    JDK1.8新增红黑树

    img

    通过上面的链地址法(使用散列表)扰动函数我们成功让我们的数据分布更平均,哈希碰撞减少,但是当我们的HashMap中存在大量数据时,加入我们某个bucket下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn);

    总结

    简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:

    1. 使用链地址法(使用散列表)来链接拥有相同hash值的数据;
    2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
    3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;

    能否使用任何类作为 Map 的 key?

    可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点:

    • 如果类重写了 equals() 方法,也应该重写 hashCode() 方法。

    • 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。

    • 如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。

    • 用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。

    为什么HashMap中String、Integer这样的包装类适合作为K?

    答:String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率

    1. 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
    2. 内部已重写了equals()hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;

    如果使用Object作为HashMap的Key,应该怎么办呢?

    答:重写hashCode()equals()方法

    1. 重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;
    2. 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性

    HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?

    答:hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;

    那怎么解决呢?

    1. HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
    2. 在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;

    HashMap 的长度为什么是2的幂次方

    为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。

    这个算法应该如何设计呢?

    我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。

    那为什么是两次扰动呢?

    答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的;

    HashMap 与 HashTable 有什么区别?

    1. 线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
    2. 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
    3. 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。
    4. **初始容量大小和每次扩充容量大小的不同 **: ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
    5. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
    6. 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。

    如何决定使用 HashMap 还是 TreeMap?

    对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。

    HashMap 和 ConcurrentHashMap 的区别

    1. ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)
    2. HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。

    ConcurrentHashMap 和 Hashtable 的区别?

    ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。

    • 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
    • 实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

    两者的对比图

    HashTable:

    img

    JDK1.7的ConcurrentHashMap:

    img

    JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):

    img

    答:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题。但是 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。

    ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?

    JDK1.7

    首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

    在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下:

    一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。

    img

    1. 该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;
    2. Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。

    JDK1.8

    JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

    结构如下:

    img

    附加源码,有需要的可以看看

    插入元素过程(建议去看看源码):

    如果相应位置的Node还没有初始化,则调用CAS插入相应的数据;

    else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
            break;                   // no lock when adding to empty bin
    }
    

    如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点;

    if (fh >= 0) {
        binCount = 1;
        for (Node<K,V> e = f;; ++binCount) {
            K ek;
            if (e.hash == hash &&
                ((ek = e.key) == key ||
                 (ek != null && key.equals(ek)))) {
                oldVal = e.val;
                if (!onlyIfAbsent)
                    e.val = value;
                break;
            }
            Node<K,V> pred = e;
            if ((e = e.next) == null) {
                pred.next = new Node<K,V>(hash, key, value, null);
                break;
            }
        }
    }
    
    1. 如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;
    2. 如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount;

    辅助工具类

    Array 和 ArrayList 有何区别?

    • Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
    • Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
    • Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。

    对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。

    如何实现 Array 和 List 之间的转换?

    • Array 转 List: Arrays. asList(array) ;
    • List 转 Array:List 的 toArray() 方法。

    comparable 和 comparator的区别?

    • comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序
    • comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序

    一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort().

    Collection 和 Collections 有什么区别?

    • java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
    • Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。

    TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素?

    TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。

    Collections 工具类的 sort 方法有两种重载的形式,

    第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;

    第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。

    展开全文
  • C++ 容器(一):顺序容器简介

    千次阅读 多人点赞 2015-08-18 21:01:40
    C++提供了使用抽象进行高效编程的方式,标准库中定义了许多容器类以及一系列泛型函数,使程序员可以更加简洁、抽象和有效地编写程序。本文分三部分将解包括:顺序容器,关联容器和泛型算法。

    C++提供了使用抽象进行高效编程的方式,标准库中定义了许多容器类以及一系列泛型函数,使程序员可以更加简洁、抽象和有效地编写程序,其中包括:顺序容器,关联容器和泛型算法。本文将简介顺序容器(vectorlistdeque)的相关内容。

    1.顺序容器的概念

    标准库vector类型,就是一种最常见的顺序容器,它将单一类型元素聚集起来成为容器,然后根据位置来存储和访问这些元素,这就是顺序容器。顺序容器的元素排列顺序与其值无关,而仅仅由元素添加到容器里的次序决定。

    标准库定义了三种顺序容器:vectorlistdeque。它们的区别在于访问元素的方式,以及添加或删除元素相关操作的运行代价。如下表:

    顺序容器 功能
    vector 支持快速随机访问
    list 支持快速插入/删除
    deque 双端队列

    (1) 头文件
    为了定义一个容器类型的对象,必须先包含相关的头文件:

    #include <vector>  // vector
    #include <list>    // list
    #include <deque>   // deque

    (2) 定义
    所有容器都是类模版,要定义某种特殊的容器,必须在容器名后加一对尖括号,里面提供存放元素的类型:

    vector<string> sVec; // empty vector that can hold strings
    list<int> iList;     // empty list that can hold ints
    deque<float> fDeque; // empty deque that can holds floats

    (3)初始化
    容器的构造函数:

    构造函数 含义
    C<T> c 创建一个名为c的空容器,C是容器类型名,如vectorT是元素类型,如intstring。适用于所有容器
    C c(c2) 创建容器c2的副本cc2c必须具有相同的容器类型,并存放相同类型的元素。适用于所有容器
    C c(n) 创建有n个初始化元素的容器c。只适用顺序容器
    C c(n, t) 使用n个为t的元素创建容器c,其中值t必须是容器类型C的元素类型的值,或者是可以转换为该类型的值。只适用顺序容器
    C c(b, e) 创建容器c,其中元素是迭代器be标示的范围内元素的副本。适用于所有容器

    注意: 所有的容器类型都定了默认构造函数,用于创建制定类型的空容器对象。默认构造函数不带参数。为了使程序更加清晰、简短,容器类型最常用的构造函数时默认构造函数。在大多数的程序中,使用默认构造函数能达到最佳运行性能,并且使容器更容易使用。

    • 将一个容器初始化为另一个容器的副本
    vector<int> iVec;
    vector<int> iVec2(iVec);   // ok
    vector<double> dVec(iVec); // error, iVec holds int not double
    list<int> iList(iVec);     // error, iVec is not list<int>

    注意:讲一个容器复制给另一个容器时,必须类型匹配(容器的类型和元素的类型都必须相同)。

    • 初始化为一段元素的副本
      通过使用迭代器,间接实现将一种容器内的元素复制给另一种容器。使用迭代器时,不要求容器类型相同,容器内的元素类型也可以不相同,只要它们相互兼容,能够将要复制的元素转换为所构建的新容器的元素类型,即可实现复制。
    vector<string> sVec;
    // initialize sList with copy of each element of sVec
    list<string> sList(sVec.begin(), sVec.end());
    // calculate the midpoint in the vector
    vector<string>::iterator mid = sVec.begin() + sVec.size() / 2;
    // initialize front with first half of sVec: the elements up to but not including *mid
    vector<string> front(sVec.begin(), mid);
    
    // also can initialize with a pointer
    char* words[] = {"first", "second", "third", "forth"};
    int sizeOfWords = sizeof(words) / (sizeof(char*));
    vector<string> word2(words, words + sizeOfWords);
    
    // cout
    for ( int idx=0; idx<sizeOfWords; idx ++ )
        cout << word2[idx] << endl;
    • 分配和初始化指定数目的元素
      创建顺序容器时,可显式地指定容器大小和一个(可选的)元素初始化式。容器的大小可以是常量或者非常量表达式,元素初始化式必须是可用于初始化其元素类型对象的值:
    const list<int>::size_type listSize = 64; // also can be: int listSize = 64
    list<std::string> lstr(listSize, "str");  // 64 strings, each is str
    
    vector<int> iVec(listSize); // 64 ints, each initialized to 0 

    (4)容器内元素的类型约束
    C++语言中,大多数类型都可用作容器的元素类型。容器元素类型必须满足最基本的两个约束:

    • 元素类型必须支持赋值运算;
    • 元素类型的对象必须可以复制。

    除此外,一些容器操作对元素类型还有特殊要求。如果元素类型不支持这些要求,则相关的容器操作就不能执行:我们可以定义该类型的容器,但不能使用某些特定的操作。

    另外,旧版C++标准中,指定容器作为容器类型时,必须使用如下空格:

    vector<vector<int> > myVec;  // the space required between close >

    而在新版标准中,并无要求:

    vector<vector<int> > myVec; // ok
    vector<vector<int>> myVec;  // ok

    2.顺序容器的操作

    每种顺序容器都提供了一组有用的类型定义以及以下操作:

    • 在容器中添加元素;
    • 在容器中删除元素;
    • 设置容器大小;
    • (如果有的话)获取容器内的第一个和最后一个元素

    (1)容器定义的类型别名

    类型别名 含义
    size_type 无符号整型,足以存储此容器类型的最大可能容器长度
    iterator 此容器类型的迭代器类型
    const_iterator 元素只读迭代器类型
    reverse_iterator 按逆序寻址元素的迭代器类型
    const_reverse_iterator 元素只读逆序迭代器类型
    difference_type 足够存储两个迭代器差值的有符号整型,可为负数
    value_type 元素类型
    reference 元素的左值类型,是value_type&的同义词
    const_value_type 元素的常量左值类型,等效于const value_type&

    例如:

    // iter is the iterator type defined by vector<string>
    vector<string>::iterator iter;
    
    // cnt is the difference_type type defined by vector<int> 
    vector<int>::difference_type cnt;

    (2)容器内元素操作

    • beginend成员
    操作 功能
    c.begin() 返回一个迭代器,指向容器c的第一个元素
    c.end() 返回一个迭代去,指向容器c的最后一个元素的下一个位置
    c.rbegin() 返回一个逆序迭代器,指向容器c的最后一个元素**
    c.rend() 返回一个逆序迭代器,指向容器c的第一个元素前面的位置

    注意:
    (a) 迭代器范围是左闭右开区间,标准表达方式为:

    // includes the first and each element up to but not including last
    [first, lase)

    (b) 容器元素都是副本。在容器中添加元素时,系统是将元素值复制到容器里,被复制的原始值与新容器中的元素互不相关,此后,容器内元素值发生变化时,被复制的原值不会收到影响,反之亦然。

    (c) 不要存储end操作返回的迭代器。添加或者删除vectordeque容器内的元素都会导致迭代器失效。

    vector<int> v(42);
    // cache begin and end iterator
    vector<int>::iterator first = v.begin(), last = v.end();
    
    while( first!= last ) // disaster: this loop is undefined
    {
        // insert new value and reassign first, which otherwise would be invalid
        first = v.insert(++first, 2);
        ++ first; // advance first just past the element we added
    }
    • 添加元素
    操作 功能
    c.push_back(t) 在容器c的尾部添加值为t的元素,返回void类型
    c.push_front(t) 在容器c的前端添加值为t的元素,返回void类型(只适用于listdeque容器类型)
    c.insert(p, t) 在迭代器p所指向的元素前面插入值为t的新元素,返回指向新添加元素的迭代器
    c.insert(p, n, t) 在迭代器p所指向的元素前面添加插入n个值为t的新元素,返回void类型
    c.insert(p, b, e) 在迭代器p所指向元素前面插入由迭代器bc标记范围的元素,返回void类型
    // add elements at the end of vector
    vector<int> iVec;
    for ( int idx=0; idx<4; ++ idx )
    {
        iVec.push_back( idx );
    }
    
    // insert an element
    vector<string> sVec;
    string str("Insert");
    // warning: inserting anywhere but at the end of a vector is an expensive operation
    sVec.insert(sVec.begin(), str);
    
    // insert some elements
    sVec.insert(sVec.begin(), 10, "Anna");
    string array[4] =  {"first", "second", "third", "forth"};
    sVec.insert(sVec.end(), array, array+4);
    • 容器大小的操作
    操作 功能
    c.size() 返回容器c中元素个数,返回类型为c::size_type
    c.max_size() 返回容器c可容纳的最多元素个数,返回类型为c::size_type
    c.empty() 返回标记容器大小是否为0的布尔值
    c.resize(n) 调整容器c的长度大小,使其能容纳n个元素。如果n<c.size(),则删除多余的元素,否则,添加采用值初始化的新元素
    c.resize(n, t) 调整容器c的大小,使其能包纳n个元素,所有元素的值都为t
    vector<int> iVec(10, 1); // 10 ints, each has value 1
    iVec.resize(15);     // adds 5 elements of value 0 to back of iVec
    iVec.resize(25, -1); // adds 10 elements of value -1 to back of iVec
    iVec.resize(5);      // erases 20 elements from the back of iVec

    注意:resize操作可能会使迭代器失效。在vectordeque容器上做resize操作可能使其所有迭代器都失效。对于所有容器类型,如果resize操作压缩了容器,则指向已删除的元素的迭代器失效。

    • 访问元素
    操作 功能
    c.back() 返回容器c的最后一个元素的引用,如果c为空,则该操作未定义
    c.front() 返回容器c的第一个元素的引用,如果c为空,则该操作未定义
    c[n] 返回下标为n的元素的引用,如果n<0n>c.size(),则该操作未定义(只适用于vectordeque容器)
    c.at(n) 返回下标为n的元素的引用。如果下标越界,则该操作未定义(只适用于vectordeque容器)

    注意:使用越界的下标,或调用空容器的frontback函数,都会导致程序出现 严重的错误。

    • 删除元素:与插入元素对应容器类型提供了删除容器内元素的操作。
    操作 功能
    c.erase(p) 删除迭代器p所指向的元素,返回一个迭代器,它指向被删除元素后面的元素。如果p指向容器内的最后一个元素,则返回的迭代器指向容器的超出末端的下一位置,如果p本身就是指向超出末端的下一位置的迭代器,则该函数未定义
    c.erase(b, e) 删除迭代器be标记的范围内的所有元素。返回一个迭代器,它指向被删除元素段后面的元素。如果e本身就是指向超出末端的下一位置的迭代器,则返回的迭代器也指向容器末端的下一位置
    c.clear() 删除容器c内的所有元素,返回void
    c.pop_back() 删除容器c的最后一个元素,返回void。如果c为空容器,则该操作未定义
    c.pop_font() 删除容器c的第一个元素,返回void。如果c为空容器,则该操作未定义

    注意:
    (a) pop_front操作通常与front操作配套使用,实现栈(先进先出)的方式处理:

    while ( !vec.empty() )
    {
        // do something with the current top of vec
        process(vec.front()); 
        // remove first element
        vec.pop_front();
    }

    (b)删除一个或一段元素更通用的方法是erase操作。erase操作不会检查它的参数,使用时必须确保用作参数的迭代器或迭代器范围是有效的。

    (c) 寻找一个指定元素的最简单的方法是使用标准库的find算法(编程时需要添加头文件#include <algorithm>)。find函数需要一对标记查找范围的迭代器以及一个在该范围内查找的值作为参数。查找完成后,返回一个迭代器,它指向具有指定值的第一个元素或超出末端的下一位置。

    string searchValue("find");
    vector<std::string> vec(1, "find");
    vector<string>::iterator iter = std::find(vec.begin(), vec.end(), searchValue);
    if ( iter!= vec.end() )
        cout << *iter << endl;

    (d) 删除所有元素,可以用clear或将beginend迭代器传递给erase函数。

    vec.clear(); // delete all the elements within the container
    vec.erase(vec.begin(), vec.end()); // equivalent
    • 赋值与swap

    赋值操作中,首先删除其左操作数容器内的所有元素,然后将右操作数容器中的所有容器插入到左边容器中:

    vec1 = vec2; // replace contents of vec1 with a copy of elements in vec2
    // equivalent operation using erase and insert
    vec1.erase(vec1.begin(), vec1.end()); // delete all elements in vec1
    vec1.insert(vec2.begin(), vec2.end()); // insert vec2
    操作 功能
    c1=c2 删除容器c1中所有的元素,然后将c2的元素复制给c1c1c2的类型(包括容器类型和元素类型)必须相同
    c1.swap(c2) 交换内容:调用完该函数后,c1中存放的是c2原来的元素,c2中存放的是原来c1的元素。c1c2的类型必须相同。该函数的执行速度通常要比将c2复制到c1的操作快
    c.assign(b, e) 重新设置c的元素,将迭代器bc标记范围内的所有元素复制到c中。be必须不是指向c中元素的迭代器
    c.assign(n, t) c重新设置为存储n个值为t的元素

    注意:
    (a) swap操作不会删除或插入任何元素,而且保证在常量的时间内刷新交换。由于容器内没有移动任何元素,因此迭代器不会失效。

    (b) 在这里补充一点,vector容器大小有两个描述参数sizecapacitysize前面已经讲述过,指容器中当前已存储元素的数目,而capacity存储的是容器所分配的存储空间可以存储的元素总数。一般来说capacity >= size。在clear, 赋值(c1 = c2),assign(不超过原容器大小)等操作中,并未改变容器的capacity,也就是说,只是把已经分配好的内存上写入的元素数据清掉或者重新赋值,但并未对存储空间进行变动;但是swap操作时,sizecapacity都会改变。

    vector<int> vec(100); // size: 100, capacity: 100;
    vec.clear();          // size: 0, capacity: 100
    
    vector<int> vec2(50);
    vec = vec2;           // size: 50, capacity: 100
    
    vector<int> vec3(30);
    vec.assign(vec3.begin(), vec.end()); // size: 30, capacity: 100
    
    vec.swap(vec3); // error!
    
    vector<int> v1(30), v2(50);
    v1.swap(v2); // v1: size 50, capacity 50; v2: size 30, capacity 30

    基于此原因,有些时候,当我们想删除一个容器的所有元素的同时,又想把容器占用的内存释放掉时,clear并不能完全实现这一目的,但是可以通过swap:

    vector<int> vec(100);      // size 100, capacity 100
    vector<int>().swap( vec ); // size 0, capacity 0

    (c) vector容器中有reserve操作,可以设定存储空间大小:

    vector<int> vec(24);  // size 24, capacity 24
    vec.reserve(50);      // size 24, capacity 50
    cout<< "size" << vec.size() << endl << 
        "capacity" << vec.capacity() << endl;

    3.结束语

    我们很喜欢使用容器,因为确实很便捷,相比于数组,它可以很随意的实现元素的添加、删除等。我们也无需担心内存分配的问题,因为标准库会帮我们都搞定。但是我们最好还是了解一下。

    vector为例,为了支持快速的随机访问,vector容器的元素以连续的方式存放,即每一个元素都挨着前一个元素存储。当我们向容器中添加元素时,想想会发生什么:如果容器中已经没有空间容纳新元素,由于容器必须连续存储以便索引访问,所以不能在内存中随便找个地方来存储新元素,而是必须重新分配存储空间,存放在旧存储空间的元素被复制到新存储空间里,接着插入新元素,最后撤销旧的存储空间。如果vector容器在每次添加新元素时,都要这么分配和撤销内存空间,那么其性能将会非常慢!所以,标准库不会这么做,为了使vector容器实现快速的内存分配,其实际分配的容量要比当前所需的空间大一些,例如分配旧存储空间n倍(例如2倍)大小的新存储空间,这样的策略显著提高了其效率。

    vector容器的内存分配策略是以最小的代价连续存储元素,通过访问上的便利弥补其存储代价,虽然list容器优于vector容器,但是大部分情况下人们还是觉得vector更好用。实际中vector的增长效率比起listdeque通常会更高。

    参考文献:

    • 《C++ Primer中文版(第四版)》,Stanley B.Lippman et al. 著, 人民邮电出版社,2013。
    展开全文
  • HTML学习笔记:表单标记

    千次阅读 2008-02-21 16:32:00
    1、 用来定义一个表单区域,它是一个容器标记,其他表单标记需要放在与之间。常用属性有: (1)action:用来设定处理表单数据的页面或脚本,属性值通常为动态网页文件的路径。例如:action="do.jsp"、action="do....

    1、<form>
     <form>用来定义一个表单区域,它是一个容器标记,其他表单标记需要放在<form>与</form>之间。<form>常用属性有:
     (1)action:用来设定处理表单数据的页面或脚本,属性值通常为动态网页文件的路径。例如:action="do.jsp"、action="do.aspx"。    如果属性值为空则表示提交到页面本身。
     (2)method:用来设定将表单数据传输到服务器所使用的方法,可取的属性值有:
        (a)get:将表单数据附加到所请求的RUL中,此种方法不能传送大量数据,且不安全,所以不常用。
        (b)post:将表单数据嵌入HTTP请求中,此种方法容许传送大量数据,较为常用。
    2、文本框
     文本框允许用户输入单行信息,如姓名、邮件地址等。定义文本框的语法为:
      <input type="text">
     文本框除了type属性外,其他常用属性有:
     (1)name:用来设定文本框的名称,所选名称必须在表单内唯一标认该文本框,名称字符串不能包含空格或特殊字符,可以使用字母、   数字和下划线(_)的任意组合。表单提交到服务器后需要使用指定的名字来获取文本框的值。
     (2)value:用来设定文本框的默认值,也就是用户输入前文本框里显示的文本。
     (3)size:用来设定文本框最多可以显示的字符数,也就是文本框的长度。
     (4)maxlength:用来设定文本框中最多可以输入的字符数。通过此属性可以将邮政编码限制为6位数,将密码限制为10个字符,等等。
    3、密码框
     
     密码框用来让用户输入密码,当用户在密码框中键入时,输入内容显示为项目符号或星号,以保护它不被其他人看到。定义语法为:
      <input type="password">
     密码框的其它属性与文本框相同
    4、单选按钮
     单选按钮用来让用户只能从一组选项中选择一个选项,例如,性别的选择。单选按钮通常成组的使用,在同一个组中的所有单选按钮 必须具有相同的名字。定义单选按钮的语法为:
      <input type="radio">
     单选按钮除type外其它常用属性有:
     (1)name:用来设定单选按钮的名称,作用同文本框的name。同一组中所有单选按钮的此属性必须设定相同的值,否则,各选项不会相  互排斥。
     (2)value:用来设定在单选按钮被选种时发送级服务器的值。
     (3)checked:服务业设定在浏览器中载入表单时,该单选按钮是否被选种。如果开始标记里加入checked一词,则初始被选中。
    5、复选框
     定义复选框的语法为:
      <input type="checkbox">
     复选框除type外其它常用属性有:
     (1)name:用来设定复选框的名字,作用同文本框的name。
     (2)value:用来设定在复选框被先选中时发送给服务器的值。
     (3)checked:用来在浏览器中载入表单时,该复选框是否被选中。如果开始标记里加入checked一词,则初始被选中。
    6、下拉菜单
     下拉菜单的定义语法:
     <select name="from">
      <option value="henan">河南</option>
      <option value="hebei">河北</option>
      <option value="beijing" selected>北京</option>
     </select>
     一个下拉菜单由<select>和</select>来定义,<select>提供容器,它的name属性意义同文本框的相同。<option>用来定义一个菜单项  <option>与</option>之间的文本是呈现给访问者的,而选中后传送的值是由value指定的,如果省略value属性,则value的值与文本 相同,加入selected属性可以初始为选中状态。
    7、列表
     列表的作用与下拉列表相似,但显示的外观不同,列表在浏览器里显示时列出部分或全部选项,另外列表允许访问者选择一个或多个 项目。定义列表的语法如下:
      <select name="from" size="3" multiple>
       <option value="henan">河南</option>
       <option value="hebei">河北</option>
       <option value="beijing" selected>北京</option>
      </select>
     
     与下拉列表相似,<select>多了两个属性:size和multiple。size用来设定列表中显示的选项个数;加入multiple属性允许用户从列表 中选择多项。
    8、文件域
     文件域使用户可以选择其计算机上的文件,如字处理文档或图形文件,并将该文件上传到服务器。文件域的外观与文件框相似,只是  文件域还包含一个“浏览”按钮。用户可以手动输入要上传的文件的路径,也可以使用“浏览”按钮来定位并选择文件。如果要上传 文件,需要注意的是,<form>的method属性必须设置为post,另外,<form>必须加上属性enctype="multipart/form-data"。
     定义文件域的语法为:
      <input type="file">
     文件域除type属性外其它属性与文本的相同。
    9、隐藏域
     隐藏域用来存储并提取非用户输入信息,该信息对用户而言是隐藏的。隐藏域不在浏览器中显示任何外观。定义隐藏域的语法为:
       <input type="hidden" name="add" value="add">
     name属性用来指定名称,value属性用来指定传输的值。
    10、文本区域
     文本区域可以使用户输入多行信息,例如,输入留言、自我介绍等。定义文本区域的语法为:
      <textarea></textarea>
     开始标记与结束标记之间的文本为初始值,可以为空,但结束标记一定要有且正确。
         <textarea>常用属性有:
     (1)name:用来指定文本区域的名称。
     (2)rows:用来指定文本区域能够显示的行数,也就是文本区域的高度。
     (3)cols:用来指定文本区域能够显示的列数,也就是文本区域的宽度。
     (4)wrap:用来指定当用户在一行中输入的信息较多,无法在定义的文本区域内显示时,如何显示用户输入的信息。可取属性有:
      (a)off:防止文本换行到下一行。当用户输入的内容超过文本区域的右边界时,文本将向左侧滚动。用户必须按回车才能将插         入点移到文本区域的下一行 。
      (b)physical:在文本区域设置自动换行。当用户输入的内容超过文本区域的右边界时,文本换行到下一行。当提交数据进行       处理时,这些数据中包含自动换行时产生的换行符。
      (c)virtual:在文本区域中设置自动换行。当用户输入的内容超过文本区域的右边界时,文本换行到下一行。当提交数据进行       处理时,自动换行并不应用于数据,数据作为一个字符串进行处理。
    11、提交按钮
     提交按钮用来将表单数据提交到服务器。定义表单的语法为:
      <input type="submit">
     除type外其它常用属性有:
     (1)value:用来指定按钮上显示的文本。
     (2)name:用来指定按钮的名称。
    12、重置按钮
     重置按钮用来还原表单为初始状态。定义重置按钮的语法为:
      <input type="reset">
     
     重置按钮除type属性外其它属性与提交按钮的相同。
    13、普通按钮
     普通按钮在不添加脚本的情况下,只呈现一个按钮的外观,单击后没有任何动作。普通按钮通常用来跟JavaScript脚本相结合产生特  定的动作。定义普通按钮的语法为:
       <inype="button">
     普通按钮除type属性外其它属性与提交按钮的相同。
    14、图像按钮
     图像按钮就是用图像作为按钮图标,来实现按钮的功能。定义图像按钮的语法为:
      <input type="image">
     图像按钮除type外其它常用属性有:
     (1)name:用来指定图像按钮的名称。
     (2)src:用来指定要为该按钮使用的图像,属性值为图像文件的相对路径或绝对路径。
     (3)alt:用于输入描述性文本,一旦图像在浏览器中载入失败,将显示文本。
     (4)width:用来设定图像的高度。
     (5)height:用来设定图像的宽度。 

    表单标记举例:

    <html>
    <head>
    <title>表单举例:用户注册</title>
    </head>
    <body>
         
    <form method ="" enctype = "multipart/form-data">
        
    <table width = "432" border = "30" align = "center">
           
    <tr>
           
    <th colspan = "2">用户注册</th>
           
    </tr>
           
    <tr>
               
    <td width = "169" align = "right">昵称:</td>
            
    <td width = "253"><input name = "name" type = "text"></td>
           
    </tr>
           
    <tr>
            
    <td align = "right">密码:</td>
            
    <td><input name = "pass" type ="password"></td>
           
    </tr>
           
    <tr>
            
    <td align = "right">爱好:</td>
            
    <td><input name = "love1" type = "checkbox" value = "跑步">跑步
                
    <input name = "love2" type = "checkbox" value = "上网">上网
                
    <input name = "love3" type = "checkbox" value = "打拳">打拳
            
    </td>
           
    </tr>
           
    <tr>
            
    <td align = "right">来自:</td>
            
    <td><select name = "from">
                
    <option value = "beijing">北京市</option>
                
    <option value = "henan" selected>河南省</option>
                
    <option value = "hainan">海南省</option>
                
    <option value = "zhoukou" >周口市</option>
                
    </select>
            
    </td>
           
    </tr>
           
    <tr>
            
    <td align = "right">相片:</td>
            
    <td><input type = "file" name = "file"></td>
           
    </tr>
           
    <tr>
            
    <td align = "right" valign = "top">介绍</td>
            
    <td><textarea name = "intro" rows = "8"></textarea></td>
           
    </tr>
           
    <tr align = "center">
            
    <td colspan = "2"><input type = "submit" value = "我想好了">
            
    <input type = "reset" value = "容我三想"></td>
           
    </tr>
        
    </table>


    </form>

    </body>
    </html>
    展开全文
  • Java中的标记接口和标记注解

    千次阅读 2016-09-05 16:49:30
    一、标记接口  标识接口是没有任何方法和属性的接口.它仅仅表明它的类属于一个特定的类型,供其他代码来测试允许做一些事情...一些容器例如Ejb容器,servlet容器或运行时环境依赖标记接口识别类是否需要进行某种处理,
  • 容器编排解决方案

    千次阅读 2018-06-20 11:25:26
    目前市场上有很多容器编排工具可用,它们之间的能力有许多重叠的地方。通常的情况下,需要使用这些工具中的一个或多个的组合来满足业务的需求。容器管理或编排工具采用容器(包含要执行的服务)和一组约束或规则作为...
  • 标记接口

    2013-01-09 10:26:42
    标记接口:普通的接口的通常目的是保证类实现了某个或某组方法。而标记接口没有方法,像Cloneable接口是Java提供的几个标记接口之一。...一些容器例如Ejb容器,servlet容器或运行时环境依赖标记接口识别类是否需要
  • docker外部访问容器

    千次阅读 2019-05-13 21:33:55
    作者:【吴业亮】 ...外部访问容器 容器中可以运行一些网络应用,...当使用 -P 标记时,Docker 会随机映射一个 49000~49900 的端口到内部容器开放的网络端口。 使用 docker container ls 可以看到,本地主机的 49155 被...
  • Spring IoC容器

    千次阅读 2019-05-31 11:40:01
    核心技术 发行版5.0.8 这一部分的指导文档覆盖了所有的完全集成到Spring框架中的...Spring框架拥有他自己的AOP框架,该技术在概念上是很容器理解的,并且成功的解决了在Java企业编程中的AOP需求的80%的点。 该框...
  • spring @Order标记

    2019-08-05 14:49:45
    spring @Order标记 @Order标记定义了组件的加载顺序。...spring 4.0对@Order做了增强,它开始支持对装载在诸如Lists和Arrays容器中的自动包装(auto-wired)组件的排序。 在spring内部,对基于spring xml的应用,...
  • 今天同事写程序时恰好遇到这个Struts2+Hibernate开发时比较典型的问题: (1)Hibernate使用SQLQuery查询部分字段提高效率;(2)Struts2使用迭代标签s:iterate显示嵌套的Map和List以及数组这样比较复杂的分组一类...
  • C++容器(顺序容器、关联容器

    千次阅读 2012-04-05 20:18:12
    转载:http://hi.baidu.com/36470902/blog/item/50ed5a8b21491f1bc9fc7a3b.html ... 容器主要分为顺序容器和关联容器。 一,顺序容器 vector--连续存储的元素,单向的 list----由节点组
  • C++ 容器详解

    千次阅读 2018-04-14 20:35:56
    顺序容器:此处的顺序不是体现在元素的值的顺序,而是指的是元素加入容器时的位置相对应的。 关联容器:顺序容器分类: 类型 特点 vector 可变大小数组 支持快速随机访 在尾部之外的位置插入或删除元素可能...
  • C++容器容器操作

    2009-07-25 16:03:00
    1.容器位置操作:c.begin() 返回一个迭代器,它指向容器 c 的第一个元素c.end() 返回一个迭代器,它指向容器 c 的第一个元素c.rbegin() 返回一个逆序迭代器,它指向容器 c 的最后一个元素c.rend() 返回一个逆序迭代...
  • docker容器资源配额控制

    万次阅读 2016-06-22 07:48:10
    docker通过cgroup来控制容器使用的资源配额,包括CPU、内存、磁盘三大方面,基本覆盖了常见的资源配额和使用量控制。
  • Java 标记接口

    2016-09-10 23:58:24
    使用标记接口的唯一目的是使得可以用instanceof进行类型查询,例如:if(obj instanceof Cloneable) {………}一些容器例如Ejb容器,servlet容器或运行时环境依赖标记接口识别类是否需要进行某种处理,比如serialialbe...
  • 容器网络

    千次阅读 2017-07-15 11:54:15
    运行容器时,可通过“--network”标记来指定想要连接的网络,不然默认使用的是 bridge 网络。容器中的网络接口 docker0 代表的就是 bridge 网络。 none 网络可将容器添加到特定于容器的网络堆栈中。那个容器缺少...
  • Docker容器性能监控工具google/cadvisor

    千次阅读 2018-10-30 10:05:39
    cAdvisor(Container Advisor)为容器用户提供了对其运行容器的资源使用和性能特征的理解。 它是一个运行守护程序,用于收集,聚合,处理和导出有关正在运行的容器的信息。 具体而言,对于每个容器,它保留资源隔离...
  • 通过-p或-P参数来指定端口映射,使用-P(大写)标记时,docker会随机选择一个端口映射到容器内部开放的网络端口 启动容器docker run -d -P training/webapp python app.py,随机分配了32769端口 访问后,可以链接到...
  • 3、顺序容器所谓序列式容器,其中的元素都可序(ordered),但未必有序(sorted)。array为C++语言内置的序列容器,STL另外提供vector、list、deque、stack、queue等等顺序式容器。它们的差别在于访问元素的方式,...
  • Docker run 启动容器

    千次阅读 2019-05-08 17:23:20
    使用 docker run [OPTIONS] IMAGE [COMMAND] [ARG...] 选项 名称 描述 ...指定标准输入输出内容类型,可选 STDIN/STDOUT/STDERR 三项 详情 ...指定容器的工作目录 ...后台运行容器,并返回容器ID 详情 -p 端口映...
  • 1.1 容器技术概念

    万次阅读 2018-08-01 17:13:23
    容器 对于容器,它首先是一个相对独立的运行环境,在这一点有点类似于虚拟机,但是不像虚拟机那样彻底。在容器内,应该最小化其对外界的影响,比如不能在容器内把宿主机上的资源全部消耗,这就是资源控制。 ...
  • 容器与虚拟机对比

    千次阅读 2019-04-21 02:01:43
    容器简介 什么是容器 一句话概括容器容器就是将软件打包成标准化单元,以用于开发、交付和部署。 容器镜像是轻量的,可执行的独立软件包,包含软件运行所需的所有内容:代码,运行时环境,系统工具,系统库和...
  • 数据卷是一个可供容器使用的特殊目录,它绕过文件系统,可以提供很多有用的特性: ...在docker run命令的时候,使用-v标记可以在容器内创建一个数据卷。多次使用-v标记可以创建多个数据卷。 docker run -...
  • SkyForm CMP容器化实践

    万次阅读 2016-05-12 08:18:31
    Docker自从发布以来,短短3年的时间就迅速成长为各家争先追捧的宠儿,各家企业都希望能够...容器技术的引入为天云软件的SkyForm CMP带来了哪些变化;围绕着CI/CD,如何使天云软件在日常研发活动中变得更加敏捷、高效。
  • STL容器学习总结

    千次阅读 2016-05-18 14:53:57
    本文主要讨论C++标准库中的顺序容器及相应的容器适配器,这些内容主要涉及顺序容器类型:vector、list、deque,顺序容器适配器类型:stack、queue、priority_queue。  标准库中的容器分为顺序容器和关联容器。顺序...
  • DB2的高水位标记

    千次阅读 2017-06-24 14:38:11
    仅当该操作删除的扩展数据块的数目小于或等于表空间中的高水位标记之上的可用扩展数据块的数目时,才允许删除或缩小容器,就是说只能够删除还没有使用到的EXTENT。   高水位标记是表空间中分配的最高页的页数。...
  • 关联容器

    千次阅读 2007-05-22 16:04:00
    关联容器
  • docker容器介绍

    千次阅读 多人点赞 2020-03-09 17:11:00
    容器介绍 容器其实是一种沙盒技术。沙盒就是能够像一个集装箱一样,把你的应用"装"起来的技术。这样,应用与应用之间,就因为有了边界而不至于相互干扰;而被装进集装箱的应用,也可以被方便地搬来搬去。 ​ 问题:...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 192,991
精华内容 77,196
关键字:

容器标记