系列文章导航

  1. Java 内存管理面试指南一
  2. Java 基础面试指南一
  3. Java 基础面试指南二
  4. Java 基础面试指南三
  5. Java 基础面试指南四
  6. Java 线程面试指南一
  7. Java 线程面试指南二
  8. Redis 面试指南一
  9. Kafka 面试指南一
  10. Spring 面试指南一
  11. SpringBoot 面试指南一
  12. 微服务面试指南一

1. Java 是否使用指针?

不,Java 并不完全使用指针。指针是将另一个变量的确切内存地址存储在其内部的变量。由于安全性和健壮性的考虑,Java 避开了指针这一概念,而指针是 C 和 C++ 内存寻址的主要组成部分。

如果我们知道一个变量的地址,就可以从任何地方访问和修改它,即使它是私有的,这与封装原则相矛盾。因此,Java 不使用指针,而是坚持使用更安全的选项,称为引用(Reference)

引用是一个地址,它指向对象变量和方法的存储位置。将对象分配给变量时,我们从不实际使用对象本身或其副本,而是使用对该对象的引用。

虽然引用类似于地址,但它具有以下限制:

  • 无法执行算术运算。
  • 无法直接分配地址。
  • 不能设置为指向没有对象的变量(即不能随意指向任意内存位置)。

2. 用 Java 显示当年的最后两位数字

要显示当前年份的最后两位数字,请使用日期格式字符 y,如以下示例所示:

import java.util.Date;

public class Example {
    public static void main(String[] args) throws Exception {
        Date date = new Date();
        System.out.printf("Two-digit Year = %ty\n", date);
    }
}

当前 2020 年的输出显示 20:

Two-digit Year = 20

3. 检查字符串是否以特定的子字符串开头

startsWith() 方法用于检查字符串是否以特定字符串开头。

在这里,我们检查给定的字符串是否以子字符串 "One" 开头:

public class Example {
    public static void main(String[] args) {
        String s = "OneAndOnly";
        // 注意:startsWith 是区分大小写的
        if (s.startsWith("One")) {
            System.out.println("Begins with the specified word!");
        } else {
            System.out.println("Does not begin with the specified word!");
        }
    }
}

输出:

Begins with the specified word!

4. Java 中局部变量的默认值?

Java 中的局部变量是在方法、代码块、构造函数等中局部声明的变量。当程序控制进入方法、代码块或构造函数时创建局部变量,当程序控制离开它们时销毁局部变量。

局部变量在 Java 中没有任何默认值。这意味着应在首次使用变量之前声明它们并为其分配值。否则,编译器将引发错误。

给出了一个演示 Java 局部变量的程序,如下所示:

public class Demo {
    public void func() {
        int num;
        System.out.println("The number is : " + num);
    }

    public static void main(String args[]) {
        Demo obj = new Demo();
        obj.func();
    }
}

上面的程序包含一个局部变量 num,会导致错误:“变量 num 可能尚未初始化”。

上面程序的正确版本如下:

public class Demo {
    public void func() {
        int num = 50;
        System.out.println("The number is : " + num);
    }

    public static void main(String args[]) {
        Demo obj = new Demo();
        obj.func();
    }
}

上面程序的输出如下:

The number is : 50

5. Java 中的 Collections API

可以使用 Java 的 Collections API 提供的架构来存储和操作一组对象。可以使用 Java 集合执行 Java 中所有可能的操作,例如搜索、排序、删除、插入等。

Java 中的集合层次结构如下:

Java 中的 Collections API

Java 中 Collections API 的一些特征如下:

  1. 有些集合不允许重复的元素。
  2. 一些集合可能使用链表来存储元素。链表中的所有元素都有对链表中上一个和下一个元素的引用。与 ArrayList 相比,在 LinkedList 中插入元素更加容易和快捷。
  3. 一些集合可能使用数组来存储元素。可以动态调整数组的大小,以根据需要存储越来越多的元素。
  4. 一些集合可能使用哈希表来存储元素。这些表包含它们存储的所有元素的特定键。
  5. 一些集合可能使用树来存储元素。基于树的存储对于排序和搜索元素非常有用。

6. Java 中的标记接口

不包含任何字段或方法的空接口称为标记接口(Marker Interface)。标记接口的实际示例是 Serializable 接口、Cloneable 接口和 Remote 接口。这些接口的详细信息如下:

可序列化的接口 (Serializable)

可序列化接口使对象有资格将其状态保存在文件中。该接口存储在 java.io 包中。未实现可序列化接口的类未对其状态进行序列化或反序列化。

可克隆的接口 (Cloneable)

可克隆接口由一个类实现,该类指示允许 Object 类中的 clone() 方法对类实例进行字段到字段的复制。java.lang 包包含可克隆的接口。

如果为没有实现 Cloneable 接口的类调用 clone() 方法,则抛出 CloneNotSupportedException 异常。具有此接口实现的类通常会按惯例覆盖 Object.clone() 方法。

远程接口 (Remote)

存储在计算机上并且可以从另一台计算机访问的对象称为远程对象。需要远程接口来标记对象并将其转换为远程对象。java.rmi 包包含远程接口。

可以使用非本地虚拟机调用其方法的接口也可以由远程接口标识。同样,远程方法调用 (RMI) 具有一些方便类,可以使用远程对象实现来扩展这些方便类,这些便利类有助于创建远程对象。

7. Java 异常处理的最佳实践

异常处理是处理计算过程中异常发生的过程。Java 中异常处理的一些最佳实践如下:

  • 将清理代码放在 finally 块中
    try 块中使用了许多资源,之后需要关闭它们。但是,不应在 try 块的末尾关闭这些资源,因为如果引发任何异常,则可能永远无法访问这些资源。因此,所有清理代码都应放在 finally 块中,以获得更好的结果。
  • 不要抓东西 (Don't Catch Throwable)
    所有异常和错误的超类都是 Throwable。绝对不要在 catch 子句中使用它,因为它将捕获所有异常和错误,其中某些异常和错误可能不在应用程序的控制范围内,因此无法处理。
  • 使用描述性消息,但有例外
    描述性消息应提供例外,以帮助理解为什么将例外报告给监视工具或日志文件。描述性消息应准确,并尽可能清晰地描述异常事件问题。
  • 记录指定的例外
    方法签名中指定的所有异常也应记录在 Javadoc 中。这非常有用,因为它为调用者提供了更多信息,有助于按需处理或避免异常。
  • 永远不要忽略异常
    在永远不会发生的假设下,切勿忽略异常。这是有缺陷的,因为将来代码可能会以无法预料的方式更改,并且可能永远不会发生认为不需要的异常(因为特定事件永远不会发生)。
  • 首先捕获最具体的异常
    应该首先捕获最具体的异常类,然后在执行与异常匹配的第一个捕获块时,提供较不具体的捕获块。因此,如果首先给出不太具体的异常捕获块,则控件可能永远不会到达更具体的异常捕获块。
  • 使用更具体的例外
    最好有尽可能特殊的异常,因为它们会使 API 易于理解。这意味着应使用最适合异常事件的类,并应避免未指定的异常。
  • 不要记录并抛出异常
    不要记录并重新抛出异常,因为它会导致同一异常的多个错误消息。这些额外的错误消息非常有用,因为它们不提供任何额外的信息。如果需要任何其他信息,则应捕获异常并将其包装在定制的异常中。

8. Java 中的线程需求

Java 中的线程有助于在程序中实现并行性。这意味着可以使用多线程同时执行多个操作。

使用多线程可以实现线程最重要的用法。这意味着可以使用多线程同时完成多个任务。Java 中多线程的一些主要用途如下:

  • 减少响应时间
    通过将特定问题分解为较小的块并将每个这些块分配给一个线程,可以减少响应时间。这意味着可以使用多个线程在相对较短的时间内解决问题。
  • 多个任务可以并行运行
    可以使用多线程并行运行多个任务。一个示例是事件处理或绘制,可以使用多个线程同时执行该事件处理或绘制。
    此外,由于一个线程正在执行特定功能,因此在图形用户界面中需要多个线程,因为无法冻结 GUI,因此更多线程需要其他线程来执行更多用户任务。
  • 可以同时为多个客户提供服务
    在客户端服务器应用程序中,许多客户端可以使用多个线程连接到服务器。这意味着客户端不必等到服务器已满足先前客户端的请求。
  • 可以充分利用 CPU 的优势
    线程可用于充分利用 CPU 的能力并增加系统的吞吐量。如果 CPU 有多个内核,则需要多个线程在这些内核上并行运行以优化系统性能。

9. Java 中的线程优先级

在多线程中,每个线程都被分配一个优先级。调度程序根据优先级将处理器分配给线程,即,优先级最高的线程首先分配给处理器,依此类推。

Thread 类中为线程优先级定义的三个静态值如下:

  • MAX_PRIORITY
    这是值为 10 的最大线程优先级。
  • NORM_PRIORITY
    这是默认的线程优先级,值为 5。
  • MIN_PRIORITY
    这是值为 1 的最小线程优先级。

给出了一个演示 Java 线程优先级的程序,如下所示:

import java.lang.*;

public class ThreadPriorityDemo extends Thread {
    public static void main(String[] args) {
        ThreadPriorityDemo thread1 = new ThreadPriorityDemo();
        ThreadPriorityDemo thread2 = new ThreadPriorityDemo();
        ThreadPriorityDemo thread3 = new ThreadPriorityDemo();

        System.out.println("Default thread priority of thread1: " + thread1.getPriority());
        System.out.println("Default thread priority of thread2: " + thread2.getPriority());
        System.out.println("Default thread priority of thread3: " + thread3.getPriority());

        thread1.setPriority(8);
        thread2.setPriority(3);
        thread3.setPriority(6);

        System.out.println("New thread priority of thread1: " + thread1.getPriority());
        System.out.println("New thread priority of thread2: " + thread2.getPriority());
        System.out.println("New thread priority of thread3: " + thread3.getPriority());
    }
}

上面程序的输出如下:

Default thread priority of thread1: 5
Default thread priority of thread2: 5
Default thread priority of thread3: 5
New thread priority of thread1: 8
New thread priority of thread2: 3
New thread priority of thread3: 6

10. Java 中的内存管理

内存管理是 Java 的重要组成部分,理解它非常重要。Java 中的内存分为两个主要部分:栈(Stack)和堆(Heap)。演示此过程的图如下所示:

Java 中的内存管理

Java 中有关堆栈和堆内存的详细信息如下:

Java 中的栈内存 (Stack Memory)

Java 中的栈内存用于线程执行。特定值存储在线程栈中,这些值在短时间内可用。同样,栈存储器可能包含对从堆存储器中的方法引用的对象的数据引用。

线程栈内存中的顺序为后进先出 (LIFO)。调用该方法时,会在栈存储器中为所有原始值和对该方法中其他对象的引用创建一个块。方法结束后,栈存储器中的存储块将释放,并且可以由另一种方法使用。

Java 中的堆内存 (Heap Memory)

Java 中的堆内存是 Java 运行时分配给 Objects 和 JRE 类的内存。应用程序中的所有对象都在堆内存中创建。堆内存中的对象可从应用程序中的任何位置全局访问,因此它们对于整个应用程序执行都有生命周期。

11. super vs this

superthis 都是 Java 中的关键字。有关这些的详细信息如下:

super 关键字

super 关键字是 Java 中的保留关键字,用于引用直接父类。super 关键字还可以调用直接父类的方法和构造函数。

给出了一个演示 super 关键字的程序,如下所示:

class A {
    int x = 26;
    static int y = 15;
}

public class B extends A {
    void display() {
        System.out.println(super.x);
        System.out.println(super.y);
    }

    public static void main(String[] args) {
        B obj = new B();
        obj.display();
    }
}

上面程序的输出如下:

26
15

this 关键字

this 关键字是 Java 中的保留关键字,用于引用当前的类实例变量。this 关键字还可以调用当前类的方法和构造函数。也可以在方法或构造函数调用中将其作为参数传递。

给出了一个演示 this 关键字的程序,如下所示:

public class Demo {
    int x = 25;
    static int y = 12;

    void display() {
        this.x = 250;
        System.out.println(x);
        this.y = 120;
        System.out.println(y);
    }

    public static void main(String[] args) {
        Demo obj = new Demo();
        obj.display();
    }
}

上面程序的输出如下:

250
120

12. Java 中的序列化与反序列化

Java 中的序列化涉及将对象状态写入字节流,以便可以将其发送到数据库或磁盘。反序列化是逆向过程,其中流被转换为对象。

默认情况下,String.class 和所有包装器类都实现 java.io.Serializable 接口。ObjectOutputStream 类的 writeObject() 方法提供了序列化对象的功能。使用 ObjectInputStream 对对象和原始数据进行反序列化。

给出了一个演示 Java 中的序列化和反序列化的程序,如下所示:

import java.io.*;

class Employee implements Serializable {
    int empID;
    String name;

    Employee(int e, String n) {
        this.empID = e;
        this.name = n;
    }
}

public class Demo {
    public static void main(String[] args) throws Exception {
        Employee emp1 = new Employee(251, "Jason Scott");
        FileOutputStream f = new FileOutputStream("file.txt");
        ObjectOutputStream o = new ObjectOutputStream(f);
        o.writeObject(emp1);
        o.flush();

        ObjectInputStream i = new ObjectInputStream(new FileInputStream("file.txt"));
        Employee emp2 = (Employee) i.readObject();
        System.out.println(emp2.empID + " " + emp2.name);
        i.close();
    }
}

上面程序的输出如下:

251 Jason Scott

13. 停止 Java 中的线程

在 Java 中停止线程可能有点复杂,因为没有有效的 stop 方法。这与在 Java 中启动线程完全不同,因为有可用的 start() 方法。首次发布 Java 时,Thread 类中有一个 stop() 方法,但此方法已被弃用(Deprecated),因为它不安全。

给出了一个演示如何使用自定义标志位安全停止 Java 中的线程的程序,如下所示:

import static java.lang.Thread.currentThread;
import java.util.concurrent.TimeUnit;

public class Demo {
    public static void main(String args[]) throws InterruptedException {
        Server ser = new Server();
        Thread thread = new Thread(ser, "T1");
        thread.start();
        System.out.println(currentThread().getName() + " is stopping Server thread");
        ser.stop();
        TimeUnit.MILLISECONDS.sleep(200);
        System.out.println(currentThread().getName() + " is finished now");
    }
}

class Server implements Runnable {
    private volatile boolean exit = false;

    public void run() {
        while (!exit) {
            System.out.println("The Server is running");
        }
        System.out.println("The Server is now stopped");
    }

    public void stop() {
        exit = true;
    }
}

上面程序的输出如下:

main is stopping Server thread
The Server is running
The Server is now stopped
main is finished now

14. 为什么 Java 中的字符串是不可变的?

Java 中的字符串是不可变的。这意味着它们不可更改或不可修改。有几个原因,其中一些如下:

  • 同步与并发
    通过将 Java 中的字符串设为不可变,可以解决同步问题。这是因为如果它们是不可变的,它们将自动变为线程安全的。
  • 类加载
    类加载具有字符串参数。如果字符串是可变的,那么当可变对象更改其状态时,可能会加载错误的类。
  • 安全
    网络连接、URL、数据库连接、用户名/密码等具有以字符串表示的参数。如果字符串是可变的,则可以轻松更改这些参数,这将构成安全漏洞。

演示字符串的程序如下所示:

public class Demo {
    public static void main(String args[]) {
        String str = "Snow";
        str.concat("White");
        System.out.println(str);
    }
}

上面程序的输出如下:

Snow

在上述程序中,仅打印 Snow,因为 Strings 是不可变的对象,concat 方法返回了一个新字符串而不是修改原字符串。

15. Java 线程如何相互通信?

线程间通信涉及 Java 线程之间的通信。Java 的三种用于实现线程间通信的方法如下:

  • wait()
    此方法使当前线程释放锁。直到完成特定时间段或另一个线程为此对象调用 notify()notifyAll() 方法,该操作才完成。
  • notify()
    此方法从当前对象的监视器上的多个线程中唤醒一个线程。线程的选择是任意的。
  • notifyAll()
    此方法唤醒当前对象监视器上的所有线程。

给出了一个演示 Java 中线程间通信的程序,如下所示:

class BankCustomer {
    int balAmount = 10000;

    synchronized void withdrawMoney(int amount) {
        System.out.println("Withdrawing money");
        balAmount -= amount;
        System.out.println("The balance amount is: " + balAmount);
    }

    synchronized void depositMoney(int amount) {
        System.out.println("Depositing money");
        balAmount += amount;
        System.out.println("The balance amount is: " + balAmount);
        notify();
    }
}

public class Demo {
    public static void main(String args[]) {
        final BankCustomer cust = new BankCustomer();
        new Thread() {
            public void run() {
                cust.withdrawMoney(5000);
            }
        }.start();
        new Thread() {
            public void run() {
                cust.depositMoney(2000);
            }
        }.start();
    }
}

上面程序的输出如下:

Withdrawing money
The balance amount is: 5000
Depositing money
The balance amount is: 7000

16. 避免在 Java 中死锁?

死锁是通常在多线程或多任务处理中发生的情况。这意味着两个或更多线程正在无限期地等待彼此释放它们完成执行所需的资源。

通过尝试避免产生死锁的可能性,可以在 Java 中避免死锁。这些可能性无法完全消除,但绝对可以减少。

避免 Java 中死锁的一些方法如下:

  • 避免不必要的锁
    使用不必要的锁可能导致死锁,因此仅应锁定实际需要的那些成员。
  • 避免嵌套锁
    嵌套锁到多个线程是死锁的主要原因。因此,如果一个线程已被锁定,则应避免锁定多个线程。
  • 使用 Thread.join
    如果一个线程正在等待另一个线程,则会发生死锁。因此,如果死锁条件出现在执行所需的最长时间内,则可以使用 Thread.join

17. Java 中的多捕获块

在 Java 7 之前,当我们需要处理多个异常时,我们需要多个 catch 块来处理这些异常。

让我们看一个例子:

import java.util.*;

public class Example {
    public static void main(String args[]) {
        Scanner sc = new Scanner(System.in);
        try {
            int n = Integer.parseInt(sc.next());
            System.out.println(n / 0);
        } catch (ArithmeticException ex) {
            System.out.println("Exception caught " + ex);
        } catch (NumberFormatException ex) {
            System.out.println("Exception caught " + ex);
        }
    }
}

这里的输出取决于输入。当我们输入整数时,它将生成算术异常。

对于以下输入:3

输出如下:

$javac Example.java
$java Example
Exception caught java.lang.ArithmeticException: / by zero

对于字符串或字符输入,输出将不同。

对于以下输入:Hello

输出如下:

$javac Example.java
$java Example
Exception caught java.lang.NumberFormatException: For input string: "Hello"

从 Java 7 开始,Java 中引入了多捕获 (Multi-catch) 块。一个 catch 块可以捕获多个分开的异常,使用 | 符号。

让我们看一个多捕获块的例子:

import java.util.*;

public class Example {
    public static void main(String args[]) {
        Scanner sc = new Scanner(System.in);
        try {
            int n = Integer.parseInt(sc.next());
            System.out.println(n / 0);
        } catch (NumberFormatException | ArithmeticException ex) {
            System.out.println("Exception caught " + ex);
        }
    }
}

当我们输入整数时,它将生成算术异常。

对于以下输入:3

输出如下:

$javac Example.java
$java Example
Exception caught java.lang.ArithmeticException: / by zero

对于字符串或字符输入,输出将不同。

对于以下输入:Hello

输出如下:

$javac Example.java
$java Example
Exception caught java.lang.NumberFormatException: For input string: "Hello"

18. Java 注解

Java 中的注释(Annotation)是一种形式语法元数据,用于传达有关程序元素(如构造函数、方法、实例变量和类)的更多信息。

它们始终以 @ 符号开头。注释在程序的编译中没有直接作用。另一方面,注释并不完全像注释,因为它们可以改变编译器对程序的看法。

让我们看一个示例,其中注释会影响程序的输出。我们正在尝试重写私有方法。这将产生一个错误。

class Parent {
    private void display() {
        System.out.println("Super class");
    }
}

public class Example extends Parent {
    // 使用 Override 注解
    @Override
    void display() { // 尝试覆盖 display()
        System.out.println("Sub class");
    }

    public static void main(String[] args) {
        Parent obj = new Example();
        obj.display();
    }
}

输出如下:

$javac Example.java
Example.java:11: error: method does not override or implement a method from a supertype
   @Override
   ^
Example.java:19: error: display() has private access in Parent
     obj.display();
        ^
2 errors

基本上,注释分为三类:

  1. 标记注释:它们用于指出声明。这些注释没有任何参数。

    • 例如:@ExampleAnnotation()
    • @Override 是标记注释的示例。
  2. 完整注释:这些注释由多个变量的名称、值、对组成。

    • 例如:@ExampleAnnotation(name="Program", value="Java")
  3. 单值注释:这些注释仅包含一个带有简写值的成员。

    • 例如:@ExampleAnnotation("running")

Java 中有一些内置/原始注释。java.lang 包中包含 3 个内置注释,他们如下:

  • @Deprecated
  • @Override
  • @SuppressWarnings

其中的 4 个导入了 java.lang.annotation 类,即:

  • @Retention
  • @Documented
  • @Inherited
  • @Target
注解描述
@Deprecated标记注释,指示声明已过时且已更新
@Override当方法覆盖父类的另一个方法时使用的标记注释
@SuppressWarnings它用于生成编译器警告
@Documented它是一个标记注释,告诉工具注释将被记录
@Inherited它仅在声明注释时使用
@Target它充当另一个注释的注释

19. Java 8 中的 Lambda 表达式

Java 中的 Lambda 表达式是实现功能接口的表达式。功能接口是只有一种抽象方法的那些接口。

创建这些表达式是为了减少匿名类的繁琐的开销代码。匿名类是没有名称的本地类,并且在同时声明和实例化。

直到 Java 8 为止,即使是最简单的操作,都使用匿名类编写了其他语法代码。引入 Lambda 表达式以消除此缺点。

语法:

lambda operator -> body

lambda 运算符是一个参数列表,可以具有 0、1 或多个参数。

零参数:

()->System.out.println("I have no argument");

一个参数:

(arg)->System.out.println("I have one argument" + arg)

多个参数:

(arg1,arg2)->System.out.println("I have many arguments" + arg1 + " " + arg2)

让我们使用 lambda 表达式从列表中打印偶数个整数:

import java.util.*;

public class Example {
    public static void main(String args[]) {
        List<Integer> list = new ArrayList<Integer>();
        for (int i = 1; i <= 10; i++) { // adding 1 to 10 in the integer ArrayList
            list.add(i);
        }
        // printing even elements in list using lambda expression
        list.forEach(arg -> {
            if (arg % 2 == 0) System.out.println(arg);
        });
    }
}

输出如下:

$javac Example.java
$java Example
2
4
6
8
10

20. 如何在 Java 中比较两个字符串

使用比较方法

使用 compareTo()compareToIgnoreCase() 方法比较 Java 中的两个字符串。compareTo() 方法用于按字典顺序或字典顺序比较两个字符串。每个字符都转换为 Unicode 值以进行比较。如果两个字符串相等,则返回 0,否则返回正值或负值。

如果第一个字符串在字典上大于第二个字符串,则 compareTo() 方法返回正值;如果第一个字符串在字典上小于第二个字符串,则返回负值。compareToIgnoreCase() 方法按字典顺序比较两个字符串,而不考虑它们的大小写。

让我们看一个示例,其中 compareTo()compareToIgnoreCase() 方法用于比较两个字符串:

public class Example {
    public static void main(String args[]) {
        String s1 = "Same string";
        String s2 = "same string";
        int a1 = s1.compareTo(s2);
        int a2 = s1.compareToIgnoreCase(s2);

        if (a1 < 0)
            System.out.println("String s2 is greater");
        else if (a1 > 0)
            System.out.println("String s1 is greater");
        else
            System.out.println("String s1 is equal to String s2");

        if (a2 == 0)
            System.out.println("After Ignoring the case, s1 and s2 are equal");
        else
            System.out.println("Even after ignoring the case, s1 and s2 are not equal");
    }
}

输出如下:

$javac Example.java
$java Example
String s2 is greater
After Ignoring the case, s1 and s2 are equal

使用 equals() 和 equalsIgnoreCase()

equals() 方法将字符串与特定对象进行比较。它返回布尔值 true 或 false。当 arguments 不为 null 且字符串与对象的字符匹配时,它将返回 true。equalsIgnoreCase() 进行相同的操作,但没有考虑字符串的大小写和对象的字符序列。

让我们看一个示例,其中 equals()equalsIgnoreCase() 用于比较两个字符串。

public class Example {
    public static void main(String[] args) {
        String s1 = "Same String";
        String s2 = "same string";
        System.out.println(s1.equals(s2));
        System.out.println(s1.equalsIgnoreCase(s2));
    }
}

输出如下:

$javac Example.java
$java Example
false
true

使用 == 运算符

我们可以使用 == 运算符比较两个字符串。此运算符的行为不同于 equals() 方法,它比较的是对象的引用地址,并返回布尔值作为结果。

让我们看一下 == 运算符在两个字符串比较中的应用:

public class Example {
    public static void main(String[] args) {
        String s1 = "Same String";
        String s2 = "same string";
        String s3 = "Same String";
        System.out.println(s1 == s2);
        System.out.println(s1 == s3);
    }
}

输出如下:

$javac Example.java
$java Example
false
true

21. 重载 Java 中的 main() 方法

Java 中的 main() 方法很可能会重载,因为它不是地面上的方法。main() 方法与其他任何方法一样,可以像其他方法一样重载。

由于 public static void main(String[] args) 充当 main 方法的方法签名,因此 JVM 首先调用它。public static void main(String[] args) 充当 Java 程序的入口点。

我们可以重载 Java 中的 main 方法。由于程序执行时程序不会执行重载的 main 方法,因此我们需要从具有方法签名的实际 main 方法中调用重载的 main 方法。

让我们看一下 Java 中 main 方法重载的示例:

public class Example {
    public static void main(String x) {
        System.out.println(x + " World");
    }

    public static void main(String a, int b) {
        System.out.println(a + "," + b);
    }

    public static void main(String[] args) {
        System.out.println("Hello from public static void main(String []args)!");
        main("Hello");
        main("Hello", 2);
    }
}

输出如下:

$javac Example.java
$java -Xmx128M -Xms16M Example
Hello from public static void main(String []args)!
Hello World
Hello,2

22. 集合框架中泛型的好处?

在 J2SE 5 中引入了泛型来处理类型安全的对象。在泛型的强制下,只能将特定类型的对象存储在 Collection 中。

Java 中的泛型的一些优点如下:

消除类型转换

泛型出现后,不需要类型转换。演示此的示例如下:

// Before generics type casting was required.
List l = new ArrayList();
l.add("apple");
String str = (String) l.get(0); // type casting

// After generics type casting was no longer needed.
List<String> l = new ArrayList<String>();
l.add("apple");
String str = l.get(0); // no type casting

编译时检查

提供了编译时检查,以便在运行时没有问题,因为在编译时处理问题比在运行时处理问题要好得多。演示此的示例如下:

List<String> l = new ArrayList<String>();
l.add("apple");
l.add("mango");
l.add(98); // This will lead to a Compile Time Error

类型安全

泛型导致类型安全,因为只能在其中保留一种类型的对象。这意味着不允许使用其他类型的对象。

23. Java 中 ClassLoader 的作用

编译后,Java 类以字节代码的形式存储在 .class 文件中。需要时,ClassLoader 将 Java 程序的类加载到内存中。

ClassLoader 是分层的,因此,如果有加载类的请求,则会将其委派给父类加载器。使用此方法可以维护 Java Runtime Environment 中的唯一性。

Java 中内置的 ClassLoader 的类型如下:

  1. Bootstrap 类加载器
  2. 扩展类加载器
  3. 系统类加载器

给出了一个用 Java 演示 ClassLoader 的程序,如下所示:

public class Demo {
    public static void main(String[] args) {
        System.out.println("class loader for this class: " + Demo.class.getClassLoader());
        System.out.println("class loader for DNSNameService: " + sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());
        System.out.println("class loader for HashMap: " + java.util.HashMap.class.getClassLoader());
    }
}

上面程序的输出如下:

class loader for this class: sun.misc.Launcher$AppClassLoader@4e0e2f2a
class loader for DNSNameService: sun.misc.Launcher$ExtClassLoader@5c647e05
class loader for HashMap: null

24. 用 Java 确定给定日期的星期几

在 Java 中,Calendar.DAY_OF_WEEK 常量用于获取星期几。

该示例显示相同的内容:

import java.util.Calendar;

public class Example {
    public static void main(String[] args) {
        Calendar c = Calendar.getInstance();
        System.out.println(c.getTime().toString());
        System.out.println("Day = " + c.get(Calendar.DAY_OF_WEEK));
    }
}

该示例显示星期几:

Sun Dec 16 21:32:34 UTC 2018
Day = 1

25. 反转 Java 中的整数

反转整数涉及反转其所有数字。下面是一个示例:

Integer = 123
Reverse of the integer = 321

给出了一个演示用 Java 反转整数的程序,如下所示:

public class Demo {
    public static void main(String args[]) {
        int num = 2413, rev = 0;
        System.out.println("The number is " + num);
        while (num != 0) {
            rev = rev * 10;
            rev = rev + num % 10;
            num = num / 10;
        }
        System.out.println("Reverse of the above number is " + rev);
    }
}

上面程序的输出如下:

The number is 2413
Reverse of the above number is 3142

在上面的程序中,数字 2413 使用 while 循环反转,结果存储在 rev 中,然后显示出来。

26. 检查 Java 中的溢出

当给变量分配的值大于该变量的最大允许值时,就会发生溢出。如果发生溢出,JVM 不会抛出任何异常,并且程序员有责任处理溢出情况。

给出了一个检查 Java 中溢出的程序,如下所示:

public class Demo {
    public static void main(String[] args) {
        int num1 = 2147483647;
        int num2 = 1;
        System.out.println("Number 1: " + num1);
        System.out.println("Number 2: " + num2);
        long sum = (long) num1 + (long) num2;
        if (sum > Integer.MAX_VALUE) {
            throw new ArithmeticException("Overflow occurred!");
        }
        System.out.println("The sum of two numbers is: " + (int) sum);
    }
}

上面程序的输出如下:

Number 1: 2147483647
Number 2: 1
Exception in thread "main" java.lang.ArithmeticException: Overflow occurred!
at Demo.main(Demo.java:14)

说明:

  • 本文部分代码示例基于 Java 7 及 Java 8 特性(如多捕获块、Lambda 表达式)。
  • 部分日期处理示例使用了 legacy DateCalendar 类,在新版 Java 开发中推荐使用 java.time 包(Java 8+)。
  • 部分内部 API(如 sun.net)仅用于演示 ClassLoader,生产环境中不建议依赖。