Java 面试&基础100问(持续更新)

这不只是一篇面试题的汇总,也有自己在学习 Java 过程总结的比较重要的或容易模糊的知识点,故整理如下

1. 为什么说内部类会隐式持有外部类的引用

编译器会在编译阶段做4件事:

  • 给内部类添加一个类型为外部类的字段;
  • 给内部类的构造函数增加一个类型为外部类的参数;
  • 在内部类的所有构造函数中增加初始化外部类字段的代码;
  • 使用内部类的任何构造函数实例化内部类的地方,编译器都会为构造函数传入外部类的引用

这样就实现了内部类隐式持有外部类,在代码层面看不到,但是通过 javap 命令反编译,从字节码层面就能清晰看到,例如如下代码:

public class Outer {
    class Inner{
    }
}

反编译 Outer$Inner.class 的结果如图(省略常量池部分):


反编译Outer$Inner.class
2. 为什么在方法中定义的内部类可以引用方法的局部变量?并且该局部变量必须为 final 类型?

其原理和上个问题类似,也是编译器在编译阶段对内部类进行了一些改造:

  • 为内部类增加一个类型和所使用的局部变量相同的字段
  • 为内部类的所有构造函数增加一个局部变量类型的参数
  • 在内部类的所有构造函数中给添加的字段赋值,这个值即是所引用的外部方法的局部变量的值

简单的说,就是在方法中定义内部类时,如果引用了方法中的局部变量,那么编译器就会把该局部变量拷贝一份保存在内部类中。
而被引用的局部变量必须为 final 类型的原因也很清楚了,因为内部类只是拷贝了一份局部变量的值,如果之后局部变量发生改变,内部类是无法获知的,这样就可能出现不符合预期的结果。所以强制局部变量为 final 类型主要是为了在编译阶段就发现这种可能的错误。比如下面的代码,假设编译器没有强制局部变量为 final :

public class Outer {
    Runnable runnable;
    public Outer(){
        int i = 1;
        runnable = new Runnable(){
            @Override
            public void run() {
               System.out.println(i);
            }
        };
        i = 2; // 错误代码,编译器会报错,仅为说明问题
        runnable.run();
    }
}

我们可能会预期打印出的值为 2,但实际上 runnable 对象中保存的仅是 i 的一份拷贝,在定义之后对 i 的改变无法反映到 runnable 中。所以强制 i 为 final 类型,就确保了内部类和局部变量之间的一致性。
最后,在 Java8 中有一点变化,编译器变得更加智能,对于逻辑上和 final 类型等价的局部变量 可以不用强制声明为 final。简单说就是如果这个局部变量初始化之后,再没有改变其值的操作,那么不用声明为 final 也不会报错

3. Java 中的数组是对象么?有哪些特点?

Java 中的数组类型也是一种对象,从其具有 length 字段和 toString(),clone() 方法就能看出。数组对象的父类是 Object,所以以下代码都正确:

int[] array = new int[10];  
//可以向上转型成 Object  
Object obj = array ; 
//可以向下转型成 int[]   
int[] b = (int[])obj; 
//可以用instanceof关键字进行  
if(obj instanceof int[]){
    ...
}  

数组还有一些令人迷惑的特性,比如下面这段代码:

String[] s = new String[5];  
Object[] obja = s; 

这段代码是正确的!而前面我们已经知道 String[] 是 Object 的子类,不可能也同时是 Object[] 的子类,不然就违反了单继承原则。只能把这个当作数组对象的一种特殊性质来理解了(背后原理还有待研究)。概括一下就是:
** 如果B继承(extends)了A,那么A[]类型的引用就可以指向B[]类型的对象。
**
另外这种用法不包括基本类型,这也很好理解,因为基本类型并不继承于 Object:

int[] a = new int[4];  
//Object[] obja = a;  //错误,不能通过编译  

再看下面这段代码:

List list = new LinkedList<String>();
list.add("a");
// String[] strs = (String[]) list.toArray(); // 错误,运行时异常,无法强转
Object[] objs = list.toArray(new String[1]); 
String[] strs = (String[]) objs; // 正确,可以强转
System.out.println(strs[0] );

List.toArray() 方法返回 Object[],无法强转成 String[],尽管其数组成员实际上都是 String 类型。而 List.toArray(T[] a) 方法返回 T[],在本例中也就是返回 String[],可以用一个 Object[] 类型的变量指向 String[],然后还能强转。
所以进一步总结就是:
一个类型为 Object[] 的数组对象,尽管其数组元素类型为 A, 但是也不能强转成 A[]。但是一个类型为 A[] 的数组对象,可以用 Object 或者 A的父类类型的数组 类型的变量来指向,并且可以再强转成 A[]。

4. Java 中对象的初始化顺序遵循怎样的规则?
  • 先基类,后父类
  • 先成员变量,后构造函数
  • 先静态成员,后非静态成员
  • 静态变量只在初次使用时初始化一次,之后不再执行
  • 触发静态变量(或静态块)初始化的动作有:
    • 使用 new 关键字实例化对象;
    • 读取或设置一个类的静态字段;
    • 调用一个类的静态方法
    • 对类进行反射调用
    • 初始化子类时,如果父类还未初始化,会触发父类的初始化
    • 虚拟机启动时用户需要指定一个要执行的主类(包含 main() 函数的那个类),虚拟机会先初始化这个类
5. Java 虚拟机是怎样实现方法的重载(Overload)和重写(Override)的?

概括的说,方法的重载是在编译期确定,根据变量的静态类型决定要调用的方法,方法的重写是在运行时确定,根据变量的实际类型决定要调用的方法。

  • 重载举例(引用自《深入理解 Java 虚拟机》):
public class StaticDispatch {

    static abstract class Human {}

    static class Man extends Human {}

    static class Woman extends Human {}

    public void sayHello(Human guy) {
        System.out.println("hello,guy!");
    }

    public void sayHello(Man guy) {
        System.out.println("hello,gentleman!");
    }

    public void sayHello(Woman guy) {
        System.out.println("hello,lady!");
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sr = new StaticDispatch();
        sr.sayHello(man);
        sr.sayHello(woman);
    }
}

变量 man 和 woman 的静态类型都是 Human,所以 sr.sayHello(man) 和 sr.sayHello(woman) 这两条语句,编译器在编译时就确定了调用的版本为sayHello(Human guy) ,这点通过反编译后字节码也可以看出,在第26和第31行的字节码可以看到,调用的方法已经确定为 sayHello(Human guy) 。这也被叫做方法的 静态分派

反编译字节码

另外对于基本类型的重载需要单独说明一下,以 char 为例,其匹配重载方法的优先级是
char->int->long->float->double->Character->Serializable/Comparable(这两个优先级一样不能同时出现)-> Object。注意 byte-char-short 三者之间不能转型,因为 char 是无符号数,short 是有符号数,所以数据范围不同8

  • 重写举例(引用自《深入理解 Java 虚拟机》):
public class DynamicDispatch {

    static abstract class Human {
        protected abstract void sayHello();
    }

    static class Man extends Human {
        @Override
        protected void sayHello() {
            System.out.println("man say hello");
        }
    }

    static class Woman extends Human {
        @Override
        protected void sayHello() {
            System.out.println("woman say hello");
        }
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();
        man = new Woman();
        man.sayHello();
    }
}

显然这里 man 和 woman 会调用到各自重写的方法,背后的原理还是从字节码角度说明比较清楚:


反编译字节码

第16、17行的 aload_1 和 invokevirtual 指令。aload_1 把刚刚创建的 Man 对象压到操作数栈顶。Java 虚拟机规定执行 invokevirtual 指令时,会找到操作数栈顶的第一个元素所指向的对象的实际类型(本例中就是 Man),在其类型定义中查找对应的方法,如果找到那么就返回该方法的直接引用,否则在其父类中查找。

6. HashMap 的实现原理(基于Android SDK 里的实现,与 OpenSDK 略有不同)

一句话概括,横向是一个 HashMapEntry 数组,纵向是一个 HashMapEntry 链表。另外有一个单独的 HashMapEntry 保存 Key == null 的元素

  • Map 接口有个内部接口 Entry,它定义了 Map 的基本元素,键值对。HashMap 中实现 Entry 接口的内部类时 HashMapEntry
  • HashMap 维护一个 HashMapEntry 的数组 table,初始化大小总是为2的n次幂
  • put():
    1. 根据 Key 的 hashCode() 做二次hash计算出 Key 的hash值
    2. hash值取模数组长度,得到应该插入数组的位置 index
    3. 如果 index 位置不空,遍历 table[index] 为头的链表,查找是否有 Key 的 hash值相等且 equal() 为 true 的元素,如果有则返回旧值,保存新值
    4. 如果 table[index] == null,或者链表中未找到 Key 值相等的 Entry,那么size++(size > threshold 需要扩容,新建一个大小*2的数组,然后把之前的元素全部取出重新找到各自的位置),然后插入新 Entry 到数组 index 位置,新 Entry.next 指向之前的 table[index] (其实就是链表在头部的插入操作)
  • get() 和 remove() 很简单,前两步跟 put() 一样,之后就是遍历链表根据 Key 查找。
  • 遍历实现都基于 HashIterator.nextEntry() 方法,会从数组的第一个元素开始,按照先纵向后横向的顺序遍历

Java8 里的优化:HashMap 的实现在 Java8 里做了进一步的优化,当一个 index 下面的链表长度超过8时,该链表就转变成一颗红黑树,这样的查找效率就更高,一图胜千言:

http://coding-geek.com/how-does-a-hashmap-work-in-java/

7. 使用 AtomicInteger 和 使用 synchronized 实现对变量的原子操作有什么不同?
  • synchronized 是阻塞式的,会导致线程上下文的切换,对于简单的赋值操作来说,代价太高。AtomicInteger 通过 CPU 对 CAS(compare and swap) 操作的原子性 以及 volatile 关键字实现了非阻塞式的原子操作,是非阻塞的,没有线程切换的开销
  • synchronized 是悲观的,它假设一定会有竞争,所以会先获取锁再执行操作;AtomicInteger 是乐观的,它先尝试更新操作,如果当前值与期望值不等,则代表出现竞争,返回false,然后不断尝试直到成功

以 AtomicInteger.getAndIncrement() 为例,它实现了 i++ 的原子操作:

public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }

可以看到有一个死循环(这也是自旋锁说法的由来),只要 compareAndSet() 不成功,就不断尝试,直到成功再返回。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • Java虚拟机的执行引擎:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。本章主要是从概念模...
    maxwellyue阅读 513评论 0 0
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,293评论 18 399
  • 本文主要参考自 深入理解Java虚拟机 概述 Java能够做到“一处编译,处处运行”,这与.class文件的...
    东溪95阅读 1,797评论 0 4
  • Win7下如何打开DOS控制台? a:开始--所有程序--附件--命令提示符 b:开始--搜索程序和文件--cmd...
    逍遥叹6阅读 1,564评论 4 12
  • 文| 涂山狐狸 图| 网络 “只管走过去,不必逗留着采了花朵来保存,因为一路上花朵自会继续开放的。” 1 有一个亲...
    涂山狐狸阅读 293评论 1 2