Java 基础面试指南四

系列导航

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

1. 我可以使用受保护的方法重写公共方法吗?为什么会这样呢?

不能通过受保护(protected)的方法重写公共(public)方法。

在 Java 中,子类重写(Override)父类方法时,访问修饰符不能比父类方法的访问范围更严格。这是因为我们通过超类引用调用该方法,而超类引用指向的是子类的实现。由于引用的类型是父类,客户端代码基于父类的 API 编写,知道该方法的范围是更广的(public)。因此,限制重写方法(子类)的访问范围是没有意义的,但相反的做法(扩大访问范围)是允许的。

2. Java 中的字符串池是什么?

字符串(String)在 Java 中是不可变(Immutable)的。当我们在 String 中进行更改时,实际上会创建一个新的 String 对象。程序在运行时会创建大量的 String 对象,为了提供最佳性能,JVM 最小化了 String 对象的创建,并在堆内存中维护了一个字符串常量池(String Pool)(从 Java 7 开始,字符串池位于堆中)。

因此,当需要创建字符串字面量时,JVM 会首先在池中检查它是否存在:

  • 如果找到,它将返回该对象的引用。
  • 否则,它将创建新对象并将其保留在池中,以供以后使用。

3. 不可变类是什么意思?我们如何使类不可变?

不可变类(Immutable Class)不允许在创建后更改其对象的状态。如果需要更改对象的状态,它将创建一个新对象。不可变类最著名的例子之一是 String

要使一个类成为不可变类,需要注意以下几点:

  1. 类必须是 final 的:不允许扩展,因为子类可以访问敏感字段或方法,可能会更改对象的状态。
  2. 所有成员变量应声明为 private:防止外部直接访问。
  3. 不提供修改状态的公共 API:例如 setter 方法。即使需要修改,也应该创建自己的副本,在副本上进行更改并返回新对象。
  4. 可变成员应声明为 final:如果类的任何成员是可变的,应确保其引用不可变。
  5. 构造函数初始化与深克隆:通过构造函数初始化对象时,对作为参数传递的可变对象进行深层克隆(Deep Clone)。
  6. 不公开可变成员的引用:如果需要返回可变成员,需要正确克隆该成员并返回副本。这样可以确保即使通过引用更改了对象,也不会影响原对象的状态。

4. 什么是默认方法?为什么要进行此设计更改?

在 Java 的早期版本(Java 7 及之前)中,接口(Interface)被设计为仅具有方法签名。从 Java 8 开始,接口可以包含方法实现。这些方法应标记为 default 关键字,称为接口的默认方法(Default Methods)

设计原因:
这意味着实现类不必强制覆盖默认方法。当接口被许多应用程序广泛使用时,很难在同一接口中添加新方法,因为这会破坏现有代码(实现者需要在许多地方更改其代码)。为了克服这种复杂性并使接口向后兼容,引入了此更改。

示例:

public interface Shape {
    default double area() { 
        return 0.0;
    } 

    default double volume() { 
        return 0.0;
    }
}

5. 堆栈和堆内存有什么区别?为什么要使用堆栈来存储局部变量?

  • 堆栈内存(Stack Memory):用于在通过线程执行方法时创建局部变量和对象引用。这意味着每个线程都有一个单独的堆栈和一组局部变量。堆栈不包含实际的对象,它仅包含引用。
  • 堆内存(Heap Memory):实际对象的内存空间在堆内存中分配。堆内存由许多部分组成,包括年轻代(伊甸园和幸存者空间)和老年代。

对于每个方法调用,JVM 都会创建一个包含局部变量的新堆栈帧(Stack Frame)。将它们保持为堆栈结构有助于检索最近的堆栈帧,即在方法返回时轻松调用方方法的变量集。

6. String 是不可变的类吗?为什么将 String 定义为不可变的?

是的,String 是一个不可变类。

原因:
字符串广泛用于不同的场景,例如作为方法的参数共享、按名称加载类、作为值返回等。因此,很可能在没有其他用户知的情况下更改 String 对象,从而导致系统中出现错误。

  1. 安全性:在 HashMapHashTable 中,字符串常作为键(Key)。HashMap 中键的良好候选者应该是不可变的。如果 String 是可变的并且更改了其状态,则可能会导致为同一键检索错误的值或根本找不到它。
  2. 字符串池优化:字符串字面量从字符串池中获取,而不是每次都创建它。如果 String 是可变的,这将是不可能的,因为在其状态发生任何更改后,无法识别该值是否存在于池中。

7. 什么是克隆?浅克隆和深克隆有什么区别?

克隆(Clone)意味着创建对象的副本或创建重复的对象,它们的状态应该相同。一个对象可能由其他几个对象组成。

  • 浅克隆(Shallow Clone):创建一个新对象,并将其字段值分配给新对象的相应字段。由于字段仅包含驻留在堆中的对象的引用,因此新对象的字段也指向相同的组件实例。浅克隆虽然速度很快,但缺点是,如果更改了任何组件对象,它也会在克隆的对象中反映出来,因为这两个对象都持有相同对象的引用。
  • 深克隆(Deep Clone):不仅复制字段中的引用,还会创建组件对象的副本。与深克隆一样,所有组件对象都被克隆,这比较慢,但是会创建实际对象的真实副本。

8. Java 是否支持远程方法调用 (RMI)?

远程方法调用(RMI, Remote Method Invocation)是 Java 中的 API,它通过允许对象在可能位于同一台计算机或另一台远程计算机上(但在另一个地址空间)的另一个对象上调用方法,来管理分布式应用程序的创建。

RMI 中客户端和服务器之间的通信是通过分别使用客户端和服务器端上的存根对象(Stub)骨架对象(Skeleton)来完成的。

客户端与服务器之间的通信

  • 存根对象(Stub):客户端的所有传出请求都通过存根对象进行路由,因此它也被称为客户端对象的网关。
  • 骨架对象(Skeleton):服务器端的所有传入请求都通过骨架对象进行路由,因此也称为服务器端对象的网关。

创建远程方法调用程序的步骤:

  1. 首先,创建远程接口。
  2. 提供远程接口的实现。
  3. 编译实现类,并创建存根和骨架对象。
  4. 使用 rmiregistry 工具启动注册表服务。
  5. 远程应用程序已创建并启动。
  6. 客户端应用程序已创建并启动。
说明:上述关于 Skeleton 的描述适用于 Java 5 之前的版本。在现代 Java 版本中,RMI 通常使用动态代理,不再需要显式的 Skeleton 类。

9. Java 8 中的 Predicate

Predicatejava.util.function 包中定义的功能接口(Functional Interface)。它有助于改善代码的控制,可以在 Lambda 表达式和功能接口中用作分配目标。功能接口是只有一种抽象方法的接口。

@FunctionalInterface
public interface Predicate<T>

主要方法:

  • boolean test(T t)test() 方法根据给定的参数评估 Predicate。
  • default Predicate<T> and(Predicate<? super T> other)and() 方法通过该 Predicate 与另一个 Predicate 的短路逻辑与(Short-circuiting logical AND)返回一个组合后的 Predicate。如果另一个为 null,则抛出 NullPointerException。如果此 Predicate 为假,则不评估另一个 Predicate。
  • default Predicate<T> negate()negate() 方法返回表示该 Predicate 逻辑非的 Predicate。
  • default Predicate<T> or(Predicate<? super T> other)or() 方法返回由该 Predicate 和另一个 Predicate 的短路或(Short-circuiting logical OR)得出的组合 Predicate。如果该 Predicate 为 true,则不评估另一个 Predicate。
  • static <T> Predicate<T> isEqual(Object targetRef)isEqual() 方法返回一个 Predicate,该 Predicate 根据 Objects.equals(Object, Object) 测试两个参数是否相等。

示例程序:

import java.util.function.Predicate;  

public class Example {  
    public static void main(String[] args) {
        Predicate<Integer> a = n -> (n % 3 == 0);  // 使用 Lambda 表达式创建 Predicate
        System.out.println(a.test(36));  // 调用 Predicate 的 test 方法
    }
}

输出如下:

$javac Example.java
$java Example
true

10. Java 8 中的新日期/时间 API

Java 8 引入了新的日期/时间应用程序接口 (API) 以解决以前日期/时间 API 的缺点。

  • 不是线程安全的java.util.Date 类不是线程安全的。当 Date 类的线程彼此之间的顺序不完整时,这迫使开发人员解决并发问题。新的日期时间 API 是不可变的,并且没有 setter 方法。
  • 设计不良:默认日期自 1900 年开始就已过时,月份从 1 开始,而天从 0 开始,无法实现统一性。旧的 API 对于要执行的功能没有那么直接的方法。新的 API 为此类操作提供了许多有用的方法。
  • 时区的不便处理:时区所面临的问题需要开发人员编写大量代码。开发新 API 时要牢记特定领域的蓝图。

新的 API 已列在 java.time 包下。一些重要类包括:

  • LocalLocal 类是简化的日期/时间 API,没有时区处理的麻烦。
  • ZonedZoned 类是考虑了时区的简化的日期/时间 API。

Local 类示例:

import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
import java.time.Month;

public class Example {
    public void checkDate() {
        LocalDateTime currentTime = LocalDateTime.now(); // 计算本地日期/时间
        System.out.println("Present DateTime: " + currentTime);
        
        LocalDate date = currentTime.toLocalDate();  // 计算本地日期
        System.out.println("Present Local Date : " + date);
        
        // 计算当前本地时间的小时、分钟和秒
        int second = currentTime.getSecond();  
        int minute = currentTime.getMinute();
        int hour = currentTime.getHour();
        System.out.println("Hour: " + hour + "|Minute: " + minute + "|seconds: " + second);
    }

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

输出如下:

$javac Example.java
$java Example
Present DateTime: 2018-12-13T18:39:24.730
Present Local Date : 2018-12-13
Hour: 18|Minute: 39|seconds: 24

Zoned 类示例:

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

public class Example {
    public static void Zone() {
        LocalDateTime dt = LocalDateTime.now();
        DateTimeFormatter format = DateTimeFormatter.ofPattern(" HH:mm:ss dd-MM-YYYY");  
        String fcd = dt.format(format); // 存储格式化的当前日期
        System.out.println("Present formatted Time and Date: " + fcd);  
        
        ZonedDateTime zone = ZonedDateTime.now();  
        System.out.println("The Present zone is " + zone.getZone());  
    }

    public static void main(String[] args) {  
        Zone();
    }
}

输出如下:

$javac Example.java
$java Example
Present formatted Time and Date:  19:24:52 13-12-2019
The Present zone is Etc/UTC

11. Java 8 中的 Optional

Optional 是一个对象容器,其中可能不包含非 null 值。如果有值,则 isPresent() 将返回 true,而 get() 将返回该值。提供了依赖于包含值的可用性的补充方法,例如 orElse() 方法(如果不存在值,则返回默认值)和 ifPresent()(如果存在值,则执行代码块)。

Optional 的语法:

public final class Optional<T> extends Object

Optional 是基于值的类。与身份有关的操作,包括引用相等 (==)、身份哈希码或对该类对象的同步,可能会产生空前的结果,应避免使用。

示例(使用 isPresent() 方法):

import java.util.Optional;  

public class Example {  
    public static void main(String[] args) {
        String s1 = new String("Hello");
        String s2 = null;
        
        Optional<String> obj1 = Optional.ofNullable(s1);
        Optional<String> obj2 = Optional.ofNullable(s2);
        
        if (obj1.isPresent()) {    // 检查 String 对象是否存在
            System.out.println(s1.toUpperCase());  
        } else {
            System.out.println("s1 is a Null string");
        }
        
        if (obj2.isPresent()) {   // 检查 String 对象是否存在
            System.out.println(s1.toUpperCase());
        } else {
            System.out.println("s2 is a Null string");
        }
    }
}

输出如下:

$javac Example.java
$java Example
HELLO
s2 is a Null string

12. 内置的编码器和解码器,用于 Java 8 中的 Base64 编码

Base64 类包含用于获取用于 Base64 编码的编码器和解码器的静态方法。Java 8 允许我们使用三种类型的编码:

  • 基本(Basic):输出被绘制为一组以 Base64 字母形式存在的字符。编码器不会在输出中添加任何换行符,并且解码器会拒绝除 Base64 字母以外的任何字符。
  • 统一资源定位符 (URL) 和文件名安全:将输出绘制为以 Base64 字母表示的字符集。解码器拒绝包含 Base64 字母之外的字符的数据。
  • 多用途 Internet 邮件扩展 (MIME):输出被绘制为 MIME 兼容格式。编码后的输出必须每行最多包含 76 个字符,并应用回车符 \r,然后立即使用换行符 \n 作为行分隔符。编码输出的末尾没有换行分隔符。解码器将忽略除 Base64 字母以外的所有字符。

Base64 类的声明:

public class Base64 extends Object

基本 Base64 编码和解码示例:

import java.util.Base64;

public class Example {
    public static void main(String[] args) {
        String enc = Base64.getEncoder().encodeToString("Encoding in Base64".getBytes());
        System.out.println("Encoder Output : " + enc);
        
        byte[] dec = Base64.getDecoder().decode(enc);
        System.out.println("Decoder Output : " + new String(dec));
    }
}

输出如下:

$javac Example.java
$java -Xmx128M -Xms16M Example
Encoder Output : RW5jb2RpbmcgaW4gQmFzZTY0
Decoder Output : Encoding in Base64

13. 检查两个字符串是否在 Java 中彼此为字谜

字符串的字谜(Anagram)是具有相同字符和相同频率的字符串。只有字符顺序可以不同。

  • 示例:String = silent, Anagram = listen

检查字谜的程序:

import java.io.*;
import java.util.*;

public class Demo {
    static boolean checkAnagram(char s1[], char s2[]) {
        int alphaCount1[] = new int[256];
        Arrays.fill(alphaCount1, 0);
        
        int alphaCount2[] = new int[256];
        Arrays.fill(alphaCount2, 0);
        
        int i;
        for (i = 0; i < s1.length && i < s2.length; i++) {
            alphaCount1[s1[i]]++;
            alphaCount2[s2[i]]++;
        }
        
        if (s1.length != s2.length)
            return false;
            
        for (i = 0; i < 256; i++) {
            if (alphaCount1[i] != alphaCount2[i])
                return false;
        }
        return true;
    }

    public static void main(String args[]) {
        String str1 = "triangle";
        String str2 = "integral";
        
        char s1[] = str1.toCharArray();
        char s2[] = str2.toCharArray();
        
        System.out.println("String 1: " + str1);
        System.out.println("String 2: " + str2);
        
        if (checkAnagram(s1, s2))
            System.out.println("The two strings are anagram of each other");
        else
            System.out.println("The two strings are not anagram of each other");
    }
}

输出如下:

String 1: triangle
String 2: integral
The two strings are anagram of each other

14. 比较 Java 中的 sleep() 和 wait() 方法

可以使用 sleep()wait() 方法在 Java 中的多线程环境中暂停线程的执行。使用 sleep() 将线程暂停所需的时间,而使用 wait() 使其进入等待状态,则只能通过调用 notify()notifyAll() 来使线程恢复。

区别对比:

sleep() 方法wait() 方法
通常在当前正在执行的线程上调用 sleep() 方法。在对象上调用 wait() 方法。锁定对象必须与当前线程同步。
sleep() 方法不会释放监视器或锁定。wait() 方法释放监视器或锁定。
sleep() 方法用于在指定的时间内暂停执行。wait() 方法可用于线程间通信。
在使用 sleep()interrupt() 所需的时间后,线程被唤醒。对象调用 notify()notifyAll() 方法后,线程被唤醒。
sleep() 方法可用于多线程同步。wait() 方法可用于线程同步。

sleep() 示例:

synchronized(LOCK) {  
    Thread.sleep(1000);
}

wait() 示例:

synchronized(LOCK) {  
    LOCK.wait();
}

15. 关于 Java 多线程的同步

多个线程可以使用同步(Synchronization)来管理对共享资源的访问,其中一次只能有一个线程访问该资源。

Java 中有两种类型的线程同步:

  1. 互斥(Mutual Exclusion)
  2. 线程间通讯(Inter-thread Communication)

线程同步程序示例:

class Demo {
    synchronized void display(int n) {
        for (int i = 1; i <= 5; i++) {
            System.out.println(n);
            try {
                Thread.sleep(400);
            } catch (Exception e) {
                System.out.println(e);
            }
        }
    }
}

class Thread1 extends Thread {
    Demo obj;
    Thread1(Demo obj) {
        this.obj = obj;
    }
    public void run() {
        obj.display(8);
    }
}

class Thread2 extends Thread {
    Demo obj;
    Thread2(Demo obj) {
        this.obj = obj;
    }
    public void run() {
        obj.display(3);
    }
}

public class SynchronizationDemo {
    public static void main(String args[]) {
        Demo obj1 = new Demo();
        Thread1 thr1 = new Thread1(obj1);
        Thread2 thr2 = new Thread2(obj1);
        thr1.start();
        thr2.start();
    }
}  

输出如下:

8
8
8
8
8
3
3
3
3
3

16. 在 Java 中,何时使用 double 型而不是 float 型?

double 类型和 float 类型都用于表示 Java 中的浮点数。但是,在某些情况下,双精度型更好,在某些情况下,浮点型更好。

  • 精度:如果需要更精确的结果,则双精度型优于浮点型。double 类型的精度最高为 15 到 16 个小数点,而 float 类型的精度仅为 6 到 7 个十进制数字。
  • 范围double 类型具有更大的范围。它对符号使用 1 位,对指数使用 11 位,对尾数使用 52 位;而 float 型仅对符号使用 1 位,对指数使用 8 位,对尾数使用 23 位。

示例程序:

public class Demo {
    public static void main(String[] args) {
        double d = 55.637848675695785;
        float f = 25.657933f;
        System.out.println("Value of double = " + d);
        System.out.println("Value of float = " + f);
    }
}

输出如下:

Value of double = 55.637848675695786
Value of float = 25.657932

17. 在 Java 中使用多线程的最佳实践

使用多线程可以实现线程最重要的用法,这意味着多个任务可以并行运行。Java 的一些多线程最佳实践如下:

  1. 最小化锁定范围:由于不能同时执行锁中的任何代码,因此应将锁定范围最小化,这会降低应用程序性能。
  2. 并发集合应优先于同步集合:并发集合应该比同步集合更可取,因为它们可以提供更多的可伸缩性和性能。
  3. 首选不可变类:应该使用不可变的类(例如 String, Integer 等)以及其他包装器类,因为它们可以简化并发代码的编写。这是因为无需担心状态。
  4. 应该首选线程池执行器,而不是线程:对于可伸缩的 Java 应用程序来说,线程池是更好的选择,因为线程创建非常昂贵。
  5. 使用局部变量:不应使用类或实例变量,而应尽可能使用局部变量。
  6. 生产者 - 消费者设计应首选 BlockingQueue:实施生产者消费者设计模式的最佳方法是使用 BlockingQueue。这是非常重要的,因为许多并发问题都是基于生产者消费者设计的。
  7. 同步实用程序应优先于等待通知:有许多同步实用程序,例如 CyclicBarrier, CountDownLatchSemaphore 都应使用,而不是等待和通知。
  8. 使用信号量创建边界:建立一个稳定的系统应该在不同的资源(例如文件系统,数据库,套接字等)上有界限。可以使用信号量创建这些界限。

18. 在 Java 中使用 Collections 时的最佳实践

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

最佳实践:

  1. 选择合适的集合:在使用集合之前,需要根据需要解决的问题选择合适的集合。
  2. 使用数组和集合实用程序类:应该根据需要使用 Java Collections Framework 提供的 ArraysCollections 实用程序类,因为它们提供了许多有用的方法来搜索,排序和修改集合中的元素。
  3. 如果可能,指定集合的初始容量:集合的初始容量始终由几乎所有具体集合类中都包含的重载构造函数指定。
  4. 优先于 isEmpty() 而不是 size():在检查集合是否为空时,应优先使用 isEmpty() 方法而不是 size() 方法。即使这两种方法在性能上存在差异,也可以这样做以提高此代码的可读性。
  5. 不要在返回集合的方法中返回 null:如果方法返回一个集合,那么如果集合中没有元素,则该方法不应返回 null。相反,它应该返回一个空集合。
  6. 在集合上使用 Stream API:Java 8 中的每个集合中都有一个流方法,该方法返回元素流。这意味着可以使用 Stream API 轻松地执行聚合功能。
  7. 不要使用经典的 for 循环:与其使用经典的 for 循环来迭代列表集合,不如使用迭代器。这是因为如果在循环内部更改了 for 循环变量,则可能导致错误。

19. 为什么在 Java 8 中引入了 Streams?

流(Stream)支持聚合操作,并在 Java 8 中引入。它是一系列对象,具有诸如 Sorted, Map, Filter 等操作。

对于流,使用以下软件包:

import java.util.stream.*;

Stream 操作示例(map 和 collect):

import java.util.*;
import java.util.stream.*;

public class Demo {
    public static void main(String args[]) {
        List<Integer> l = Arrays.asList(29, 35, 67);
        List<Integer> res = l.stream().map(a -> a * a).collect(Collectors.toList());
        System.out.println(res);
    }
}

20. Java 中的内存不足错误?

Java 中的所有对象都是从堆内存中分配的内存。当由于没有更多的可用内存而无法分配对象任何内存,并且使用垃圾回收器无法获得任何内存时,则 Java 中会发生 OutOfMemoryError 异常。

如果一次处理的数据过多或对象保留的时间太长,通常会发生内存不足错误。由于程序员无法控制的问题(例如在部署后无法清除的应用服务器)也可能发生此异常。

示例程序:

import java.util.*;

public class Demo {
    static List<String> l = new ArrayList<String>();
    public static void main(String args[]) throws Exception {
        Integer[] arr = new Integer[5000 * 5000];
    }
}

输出如下:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at Demo.main(Demo.java:9)

21. Java 中的 volatile 关键字

使用 volatile 关键字可以使类成为线程安全的。这意味着多个线程可以同时使用类实例或方法,而不会出现任何问题。

volatile 关键字的含义:

  1. 具有 volatile 关键字的变量的值永远不会在本地缓存线程。这意味着对变量的所有读和写操作都保存在主存储器中。
  2. 对该变量的访问就好像它被包含在一个同步块中并且本身已同步一样。

示例:

class Shared {
    static volatile int value = 15;
}

在上面的示例中,如果一个线程进行了任何更改,那么它们也将使用 volatile 关键字反映在其他线程中。

volatile 和 synchronized 之间的区别:

特性volatilesynchronized
是否可以使用 null?
变量类型对象变量或原始变量对象变量
何时同步?当访问 volatile 变量时显式进入或退出同步块时
所有缓存的变量是否在访问时同步?从 Java 5 开始,这是正确的
可以用于将多个操作合并为一个原子操作吗?在 Java 5 之前这是不可能的

22. Java 中的 Iterator vs ListIterator

Java 中的 IteratorListIterator 都是 Collection 框架中的接口。

  • Iterator:用于通过向前迭代各个元素来遍历 Collection 元素。
  • ListIterator:扩展了 Iterator,并用于在向前和向后两个方向遍历 Collection 元素。另外,可以使用 ListIterator 在 Collection 中添加,修改和删除元素,而使用 Iterator 则无法实现。

区别对比:

IteratorListIterator
迭代器用于沿向前方向遍历 Collection 元素。ListIterator 用于在向前和向后方向遍历 Collection 元素。
可以使用迭代器遍历 Map, List, Set 等。使用 ListIterator 只能遍历 List 对象。
元素不能由 Iterator 修改 Collection 中的元素。可以通过 ListIterator 在 Collection 中修改元素。
元素不能由迭代器添加到集合中。元素可以通过 ListIterator 添加到 Collection 中。
迭代器中没有方法可以在 Collection 中查找元素索引。ListIterator 中有一个方法可以在 Collection 中查找元素索引。

23. 限制 Java 中类的继承

可以使用关键字 final 对 Java 中的类限制继承。换句话说,如果将一个类声明为 final,则不能从该类扩展任何其他类。这在创建不可变类时非常有用。

示例:

final class C1 {
    // methods and fields
}

class C2 extends C1 {
    // Not possible
}

如果类 C2 试图扩展类 C1,因为它是最终类,因此将无法继承,则会生成错误。

24. Java 中的空白或未初始化的 final 变量

Java 中的 final 变量是一种特殊类型的变量,只能在声明时或在其他时间一次赋值。

在声明时未分配值的最终变量称为空白或未初始化的最终变量(Blank Final Variable)。换句话说,此变量不会在其声明时初始化。

示例:

final int val;    // 这是一个空白 final 变量
val = 6;

演示程序:

class Demo {
    final int num;
    Demo(int num1) {
        num = num1;
    }
}

public class Main {
    public static void main(String args[]) {
        Demo obj = new Demo(87);
        System.out.println("Value of num is: " + obj.num);
    }
}

输出如下:

Value of num is: 87

25. 为 Java 中的文件设置权限

当用户需要限制文件允许的操作时,将在文件上设置文件许可权。

文件的允许权限:

  1. 可读:此权限测试应用程序是否可以读取由抽象路径名表示的文件。
  2. 可写:此权限测试应用程序是否可以更改由抽象路径名表示的文件。
  3. 可执行:此权限测试应用程序是否可以执行由抽象路径名表示的文件。

可用于更改文件权限的方法为 setExecutable, setReadablesetWritable

演示程序:

import java.io.*;

public class Demo {
    public static void main(String[] args) {
        File f = new File("C:\\Users\\Aaron\\Desktop\\text.txt");
        boolean exists = f.exists();
        
        if (exists == true) {
            f.setExecutable(true);
            f.setReadable(true);
            f.setWritable(false);
            System.out.println("The File permissions are now changed.");
            System.out.println("Executable: " + f.canExecute());
            System.out.println("Readable: " + f.canRead());
            System.out.println("Writable: " + f.canWrite());
        } else {
            System.out.println("File not found.");
        }
    }
}

输出如下:

The File permissions are now changed.
Executable: true
Readable: true
Writable: false 

26. 用 Java 创建不可变类

Java 中的不可变类是在构造后无法更改其状态的类。

创建要求:

  1. 该类应该是 final 类,以便其他类不能扩展它。
  2. 该类的所有字段都应为 final,以便它们只能在构造函数中初始化一次。
  3. 设置方法(Setter)不应该公开。
  4. 如果公开了任何修改类状态的方法,则应返回该类的新实例。
  5. 如果构造函数中存在可变对象,则仅应使用传递的参数的克隆副本。

示例:

public final class Employee {
    final int empNum;
    final String name;
    final int salary;

    public Employee(int empNum, String name, int salary) {
        this.empNum = empNum;
        this.name = name;
        this.salary = salary;
    }

    public int empNumReturn() {
        return empNum;
    }

    public String nameReturn() {
        return name;
    }

    public int salaryReturn() {
        return salary;
    }
}

上面给出的类是基本的不可变类。此类不包含任何可变对象或任何 setter 方法。这种类型的基本类通常用于缓存。

27. Java 中的协变返回类型

协变返回类型(Covariant Return Type)是指重写方法的返回类型。从 Java 5 开始,子类中的重写方法可能具有不同的返回类型。但是,子类的返回类型应该是父类的返回类型的子类型。

示例程序:

class C1 {  
    C1 ret() {
        return this;
    }  
}  

public class C2 extends C1 {  
    C2 ret() {
        return this;
    }  
    
    void display() {
        System.out.println("This is the covariant return type");
    }  
    
    public static void main(String args[]) {  
        new C2().ret().display();  
    }  
}  

输出如下:

This is the covariant return type

上面的程序演示了协变返回类型,因为 C1ret() 方法的返回类型为 C1,而 C2ret() 方法的返回类型为 C2,并且这两种方法都具有不同的返回类型时,它是方法覆盖。


说明:本文内容主要基于 Java 8 特性及部分早期 Java 版本机制(如 RMI)。部分代码示例在不同 JDK 版本下输出可能略有差异,实际开发中请以最新官方文档为准。