乐趣区

3年Java程序员面试集锦Java基础

Java 基础

1.HashMap 的源码,实现原理,JDK8 中对 HashMap 做了怎样的优化。

Hashtable、HashMap、TreeMap 心得

三者均实现了 Map 接口,存储的内容是基于 key-value 的键值对映射,一个映射不能有重复的键,一个键最多只能映射一个值。

(1)元素特性

HashTable 中的 key、value 都不能为 null;HashMap 中的 key、value 可以为 null,很显然只能有一个 key 为 null 的键值对,但是允许有多个值为 null 的键值对;TreeMap 中当未实现 Comparator 接口时,key 不可以为 null;当实现 Comparator 接口时,若未对 null 情况进行判断,则 key 不可以为 null,反之亦然。

(2)顺序特性

HashTable、HashMap 具有无序特性。TreeMap 是利用红黑树来实现的(树中的每个节点的值,都会大于或等于它的左子树种的所有节点的值,并且小于或等于它的右子树中的所有节点的值),实现了 SortMap 接口,能够对保存的记录根据键进行排序。所以一般需要排序的情况下是选择 TreeMap 来进行,默认为升序排序方式(深度优先搜索),可自定义实现 Comparator 接口实现排序方式。

(3)初始化与增长方式

初始化时:HashTable 在不指定容量的情况下的默认容量为 11,且不要求底层数组的容量一定要为 2 的整数次幂;HashMap 默认容量为 16,且要求容量一定为 2 的整数次幂。

扩容时:Hashtable 将容量变为原来的 2 倍加 1;HashMap 扩容将容量变为原来的 2 倍。

(4)线程安全性

HashTable 其方法函数都是同步的(采用 synchronized 修饰),不会出现两个线程同时对数据进行操作的情况,因此保证了线程安全性。也正因为如此,在多线程运行环境下效率表现非常低下。因为当一个线程访问 HashTable 的同步方法时,其他线程也访问同步方法就会进入阻塞状态。比如当一个线程在添加数据时候,另外一个线程即使执行获取其他数据的操作也必须被阻塞,大大降低了程序的运行效率,在新版本中已被废弃,不推荐使用。

HashMap 不支持线程的同步,即任一时刻可以有多个线程同时写 HashMap; 可能会导致数据的不一致。如果需要同步(1)可以用 Collections 的 synchronizedMap 方法;(2)使用 ConcurrentHashMap 类,相较于 HashTable 锁住的是对象整体,ConcurrentHashMap 基于 lock 实现锁分段技术。首先将 Map 存放的数据分成一段一段的存储方式,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。ConcurrentHashMap 不仅保证了多线程运行环境下的数据访问安全性,而且性能上有长足的提升。

(5) 一段话 HashMap

HashMap 基于哈希思想,实现对数据的读写。当我们将键值对传递给 put() 方法时,它调用键对象的 hashCode() 方法来计算 hashcode,让后找到 bucket 位置来储存值对象。当获取对象时,通过键对象的 equals() 方法找到正确的键值对,然后返回值对象。HashMap 使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。HashMap 在每个链表节点中储存键值对对象。当两个不同的键对象的 hashcode 相同时,它们会储存在同一个 bucket 位置的链表中,可通过键对象的 equals() 方法用来找到键值对。如果链表大小超过阈值(TREEIFY_THRESHOLD, 8),链表就会被改造为树形结构。

2.HaspMap 扩容是怎样扩容的,为什么都是 2 的 N 次幂的大小。

2、详解

  hashMap 的存取就是 O(1),也就是直接根据 hashcode 就可以找到它,每个 bucket 只存储一个节点,链表指向都是 null, 这样就比较开心了,不要出现一个链表很长的情况。

  所以我们希望它能分布的均匀一点,如果让我们设计的话,我们肯定是直接对长度取模 —–hashcode % length, 但 HashMap 的设计者却不是这样写的,它写成了 2 进制运算,如下:

static final int hash(Object key) {

int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

index = (n – 1) & hash

  为什么设计成 (n – 1) & hash 这样呢?在 n 为 2 次幂的情况下时,(n – 1) & hash ≈ hash % n , 因为 2 进制的运算速度远远高于取模,所以就使用了这种方式,所以要求为 2 的幂。

  我们可以看到它求 hash 的过程,将 32 位的 hashCode 值向左移动 16 位,高位补 0,也就是只要了高 16 位,这是为什么呢?因为 hashcode 的计算方法导致哈希值的差异主要在高位,而 (n – 1) & hash 是忽略了容量以上的高位的,所以 使用 h >>>16 就是为了避免类似情况的哈希冲突

3.HashMap,HashTable,ConcurrentHashMap 的区别。

1.7

put 加锁

通过分段加锁 segment,一个 hashmap 里有若干个 segment,每个 segment 里有若干个桶,桶里存放 K - V 形式的链表,put 数据时通过 key 哈希得到该元素要添加到的 segment,然后对 segment 进行加锁,然后在哈希,计算得到给元素要添加到的桶,然后遍历桶中的链表,替换或新增节点到桶中

size

分段计算两次,两次结果相同则返回,否则对所以段加锁重新计算

1.8

put CAS 加锁

1.8 中不依赖与 segment 加锁,segment 数量与桶数量一致;

首先判断容器是否为空,为空则进行初始化利用 volatile 的 sizeCtl 作为互斥手段,如果发现竞争性的初始化,就暂停在那里,等待条件恢复,否则利用 CAS 设置排他标志(U.compareAndSwapInt(this, SIZECTL, sc, -1)); 否则重试

对 key hash 计算得到该 key 存放的桶位置,判断该桶是否为空,为空则利用 CAS 设置新节点

否则使用 synchronize 加锁,遍历桶中数据,替换或新增加点到桶中

最后判断是否需要转为红黑树,转换之前判断是否需要扩容

size

利用 LongAdd 累加计算

4. 极高并发下 HashTable 和 ConcurrentHashMap 哪个性能更好,为什么,如何实现的。

5.HashMap 在高并发下如果没有处理线程安全会有怎样的安全隐患,具体表现是什么。

多线程 put 时可能会导致 get 无限循环,具体表现为 CPU 使用率 100%;

原因:在向 HashMap put 元素时,会检查 HashMap 的容量是否足够,如果不足,则会新建一个比原来容量大两倍的 Hash 表,然后把数组从老的 Hash 表中迁移到新的 Hash 表中,迁移的过程就是一个 rehash() 的过程,多个线程同时操作就有可能会形成循环链表,所以在使用 get() 时,就会出现 Infinite Loop 的情况

当多个线程同时执行 addEntry(hash,key ,value,i) 时,如果产生哈希碰撞,导致两个线程得到同样的 bucketIndex 去存储,就可能会发生元素覆盖丢失的情况

6.java 中四种修饰符的限制范围。

public:Java 语言中访问限制最宽的修饰符,一般称之为“公共的”。被其修饰的类、属性以及方法不仅可以跨类访问,而且允许跨包(package)访问。

private: Java 语言中对访问权限限制的最窄的修饰符,一般称之为“私有的”。被其修饰的类、属性以及方法只能被该类的对象访问,其子类不能访问,更不能允许跨包访问。

protect: 介于 public 和 private 之间的一种访问修饰符,一般称之为“保护形”。被其修饰的类、属性以及方法只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问。

default:即不加任何访问修饰符,通常称为“默认访问模式“。该模式下,只允许在同一个包中进行访问。

7.Object 类中的方法。

registerNatives() // 私有方法

getClass() // 返回此 Object 的运行类。

hashCode() // 用于获取对象的哈希值。

equals(Object obj) // 用于确认两个对象是否“相同”。

clone() // 创建并返回此对象的一个副本。

toString() // 返回该对象的字符串表示。

notify() // 唤醒在此对象监视器上等待的单个线程。

notifyAll() // 唤醒在此对象监视器上等待的所有线程。

wait(long timeout) // 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或 者超过指定的时间量前,导致当前线程等待。

wait(long timeout, int nanos) // 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。

wait() // 用于让当前线程失去操作权限,当前线程进入等待序列

finalize() // 当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。

8. 接口和抽象类的区别,注意 JDK8 的接口可以有实现。

接口和抽象类是 java 面向对象设计的两个基础机制。

    • 接口是对行为的抽象,他是抽象方法的集合,利用接口可以达到 API 定义和实现的分离。接口不能实例化 / 不能包含非常量成员,任何的 feild 都是默认 public static final 的意义;没有非静态方法的实现。
    • 抽象类也是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用,通常抽取相关的 java 类的公用方法实现或共同的成员变量,然后通过继承的方式达到代码复用的目的。除了不能实例化,同普通的 java 类没啥区别,可以有 0 到多个的抽象方法。
    • 实现 interface 使用 implements
    • 继承 abstract 类使用 extends
    • 接口类增加方法的情况下,其实现类都需要做相应的修改。当然 java8 引入的 default 方法除外。
    • 抽象类增加方法,其子类只会享受能力扩展,不用担心编译问题。
    • 扩展

      • java 不支持多继承的问题

        • 规范了代码实现的同时,也产生了一些局限性,影响着程序的设计结构。
        • 比如有一些场景需要抽象出与具体实现无关的通用逻辑,或者单纯调用关系的逻辑,使用传统的抽象类会陷入单继承的窘境。成熟的做法是:实现工具类呗。
      • 接口类 Marker Interface

        • 没有任何抽象方法的接口。目的就是为了生命某些东西,如 Cloneable,Serializable 等。
        • 相比 Annotation,该形式简单直接。但是 Annotation 可以有参数和值,更加强大。
      • @FunctionalInterface Annotation

        • 只有一个抽象方法的接口。
      • 面向对象的要素

        • 封装:隐藏事务内部的实现细节,以便提高安全性和简化编程。封装提供了合理的边界,避免外部调用者接触到内部的细节。可能会触发更多的问题,如并发问题。
        • 继承:是代码复用的基础机制。
        • 多态:会立刻想到重写 / 重载 / 向上转型。多态是同一个行为具有多个不同表现形式或形态的能力。同一个接口使用不同的实例而执行不同操作。
      • 需要遵守的设计原则

        • 单一职责
        • 开关原则

          • 对扩展开放,对修改关闭。
        • 里氏替换

          • 方式可以用父类或者基类的地方,都可以用子类替换。
        • 接口分离

          • 就是接口的单一职责,这个比普通类的更加重要。
        • 依赖反转

          • 这个我感觉应该就是面向接口的编程吧。实体应该是依赖抽象而不是实现

    9. 动态代理的两种方式,以及区别。

    反射与动态代理原理

    1 关于反射

    反射最大的作用之一就在于我们可以不在编译时知道某个对象的类型,而在运行时通过提供完整的”包名 + 类名.class”得到。注意:不是在编译时,而是在运行时。

    功能:

    •在运行时能判断任意一个对象所属的类。

    •在运行时能构造任意一个类的对象。

    •在运行时判断任意一个类所具有的成员变量和方法。

    •在运行时调用任意一个对象的方法。

    说大白话就是,利用 Java 反射机制我们可以加载一个运行时才得知名称的 class,获悉其构造方法,并生成其对象实体,能对其 fields 设值并唤起其 methods。

    应用场景:

    反射技术常用在各类通用框架开发中。因为为了保证框架的通用性,需要根据配置文件加载不同的对象或类,并调用不同的方法,这个时候就会用到反射——运行时动态加载需要加载的对象。

    特点:

    由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射。另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。

    2 动态代理

    为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在两者之间起到中介的作用(可类比房屋中介,房东委托中介销售房屋、签订合同等)。

    所谓动态代理,就是实现阶段不用关心代理谁,而是在运行阶段才指定代理哪个一个对象(不确定性)。如果是自己写代理类的方式就是静态代理(确定性)。

    组成要素:

    (动态) 代理模式主要涉及三个要素:

    其一:抽象类接口

    其二:被代理类(具体实现抽象接口的类)

    其三:动态代理类:实际调用被代理类的方法和属性的类

    实现方式:

    实现动态代理的方式很多,比如 JDK 自身提供的动态代理,就是主要利用了反射机制。还有其他的实现方式,比如利用字节码操作机制,类似 ASM、CGLIB(基于 ASM)、Javassist 等。

    举例,常可采用的 JDK 提供的动态代理接口 InvocationHandler 来实现动态代理类。其中 invoke 方法是该接口定义必须实现的,它完成对真实方法的调用。通过 InvocationHandler 接口,所有方法都由该 Handler 来进行处理,即所有被代理的方法都由 InvocationHandler 接管实际的处理任务。此外,我们常可以在 invoke 方法实现中增加自定义的逻辑实现,实现对被代理类的业务逻辑无侵入

    • 反射机制(JDK 自带)
    • 利用传说中更高性能的字节码操作机制,类似 ASM、cglib

    10.Java 序列化的方式。

    • 序列化方式一:实现 Serializable 接口 (隐式序列化)

      通过实现 Serializable 接口,这种是隐式序列化 (不需要手动),这种是最简单的序列化方式,会自动序列化所有非 static 和 transient 关键字修饰的成员变量。

    • 序列化方式二:实现 Externalizable 接口。(显式序列化)

      Externalizable 接口继承自 Serializable, 我们在实现该接口时,必须实现 writeExternal() 和 readExternal() 方法,而且只能通过手动进行序列化,并且两个方法是自动调用的,因此,这个序列化过程是可控的,可以自己选择哪些部分序列化

    • 序列化方式三:实现 Serializable 接口 + 添加 writeObject() 和 readObject() 方法。(显 + 隐序列化)

      如果想将方式一和方式二的优点都用到的话,可以采用方式三,先实现 Serializable 接口,并且添加 writeObject() 和 readObject() 方法。注意这里是添加,不是重写或者覆盖。但是添加的这两个方法必须有相应的格式。

    1,方法必须要被 private 修饰 —–> 才能被调用

    2,第一行调用默认的 defaultRead/WriteObject(); —–> 隐式序列化非 static 和 transient

    3,调用 read/writeObject() 将获得的值赋给相应的值 —> 显式序列化

    11. 传值和传引用的区别,Java 是怎么样的,有没有传值引用。

    12. 一个 ArrayList 在循环过程中删除,会不会出问题,为什么。

    13.@transactional 注解在什么情况下会失效,为什么。

    14.java 8 新特性

    • Lambda
    • 函数式接口
    • @FuncationInterface
      1. 当前接口有且仅有一个抽象方法,Object 对象的 public 方法除外
      1. 可以有默认方法和静态方法
      1. 该注解不是必须的,加不加都行,加的话会使编译器进行检查
    • java 8 Date 新特性
    • LocalDate
    • LocalTime
    • LocalDateTime
    • 格式化:TemporalAdjusters 进行日期的常规操作 获取月初 月底
    • 与传统 Date 互相转换

    15. 多线程

    • juc
    • AQS
    • Lock

    16. 集合

    • Collection
    • List
    • ArrayList LinkedList Vetor
    • queue
    • PriorityQUeue
    • set
    • HashSet LinkedHashSet TreeSet
    • Map
    • AbstractMap
    • 子主题 2
    退出移动版