面试题 / Java

基本语法

注释有哪几种形式?

Java 中的注释有三种:

  1. 单行注释:通常用于解释方法内某单行代码的作用。

  2. 多行注释:通常用于解释一段代码的作用。

  3. 文档注释:通常用于生成 Java 开发文档。

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

在我们编写代码的时候,如果代码量比较少,我们自己或者团队其他成员还可以很轻易地看懂代码,但是当项目结构一旦复杂起来,我们就需要用到注释了。注释并不会执行(编译器在编译代码之前会把代码中的所有注释抹掉,字节码中不保留注释),是我们程序员写给自己看的,注释是你的代码说明书,能够帮助看代码的人快速地理清代码之间的逻辑关系。因此,在写程序的时候随手加上注释是一个非常好的习惯。

《Clean Code》这本书明确指出:

代码的注释不是越详细越好。实际上好的代码本身就是注释,我们要尽量规范和美化自己的代码来减少不必要的注释。

若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述。

举个例子:

去掉下面复杂的注释,只需要创建一个与注释所言同一事物的函数即可

// check to see if the employee is eligible for full benefits
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))

应替换为

if (employee.isEligibleForFullBenefits())

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

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

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

Java 语言关键字有哪些?

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

Tips:所有的关键字都是小写的,在 IDE 中会以特殊颜色显示。

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

  • 在程序控制中,当在 switch 中匹配不到任何情况时,可以使用 default 来编写默认匹配的情况。
  • 在类,方法和变量修饰符中,从 JDK8 开始引入了默认方法,可以使用 default 关键字来定义一个方法的默认实现。
  • 在访问控制中,如果一个方法前没有任何修饰符,则默认会有一个修饰符 default,但是这个修饰符加上了就会报错。

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

官方文档:https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html

⭐️自增自减运算符

在写代码的过程中,常见的一种情况是需要某个整数类型变量增加 1 或减少 1。Java 提供了自增运算符 (++) 和自减运算符 (--) 来简化这种操作。

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

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

为了方便记忆,可以使用下面的口诀:符号在前就先加/减,符号在后就后加/减

flowchart LR
    %% 定义全局样式
    classDef step fill:#4CA497,color:#fff,rx:10,ry:10
    classDef example fill:#E99151,color:#fff,rx:10,ry:10

    subgraph Prefix["前缀形式 ++a / --a"]
        direction TB
        style Prefix fill:#F5F7FA,stroke:#E0E6ED,stroke-width:1.5px
        P1["第一步:变量自增/自减"]:::step --> P2["第二步:使用新值参与运算"]:::step
        P3["示例:b = ++a<br先 a=a+1,再 b=a"]:::example
    end

    subgraph Suffix["后缀形式 a++ / a--"]
        direction TB
        style Suffix fill:#F5F7FA,stroke:#E0E6ED,stroke-width:1.5px
        S1["第一步:使用当前值参与运算"]:::step --> S2["第二步:变量自增/自减"]:::step
        S3["示例:b = a++<br先 b=a,再 a=a+1"]:::example
    end

    linkStyle default stroke-width:1.5px,opacity:0.8

下面来看一个考察自增自减运算符的高频笔试题:执行下面的代码后,abcde的值是?

int a = 9;
int b = a++;
int c = ++a;
int d = c--;
int e = --d;

答案:a = 11b = 9c = 10d = 10e = 10

⭐️移位运算符

移位运算符是最基本的运算符之一,几乎每种编程语言都包含这一运算符。移位操作中,被操作的数据被视为二进制数,移位就是将其向左或向右移动若干位的运算。

移位运算符在各种框架以及 JDK 自身的源码中使用还是挺广泛的,HashMap(JDK1.8) 中的 hash 方法的源码就用到了移位运算符:

static final int hash(Object key) {
    int h;
    // key.hashCode():返回散列值也就是hashcode
    // ^:按位异或
    // >>>:无符号右移,忽略符号位,空位都以0补齐
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

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

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

移位运算符最常用于快速乘以或除以 2 的幂次方。除此之外,它还在以下方面发挥着重要作用:

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

掌握最基本的移位运算符知识还是很有必要的,这不光可以帮助我们在代码中使用,还可以帮助我们理解源码中涉及到移位运算符的代码。

flowchart TB
    %% 定义全局样式,保持统一风格
    classDef left fill:#4CA497,color:#fff,rx:10,ry:10
    classDef right fill:#00838F,color:#fff,rx:10,ry:10
    classDef uright fill:#E99151,color:#fff,rx:10,ry:10

    subgraph ShiftOps["Java 三种移位运算符"]
        direction TB
        style ShiftOps fill:#F0F2F5,stroke:#E0E6ED,stroke-width:1.5px

        subgraph Left["左移 <<"]
            style Left fill:#F5F7FA,stroke:#E0E6ED,stroke-width:1.5px
            L1["操作:向左移动 n 位"]:::left
            L2["规则:高位丢弃,低位补 0"]:::left
            L3["效果:相当于 × 2^n"]:::left
            L4["示例:8 << 2 = 32"]:::left
        end

        subgraph Right["带符号右移 >>"]
            style Right fill:#F5F7FA,stroke:#E0E6ED,stroke-width:1.5px
            R1["操作:向右移动 n 位"]:::right
            R2["规则:低位丢弃,高位补符号位"]:::right
            R3["效果:相当于 ÷ 2^n"]:::right
            R4["示例:-8 >> 2 = -2"]:::right
        end

        subgraph URight["无符号右移 >>>"]
            style URight fill:#F5F7FA,stroke:#E0E6ED,stroke-width:1.5px
            U1["操作:向右移动 n 位"]:::uright
            U2["规则:低位丢弃,高位补 0"]:::uright
            U3["效果:逻辑右移"]:::uright
            U4["示例:-8 >>> 2 = 1073741822"]:::uright
        end
    end

    linkStyle default stroke-width:1.5px,opacity:0.8

Java 中有三种移位运算符:

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

虽然移位运算本质上可以分为左移和右移,但在实际应用中,右移操作需要考虑符号位的处理方式。

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

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

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

当 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

左移运算符代码示例

int i = -1;
System.out.println("初始数据:" + i);
System.out.println("初始数据对应的二进制字符串:" + Integer.toBinaryString(i));
i <<= 10;
System.out.println("左移 10 位后的数据 " + i);
System.out.println("左移 10 位后的数据对应的二进制字符 " + Integer.toBinaryString(i));

输出:

初始数据:-1
初始数据对应的二进制字符串:11111111111111111111111111111111
左移 10 位后的数据 -1024
左移 10 位后的数据对应的二进制字符 11111111111111111111110000000000

由于左移位数大于等于 32 位操作时,会先求余(%)后再进行左移操作,所以下面的代码左移 42 位相当于左移 10 位(42%32=10),输出结果和前面的代码一样。

int i = -1;
System.out.println("初始数据:" + i);
System.out.println("初始数据对应的二进制字符串:" + Integer.toBinaryString(i));
i <<= 42;
System.out.println("左移 10 位后的数据 " + i);
System.out.println("左移 10 位后的数据对应的二进制字符 " + Integer.toBinaryString(i));

右移运算符使用类似,篇幅问题,这里就不做演示了。

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

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

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

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

  1. return;:直接使用 return 结束方法执行,用于没有返回值函数的方法
  2. return value;:return 一个特定值,用于有返回值函数的方法
flowchart TB
    subgraph Method["方法体"]
        direction TB
        style Method fill:#F5F7FA,stroke:#E0E6ED,stroke-width:1.5px
        Start["方法开始"] --> Loop

        subgraph Loop["循环体 for/while"]
            direction TB
            style Loop fill:#F0F2F5,stroke:#E0E6ED,stroke-width:1.5px
            L1["循环条件判断"] -->|"满足"| L2["执行循环体"]
            L2 --> L3{{"遇到关键字?"}}
            L3 -->|"continue"| Continue["跳过本次<br/>继续下一次循环"]
            L3 -->|"break"| Break["跳出整个循环"]
            L3 -->|"无"| L1
            Continue --> L1
        end

        Break --> AfterLoop["循环后的代码"]
        L1 -->|"不满足"| AfterLoop
        AfterLoop --> L4{{"遇到 return?"}}
        L4 -->|"是"| Return["结束整个方法"]
        L4 -->|"否"| End["方法正常结束"]
    end

    classDef start fill:#E99151,color:#fff,rx:10,ry:10
    classDef loop fill:#4CA497,color:#fff,rx:10,ry:10
    classDef decision fill:#00838F,color:#fff,rx:10,ry:10
    classDef alert fill:#C44545,color:#fff,rx:10,ry:10

    class Start,End start
    class L1,L2,AfterLoop loop
    class L3,L4 decision
    class Continue,Break,Return alert

    linkStyle default stroke-width:1.5px,opacity:0.8

思考一下:下列语句的运行结果是什么?

public static void main(String[] args) {
    boolean flag = false;
    for (int i = 0; i <= 3; i++) {
        if (i == 0) {
            System.out.println("0");
        } else if (i == 1) {
            System.out.println("1");
            continue;
        } else if (i == 2) {
            System.out.println("2");
            flag = true;
        } else if (i == 3) {
            System.out.println("3");
            break;
        } else if (i == 4) {
            System.out.println("4");
        }
        System.out.println("xixi");
    }
    if (flag) {
        System.out.println("haha");
        return;
    }
    System.out.println("heihei");
}

运行结果:

0
xixi
1
2
xixi
3
haha

来源引用