1, Object有哪些公用方法?
答: object 的公有方法主要有 equals , hasCode , toString 等这类基本方法, 此外还有一些关于并发方面的方法, 如 wait, notify, notifyAll , 最值得我们思考的就是这几个方法, 也许我们会疑惑, 为什么关于并发方面的方法会被设计成基类的方法, 而不是 Thread 这类专门用来并发的类的方法呢? 这里我们要从这几个方法的意义去思考了, 这几个方法是用来进行线程同步的, 而之所以要进行线程同步, 是因为存储在堆区共享资源被多个线程所访问而可能导致的线程不安全的因素, 而任何对象都可以成为位于堆区的共享资源, 所以这些方法被设计成 object 的方法自然是合理的.
2, Java集合框架中有哪些类?都有什么特点?
答: 主要有两大类, Collection 和 Map, 这两大类分别有如下常用的小类:
collection:
- List 接口, 如名字一样, 这是一个 列表 集合, 按照添加元素的顺序保存.
- Set 接口, 其不能保存重复值, 具体实现不重复的细节是使用 hasCode 和 equals 方法, 这个接口的功能实现绝大部分都由 map 的实现子类来实现.
- Queue 接口, 如名字一样, 这是一个队列接口, LinkedList 实现了这个接口, 可以用来实现队列的行为
map:
map 没有中间接口, 其和 Collection 一样, 都有很多 abstract 中间类, 但是这些类我们通常都可以忽列掉.
- HashMap , 存储和查询速度最快, 其采用了 散列 机制, 初始容量为 16, 默认负载因子为 0.75, 它应该是我们在使用 Map 的默认选择.
- TreeMap, 有序的Map, 按照比较结果的自然排序方式进行排序, 其内部元素要实现 comparable 接口.
- LinkedHashMap, 拥有和 HasMap 媲美的查询速度, 其可以按照添加元素的顺序保存元素, 其实现了 LRU 算法, 通过构造方法可以使用这个特性.
3, 集合、数组、泛型的关系,并比较
答: 集合的底层实现离不开数组, 集合和数组都是一种存储数据的容器, 但是相对于数组, 集合更加灵活, 其可以自动扩容, 扩容方式一般是通过对原数组的复制, 虽然我们自己也可以实现类似的行为, 但肯定没有官方的优秀. 泛型 是 java 中一个很重要的特性, 在 JDK 1.5 引入的, 其作用是为了能够在编译期就能够发现某些类型错误, 从而提高程序的安全性. 集合中一般都需要指定泛型来确定元素的类型, 但是不能创建泛型数组, 之所以不能创建是因为这违背了 泛型 引入的最初目的, 即在编译期就能发现某些类型错误, java 为什么不能泛型数组
4, ArrayList和LinkList的区别?
答: ArrayList 底层实现的数据结构是数组, 初始容量为 10, LinkList 底层实现为双向链表.
5, HashSet和TreeSet的区别?
答: HashSet 由 HashMap 实现, 其具体的行为特征和 HashMap 类似, TreeSet 由 TreeMap 实现, 对元素按照自然排序, 因为其内部支持排序, TreeSet 迭代遍历的速度比 HashSet 稍快.
6, HashMap和Hashtable的区别?
答: Hashtable 是 HashMap 的线程安全实现. 大量的方法使用了 synchronized, 所以导致其效率较为低下, 我们不应该在现在的编码中使用它, 它是JDK 1.0 中较老的容器类, 我们应该使用 ConcurrentHashMap 来实现并发访问, 其采用了分段锁的技术, 比 HashTable 效率要高.
7, ArrayList和Vector的区别?
答: Vector 是 ArrayList 的线程安全版, 其和 HashTable 一样, 大量的方法都使用了 sync 关键字, 其效率较为低下, 也为 JDK 1.0 中较老的容器类, 我们应该使用 CopyOnWriteArrayList 来替换他, 其 采用了读写分离的思想, 并发效率得到了改善.
8, 如何解决Hash冲突?
答: 常见的解决 Hash 冲突的方法有:
- 线性探测法. --- 最简单的探测方法, 从发生冲突的地方开始, 进行线性探测, 逐个往后探测.
- 平方探测法. --- 从发生冲突的地方, 依次加上自然数的平方, 直到找到空闲区间.
- 双散列函数探测法. --- 使用第二个散列函数对关键字再散列, 然后以这个值为步长进行探测.
- 链地址法. --- 采用局部链表来维持冲突, JDK 1.7 中的 HashMap 就是如此, 而 1.8 则采用了红黑树.
- 再 Hash 法. --- 发生冲突, 使用第二个散列函数再散列.
- 建立公共溢出区间. --- 建立一个冲突表, 单独用来存放发生冲突的元素.
9. HashMap在put、get元素的过程?体现了什么数据结构?
答: get 过程:
- 根据传入的 key, 获取其 hashCode,
- 然后根据 hashCode 得到地址的索引
- 判断该地址索引上的筒位是否为 null,如果为 null,直接返回, 如果不为 null, 比较 key 的 hashCode 和是否相等.
- 然后比较 key 是否相等, 相等则返回, 如果不相等则获取该筒位的 next 筒位.(HashMap 是采用局部链表来维持冲突的)
put 过程:
- 获取 hashCode, 然后得到地址索引.
- 判断该地址上的筒位是否为 null, 如果为 null,直接 new 一个新的 node, 并保存 key 和 value, 然后判断是否需要扩容, 最后返回 null.
- 如果不为 null, 可能是产生了冲突, 也可能是需要更新这个 key 原来对应的 value.
- 判断 key 是否相等, 如果相等, 更新 value 并返回.
- 如果不相等,一直遍历到这个局部冲突链表的尾部, 然后插入这个节点.
这两个过程都表明了, HashMap 中, 采用了链表来维持 hash 冲突, 对于 node 的存储, 则是采用的 数组, 但是数组中的每一个元素都是一条链表的第一个节点.
10, 如何保证HashMap线程安全?什么原理?
答:
- 使用 Collections.synchronizedMap() 其实现原理还是大量的使用了 sync 关键字, 在绝大部分方法中都采用了一个 object 对象来作为锁.
- 使用 Hashtable, 其效率低下, 因为大量的方法使用了 sync 关键字.
- 使用 ConcurrentHashMap, 在 JDK 7 中其采用了分段锁的技术, 即不对整个 hash 表加锁, 把整个 hash 表分为多个 Segment 片段, 每个 Segment 都是继承自 ReentrantLock , 从而很容易实现分别加锁, 提高并发效率, 在 JDK 8 中则采用了基于机器级别上的CAS 原子性操作, 底层的数据结构采用了数组和红黑树.
HashMap 在并发环境中, 在 HashMap 扩容的 reHash 中, 会导致环形链表, 从而在 get 的元素时进入死循环. 具体博客: HashMap 死循环
11, HashMap是有序的吗?如何实现有序?
答: HashMap 默认是无需的, 其具体位置和 key 的 hasCode 和 map 中数组长度有关, 如果要实现有序, 可以使用 linkedHashMap, 其实现有序的原理是 LinkedHashMap 对 Node 进行了重写, 增加了 before 和 next 这两个成员变量, 其在插入元素的时候, 通过这两个引用构成了一个双向链表, 从而可以实现在遍历元素的有序状态.
12, hashcode()的作用,与equal()有什么区别?
答: 一些常见的基于 hash 原理的散列算法和数据结构都需要使用到 hashCode, hashCode 主要作用是用于在为这些基于 hash 算法的数据结构提供支持, 其与 equals 的区别是, 返回值不同, 具体的含义也不同, 一个是比较两个对象, 一个是获取这个对象的一个特有的属性值.
13, HashMap是如何扩容的?如何避免扩容?
答: HashMap 在新添加一个元素后, 如果尺寸大于负载量, 则会进行扩容, 其扩容的大致过程为重新建立一个数组长度是原数组两倍的数组, 然后遍历原数组中的每一个元素, 分别进行再 hash 然后放入到新的数组中, 因此, HashMap 的扩容过程还是有点开销的, 如果我们在一开始创建 HashMap 的时候可以预估元素的数量, 便可以通过构造方法传入, 从而减少扩容的可能性和次数, 另外, 还可以通过提高负载因子来减少扩容, 但是此方法会到这查询效率变低, 不太值得.