第一部分
1 java 语言有哪些特点?
- 面向对象(封装,继承,多态);
- 平台无关性,一次编译到处运行;
- Java 语言天生支持多线程;
- 可靠性,具备异常处理和自动内存管理机制;
- 安全性,Java 提供了很多安全机制,比如访问权限修饰符、限制程序直接访问操作系统资源;
- 高效性,通过JIT即时编译等优化,Java 语言的运行效率还是很高的;
- 解释与编译并存;
2 Java SE 和 Java EE
Java SE 是 Java 标准版,可以用于构建桌面应用程序或简单的服务器应用程序。
Java EE 是 Java 企业版,更适合开发复杂的企业级应用程序。
Java ME 是 Java 的微型版本, 主要用于开发嵌入式消费电子设备的应用程序。
3 JVM vs JDK vs JRE
Java 虚拟机是运行 Java 字节码的虚拟机。JVM 并不是只有一种,只要满足 JVM 规范,每个公司、组织或者个人都可以开发自己的专属 JVM。除了 HotSpot VM 外,还有 J9 VM、JRockit VM 等 虚拟机 。
JDK是功能齐全的 Java SDK,是提供给开发者使用,能够创建和编译 Java 程序的开发套件。
JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机、Java 基础类库。
从 JDK 9 开始,就不需要区分 JDK 和 JRE 的关系了,取而代之的是模块系统,JDK 被重新组织成 94 个模块 + jlink 工具 (用于生成自定义 Java 运行时镜像,镜像像仅包含给定应用程序所需的模块) 。并且,从 JDK 11 开始,Oracle 不再提供单独的 JRE 下载。
4 什么是字节码?采用字节码的好处是什么?
Java 文件通过 javac 编译后产生的代码叫做字节码,字节码存在以 .class 结尾的文件里,被称为字节码文件。
字节码不面向特定的处理器,只面向虚拟机,因此它编译后无需重新编译就可以在不同的操作系统上执行。并且通过字节码的方式一定上解决了传统型解释性语言效率低的问题,同时又保留了解释性语言可移植的特点。
5 为什么说 Java 语言“编译与解释并存”?
编译型:通过编译器一次性将代码编译成平台可执行代码,比如 C、C++,编译型语言的特点是执行速度快,开发效率低。
解释型:通过解释器,一句一句将代码解释为机器码后再执行,比如 Python、php、js,解释型语言的特点是开发开发效率高,执行速度较慢。
Java 需要先将 Java 代码编译成字节码,然后由虚拟机来解释执行字节码,所以它是编译与解释并存。
6 AOT 有什么优点?为什么不全部使用 AOT 呢?
AOT 是 Java 9 引入的一种新的编译模式,叫做提前编译。AOT 是在程序被执行前就将代码编译成机器码,属于静态编译,AOT 避免了 JIT 编译的预热开销,可以提高程序的启动速度,避免预热时间长,并且 AOT 还能减少内存占用,特别适合云原生场景。
7 既然 AOT 这么多优点,那为什么不全部使用这种编译方式呢?
JIT 与 AOT 各有优点, AOT 更适合当下的云原生场景,对微服务架构的支持也比较友好。除此之外,但是 AOT 编译无法支持 Java 的一些动态特性,如反射、动态代理、动态加载、JNI(Java Native Interface)等。
8 Oracle JDK vs OpenJDK
- 是否开源:OpenJDK 是一个参考模型并且是完全开源的,而 Oracle JDK 是基于 OpenJDK 实现的,并不是完全开源的。
- 是否免费:Oracle JDK 会提供免费版本,但一般有时间限制。JDK17 之后的版本可以免费分发和商用,但是仅有 3 年时间,3 年后无法免费商用。不过,JDK8u221 之前只要不升级可以无限期免费。OpenJDK 是完全免费的。
- 功能性:Oracle JDK 在 OpenJDK 的基础上添加了一些特有的功能和工具,比如 一些监控工具。不过,在 Java 11 之后,OracleJDK 和 OpenJDK 的功能基本一致,之前 OracleJDK 中的私有组件大多数也已经被捐赠给开源组织。
- 稳定性:OpenJDK 不提供 LTS 服务,而 OracleJDK 大概每三年都会推出一个 LTS 版进行长期支持。不过,很多公司都基于 OpenJDK 提供了对应的和 OracleJDK 周期相同的 LTS 版。因此,两者稳定性其实也是差不多的。
- 协议:Oracle JDK 使用 BCL/OTN 协议获得许可,而 OpenJDK 根据 GPL v2 许可获得许可。
9 既然 Oracle JDK 这么好,那为什么还要有 OpenJDK?
- OpenJDK 是开源的,开源意味着你可以对它根据你自己的需要进行修改、优化。
- OpenJDK 是商业免费的。虽然 Oracle JDK 也是商业免费(比如 JDK 8),但并不是所有版本都是免费的。
- OpenJDK 更新频率更快。Oracle JDK 一般是每 6 个月发布一个新版本,而 OpenJDK 一般是每 3 个月发布一个新版本。
基于以上这些原因,OpenJDK 还是有存在的必要的。
10 Oracle JDK 和 OpenJDK 如何选择?
建议选择 OpenJDK 或者基于 OpenJDK 的发行版,比如 AWS 的 Amazon Corretto,阿里巴巴的 Alibaba Dragonwell。
11 标识符和关键字的区别是什么?
简单来说,标识符就是一个名字,比如:类名、变量名、方法名等。
有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这些特殊的标识符就是关键字。
12 Java 语言关键字有哪些?
- 访问控制:private、protected、public;
- 类,方法和变量修饰符:abstract、class、extends、final、implements、interface、native;
- 程序控制:break、continue、return、do、while、if、else、for;
- 异常处理:try、catch、throw、throws、finally;
- 包相关:import、package;
- 基本类型:boolean、byte、char、double、float、int、long、short;
- 变量引用:super、this、void;
- 保留字:goto、const;
13 自增自减运算符
符号在前就先加减再赋值(会把后面的的值自增后,赋值给变量),符号在后就先赋值再加减(会先把值赋值给变量,然后再自增)。
// 例子一:
int a = 1;
int b = 0;
b = ++a;
System.out.println(a); // 输出:2
System.out.println(b); // 输出:2
// 例子二:
int a = 1;
int b = 0;
b = a++;
System.out.println(a); // 输出:2
System.out.println(b); // 输出:1
14 continue、break 和 return 的区别是什么?
- continue:指跳出当前的这一次循环,继续下一次循环。
- break:指跳出当前层循环的整个循环体,继续执行循环下面的语句。
- return 用于跳出所在方法,结束该方法的运行。分为返回值和不返回值两种情况。
15 Java 中的几种基本数据类型了解么?
- 6 种数字类型:
- 4 种整数型:byte、short、int、long
- 2 种浮点型:float、double
- 1 种字符类型:char
- 1 种布尔型:boolean
16 八种基本类型都有对应的包装类
Byte、Short、Integer、Long、Float、Double、Character、Boolean。
17 基本类型和包装类型的区别?
- 用途:除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且,包装类型可用于泛型,而基本类型不可以。
- 存储方式:基本数据类型的局部变量存放在局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
- 占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。
- 默认值:成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null。
- 比较方式:对于基本数据类型来说,== 比较的是值。对于包装数据类型来说,== 比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用 equals() 方法。
18 为什么说是几乎所有对象实例都存在于堆中呢?
这是因为 HotSpot 虚拟机引入了 JIT 优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存。
19 基本数据类型存放在栈中对吗?
成员变量存放在堆中,静态成员在 jdk 7 之前存放在永久代,之后也存放在堆中,局部变量存放在栈中。
20 包装类型的缓存机制了解么?
- Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。
- 浮点数 float 和 double 没有实现缓存。
- 对于强制 new 的包装类,会创建新的对象。所以 Integer a = 1; Integer b = new Integer(1); a == b 会返回 false。
21 自动装箱与拆箱了解吗?原理是什么?
- 装箱:将基本类型用它们对应的引用类型包装起来,装箱调用了包装类的 valueOf() 方法。
- 拆箱:将包装类型转换为基本数据类型,拆箱调用了 xxxValue() 方法。
- 如果频繁装拆箱会影响性能,比如在循环里对包装类进行计算,我们应该尽量避免不必要的装拆箱。
22 为什么浮点数运算的时候会有精度丢失的风险?
计算机在表示一个数字时宽度是有限的,无限循环的小数存储在计算机时只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。
23 如何解决浮点数运算的精度丢失问题?
BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。
24 超过 long 整型的数据应该如何表示?
基本数值类型都有一个表达范围,如果超过这个范围就会有数值溢出的风险。在 Java 中,64 位 long 整型是最大的整数类型。对于超过 long 的整数可以用 BigInteger 来表示,内部使用 int[] 数组来存储任意大小的整形数据。相对于常规整数类型的运算来说,BigInteger 运算的效率会相对较低。
25 成员变量与局部变量的区别?
- 语法形式:从语法形式上看成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是成员变量和局部变量都能被 final 所修饰。
- 存储方式:从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
- 生存时间:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
- 默认值:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
26 为什么成员变量有默认值?
- 从面向对象的角度来说,Java 设计者希望在对象被初始化后,对象的成员都是有一个确定值初始值的,而不是未知的。
- 从类型安全角度来说,在对象被初始化后,给每个成员分配一个类型确定的默认值,可以保证成员在被使用前都有一个类型正确的默认值,这对于强类型的语言来说很重要。
27 成员变量为什么没有默认值?
- 避免潜在的bug,比如一个方法参数如果有默认值,对于需要传入参数但是没有传入参数的方法仍然能调用成功,会导致潜在的bug。
- 避免资源浪费,局部变量如果在栈上分配之前就已经有默认值了,会占用不必要的内存空间。
28 静态变量有什么作用?
静态变量也就是被 static 关键字修饰的变量。它可以被类的所有实例共享,它们都共享同一份静态变量。也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。
29 字符型常量和字符串常量的区别?
- 形式: 字符常量是单引号引起的一个字符;字符串常量是双引号引起的 0 个或若干个字符。
- 含义: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。
- 占内存大小:字符常量只占 2 个字节; 字符串常量占若干个字节。
30 静态方法为什么不能调用非静态成员?
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
- 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
31 静态方法和实例方法有何不同?
- 调用方式:在外部调用静态方法时可以使用
类名.方法名
的方式,也可以使用对象.方法名
的方式,而实例方法只有后面这种方式。也就是说调用静态方法可以无需创建对象 。但是一般不建议使用对象.方法名
的方式来调用静态方法。这种方式非常容易造成混淆,静态方法不属于类的某个对象而是属于这个类。 - 访问类成员是否存在限制:静态方法在访问本类的成员时,只允许访问静态成员变量和静态方法,不允许访问实例成员变量和实例方法,而实例方法不存在这个限制。
32 重载和重写有什么区别?
重载:发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。综上重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。
重写:重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。方法的重写要遵循“两同两小一大”,“两同”即方法名相同、形参列表相同;“两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;“一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。构造方法无法被重写。
33 什么是可变长参数?
从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。可变参数只能作为函数的最后一个参数,遇到方法重载的情况会优先匹配固定参数方法,应为固定参数方法匹配度更高。
第二部分
1 面向对象和面向过程的区别?
面向过程的思维方式是分解问题为一系列步骤或函数。程序是由一组函数或子程序组成,通过调用它们来执行任务。
面向对象的思维方式是将问题领域抽象为对象,并分析对象之间的关系和相互作用。程序由对象组成,每个对象包含数据和行为(方法)。关注对象,以及对象之间如何协作。
2 创建一个对象用什么运算符?对象实体与对象引用有何不同?
new 运算符,new 创建对象实例。对象引用指向对象实例,对象存放在堆里,对象引用存放在栈中。一个对象引用可以指向 0 个或 1 个对象。一个对象可以有 n 个引用指向它。
3 对象的相等和引用相等的区别?
对象的相等一般比较的是内存中存放的内容是否相等。
引用相等一般比较的是他们指向的内存地址是否相等,如果对象引用相等,那么对象一定相等。
4 构造方法的作用是什么?如果一个类没有声明构造方法,该程序能正确执行吗?
- 构造方法是一种特殊的方法,主要作用是完成对象的初始化工作。
- 如果一个类没有声明构造方法,也会自动生成一个默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会添加默认的无参数的构造方法了。
5 构造方法有哪些特点?是否可被 override?
- 名字与类名相同。
- 没有返回值,但不能用 void 声明构造函数。
- 生成类的对象时自动执行,无需调用。
- 构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。
6 面向对象三大特征?
- 封装:封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
- 继承:一个类继承父类的数据和行为。
- 多态:表示一个对象具有多种的状态。
继承的特点:
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,
- 子类可以用自己的方式实现父类的方法(重写)。
多态的特点:
- 父类的引用指向子类的实例;
- 多态的对象和引用类型之间具有继承或实现的关系;
- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
- 多态不能调用“只在子类存在但在父类不存在”的方法;
- 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
7 接口和抽象类有什么共同点和区别?
共同点:
- 都不能被实例化;
- 都可以包含抽象方法;
- 都可以有默认实现的方法。
区别:
- 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
- 一个类只能继承一个类,但是可以实现多个接口。
- 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。
8 深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
浅拷贝:
- 创建一个新的对象。
- 复制非静态字段到新的对象中。
- 对于基本类型字段会直接复制值到新的对象中。
- 对于引用字段类型,会复制引用到新的对象中,如果新旧对象任一一方修改了引用对象的值,都会影响另一边。
深拷贝:
- 创建一个新的对象。
- 复制非静态字段到新的对象中。
- 对于基本类型字段会直接复制值到新的对象中。
- 对于引用字段类型,会将引用指向的对象进行复制,新对象会指新复制的对象。
9 那什么是引用拷贝呢?
引用拷贝就是两个不同的引用指向同一个对象。
10 Object 类的常见方法有哪些?
getClass()
, hashCode()
,equals(Object obj)
,clone()
,toString()
,notify()
,notifyAll()
,wait()
,finalize()
。
11 == 和 equals() 的区别?
- 对于基本数据类型来说,== 比较的是值。
- 对于引用数据类型来说,== 比较的是对象的内存地址。
- equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。
- equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。
12 equals() 方法存在两种使用情况?
- 类没有重写 equals()方法:通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object类equals()方法。
- 类重写了 equals()方法:一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
13 hashCode() 有什么用?
hashCode() 的作用是获取哈希码,哈希码的作用是确定该对象在哈希表中的索引位置。
14 为什么要有 hashCode?
- 在使用了哈希表的数据结构中,需要用 hashCode 确定它在哈希表中的位置。
- 当两个对象的 hashCode 相同时,说明他们在同一个哈希桶中,这时再根据 equals 方法判断是否真的相等。
15 那为什么 JDK 同时提供 hashCode() 和 equals() 这两个方法来判断对象是否相同呢?
hashCode 可以提高比较效率,当两个对象的 hashCode 相同时,我们才用会继续使用 equals() 来判断是否真的相同。这样会大大缩小查询成本。
16 那为什么不只提供 hashCode() 方法呢?
这是因为两个对象的hashCode 值相等并不代表两个对象就相等。
17 那为什么两个对象有相同的 hashCode 值,它们也不一定是相等的?
- 如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。
- 如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等。
- 如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。
18 为什么重写 equals() 时必须重写 hashCode() 方法?
- 因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
- 如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。
19 String、StringBuffer、StringBuilder 的区别?
可变性:
- String 是不可变的。
- StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。
线程安全性:
- String 中的对象是不可变的,也就可以理解为常量,线程安全。
- StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
- StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
使用总结:
- 操作少量的数据: 适用 String;
- 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder;
- 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer。
20 String 为什么是不可变的?
- 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
- String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。
21 Java 9 为何要将 String 的底层实现由 char[] 改成了 byte[]?
新版的 String 其实支持两个编码方案:Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。Latin-1 编码方案下,byte 占一个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间。
22 字符串拼接用“+” 还是 StringBuilder?
- 字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象,
- 不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象。
- 如果直接使用 StringBuilder 对象进行字符串拼接的话,就不会存在这个问题了。
- 使用 “+” 进行字符串拼接会产生大量的临时对象的问题在 JDK9 中得到了解决。在 JDK9 当中,字符串相加 “+” 改为了用动态方法 makeConcatWithConstants() 来实现,而不是大量的 StringBuilder 了。这意味着 JDK 9 之后,你可以放心使用“+” 进行字符串拼接了。
23 String#equals() 和 Object#equals() 有何区别?
String 中的 equals 方法是被重写过的,比较的是 String 字符串的值是否相等。 Object 的 equals 方法是比较的对象的内存地址。
24 字符串常量池的作用了解吗?
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
25 String s1 = new String("abc");这句话创建了几个字符串对象?
会创建 1 或 2 个字符串对象。
- 如果字符串常量池中不存在字符串对象“abc”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。
- 如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。
26 String#intern 方法有什么作用?
String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
// 在堆中创建字符串对象”Java“
// 将字符串对象”Java“的引用保存在字符串常量池中
String s1 = "Java";
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s2 = s1.intern();
// 会在堆中在单独创建一个字符串对象
String s3 = new String("Java");
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一个对象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的对象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一个对象
System.out.println(s1 == s4); //true
27 String 类型的变量和常量做“+”运算时发生了什么?
对于字符串不加 final 关键字拼接的情况(JDK1.8):
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
String str5 = "string";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
- 对于 str3,编译器会进行折叠优化,优化为 ”string“, 所以 str3 与 str5 是相同的,在常量池。
- 对于 str4,由于是变量,编译器无法进行推论,因此无法进行折叠优化,此时会使用StringBuilder 的 append() 拼接后得到一个新的 String 对象。
- 由于 equals 是对对象的值进行比较,这些对象的值都是相同的。
对于字符串使用 final 关键字声明之后,可以让编译器当做常量来处理:
final String str1 = "str";
final String str2 = "ing";
// 下面两个表达式其实是等价的
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// true
但是对于运行时才知道确切值的情况,也无法进行优化,比如有方法调用时:
final String str1 = "str";
final String str2 = getStr();
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 在堆上创建的新的对象
System.out.println(c == d);// false
public static String getStr() {
return "ing";
}
第三部分
1 Exception 和 Error 有什么区别?
在 Java 中,所有的异常都有一个共同的祖先 java.lang.Throwable 类。Throwable 类有两个重要的子类:
- Exception:程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
- Error:Error 属于程序无法处理的错误,不建议通过catch捕获。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
2 Checked Exception 和 Unchecked Exception 有什么区别?
- 受检查异常,Java 代码在编译过程中,如果受检查异常没有被 catch 或者 throws 关键字处理的话,就没办法通过编译。
- 除了RuntimeException及其子类以外,其他的 Exception 类及其子类都属于受检查异常。常见的受检查异常有:IO 相关的异常、ClassNotFoundException、SQLException等。
- 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
RuntimeException 及其子类都统称为非受检查异常,常见的有:NullPointerException、IllegalArgumentException、NumberFormatException、ArrayIndexOutOfBoundsException等。
3 Throwable 类常用方法有哪些?
- String getMessage(): 返回异常发生时的简要描述;
- String toString(): 返回异常发生时的详细信息;
- String getLocalizedMessage(): 返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage() 返回的结果相同;
- void printStackTrace(): 在控制台上打印 Throwable 对象封装的异常信息;
4 try-catch-finally 如何使用?
- try块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
- catch块:用于处理 try 捕获到的异常。
- finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。
- 不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。
5 finally 中的代码一定会执行吗?
- 正常执行,如果 try 块中的代码正常执行完毕,没有出现异常,那么 finally 块中的代码一定会执行。
- 出现异常,如果在 try 块或 catch 块中出现了异常,并且没有在代码中手动终止程序(如System.exit()),那么 finally 块中的代码也会执行。
- 在 try 中调用 System.exit() 如果在 try 块中调用了 System.exit() 方法,那么 finally 块中的代码将不会被执行,因为程序已经退出运行。
- 在 finally 块中有未被捕获的异常 如果在 finally 块中出现了未被捕获的异常,那么该异常会在 finally 块之后抛出, finally 块之后的代码将不会被执行。
- 其他情况,如程序所在线程死亡,cpu 被关闭等。
6 如何使用 try-with-resources 代替try-catch-finally?
- try-with-resources 适用于任何实现 java.lang.AutoCloseable 或者 java.io.Closeable 的对象。
- 在 try-with-resources 语句中,catch 或 finally 块在声明的资源关闭后运行。
- 当多个资源需要关闭时,try-with-resources 是按照申明顺序的倒序进行关闭资源。
7 异常使用有哪些需要注意的地方?
- 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
- 抛出的异常信息一定要有意义。
- 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出NumberFormatException而不是其父类IllegalArgumentException。
8 什么是泛型?有什么作用?
- 泛型是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
- 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
- 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
- 泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
9 泛型的使用方式有哪几种?
泛型一般有三种使用方式: 泛型类、泛型接口、泛型方法。
泛型常用的通配符有哪些?
- 常用的通配符为:K、V、?、T、E
- K V(key value)分别代表Java键值中的Key Value
- ?表示不确定的Java类型
- T(type)表示具体的一个Java类型
- E(element)代表Element
介绍一下泛型的上限和下限?
- 上限:?extends E:可以接收E类型或者E的子类型对象。
- 下限:?super E:可以接收E类型或者E的父类型对象。
- 上限什么时候用:往集合中添加元素时,既可以添加E类型对象,又可以添加E的子类型对象。为什么?因为取的时候,E类型既可以接收E类对象,又可以接收E的子类型对象。
- 下限什么时候用:当从集合中获取元素进行操作的时候,可以用当前元素的类型接收,也可以用当前元素的父类型接收。
10 项目中哪里用到了泛型?
- 自定义接口通用返回结果
- 构建集合工具类
11 什么是反射?
反射是 Java 的一个特性, 它允许在运行时查看对象所属类的结构信息, 包括其修饰符、属性、方法等, 并且可以动态调用对象的属性和方法。
12 反射的优缺点?
- 反射可以让我们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。
- 反射让我们在运行时有了分析操作类的能力的同时,也增加了安全问题,比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外反射的性能也要稍差点,不过对于框架来说实际是影响不大的。
13 反射的应用场景?
- Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
- 这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。
- Java 中的一大利器 注解 的实现也用到了反射。
14 什么是注解?
注解是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
15 注解的解析方法有哪几种?
- 编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用@Override 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
- 运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的 @Value、@Component)都是通过反射来进行处理的。
16 什么是spi?
spi 就是服务提供者接口,它是 java 中实现面向服务的设计模式的手段,是通过在 ClassPath 中查找指定的配置文件来获取服务的实现。
17 spi 的工作原理?
- 定义一个服务接口;
- 服务提供商实现这个服务接口;
- 服务提供商在配置目录中创建一个以服务接口命名的配置文件,将服务接口的实现类的全限定名写入其中;
- 服务启动时,通过读取配置文件发现所有服务提供商的实现,加载并实例化它们。
18 spi 的优缺点?
- 解耦:服务实现与服务接口是松散耦合的,只要符合约定,就可以插入新的实现;
- 扩展性:可以在运行时动态发现服务提供商的实现,实现架构的可扩展性;
- spi 需要遍历加载所有的实现类,不能做到按需加载,这样效率相对较低。
19 实现 spi 的步骤:
- 定义接口;
- 实现接口;
- 在 /resources/META-INF/services/ 目录下,添加一个以接口全限定名为名称的文件,内容是接口实现类的全限定名;
- 编写类加载代码,通过 ServiceLoader 加载实现类。
20 什么是序列化?什么是反序列化?
- 序列化:将数据结构或对象转换成二进制字节流的过程;
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程;
21 序列化和反序列化的常见应用场景?
- 对象在进行网络传输之前需要被序列化,接收到序列化的对象之后需要再进行反序列化,比如说 rpc 调用;
- 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
- 将对象存储到数据库之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
22 序列化协议对应于 TCP/IP 四层模型的哪一层?
四层分别为:
- 应用层
- 传输层
- 网络层
- 网络接口层
其中序列化和反序列化在应用层进行。
23 如果有些字段不想进行序列化怎么办?
对于不想进行序列化的变量,使用 transient 关键字修饰。
transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
关于 transient 还有几点注意:
- transient 只能修饰变量,不能修饰类和方法。
- transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0。
- static 变量因为不属于任何对象,所以无论有没有 transient 关键字修饰,均不会被序列化。
24 常见序列化协议有哪些?
- java 的序列化协议;
- google 的 Protobuf、ProtoStuff;
- meta 的 Thrift;
- Apache 的 Avro;
- json,广泛应用于服务端和浏览器之间的数据传输;
- XML,常用于 Web 服务中,如 SOAP。
25 为什么不推荐使用 JDK 自带的序列化?
- 不支持跨语言调用: 如果调用的是其他语言开发的服务的时候就不支持了;
- 性能差:相比于其他序列化框架性能更低,主要原因是采用了较为低效的编码方式,序列化之后的字节数组体积较大,导致传输成本加大。
- 存在安全问题:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。
26 什么是语法糖?
- 语法糖是指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性。
- jvm 并不识别语法糖,语法糖要被正确执行,必须要先通过编译器进行解糖,也就是把语法糖转换成 jvm 识别的基本语法。
27 java 中常见的语法糖?
泛型、自动装拆箱、变长参数、枚举、内部类、增强for循环,try-with-resources 语法。
28 常见的语法糖详解?
- switch 支持 String 与枚举。
switch 本身是支持基本类型的,比如 int 和 char,对于 int 直接进行数值比较,对于 char 类型则是比较 ascii 码。所以 switch 本身是只支持整形的,其他任何类型都有转换为整形比较。
switch 对于 String 的支持本质是通过 equals 和 hashCode 来实现的。
String str = "world";
String s;
switch((s = str).hashCode()){
case 99162322:
if(s.equals("hello"))
System.out.println("hello");
break;
default:
break;
}
泛型
java 需要在编译阶段把泛型擦除掉来解语法糖。自动装拆卸
装箱的过程调用的是包装类的 valueOf() 方法,拆箱调用的是 xxxValue() 方法。变长参数
变长参数的实质是在它会创建一个数组,把变长参数放到数组里,然后用数组来传递参数。枚举类
反编译后的代码可以看到,当我们使用enum来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。内部类
内部类又称为嵌套类,可以把内部类理解为外部类的一个普通成员。
内部类之所以也是语法糖,是因为它仅仅是一个编译时的概念,一个类里面定义了一个内部类,一旦编译成功,就会生成两个完全不同的.class文件了,分别是outer.class
和outer$inner.class
。所以内部类的名字完全可以和它的外部类名字相同。数值字面量
在 java 7 中,数值字面量,不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,目的就是方便阅读。
反编译后就是把删除了。也就是说编译器并不认识在数字字面量中的,需要在编译阶段把他去掉。增强for循环 for-each
for-each 的实现原理其实就是使用了普通的 for 循环和迭代器Iterator iterator = strList.iterator();
。try-with-resources
Java 7 开始,jdk 提供了一种更好的方式关闭资源,使用try-with-resources语句,可以不用手动关闭资源。
反编译后发现,是编译器自动添加了关闭资源的逻辑。
29 语法糖带来的哪些副作用?
- 当泛型遇到重载时,会发生编译不通过的问题,泛型擦除后,这两个方法的签名是一样的:
public void method(List<String> list) {
System.out.println("invoke method(List<String> list)");
}
public void method(List<Integer> list) {
System.out.println("invoke method(List<Integer> list)");
}
catch 块没有办法处理带有泛型的异常:
比如 jvm 没有办法区分MyException<String>
和MyException<Integer>
,因为异常是 jvm 在运行时进行处理的,此时泛型被擦除掉了。当泛型中包含静态变量:
下面的代码会输出 2,
public class StaticTest{
public static void main(String[] args){
GT<Integer> gti = new GT<Integer>();
gti.var=1;
GT<String> gts = new GT<String>();
gts.var=2;
System.out.println(gti.var);
}
}
class GT<T>{
public static int var = 0;
}
- 自动装拆箱:
由于 java 5 对整形 -128 到 127 进行了缓存,对于这个范围内的不是通过 new 创建的整形用 == 比较会返回 true,超过这个范围的整形比较会返回 false。
值传递
什么是形参?什么是实参?
实参:传递给函数/方法的确定的值。
形参:函数/方法参数的定义,不需要有确定的值。
值传递和引用传递?
- 值传递:方法接收的是实参值的拷贝,会创建副本。
- 引用传递:方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。
为什么 Java 只有值传递?
- 对于 java 的参数传递有两种情况,一种是基本类型参数,这种会对参数值进行拷贝传递,是值传递。
- 对于引用类型,会对引用类型的引用进行拷贝作为参数进行传递。此时至少有两个引用指向这个引用类型的对象。
- 由于引用传递是把实参的引用作为参数传递给方法,所以验证 java 对于引用类型是值传递的方法就是:使用两个相同的对象作为参数传递给方法,在方法内部将两个对象进行交换。最后分别在方法内部和调用方法之后打印这个这两个对象,会发现方法内部的对象引用交换了,但是没有影响到传递给方法的实参的引用。
public class Person {
private String name;
}
public static void main(String[] args) {
Person xiaoZhang = new Person("小张");
Person xiaoLi = new Person("小李");
swap(xiaoZhang, xiaoLi);
System.out.println("xiaoZhang:" + xiaoZhang.getName());
System.out.println("xiaoLi:" + xiaoLi.getName());
}
public static void swap(Person person1, Person person2) {
Person temp = person1;
person1 = person2;
person2 = temp;
System.out.println("person1:" + person1.getName());
System.out.println("person2:" + person2.getName());
}
/**
输出:
person1:小李
person2:小张
xiaoZhang:小张
xiaoLi:小李
*/
为什么 Java 不引入引用传递呢?
- 实现上的复杂性:引入引用传递需要对Java虚拟机、内存管理、垃圾回收等机制进行大量修改和调整,这增加了编译器和虚拟机的实现复杂度。
- 避免意外的副作用:引用传递可能在不当使用时导致意外修改原始参数值,从而引发一些难以发现和调试的Bug。Java设计者希望通过值传递来避免这些潜在的风险。
- 保持语言的一致性:即保持任何数据在被使用时都会获得其完整拷贝。
泛型&通配符详解
Java 代理模式详解
什么是代理模式?
- 代理模式就是在不修改原目标对象的前提下,使用代理对象来对代替对目标对象的访问。它的目标是在访问目标对象前后提供额外的操作。
- 代理模式又分为静态代理和动态代理。
静态代理
静态代理需要我们手动完成对目标对象的每个方法进行增强,实际应用场景较少,从 jvm 层面来说,静态代理是在编译期就将接口、实现类、代理类变成了一个实际的 Class 文件。
静态代理实现步骤?
- 定义一个接口和一个它的实现类;
- 定义一个代理类,也实现第一步的接口;
- 代理类持有第一步的实现类的对象,并对第一步的实现类的对象进行增强并且调用它需要增强的方法。
动态代理
- 动态代理不需要针对每个目标类单独创建一个代理类,从 jvm 角度来说,动态代理是在运行时生成字节码,并加载到 jvm 中。
- 动态代理主要有 jdk 动态代理和 cglib 动态代理。
jdk 动态代理的使用步骤
- 定义一个接口及其实现类。
- 自定义 InvocationHandler 并重写 invoke 方法,在 invoke 方法中,我们需要调用被代理类的方法及自定义逻辑。
- 通过 Proxy.newProxyInstance 方法创建代理对象。
- 调用第 3 步生成的代理对象的被调用方法,最终会调用到自定义的 InvocationHandler 的 invoke 方法。
jdk 动态代理的关键方法参数解释
-
InvocationHandler.invoke(Object proxy, Method method, Object[] args)
方法有三个参数, 我们会用Object result = method.invoke(target, args);
调用目标对象 target 的目标方法。其中 args 是参数。 -
Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
,其中 loader 是类加载器,可以通过target.getClass().getClassLoader()
获取 , interfaces 是目标对象实现的接口,可以通过target.getClass().getInterfaces()
获取,h 是我们自定义的 InvocationHandler。
下面是完整的示例:
public interface Test {
void send();
}
public class TestImpl implements Test {
@Override
public void send() {
System.out.println("invoke send");
}
}
public class TestInvokeHandler implements InvocationHandler {
private Test test;
public TestInvokeHandler(Test test) {
this.test = test;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before invoke");
Object result = method.invoke(test, args);
System.out.println("after invoke");
return result;
}
}
public static void main(String[] args) {
Test t = new TestImpl();
TestInvokeHandler invokeHandler = new TestInvokeHandler(t);
Test test = (Test) Proxy.newProxyInstance(t.getClass().getClassLoader(), t.getClass().getInterfaces(), invokeHandler);
test.send();
}
CGLIB 动态代理机制
由于 jdk 动态代理只能代理实现了接口的类,为了解决这个问题,可以用 cglib 来处理。cglib 是基于 asm 字节码生成库实现的,它允许我们在运行时对字节码进行修改和动态生成。cglib 是基于继承方式来实现代理的。在 Spring aop 中,如果目标对象实现了接口,默认会采用 jdk 动态代理,否则会采用 cglib 动态代理。
cglib 动态代理的使用步骤:
- 定义一个类;
- 自定义 MethodInterceptor 并重写 intercept 方法,用于增强被代理类的方法,它类似于 invoke 方法;
- 通过 Enhancer 类的 create() 创建代理类。
下面是完整的例子:
public class Test {
public void test() {
System.out.println("invoke");
}
}
public class TestMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("invoke before");
Object invoke = proxy.invoke(obj, args);
System.out.println("invoke after");
return invoke;
}
}
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setClassLoader(Test.class.getClassLoader());
enhancer.setSuperclass(Test.class);
enhancer.setCallback(new TestMethodInterceptor());
Test test = (Test)enhancer.create();
test.test();
}
JDK 动态代理和 CGLIB 动态代理对比?
- JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
- 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。
静态代理和动态代理的对比?
- 灵活性:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
- JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
由于有精度问题,浮点类型 float 和 double 类型的允许有以下注意点
- 对于浮点数,基本类型不能用 == 比较;
- 对于浮点数,包装类型不能用 equals 比较;
- 对于精确计算场景,浮点数可以用 BigDecimal 来进行比较;
- 不能用浮点数构造 BigDecimal 对象,需要把浮点数转成字符串后进行构造;
- 对于值相等的浮点数和整数 BigDecimal 进行 equals 比较时会返回 false,因为有精度问题,用 compareTo 比较时,会忽略精度,比如:'1' 和 '1.0'。compareTo 返回 0 就是相等, 如果前面的大就会返回 1, 如果前面的小,就会返回 -1;
- 使用 BigDecimal 进行减法运算时,尽量要指定精度和舍入模式,防止出现无限循环小数。