基础概念与常识
Java 语言有哪些特点?
- 简单易学;
- 面向对象(封装,继承,多态);
- 平台无关性( Java 虚拟机实现平台无关性);
- 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持);
- 可靠性(具备异常处理和自动内存管理机制);
- 安全性(Java 语言本身的设计就提供了多重安全防护机制如访问权限修饰符、限制程序直接访问操作系统资源);
- 高效性(通过 Just In Time 编译器等技术的优化,Java 语言的运行效率还是非常不错的);
- 支持网络编程并且很方便;
- 编译与解释并存;
Java SE vs Java EE
Java SE是Java的基础版本,更适合开发桌面应用程序或简单的服务器应用程序;Java EE是Java的企业版本,适合开发复杂的企业级应用程序或者Web应用
JVM vs JDK vs JRE
JVM
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。
JDK 和 JRE
JDK(Java Development Kit),它是功能齐全的 Java SDK,是提供给开发者使用,能够创建和编译 Java 程序的开发套件。它包含了 JRE,同时还包含了编译 java 源码的编译器 javac 以及一些其他工具比如 javadoc(文档注释工具)、jdb(调试器)、jconsole(基于 JMX 的可视化监控⼯具)、javap(反编译工具)等。
JRE(Java Runtime Environment) 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)。
什么是字节码?采用字节码的好处是什么?
字节码是JAVA可以理解的代码,即扩展名为.class的文件,不面向任何特定的处理器,只面向虚拟机。
采用字节码的好处在于:一定程度上解决了传统解释型语言执行效率的问题,同时又保留了解释性语言可移植的特点。
- 字节码不针对特定机器,所以Java程序无需重新编译便可在多种不同的操作系统上运行。
- JIT编译器在完成第一次编译后,会将字节码对应的机器码保存下来,下次可以直接使用。
为什么说Java语言“编译与解释并存”?
- 编译型:编译型语言会通过编译器将源代码一次性翻译成可悲该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低
- 解释型:解释型语言会通过解释器逐句将代码解释为机器码,执行速度比较慢,开发效率比较高
Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class 文件),这种字节码必须由 Java 解释器来解释执行。
AOT有什么优点,为什么不全部使用AOT?
AOT是JDK 9新引入的编译模式,会在程序被执行前就将其编译成机器码,属于静态编译。AOT避免了JIT在预热等各方面的开销,可以提高JAVA程序启动速度,避免预热时间长。同时,还能减少内存占用和增加JAVA程序的安全性(不易被反编译和修改)。
AOT的缺点在于无法支持JAVA的一些动态特性,比如反射、动态代理、动态加载等,很多框架和库都用到这些特性,例如Spring等。
Oricle JDK vs Open JDK 区别
- OpenJDK 是一个参考模型并且是完全开源的,而 Oracle JDK 是基于 OpenJDK 实现的,并不是完全开源的。
- Oracle JDK 会提供免费版本,但一般有时间限制。OpenJDK 是完全免费的。
- 更新频率上open JDK更快
Java和C++区别?
- java不提供指针来直接访问内存,程序内存更加安全
- Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是接口可以多继承
- Java有自动内存管理垃圾回收机制,不需要手动释放无用内存
- Java只支持方法重载,而C++支持方法和操作符重载
基本语法
Java的注释有几种形式
- 单行注释
//
- 多行注释
/**/
- 文档注释:用于生成Java开发文档
标识符和关键字的区别
- 标识符:为程序、类、变量、方法等取的名字
- 关键字:被赋予特殊含义,只能用于特殊位置的标识符
常见关键字
- 访问控制:private、protected、public
- 类,方法和变量修饰符:abstract、class、extends、final、implements、interface、native
- 程序控制: 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
移位运算符
<<
:左移运算符,向左移若干位,高位丢弃,低位补零。x << 1,相当于 x 乘以 2(不溢出的情况下)。>>
:带符号右移,向右移若干位,高位补符号位,低位丢弃。正数高位补 0,负数高位补 1。x >> 1,相当于 x 除以 2。>>>
:无符号右移,忽略符号位,空位都以 0 补齐。
如果移位的位数超过数值所占有的位数会怎样?
当 int 类型左移/右移位数大于等于 32 位操作时,会先求余(%)(针对移动位数)后再进行左移/右移操作。
基本数据类型![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/55c74a39ad311eb973f852a2546298ab.png)
基本类型和包装类型区别
- 用途:除了定义常量和局部变量外,在方法参数和对象属性上通常用包装类型。包装类型可以用于泛型,基本类型不行
- 基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
- 占用空间:基本类型的占用空间远小于包装类型
- 默认值:包装类型不赋值默认为null,基本类型有默认值且不为bull
- 比较方法:基本类型
==
比较的是值,包装类型比较的是对象的内存空间,进行值比较统一用equals
方法
包装类型的缓存机制
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。
自动装箱与拆箱了解吗?原理是什么?
- 装箱: 将基本类型用对应的引用类型包装起来
- 拆箱:将包装类型转换为基本数据类型
Integer i = 10; //装箱
int n = i; //拆箱
Integer i = 10
等价于Integer i = Integer.valueOf(10)
int n = i
等价于int n = i.intValue();
如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
为什么浮点数运算的时候会有精度丢失的风险?
计算机在表示一个数字时,精度是有限的,超过宽度的小数在表示时会被截断,导致精度损失
如何解决浮点数运算的精度丢失问题?
BigDecimal
可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal
来做的。
成员变量和局部变量区别
- 语法形式: 成员变量属于类,可以被public,private,static等修饰符修饰;局部变量是方法或代码块中定义的,不能被访问控制修饰符以及static修饰。但是两者都可以被final修饰
- 生存时间:成员变量是对象的一部分,随着对象的创建而存在;局部变量随着方法的调用自动生成,方法结束后回收
- 默认值:成员变量会自动赋予默认值,自动变量不会自动赋予默认值
为什么成员变量有默认值
- 变量存储的是内存地址对应的任意随机值,程序读取时会出现意外
- 默认值有两种设置方式:自动和手动。成员变量在运行时可借助反射等方法手动赋值,而局部变量不行。
- 对于编译器而言,局部变量没有赋值可以直接报错,但成员变量会在运行时赋值,无法判断是否报错,因此自动赋予默认值。
静态变量作用
静态变量也就是被 static 关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。
静态变量是通过类名来访问的,例如StaticVariableExample.staticVar
字符型常量和字符串常量的区别
- 形式:字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。
- 含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。
- 占内存大小:字符常量只占 2 个字节; 字符串常量占若干个字节。
静态方法为什么不能调用非静态成员
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
- 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
静态方法和实例方法有何不同?
- 调用方法: 在外部调用静态方法时,可以使用
类名.方法名
或者对象名.方法名
,而实例方法只能采用后者 - 访问限制: 静态方法只能访问静态成员,实例方法没有此限制
重载和重写区别
- 重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理。方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
- 重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法
- 方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
- 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
- 构造方法无法被重写
什么是可变长参数
从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。另外,可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数。
public static void method2(String arg1, String... args) {
//......
}
面向对象基础
面向对象和面向过程的区别
- 面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题
- 面向对象会先抽象出对象,然后通过对象调用方法的形式来解决问题。面向对象开发的程序一般更易维护、易复用、易扩展
对象的相等和引用相等的区别
- 对象的相等一般比较的是内存中存放的内容是否相等
- 引用相等一般比较的是他们指向内存地址是否相等
构造方法有哪些特点?是否可被 override?
- 名字与类名相同。
- 没有返回值,但不能用 void 声明构造函数。
- 生成类的对象时自动执行,无需调用。
不可以被重写,但是可以被重载,一个类中可以有多个构造函数
面向对象三大特征
- 封装:封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
- 继承:继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。
- 多态:表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
多态的特点
1. 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
2. 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
3. 多态不能调用“只在子类存在但在父类不存在”的方法;
4. 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
接口和抽象类的异同点
- 共同点:*
- 都不能实例化;
- 都可以包含抽象方法;
- 都可以有默认实现的方法
区别: - 接口主要用于对类的行为进行约束, 实现了某个接口就得有对应的行为。抽象类主要是用于代码复用,强调的是所属关系。
- 一个类只能继承一个类,但是可以实现多个接口。
- 接口中的成员变量只能是
public static final
类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。
深拷贝、浅拷贝、引用拷贝
- 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
- 深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
- 引用拷贝:引用拷贝就是两个不同的引用指向同一个对象。
Object
Object常见方法
Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
/**
* native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
*/
public final native Class<?> getClass()
/**
* native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
*/
public native int hashCode()
/**
* 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
*/
public boolean equals(Object obj)
/**
* native 方法,用于创建并返回当前对象的一份拷贝。
*/
protected native Object clone() throws CloneNotSupportedException
/**
* 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
*/
public String toString()
/**
* native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
*/
public final native void notify()
/**
* native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
*/
public final native void notifyAll()
/**
* native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
*/
public final native void wait(long timeout) throws InterruptedException
/**
* 多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。。
*/
public final void wait(long timeout, int nanos) throws InterruptedException
/**
* 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
*/
public final void wait() throws InterruptedException
/**
* 实例被垃圾回收器回收的时候触发的操作
*/
protected void finalize() throws Throwable { }
hashCode() 有什么用?
hashCode()
的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
为什么重写 equals() 时必须重写 hashCode() 方法?
因为两个相等的对象的 hashCode
值必须是相等。也就是说如果 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。如果重写 equals()
时没有重写 hashCode()
方法的话就可能会导致 equals
方法判断是相等的两个对象,hashCode
值却不相等。
String
String、StringBuffer、StringBuilder 的区别?
可变性
String 是不可变的。
StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。
线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
String为什么是不可变的
- 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
- String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。
String#equals() 和 Object#equals() 有何区别?
String
中的 equals
方法是被重写过的,比较的是 String
字符串的值是否相等。 Object
的 equals
方法是比较的对象的内存地址。
字符串常量池的作用了解吗?
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
异常
Exception和Error有什么区别
- Exception:程序本身可以处理的异常,可以通过
catch
来进行捕获。Exception
又可以分为Checked Exception
(受检查异常,必须处理)和Unchecked Exception
(不受检查异常,可以不处理)。 - Error:
Error
属于程序无法处理的错误,不建议使用catch
捕获。当这些错误发生时,JVM一般会选择终止线程。
Checked Exception 和 Unchecked Exception 有什么区别?
- Checked Exception 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch或者throws 关键字处理的话,就没办法通过编译。
- Unchecked Exception 即 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。主要为RuntimeException及其子类
Throwable类常用方法有哪些
String getMessage()
: 返回异常发生时的简要描述String toString()
: 返回异常发生时的详细信息String getLocalizedMessage()
: 返回异常对象的本地化信息。使用Throwable
的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()
返回的结果相同void printStackTrace()
: 在控制台上打印Throwable
对象封装的异常信息
try-catch-finally如何使用?
try
块:用于捕获异常。其后可接0至多个catch
块,如果没有catch
块,则必须跟一个finally
块catch
块:用于处理try捕获到的异常finally
块:无论是否捕获或处理异常,finally
块里的语句都会被执行。当在try
块或catch
块中遇到return
语句时,finally
语句块将在方法返回之前被执行。不要再finally
块中使用return
,会覆盖try
语句中的return
值。
finally块中的代码一定会被执行吗?
不一定,在以下情况finally中的代码不会被执行
- finally 执行之前虚拟机被终止运行的话,finally 中的代码就不会被执行。
- 程序所在的线程死亡。
- CPU被关闭
如何使用 try-with-resources 代替 try-catch-finally
- 适用范围(资源的定义): 任何实现 java.lang.AutoCloseable或者 java.io.Closeable 的对象,类似于InputStream、OutputStream、Scanner、PrintWriter等的资源
- 关闭资源和 finally 块的执行顺序: 在 try-with-resources 语句中,任何 catch 或 finally 块在声明的资源关闭后运行
- 通过使用分号分隔,可以在try-with-resources块中声明多个资源。
异常使用有哪些需要注意的地方?
- 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
- 抛出的异常信息一定要有意义。建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出NumberFormatException而不是其父类IllegalArgumentException。
- 使用日志打印异常之后就不要再抛出异常了(两者不要同时存在一段代码逻辑中)。
泛型
什么是泛型?有什么作用?
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如
ArrayList<Person> persons = new ArrayList<Person>()
这行代码就指明了该 ArrayList 对象只能传入 Person 对象,如果传入其他类型的对象就会报错。
泛型的使用方式有哪几种?
- 泛型类
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
- 泛型接口
public interface Generator<T> {
public T method();
}
- 泛型方法
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
反射
什么是反射
反射赋予了运行时分析类以及执行类中方法的能力。通过反射可以获取任意一个类的所有属性和方法。
反射的优缺点
- 优点:使代码更加灵活,为各种框架提供开箱即用的功能提供便利
- 缺点:增加安全问题,比如可以无视泛型参数的安全检查;会带来一定的性能,但总体影响不大
反射的使用场景
- 框架中动态代理的实现
- 注释的实现使用到反射
注解
何为注解
注解是一种特殊的注释,主要用于修饰类、方法或者变量,提供木屑信息供程序在编译或运行时使用。
注解的解析方法有哪几种?
- 编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用@Override 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
- 运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的 @Value、@Component)都是通过反射来进行处理的。