Skip to content

一、基础知识与概念

1、Java 语言的特点

  • 面向对象

    封装、继承、多态

  • 平台无关性

    Java 虚拟机实现平台无关性

  • 可靠性

    具备 异常处理 和 自动内存管理机制

  • 支持网络编程且生态丰富,很方便

2、Java SE vs Java EE

  • Java SE(Java Platform,Standard Edition): Java 平台标准版,Java 编程语言的基础,它包含了支持 Java 应用程序开发和运行的核心类库以及虚拟机等核心组件。Java SE 可以用于构建桌面应用程序或简单的服务器应用程序。

  • Java EE(Java Platform, Enterprise Edition ):Java 平台企业版,建立在 Java SE 的基础上,包含了支持企业级应用程序开发和部署的标准和规范(比如 Servlet、JSP、JDBC 等)。 Java EE 可以用于构建分布式、可移植、健壮、可伸缩和安全的服务端 Java 应用程序,例如 Web 应用程序。

  • Java ME(Java Platform,Micro Edition)。Java ME 是 Java 的微型版本,主要用于开发嵌入式消费电子设备的应用程序,例如手机、PDA、机顶盒、冰箱、空调等。Java ME 无需重点关注,知道有这个东西就好了,现在已经用不上。

3、JVM vs JDK vs JRE

3.1 JVM

Java 虚拟机(Java Virtual Machine, JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。

字节码 和 不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。

如下图所示,不同编程语言(Java、Groovy、Kotlin、JRuby、Clojure ...)通过各自的编译器编译成 .class 文件,并最终通过 JVM 在不同平台(Windows、Mac、Linux)上运行。

3.2 JDK

JDK(Java Development Kit)是一个功能齐全的 Java 开发工具包,供开发者使用,用于创建和编译 Java 程序。它包含了 JRE(Java Runtime Environment),以及编译器 javac 和其他工具,如 javadoc(文档生成器)、jdb(调试器)、jconsole(监控工具)、javap(反编译工具)等

3.3 JRE

JRE 是运行已编译 Java 程序所需的环境,主要包含以下两个部分:

  1. JVM : 也就是我们上面提到的 Java 虚拟机。
  2. Java 基础类库(Class Library):一组标准的类库,提供常用的功能和 API(如 I/O 操作、网络通信、数据结构等)

简单来说,JRE 只包含运行 Java 程序所需的环境和类库,而 JDK 不仅包含 JRE,还包括用于开发和调试 Java 程序的工具。

不过,从 JDK 9 开始,就不需要区分 JDK 和 JRE 的关系了,取而代之的是模块系统(JDK 被重新组织成 94 个模块)+ jlink 工具 (随 Java 9 一起发布的新命令行工具,用于生成自定义 Java 运行时映像,该映像仅包含给定应用程序所需的模块) 。并且,从 JDK 11 开始,Oracle 不再提供单独的 JRE 下载。

Java 应用可以通过新增的 jlink 工具,创建出只包含所依赖的 JDK 模块的自定义运行时镜像。这样可以极大的减少 Java 运行时环境的大小。

定制的、模块化的 Java 运行时映像有助于简化 Java 应用的部署和节省内存并增强安全性和可维护性。这对于满足现代应用程序架构的需求,如虚拟化、容器化、微服务和云原生开发,是非常重要的。

4、 字节码 与 JIT

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

需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点的字节码),所以后面引进了 JIT(Just in Time ) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将热点字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言

项目解释执行编译执行(JIT)
执行方式一条一条翻译边执行先全部翻译,再执行
执行效率较慢(重复解释)❌较快(机器码直接执行)✅
内存占用较少✅较多❌
启动速度快✅稍慢❌
开销较小✅较大❌
适合场景小程序、短生命周期代码长循环、频繁调用的热点代码

JDK、JRE、JVM、JIT 这四者的关系如下图所示。

5、为什么说Java是 编译和解释共存 的语言

(1)阶段1:编译阶段(源代码 → 字节码)

  • javac 编译器完成
  • .java 源代码编译成 .class 字节码文件
  • 这个阶段属于 静态编译(Static Compilation)
  • 和 C/C++ 把代码编译成 .exe 可执行文件很像,只不过 Java 编译成的是平台无关的中间码

(2)阶段2:运行阶段(字节码 → 机器码)

  • JVM 加载 .class 文件
  • 使用 解释器 把字节码逐条“翻译”为机器指令执行
  • 对频繁代码使用 JIT 编译器 转换成机器码,提高效率
  • 这属于 动态编译(Just-in-time Compilation)

总结:“**先编译,后解释(或编译)**执行”的混合模式

6、AOT 有什么优点?为什么不全部使用 AOT 呢?

JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation) 。和 JIT 不同的是,这种编译模式会在程序被执行前,就将字节码文件.class编译成机器码,属于静态编译(C、 C++,Rust,Go 等语言就是静态编译)。AOT 避免了 JIT 预热等各方面的开销,可以提高 Java 程序的启动速度,避免预热时间长。并且,AOT 还能减少内存占用和增强 Java 程序的安全性(AOT 编译后的代码不容易被反编译和修改),特别适合云原生场景。

可以看出,AOT 的主要优势在于启动时间、内存占用和打包体积。JIT 的主要优势在于具备更高的极限处理能力,可以降低请求的最大延迟。

(1)既然 AOT 这么多优点,那为什么不全部使用这种编译方式呢?

前面也对比过 JIT 与 AOT,两者各有优点,只能说 AOT 更适合当下的云原生场景,对微服务架构的支持也比较友好。

除此之外,AOT 编译无法支持 Java 的一些动态特性,如反射、动态代理、动态加载、JNI(Java Native Interface)等。很多框架和库(如 Spring、CGLIB)都用到了这些特性。如果只使用 AOT 编译,那就没办法使用这些框架和库了,或者说需要针对性地去做适配和优化。举个例子,CGLIB 动态代理使用的是 ASM 技术,而这种技术大致原理是运行时直接在内存中生成并加载修改后的字节码文件也就是 .class 文件,如果全部使用 AOT 提前编译,也就不能使用 ASM 技术了。为了支持类似的动态特性,所以选择使用 JIT 即时编译器。

7、Java 和 C++ 的区别?

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

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

二、基础语法

1、注释有哪几种形式?

Java 中的注释有三种:

  1. 单行注释:通常用于解释方法内某单行代码的作用。
  2. 多行注释:通常用于解释一段代码的作用。
  3. 文档注释:通常用于生成 Java 开发文档。

用的比较多的还是单行注释和文档注释,多行注释在实际开发中使用的相对较少。

2、标识符和关键字的区别是什么?

在我们编写程序的时候,需要大量地为程序、类、变量、方法等取名字,于是就有了 标识符 。简单来说, 标识符就是一个名字

有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这些特殊的标识符就是 关键字 。简单来说,关键字是被赋予特殊含义的标识符 。比如,在我们的日常生活中,如果我们想要开一家店,则要给这个店起一个名字,起的这个“名字”就叫标识符。但是我们店的名字不能叫“警察局”,因为“警察局”这个名字已经被赋予了特殊的含义,而“警察局”就是我们日常生活中的关键字。

3、Java 语言关键字有哪些?

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

default 这个关键字很特殊,既属于程序控制,也属于类,方法和变量修饰符,还属于访问控制。

  • 在程序控制中,当在 switch 中匹配不到任何情况时,可以使用 default 来编写默认匹配的情况。

  • 在类,方法和变量修饰符中,从 JDK8 开始引入了默认方法,可以使用 default 关键字来定义一个方法的默认实现。

    java
    interface MyInterface {
        // 抽象方法(必须由实现类实现)
        void abstractMethod();
      
        // 默认方法(可以被实现类继承或重写)
        default void defaultMethod() {
            System.out.println("这是接口中的默认方法");
        }
    }
      
    class MyClass implements MyInterface {
        @Override
        public void abstractMethod() {
            System.out.println("实现了抽象方法");
        }
      
        // 可选:重写默认方法
        @Override
        public void defaultMethod() {
            System.out.println("重写了默认方法");
        }
    }
      
    public class Main {
        public static void main(String[] args) {
            MyInterface obj = new MyClass();
            obj.abstractMethod();   // 输出:实现了抽象方法
            obj.defaultMethod();    // 输出:重写了默认方法
        }
    }
  • 在访问控制中,如果一个方法前没有任何修饰符,则默认会有一个修饰符 default,但是这个修饰符加上了就会报错。

⚠️ 注意:虽然 true, false, 和 null 看起来像关键字但实际上他们是字面值,同时你也不可以作为标识符来使用。

4、自增自减运算符

++-- 运算符可以放在变量之前,也可以放在变量之后:

  • 前缀形式(例如 ++a--a):先自增/自减变量的值,然后再使用该变量,例如,b = ++a 先将 a 增加 1,然后把增加后的值赋给 b
  • 后缀形式(例如 a++a--):先使用变量的当前值,然后再自增/自减变量的值。例如,b = a++ 先将 a 的当前值赋给 b,然后再将 a 增加 1。

5、移位运算符

(1)Java 中有三种移位运算符:

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

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

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

(2)使用移位运算符的主要原因

  1. 高效:移位运算符直接对应于处理器的移位指令,最常用于 快速乘以或除以 2 的幂次方 。现代处理器具有专门的硬件指令来执行这些移位操作,这些指令通常在一个时钟周期内完成。相比之下,乘法和除法等算术运算在硬件层面上需要更多的时钟周期来完成。
  2. 节省内存:通过移位操作,可以使用一个整数(如 intlong)来存储多个布尔值或标志位,从而节省内存。

(3)它还在以下方面发挥着重要作用:

  • 位字段管理:例如存储和操作多个布尔值。
  • 哈希算法和加密解密:通过移位和与、或等操作来混淆数据。
  • 数据压缩:例如霍夫曼编码通过移位运算符可以快速处理和操作二进制数据,以生成紧凑的压缩格式。
  • 数据校验:例如 CRC(循环冗余校验)通过移位和多项式除法生成和校验数据完整性。。
  • 内存对齐:通过移位操作,可以轻松计算和调整数据的对齐地址。

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

当 int 类型左移/右移位数大于等于 32 位操作时,会先求余(%)后再进行左移/右移操作。也就是说左移/右移 32 位相当于不进行移位操作(32%32=0),左移/右移 42 位相当于左移/右移 10 位(42%32=10)。当 long 类型进行左移/右移操作时,由于 long 对应的二进制是 64 位,因此求余操作的基数也变成了 64。

也就是说:x<<42等同于x<<10x>>42等同于x>>10x >>>42等同于x >>> 10

6、continue、break 和 return 的区别是什么?

在循环结构中,当循环条件不满足或者循环次数达到要求时,循环会正常结束。但是,有时候可能需要在循环的过程中,当发生了某种条件之后 ,提前终止循环,这就需要用到下面几个关键词:

  1. continue:指跳出当前的这一次循环,继续下一次循环。
  2. break:指跳出整个循环体,继续执行循环下面的语句。

return 用于跳出所在方法,结束该方法的运行。return 一般有两种用法:

  1. return;:直接使用 return 结束方法执行,用于没有返回值函数的方法
  2. return value;:return 一个特定值,用于有返回值函数的方法

三、基本数据类型

1、Java 共有8中基本数据类型

  • 6种数字类型:
    • 4种整形:byteshortintlong
    • 2种浮点:floatdouble
  • 1种字符类型:char
  • 1种布尔类型:boolean

这 8 种基本数据类型的默认值以及所占空间的大小如下:

基本类型位数字节数默认值取值范围
byte810-128 ~ 127
short1620-32768(-2^15) ~ 32767(2^15 - 1)
int3240-2147483648 ~ 2147483647
long6480l-9223372036854775808(-2^63) ~ 9223372036854775807(2^63 -1)
float3240f1.4E-45 ~ 3.4028235E38
double6480d4.9E-324 ~ 1.7976931348623157E308
char162'u0000'0 ~ 65535(2^16 - 1)
boolean1falsetrue、false

可以看到,像 byteshortintlong能表示的最大正数都减 1 了。这是为什么呢?这是因为在二进制补码表示法中,最高位是用来表示符号的(0 表示正数,1 表示负数),其余位表示数值部分。所以,如果我们要表示最大的正数,我们需要把除了最高位之外的所有位都设为 1。如果我们再加 1,就会导致溢出,变成一个负数。

对于 boolean,官方文档未明确定义,它依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1 位,但是实际中会考虑计算机高效存储因素。

另外,Java 的每种基本类型所占存储空间的大小不会像其他大多数语言那样随机器硬件架构的变化而变化。这种所占存储空间大小的不变性是 Java 程序比用其他大多数语言编写的程序更具可移植性的原因之一

⚠️注意:

  1. Java 里使用 long 类型的数据一定要在数值后面加上 L,否则将作为int整型解析。

  2. Java 里使用 float 类型的数据一定要在数值后面加上 f 或 F,否则将无法通过编译

    java
    float i = 1;     // 编译正常,通过 int -->  float 实现
    float j = 1.5;   // 编译报错, double !--> float  失败 
    double k = 1.5;  // 编译正常,浮点数默认 double
    System.out.println(i);

这八种基本类型都有对应的包装类分别为:ByteShortIntegerLongFloatDoubleCharacterBoolean

2、基本类型和包装类型的区别?

用途:除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且,包装类型可用于泛型,而基本类型不可以。

存储方式:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型【含引用类型,如String】的成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。

⚠️注意:

  • 对于静态成员变量(无论是基本类型还是引用类型)
    • 在 JDK 7 及之前版本:存放在 方法区(Method Area)
    • 在 JDK 8 及之后:方法区被废弃,由 元空间(Metaspace) 取代,静态变量保存在类元信息区域的静态变量表中,但本质仍与类信息绑定,而非堆。

占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。

默认值:成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null

⚠️⚠️比较方式:对于基本数据类型来说,== 比较的是值。对于包装数据类型来说,== 比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用 equals() 方法。

java
// 对于[-128, 127]中的值,JVM做了对象缓存
Integer a = 9;
Integer b = 9;
System.out.println(a == b);         // true
System.out.println(a.equals(b));    // true

// 超出[-128, 127]中的值,会创建新对象
Integer c = 900;
Integer d = 900;
System.out.println(a == b);         // false
System.out.println(a.equals(b));    // true

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

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

⚠️ 注意:基本数据类型存放在栈中是一个常见的误区! 基本数据类型 的存储位置取决于它们的作用域和声明方式。如果它们是局部变量,那么它们会存放在栈【局部变量】中;如果它们是成员变量,那么它们会存放在堆【成员变量】/方法区/元空间中【static】。

java
public class Test {
    // 成员变量,存放在堆中
    int a = 10;
    // 被 static 修饰的成员变量,JDK 1.7 及之前位于方法区,1.8 后存放于元空间,均不存放于堆中。
    // 变量属于类,不属于对象。
    static int b = 20;

    public void method() {
        // 局部变量,存放在栈中
        int c = 30;
        static int d = 40; // 编译错误,不能在方法中使用 static 修饰局部变量
    }
}

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

Java 基本数据类型的包装类型的大部分都用到了缓存机制来 提升性能

  • Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据

  • Character 创建了数值在 [0,127] 范围的缓存数据,

  • Boolean 直接返回 TRUE or FALSE

  • 两种浮点数类型的包装类 Float,Double没有 实现缓存机制

    java
    Integer i1 = 33;
    Integer i2 = 33;
    System.out.println(i1 == i2);// 输出 true
    
    Float i11 = 333f;
    Float i22 = 333f;
    System.out.println(i11 == i22);// 输出 false
    
    Double i3 = 1.2;
    Double i4 = 1.2;
    System.out.println(i3 == i4);// 输出 false
    
    Integer i1 = 40;	// 使用缓存对象,装箱,等价于 Integer i1=Integer.valueOf(40)
    Integer i2 = new Integer(40);	// 创建新对象
    System.out.println(i1==i2);	// 输出false

Integer,可以通过 JVM 参数 -XX:AutoBoxCacheMax=<size> 修改缓存上限,但不能修改下限 -128。实际使用时,并不建议设置过大的值,避免浪费内存,甚至是 OOM。

对于Byte,Short,Long ,Character 没有类似 -XX:AutoBoxCacheMax 参数可以修改,因此缓存范围是固定的,无法通过 JVM 参数调整。Boolean 则直接返回预定义的 TRUEFALSE 实例,没有缓存范围的概念。

Integer 缓存源码:

java
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static {
        // high value may be configured by property
        int h = 127;
    }
}

Character 缓存源码:

java
public static Character valueOf(char c) {
    if (c <= 127) { // must cache
      return CharacterCache.cache[(int)c];
    }
    return new Character(c);
}

private static class CharacterCache {
    private CharacterCache(){}
    static final Character cache[] = new Character[127 + 1];
    static {
        for (int i = 0; i < cache.length; i++)
            cache[i] = new Character((char)i);
    }

}

Boolean 缓存源码:

java
public static Boolean valueOf(boolean b) {
    return (b ? TRUE : FALSE);
}

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

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

什么是自动拆装箱?

  • 装箱:将基本类型用它们对应的引用类型包装起来;
  • 拆箱:将包装类型转换为基本数据类型;

举例:

java
Integer i = 10;  //装箱
int n = i;   //拆箱


Integer integer = Integer.valueOf(1);   // 装箱
int i = integer.intValue();             // 拆箱

上面这两行代码对应的字节码为:

java
   L1

    LINENUMBER 8 L1

    ALOAD 0

    BIPUSH 10

    INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;

    PUTFIELD AutoBoxTest.i : Ljava/lang/Integer;

   L2

    LINENUMBER 9 L2

    ALOAD 0

    ALOAD 0

    GETFIELD AutoBoxTest.i : Ljava/lang/Integer;

    INVOKEVIRTUAL java/lang/Integer.intValue ()I

    PUTFIELD AutoBoxTest.n : I

    RETURN

从字节码中,我们发现装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法。

因此,

  • Integer i = 10 等价于 Integer i = Integer.valueOf(10)
  • int n = i 等价于 int n = i.intValue();

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

java
private static long sum() {
    // 应该使用 long 而不是 Long
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++)
        sum += i;
    return sum;
}

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

这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。

java
float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.printf("%.9f",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(发生循环)
...

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

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

java
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
BigDecimal c = new BigDecimal("0.8");

BigDecimal x = a.subtract(c);
BigDecimal y = b.subtract(c);

System.out.println(x); /* 0.2 */
System.out.println(y); /* 0.20 */
// 比较内容,不是比较值
System.out.println(Objects.equals(x, y)); /* false */
// 比较值相等用相等compareTo,相等返回0
System.out.println(0 == x.compareTo(y)); /* true */

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

基本数值类型都有一个表达范围,如果超过这个范围就会有数值溢出的风险。

在 Java 中,64 位 long 整型是最大的整数类型。

java
long l = Long.MAX_VALUE;
System.out.println(l + 1); // -9223372036854775808
System.out.println(l + 1 == Long.MIN_VALUE); // true

BigInteger 内部使用 int[] 数组来存储任意大小的整形数据。

相对于常规整数类型的运算来说,BigInteger 运算的效率会相对较低。

四、变量

1、成员变量与局部变量的区别?

  • 语法形式:从语法上看,
    • 成员变量属于类,而局部变量是在代码块中定义的变量或者方法中定义的参数;
    • 成员变量可以被 publicprivatestatic 等访问修饰符修饰,而局部变量不能被访问控制修饰符及 static 修饰;
    • 但二者都能被 final 修饰
  • 存储方式
    • 如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,储存于元空间(Java7及之前存储在方法区)【静态成员变量】;
    • 如果未使用 static 修饰,这个成员变量是属于实例的,储存于堆内存【实例成员变量】;
    • 局部变量属于代码块或方法,储存在栈内存中。
  • 生存时间
    • 静态成员变量随着类的加载而存在,类卸载时销毁(通常程序退出);
    • 实例成员变量是对象的一部分,它随着对象的创建而存在,对象被垃圾回收时销毁;
    • 局部变量在方法或代码块进入时创建,退出时销毁
  • 初始化
    • 成员变量(静态和实例)不需要初始化,有默认值。⚠被 final 修饰的成员变量也必须被显示赋值。
    • 局部变量必须显式赋值后才能使用
  • 线程安全性
    • 成员变量如果多个线程共享同一对象(或静态变量),则需要额外的同步措施。
    • 局部变量是线程安全的(每个线程独享一个栈)。

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

  • 先不考虑变量类型,如果没有默认值会怎样?变量存储的是内存地址对应的任意随机值,程序读取该值运行会出现意外
  • 默认值有两种设置方式:手动和自动,根据第一点,没有手动赋值一定要自动赋值。成员变量在运行时可借助反射等方法手动赋值,而局部变量不行
  • 对于编译器(javac)来说,局部变量没赋值很好判断,可以直接报错。而成员变量可能是运行时赋值,无法判断,误报“没默认值”又会影响用户体验,所以采用自动赋默认值

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

  • 防止使用未初始化的变量

    局部变量只在方法或代码块内部声明和使用,Java 设计者希望强制开发者显式地初始化这些变量,这样可以避免“忘记赋值就使用”的逻辑错误

  • 编译时安全检查机制

    Java 编译器会在编译阶段检查局部变量是否被初始化,这样在运行程序之前就能发现潜在问题,有利于提升代码的健壮性

  • 局部变量生命周期短,设计上要求程序员明确控制

    局部变量是放在栈上的临时数据,不像成员变量或静态变量那样生命周期长,既然生命周期短,就要求使用它时必须明确知道它的初始值,不应该让它“自动变成某个默认值”

2、静态变量有什么作用?

静态变量就是被 static 关键字修饰的变量,能被该类的所有实例所共享,无论一个类被创建了多少个实例对象,他们都公用一份静态变量,也就是静态变量只会被分配一次内存,可以节省内存。

静态变量是通过类名来访问的,例如StaticVariableExample.staticVar(⚠如果被 private关键字修饰就无法这样访问了)。

java
public class StaticVariableExample {
    // 静态变量
    public static int staticVar = 0;
}

通常情况下,静态变量会被 final 关键字修饰成为常量。

java
public class ConstantVariableExample {
    // 常量
    public static final int constantVar = 0;
}

3、字符型常量和字符串常量的区别?

  • 形式

    • 字符常量:由单引号引起来的一个字符

      ⚠注:引号中必须是1个字符,0个或多个都会编译报错

    • 字符串常量:由双引号引起来的0个或多个字符

  • 含义:

    • 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算;
    • 字符串常量代表一个地址值(该字符串在内存中存放位置)。
  • 占内存大小

    • 字符常量只占 2 个字节;

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

    • 字符串常量占若干个字节。

java
public class StringExample {
    // 字符型常量
    public static final char LETTER_A = 'A';

    // 字符串常量
    public static final String GREETING_MESSAGE = "Hello, world!";
    public static void main(String[] args) {
        System.out.println("字符型常量占用的字节数为:"+Character.BYTES);	// 字符型常量占用的字节数为:2
        System.out.println("字符串常量占用的字节数为:"+GREETING_MESSAGE.getBytes().length);	// 字符串常量占用的字节数为:13
    }
}

五、方法

1、什么是方法的返回值?方法有哪几种类型?

方法的返回值 是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用是接收出结果,使得它可以用于其他的操作。

我们可以按照方法的返回值和参数类型将方法分为下面这几种:

  • 无参数无返回值的方法
  • 有参数无返回值的方法
  • 有返回值无参数的方法
  • 有返回值有参数的方法

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

这个需要结合 JVM 的相关知识,主要原因如下:

  • 静态成员变量和静态方法属于类,在类加载阶段(即首次使用类时)会被分配内存,可以通过类名直接访问;

  • 实例成员变量和实例方法属于对象,在类实例化之后才会为其分配内存;

  • 静态方法在没有对象实例的情况下就可以调用,如果直接调用非静态成员,可能还没有实例化对象,因此内存中没有该成员变量,因此在静态方法中不能访问非静态成员变量或调用非静态方法,除非通过已有的对象引用访问。

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

  • 调用方式:

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

    不过,需要注意的是一般不建议使用 对象.方法名 的方式来调用静态方法。这种方式非常容易造成混淆,静态方法不属于类的某个对象而是属于这个类。

    因此,一般建议使用 类名.方法名 的方式来调用静态方法。

    java
    public class Person {
        public void method() {
          //......
        }
    
        public static void staicMethod(){
          //......
        }
        public static void main(String[] args) {
            Person person = new Person();
            // 调用实例方法
            person.method();
            // 调用静态方法
            Person.staicMethod()
        }
    }
  • 访问类成员是否存在限制

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

4、重载和重写有什么区别?

重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理

重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法

(1)重载

  • 范围

    发生在同一个类中(或者父类和子类之间)

  • 满足要求

    • 必须相同:方法名必须相同
    • 必须有一个不同:参数类型、个数、顺序
    • 可以不同:方法返回值和访问修饰符可以不同
  • 允许被重载:

    • 任何方法都能被重载(包括构造器方法)

《Java 核心技术》这本书是这样介绍重载的:

如果多个方法(比如 StringBuilder 的构造方法)有相同的名字、不同的参数, 便产生了重载。

java
StringBuilder sb = new StringBuilder();
StringBuilder sb2 = new StringBuilder("HelloWorld");

编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。 如果编译器找不到匹配的参数, 就会产生编译时错误, 因为根本不存在匹配, 或者没有一个比其他的更好(这个过程被称为重载解析(overloading resolution))。

Java 允许重载任何方法,而不只是构造器方法。

综上:重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理

(2)重写

重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。

  • 范围

    子类中

  • 满足要求:

    • 方法名 和 参数列表 必须相同

    • 子类方法返回值类型 ≤ 父类方法返回值类型【如果是void和基本类型,则不可修改,引用类型可改为子类】

      保证多态调用的类型一致性:

      • 如果允许随意改变返回值类型,调用 Parent p = new Child(); p.getValue(); 就无法在编译期确定返回值类型。
      • Java 编译器要在静态编译时决定方法调用的返回类型,所以必须保证子类方法返回的类型是父类返回类型的子类。在父类引用调用子类方法时,返回值类型必须是父类方法返回类型或其子类,不能更宽泛,否则就会破坏类型安全和多态机制。
    • 子类方法抛出异常类型 ≤ 父类方法抛出异常类型

      保持异常处理行为的一致性

      • 调用者通常只知道父类类型,如果子类方法抛出新的、更广泛的异常,调用者可能没有处理这个异常,导致编译或运行出错
      • 为了不破坏父类 API 的契约,子类在重写时只能“收敛”异常类型,不能扩大。
    • 子类方法访问修饰符类型 ≥ 父类方法访问修饰符类型

      保证子类能“覆盖”父类行为而不降低可见性:

      • 如果子类把方法变得更“私有”,外部代码通过父类引用调用时可能无法访问这个方法。
      • 这会违背多态原则:父类能用的方法,子类不能隐藏。
      • 所以 Java 强制子类在重写时不能降低访问权限,否则会破坏继承体系的可用性。
  • 其他情况:

    • 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法

      • private 方法对子类不可见,等于这个方法对子类不存在,所以子类定义了一个新的同名方法,但这不是重写(override),JVM 中两个方法是完全独立的
      • final 意味着这个方法在类层级中不能被修改,这是 Java 为了 保证行为不变,用于安全性或API稳定性,``final` 是强制性的“禁止重写”标记,JVM 和编译器都会阻止。
      • static 方法属于类本身,不属于对象实例,所以调用的 静态方法 是编译期就决定好的,JVM 不会使用 “多态绑定” 来调用 static 方法
    • 但是被 static 修饰的方法能够被再次声明。

      • static 方法是类级别的,不参与多态,不存在虚方法表(vtable)映射

      • 所以你可以在子类中定义一个同名 static 方法,这是合法的

      • 只是不会发生“多态”,也不会替代父类的方法

        java
        class Parent {
            public static void staticMethod() {
                System.out.println("Parent static");
            }
            public void instanceMethod() {
                System.out.println("Parent instance");
            }
        }
          
        class Child extends Parent {
            public static void staticMethod() {
                System.out.println("Child static");
            }
            @Override
            public void instanceMethod() {
                System.out.println("Child instance");
            }
        }
          
        public class Test {
            public static void main(String[] args) {
                Parent p = new Child();
                p.staticMethod();     // 输出:Parent static  ← 这是静态方法隐藏,不是重写
                p.instanceMethod();   // 输出:Child instance ← 这是实例方法重写,多态行为
            }
        }
    • final 修饰的方法就不能在子类中再次声明,但是进行重载

      当你在父类中声明一个方法为 final,Java 编译器和 JVM 都会强制保证子类不能改变该方法的行为。这是为了:

      • 保证 API 的一致性和安全性;

      • 防止子类破坏父类的预期行为(比如在框架或安全敏感场景中);

      • JVM 可以对 final 方法做更多优化(如内联)。

        java
        class Parent {
            public final void sayHi() {
                System.out.println("Parent says hi");
            }
        }
          
        class Child extends Parent {
            // ❌ 编译错误:Cannot override the final method from Parent
            public void sayHi() {
                System.out.println("Child says hi");
            }
        }
        java
        class Parent {
            public final void sayHi() {
                System.out.println("Parent says hi");
            }
        }
          
        class Child extends Parent {
            public void sayHi(String name) {  // ✅ 合法:方法重载,不是重写
                System.out.println("Hi, " + name);
            }
        }
    • 构造方法无法被重写

    方法的重写要遵循“两同两小一大”

    • “两同”即方法名相同、形参列表相同;
    • “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
    • “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。

[总结]

综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。

区别点重载方法重写方法
发生范围同一个类子类
方法名相同相同
参数列表必须不同必须相同
返回类型可修改子类 ≤ 父类
构造方法可重载不可重写
异常可修改子类 ≤ 父类
访问修饰符可修改子类 父类
发生阶段编译期运行期

5、什么是可变长参数?

从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数,Java 的可变参数编译后实际会被转换成一个数组

就比如下面这个方法就可以接受 0 个或者多个参数。

java
public static void main(String[] args) {
       method("a","bb","ccc");
    }

public static void  method(String... args){
    System.out.println("可变长参数的参数值个数为:" + args.length);      // 可变长参数的参数值个数为:3

    for (String arg : args) {
        System.out.println(arg);
    }
}

输出:

可变长参数的参数值个数为:3
a
bb
ccc

另外,可变参数只能作为函数的最后一个参数,但其前面可以有,也可以没有任何其他参数。

java
public static void method2(String arg1, String... args) {
   //......
}

(1)遇到方法重载的情况怎么办呢?会优先匹配固定参数,还是可变参数的方法呢?

答案是会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。

java
public class VariableLengthArgument {

    public static void printVariable(String... args) {
        for (String s : args) {
            System.out.println(s);
        }
    }

    public static void printVariable(String arg1, String arg2) {
        System.out.println(arg1 + arg2);
    }

    public static void main(String[] args) {
        printVariable("a", "b");
        printVariable("a", "b", "c", "d");
    }
}

输出:

ab
a
b
c
d