Java SE 之 String 、 Object 类的基本方法 和 集合 丨【奔跑吧!JAVA】

考过IE励志当攻城狮 发表于 2021/05/25 10:49:36 2021/05/25
【摘要】 Java SE 之 String 、 Object 类的基本方法 和 集合


一、String 和 Object 类的基本方法

1. String 类常用的方法


2. Object 类常用的方法

Object类有11个成员方法,加上一个构造方法。下面分别简介一下这些方法。

(1)public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。

(2)public native int hashCode()//native方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK中的HashMap。

(3)public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写,用户比较字符串的值是否相等。

(4)protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象x,表达式x.clone()!=x为true, x.clone().getClass()==x.getClass()为true。Object本身没有实现Cloneable接口,所以不重写clone方法<br>并且进行调用的话会发生CloneNotSupportedException异常。

(5)public String toString(//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。

(6)public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。

(7)public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。

(8)public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意: sleep方法没有释放锁,而wait方法释放了锁。timeout是等待时间。

(9)public final void wait(long timeout,int nanos) throws InterruptedException/l多了nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是0-999999)。所以超时的时间还需要加上nanos毫秒。

(10)public final void wait()throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念

(11)protected void finalize() throws Throwable { //实例被垃圾回收器回收的时候触发的操作

三、常见面试题

1  String . StringBuilder . StringBuffer 的区别?

Java平台提供了两种类型的字符串: String 和 StringBuffer/StringBuilder,它们都可以储存和操作字符串,区别如下。
1) String 是只读字符串,也就意味着String 引用的字符串内容是不能被改变的。初学者可能会有这样的误解:
     1.String str ="abc";
     2. str =“bcd";
     如上,字符串str明明是可以改变的呀!其实不然,str仅仅是一个引用对象,它指向一个字符串对象"abc"。第二行代码的含义是让 str 重新指向了一个新的字符串"bcd"对象,而"abc"对象并没有任何改变,只不过该对象已经成为一个不可及对象罢了。
2)StringBuffer/StringBuilder表示的字符串对象可以直接进行修改。
3) StringBuilder是Java5中引入的,它和StringBuffer 的方法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方法都没有被synchronized 修饰,因此它的效率理论上也比StringBuffer要高。

2  equals()与==的区别

基本类型(int,char,long,boolean等)都是用==判断相等性。对象引用的话,判断引用所指的对象是否是同一个。equals是Object 的成员函数,默认是使用==,都是比较地址值。但一般的类会覆盖(override)这个方法.用于判断对象的等价性。例如 String类,两个引用所指向的String 都是"abc",但可能出现他们实际对应的对象并不是同一个(和jyvm 实现方式有关),因此用判断他们可能不相等,但用equals判断一定是相等的。

3  是否可以继承String类?

String类是final修饰的类,故不可以继承。
拓展,String为什么要用final修饰?
        答:为了安全和效率。
        因为只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么String interning将不能实现,因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。
        如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
        因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。 
        因为字符串是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map 中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。


二、集合

1.掌握Collection和Map的继承体系。

链表
链表是一种物理上非连续,非顺序的存储结构,数据元素之间的顺序是通过每个元素的指针关联的
链表有一系列节点组成,每个节点一般至少会包含两部分的信息:
          1.元素数据
          2.指向下一个元素的指针
链表分类:
          1.单向链表和双向链表
          2.静态链表(数组实现),动态链表(指针)
链表的操作:创建,插入,删除,输出
 链表的特点:
           1.物理空间不连续,空间开销更大
           2.在运行时可以动态添加
           3.查找元素需要顺序查找 
  
2. 常见面试题
(1)ArrayList和 LinkList的区别
       ArrayList(数组结构)︰
             优点: get和set调用花费常数时间,也就是查询的速度快;
             缺点∶新项的插入和现有项的删除代价昂贵,也就是添加删除的速度慢
       LinkedList(链表结构)︰
             优点:新项的插入和和现有项的删除开销很小,即添加和删除的速度快
             缺点:对get和set的调用花费昂贵,不适合做查询
面试中经常问到一些深入的东西,比如︰
  1.是否保证线程安全:ArrayList和 LinkedList 都是不同步的,也就是不保证线程安全;
  2.   底层数据结构:Arraylist底层使用的是Object 数组; LinkedList底层使用的是双向循环链表数据结构;
  3.   插入和删除是否受元素位置的影响:

①. ArrayList 采用数组存储.所以插入和删除元素的时间复杂度受元素位置的影响。比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置i插入和删除元素的话(add(int index,E element))时间复杂度就为O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。

.LinkedList采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。

4.是否支持快速随机访问:LinkedList 不支持高效的随机元素访问,而ArrayList实现了RandmoAccess接口,所以有随机访问功能。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。

5.内存空间占用:ArrayList 的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

(2)HashMap底层原理
HashMap实际上是一个"链表散列"的数据结构,即数组和链表的结合体。这是 jdk8之前的实现方式,但是在JDK8后对 HashMap进行了底层优化,改为了由数组+链表+红黑树实现,主要的目的是提高查找效率。

HashMap的主结构类似于一个数组,添加值时通过key确定储存位置.
每个位置是一个Entry的数据结构,该结构可组成链表.当发生冲突时,相同hash值的键值对会组成链表.这种数组+链表的组合形式大部分情况下都能有不错的性能效果,Java6、7就是这样设计的.
然而,在极端情况下,一组(比如经过精心设计的)键值对都发生了冲突,这时的哈希结构就会退化成一个链表,使HashMap 性能急剧下降.
所以在Java8中,HashMap的结构实现变为数组+链表+红黑树
可以看出,HashMap底层就是一个数组结构。
数组中的每一项又是一个链表.当新建一个HashMap时,就会初始化一个数组.
简单地说,HashMap 在底层将key-value 当成一个整体进行处理,这个整体就是一个Entry对象。HsahMap底层采用一个Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash 算法来决定其在数组的存储位置,在根据equals 方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry 时,也会根据hash算法找到其在数组中的存储位置,再根据equals 方法从该位置上的链表中取出该 Entry。
3)HashMap 的put()和get()原理
1)java7及以前:
get()方法
首先判断输入的key是否为空,如果为空,从hashmap数组下标为0的位置获取值返回。如果不为空,根据key的值,从 hashmap数组中获取对应的entry对象,判断这个对象是否为空,为空返回null,不为空返回对应的value值,获取 value的方法中 key为空和不为空时的方法里都先判断数组中的元素是否为0 ,如果不为0,才继续查找
put()方法
调用put方法的时候首先判断hashmap 数组是否为空数组
如果为空,进行初始化,判断 key的值是否是null,如果是null,把对应的 value值存进数组中下标为0的位置,计算key的 hash值,并计算出下标,遍历下标对应的链表,匹配 hash值和key 的值,如果存在,则覆盖,返回旧值,如果不存在,新添加一个,返回null
最后判断数组大小,是否扩容
2) java8 get()方法
对输入的key的值计算hash值,
首先判断hashmap中的数组是否为空和数组的长度是否为0,如果为空和为0,则直接放回null
如果不为空和0,计算 key对应的数组下标,判断对应位置上的第一个node是否满足条件,如果满足条件,直接返回
如果不满足条件,判断当前node是否是最后一个,如果是,说明不存在key,则返回null如果不是最后一个,判断是否是红黑树,如果是红黑树,则使用红黑树的方式获取对应的key,如果不是红黑树,遍历链表是否有满足条件的,如果有,直接放回,否则返回null
put()方法
首先计算key的hash值,获取hashmap 中的数组和数组长度,如果数组为空,初始化计算key的下标
数组对应下标的位置是否为空,如果为空,则先添加一个,放在这个下标位置,然后判断数组内元素是否大于阈值,如果大于,则进行扩容
如果数组对应下标不为空,则先获取对应链表的第一个值,判断 hash和key是否相同,如果相同,新value替换旧value,返回旧value
如果第一个值key不相同,判断当前链表是否是红黑树,如果是红黑树,调用红黑树链表put的方法
如果也不是红黑树,遍历链表,判断当前node是否是最后一个,如果是,说明链表中没有新添加的key,则在最后面新添加一个,然后判断是否超过阈值(8-1),如果超过,则转换成红黑树
如果不是最后一个,说明在中间已经存在key了,把新值赋值给旧值,并返回旧值,判断是否需要扩容。
注:哈希碰撞:当两个不同的键对象的hashcode 相同时,它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对()。
(4)HashMap 的resize过程是什么样的(扩容)?
HashMap在put 的时候会先检查当前数组的length,如果插入新的值的时候使得length > 0.75f * size (f 为加载因子,可以在创建 hashMap时指定)的话,会将数组进行扩容为当前容量的2倍。扩容之后必定要将原有hashMap中的值拷贝到新容量的hashMap里面,HashMap 默认的容量为16,加载因子为0.75,也就是说当HashMap中 Entry的个数超过16*0.75=12时,会将容量扩充为16*2=32,然后重新计算元素在数组中的位置,这是一个非常耗时的操作,所以我们在使用HashMap的时候如果能预先知道Map中元素的大小,预设其大小能够提升其性能。
resize 代码:
//HashMap 数组扩容
void resize(int newCapacity) {
        Entry[]oldTable = table;
        int oldCapacity = oldTable.length;
如果当前的数组长度已经达到最大值,则不在进行调整
        if (oldCapacity == MAXIMUM_CAPACITY){
        threshold = Integer.MAX_VALUE;
        return;
}
//根据传入参数的长度定义新的数组
        Entry[]newTable = new Entry[newCapacity];
//按照新的规则,将旧数组中的元素转移到新数组中
        transfer(newTable);
        table = newTable;//更新临界值
        threshold = (int)(newCapacity * loadFactor);
}
旧数组中元素往新数组中迁移
void transfer(Entry[]newTable) {
         //旧数组
        Entry[]src = table;//新数组长度
        int newCapacity = newTable.length; //遍历旧数组
        for (int j = o; j < src.length; j++){
         Entry e = src[];
                  src[i] = null;
                  do {
                           Entry next = e.next;
                           int i = indexFor(e.hash, newCapacity);//放在新数组中的index位置
                           e.next = newTable[li];//实现链表结构,新加入的放在链头,之前的的数据放在链尾
                           newTable[i]= e;
                           e = next;
                      }while (e != null);
                }
         }
}
这是1.7版本中的代码,1.8中引入了红黑树的概念,代码会相对复杂一些。
通俗的讲,如果数组中元素的容量超过阈值0.75,会触发扩容,扩容是先把源数组放进一个临时数组中,获取老数组的长度,通过老数组长度乘⒉获取新数组长度,并创建新数组,把临时数组中的数据通过重新计算下表,存进扩容后的数组中.
(5)HashMap在扩容的时候为什么容量都是原来的2倍,即容量为2^n
HashMap 在计算数组中key的位置时,使用的算法为︰
/* * Returns index for hash code h.*/
static int indexFor(int h, int length){
 // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1);}
即对key 的 hashcode 与当前数组容量-1进行与操作我们假设有一个容量为分别为15和16 的hashMap ,有两个key的 hashcode分别为4和5,进行indexFor操作之后:
H & (length -1) hash & table.length-1 4&(15-1)0100 & 1110=01005 & ( 15-1 ) 0101 & 1110=01004 & (16- 1)0100 & 1111 =0100 5&( 16-1 )  0101 &1111= 0101
我们能够看到在容量为16时进行indexFor操作之后获得相同结果的几率要比容量为15时的几率要小,这样能够减少出现hash冲突的几率,从而提高查询效率。2^n是一个非常神奇的数字。
(6)HashMap你经常用在那个地方?
因为HashMap 的好处非常多,我曾经在电子商务的应用中使用HashMap作为缓存。因为金融领域非常多的运用Java,也出于性能的考虑,我们会经常用到HashMap和ConcurrentHashMap。另外, controller层向前台返回数据可以用map封装数据,还有mybatis中的map可以作为参数或者封装结果集
(7)HashMap和 Hashtable的区别?
1.hashMap去掉了HashTable 的contains方法,但是加上了containsValue ()和containsKey ()方法。
2.hashTable同步的,而 HashMap是非同步的,效率上逼hashTable 要高。
3.hashMap 允许空键值,而hashTable 不允许。一般用hashmap 代替 hashtable
注意:
TreeMap︰非线程安全基于红黑树实现。TreeMap没有调优选项,因为该树总处于平衡状态。
Treemap:适用于按自然顺序或自定义顺序遍历键(key)。
(8)List、Map、Set三个接口,存取元素时,各有什么特点?
List以特定次序来持有元素,可有重复元素;
Set无法拥有重复元素,内部排序(无序);Map 保存key-value 值, value 可多值。
Hashmap 可以存重复键,重复值吗?不能存重复键
(9)HashSet是如何保证元素唯一性的呢?
是通过元素的两个方法, hashCode和 equals来完成。
(10)TreeSet怎么对集合中的元素进行排序?
TreeSet底层数据结构是二叉树。保证元素唯一性的依据: compareTo方法return 0.
1):让元素自身具备比较性,需要元素对象实现 Comparable接口,覆盖compareTo方法。
2):让集合自身具备比较性,需要定义一个实现了Comparator接口的比较器,并覆盖compare方法
(11)map集合的两种取出方式?
//第一种:普遍使用,二次取值。通过Map.keySe()t 遍历key和value
for (String key : map.keySet()) {
            System.out.println("key= "+ key + " and value= " + map.get(key));
}
//第二种:推荐,尤其是容量大时。通过Map.entrySet遍历key和 value
for (Map.Entry<String, String> entry : map.entrySet()){
System.out.printIn("key= " + entry.getKey() + " and value= " + entry.getValue();
}
注意map集合不能直接使用增强for 循环,因为增强 for 循环底层是使用的迭代器,而map没有实现迭代器接口,所以不能用迭代器直接遍历或者增强for循环遍历
(12)Collection和 Collections的区别?
Collection是集合类的上级接口,继承与他的接口主要有Set和List.
Collections是工具类,有很多对集合操作的方法
(13)请问ArrayList、HashSet、HashMap是线程安全的吗?如果不是我想要线程安全的集合怎么办?
我们都看过一些集合的源码(如果没有那就看看吧),每个方法都没有加锁,显然都是线程不安全的。话又说过来,如果他们安全了也就没第二问了。在集合中Vector和 HashTable倒是线程安全的。你打开源码会发现其实就是把各自核心方法添加上了synchronized 关键字。
Collections工具类提供了相关的 API,可以让上面那3个不安全的集合变为安全的。
1.//    collections.synchronizedcollection (c)
2.//    collections.synchronizedList (list)
3.//    collections.synchronizedMap (m)
4.//    collections.synchronizedset (s)

上面几个函数都有对应的返回值类型,传入什么类型返回什么类型。打开源码其实实现原理非常简单,就是将集合的核心方法添加上了synchronized关键字。
(14)ConcurrentHashMap 的工作原理及代码实现
HashTable里使用的是synchronized关键字,这其实是对对象加锁,锁住的都是对象整体,当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。ConcurrentHashMap算是对上述问题的优化,其构造函数如下,默认传入的是16,0.75,16。
ConcurrentHashMap引入了分割(Segment),上 面代码中的最后一行其实就可以理解为把一个大的Map拆分成N个小的HashTable,在put方法中,会根据hash(paramK.hashCode()来决定具体存放进哪个Segment,如果查看Segment的put操作,我们会发现内部使用的同步机制是基于lock操作的,这样就可以对Map的一部分(Segment) 进行上锁,这样影响的只是将要放入同一个Segment的元素的put操作,保证同步的时候,锁住的不是整个Map (HashTable 就是这么做的),相对于HashTable提高了多线程环境下的性能,因此HashTable已经被淘汰了。
【奔跑吧!JAVA】有奖征文火热进行中:https://bbs.huaweicloud.com/blogs/265241
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区),文章链接,文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:cloudbbs@huaweicloud.com进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。