String 类为什么是 final 的?

  1. 为了效率。若允许被继承,则其高度的被使用率可能会降低程序的性能。
  2. 为了安全。JDK 中提供的好多核心类比如 String,类的内部好多方法的实现都不是 java 编程语言本身编写的,好多方法都是调用的操作系统本地的 API,这就是著名的“本地方法调用”,也只有这样才能做事,这种类和操作系统交流频繁,如果这种类可以被继承而且把它的方法重写了,那么就可以往操作系统内部写入一段具有恶意攻击性质的代码。
  3. 不希望被修改。这个类就像一个工具一样,类的提供者不希望被改。如果随便能改了,那么 java 编写的程序就会不稳定,而 java 和 C++ 相比的优点之一就是比较稳定。

String、StringBuffer 与 StringBuilder 的区别?

String 类型是不可变类,StringBuffer 和 StringBuilder 是可变类。

速度:StringBuilder > StringBuffer > String。

StringBuffer 和 StringBuilder 底层是 char[] 数组实现的。

StringBuffer是线程安全的,而 StringBuilder 是线程不安全的。

如果要操作少量的数据用 String,单线程操作大量数据用 StringBuilder,多线程操作大量数据用 StringBuffer。


TreeSet 的原理和使用(Comparable 和 comparator)

  1. TreeSet 中的元素不允许重复,但是有序。
  2. TreeSet 采用树结构存储数据,存入元素时需要和树中元素进行对比,需要指定比较策略。可以通过 Comparable 和 Comparator 来指定比较策略。
  3. 实现了 Comparable 的系统类可以顺利存入 TreeSet;自定义类可以实现 Comparable 接口来指定比较策略。
  4. 可创建 Comparator 接口实现类来指定比较策略,并通过 TreeSet 构造方法参数传入;这种方式尤其对系统类非常适用。

请简述 Java 的垃圾回收机制

垃圾回收由 java 虚拟机自动执行,不能人为的干预,系统在空闲的时候会自动执行垃圾回收机制,可以通过 System.gc() 方法建议执行垃圾回收,但不能确定什么时候回执行回收。

在 JVM 垃圾回收器收集一个对象之前,一般要求程序调用适当的方法释放资源,但在没有明确释放资源的情况下,Java 提供了默认机制来终止该对象并释放资源,这个方法就是 finalize()。

垃圾回收指的是对内存的回收,而这里的内存主要指 JVM 的堆区和方法区的内存。


sleep() 和 wait() 有什么区别

sleep() 方法是线程类 Thread 的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu 的执行时间。sleep() 是 static 静态的方法,它不能改变对象的锁,当一个 synchronized 块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。

wait() 是 Object 类的方法,当一个线程执行到 wait 方法时,它就进入到一个和该对象相关的等待池,同时释放对象的锁,使得其他线程能够访问,可以通过 notify 或 notifyAll 方法来唤醒等待的线程。


青蛙跳台阶:青蛙可以一次跳 1 级 / 2 级台阶,请问跳上第 n 级台阶有多少种方法?

/**
 * @author Renda Zhang
 * @create 2020-06-24 16:40
 */
public class FrogJumpStairs {

    // 总台阶数
    private static final int TOTAL_STAIRS = 10;

    /**
     * 数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...
     * 使用 Fibonacci sequence(斐波那契数列)来解答
     * 时间复杂度:O(n) 单循环到 n
     * 空间复杂度:O(1)
     *
     * @param total 总的台阶数量
     * @return 所有跳法的总数量
     */
    private static int jumpStairsFibonacci(int total) {
        if (total == 1) {
            return 1;
        }
        int firstNum = 1;
        int secondNum = 2;
        for (int i = 3; i <= total; i++) {
            int third = firstNum + secondNum;
            firstNum = secondNum;
            secondNum = third;
        }
        return secondNum;
    }

    /**
     * Dynamic Programming (动态规划)
     * 时间复杂度:O(n) 单循环到 n
     * 空间复杂度:O(n) dp 数组用了 n 空间
     *
     * @param total 总的台阶数量
     * @return 所有跳法的总数量
     */
    private static int jumpStairsDp(int total) {
        if (total == 1) {
            return 1;
        }
        int[] dp = new int[total + 1];
        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3; i <= total; i++) {
            dp[i] = dp[i - 1] + dp[i -2];
        }
        return dp[total];
    }

    // 记忆每一层递归对应的数值
    private static int[] memo;

    /**
     * 递归解法的优化
     * 时间复杂度:O(n)
     * 空间复杂度:O(n)
     *
     * @param current 青蛙已经跳过的台阶数量
     * @param total 总的台阶数量
     * @return 所有跳法的总数量
     */
    private static int jumpStairsMemo(int current, int total) {
        // 如果目前已经跳过的台阶数大于总台阶数,说明传入的参数不合理,返回 0 代表跳法为 0
        if (current > total) {
            return 0;
        }
        // 如果相等,说明青蛙已经跳完一次。
        if (current == total) {
            return 1;
        }
        // 说明已有记录,直接返回
        if (memo[current] > 0) {
            return memo[current];
        }
        // 通过递归把所有次数相加即得到总次数。
        memo[current] = jumpStairsMemo(current + 1, total) + jumpStairsMemo(current + 2, total);
        return memo[current];
    }

    /**
     * 递归暴力破解法
     * 时间复杂度:O(2ⁿ)  - 递归树的所有节点数
     * 空间复杂度:O(n) - 递归树可达深度
     *
     * @param current 青蛙已经跳过的台阶数量
     * @param total 总的台阶数量
     * @return 所有跳法的总数量
     */
    private static int jumpStairs(int current, int total) {
        // 如果目前已经跳过的台阶数大于总台阶数,说明传入的参数不合理,返回 0 代表跳法为 0
        if (current > total) {
            return 0;
        }
        // 如果相等,说明青蛙已经跳完一次。
        if (current == total) {
            return 1;
        }
        // 通过递归把所有次数相加即得到总次数。
        return jumpStairs(current + 1, total) + jumpStairs(current + 2, total);
    }

    public static void main(String[] args) {
        System.out.println(jumpStairs(0, TOTAL_STAIRS));

        memo = new int[TOTAL_STAIRS + 1];
        System.out.println(jumpStairsMemo(0, TOTAL_STAIRS));

        System.out.println(jumpStairsDp(TOTAL_STAIRS));

        System.out.println(jumpStairsFibonacci(TOTAL_STAIRS));
    }
}

HashMap 和 Hashtable 的区别和联系

实现原理相同,功能相同,底层都是哈希表结构,查询速度快,在很多情况下可以互用。

两者的主要区别如下

  1. Hashtable 是早期 JDK 提供的接口,HashMap 是新版 JDK 提供的接口。
  2. Hashtable 继承 Dictionary 类,HashMap 实现 Map 接口。
  3. Hashtable 线程安全,HashMap 非线程安全。
  4. Hashtable 不允许 null 值,HashMap 允许 null 值
  5. Hashtable 和 HashMap 都使用了 Iterator。而由于历史原因,Hashtable 还使用了 Enumeration 的方式 。
  6. 初始容量大小和每次扩充容量大小的不同:Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1;HashMap 默认的初始化大小为 16,之后每次扩充,容量变为原来的2倍。
  7. 哈希值的使用不同,HashTable 直接使用对象的 hashCode。而 HashMap 重新计算 hash 值。hashCode 是 jdk 根据对象的地址或者字符串或者数字算出来的 int 类型的数值。Hashtable 计算 hash 值,直接用 key 的 hashCode();而 HashMap 重新计算了 key 的 hash 值。Hashtable 在求 hash 值对应的位置索引时,用取模运算;而 HashMap 在求位置索引时,则用与运算,且这里一般先用 hash &amp; 0x7FFFFFFF 后,再对 length 取模,&amp; 0x7FFFFFFF 的目的是为了将负的 hash 值转化为正值,因为 hash 值有可能为负数,而 &amp; 0x7FFFFFFF 后,只有符号位改变。

字符流字节流的联系和区别。什么时候使用字节流和字符流?

字符流 Character Stream 和字节流 Byte Stream 是 IO 流的划分,按处理照流的数据单位进行的划分,两类都分为输入和输出操作。

在字节流中输出数据主要是使用 OutputStream 类完成,输入使的是 InputStream 类;在字符流中输出主要是使用 Writer 类完成,输入流主要使用 Reader 类完成。这四个都是抽象类。

字符流处理的单元为 2 个字节的 Unicode 字符,操作“字符、字符数组或字符串”;而字节流处理的单元为 1 个字节,操作“字节和字节数组”。

字节流是最基本的,所有的 InputStrem 和 OutputStream 的子类都是主要用在处理二进制数据,是按字节来处理的;但是实际中很多的数据是文本,所以又提出了字符流的概念,它是按虚拟机的字符编码来处理的,也就是要进行字符集的转化。

字节流和字符流之间通过 InputStreamReader 和 OutputStreamWriter 来关联,底层是通过 byte[] 和 String 来关联的。


说出 ArrayList,LinkedList 的储存性能和特性

1、ArrayList 支持以角标位置索引来获取对应的元素(随机访问);但是 LinkedList 则需要遍历整个链表来获取对应的元素。因此一般 ArrayList 的访问速度要快于 LinkedList。
2、ArrayList 由于是数组结构,对于删除和修改而言消耗是比较大(需要复制和移动数组);而 LinkedList 是双向链表,删除和修改只需要修改对应的指针即可,其消耗是很小的。因此一般 LinkedList 的增删速度比 ArrayList 快。

3、它们都是线程不安全的,但动态数组 Vector 类集合依靠同步访问的方式达到线程安全的目的。

4、ArrayList 调用无参构造后初始长度为 0,当第一次调用 add 后,长度变为 10;而 LinkedList 是一个双向链表,没有初始化大小,也没有扩容的机制,就是一直在头或尾进行增删。


Null 值作为参数的重载问题

考察重载函数调用时精确性的问题。

思考一下下面程序的输出结果

public class TestNull { 
    public void show(String a){ 
        System.out.println("String"); 
    } 
    public void show(Object o){ 
        System.out.println("Object"); 
    } 
    public static void main(String args[]){ 
        TestNull t = new TestNull(); 
        t.show(null); 
    } 
}

Java 的重载 Overload 解析过程是以两阶段运行的:第一阶段,选取所有可获得并且可应用的方法或构造器;第二阶段,在第一阶段选取的方法或构造器中选取最精确的一个。如果一个方法或构造器可以接受传递给另一个方法或构造器的任何参数,那么就认为第一个方法比第二个方法缺乏精确性。

这个程序的 show(Object o) 可以接受任何传递给 show(String a) 的参数,因此 show(Object o) 相对缺乏精确性。

所以,运行的结果为:"String"。