Java基础常见面试题总结

文章目录

主要参考:JavaGuide小林coding,同时加上网上搜索整理和个人理解总结

1 基础概念

1.1 说一下Java的特点

  • 平台无关性:Java的“一次编译,到处运行”哲学是其最大的特点之一。Java编译器将源代码编译成字节码(bytecode),该字节码可以在任何安装了Java虚拟机(JVM)的系统上运行。
  • 面向对象:Java是一门严格的面向对象编程语言,几乎一切都是对象。面向对象编程(OOP)特性使得代码更易于维护和重用,包括类(class)、对象(object)、继承(inheritance)、多态(polymorphism)、抽象(abstraction)和封装(encapsulation)。
  • 内存管理:Java有自己的垃圾回收机制,自动管理内存和回收不再使用的对象。这样,开发者不需要手动管理内存,从而减少内存泄漏和其他内存相关的问题。

1.2 JVM vs JDK vs JRE🔥

JDK(Java Development Kit)是Java开发工具包。它包含了 JRE,同时还包含了编译 java 源码的编译器 javac 以及一些其他工具比如 javadoc(文档注释工具)、jdb(调试器)、jconsole(基于 JMX 的可视化监控⼯具)、javap(反编译工具)等等。

JRE(Java Runtime Environment) 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)。

Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现,目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,到处运行”的关键所在。

JDK 包含 JRE

1.3 什么是字节码?采用字节码的好处是什么?

JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以, Java 程序运行时相对来说还是高效的,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。

1.4 为什么说 Java 语言“编译与解释并存”?

我们可以将高级编程语言按照程序的执行方式分为两种:

  • 编译型编译型语言 会通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
  • 解释型解释型语言会通过解释器一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。

为什么说 Java 语言“编译与解释并存”?

这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class 文件),这种字节码必须由 Java 解释器来解释执行。

1.5 Java 和 C++ 的区别?

虽然,Java 和 C++ 都是面向对象的语言,都支持封装、继承和多态,但是,它们还是有挺多不相同的地方:

  • Java 不提供指针来直接访问内存,程序内存更加安全
  • Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
  • Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。
  • C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。
  • ……

1.6 Java 语言关键字有哪些?

分类关键字
访问控制privateprotectedpublic
类,方法和变量修饰符abstractclassextendsfinalimplementsinterfacenative
newstaticstrictfpsynchronizedtransientvolatileenum
程序控制breakcontinuereturndowhileifelse
forinstanceofswitchcasedefaultassert
错误处理trycatchthrowthrowsfinally
包相关importpackage
基本类型booleanbytechardoublefloatintlong
short
变量引用superthisvoid
保留字gotoconst

1.7 移位运算符

  • << :左移运算符,向左移若干位,高位丢弃,低位补零。x << 1,相当于 x 乘以 2(不溢出的情况下)。
  • >> :带符号右移,向右移若干位,高位补符号位,低位丢弃。正数高位补 0,负数高位补 1。x >> 1,相当于 x 除以 2。
  • >>> :无符号右移,忽略符号位,空位都以 0 补齐。

由于 doublefloat 在二进制中的表现比较特殊,因此不能来进行移位操作。

移位操作符实际上支持的类型只有intlong,编译器在对shortbytechar类型进行移位前,都会将其转换为int类型再操作。

如果移位的位数超过数值所占有的位数会怎样?

当 int 类型左移/右移位数大于等于 32 位操作时,会先求余(%)后再进行左移/右移操作。也就是说:x<<42等同于x<<10x>>42等同于x>>10x >>>42等同于x >>> 10

1.8 Java 中的几种基本数据类型了解么?🔥

基本类型位数字节默认值取值范围包装类
byte810-128(-2^7) ~ 127(2^7 - 1)Byte
short1620-2^15 ~ 2^15 - 1Short
int3240-2^31~ 2^31 - 1Integer
long6480L-2^63 ~ 2^63 -1Long
char162‘u0000’0 ~ 2^16 - 1Character
float3240f1.4E-45 ~ 3.4028235E38Float
double6480d4.9E-324 ~ 1.7976931348623157E308Double
boolean1falsetrue、falseBoolean

对于 boolean 所占位数,官方文档未明确定义,它依赖于 JVM 厂商的具体实现。

注: -128 的二进制为 1000 0000

1.9 基本类型和包装类型的区别?

  • 用途:除了定义一些常量和局部变量之外,在其他地方如方法参数、对象属性中很少会使用基本类型来定义变量。并且,包装类型可用于泛型,而基本类型不可以。
  • 存储方式:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量存放在 Java 虚拟机的堆中。包装类型属于对象类型,都存在于堆中。
  • 占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。
  • 默认值:成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null
  • 比较方式:对于基本数据类型来说,== 比较的是值。对于包装数据类型来说,== 比较的是对象的内存地址。

为什么说是几乎所有对象实例都存在于堆中呢?

因为 HotSpot 虚拟机引入了 JIT 优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存

⚠️ 注意:基本数据类型存放在栈中是一个常见的误区!

基本数据类型的存储位置取决于它们的作用域和声明方式。如果它们是局部变量,那么它们会存放在栈中;如果它们是成员变量,那么它们会存放在堆中。

1.10 包装类型的缓存机制了解么?

Byte, Short, Integer, Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。两种浮点数类型的包装类 Float, Double 并没有实现缓存机制。

如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。

Integer i1 = 40; // 缓存中的对象
Integer i2 = new Integer(40); // 创建一个新的对象
System.out.println(i1==i2); // false

记住:所有整型包装类对象之间值的比较,全部使用 equals 方法比较

1.11 自动装箱与拆箱了解吗?原理是什么?

  • 装箱:将基本类型用它们对应的引用类型包装起来;
  • 拆箱:将包装类型转换为基本数据类型;
Integer i = 10;  //装箱  等价于 `Integer i = Integer.valueOf(10)`
int n = i;   //拆箱  等价于 `int n = i.intValue()`;

注意:如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。

1.12 为什么浮点数运算的时候会有精度丢失的风险?

float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999905
System.out.println(a == b);// false

计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。

// 十进制下的 0.2 就没办法精确转换成二进制小数:
// 0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止,
// 在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0(发生循环)
...

1.13 如何解决浮点数运算的精度丢失问题?

BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal x = a.subtract(b);

1.14 超过 long 整型的数据应该如何表示?

long l = Long.MAX_VALUE; // long 整型是最大的整数类型
System.out.println(l + 1); // -9223372036854775808
System.out.println(l + 1 == Long.MIN_VALUE); // true

BigInteger 内部使用 int[] 数组来存储任意大小的整形数据。但相对于常规整数类型的运算来说,BigInteger 运算的效率会相对较低。

1.15 成员变量与局部变量的区别?

  • 语法形式:成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
  • 存储方式:如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,否则是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
  • 生存时间:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
  • 默认值:成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

为什么成员变量有默认值?

  1. 没有默认值,变量存储的是内存地址对应的任意随机值,程序读取该值运行会出现意外。

  2. 默认值有两种设置方式:手动和自动,根据第一点,没有手动赋值一定要自动赋值。成员变量在运行时可借助反射等方法手动赋值,而局部变量不行。

  3. 对于编译器(javac)来说,局部变量没赋值很好判断,可以直接报错。而成员变量可能是运行时赋值,无法判断,误报“没默认值”又会影响用户体验,所以采用自动赋默认值。

1.16 静态变量有什么作用?

静态变量也就是被 static 关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。

静态变量是通过类名来访问的,通常情况下,静态变量会被 final 关键字修饰成为常量。public static final int constantVar = 0;

1.17 字符型常量和字符串常量的区别?

  • 形式 : 字符常量是单引号引起的一个字符;字符串常量是双引号引起的 0 个或若干个字符。
  • 含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。
  • 占内存大小:字符常量只占 2 个字节; 字符串常量占若干个字节。

⚠️ 注意 char 在 Java 中占两个字节。

public class StringExample {
    // 字符型常量 占 2 字节
    public static final char LETTER_A = 'A'; 
    // 字符串常量 占 5 字节
    public static final String GREETING_MESSAGE = "Hello";
}

1.18 静态方法和实例方法有何不同?

  • 调用方式:调用静态方法时,可以使用 类名.方法名 ,也可以使用 对象.方法名 ,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象

  • 访问类成员是否存在限制:静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。

1.19 静态方法为什么不能调用非静态成员?

  1. 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
  2. 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。

1.20 重载和重写有什么区别?🔥

重载发生在同一个类中(或者父类和子类之间),方法名必须相同,它们具有不同的参数列表(参数类型、参数个数或参数顺序不同),编译器根据调用时的参数类型来决定调用哪个方法。

重写指子类可以重新定义父类中的方法,通过@override注解来明确表示这是对父类方法的重写。方法的重写要遵循“两同两小一大”

  1. 方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
  2. 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
  3. 构造方法无法被重写(构造方法要和类名一致)

关于 重写的返回值类型 ,如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。

1.21 什么是可变长参数?

可变长参数就是允许在调用方法时传入不定长度的参数。比如下面的这个 method1 方法就可以接受 0 个或者多个参数。

public static void method1(String... args) {}

可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数,Java 的可变参数编译后实际会被转换成一个数组。

遇到方法重载的情况会优先匹配固定参数的方法,因为固定参数的方法匹配度更高

1.22 面向对象和面向过程的区别

  • 面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
  • 面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。面向对象开发的程序一般更易维护、易复用、易扩展。

1.23 对象实体与对象引用有何不同?

new 创建对象实例。一个对象引用可以指向 0 个或 1 个对象,一个对象实体可以有 n 个引用指向它。

Object obj = new Object(); 中,等号左边的 obj 是对象引用,它是一个变量,用来存储对象在内存中的地址,对象引用存放在栈内存中。而等号右边的 new Object() 是创建对象实例的过程,对象实例在堆内存中。

1.24 如果类没有声明构造方法能正确执行吗?

构造方法是一种特殊的方法,主要作用是完成对象的初始化工作。

如果一个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法也会有默认的无参的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会添加默认的无参的构造方法了。

这也是为什么我们在创建对象的时候后面要加一个括号(调用无参的构造方法)。

如果我们重载了有参的构造方法,记得也要把无参的构造方法写出来

1.25 构造方法有哪些特点?是否可被 override?

  • 名字与类名相同。
  • 没有返回值,但不能用 void 声明构造函数。
  • 生成类的对象时自动执行,无需调用。

构造方法不能被 override(重写),但是可以 overload(重载),所以一个类中可能有多个构造函数。

2 面向对象基础

2.1 面向对象三大特征

面向对象将现实世界中的事物抽象为对象,对象具有属性(字段)和行为(方法)。面向对象编程的设计思想是以对象为中心,通过对象之间的交互来完成程序的功能,具有灵活性和可扩展性,通过封装和继承可以更好地应对需求变化。

封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。

继承是实现代码复用的重要手段,通过extends实现类的继承,实现继承的类被称为子类,被继承的类称为父类;

  1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有
  2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
  3. 子类可以用自己的方式实现父类的方法。

多态的实现离不开继承,我们可以将参数的类型定义为父类型。在调用程序时根据实际情况,传入该父类型的某个子类型的实例,这样就实现了多态。

多态的特点:

  • 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
  • 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
  • 多态不能调用“只在子类存在但在父类不存在”的方法;
  • 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
Animal animal1 = new Dog();
animal1.sound(); // 输出:汪汪汪

2.2 面向对象的设计原则你知道有哪些吗

  • 单一职责原则(SRP):一个类应该只有一个引起它变化的原因,即一个类应该只负责一项职责。例子:考虑一个员工类,它应该只负责管理员工信息,而不应负责其他无关工作。
  • 开放封闭原则(OCP):软件实体应该对扩展开放,对修改封闭。例子:通过制定接口来实现这一原则,比如定义一个图形类,然后让不同类型的图形继承这个类,而不需要修改图形类本身。
  • 里氏替换原则(LSP):子类对象应该能够替换掉所有父类对象。例子:一个正方形是一个矩形,但如果修改一个矩形的高度和宽度时,正方形的行为应该如何改变就是一个违反里氏替换原则的例子。
  • 接口隔离原则(ISP):客户端不应该依赖那些它不需要的接口,即接口应该小而专。例子:通过接口抽象层来实现底层和高层模块之间的解耦,比如使用依赖注入。
  • 依赖倒置原则(DIP):高层模块不应该依赖低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。例子:如果一个公司类包含部门类,应该考虑使用合成/聚合关系,而不是将公司类继承自部门类。
  • 最少知识原则 (Law of Demeter):一个对象应当对其他对象有最少的了解,只与其直接的朋友交互。

2.3 接口和抽象类有什么共同点和区别?🔥

共同点

  • 都不能被实例化。
  • 都可以包含抽象方法。
  • 都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。

区别

  • 特点:抽象类用于描述类的共同特性和行为,可以有成员变量、构造方法和具体方法,适用于有明显继承关系的场景;接口用于定义行为规范,可以多实现,只能有常量和抽象方法(Java 8 以后可以有默认方法和静态方法),适用于定义类的能力或功能。
  • 实现方式:实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
  • 方法方式:接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
  • 访问修饰符:接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。

2.4 深拷贝和浅拷贝的区别🔥

  • 浅拷贝:浅拷贝只是创建一个新的对象,然后将原对象的字段值复制到新对象中,但如果原对象内部有引用类型的字段,只是将引用复制到新对象中,两个对象指向的是同一个引用对象。
  • 深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
    • 实现 Cloneable 接口并重写 clone() 方法
    • 使用序列化和反序列化
    • 手动递归复制
  • 引用拷贝:两个不同的引用指向同一个对象。

浅拷贝、深拷贝、引用拷贝示意图

2.5 == 和 equals() 的区别🔥

  • 对于基本数据类型来说,== 比较的是值。
  • 对于引用数据类型来说,== 比较的是对象的内存地址。

equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals() 方法存在于 Object 类中,因此所有的类都有 equals() 方法。

  • 类没有重写 equals() 方法:通过 equals() 比较该类的两个对象时,等价于通过 == 比较这两个对象。
  • 类重写了 equals() 方法:一般我们都重写 equals() 方法来比较两个对象中的属性是否相等;若它们的属性相等,则认为两个对象相等。
    • 例如 String 中的 equals 方法是被重写过的,比较的是对象的值。

2.6 hashCode() 有什么用?

hashCode() 的作用是获取哈希码,也称为散列码。

hashCode() 定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。ObjecthashCode() 方法是本地方法,也就是用C++ 实现的。

2.7 为什么 JDK 要同时提供 hashCode 和 equals?

hashCode()equals() 都是用于比较两个对象是否相等。

在一些集合(如 HashMap)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高, hashCode 帮助我们大大缩小查找成本。

添加元素进 HashMap 的过程,如果多个对象有同样的 hashCode ,它会继续使用 equals() 来判断是否真的相同。

如果两个对象的 hashCode 值相等,那这两个对象不一定相等(哈希碰撞),并且 equals() 方法也返回 true,我们才认为这两个对象相等。

2.8 为什么重写 equals() 时必须重写 hashCode() 方法?

如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。

如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。在 HashMap 中可能会出现重复键,即使这些键在逻辑上是相等的,因为它们的哈希码不同,导致 HashMap 认为它们是不同的键。

2.9 String、StringBuffer、StringBuilder 的区别?

可变性

String 是不可变的。StringBuilderStringBuffer 都是可变的。

线程安全性

  • String 中的对象是不可变的,也就可以理解为常量,线程安全。
  • StringBuffer 对方法加了同步锁,所以是线程安全的。
  • StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

性能

  • 每次对 String 类型进行改变的时候,都会生成一个新的 String 对象。
  • StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象。
  • 相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得很少的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

  1. 操作少量的数据: 适用 String
  2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

2.10 String 为什么是不可变的?🔥

  1. 保存字符串的数组被 final 修饰且为私有的,并且 String 类没有提供/暴露修改这个字符串的方法。
  2. String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 的不可变。

2.11 15.字符串拼接用“+” 还是 StringBuilder?

Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。

字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象。

在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器会创建过多的 StringBuilder 对象

使用 “+” 进行字符串拼接会产生大量的临时对象的问题在 JDK9 中得到了解决。在 JDK9 当中,字符串相加 “+” 改为了用动态方法 makeConcatWithConstants() 来实现。这也意味着 JDK 9 之后,你可以放心使用“+” 进行字符串拼接了。

2.12 字符串常量池的作用了解吗?

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true

2.13 String s1 = new String(“abc”);这句话创建了几个字符串对象?

会创建 1 或 2 个字符串对象。

1、如果字符串常量池中不存在字符串对象“abc”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。

2、如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。

2.14 String 的 intern 方法有什么作用?

String.intern() 是一个 native(本地)方法,其作用是将字符串添加到字符串常量池中,并返回字符串常量池中的引用。

2.15 String 类型的变量和常量做“+”运算时发生了什么?

String str3 = "str" + "ing";
String str5 = "string";
System.out.println(str3 == str5); //true

对于 String str3 = "str" + "ing"; 编译器会优化成 String str3 = "string";

并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以。

String str1 = "str";  String str2 = "ing";
String str4 = str1 + str2;
String str3 = "str" + "ing";
System.out.println(str3 == str4); //false

引用的值在程序编译期是无法确定的,编译器无法对其进行优化。

对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象。

不过,字符串使用 final 关键字声明之后,可以让编译器当做常量来处理。

3 基础进阶

3.1 Java 异常层次结构图

Java 异常类层次结构图

3.2 Exception 和 Error 有什么区别?

  • Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
  • ErrorError 属于程序无法处理的错误。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存溢出错误(OutOfMemoryError)等。

3.3 Checked Exception 和 Unchecked Exception 有什么区别?🔥

Checked Exception受检查异常,Java 代码在编译过程中,如果受检查异常没有被 catch 或者 throws 关键字处理的话,就没办法通过编译。常见的受检查异常有:IO 相关的异常、ClassNotFoundExceptionSQLException

Unchecked Exception不受检查异常 ,Java 代码在编译过程中,我们即使不处理不受检查异常也可以正常通过编译。

RuntimeException 及其子类都统称为非受检查异常,常见的有(建议记下来,日常开发中会经常用到):

  • NullPointerException (空指针错误)
  • IllegalArgumentException (参数错误比如方法入参类型错误)
  • NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException 的子类)
  • ArrayIndexOutOfBoundsException(数组越界错误)
  • ClassCastException(类型转换错误)
  • ArithmeticException(算术错误)
  • SecurityException (安全错误比如权限不够)
  • UnsupportedOperationException (不支持的操作错误比如重复创建同一用户)
  • ……

3.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 返回值。

try{return "a"} fianlly{return "b"}	  // 返回 "b"

3.5 finally 中的代码一定会执行吗?

不一定的!比如说 finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。

另外,程序所在的线程死亡或者关闭 CPU 的情况下,finally 块的代码也不会被执行:

3.6 什么是泛型和泛型擦除?

泛型可以明确集合接受哪些对象类型,编译器可以对泛型参数进行检测,在编译阶段告知是否插入类型错误的对象,可以增强代码的可读性以及稳定性。

Java泛型是伪泛型,因为Java代码在编译阶段,所有的泛型信息会被擦除,Java的泛型基本上都是在编辑器这个层次上实现的,在生成的字节码文件中是不包含泛型信息的,使用泛型的时候加上的类型,在编译阶段会被擦除掉,这个过程称为泛型擦除。

public class Generic<T>{} // 泛型类
public interface Generator<T> {} // 泛型接口
public static < E > void printArray( E[] inputArray ){} // 泛型方法

3.7 项目中哪里用到了泛型?

  • 自定义通用返回结果 Result<T> 通过参数 T 可根据具体的返回类型动态指定结果的数据类型
  • 定义 Excel 处理类 ExcelUtil<T> 用于动态指定 Excel 导出的数据类型
  • 构建集合工具类(参考 Collections 中的 sort, binarySearch 方法)。
  • ……

3.8 什么是反射,优缺点?

反射赋予了我们在运行时分析类以及执行类中方法的能力。通过反射可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。反射特性:运行时类信息访问、动态创建对象、动态方法调用、访问和修改字段值。

反射可以让我们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。

不过,反射让我们在运行时有了分析操作类的能力的同时,也增加了安全问题,比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。

3.9 反射的应用场景?

像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。

这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。

另外,像 Java 中的一大利器 注解 的实现也用到了反射。

为什么你使用 Spring 的时候,一个 @Component 注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 @Value 注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?

这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。

3.10 何谓注解?

Annotation (注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。

注解本质是一个继承了 Annotation 的特殊接口,其具体实现类是Java运行时生成的动态代理类。

JDK 提供了很多内置的注解,同时,我们还可以自定义注解。

3.11 注解的解析方法有哪几种?

注解只有被解析之后才会生效,常见的解析方法有两种:

  • 编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用 @Override 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
  • 运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的 @Value@Component)都是通过反射来进行处理的。

3.12 什么是序列化?什么是反序列化?

如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。

  • 序列化:将数据结构或对象转换成二进制字节流的过程
  • 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程

对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。

下面是序列化和反序列化常见应用场景:

  • 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
  • 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
  • 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
  • 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。

综上:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。

3.13 序列化协议对应于 TCP/IP 4 层模型的哪一层?

OSI 七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据。这不就对应的是序列化和反序列化么?

因为,OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。

3.14 如果有些字段不想进行序列化怎么办?

对于不想进行序列化的变量,使用 transient 关键字修饰。

transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。

关于 transient 还有几点注意:

  • transient 只能修饰变量,不能修饰类和方法。
  • transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0
  • static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化。

3.15 常见序列化协议有哪些?

JDK 自带的序列化方式一般不会用,因为序列化效率低并且存在安全问题。比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。

像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。

3.16 为什么不推荐使用 JDK 自带的序列化?

我们很少或者说几乎不会直接使用 JDK 自带的序列化方式,主要原因有下面这些原因:

  • 不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。
  • 性能差:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。
  • 存在安全问题:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。

3.17 Java IO 流了解吗?

IO 即 Input/Output,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。

Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。

  • InputStream / Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
  • OutputStream / Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

3.18 I/O 流为什么要分为字节流和字符流呢?🔥

问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?

  • 如果我们不知道编码类型的话,使用字节流的过程中很容易出现乱码问题
  • 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值