Java容器

抽象类和接口的区别:

  • 抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法;
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;
  • 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;一个类只能继承一个抽象类,而一个类却可以实现多个接口。

throw与throws的区别:

  • throw代表动作,表示抛出一个异常的动作;throws代表一种状态,代表方法可能有异常抛出;
  • throw用在方法实现中,而throws用在方法声明中;
  • throw只能用于抛出一种异常,而throws可以抛出多个异常

容器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xJFMFvTB-1658991226080)(C:\Users\Hao\Desktop\面试\img\image-20220715152832138.png)]

为什么使用集合?

我们知道,如果定义一个int数组,需要一开始就要制定它的大小。在一些情况下,我们根本不知道它的长度是多少,开辟很大的长度会导致空间浪费。
此外,数组还有很多缺点,例如数组中提供的方法非常有限,对于添加、删除、插入数据等操作,非常不便,同时效率不高。获取数据中实际元素的个数的需求,数组没有现成的属性或方法可用。数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。
为了数组能够更灵活的应用,提出了Java容器的概念。

Java的容器主要分为2个大类,即Collection和Map。Collection代表着集合,类似数组,只保存一个数字。而Map则是映射,保留键值对两个值

说说List,Set,Map三者的区别

对于collection有三个重要的接口:

List:存储的元素是有序的、可重复的。。比如Vector,ArrayList,LinkedList

Set:存储的元素是无序的、不可重复的。比如HashSet,TreeSet。

Queue:类似于一个先进先出的队列,存储的元素是有序的、可重复的。比如priorityQueue,他有个子类接口Dequeue,实现了双向队列比如ArrayDequeuq。

Map接口,使用键值对(key-value)来存储,key是无序的,不可重复的,value是无序的,可重复的,每个键最多映射到一个值。,其具体实现有HashMap,HashTable,TreeMap。

无序性和不可重复性的含义是什么

​ 无序性:无序性不等于随机性,无序性是指存储的数据在底层数据结构中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的。
​ 不可重复性:不可重复性指添加的元素按照equals()判断时,返回false,需要同时重写equals()方法和hashcode()方法。

comparable和comparator的区别

  • ​ comparable接口实际上是出自java.lang包,它有一个compareTo(Object object)方法来实现排序。
  • ​ comparator实际上是出自java.util包,它有一个compare(Object obj1,Object obj2)方法来排序。
  • ​ 一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()方法或compare()方法。当我们需要对某个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo()方法和使用自制的compare()方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用参数版的Collection.sort()。

RandomAccess接口

  • ​ RandomAccess接口没有定义方法,该接口是一个标志,标志实现这个接口的类具有随机访问功能。
  • ​ 在binarySearch()方法中,它要判断传入的list是否是RandomAccess的实例,如果是就调用indexedBinarySearch()方法,如果不是就调用iteratorBinarySearch()方法。
  • ​ ArrayList实现了RandomAccess接口,而LinkedList没有实现。因为ArrayList底层是数组,数组天然支持随机访问;LinkedList底层为链表,需要遍历到特定位置才能访问特定位置的元素,所以不支持快速随机访问。
  • RandomAccess只是一个标志,并不是实现了该接口采用随机访问功能。

如何选用集合

​ 主要根据集合的特点来使用,当我们需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时选择HashMap,需要保证线程安全时就选择ConcurrentHashMap。
当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择Set接口的集合比如TreeSet/Hashset,不需要就选择实现List的接口比如ArrayList/LinkedList,然后根据这些接口的特点来使用。

Collection和Collections的 区别是什么?

java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。

Collection 接口在 Java 类库中有很多具体的实现。

Collection 接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有 List 与 Set。

Collections 则是集合类的一个工具类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。

线程安全的集合有哪些?线程不安全的呢?

线程安全的:

Hashtable:比HashMap多了个线程安全。

ConcurrentHashMap:是一种高效而且线程安全的集合。

Vector:比Arraylist多了个同步化机制。

Stack:栈,也是线程安全的,继承于Vector。

线性不安全的:

**HashMap

Arraylist

LinkedList

HashSet

TreeSet

TreeMap

二叉搜索树

二叉搜索树又称二叉排序树,具有以下性质:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树

什么是红黑树为什么需要红黑树

红黑树是一种自平衡的二叉查找树,是一种高效的查找树。红黑树具有良好的效率,它可在 O(logN) 时间内完成查找、增加、删除等操作。

出现红黑树的原因就是对于二叉搜索树,如果插入的数据是随机的,那么它就是接近平衡的二叉树,平衡的二叉树,它的操作效率(查询,插入,删除)效率较高,时间复杂度是O(logN)。但是可能会出现一种极端的情况,那就是插入的数据是有序的(递增或者递减),那么所有的节点都会在根节点的右侧或左侧,此时,二叉搜索树就变为了一个链表,它的操作效率就降低了,时间复杂度为O(N),所以可以认为二叉搜索树的时间复杂度介于O(logN)和O(N)之间。

为了解决二叉搜索数的这种问题,就会在插入或者删除的时候进行旋转操作保持平衡。如果在AVL树中插入或删除节点后,使得高度之差大于1。此时,AVL树的平衡状态就被破坏,它就不再是一棵二叉树;为了让它重新维持在一个平衡状态,就需要对其进行旋转处理, 那么创建一颗平衡二叉树的成本其实不小.在频繁进行插入/删除的场景中,频繁的旋转操作使得AVL的性能大打折扣。

那么为了应对这种极端情况,红黑树就出现了,红黑树是一种接近平衡的二叉树(说它是接近平衡因为它并没有像AVL树的平衡因子的概念,它只是靠着满足红黑节点的5条性质来维持一种接近平衡的结构,进而提升整体的性能,并没有严格的卡定某个平衡因子来维持绝对平衡)。

首先,红黑树是一个二叉搜索树,它在每个节点增加了一个存储位记录节点的颜色,可以是RED,也可以是BLACK;通过任意一条从根节点到叶子节点路径上颜色的约束来保证最长路径不超过最短路径的二倍,因而近似平衡。

1、节点是红色或黑色
2、根是黑色
3、叶子节点(外部节点,空节点)都是黑色,这里的叶子节点指的是最底层的空节点(外部节点),下图中的那些null节点才是叶子节点,null节点的父节点在红黑树里不将其看作叶子节点
4、红色节点的子节点都是黑色
红色节点的父节点都是黑色
从根节点到叶子节点的所有路径上不能有 2 个连续的红色节点
5、从任一节点到叶子节点的所有路径都包含相同数目的黑色节点

那么在插入时我们要进行以下两个操作,来维持红黑的规则正确:操作1:变色,操作2:旋转,在插入时,新插入的节点我们总是认为是红色,并通过变色和旋转的操作实现5条原则。来保持一种自平衡的状态。

HashSet的实现原理

HashSet 实现 Set 接口,它不保证 set 的迭代顺序,并且不允许有重复的元素。是基于 HashMap 实现的,HashSet 中的元素都存放在 HashMap 的 key 上面,而 value 中的值都是统一的一个 private static final Object PRESENT = new Object()。保证了值不会重复,但是不保证 set 的迭代顺序。HashSet跟HashMap一样,都是一个存放链表的数组。HashSet 中不允许有重复元素

HashSet如何检查重复

检查方式
​ 当把对象放入HashSet时,首先计算其hashcode,并与其他已加入对象的hashcode相比较,如果没有相同的hashcode,则认为没有重复对象。如果发现有相同hashcode,则用equals方法判断两个对象是否相等,如果相等则不插入。
​ hashcode()与equals()的相关规定
​ 1.如果两个对象相等,则hashcode一定相等。
​ 2.两个对象相等, 调用equals()方法会返回True。
​ 3.两个对象hashcode相等,对象不一定相等。
​ 4.综上,equals方法被覆盖过,则hashcode()方法也必须被覆盖。
​ 5.hashcode()的默认行为是对堆上的对象产生hashcode,如果没有重写hashcode()方法,两个class对象无论如何都不会相等。

Arraylist与 LinkedList 异同点?

  • 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
  • 底层数据结构: Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向循环链表数据结构;
  • 插入和删除是否受元素位置的影响:
  • 是否支持快速随机访问:
  • 内存空间占用: ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。

CopyOnWriteArrayList:

CopyOnWriteArrayList是一个线程安全的ArrayList

如果一段并发程序,读操作明显多于写操作的话,那么使用CopyOnWriteArrayList的性能会比Vector更高

CopyOnWriteArrayList的实现原理就是读写分离,它对所有的写操作都使用ReentrantLock来加锁,对所有的读操作都不加锁

**原理:**CopyOnWriteArrayList在写操作的时候,都会将list中的数组copy一份作为缓存,然后对该缓存中的数组进行操作(此时若有其他线程过来读的话,那么该线程读的还是原先没有被修改过的数组,若有其他线程过来写的话,那么该线程会因为ReentrantLock的原因被锁在外面。),操作完毕后再将list中的数组地址引用指向修改后的新数组地址。

ArrayList 与 Vector 区别?

  • Vector是线程安全的,ArrayList不是线程安全的。Vector在关键性的方法前面都加了synchronized关键字,来保证线程的安全性。如果有多个线程会访问到集合,那最好是使用 Vector,因为不需要我们自己再去考虑和编写线程安全的代码。但是由于同步机制使得访问的效率比ArrayList低好多。
  • ArrayList在底层数组不够用时在原来的基础上扩展1.5倍,初始化长度为10,Vector初始化长度为10,是在基础之上扩展两倍,这样ArrayList就有利于节约内存空间。

ArrayList与LinkedList的区别

  • ​ 1.是否保证线程安全:ArrayList和LinkedList都是不同步的,都是线程不安全的。

  • ​ 2.底层数据结构:ArrayList底层使用的是Object数组,LinkedList底层使用的是双向链表。

  • ​ 3.插入和删除元素时是否受元素位置的影响:ArrayList采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。LinkedList采用链表存储,所以对于add(E e)方法的插入,删除元素时间复杂度不受元素位置的影响,近似O(1),如果要在指定位置i插入和删除元素的话add(int index,E element),时间复杂度近似O(n),因为需要先移动到指定位置再插入。

  • ​ 4.是否支持快速随机访问:ArrayList支持,LinkedList不支持。快速随机访问指的是通过元素的序号快速获取元素对象(对应于get(int index)方法)。

  • ​ 5.内存空间占用:ArrayList的空间浪费主要体现在list的结尾会预留一定的容量空间(并不会全部填充完毕才扩容,达到一定阈值就会扩容),而LinkedList的花费体现在它的每一个元素都需要额外的空间消耗,因为要存放前驱结点和后继节点。

使用Arrays.asList()时的问题

会返回一个return new ArrayList<>(a)发现,其实这个ArrayList并不是我们平时用的ArrayList.,返回对象是一个Arrays内部类,不能使用集合相关的方法,它的add/remove/clear方法会抛出 UnsupportedOperationException 异常。
String[] str = new String[] { “chen”, “yang”, “hao” };
List list = Arrays.asList(str);
第一种情况: list.add(“yangguanbao”); 运行时异常。
第二种情况: str[0] = “change”; 也会随之修改,反之亦然。

而是Arrays里面的一个内部类。而且这个内部类没有add,clear,remove方法。

说一说ArrayList 的扩容机制?

扩容:在JDK1.6以前,初始容量为10,扩容按照1.5倍扩容。

jdk1.8中:通过无参构造器创建的ArrayList的初始大小为0,当第一次就插入元素的时候才分配10个对象空间,当空间不够时,比如要插入20个对象,在第11个的时候,会扩容1.5倍变成15,当添加第16个元素的时候,会在(当前容量)15的基础上再库容1.5倍,变成22,之后每次按当前长度的1.5倍扩容。

HashMap什么时候扩容?怎么扩容?

Hashmap 在容量超过负载因子所定义的容量之后,就会扩容。方法是将 Hashmap 的大小扩大为原来数组的两倍,然后原来的对象放入新的数组中。

jdk1.7 采用了数组加链表的数据结构。在扩容时候,创建新数组长度为原来的两倍,用元素hash值和需要扩容的二进制数进行&运算,后计算各个元素在新数组中的位置,并采用了头插法,将单向链表的数据进行迁移,。导致迁移过后顺序会改变

jdk1.8 采用了数组加是红黑树加链表的数据结构 如果对应角标是单向链表,将单向链表进行数据迁移 如果对应角标是红黑树,将双向链表进行数据迁移 如果发现迁移完数据后,双向链表的结构小于等于6,会将红黑树重新转回单向链表的结构,加上一个间隔可以防止链表和树之间的频繁转换造成的性能问题。在进行扩容时,根据 (e.hash & oldCap) == 0 来判断的:等于0时,则将该节点放到新数组时的索引位置等于其在旧数组时的索引位置,不等于0时,则将该节点放到新数组时的索引位置等于其在旧数组时的索引位置再加上旧数组长度。并采用尾插的方式来保证顺序不会发生改变。

jdk1.7多线程会形成环形链表(头插法)多个线程进行扩容时候,由于采用头插法造成了链表逆序的问题,就有可能造成死链的问题。

jdk1.8多线程下可能数据丢失(尾插法)假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

HashMap与HashTable的区别

​ 1.线程是否安全:HashMap是非线程安全的,HashTable是线程安全的,方法使用synchronized修饰。
​ 2.底层数据结构:HashMap底层为数组+链表,jdk1.8后增加了红黑树,而HashTable底层为数组+链表。
​ 3.效率:因为线程安全的问题,HashMap效率高一点,HashTable基本被淘汰。
​ 4.对null key 和null value的支持:HashMap可以存储null的key和value,null作为键只能有一个,null作为值可以有多个。HashTable不允许有null键和null值,否则会报nullpointerException。
​ 5.初始容量大小和每次扩容大小的不同:创建时如果不指定初始容量值,HashMap的默认大小为16,每次扩容后容量变为原来2倍,HashTable的默认大小为11,每次扩容大小变为原来的2n+1倍;创建时如果给定了初始容量大小,HashMap会将其扩充为2的幂次方大小,HashTable会直接使用给定的大小。

HashMap和TreeMap的区别

  • **数据结构不一致:**HashMap数组加链表,TreeMap是红黑树的数据结构

  • 线程安全
    HashMap 和 TreeMap 都不是线程安全的。

  • 没有关键字synchronized修饰,也没有JUC包类的同步机制。

  • 实现的接口
    TreeMap 和 HashMap 都继承自 AbstractMap ,但是需要注意的是 TreeMap 它还实现了 NavigableMap 接口和 SortedMap 接口。

    实现 AbstractMap 类,覆盖了hashcode() 和 equals() 方法,以确保两个相等的映射返回相同的哈希值。

    实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力。

    实现 SortedMap 接口让 TreeMap 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。

  • 性能
    HashMap:适用于在Map中插入、删除和定位元素。
    Treemap:适用于按自然顺序或自定义顺序遍历键(key)。

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

​ 取余%操作如果除数是2的幂则等价于其除数-1的按位与&操作,即hash%length==hash&(length-1),前提是length为2的n次方。此外,采用位运算相比于%具有更高的效率。
​ 为了能让HashMap存取高效,尽量减少碰撞,需要把数据分配均匀。

JDK1.7和JDK1.8中HashMap为什么是线程不安全的?

  • 在JDK1.7中,HashMap的线程不安全主要发生在扩容函数中,即根源是在transfer函数中。HashMap在该函数中的扩容操作为,重定位每个桶的下标,采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点。

    假设有A/B两个线程同时对HashMap进程扩容,若A在处理节点时CPU时间片用完,线程A被挂起。线程B此时执行扩容操作,并完成了数据迁移,并且链表已逆向排序。由于Java内存模型可知,线程B执行完数据迁移后,此处主内存中数组都是最新的,此时线程A中在处理节点时就可能出现环形链表,导致中间的某些数据丢失。

  • JDk1.8中的线程不安全主要体现在存入元素的时候,存入元素时要先判断是否出现了hash碰撞,假设A/B两个线程都在执行put操作,并且hash计算得到的值是相同的,当A执行完碰撞检测后CPU时间片耗尽被挂起,B得到时间片后在该位置插入了元素。随后在A获得时间片后,由于已经进行过hash碰撞的判断了,所以不会再进行判断,而是直接插入元素,这个操作就会覆盖掉B线程插入的元素,造成数据覆盖,从而线程不安全。

  • 此外,在HashMap的put函数中存在一个++size,当线程A/B同时进行put操作的时候,假设当前size为10,线程A从主内存中拿到size值为10并准备进行+1操作,但是CPU时间片耗尽,线程A挂起,此时B从主内存中拿到size的值为10并进行+1操作,完成put操作并将size=11写会主内存。随后线程A拿到时间片继续执行(此时size还为10),执行完put操作后,将size=11放回主内存。此时线程A/B都执行了一次put操作,但是size的值只增加了1,也导致数据覆盖,引发了线程的不安全。

HashMap为什么将链表转成红黑树

​ 当采用数组加链表的方式作为HashMap的数据结构的时候,如果Hash冲突较为严重,那么链表的长度将会非常的长,当查找元素的时候通过便利的方式使得性能影响非常严重,时间复杂度达到了O(n)。通常思路就是将其转化为搜索二叉树,左子树左右的节点逗比根节点小,右子树所有的节点都都比根节点大。这样在查找的时候就可以通过二分查找提高查找效率问题,时间复杂度达到了o(nlogn)。但是,如果插入的元素是随机的,那么搜索树平衡,能够保障查找效率。但是如果插入元素是顺序的,那么搜索树将会退化成链表的形式,失去了高效的查找效率。

红黑树是近似平衡的,是不严格的平衡。红黑树与avl树相比,在检索的时候效率是差不多的,都是通过平衡来二分查找。但是红黑树对于插入删除等操作效率会提高很多。因为红黑树不像AVL树一样追求绝对的平衡,红黑树允许局部很少的不完全平衡,这样子对于效率影响不大,但省去了很多不必要的调平衡操作。AVL树调平衡有时候代价比较大,所以效率不如红黑树,到现在很多地方都是红黑树的天下了!
红黑树牺牲了一些查找性能,但是其本身不是完全平衡的二叉树,因此插入操作的效率略高于AVL树。
转红黑树的原因:因为在链表中取一个数需要遍历链表,时间复杂度为O(N),而红黑树效率为O(logN).

为什么不直接使用红黑树,而是先使用链表,实在不行再转红黑树呢

​ 因为树节点的大小是链表节点大小的两倍,所以只有在容器中包含足够节点的时候才使用它。尽管转为树使得查找的速度更快,但是在节点个数比较少的时候,对于红黑树来说,内存上的劣势会超过查找操作的优势,自然使用链表更加好。但是在节点较多的时候,综合考虑,红黑树比链表好。

HashMap中红黑树退化成链表的条件

​ 1.扩容resize()时,红黑树拆分成的树的节点个数小于等于临界值6个,则退化成链表
​ 2.移除元素remove()时,在removeTreeNode()方法中会检查红黑树是否满足退化条件。如果红黑树根节点root为null或root的左子树/右子树为null,root的左子树的左子树(root.left.left)为null,都会发生红黑树退化成链表。

HashMap1.7和1.8区别

  • 底层数据结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构(当链表长度大于8,且数组长度为64的时候,转为红黑树)。这样解决了当一个链表的长度过长时候,遍历的性能就会降低。
  • 插入位置不一致,JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
  • 扩容后数据存储位置的计算方式也不一样:在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&运算,而在JDK1.8的时候Java 是根据 (e.hash & oldCap) == 0 来判断的:等于0时,则将该节点放到新数组时的索引位置等于其在旧数组时的索引位置,不等于0时,则将该节点放到新数组时的索引位置等于其在旧数组时的索引位置再加上旧数组长度,而不再是JDK1.7的那种异或的方法。
  • 扰动函数不一致扰动函数是为了是的散列位置更加均匀,,JDK1.7用了9次扰动处理=4次位运算+5次异或,而JDK1.8只用了2次扰动处理=1次位运算+1次异或。
  • 1.8中没有区分键为null的情况,而1.7版本中对于键为null的情况调用putForNullKey()方法。但是两个版本中如果键为null,那么调用hash()方法得到的都将是0,所以键为null的元素都始终位于哈希表table【0】中。
  • 初始化的方式也不一致,在1.7中,初始化是通过单独的函数inflateable实现的,而在1.8中初始化再结集成到了resize()方法中,第一次直接扩容到16,之后保存了下一次扩容的数组大小。

String、StringBuffer、StringBuilder(StringBuffer和StringBuilder的各种方法,见书310页)

String、StringBuffer、Stringbuilder都是使用char型数组实现的,jdk9以后,改用byte数组实现,

其中String中的char型数组被final修饰,不可变,可以理解为是常量,因此是线程安全的。每次修改String对象时,都要新建一个String对象,而原来的String对象将被弃用,这样会触发GC,并且字符串的创建的十分频繁的操作。由于字符串的原因导致频繁的GC使程序的效率变慢,当对字符串进行频繁操作时,一般使用Stringbuffer或者是Stringbuilder,因为其byte数组没有被final修饰,是可变的,是对Stringbuffer和Stringbuilder本身进行操作,而不是生成新的对象并改变对象的引用。并且在java9之前,String类型的保存是通过char数组实现的,在java9之后使用byte数组实现这是因为为了节省空间。char 类型的数据在 JVM 中是占用两个字节的,并且使用的是 UTF-8 编码,其值范围在 ‘\u0000’(0)和 ‘\uffff’(65,535)(包含)之间。而byte类型只占用一个字节,这导致保存向A这样的字符也得占用两个字节,实际上只需要一个字节就可以保存,造成了一半空间的浪费。例如:String name = “jack”;采用char数组保存需要8个字节,而采用byte数组保存只需要4个字节。减少一半的空间使用。

也就是说,使用 char[] 来表示 String 就导致了即使 String 中的字符只用一个字节就能表示,也得占用两个字节。

其中,Stringbuffer是线程安全的,而Stringbuilder是线程不安全的。但是在单线程条件下,Stringbuilder效率比Stringbuffer高。Stringbuffer与Stringbuilder的扩容机制都是一样的,初始时调用父类(AbstractStringBuilder)的有参或无参构造方法进行初始化,若为无参构造方法,则初始容量为16,若为有参构造时,初始容量为传入的字符串参数长度+16. 扩容,若要追加的长度大于可用容量,则按照(当前容量)*2+2进行扩容。当追加的长度大于(当前容量)*2+2时,在原来已经被占用的基础上直接扩容与所添加的字符串长度相等的长度(如,原已用4,现需要追加60,直接扩容60,即总容量扩为64)。使用sb.capacity()查看总容量。

String为什么是final?

**性能(效率):**如果指定一个类为final,则该类所有的方法都是final。Java编译器会寻找机会内联(inline)所有的final方法(这和具体的编译器实现有关)。此举能够使性能平均提高50%。若 String允许被继承, 由于它的高度被使用率, 可能会降低程序的性能,所以String被定义成final。

**安全性:**String类的内部好多方法的实现都不是Java编程语言本身编写的,好多方法都是调用的操作系统本地的API,也就是“本地方法调用”(),这种类是非常底层的,和操作系统交流频繁的,那么如果这种类可以被继承的话,如果我们再把它的方法重写了,往操作系统内部写入一段具有恶意攻击性质的代码什么的,这不就成了核心病毒了么?

String s=“a”+"b"创建了几个对象?

在JDK1.7以前,创建了3个对象,在jdk1.8以后,只是创建了一个对象。

String s2=new String(“hello”)–>创建两个对象

new String(“a”)+new String(“b”)–>创建6个对象对象

1:new StringBuilder()

对象2:new String(“a”)

对象3:常量池中的(“a”)

对象4:new String(“b”)

对象5:常量池中的"b"

对象6:new String(“ab”)

String Table

String有两种初始化方式:String s1=“hello”;

String s2=new String(“hello”)–>创建两个对象

new String(“a”)+new String(“b”)–>创建6个对象

对象1:new StringBuilder()

对象2:new String(“a”)

对象3:常量池中的(“a”)

对象4:new String(“b”)

对象5:常量池中的"b"

对象6:new String(“ab”)

StringBuffer的ToString的调用,在字符串常量池中,没有生成"ab"。只是返回了堆中的ab
String 声明为final,不可被继承。String支持Serializable接口,表示字符串是支持序列化的。实现了Comparable接口,表示String可以比较大小。

在jdk8及以前内部定义了final char[],value用于存储字符串数据,JDK9时改成了byte[].

String 的String pool是一个固定大小的Hashtable,默认大小是1009(JDK6)、60013(JDK7)、JDK8开始1009是StringTable长度可设置的最小值。

JDK6及以前,字符串常量池放在永久代中。

JDK7中将字符串常量池的位置调整到java堆中。

字符的拼接操作:

1 常量与常量的拼接结果在常量池,原理是编译期优化

2 常量池中不会存在相同内容的常量。

3 只要其中有一个是变量,结果就在堆中。

变量拼接的原理是StringBuilder。

4 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址(常量池地址)。

讲一讲快速失败(fail-fast)和安全失败(fail-safe)

  • 快速失败(fail—fast):在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),就会抛出Concurrent Modification Exception。
  • 原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,modCount的值就会改变。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值(最开始的时候modCount=expectedModCount),是的话就返回遍历;否则抛出异常,终止遍历。
  • 注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改),比如HashMap、ArrayList 这些集合类。

避免fail-fast(不问就不说):

  • 在单线程的遍历过程中,如果要进行remove操作,可以调用迭代器的remove方法而不是集合类的remove方法,比如ArrayList的迭代器的remove方法只能remove遍历到的那个元素,所以不会发生fail-fast现象,但是这种方法具有局限性。
  • 使用java并发包(java.util.concurrent)中的类来代替 ArrayList 和hashMap。ArrayList–> CopyOnWriterArrayList (写时拷贝技术,在add、remove方法的时候,并不会在原来的数组上进行操作,而是把原来的数组拷贝一份,在数组上进行修改,然后把引用指向新数组,它只能保证最终一致性,不能保证强一致)、hashMap–> ConcurrentHashMap。

安全失败(fail—safe)

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。原理:由于迭代时是对原来集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。

缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改,比如:ConcurrentHashMap。

linux

cd 切换目录
ls 查看文件和目录
grep 分析一行的信息,有的话就显示出来

-a 把binary文件以text文件的方式查找数据

-c计算找到目标字符串的次数 -i 忽略大小写

-v显示没有目标字符串的哪一行

比如:ls -l | grep -i file 把ls -l的输出中包含file的内容输出。

cp 复制

-a 把目标文件的特性一起复制

-p 连同属性一起复制,一般用来备份
*
-i 目标文件存在,覆盖的时候会询问操作的进行
*
-r递归复制,一般用来复制目录
*
-u目标文件和源文件有差异的时候才会复制

mv 移动
*
-f 不会询问,直接覆盖
*
-i 会询问覆盖
*
-u 比目标文件新,才更新

rm 删除 -f 忽略不存在的文件,不会出现警告
*
-i 删除前询问用户是否删除
*
-r递归删除,一般用于目录删除。

ps 选取进程的运行情况并且输出
*
-A 显示所有进程
*
*
*
-a 不与terminal有关的所有进程
*
-u 有效用户的一些线程
*
-X 一般和a参数一起使用,列出比较完整的信息
*
-l 把PID信息详细地列出

-a 把目标文件的特性一起复制

-p 连同属性一起复制,一般用来备份
*
-i 目标文件存在,覆盖的时候会询问操作的进行
*
-r递归复制,一般用来复制目录
*
-u目标文件和源文件有差异的时候才会复制

mv 移动
*
-f 不会询问,直接覆盖
*
-i 会询问覆盖
*
-u 比目标文件新,才更新

rm 删除 -f 忽略不存在的文件,不会出现警告
*
-i 删除前询问用户是否删除
*
-r递归删除,一般用于目录删除。

ps 选取进程的运行情况并且输出
*
-A 显示所有进程
*
*
*
-a 不与terminal有关的所有进程
*
-u 有效用户的一些线程
*
-X 一般和a参数一起使用,列出比较完整的信息
*
-l 把PID信息详细地列出

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值