普通视图

Received before yesterday陶其的个人博客

在CentOS7安装软件系列 — JDK8安装

作者陶其
2025年2月28日 15:05

感谢订阅陶其的个人博客!

前提配置:
物理机系统:Windows11 家庭中文版(已激活),内存 32G,已联网
VMware 版本:17.6.1
CentOS 系统版本:CentOS Linux release 7.9.2009(Core)
CentOS 已安装vim

预安装JDK版本:jdk-8u241-linux-x64

一、卸载openjdk

安装jdk 1.8之前先将openjdk卸载掉。

1.1 卸载已存在的openjdk

某些linux系统下默认已安装openjdk,这个版本的jdk我们一般不使用,我们需要把它卸载,再重新安装oracle的JDK。

输入以下命令,查看已有的openjdk版本。

rpm -qa|grep jdk

如果输出了几行带有openjdk字样的结果,说明你的linux默认安装了;
如果没输出什么,说明你的系统没安装,可以跳过下面的一步。

1.2 卸载示例

输入以下命令将openjdk卸载。

yum -y remove copy-jdk-configs-3.3-10.el7_5.noarch

yum -y remove xxx,xxx代表上一步输出一行的结果,每一个都在执行一下卸载

卸载掉所有通过rpm -qa|grep jdk命令输出的openjdk文件,卸载完成之后java相关的命令都不可使用(如:javajava -versionjavac等),可以试验一下


二,安装JDK

本文使用版本:jdk-8u241-linux-x64

2.1 下载方式

  • 官网下载:点此跳转 (国外网站,访问较慢,需要注册登录Oracle)
    根据自己的需求选择版本,本文推荐版本:Linux x64 Compressed Archive
  • 百度网盘: 可以在资源下载页【技术专栏 > 3】找到。

2.2 上传到服务器

使用工具将压缩包上传到linux,本人使用的是:FinalShell

创建一个目录用于存放安装包

cd /usr/local
mkdir package

将安装包上传至路径/usr/local/package文件夹中

2.3 解压

我们先在/usr/local路径下新建文件夹【java】,用于存放我们的JDK安装文件。

cd /usr/local
mkdir java

然后我们再回到/usr/local/package执行下面的语句,会将压缩包解压到/usr/local/java路径中。

注意:jdk-8u241-linux-x64.tar.gz是我的版本文件名,如果你们的压缩包名不一样,可以修改,最好用Tab键自动生成以免打错。

cd /usr/local/package
tar -zxvf jdk-8u241-linux-x64.tar.gz -C /usr/local/java

等疯狂的刷屏结束之后,解压就完成了。

可以查看一下解压结果:

2.4 配置JAVA_HOME(全局配置)

请保证输入法全程都在英文状态下,否则会出现意想不到的问题。

1、 打开/etc/profile文件

vim /etc/profile

2、 编辑:按【下键】移动光标到文末,按【i】开始输入,复制下面的代码粘贴进去

如果你的版本与我的不同一定要记得修改代码里的版本号。
如果不确定自己版本号是什么,可以去路径/usr/local/java下看一下文件夹名。

这一步是为了让指令javajavac成为全局指令。

export JAVA_HOME=/usr/local/java/jdk1.8.0_241/
export JRE_HOME=/usr/local/java/jdk1.8.0_241/jre
export PATH=$PATH:/usr/local/java/jdk1.8.0_241/bin
export CLASSPATH=./:/usr/local/java/jdk1.8.0_241/lib:/usr/local/java/jdk1.8.0_241/jre/lib

3、 保存。粘贴好之后,将光标移到最后,按下【Ctrl+C】,输入英文的冒号【:】,在输入【wq】,回车就保存成功了。
可以查看一下结果,cat /etc/profile

4、 生效。执行下面代码让文件生效,没有结果返回

source /etc/profile

2.5 验证结果

执行命令查看结果:

java -versionjavajavac

如果出现下面的三张图,那么就意味着安装成功了。

喜欢在CentOS7安装软件系列 — JDK8安装这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

面试笔记03-Java基础(三):异常处理

作者陶其
2025年2月25日 15:58

感谢订阅陶其的个人博客!

一、异常的概念

Java语言中,异常是程序运行时出现的不正常或意外情况。

二、异常的分类

1. 异常的分类
答:
Java异常都继承自 Throwable,主要分两类:

  • Error:系统严重错误,如OutOfMemoryError(堆内存溢出错误),程序无法处理。
  • Exception:程序可处理异常,又分为:
    • 受检异常:编译时需处理,如IOException(输入输出异常)。
    • 非受检异常:编译时不强制处理,如NullPointerException(空指针异常)。

2. 受检异常和非受检异常在使用上有什么区别?
答:

  • 受检异常继承自 Exception。使用时:
    • 要么在方法内部使用 try-catch 捕获处理,
    • 要么在方法声明处使用 throws 关键字抛出,否则编译不通过。
  • 非受检异常继承自 RuntimeException。使用时:
    • 不需要在方法声明中显式抛出,
    • 也不强制要求在方法内部捕获,编译能正常通过。

3. 常见的Error、受检异常、非受检异常有哪些?
答:
常见 Error

  • StackOverflowError:栈溢出错误
  • OutOfMemoryError:内存溢出错误
  • NoClassDefFoundError:类定义未找到错误
  • LinkageError:链接错误

常见受检异常

  • IOException:输入输出异常
  • SQLException:数据库操作异常
  • ClassNotFoundException:类未找到异常
  • InterruptedException:线程中断异常

常见非受检异常

  • NullPointerException:空指针异常
  • ArrayIndexOutOfBoundsException:数组越界异常
  • ArithmeticException:算术异常
  • NumberFormatException:数字格式异常
  • IllegalArgumentException:非法参数异常
  • IndexOutOfBoundsException:索引越界异常

4. 分别给出一个Error、受检异常、非受检异常的简单示例代码。
答:
(1) Error 示例
StackOverflowError 属于 Error 类型,通常在方法无限递归调用时产生,因为每次方法调用都会在栈上分配空间,无限递归会使栈空间耗尽。

// 测试类
public class StackOverflowErrorExample {
    public static void recursiveMethod() {
        // 无限递归调用自身
        recursiveMethod();
    }

    public static void main(String[] args) {
        try {
            recursiveMethod();
        } catch (StackOverflowError e) {
            System.out.println("捕获到 StackOverflowError: " + e);
        }
    }
}

测试结果:

捕获到 StackOverflowError: java.lang.StackOverflowError

解释:
recursiveMethod 方法中,它不断地调用自身,没有终止条件,最终会导致栈空间耗尽,抛出 StackOverflowError。在 main 方法里,使用 try-catch 块捕获该错误并输出错误信息。

(2) 受检异常示例
IOException 是典型的受检异常,在进行文件操作、网络操作等可能发生输入输出问题的场景中经常出现,使用时必须进行处理。

import java.io.FileReader;
import java.io.IOException;

public class IOExceptionExample {
    public static void readFile() throws IOException {
        // 尝试打开一个文件进行读取
        FileReader reader = new FileReader("nonexistentfile.txt");
        int data;
        while ((data = reader.read()) != -1) {
            System.out.print((char) data);
        }
        reader.close();
    }

    public static void main(String[] args) {
        try {
            readFile();
        } catch (IOException e) {
            System.out.println("捕获到 IOException: " + e.getMessage());
        }
    }
}

测试结果:

捕获到 IOException: nonexistentfile.txt (系统找不到指定的文件。)

解释:
readFile 方法尝试打开一个不存在的文件 nonexistentfile.txt 进行读取,这会引发 IOException
由于 IOException 是受检异常,readFile 方法使用 throws 关键字声明抛出该异常。
main 方法中,调用 readFile 方法并使用 try-catch 块捕获并处理该异常。

(3) 非受检异常示例
NullPointerException 属于非受检异常,通常在尝试对一个 null 对象进行操作时抛出。

public class NullPointerExceptionExample {
    public static void main(String[] args) {
        String str = null;
        try {
            // 对 null 对象调用方法,会抛出 NullPointerException
            int length = str.length();
            System.out.println("字符串长度: " + length);
        } catch (NullPointerException e) {
            System.out.println("捕获到 NullPointerException: " + e);
        }
    }
}

测试结果:

捕获到 NullPointerException: java.lang.NullPointerException

解释:
main 方法中,定义了一个 null 的字符串对象 str,然后尝试调用它的 length() 方法,这会导致 NullPointerException 异常。使用 try-catch 块捕获并处理该异常。

三、try-catch-finally 语句

1. 简述 try-catch-finally 语句块中各部分的作用
答:

  • try 块包含可能抛出异常的代码;
  • catch 块用于捕获并处理 try 块中抛出的指定类型异常;
  • finally 块无论是否发生异常都会执行,常用于释放资源。

2. 在 try-catch-finally 结构中,如果 try 块中有 return 语句,finally 块中的代码会执行吗?
答:
会执行。
try 块遇到 return 语句时,会先保存返回值,然后执行 finally 块中的代码,最后再返回之前保存的值。

3. catch 块捕获异常后,若 finally 块中有 return 语句,会对 trycatch 块中的 return 结果有什么影响?
答:
finally 块中有 return 语句,会覆盖 trycatch 块中的 return 结果,最终返回 finally 块中 return 的值。

4. 请给出一个经典且简洁的try-catch-finally 语句的使用场景
答:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class TryCatchFinallyExample {
    public static void main(String[] args) {
        BufferedReader reader = null;
        try {
            // 尝试打开文件进行读取
            reader = new BufferedReader(new FileReader("test.txt"));
            String line;
            // 逐行读取文件内容并输出
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            // 捕获并处理可能出现的输入输出异常
            System.err.println("读取文件时出现错误: " + e.getMessage());
        } finally {
            try {
                if (reader != null) {
                    // 确保文件读取器被关闭
                    reader.close();
                }
            } catch (IOException e) {
                // 处理关闭文件读取器时可能出现的异常
                System.err.println("关闭文件读取器时出现错误: " + e.getMessage());
            }
        }
    }
}

代码解释

  • try 块: 代码尝试打开并读取文件 test.txt 的内容,将文件内容逐行输出。由于文件操作可能会出现异常,如文件不存在、文件无法访问等,所以这些操作被放在 try 块中。
  • catch 块:try 块中的代码抛出 IOException 时,catch 块会捕获该异常,并将错误信息打印到标准错误输出。
  • finally 块: 无论 try 块中是否发生异常,finally 块中的代码都会执行。在这个例子中,finally 块用于关闭文件读取器 BufferedReader,以确保资源被正确释放。关闭操作也可能会抛出 IOException,因此同样使用 try-catch 语句来处理。

四、自定义异常

1. 为什么需要自定义异常?
答:
在实际开发中,Java内置的异常类型可能无法精准描述业务场景中出现的问题。
自定义异常能使异常信息更具针对性,便于代码的维护和调试,让开发者可以根据业务需求对特定情况进行异常处理。

2. 简述自定义异常的步骤。
答:
(1) 创建一个类继承自 Exception(受检异常)或 RuntimeException(非受检异常)。
(2) 提供至少一个构造方法,通常包含一个接收异常信息字符串的构造方法。
(3) 若有需要,可添加其他自定义方法。

3. 给出一个经典的自定义异常的简单示例代码
答:
以下是一个自定义异常示例,模拟在一个简单的银行账户系统中,当尝试取款金额超过账户余额时抛出异常。(日常我们判定余额不足并不会使用异常抛出,这里只是作示例。)
自定义异常类

// 自定义异常类(余额不足异常),继承自 Exception,属于受检异常
class InsufficientBalanceException extends Exception {
    // 构造方法,接收异常信息
    public InsufficientBalanceException(String message) {
        super(message);
    }
}

银行账户类

// 银行账户类
class BankAccount {
    // 成员变量:账户余额
    private double balance;

    // 构造方法,初始化账户余额
    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    // 取款方法,可能抛出 InsufficientBalanceException 异常
    public void withdraw(double amount) throws InsufficientBalanceException {
        if (amount > balance) {
            // 当取款金额大于账户余额时,抛出异常
            throw new InsufficientBalanceException("余额不足,无法取出 " + amount + " 元。");
        }
        balance -= amount;
        System.out.println("成功取出 " + amount + " 元,当前余额为 " + balance + " 元。");
    }
}

测试代码

// 测试类
public class CustomExceptionTest {
    public static void main(String[] args) {
        // 创建一个初始余额为 1000 元的银行账户
        BankAccount account = new BankAccount(1000);

        try {
            // 尝试取款 1500 元
            account.withdraw(1500);
        } catch (InsufficientBalanceException e) {
            // 捕获并处理异常,输出异常信息
            System.out.println("捕获到异常: " + e.getMessage());
        }

        try {
            // 尝试取款 500 元
            account.withdraw(500);
        } catch (InsufficientBalanceException e) {
            System.out.println("捕获到异常: " + e.getMessage());
        }
    }
}

运行结果:

捕获到异常: 余额不足,无法取出 1500.0 元。
成功取出 500.0 元,当前余额为 500.0 元。

代码解释

  • 自定义异常类 InsufficientBalanceException:继承自 Exception,表示这是一个受检异常。它包含一个构造方法,用于接收异常信息并传递给父类。
  • 银行账户类 BankAccount:包含一个 balance 字段表示账户余额,以及一个 withdraw 方法用于取款。在 withdraw 方法中,如果取款金额大于账户余额,就会抛出 InsufficientBalanceException 异常。
  • 测试类 CustomExceptionTest:创建一个银行账户对象,分别尝试取款 1500 元和 500 元,并使用 try-catch 块捕获和处理可能抛出的 InsufficientBalanceException 异常。

喜欢面试笔记03-Java基础(三):异常处理这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

面试笔记02-Java基础(二):面向对象

作者陶其
2025年2月24日 13:45

感谢订阅陶其的个人博客!

一、类与对象

1. 请解释类和对象的概念,以及它们之间的关系
答:

  • 是对具有相同属性和行为的一组对象的抽象描述。
    它定义了对象的属性(成员变量)和行为(方法),是创建对象的模板。
  • 对象是类的具体实例,是根据类创建出来的一个个独立个体,每个对象都有自己的状态(属性值)。
  • 类和对象的关系是抽象与具体的关系。
    类是对象的蓝图,对象是类的具体表现。
    例如,“汽车” 类描述了汽车的通用属性(颜色、品牌等)和行为(行驶、刹车等),而某一辆具体的红色宝马汽车就是 “汽车” 类的一个对象。

2. 简述构造方法的作用和特点
答:
构造方法的作用是在创建对象时对对象进行初始化操作,为对象的成员变量赋初始值。
其特点如下:

  • 方法名与类名相同:构造方法的名称必须和所在类的名称一致。
  • 没有返回值类型:构造方法不能有返回值类型,连 void 也不能有。
  • 可重载:一个类中可以有多个构造方法,它们的参数列表不同,以满足不同的初始化需求。例如:

    class Person {
    String name;
    int age;
    
    // 无参构造方法
    public Person() {
        name = "未知";
        age = 0;
    }
    
    // 带参构造方法
    public Person(String n, int a) {
        name = n;
    age = a;
    }
    }

二、重载与重写

1. 什么是方法重载?请举例说明。
答:
方法重载是指在一个类中可以定义多个方法名相同,但参数列表不同(参数的类型、个数或顺序不同)的方法。
调用时,Java 根据传入的实参类型、个数和顺序来决定调用哪个方法。
例如:

class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
    public double add(double a, double b) {
    return a + b;
    }
}

2. 方法重载是否与返回值类型有关?
答:
方法重载与返回值类型无关。

3. 什么是方法重写?重写需要满足哪些条件?
答:
方法重写是指子类重新定义父类中已有的方法。
需要满足以下条件:

  • 方法名、参数列表和返回值类型必须与父类被重写的方法相同(Java 5 开始,返回值类型可以是父类方法返回值类型的子类,即协变返回类型)。
  • 访问权限不能比父类被重写方法更严格。
  • 不能抛出比父类被重写方法更多、更宽泛的异常。

例如:

// 父类
class Animal {
    public void sound() {
        System.out.println("动物发声");
    }
}
// 子类
class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("汪汪汪");
    }
}

4. final、static、private 修饰的方法能否被重写?
答:

  • final 修饰的方法不能被重写。
    因为 final 关键字表示该方法不能被修改,保证了方法的稳定性。
  • static 修饰的方法不能被重写,只能被隐藏。
    静态方法属于类,而不是对象,子类可以定义与父类静态方法相同的方法,但这不是重写,只是隐藏了父类的静态方法。
  • private 修饰的方法不能被重写。
    因为 private 方法只能在本类中访问,子类无法访问到父类的 private 方法,也就无法重写。

5. 请简要回答方法重载和方法重写的相同与不同?
答:

方法重载 方法重写
定位位置 同一个类中 子类和父类
方法名 要相同 要相同
参数列表 不同 必须相同
返回值类型 无关 相同或为协变返回类型
访问权限 无关 子类方法不能比父类更严格
异常 无关 子类方法不能抛出比父类更多、更宽泛的异常

三、封装、继承、多态

1. 什么是封装,为什么要使用封装?
答:
封装是将对象的属性和实现细节隐藏起来,仅对外提供公共的访问方式(如 getter 和 setter 方法)。
使用封装的原因:

  • 提高代码的安全性,防止外部随意修改对象的属性;
  • 增强代码的可维护性,当内部实现改变时,只要对外接口不变,就不会影响其他代码;
  • 实现信息隐藏,让使用者只需关注接口,无需了解具体实现。

2. 如何在 Java 中实现封装?
答:
首先将类的属性声明为 private,限制外部直接访问。
然后提供公共的 getter 方法用于获取属性值,setter 方法用于设置属性值。例如:

class Student {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

3. 什么是继承,继承有什么作用?
答:
继承是指一个类(子类)可以继承另一个类(父类)的属性和方法。
其作用包括:

  • 实现代码复用,避免重复编写相同的代码;
  • 建立类之间的层次关系,便于代码的组织和管理;
  • 为多态提供基础。

4. Java 中继承有哪些限制?
答:
Java 只支持单继承,即一个子类只能有一个直接父类,但可以实现多层继承。
另外,private 成员不能被继承,构造方法也不能被继承,但子类构造方法中可以通过 super() 调用父类构造方法。

5. 实现一个继承效果

// 定义一个父类:Animal
class Animal {
    // 父类的属性
    protected String name;
    // 父类的构造方法
    public Animal(String name) {
        this.name = name;
    }
    // 父类的方法
    public void eat() {
        System.out.println(name + " 正在吃东西");
    }
    public void sleep() {
        System.out.println(name + " 正在睡觉");
    }
}

// 定义一个子类:Dog,继承自 Animal 类
class Dog extends Animal {
    // 子类特有的属性
    private String breed;
    // 子类的构造方法
    public Dog(String name, String breed) {
        // 调用父类的构造方法来初始化从父类继承的属性
        super(name);
        this.breed = breed;
    }
    // 子类重写父类的方法
    @Override
    public void eat() {
        System.out.println(name + "(品种:" + breed + ")正在啃骨头");
    }
    // 子类特有的方法
    public void bark() {
        System.out.println(name + "(品种:" + breed + ")正在汪汪叫");
    }
}

// 测试类
public class InheritanceExample {
    public static void main(String[] args) {
        // 创建 Dog 类的对象
        Dog dog = new Dog("旺财", "金毛");
        // 调用从父类继承的方法
        dog.sleep();
        // 调用子类重写的方法
        dog.eat();
        // 调用子类特有的方法
        dog.bark();
    }
}

输出结果:

旺财 正在睡觉
旺财(品种:金毛)正在啃骨头
旺财(品种:金毛)正在汪汪叫

代码解释

  1. 父类 Animal:

    • 包含一个受保护的属性 name,用于存储动物的名字。
    • 有一个构造方法 Animal(String name) 用于初始化 name 属性。
    • 定义了两个方法 eat() 和 sleep(),分别表示动物吃东西和睡觉的行为。
  2. 子类 Dog:

    • 使用 extends 关键字继承自 Animal 类。
    • 有一个特有的属性 breed,用于存储狗的品种。
    • 构造方法 Dog(String name, String breed) 中使用 super(name) 调用父类的构造方法来初始化从父类继承的 name 属性。
    • 重写了父类的 eat() 方法,提供了狗吃东西的特定行为。
    • 定义了一个特有的方法 bark(),表示狗汪汪叫的行为。
  3. 测试类 InheritanceExample:

    • 在 main 方法中创建了 Dog 类的对象 dog。
    • 调用了从父类继承的 sleep() 方法、子类重写的 eat() 方法以及子类特有的 bark() 方法,展示了继承和方法重写的效果。

6. 什么是多态,如何实现多态?
多态是指同一个方法调用,由于对象不同可能会产生不同的行为。
实现多态有三个必要条件:

  • 继承
  • 方法重写
  • 父类引用指向子类对象

例如:

// 父类:动物类
class Animal {
    public void sound() {
        System.out.println("动物发声");
    }
}
// 子类:狗类
class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("汪汪汪");
    }
}
// 子类:猫类
class Cat extends Animal {
    @Override
    public void sound() {
        System.out.println("喵喵喵");
    }
}
// 测试
public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.sound(); // 输出:汪汪汪

        animal = new Cat();
        animal.sound(); // 输出:喵喵喵
    }
}

四、抽象类与接口

1. 请简述抽象类和接口的概念及区别
答:
抽象类是用 abstract 关键字修饰的类,它可以包含抽象方法(只有声明,没有实现)和具体方法,也可以有成员变量。
接口是一种特殊的抽象类型,它只包含抽象方法(Java 8 及以后可包含默认方法和静态方法)和常量。

  • 语法层面:
    • 抽象类:
      • abstract class 定义;
      • 可以有构造方法;
      • 成员变量可以是各种类型;
    • 接口:
      • interface 定义;
      • 没有构造方法;
      • 成员变量默认是 public static final 修饰的常量
  • 设计层面:
    • 抽象类是对一类事物的抽象,是一种 “is – a” 关系;
    • 接口是对行为的抽象,是一种 “like – a” 关系。
      使用层面: 一个类只能继承一个抽象类,但可以实现多个接口。

2. 什么时候使用抽象类,什么时候使用接口?
答:

  • 抽象类:当需要为子类提供公共的方法实现和成员变量,且子类之间有共同的属性和行为,属于同一类事物时,使用抽象类。
    例如,不同种类的汽车都有启动、停止等行为,可将汽车抽象成抽象类。
  • 接口:当需要定义一组行为规范,多个不相关的类都要遵循这些规范时,使用接口。
    例如,飞机和鸟都能飞,“飞” 这个行为可以定义成接口。

3. 一个类能否同时继承抽象类和实现接口?
答: 可以。例如:class Bird extends Animal implements Flyable {...}

4. 实现一个同时继承抽象类和实现接口例子

// 定义抽象类:形状类
abstract class Shape {
    // 抽象方法,用于计算面积,具体实现由子类完成
    public abstract double area();

    // 具体方法,所有继承该抽象类的子类都可使用
    public void display() {
        System.out.println("这是一个形状。");
    }
}

// 定义接口:颜色接口
interface Colorable {
    // 接口中的抽象方法,用于设置颜色
    void setColor(String color);

    // Java 8 及以后支持的默认方法,提供了默认实现
    default void showColorInfo() {
        System.out.println("这是一个有颜色的形状。");
    }
}

// 测试类,继承抽象类 Shape 并实现接口 Colorable
class Circle extends Shape implements Colorable {
    private double radius;
    private String color;

    public Circle(double radius) {
        this.radius = radius;
    }

    // 实现抽象类的抽象方法,计算圆形面积
    @Override
    public double area() {
        return Math.PI * radius * radius;
    }

    // 实现接口的抽象方法,设置颜色
    @Override
    public void setColor(String color) {
        this.color = color;
        System.out.println("圆形的颜色设置为:" + color);
    }

    // 可以重写接口的默认方法
    @Override
    public void showColorInfo() {
        System.out.println("这是一个颜色为 " + color + " 的圆形。");
    }
}

// 主类,用于测试
public class Main {
    public static void main(String[] args) {
        Circle circle = new Circle(5.0);

        // 调用抽象类的具体方法
        circle.display();

        // 调用实现抽象类抽象方法的结果
        System.out.println("圆形的面积是:" + circle.area());

        // 调用接口的抽象方法
        circle.setColor("红色");

        // 调用接口的默认方法
        circle.showColorInfo();
    }
}

代码解释:
1. 抽象类 Shape:

  • 包含抽象方法 area(),该方法没有具体实现,需要子类去实现。
  • 包含具体方法 display(),所有继承该抽象类的子类都可以直接使用这个方法。
    2. 接口 Colorable:
  • 包含抽象方法 setColor(String color),实现该接口的类必须实现这个方法。
  • 包含默认方法 showColorInfo(),提供了默认的实现,实现类可以选择重写该方法。
    3. 测试类 Circle:
  • 继承自抽象类 Shape,实现了 area() 方法来计算圆形的面积。
  • 实现了接口 Colorable,实现了 setColor(String color) 方法来设置圆形的颜色,并且重写了 showColorInfo() 方法以提供特定的颜色信息。
    4. 主类 Main:
  • 创建了 Circle 类的对象。
  • 调用了抽象类的具体方法 display()。
  • 调用了实现抽象类抽象方法 area() 的结果。
  • 调用了接口的抽象方法 setColor(String color) 和默认方法 showColorInfo()。

五、内部类

1. 内部类有哪些类型,它们的访问权限有何不同?通过代码示例说明
答:
Java 内部类主要有四种类型:成员内部类、局部内部类、匿名内部类和静态内部类。

  • 成员内部类:
    成员内部类定义在外部类的内部,作为外部类的一个成员存在。
    它可以访问外部类的所有成员(包括私有成员),创建成员内部类对象需要先创建外部类对象。

    // 外部类
    class OuterClass {
    // 外部类私有属性
    private int outerData = 10;
    
    // 成员内部类
    class InnerClass {
        // 成员内部类方法
        void display() {
            System.out.println("访问外部类的私有成员: " + outerData);
        }
    }
    }
    // 测试类
    public class MemberInnerClassExample {
    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        OuterClass.InnerClass inner = outer.new InnerClass();
        inner.display();
    }
    }

    代码解释

    • InnerClass 是 OuterClass 的成员内部类。
    • InnerClass 中的 display 方法可以访问外部类的私有成员 outerData。
    • 在 main 方法中,需要先创建外部类对象 outer,再通过外部类对象创建内部类对象 inner。
  • 局部内部类:
    局部内部类定义在方法或代码块内部,其作用域仅限于该方法或代码块。
    它可以访问外部类的所有成员以及该方法或代码块中被 final 修饰(Java 8 及以后为事实上的 final)的局部变量。

    // 外部类
    class OuterClassForLocal {
    // 外部类私有属性(成员变量)
    private int outerData = 20;
    // 外部类方法
    void outerMethod() {
        // 局部变量
        final int localVar = 30; // Java 8 及以后可不显式声明为 final
    
        // 局部内部类
        class LocalInnerClass {
            void show() {
                System.out.println("外部类数据: " + outerData);
                System.out.println("局部变量: " + localVar);
            }
        }
        // 创建局部内部类对象
        LocalInnerClass localInner = new LocalInnerClass();
        localInner.show();
    }
    }
    // 测试类
    public class LocalInnerClassExample {
    public static void main(String[] args) {
        OuterClassForLocal outer = new OuterClassForLocal();
        outer.outerMethod();
    }
    }

    代码解释

    • LocalInnerClass 是定义在 outerMethod 方法内部的局部内部类。
    • LocalInnerClass 中的 show 方法可以访问外部类的成员 outerData 和方法中的局部变量 localVar
    • 局部内部类只能在定义它的方法内部使用。
  • 匿名内部类:
    匿名内部类没有类名,通常用于创建只使用一次的类对象。
    同样可以访问外部类的所有成员和方法中 final 修饰的局部变量。
    它必须继承一个父类或实现一个接口。

    // 接口
    interface MyInterface {
    void doSomething();
    }
    // 外部类
    public class AnonymousInnerClassExample {
    // 主方法
    public static void main(String[] args) {
        // 匿名内部类
        MyInterface obj = new MyInterface() {
            @Override
            public void doSomething() {
                System.out.println("匿名内部类实现接口方法");
            }
        };
        obj.doSomething();
    }
    }

    代码解释

    • 直接在创建对象时实现了 MyInterface 接口,没有显式定义类名,这就是匿名内部类。
    • 匿名内部类重写了 MyInterface 接口的 doSomething 方法。
    • 创建的匿名内部类对象赋值给 MyInterface 类型的引用 obj,并调用其方法。
  • 静态内部类:
    静态内部类使用 static 修饰,它只能访问外部类的静态成员。
    创建静态内部类对象不需要先创建外部类对象。

    // 外部类
    class OuterClassForStatic {
    //外部类静态私有属性
    private static int staticOuterData = 40;
    
    // 静态内部类
    static class StaticInnerClass {
        void printData() {
            System.out.println("访问外部类的静态成员: " + staticOuterData);
        }
    }
    }
    // 测试类
    public class StaticInnerClassExample {
    public static void main(String[] args) {
        OuterClassForStatic.StaticInnerClass staticInner = new OuterClassForStatic.StaticInnerClass();
        staticInner.printData();
    }
    }

    代码解释

    • StaticInnerClass 是使用 static 修饰的静态内部类。
    • StaticInnerClass 中的 printData 方法可以访问外部类的静态成员 staticOuterData
    • main 方法中,可以直接创建静态内部类对象,无需先创建外部类对象。

2. 静态内部类和非静态内部类在创建对象时有什么区别?
答:
非静态内部类(成员内部类、局部内部类、匿名内部类):
创建非静态内部类对象前,必须先创建外部类对象。
因为非静态内部类隐式持有外部类对象的引用,依赖于外部类对象存在。

静态内部类:
创建静态内部类对象不需要先创建外部类对象。
因为静态内部类不依赖于外部类的实例,它可以独立存在。

3. 成员内部类、局部内部类、匿名内部类、静态内部类经典使用场景
答:

  • 成员内部类
    • 实现多重继承效果:在 Java 中类只能单继承,但可以通过成员内部类继承其他类,实现类似多重继承的功能。
    • 事件监听机制:在 GUI 编程中,成员内部类可用于实现事件监听器,方便访问外部类的组件和状态。
  • 局部内部类
    • 方法内部的复杂逻辑封装:当一个方法中的逻辑较为复杂,需要一些辅助类来完成特定子任务时,可以使用局部内部类。
    • 临时数据处理:在某个方法中需要对一些临时数据进行处理,并且这些处理逻辑需要封装在一个类中时使用。
  • 匿名内部类
    • 事件处理:在 GUI 编程中,用于创建事件监听器,代码简洁,避免创建大量的监听器类。
    • 回调函数:在需要传递一个简单的回调函数时使用,例如线程的 Runnable 接口实现(Java8后使用lambda表达式更简洁)。
  • 静态内部类
    • 数据结构嵌套:当一个类需要包含另一个紧密相关的数据结构类时,使用静态内部类可以使代码结构更清晰。
    • 辅助工具类:在外部类中提供一些辅助功能的工具类,使用静态内部类可以避免创建不必要的外部类实例。

六、枚举类

1. 什么是 Java 中的枚举类,它有什么特点?
答:
Java 中的枚举类是一种特殊的类,用于定义一组固定的常量。
它的特点如下:

  • 常量集合:枚举类的实例是固定的,通常用于表示一组有限的、预定义的值。如一周的七天、一年的四季等。
  • 类型安全:枚举类型可以保证变量只能取枚举类中定义的值,避免了使用普通常量可能带来的类型不匹配问题。
  • 单例模式实现:枚举类天然支持单例模式,每个枚举常量在 JVM 中是唯一的实例。
  • 可添加方法和属性:枚举类可以像普通类一样拥有成员变量、方法等,并且可以为每个枚举常量提供不同的行为。

2. 如何在枚举类中定义构造方法、成员变量和方法?请举例说明。
答:
枚举类可以定义构造方法、成员变量和方法。
构造方法必须是私有的,因为枚举常量的实例是固定的,不允许外部创建新的实例。
例如:

// 枚举类:季节
enum Season {
    // 枚举常量,调用构造方法进行初始化
    SPRING("春天", "温暖"),
    SUMMER("夏天", "炎热"),
    AUTUMN("秋天", "凉爽"),
    WINTER("冬天", "寒冷");

    // 成员变量
    private final String name;
    private final String description;

    // 构造方法,必须是私有
    private Season(String name, String description) {
        this.name = name;
        this.description = description;
    }

    // 获取名称的方法
    public String getName() {
        return name;
    }

    // 获取描述的方法
    public String getDescription() {
        return description;
    }
}
// 测试类
public class EnumExample {
    public static void main(String[] args) {
        Season spring = Season.SPRING;
        System.out.println(spring.getName() + ":" + spring.getDescription());
    }
}

3. 枚举类在 switch 语句中的使用有什么优势?
答:
枚举类在 switch 语句中使用具有以下优势:

  • 类型安全:switch 语句的表达式类型为枚举类型时,case 标签只能是该枚举类的常量,编译器会进行类型检查,避免了使用普通整数或字符串常量可能出现的拼写错误等问题。
  • 代码可读性高:使用枚举常量作为 case 标签,代码更具可读性,能够清晰地表达每个分支的含义。
  • 维护方便:当枚举类的常量发生变化时,编译器会提示 switch 语句中可能需要更新的地方,便于代码的维护和扩展。
    // 枚举类:周天
    enum Weekday {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
    }
    // 测试类
    public class EnumSwitchExample {
    public static void main(String[] args) {
        // 此处today模拟外部参数的值
        Weekday today = Weekday.MONDAY;
        switch (today) {
            case MONDAY:
                System.out.println("今天是周一,开始新的一周");
                break;
            case TUESDAY:
                System.out.println("今天是周二,继续努力");
                break;
            // 其他 case 分支...
            default:
                System.out.println("未知的星期");
        }
    }
    }

喜欢面试笔记02-Java基础(二):面向对象这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

面试笔记01-Java基础(一):Java语言基础

作者陶其
2025年2月24日 10:29

感谢订阅陶其的个人博客!

一、Java语言简介

Java是面向对象的高级编程语言。其特殊性显著:

  • 具备平台无关性,代码编译成字节码,依托JVM可跨平台运行;
  • 自动内存管理,垃圾回收机制让开发者无需手动释放内存;
  • 支持多线程,能高效处理并发任务;
  • 有丰富类库,涵盖网络、文件等多领域,提升开发效率;
  • 注重安全,有字节码验证、安全管理器等保障;
  • 支持分布式计算,像RMI可实现远程方法调用,适应大规模系统开发。

二、数据类型

Java 数据类型分两类:

基本数据类型(8种):

  • 整数型:byte、short、int、long
  • 浮点型:float、double
  • 字符型:char
  • 布尔型:boolean

引用数据类型:

  • 类(class):如 String 等
  • 接口(interface)
  • 数组(array)

基本数据类型存储值,占用固定内存;
引用数据类型存储对象引用,可表示复杂数据结构。

三、变量与常量

3.1 什么是变量和常量,它们的区别是什么?

答:
变量是程序中可以改变值的存储单元,用于存储数据,在程序执行过程中其值可以被修改。
常量是在程序运行过程中值不能被改变的量,使用 final 关键字修饰。
区别在于变量值可变,常量值不可变。

3.2 Java标识符的命名规则

答:
(1) 基本命名规则:
这些规则是强制性的,违反规则会导致编译错误:

  • 组成元素: 标识符可以由字母(包含英文字母大小写、中文、日文等)、数字、下划线_、以及美元符号 $ 组成。不过,数字不能作为标识符的开头。
    例如,myVariable、_count、$price 都是合法的,但 123abc 不合法。
  • 区分大小写: Java 是大小写敏感的语言,所以 myVariable 和 MyVariable 是两个不同的标识符。
  • 不能是关键字或保留字: 不能使用 Java 的关键字(如 class、public、if 等)和保留字(如 goto、const 等)作为标识符。

(2) 常见命名规范(非强制,但推荐遵循)
这些规范有助于提高代码的可读性和可维护性:

  • 类名:大驼峰命名法:
    每个单词的首字母大写,其余字母小写。
    例如,Person、StudentInfo、DataProcessor 等。
    这种命名方式能清晰反映类的功能或代表的事物。
  • 变量名:小驼峰命名法、见名知意
    小驼峰命名法:第一个单词首字母小写,后续每个单词首字母大写。
    例如,age、studentName、totalAmount 等。
    这样命名能直观体现变量的用途。
    见名知意:变量名应具有描述性,让人一看就明白该变量存储的是什么数据。
    比如,用 studentCount 表示学生数量,而不是使用无意义的 a、b 等。
  • 方法名:小驼峰命名法、动词开头
    小驼峰命名法:和变量名类似,第一个单词首字母小写,后续每个单词首字母大写。
    例如,getAge()、calculateTotal()、printInfo() 等。
    动词开头:方法通常表示一个动作,所以方法名一般以动词开头,清晰表达该方法的功能。
  • 常量名:全部大写
    单词之间用下划线 _ 分隔。
    例如,MAX_VALUE、PI、DEFAULT_TIMEOUT 等。
    这样能明显区分常量和其他变量。
  • 包名:全小写
    通常采用域名反转的形式,以保证包名的唯一性。
    例如,一个公司的域名是 example.com,其包名可能是 com.example.project 。

四、运算符

4.1 逻辑与(&&)和按位与(&)有什么区别?
答:
逻辑与(&&)是短路运算符,当第一个操作数为 false 时,不会再计算第二个操作数。
按位与(&)会对两个操作数的每一位进行与运算,无论第一个操作数结果如何,都会计算第二个操作数。

4.2 逻辑或(||)和按位或(|)有什么区别?
答:
逻辑或(||)是短路运算符,当第一个操作数为 true 时,不会再计算第二个操作数。
按位或(|)会对两个操作数的每一位进行或运算,无论第一个操作数结果如何,都会计算第二个操作数。

4.3 请简要说明 i++ 和 ++i 的区别。
答:
i++ 是后置自增运算符,先返回 i 的当前值,然后再将 i 的值加 1;
++i 是前置自增运算符,先将 i 的值加 1,然后再返回 i 增加后的值。
比如:

int a = 5;
int b = 3;
int c = a++ + b;
// 此时a=6,b=3,c=8

a = 5;
b = 3;
int d = ++a + b;
// 此时a=6,b=3,d=9

五、循环语句

5.1 题目 1:for、while 和 do-while 循环的区别是什么?
答:

  • 执行顺序:
    • for 和 while 循环先判断条件,条件为真才执行循环体;
    • do-while 循环先执行一次循环体,再判断条件,所以 do-while 循环至少会执行一次循环体。
  • 适用场景:
    • for 循环适用于已知循环次数的场景;
    • while 循环适用于循环次数未知,仅根据条件判断是否继续循环的场景;
    • do-while 循环适用于需要先执行一次操作,再根据条件决定是否继续循环的场景。

5.2 在 循环中使用 breakcontinue 语句有什么作用?
答:

  • break 语句用于终止整个循环,跳出循环体执行循环之后的代码。
    例如在一个查找特定元素的 for 循环中,找到元素后使用 break 结束循环。
    注意:如果是在嵌套循环,那么只能跳出当前所在的循环体,无法一次性跳出多层循环体
  • continue 语句用于跳过本次循环的剩余部分,直接开始下一次循环。
    例如在统计偶数和的循环中,遇到奇数时使用 continue 跳过后续求和代码,继续下一次循环。

5.3 如何使用嵌套循环打印一个九九乘法表?
答:
可以使用两层 for 循环来实现,外层循环控制行数,内层循环控制每行的列数。
示例代码如下:

for (int i = 1; i <= 9; i++) {
    for (int j = 1; j <= i; j++) {
        System.out.print(j + " * " + i + " = " + (i * j) + "\t");
    }
    System.out.println();
}

六、流程控制语句

6.1 简述 Java 中 if – else、switch 语句的使用场景及区别
答:

  • if – else 语句适用于条件判断情况较为复杂,条件表达式为布尔类型的场景,可处理范围性的条件判断。
    如判断成绩等级(0 – 59 为不及格等)。
  • switch 语句适用于条件为固定离散值的场景。
    如根据星期几执行不同操作。
  • 区别在于 if – else 可处理更灵活的条件,switch 只能对基本数据类型(如 byte、short、int、char)、枚举类型和 String 类型进行等值判断。

6.2 分析以下 for 循环代码的执行结果,并解释执行流程

for (int i = 0; i < 5; i++) {
    if (i == 3) {
        continue;
    }
    System.out.println(i);
}

答: 执行结果为输出 0、1、2、4。

七、数组

7.1 如何声明、创建和初始化一个 Java 数组?
答:
声明数组有两种方式,以一维数组为例:

  • 数据类型[] 数组名,如 int[] arr
  • 数据类型 数组名[],如 int arr[]

创建数组使用 new 关键字。
arr = new int[5];,表示创建一个长度为 5 的 int 型数组。

初始化数组有静态和动态两种方式。

  • 静态初始化在声明时直接赋值,如 int[] arr = {1, 2, 3, 4, 5};
  • 动态初始化先创建数组,再为元素赋值,如 int[] arr = new int[5]; arr[0] = 1;

7.2 如何遍历一个 Java 数组?请列举至少两种方式
答:
普通 for 循环: 通过数组的索引访问元素,示例代码如下:

int[] arr = {1, 2, 3};
for (int i = 0; i < arr.length; i++) {
    System.out.println(arr[i]);
}

增强 for 循环: 适用于只需要顺序访问数组元素的场景,示例代码如下:

int[] arr = {1, 2, 3};
for (int num : arr) {
    System.out.println(num);
}

7.3 Java 中数组作为方法参数和返回值时,传递的是什么?
答:
Java 中数组作为方法参数和返回值时,传递的是数组的引用,而不是数组本身。
这意味着在方法中对数组元素的修改会影响到原始数组。

喜欢面试笔记01-Java基础(一):Java语言基础这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

❌