写点什么

21 道 Java 基础面试题及答案 (1),linux 系统管理技术手册

用户头像
极客good
关注
发布于: 刚刚

3、第三点不同是,**只有 HashMap 可以让你将空值作为一个表的条目的 key 或 value。**HashMap 中只有一条记录可以是一个空的 key,但任意数量的条目可以是空的 value。这就是说,如果在表中没有发现搜索键,或者如果发现了搜索键,但它是一个空的值,那么 get()将返回 null。如果有必要,用 containKey()方法来区别这两种情况。


一些资料建议,当需要同步时,用 Hashtable,反之用 HashMap。但是,因为在需要时,HashMap 可以被同步,HashMap 的功能比 Hashtable 的功能更多,而且它不是基于一个陈旧的类的,所以有人认为,在各种情况下,HashMap 都优先于 Hashtable。

11. HashMap 和 ConcurrentHashMap 的区别,HashMap 的底层源码。

(1)HashMap 的源码:


1、HashMap 中的 hash 函数实现:


详见:https://www.zhihu.com/question/20733617


2、HashMap 源码解读


详见:http://blog.csdn.net/ll530304349/article/details/53056346


(2)ConcurrentHashMap 的源码:


1、JDK1.7 版本的实现


ConcurrentHashMap 的锁分段技术:假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是 ConcurrentHashMap 所使用的锁分段技术。首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。


ConcurrentHashMap 不允许 Key 或者 Value 的值为 NULL



第一:Segment 类


Put


将一个 HashEntry 放入到该 Segment 中,使用自旋机制,减少了加锁的可能性。


final V put(K key, int hash, V value, boolean onlyIfAbsent) {


HashEntry<K,V> node = tryLock() ? null :


scanAndLockForPut(key, hash, value); //如果加锁失败,则调用该方法


V oldValue;


try {


HashEntry<K,V>[] tab = table;


int index = (tab.length - 1) & hash; //同 hashMap 相同的哈希定位方式


HashEntry<K,V> first = entryAt(tab, index);


for (HashEntry<K,V> e = first;;) {


if (e != null) {


//若不为 null,则持续查找,知道找到 key 和 hash 值相同的节点,将其 value 更新


K k;


if ((k = e.key) == key ||


(e.hash == hash && key.equals(k))) {


oldValue = e.value;


if (!onlyIfAbsent) {


e.value = value;


++modCount;


}


break;


}


e = e.next;


}


else { //若头结点为 null


if (node != null) //在遍历 key 对应节点链时没有找到相应的节点


node.setNext(first);


//当前修改并不需要让其他线程知道,在锁退出时修改自然会


//更新到内存中,可提升性能


else


node = new HashEntry<K,V>(hash, key, value, first);


int c = count + 1;


if (c > threshold && tab.length < MAXIMUM_CAPACITY)


rehash(node); //如果超过阈值,则进行 rehash 操作


else


setEntryAt(tab, index, node);


++modCount;


count = c;


oldValue = null;


break;


}


}


} finally {


unlock();


}


return oldValue;


}


scanAndLockForPut


该操作持续查找 key 对应的节点链中是否已存在该节点,如果没有找到已存在的节点,则预创建一个新节点,并且尝试 n 次,直到尝试次数超出限制,才真正进入等待状态,即所谓的?自旋等待


private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {


//根据 hash 值找到 segment 中的 HashEntry 节点


HashEntry<K,V> first = entryForHash(this, hash); //首先获取头结点


HashEntry<K,V> e = first;


HashEntry<K,V> node = null;


int retries = -1; // negative while locating node


while (!tryLock()) { //持续遍历该哈希链


HashEntry<K,V> f; // to recheck first below


if (retries < 0) {


if (e == null) {


if (node == null) //若不存在要插入的节点,则创建一个新的节点


node = new HashEntry<K,V>(hash, key, value, null);


retries = 0;


}


else if (key.equals(e.key))


retries = 0;


else


e = e.next;


}


else if (++retries > MAX_SCAN_RETRIES) {


//尝试次数超出限制,则进行自旋等待


lock();


break;


}


/*当在自旋过程中发现节点链的链头发生了变化,则更新节点链的链头,


并重置 retries 值为-1,重新为尝试获取锁而自旋遍历*/


else if ((retries & 1) == 0 &&


(f = entryForHash(this, hash)) != first) {


e = first = f; // re-traverse if entry changed


retries = -1;


}


}


return node;


}


remove


用于移除某个节点,返回移除的节点值。


final V remove(Object key, int hash, Object value) {


if (!tryLock())


scanAndLock(key, hash);


V oldValue = null;


try {


HashEntry<K,V>[] tab = table;


int index = (tab.length - 1) & hash;


//根据这种哈希定位方式来定位对应的 HashEntry


HashEntry<K,V> e = entryAt(tab, index);


HashEntry<K,V> pred = null;


while (e != null) {


K k;


HashEntry<K,V> next = e.next;


if ((k = e.key) == key ||


(e.hash == hash && key.equals(k))) {


V v = e.value;


if (value == null || value == v || value.equals(v)) {


if (pred == null)


setEntryAt(tab, index, next);


else


pred.setNext(next);


++modCount;


--count;


oldValue = v;


}


break;


}


pred = e;


e = next;


}


} finally {


unlock();


}


return oldValue;


}


Clear


要首先对整个 segment 加锁,然后将每一个 HashEntry 都设置为 null。


final void clear() {


lock();


try {


HashEntry<K,V>[] tab = table;


for (int i = 0; i < tab.length ; i++)


setEntryAt(tab, i, null);


++modCount;


count = 0;


} finally {


unlock();


}


}


Put


public V put(K key, V value) {


Segment<K,V> s;


if (value == null)


throw new NullPointerException();


int hash = hash(key); //求出 key 的 hash 值


int j = (hash >>> segmentShift) & segmentMask;


//求出 key 在 segments 数组中的哪一个 segment 中


if ((s = (Segment<K,V>)UNSAFE.getObject


(segments, (j << SSHIFT) + SBASE)) == null)


s = ensureSegment(j); //使用 unsafe 操作取出该 segment


return s.put(key, hash, value, false); //向 segment 中 put 元素


}


Get


public V get(Object key) {


Segment<K,V> s;


HashEntry<K,V>[] tab;


int h = hash(key); //找出对应的 segment 的位置


long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;


if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&


(tab = s.table) != null) { //使用 Unsafe 获取对应的 Segmen


for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile


(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);


e != null; e = e.next) { //找出对应的 HashEntry,从头开始遍历


K k;


if ((k = e.key) == key || (e.hash == h && key.equals(k)))


return e.value;


}


}


return null;


}


Size


求出所有的 HashEntry 的数目,先尝试的遍历查找、计算 2 遍,如果两遍遍历过程中整个 Map 没有发生修改(即两次所有 Segment 实例中 modCount 值的和一致),则可以认为整个查找、计算过程中 Map 没有发生改变。否则,需要对所有 segment 实例进行加锁、计算、解锁,然后返回。


public int size() {


final Segment<K,V>[] segments = this.segments;


int size;


boolean overflow; // true if size overflows 32 bits


long sum; // sum of modCounts


long last = 0L; // previous sum


int retries = -1; // first iteration isn't retry


try {


for (;;) {


if (retries++ == RETRIES_BEFORE_LOCK) {


for (int j = 0; j < segments.length; ++j)


ensureSegment(j).lock(); // force creation


}


sum = 0L;


size = 0;


overflow = false;


for (int j = 0; j < segments.length; ++j) {


Segment<K,V> seg = segmentAt(segments, j);


if (seg != null) {


sum += seg.modCount;


int c = seg.count;


if (c < 0 || (size += c) < 0)


overflow = true;


}


}


if (sum == last)


break;


last = sum;


}


} finally {


if (retries > RETRIES_BEFORE_LOCK) {


for (int j = 0; j < segments.length; ++j)


segmentAt(segments, j).unlock();


}


}


return overflow ? Integer.MAX_VALUE : size;


}


(2)JDK1.8 实现


在 JDK1.8 中对 ConcurrentHashmap 做了两个改进:


  • 取消 segments 字段,直接采用 transient volatile HashEntry<K,V>[] table 保存数据,采用 table 数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率

  • 将原先?table 数组+单向链表?的数据结构,变更为?table 数组+单向链表+红黑树?的结构。对于 hash 表来说,最核心的能力在于将 key hash 之后能均匀的分布在数组中。如果 hash 之后散列的很均匀,那么 table 数组中的每个队列长度主要为 0 或者 1。但实际情况并非总是如此理想,虽然 ConcurrentHashMap 类默认的加载因子为 0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,**那么查询某个节点的时间复杂度为 O(n);因此,对于个数超过 8(默认值)的列表,jdk1.8 中采用了红黑树的结构,那么查询的时间复杂度可以降低到 O(logN),**可以改进性能。

12. TreeMap、HashMap、LinkedHashMap 的区别。

Map 主要用于存储健值对,根据键得到值,因此不允许键重复(重复了覆盖了),但允许值


重复。Hashmap 是一个最常用的 Map,它根据键的 HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。HashMap 最多只允许一条记录的键为 Null;允许多条记录的值为 Null;HashMap 不支持线程的同步,即任一时刻可以有多个线程同时写 HashMap;可能会导致数据的不一致。如果需要同步,可以用 Collections 的 synchronizedMap 方法使 HashMap 具有同步的能力,或者使用 ConcurrentHashMap。


  • HashMap:线程不同步。根据 key 的 hashcode 进行存储,内部使用静态内部类 Node 的数组进行存储,默认初始大小为 16,每次扩大一倍。当发生 Hash 冲突时,采用拉链法(链表)。可以接受为 null 的键值(key)和值(value)。JDK 1.8 中:当单个桶中元素个数大于等于 8 时,链表实现改为红黑树实现;当元素个数小于 6 时,变回链表实现。由此来防止 hashCode 攻击。

  • LinkedHashMap:保存了记录的插入顺序,在用 Iterator 遍历 LinkedHashMap 时,先得到的记录肯定是先插入的. 也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比 HashMap 慢,不过有种情况例外,当 HashMap 容量很大,实际数据较少时,遍历起来可能会比 LinkedHashMap 慢,因为 LinkedHashMap 的遍历速度只和实际数据有关,和容量无关,而 HashMap 的遍历速度和他的容量有关。LinkedHashMap 采用的 hash 算法和 HashMap 相同,但是它重新定义了数组中保存的元素 Entry,该 Entry 除了保存当前对象的引用外,还保存了其上一个元素 before 和下一个元素 after 的引用,从而在哈希表的基础上又构成了双向链接列表。

  • TreeMap:线程不同步,基于?红黑树?(Red-Black tree)的 NavigableMap 实现,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。

  • HashTable:线程安全,HashMap 的迭代器(Iterator)是 fail-fast 迭代器。HashTable 不能存储 NULL 的 key 和 value。

13. Collection 包结构,与 Collections 的区别。

(1)Collection 是单列集合


List?元素是有序的、可重复


有序的 collection,可以对列表中每个元素的插入位置进行精确地控制。


可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。


可存放重复元素,元素存取是有序的。


List 接口中常用类


l Vector:线程安全,但速度慢,已被 ArrayList 替代。


底层数据结构是数组结构


l ArrayList:线程不安全,查询速度快。


底层数据结构是数组结构


l LinkedList:线程不安全。增删速度快。


底层数据结构是列表结构


Set(集) 元素无序的、不可重复。


取出元素的方法只有迭代器。不可以存放重复元素,元素存取是无序的。


Set 接口中常用的类


l HashSet:线程不安全,存取速度快。


它是如何保证元素唯一性的呢?依赖的是元素的 hashCode 方法和 euqals 方法。


l TreeSet:线程不安全,可以对 Set 集合中的元素进行排序。


它的排序是如何进行的呢?通过 compareTo 或者 compare 方法中的来保证元素的唯一性。元素是以二叉树的形式存放的。


(2)Map?是一个双列集合


|--Hashtable:线程安全,速度快。底层是哈希表数据结构。是同步的。


不允许 null 作为键,null 作为值。


|--Properties:用于配置文件的定义和操作,使用频率非常高,同时键和值都是字符串。


是集合中可以和 IO 技术相结合的对象。(到了 IO 在学习它的特有和 io 相关的功能。)


|--HashMap:线程不安全,速度慢。底层也是哈希表数据结构。是不同步的。


允许 null 作为键,null 作为值。替代了 Hashtable.


|--LinkedHashMap: 可以保证 HashMap 集合有序。存入的顺序和取出的顺序一致。


|--TreeMap:可以用来对 Map 集合中的进行排序.


(3)Collection 和 Collections 的区别


Collection 是集合类的上级接口,子接口主要有 Set 和 List。


Collections 是针对集合类的一个帮助类,提供了操作集合的工具方法:一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。

14. try catch finally,try 里有 return,finally 还执行么?

答案:执行,并且 finally 的执行早于 try 里面的 return


结论:


1、不管有木有出现异常,finally 块中代码都会执行;


2、当 try 和 catch 中有 return 时,finally 仍然会执行;


3、finally 是在 return 后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管 finally 中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在 finally 执行前确定的;


4、finally 中最好不要包含 return,否则程序会提前退出,返回值不是 try 或 catch 中保存的返回值。


举例:


情况 1:try{} catch(){}finally{} return;


显然程序按顺序执行。


情况 2:try{ return; }catch(){} finally{} return;


程序执行 try 块中 return 之前(包括 return 语句中的表达式运算)代码;


再执行 finally 块,最后执行 try 中 return;


finally 块之后的语句 return,因为程序在 try 中已经 return 所以不再执行。


情况 3:try{ } catch(){return;} finally{} return;


程序先执行 try,如果遇到异常执行 catch 块,


有异常:则执行 catch 中 return 之前(包括 return 语句中的表达式运算)代码,再执行 finally 语句中全部代码,


最后执行 catch 块中 return. finally 之后也就是 4 处的代码不再执行。


无异常:执行完 try 再 finally 再 return.


情况 4:try{ return; }catch(){} finally{return;}


程序执行 try 块中 return 之前(包括 return 语句中的表达式运算)代码;


再执行 finally 块,因为 finally 块中有 return 所以提前退出。


情况 5:try{} catch(){return;}finally{return;}


程序执行 catch 块中 return 之前(包括 return 语句中的表达式运算)代码;


再执行 finally 块,因为 finally 块中有 return 所以提前退出。


情况 6:try{ return;}catch(){return;} finally{return;}


程序执行 try 块中 return 之前(包括 return 语句中的表达式运算)代码;


有异常:执行 catch 块中 return 之前(包括 return 语句中的表达式运算)代码;


则再执行 finally 块,因为 finally 块中有 return 所以提前退出。


无异常:则再执行 finally 块,因为 finally 块中有 return 所以提前退出。


**最终结论:**任何执行 try 或者 catch 中的 return 语句之前,都会先执行 finally 语句,如果 finally 存在的话。


如果 finally 中有 return 语句,那么程序就 return 了,所以 finally 中的 return 是一定会被 return 的,编译器把 finally 中的 return 实现为一个 warning。

15. Excption 与 Error 包结构。OOM 你遇到过哪些情况,SOF 你遇到过哪些情况。

(1)Java 异常


Java 中有 Error 和 Exception,它们都是继承自 Throwable 类。




二者的不同之处


Exception:


  • 可以是可被控制(checked) 或不可控制的(unchecked)。

  • 表示一个由程序员导致的错误。

  • 应该在应用程序级被处理。


Error:


  • 总是不可控制的(unchecked)。

  • 经常用来用于表示系统错误或低层资源的错误。

  • 如何可能的话,应该在系统级被捕捉。


异常的分类


  • Checked exception: 这类异常都是 Exception 的子类。异常的向上抛出机制进行处理,假如子类可能产生 A 异常,那么在父类中也必须 throws A 异常。可能导致的问题:代码效率低,耦合度过高。

  • Unchecked exception:?这类异常都是 RuntimeException 的子类,虽然 RuntimeException 同样也是 Exception 的子类,但是它们是非凡的,它们不能通过 client code 来试图解决,所以称为 Unchecked exception 。

16. Java 面向对象的三个特征与含义。

答案:继承、多态、封装

17. Override 和 Overload 的含义与区别。

答案:见 JVM 静态分派和动态分派

18. Interface 与 abstract 类的区别。

答案:


1.abstract class 在 Java 中表示的是一种继承关系,一个类只能使用一次继承关系。但是,一个类却可以实现多个 interface。


2.在 abstract class 中可以有自己的数据成员,也可以有非 abstarct 的方法,而在 interface 中,只能够有静态的不能被修改的数据成员(也就是必须是 static final 的,不过在 interface 中一般不定义数据成员),所有的方法都是 public abstract 的。


3.抽象类中的变量默认是 friendly 型,其值可以在子类中重新定义,也可以重新赋值。接口中定义的变量默认是 public static final 型,且必须给其赋初值,所以实现类中不能重新定义,也不能改变其值。


4.abstract class 和 interface 所反映出的设计理念不同。其实 abstract class 表示的是"is-


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


a"关系,interface 表示的是"like-a"关系,门和报警的关系。?


5.实现抽象类和接口的类必须实现其中的所有方法。抽象类中可以有非抽象方法。接口中则不能有实现方法。


abstract class 和 interface 是 Java 语言中的两种定义抽象类的方式,它们之间有很大的相似性。但是对于它们的选择却又往往反映出对于问题领域中的概念本质的理解、对于设计意图的反映是否正确、合理,因为它们表现了概念间的不同的关系。

19. Static class 与 non static class 的区别。

答案:


比对如下:


静态对象 非静态对象?


拥有属性: 是类共同拥有的 是类各对象独立拥有的


内存分配: 内存空间上是固定的 空间在各个附属类里面分配?


分配顺序: 先分配静态对象的空间 继而再对非静态对象分配空间,也就是初始化顺序是先静态再非静态.


Java 静态对象到底有什么好处?


A,静态对象的数据在全局是唯一的,一改都改。如果你想要处理的东西是整个程序中唯一的,弄成静态是个好方法。 非静态的东西你修改以后只是修改了他自己的数据,但是不会影响其他同类对象的数据。?


B,引用方便。直接用 类名.静态方法名 或者 类名.静态变量名就可引用并且直接可以修改其属性值,不用 get 和 set 方法。


C,保持数据的唯一性。此数据全局都是唯一的,修改他的任何一处地方,在程序所有使用到的地方都将会体现到这些数据的修改。有效减少多余的浪费。


D,static final 用来修饰成员变量和成员方法,可简单理解为“全局常量”。对于变量,表示一旦给值就不可修改;对于方法,表示不可覆盖。

20. 锁的等级:方法锁、对象锁、类锁。

(1)基础


Java 中的每一个对象都可以作为锁。


  • 对于同步方法,锁是当前实例对象

  • 对于静态同步方法,锁是当前对象的 Class 对象

  • 对于同步方法块,锁是 Synchonized 括号里配置的对象


当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。那么锁存在哪里呢?锁里面会存储什么信息呢?


(2)同步的原理


JVM 规范规定 JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用 monitorenter 和 monitorexit 指令实现,而方法同步是使用另外一种方式实现的,细节在 JVM 规范里并没有详细说明,但是方法的同步同样可以使用这两个指令来实现。

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
21道Java基础面试题及答案(1),linux系统管理技术手册