JAVA面试常见问题
JAVA基础概念
Java语言特点
- 简单易学
- 面向对象(封装、继承、多态)
- 平台无关性(Java虚拟机实现)
- 支持多线程
- 可靠性
- 安全性
- 支持网络编程且很方便(Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便)
- 编译与解释并存
- “Write Once, Run Anywhere(一次编写,随处运行)”这句宣传口号,真心经典,流传了好多年!以至于,直到今天,依然有很多人觉得跨平台是 Java 语言最大的优势。实际上,跨平台已经不是 Java 最大的卖点了,各种 JDK 新特性也不是。目前市面上虚拟化技术已经非常成熟,比如你通过 Docker 就很容易实现跨平台了。在我看来,Java 强大的生态支持才是其最大的优势。
JVM & JDK & JRE
JVM
- JVM 即 Java虚拟机,是运行Java字节码的虚拟机。JVM有针对不同系统的特定实现,目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
- JVM本质上是一个规范,只要满足JVM规范,每个公司、组织或者个人都可以开发自己的专属 JVM。除了我们平时最常用的 HotSpot VM 外,还有 J9 VM、Zing VM、JRockit VM 等JVM实现
- 关于常见JVM的区别,详见Wiki百科的对比:Comparison of Java virtual machines
- 各个版本的 JDK 对应的 JVM 规范:Java SE Specifications
JDK & JRE
- JDK是 Java Development Kit 的缩写,是功能齐全的Java SDK(软件开发工具包)。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb),具有创建和编译程序的功能。
- JRE是Java运行时环境,它是运行已编译Java程序所需的所有内容的集合,包括Java虚拟机(JVM),Java类库,java命令和其他的一些基础构件。但是,它不能用于创建新程序。
- 如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装 JDK 了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用程序服务器会将 JSP 转换为Java servlet,并且需要使用 JDK 来编译 servlet。
关于字节码
- 在Java中,JVM可以理解的代码就叫做字节码(即扩展名为
.class
的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以,Java程序运行时相对来说还是高效的(不过,和C++,Rust,Go等语言还是有一定差距的),而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。 - Java程序从源码到运行的过程如下:
.java
源码- 被Javac编译器编译
.class
字节码- 由解释器解释或JIT编译
- 机器码
- 其中格外需要注意的是从
.class
字节码到机器码这一个步骤, 在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了JIT(just-in-time compilation)编译器,而JIT属于运行时编译。当JIT编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于Java解释器的。这也解释了我们为什么经常会说Java是编译与解释共存的语言。
HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9 引入了一种新的编译模式AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。JDK 支持分层编译和 AOT 协作使用。
为什么不全部使用AOT呢?
- AOT可以提前编译节省启动时间,那为什么不全部使用这种编译方式呢?
- 其实很简单,这和 Java 语言的动态特性有千丝万缕的联系了。举个例子,CGLIB动态代理使用的是ASM技术,而这种技术大致原理是运行时直接在内存中生成并加载修改后的字节码文件也就是
.class
文件,如果全部使用AOT提前编译,也就不能使用ASM技术了。为了支持类似的动态特性,所以选择使用JIT即时编译器。
关于“编译与解释并存”
- 其实在字节码那一部分就已经提到过这个问题,这里单独提出来
- 我们可以将高级编程语言按照程序的执行方式分为两种:
- 根据维基百科的介绍:
为了改善编译语言的效率而发展出的即时编译技术,已经缩小了这两种语言间的差距。这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成字节码。到执行期时,再将字节码直译,之后执行。Java与LLVM是这种技术的代表产物。
相关阅读:基本功 | Java即时编译器原理解析及实践 - 之所以说Java“编译与解释并存”是因为:Java语言既具有编译型语言的特征,也具有解释型语言的特征。因为Java程序要经过先编译,后解释两个步骤,由Java编写的程序需要先经过编译步骤,生成字节码
.class
文件,这种字节码必须由Java解释器来解释执行。
Oracle JDK & OpenJDK
- Oracle JDK 大概每 6 个月发一次主要版本(从 2014 年 3 月 JDK 8 LTS 发布到 2017 年 9 月 JDK 9 发布经历了长达 3 年多的时间,所以并不总是 6 个月),而 OpenJDK 版本大概每三个月发布一次
- OpenJDK 是一个参考模型并且是完全开源的,而 Oracle JDK 是 OpenJDK 的一个实现,并不是完全开源的
- OracleJDK比OpenJDK更稳定
- 响应性和JVM性能方面,Oracle JDK 与 OpenJDK 相比提供了更好的性能
- Oracle JDK 不会为即将发布的版本提供长期支持(如果是 LTS 长期支持版本的话也会,比如 JDK 8,但并不是每个版本都是 LTS 版本),用户每次都必须通过更新到最新版本获得支持来获取最新版本
- Oracle JDK 使用 BCL/OTN 协议获得许可,而 OpenJDK 根据 GPL v2 许可获得许可
Java和C++的区别
- Java 不提供指针来直接访问内存,程序内存更加安全
- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承
- Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存
- C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)
- …
基本语法
注释的形式
- Java中的注释有三种:
- 单行注释 :通常用于解释方法内某单行代码的作用
- 多行注释 :通常用于解释一段代码的作用
- 文档注释 :通常用于生成 Java 开发文档
- 在我们编写代码的时候,如果代码量比较少,我们自己或者团队其他成员还可以很轻易地看懂代码,但是当项目结构一旦复杂起来,我们就需要用到注释了。注释并不会执行(编译器在编译代码之前会把代码中的所有注释抹掉,字节码中不保留注释),是我们程序员写给自己看的,注释是你的代码说明书,能够帮助看代码的人快速地理清代码之间的逻辑关系。因此,在写程序的时候随手加上注释是一个非常好的习惯。
- 《Clean Code》这本书明确指出:
代码的注释不是越详细越好。实际上好的代码本身就是注释,我们要尽量规范和美化自己的代码来减少不必要的注释。
若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述。
标识符&关键字
- 在我们编写程序的时候,需要大量地为程序、类、变量、方法等取名字,于是就有了标识符 。简单来说,标识符就是一个名字
- 有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这些特殊的标识符就是关键字。简单来说,关键字是被赋予特殊含义的标识符 。比如,在我们的日常生活中,如果我们想要开一家店,则要给这个店起一个名字,起的这个“名字”就叫标识符。但是我们店的名字不能叫“警察局”,因为“警察局”这个名字已经被赋予了特殊的含义,而“警察局”就是我们日常生活中的关键字
关键字具体有哪些
分类 | 关键字 |
---|---|
访问控制 | private、protected、public |
类,方法和变量修饰符 | abstract、class、extends、final、implements、interface、native、new、static、strictfp、synchronized、transient、volatile、enum |
程序控制 | break、continue、return、do、while、if、else、for、instanceof、switch、case、default、assert |
错误处理 | try、catch、throw、throws、finally |
包相关 | import、package |
基本类型 | boolean、byte、char、double、float、int、long、short |
变量引用 | super、this、void |
保留字 | goto、const |
- 所有的关键字都是小写的,在IDE中会以特殊颜色显示
default
这个关键字很特殊,既属于程序控制,也属于类,方法和变量修饰符,还属于访问控制- 在程序控制中,当在
switch
中匹配不到任何情况时,可以使用default
来编写默认匹配的情况 - 在类,方法和变量修饰符中,从 JDK8 开始引入了默认方法,可以使用
default
关键字来定义一个方法的默认实现 - 在访问控制中,如果一个方法前没有任何修饰符,则默认会有一个修饰符
default
,但是这个修饰符加上了就会报错
- 在程序控制中,当在
- 虽然
true
,false
和null
看起来像关键字但实际上他们是字面值,同时你也不可以作为标识符来使用
自增自减运算符
- 在写代码的过程中,常见的一种情况是需要某个整数类型变量增加1或减少1,Java 提供了一种特殊的运算符,用于这种表达式,叫做自增运算符
++
和自减运算符--
++
和--
运算符可以放在变量之前,也可以放在变量之后- 当运算符放在变量之前时(前缀),先自增/减,再赋值
- 当运算符放在变量之后时(后缀),先赋值,再自增/减
移位运算符
- 移位操作中,被操作的数据被视为二进制数,移位就是将其向左或向右移动若干位的运算
- 在Java代码里使用
<<
、>>
和>>>
转换成的指令码运行起来会更高效些 - Java 中有三种移位运算符:
<<
:左移运算符,向左移若干位,高位丢弃,低位补零。x << 1
,相当于x乘以2(不溢出的情况下)>>
:带符号右移,向右移若干位,高位补符号位,低位丢弃。正数高位补0,负数高位补1。x >> 1
,相当于x除以2>>>
:无符号右移,忽略符号位,空位都以0补齐
- 由于
double
,float
在二进制中的表现有些特殊,不能进行移位操作 - 移位操作符实际上支持的类型只有
int
和long
,编译器在对short
、byte
、char
类型进行移位前,都会将其转换为int
类型再操作
如果移位的位数超过数值所占有的位数会怎样
当int
类型左移/右移位数大于等于32位操作时,会先求余%
后再进行左移/右移操作。也就是说左移/右移32位相当于不进行移位操作(32%32=0),左移/右移42位相当于左移/右移10位(42%32=10)。当long
类型进行左移/右移操作时,由于long
对应的二进制是64位,因此求余操作的基数也变成了64
- 也就是说:
x<<42
等同于x<<10
,x>>42
等同于x>>10
,x>>>42
等同于x>>>10
continue
& break
& return
- 在循环结构中,当循环条件不满足或者循环次数达到要求时,循环会正常结束。但是,有时候可能需要在循环的过程中,当发生了某种条件之后 ,提前终止循环,这就需要用到下面几个关键词
continue
:指跳出当前的这一次循环,继续下一次循环break
:指跳出整个循环体,继续执行循环下面的语句
return
用于跳出所在方法,结束该方法的运行。return
一般有两种用法return;
:直接使用 return 结束方法执行,用于没有返回值函数的方法return value;
:return 一个特定值,用于有返回值函数的方法
变量
- 成员变量与局部变量的区别:
- 语法形式:从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被
public
,private
,static
等修饰符所修饰,而局部变量不能被访问控制修饰符及static
所修饰;但是,成员变量和局部变量都能被final
所修饰 - 存储方式:从变量在内存中的存储方式来看,如果成员变量是使用
static
修饰的,那么这个成员变量是属于类的,如果没有使用static
修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存 - 生存时间:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡
- 默认值:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被
final
修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值
- 语法形式:从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被
- 静态变量的作用:
- 静态变量可以被类的所有实例共享。无论一个类创建了多少个对象,它们都共享同一份静态变量
- 通常情况下,静态变量会被
final
关键字修饰成为常量
- 字符型常量和字符串常量的区别:
- 形式:字符常量是单引号引起的一个字符,字符串常量是双引号引起的0个或若干个字符
- 含义: 字符常量相当于一个整型值(ASCII值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)
- 占内存大小:字符常量只占2个字节; 字符串常量占若干个字节
方法
关于方法的返回值
- 方法的返回值:是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用是接收出结果,使得它可以用于其他的操作
- 我们可以按照方法的返回值和参数类型将方法分为下面这几种:
- 无参数无返回值的方法
- 有参数无返回值的方法
- 有返回值无参数的方法
- 有返回值有参数的方法
关于静态方法
- 静态方法不能调用非静态成员,原因如下:
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问
- 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作
- 静态方法和实例方法的不同:
- 调用方式
- 在外部调用静态方法时,可以使用
类名.方法名
的方式,也可以使用对象.方法名
的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象 - 不过,需要注意的是一般不建议使用
对象.方法名
的方式来调用静态方法。这种方式非常容易造成混淆,静态方法不属于类的某个对象而是属于这个类
- 在外部调用静态方法时,可以使用
- 访问类成员是否存在限制
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制
- 调用方式
重载和重写的区别
- 重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理
- 重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法
区别点 | 重载方法 | 重写方法 |
---|---|---|
发生范围 | 同一个类 | 子类 |
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 |
异常 | 可修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等 |
访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) |
发生阶段 | 编译期 | 运行期 |
重载
- 发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同
- 《Java核心技术》这本书是这样介绍重载的:
如果多个方法(比如
StringBuilder
的构造方法)有相同的名字、不同的参数, 便产生了重载。
编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。 如果编译器找不到匹配的参数, 就会产生编译时错误, 因为根本不存在匹配, 或者没有一个比其他的更好(这个过程被称为重载解析(overloading resolution))
Java 允许重载任何方法, 而不只是构造器方法 - 综上:重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理
重写
- 重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写
- 方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类
- 如果父类方法访问修饰符为
private
/final
/static
则子类就不能重写该方法,但是被static
修饰的方法能够被再次声明 - 构造方法无法被重写
- 综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变
- 方法的重写要遵循“两同两小一大”
- “两同”即方法名相同、形参列表相同
- “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等
- “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等
- 关于重写的返回值类型这里需要额外多说明一下,上面的表述不太清晰准确:如果方法的返回类型是
void
和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的
关于可变长参数
- 从Java5开始,Java支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面的这个
printVariable
方法就可以接受0个或者多个参数
public static void method1(String... args) {
//......
}
- 另外,可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数
public static void method2(String arg1, String... args) {
//......
}
- 遇到方法重载的情况会优先匹配固定参数的方法,因为固定参数的方法匹配度更高
- Java的可变参数编译后实际会被转换成一个数组
基本数据类型
基本数据类型的种类
- Java中有8种基本数据类型,分别为:
- 6种数字类型:
- 4种整数型:
byte
、short
、int
、long
- 2种浮点型:
float
、double
- 4种整数型:
- 1种字符类型:
char
- 1种布尔类型:
boolean
- 6种数字类型:
- 这8种基本数据类型的默认值以及所占的空间大小如下:
基本类型 | 位数 | 字节 | 默认值 | 取值范围 |
---|---|---|---|---|
byte |
8 | 1 | 0 | -128 ~ 127 |
short |
16 | 2 | 0 | -32768 ~ 32767 |
int |
32 | 4 | 0 | -2147483648 ~ 2147483647 |
long |
64 | 8 | 0L | -9223372036854775808 ~ 9223372036854775807 |
char |
16 | 2 | ‘u0000’ | 0 ~ 65535 |
float |
32 | 4 | 0f | 1.4E-45 ~ 3.4028235E38 |
double |
64 | 8 | 0d | 4.9E-324 ~ 1.7976931348623157E308 |
boolean |
1 | false |
true 、false |
- 对于
boolean
,官方文档未明确定义,它依赖于JVM厂商的具体实现。逻辑上理解是占用1位,但是实际中会考虑计算机高效存储因素 - Java的每种基本类型所占存储空间的大小不会像其他大多数语言那样随机器硬件架构的变化而变化。这种所占存储空间大小的不变性是Java程序比用其他大多数语言编写的程序更具可移植性的原因之一
- Java里使用long类型的数据一定要在数值后面加上L,否则将作为整型解析
- 通常情况下
char
类型赋值使用单引号,String
类型使用双引号 - 这八种基本类型都有对应的包装类分别为:
Byte
、Short
、Integer
、Long
、Float
、Double
、Character
、Boolean
基本类型和包装类型的区别
- 成员变量包装类型不赋值就是
null
,而基本类型有默认值且不是null
- 包装类型可用于泛型,而基本类型不可以
- 基本数据类型的局部变量存放在Java虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被
static
修饰 )存放在Java虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中 - 相比于对象类型, 基本数据类型占用的空间非常小
注意:
- 为什么说是几乎所有对象实例呢? 这是因为 HotSpot 虚拟机引入了JIT优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存
- 基本数据类型存放在栈中是一个常见的误区! 基本数据类型的成员变量如果没有被static修饰的话(不建议这么使用,应该要使用基本数据类型对应的包装类型),就存放在堆中
包装类型的缓存机制
- Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能
Byte
,Short
,Integer
,Long
这4种包装类默认创建了数值[-128,127]的相应类型的缓存数据,Character
创建了数值在[0,127]范围的缓存数据,Boolean
直接返回True
orFalse
- 超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡
- 两种浮点数类型的包装类
Float
,Double
并没有实现缓存机制 - 所有整型包装类对象之间值的比较,全部使用 equals 方法比较
自动装箱与拆箱
- 什么是自动拆装箱
- 装箱:将基本类型用它们对应的引用类型包装起来
- 拆箱:将包装类型转换为基本数据类型