0-日常知识点补充,问题解答

为什么要实例化才能用方法,而有些类直接就用方法了,两者的区别和这样做的好处是什么,用通俗的语言解释?

可以用一个生活中的例子来理解这个问题:

假设你要做菜🍳

需要实例化的类:像"厨师"

必须先雇一个具体厨师(实例化),才能让他炒菜
每个厨师有自己的特点:老王做川菜,老张做粤菜(不同实例的不同状态)
好处:可以同时有多个厨师工作,互不干扰(实例独立)
直接使用方法的类:像"菜刀工具包"

不需要特定的人,直接拿起来就能切菜(静态方法)
比如菜刀磨锋利的方法,所有厨师共用同一套工具(无需实例化)
好处:随用随取,节省资源(无需重复创建对象)
为什么这样设计?

实例化的好处:

每个对象像独立的机器人🤖,能记住自己的状态(比如游戏角色各自的血量、等级)
避免数据混乱(你银行的存款不会和别人的混在一起)
静态方法的好处:

像公共工具箱🔧,提供通用功能(比如计算器、单位换算)
省内存(不用创建无数个相同的工具)
适合纯逻辑操作(比如Math.sqrt(9)计算平方根)

// 需要实例化的「电饭煲」类
class RiceCooker {
    private int riceAmount; // 每个电饭煲有自己的状态(米量)

    // 必须实例化后才能用的方法
    public void cook() {
        if (riceAmount > 0) {
            System.out.println("煮饭中...");
        }
    }

    public void setRice(int amount) {
        riceAmount = amount;
    }
}

// 直接使用的「数学工具」类
class MathUtils {
    // 静态方法:不需要实例化就能用
    public static int add(int a, int b) {
        return a + b;
    }
}

public class Main {
    public static void main(String[] args) {
        // 实例方法的使用(必须 new 对象)
        RiceCooker myCooker = new RiceCooker();
        myCooker.setRice(2);
        myCooker.cook(); // 输出:煮饭中...

        // 静态方法的使用(直接通过类名调用)
        int result = MathUtils.add(3, 5);
        System.out.println(result); // 输出:8
    }
}

静态变量是不是下一个新的实例会读取上一个实例最新变量的值?

是的,静态变量在类的所有实例之间是共享的,下一个新的实例会读取上一个实例对静态变量修改后的值。以下从原理和示例两方面进行说明:
原理
静态变量属于类,存储在方法区中,在类加载时就会被初始化,且在整个程序运行期间只有一份内存空间。无论通过哪个实例去访问或修改静态变量,操作的都是同一个内存地址中的数据。所以当一个实例修改了静态变量的值后,其他实例再访问该静态变量时,读取到的就是修改后的值。

public class StaticVariableExample {
    // 定义一个静态变量
    static int staticVariable = 0;

    public StaticVariableExample() {
        // 在构造函数中对静态变量进行自增操作
        staticVariable++;
        System.out.println("当前实例中静态变量的值为:" + staticVariable);
    }

    public static void main(String[] args) {
        // 创建第一个实例
        StaticVariableExample instance1 = new StaticVariableExample();
        // 创建第二个实例
        StaticVariableExample instance2 = new StaticVariableExample();
    }
}
//当前实例中静态变量的值为:1
//当前实例中静态变量的值为:2

a++和++a有什么区别,自增的顺序

在 Java 等编程语言中,a++ 和 ++a 都用于对变量 a 进行自增操作,即将变量 a 的值加 1,但它们在自增的时机和使用方式上存在区别,下面详细介绍。
区别概述
a++(后置自增运算符):先返回变量 a 当前的值,然后再将 a 的值加 1。
++a(前置自增运算符):先将变量 a 的值加 1,然后再返回 a 增加后的值。

public class IncrementExample {
    public static void main(String[] args) {
        int a = 5;
        // 使用后置自增运算符
        int b = a++; 
        System.out.println("b 的值: " + b); // 输出 5
        System.out.println("a 的值: " + a); // 输出 6

        int c = 5;
        // 使用前置自增运算符
        int d = ++c; 
        System.out.println("d 的值: " + d); // 输出 6
        System.out.println("c 的值: " + c); // 输出 6
    }
}

事件监听是什么意思

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class EventListenerExample {
    public static void main(String[] args) {
        // 创建一个 JFrame 窗口
        JFrame frame = new JFrame("事件监听示例");
        frame.setSize(300, 200);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // 创建一个 JButton 按钮
        JButton button = new JButton("点击我");
        // 将按钮添加到窗口中
        frame.add(button);

        // 创建一个事件监听器
        ActionListener listener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                // 当按钮被点击时,弹出一个消息对话框
                JOptionPane.showMessageDialog(frame, "按钮被点击了!");
            }
        };

        // 将事件监听器注册到按钮上
        button.addActionListener(listener);

        // 显示窗口
        frame.setVisible(true);
    }
}

按钮点击事件的判断和触发主要依靠以下两个关键步骤:
定义实现 ActionListener 接口的监听器,并重写 actionPerformed 方法,在该方法中编写处理按钮点击事件的逻辑。
使用 addActionListener 方法将监听器注册到按钮上,这样当按钮被点击时,就会自动触发监听器的 actionPerformed 方法。

静态变量和静态方法的用法,请用java代码案例说明,并输出结果

静态变量和静态方法的概念
静态变量:也被叫做类变量,它属于类本身,而不是类的某个具体实例。无论创建多少个该类的实例,静态变量都只有一份,所有实例共享这一个静态变量。静态变量在类加载时就会被初始化。
静态方法:同样属于类,而非类的实例。可以不创建类的实例,直接通过类名来调用静态方法。静态方法内部只能访问静态变量和调用其他静态方法,不能直接访问实例变量和实例方法。

class Student {
    // 静态变量,用于记录学生的总人数
    public static int totalStudents = 0;
    // 实例变量,记录每个学生的姓名
    private String name;

    // 构造方法,每次创建新学生实例时,总人数加 1
    public Student(String name) {
        this.name = name;
        totalStudents++;
    }

    // 实例方法,用于显示学生的信息
    public void displayInfo() {
        System.out.println("学生姓名: " + name);
    }

    // 静态方法,用于显示学生的总人数
    public static void displayTotalStudents() {
        System.out.println("学生总人数: " + totalStudents);
    }
}

public class Main {
    public static void main(String[] args) {
        // 直接通过类名访问静态变量和静态方法
        System.out.println("初始学生总人数: " + Student.totalStudents);

        // 创建第一个学生实例
        Student student1 = new Student("张三");
        // 显示当前学生信息
        student1.displayInfo();
        // 显示当前学生总人数
        Student.displayTotalStudents();

        // 创建第二个学生实例
        Student student2 = new Student("李四");
        // 显示当前学生信息
        student2.displayInfo();
        // 显示当前学生总人数
        Student.displayTotalStudents();
    }
}
//初始学生总人数: 0
学生姓名: 张三
学生总人数: 1
学生姓名: 李四
学生总人数: 2

从输出结果可以看出,静态变量 totalStudents 被所有 Student 实例共享,每次创建新的学生实例时,totalStudents 的值都会相应增加。同时,静态方法 displayTotalStudents() 可以直接通过类名调用,用于显示最新的学生总人数。

JAVA的面向过程和面向对象的区别,用一个代码案例进行对比

面向过程和面向对象的区别
面向过程(Procedure-Oriented Programming,POP)
面向过程编程是一种以过程为中心的编程范式,它将一个大的问题分解为一系列的步骤,然后按照顺序依次执行这些步骤。在面向过程编程中,重点关注的是函数(过程)的实现和调用,数据和操作数据的函数是分离的。
面向对象(Object-Oriented Programming,OOP)
面向对象编程是一种以对象为中心的编程范式,它将数据和操作数据的方法封装在一起,形成对象。通过对象之间的交互来完成程序的功能。面向对象编程具有封装、继承和多态等特性,更符合人类的思维方式,提高了代码的可维护性、可扩展性和可复用性。

// 面向过程实现计算器功能
public class ProcedureOrientedCalculator {

    // 加法函数
    public static int add(int a, int b) {
        return a + b;
    }

    // 减法函数
    public static int subtract(int a, int b) {
        return a - b;
    }

    // 乘法函数
    public static int multiply(int a, int b) {
        return a * b;
    }

    // 除法函数
    public static int divide(int a, int b) {
        if (b == 0) {
            System.out.println("除数不能为零");
            return 0;
        }
        return a / b;
    }

    public static void main(String[] args) {
        int num1 = 10;
        int num2 = 5;

        // 调用加法函数
        int resultAdd = add(num1, num2);
        System.out.println(num1 + " + " + num2 + " = " + resultAdd);

        // 调用减法函数
        int resultSubtract = subtract(num1, num2);
        System.out.println(num1 + " - " + num2 + " = " + resultSubtract);

        // 调用乘法函数
        int resultMultiply = multiply(num1, num2);
        System.out.println(num1 + " * " + num2 + " = " + resultMultiply);

        // 调用除法函数
        int resultDivide = divide(num1, num2);
        System.out.println(num1 + " / " + num2 + " = " + resultDivide);
    }
}

在上述代码中,我们将计算器的各种功能封装成独立的函数,然后在main方法中依次调用这些函数来完成计算任务。数据(num1和num2)和操作数据的函数是分离的。

// 面向对象实现计算器功能
class Calculator {
    private int num1;
    private int num2;

    // 构造函数,用于初始化两个操作数
    public Calculator(int num1, int num2) {
        this.num1 = num1;
        this.num2 = num2;
    }

    // 加法方法
    public int add() {
        return num1 + num2;
    }

    // 减法方法
    public int subtract() {
        return num1 - num2;
    }

    // 乘法方法
    public int multiply() {
        return num1 * num2;
    }

    // 除法方法
    public int divide() {
        if (num2 == 0) {
            System.out.println("除数不能为零");
            return 0;
        }
        return num1 / num2;
    }
}

public class ObjectOrientedCalculator {
    public static void main(String[] args) {
        int num1 = 10;
        int num2 = 5;

        // 创建Calculator对象
        Calculator calculator = new Calculator(num1, num2);

        // 调用加法方法
        int resultAdd = calculator.add();
        System.out.println(num1 + " + " + num2 + " = " + resultAdd);

        // 调用减法方法
        int resultSubtract = calculator.subtract();
        System.out.println(num1 + " - " + num2 + " = " + resultSubtract);

        // 调用乘法方法
        int resultMultiply = calculator.multiply();
        System.out.println(num1 + " * " + num2 + " = " + resultMultiply);

        // 调用除法方法
        int resultDivide = calculator.divide();
        System.out.println(num1 + " / " + num2 + " = " + resultDivide);
    }
}

在上述代码中,我们创建了一个Calculator类,将数据(num1和num2)和操作数据的方法封装在类中。通过创建Calculator对象,并调用对象的方法来完成计算任务。这样,数据和操作数据的方法就紧密地结合在一起了。

balance 属性被声明为 private,这意味着它只能在 BankAccount 类内部被访问,外部代码无法直接访问或修改该属性。这样就隐藏了账户余额的内部实现细节。

// 定义银行账户类
class BankAccount {
    // 封装账户余额,使用 private 修饰符,确保外部无法直接访问
    private double balance;

    // 构造方法,用于初始化账户余额
    public BankAccount(double initialBalance) {
        // 检查初始余额是否合法
        if (initialBalance >= 0) {
            this.balance = initialBalance;
        } else {
            System.out.println("初始余额不能为负数,已将余额初始化为 0。");
            this.balance = 0;
        }
    }

    // 公有方法:存款
    public void deposit(double amount) {
        // 检查存款金额是否合法
        if (amount > 0) {
            this.balance += amount;
            System.out.println("存款成功,当前余额为: " + this.balance);
        } else {
            System.out.println("存款金额必须为正数,请重新输入。");
        }
    }

    // 公有方法:取款
    public void withdraw(double amount) {
        // 检查取款金额是否合法以及余额是否充足
        if (amount > 0 && amount <= this.balance) {
            this.balance -= amount;
            System.out.println("取款成功,当前余额为: " + this.balance);
        } else if (amount <= 0) {
            System.out.println("取款金额必须为正数,请重新输入。");
        } else {
            System.out.println("余额不足,无法完成取款。");
        }
    }

    // 公有方法:获取当前账户余额
    public double getBalance() {
        return this.balance;
    }
}

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

        // 尝试直接访问和修改 balance 属性(这是不被允许的)
        // 以下两行代码会导致编译错误
        // account.balance = 2000; // 编译错误:无法访问 private 字段 balance
        // System.out.println(account.balance); // 编译错误:无法访问 private 字段 balance

        // 正确的方式是通过类提供的公有方法来操作余额
        account.deposit(500);
        account.withdraw(200);

        // 通过公有方法获取余额并输出
        double currentBalance = account.getBalance();
        System.out.println("当前账户余额为: " + currentBalance);
    }
}

在这个案例中:
BankAccount 类中的 balance 属性被声明为 private。这就限制了它的访问范围,只有在 BankAccount 类内部的方法(如构造方法、deposit、withdraw、getBalance 等)可以访问和操作 balance 属性。
在 BankAccountAccessTest 类的 main 方法中,尝试直接访问或修改 account.balance 会导致编译错误,因为 balance 是私有的,外部代码无法直接访问。
而通过调用 BankAccount 类提供的公有方法(如 deposit、withdraw、getBalance),就可以安全地对账户余额进行操作。这样就隐藏了账户余额的内部实现细节,外部代码只需要知道如何使用这些公有方法来与账户对象进行交互,而不需要了解余额是如何存储和管理的。
通过这种方式,实现了数据的封装,提高了代码的安全性和可维护性。
目的就是可以通过方法提升对余额要求的限定,而不是建立对象后想怎么改就怎么改

栈帧和方法区堆区是怎么配合的

class Chef {
    private String name;  // 实例变量(存放在堆中)

    public Chef(String name) {
        this.name = name;  
    }

    public void cookNoodles() {
        Noodles noodles = new Noodles();  // 对象分配在堆中,noodles引用存入栈帧
        boilWater();                      // 方法调用创建新栈帧
        noodles.cook();                   // 调用对象方法(动态链接到方法区)
        serve(noodles);                   // 方法参数传递引用
    }

    private void boilWater() {
        int temperature = 100;           // 局部变量(存放在栈帧)
        System.out.println(name + "煮沸水,温度:" + temperature + "℃");
    }

    private void serve(Noodles noodles) {
        System.out.println(name + "端上煮好的面:" + noodles.getStatus());
    }
}

class Noodles {
    private String status = "生的";       // 实例变量(堆中)

    public void cook() {
        status = "熟的";                  
    }

    public String getStatus() {
        return status;
    }
}

public class Restaurant {
    public static void main(String[] args) {
        Chef chef = new Chef("王师傅");   // Chef对象在堆中,chef引用在main栈帧
        chef.cookNoodles();              // 调用方法:生成cookNoodles栈帧
    }
}

执行流程与内存交互

  1. 类加载阶段
    方法区:加载 Chef、Noodles、Restaurant 类的元数据,包括:
    方法代码(cookNoodles、boilWater 等字节码)
    常量池(如字符串"王师傅", "生的")
    静态变量(此例中无静态变量).

  2. main方法栈帧(线程栈)
    调用main方法时创建栈帧:
    局部变量表:
    args: 命令行参数(此处为空)
    chef: 存储堆中Chef对象的引用(new Chef("王师傅")的地址)

  3. 对象创建
    堆区:
    new Chef("王师傅") 分配内存:
    对象头:指向方法区中的Chef类元数据(用于方法调用时动态链接)。
    实例变量:name = "王师傅"(实际值存在堆中)。

  4. 调用 chef.cookNoodles()
    新栈帧(cookNoodles方法)入栈:
    局部变量表:

this: 隐式参数(指向堆中的Chef对象)
noodles: 初始为null,后指向堆中Noodles对象
执行过程:

new Noodles():堆中创建对象,status = "生的"。
boilWater():调用私有方法,生成新的栈帧:
局部变量 temperature = 100(存放在栈帧)。
通过this.name访问堆中的Chef对象的name字段(值为"王师傅")。
noodles.cook():动态链接到方法区中Noodles.cook()的字节码,修改堆中noodles对象的status为"熟的"。
serve(noodles):传递noodles的引用(堆地址)到serve方法的栈帧。

  1. serve方法栈帧
    局部变量表:
    this: 指向堆中的Chef对象
    noodles: 接收从cookNoodles栈帧传递的引用(同一堆地址)
    执行过程:
    调用noodles.getStatus()时,根据堆中的noodles对象动态链接到方法区的getStatus()方法,返回"熟的"。

  2. 方法返回
    方法依次出栈:
    serve → cookNoodles → main
    堆中的Chef和Noodles对象在无引用后等待GC回收(此例中main执行完毕后所有对象均失效)。
    内存交互关键点
    栈帧与堆:
    局部变量(如chef, noodles)保存堆对象的引用。
    方法参数(如serve(noodles))通过传递堆地址共享对象。
    栈帧与方法区:
    动态链接确保方法调用(如noodles.cook())正确跳转到方法区中的字节码。
    堆与方法区:
    对象头中的类指针指向方法区,指导方法调用时的元数据查找。
    输出结果
    王师傅煮沸水,温度:100℃
    王师傅端上煮好的面:熟的
    场景展示了:

对象的创建与引用传递(堆与栈协作)
动态方法分派(方法区和堆的对象头协作)
局部变量生命周期(栈帧的自动管理)

可以理解一个方法就调用了一个栈帧,理解一个栈帧相当于一个桌面(桌面有变量,有方法),最后的方法(桌面)摆在了最上面,最上面一层一层执行下来
线程就相当一个厨师,在自己的桌面上干活

为什么多线程时,用非锁的方法容易出现数据错乱,不是说并发是单线执行吗?

1. 并发执行的本质

  • 看似“交替”执行,但实际不可控:即使单核 CPU 下线程是交替运行的,线程切换的时机完全由操作系统调度器决定,无法预测某个操作是否会被中途打断。
  • 操作的非原子性:大部分操作(如 i++)在底层由多个步骤组成(读值、修改、写回),这些步骤可能被其他线程打断,导致中间状态被覆盖。

示例:i++ 的非原子性

// 假设 i 初始值为 0
public void increment() {
    i = i + 1; // 非原子操作
}
  • 步骤分解
    1. 线程 A 读取 i=0
    2. 线程 A 计算 0+1=1
    3. 此时线程切换,线程 B 读取 i=0
    4. 线程 B 计算 0+1=1,并写回 i=1
    5. 线程 A 恢复执行,写回 i=1
  • 结果:两次 i++ 操作后,i=1(正确应为 2)。
    结论:可能A线程的方法执行还没返回最新对象结果(读写操作,读了数据到线程A的栈帧时,还没执行完方法写回去就被线程B插队),就被B线程插队了,而加上锁方法后,就能保证方法执行完返回(写回)最新对象结果,才被线程B开始执行

JVM的运行过程

1. 类加载阶段:玩具设计图的获取与检查

想象你要生产一款玩具(比如乐高积木),首先需要获取设计图(.class文件)。JVM 的类加载过程就像工厂的生产部门去获取并验证设计图:

  • 加载(Loading)

    • 工厂派员工(类加载器)去仓库(磁盘、网络等)找到设计图(.class文件),然后把它搬回工厂。
    • 例子:员工从仓库中找到Car.class文件,搬回工厂车间。
  • 验证(Verification)

    • 质检部门检查设计图是否合法,防止有人篡改图纸(比如字节码是否安全)。
    • 例子:检查设计图是否有错误步骤(比如轮子装在车顶上)。
  • 准备(Preparation)

    • 工厂为设计图中的“原材料”分配存储空间(静态变量),但暂时不赋值。
    • 例子:给玩具的“默认颜色”分配一个空白标签,暂时写null
  • 解析(Resolution)

    • 将设计图中的符号引用替换为直接引用(比如明确“方向盘”具体对应哪个零件)。
    • 例子:设计图里写着“轮子”,但具体用哪种型号的轮子(比如Wheel_A123)需要确定下来。
  • 初始化(Initialization)

    • 给静态变量赋值,执行静态代码块(真正的生产准备)。
    • 例子:给玩具的“默认颜色”赋值成红色,并准备好生产线。

关键点:类加载是“按需加载”,比如只有用到Car类时才会加载它。


2. 运行时数据区:工厂的各个功能区

JVM 运行时数据区像工厂的不同部门,各司其职:

  • 方法区(Method Area)

    • 存放设计图本身(类信息、常量池、静态变量)。
    • 例子:工厂的档案室,存放所有玩具的设计图。
  • 堆(Heap)

    • 存放所有生产出的玩具对象(实例对象)。
    • 例子:一个大仓库,堆满了生产好的乐高汽车、乐高小人。
  • 栈(Stack)

    • 每个工人(线程)有一个工作台,记录当前任务(方法调用)的步骤。
    • 例子:工人A正在组装汽车,他的工作台上写着步骤1(装轮子)、步骤2(装方向盘)。
  • 程序计数器(PC Register)

    • 记录每个工人(线程)当前做到哪一步了。
    • 例子:工人A看了一眼记事本,上面写着“下一步装车门”。
  • 本地方法栈(Native Method Stack)

    • 处理需要外部工具的任务(比如调用C语言写的功能)。
    • 例子:工人B用一台进口机器给玩具喷漆,这台机器的说明书是英文的。

3. 执行引擎:流水线上的工人

执行引擎是真正干活的工人,负责执行字节码指令:

  • 解释执行

    • 工人一边看设计图(字节码),一边操作(逐行解释执行)。
    • 例子:新手工人一边看说明书,一边慢慢组装。
  • 即时编译(JIT)

    • 工人发现某个步骤重复多次,直接记住操作流程,下次快速完成(编译为机器码)。
    • 例子:老工人发现“装轮子”的步骤重复了100次,直接形成肌肉记忆,速度飞快。

4. 垃圾回收(GC):清洁工回收废料

当仓库(堆)中的玩具不再被需要时,清洁工(GC)会回收空间:

  • 标记-清扫

    • 清洁工检查哪些玩具没人要了(标记),然后清理掉(回收内存)。
    • 例子:仓库里积灰的乐高恐龙(未被引用的对象)被丢进垃圾桶。
  • 分代回收

    • 仓库分为“新货区”(新生代)和“旧货区”(老年代)。新货区的玩具淘汰快,清洁工频繁检查;旧货区的玩具保留时间长,检查频率低。
    • 例子:新生产的玩具(新对象)容易过时,很快被回收;经典款玩具(长期存活对象)留在旧货区。

5. 完整流程示例:生产一辆乐高汽车

  1. 类加载

    • 工厂加载Car.class设计图,检查并初始化静态变量(比如默认颜色为红色)。
  2. 创建对象

    • 在堆中生产一辆红色乐高汽车(Car car = new Car();)。
  3. 方法调用

    • 工人(线程)在栈中记录组装步骤:car.assembleWheels()car.assembleBody()
  4. 执行指令

    • 解释器或JIT执行每一步操作(装轮子、装车身)。
  5. 垃圾回收

    • 如果汽车被丢弃(car = null),清洁工(GC)最终会回收它。

总结:JVM 就像一座智能工厂

  • 类加载:获取并验证设计图。
  • 运行时数据区:工厂的仓库、档案室、工作台。
  • 执行引擎:流水线上的工人。
  • 垃圾回收:高效的清洁工团队。

单例设计模式的作用

单例设计模式(Singleton Pattern)是一种创建型设计模式,其主要作用是确保一个类只有一个实例,并提供一个全局访问点来获取该实例。这种模式在许多应用场景中都有广泛的应用,以下是单例设计模式的主要作用和适用场景:

主要作用
控制资源的使用:

单例模式可以确保某些资源(如数据库连接、线程池、配置文件等)在整个应用程序中只有一个实例,从而避免资源的浪费和冲突。
全局访问点:

提供一个全局访问点来获取单例对象,使得在不同的地方可以方便地访问和使用该对象。
节省内存:

通过确保只有一个实例,可以节省内存,特别是对于那些创建和销毁开销较大的对象。
线程安全:

单例模式可以通过不同的实现方式(如饿汉式、懒汉式、双重检查锁等)来确保线程安全,从而在多线程环境下正确工作。
适用场景
配置文件:

一个应用程序通常只需要一个配置文件实例,单例模式可以确保配置文件在整个应用程序中是唯一的。
数据库连接:

数据库连接是一种昂贵的资源,单例模式可以确保在整个应用程序中只有一个数据库连接实例。
线程池:

线程池是一种常见的资源池,单例模式可以确保在整个应用程序中只有一个线程池实例。
日志记录:

日志记录器通常需要一个全局唯一的实例,单例模式可以确保日志记录器在整个应用程序中是唯一的。
缓存:

缓存是一种常见的优化手段,单例模式可以确保在整个应用程序中只有一个缓存实例。
计数器:

计数器是一种常见的计算工具,单例模式可以确保在整个应用程序中只有一个计数器实例。
示例
下面是一个简单的示例,展示了单例模式在配置文件管理中的应用:

java

public class Configuration {
    // 私有静态变量,持有单例对象
    private static Configuration instance;

    // 私有构造方法,防止外部实例化
    private Configuration() {
        // 初始化配置
    }

    // 公共静态方法,返回单例对象
    public static synchronized Configuration getInstance() {
        if (instance == null) {
            instance = new Configuration();
        }
        return instance;
    }

    // 示例方法,获取配置信息
    public String getConfig(String key) {
        // 从配置文件中获取配置信息
        return "配置信息";
    }

    public static void main(String[] args) {
        Configuration config = Configuration.getInstance();
        System.out.println(config.getConfig("key"));
    }
}

总结
单例设计模式的主要作用是确保一个类只有一个实例,并提供一个全局访问点来获取该实例。这种模式在许多应用场景中都有广泛的应用,特别是那些需要控制资源使用、节省内存和确保线程安全的场景。通过合理使用单例模式,可以提高应用程序的性能和稳定性。

单例设计模式有四种

1. 饿汉式单例
java

public class EagerSingleton {
    // 私有静态变量,持有单例对象
    private static final EagerSingleton instance = new EagerSingleton();

    // 私有构造方法,防止外部实例化
    private EagerSingleton() {
        // 防止通过反射创建实例
        if (instance != null) {
            throw new IllegalStateException("Already instantiated");
        }
    }

    // 公共静态方法,返回单例对象
    public static EagerSingleton getInstance() {
        return instance;
    }

    // 示例方法
    public void showMessage() {
        System.out.println("Hello from EagerSingleton!");
    }

    public static void main(String[] args) {
        EagerSingleton singleton = EagerSingleton.getInstance();//**这一步已经通过类加载,生成实例对象,和走构造方法了,因为构造方法加了private,所以只能在类内部中走**
        singleton.showMessage();
    }
}
输出结果:

Hello from EagerSingleton!

2. 懒汉式单例
特点:
延迟初始化:单例对象在第一次被请求时才创建。
线程不安全:在多线程环境下,如果不加同步机制,可能会创建多个实例。
简单实现:实现简单,适用于单线程环境或简单的多线程环境
java

public class LazySingleton {
    // 私有静态变量,持有单例对象
    private static LazySingleton instance;

    // 私有构造方法,防止外部实例化
    private LazySingleton() {
        // 防止通过反射创建实例
        if (instance != null) {
            throw new IllegalStateException("Already instantiated");
        }
    }

    // 公共静态方法,返回单例对象
    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }

    // 示例方法
    public void showMessage() {
        System.out.println("Hello from LazySingleton!");
    }

    public static void main(String[] args) {
        LazySingleton singleton = LazySingleton.getInstance();
        singleton.showMessage();
    }
}
输出结果:

Hello from LazySingleton!
3. 线程安全的懒汉式单例
java

public class SynchronizedLazySingleton {
    // 私有静态变量,持有单例对象
    private static SynchronizedLazySingleton instance;

    // 私有构造方法,防止外部实例化
    private SynchronizedLazySingleton() {
        // 防止通过反射创建实例
        if (instance != null) {
            throw new IllegalStateException("Already instantiated");
        }
    }

    // 公共静态方法,返回单例对象
    public static synchronized SynchronizedLazySingleton getInstance() {
        if (instance == null) {
            instance = new SynchronizedLazySingleton();
        }
        return instance;
    }

    // 示例方法
    public void showMessage() {
        System.out.println("Hello from SynchronizedLazySingleton!");
    }

    public static void main(String[] args) {
        SynchronizedLazySingleton singleton = SynchronizedLazySingleton.getInstance();
        singleton.showMessage();
    }
}
输出结果:

Hello from SynchronizedLazySingleton!
4. 双重检查锁单例
java

public class DoubleCheckedLockingSingleton {
    // 私有静态变量,持有单例对象
    private static volatile DoubleCheckedLockingSingleton instance;

    // 私有构造方法,防止外部实例化
    private DoubleCheckedLockingSingleton() {
        // 防止通过反射创建实例
        if (instance != null) {
            throw new IllegalStateException("Already instantiated");
        }
    }

    // 公共静态方法,返回单例对象
    public static DoubleCheckedLockingSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckedLockingSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckedLockingSingleton();
                }
            }
        }
        return instance;
    }

    // 示例方法
    public void showMessage() {
        System.out.println("Hello from DoubleCheckedLockingSingleton!");
    }

    public static void main(String[] args) {
        DoubleCheckedLockingSingleton singleton = DoubleCheckedLockingSingleton.getInstance();
        singleton.showMessage();
    }
}
输出结果:

Hello from DoubleCheckedLockingSingleton!
5. 静态内部类单例
java

public class StaticInnerClassSingleton {
    // 私有静态内部类,持有单例对象
    private static class SingletonHolder {
        private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
    }

    // 私有构造方法,防止外部实例化
    private StaticInnerClassSingleton() {
        // 防止通过反射创建实例
        if (SingletonHolder.instance != null) {
            throw new IllegalStateException("Already instantiated");
        }
    }

    // 公共静态方法,返回单例对象
    public static StaticInnerClassSingleton getInstance() {
        return SingletonHolder.instance;
    }

    // 示例方法
    public void showMessage() {
        System.out.println("Hello from StaticInnerClassSingleton!");
    }

    public static void main(String[] args) {
        StaticInnerClassSingleton singleton = StaticInnerClassSingleton.getInstance();
        singleton.showMessage();
    }
}
输出结果:

Hello from StaticInnerClassSingleton!
6. 枚举单例
java

public enum EnumSingleton {
    INSTANCE;

    // 添加需要的方法
    public void showMessage() {
        System.out.println("Hello from EnumSingleton!");
    }

    public static void main(String[] args) {
        EnumSingleton singleton = EnumSingleton.INSTANCE;
        singleton.showMessage();
    }
}
输出结果:

Hello from EnumSingleton!

java中数组,列表,map,set的区别?

  1. 数组(Array)
    定义:固定长度、同类型数据的集合。
    声明方式:int[] arr = new int[5];
    存储:按下标(索引)访问,连续内存空间。
    特点:
    长度不可变,一旦定义不能增删元素。
    元素类型要一致(如 int[], String[])。
    访问速度快,适合存储数量已知、类型固定的对象。
    示例:
java

String[] arr = {"a", "b", "c"};
arr[1]; // 访问b
  1. 列表(List)
    定义:元素有序、可重复的集合,类似动态数组。
    主要实现类:ArrayList(常用)、LinkedList。
    重点特点:
    有序(保持插入顺序)。
    可动态增删元素。
    允许重复值。
    可以通过下标访问,如 list.get(0)。
    常用用法:
java

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("a"); // 允许重复
  1. Set
    定义:元素无序、不可重复的集合。
    主要实现类:HashSet、TreeSet、LinkedHashSet。
    主要特点:
    不允许重复元素(即使add多次,只保留一次)。
    无序(HashSet中,元素存储顺序不保证)。
    没有下标,不能通过get(index)访问。
    示例:
java

Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("a"); // 该操作无效,a只会出现一次
  1. Map
    定义:键值对(key-value)结构的数据集合。
    主要实现类:HashMap、TreeMap、LinkedHashMap。
    主要特点:
    存储键值对:每个key唯一,对应一个value。
    key不可重复,value可重复。
    通过key进行存取,没有下标。
    示例:
java

Map<Integer, String> map = new HashMap<>();
map.put(1, "apple");
map.put(2, "banana");
map.put(1, "peach"); // 覆盖了key=1的value

总结对比表
类型 有序性 可否重复 可否动态扩容 访问方式 典型实现类
数组 有序(固定顺序) 可重复(同类型) 否 下标 无
List 有序(可变顺序) 可重复 是 下标、迭代 ArrayList, LinkedList
Set 无序(HashSet) 不可重复 是 迭代,不能通过下标 HashSet, TreeSet
Map 按key存取(无序) key不可重复,value可重复 是 key->value HashMap, TreeMap
适用场景举例
数组:元素数量已知、不需要经常增删,节省空间。
List:需要频繁增删、查找,有顺序且可重复。
Set:关注唯一性,不能有重复项。
Map:需要根据唯一的key查找value。

补充
5. Properties
基本定义
Properties 是 Hashtable 的子类,主要用于读写配置文件(*.properties)。
本质上就是一个键和值都是字符串类型的 Map。
主要特点
键和值都是 String 类型(非强制,但一般只用 String)。--map键值可以是任意类型
常用方法:getProperty(String key)、setProperty(String key, String value)。
可以直接从文件加载和存储数据:load(InputStream inStream)、store(OutputStream out, String comments)。
支持通过流进行配置文件的读取和写入,操作极为方便。

为什么要用接口来实现解耦

在面向对象编程中,使用接口实现解耦的核心目的是降低模块间的直接依赖关系,让系统更灵活、易扩展和易维护。下面是详细解释和代码示例:

为什么需要解耦?
当一个类直接依赖另一个具体类时:

代码扩展性差:新增功能需修改调用方代码
测试困难:依赖具体实现,难以模拟测试
更换实现成本高:牵一发而动全身
解耦示例:支付系统(无接口 vs 接口实现)

场景
假设我们有一个订单处理类 OrderProcessor,它需要调用支付功能。

  1. 未解耦方案(直接依赖具体类)
java

// 直接依赖支付宝实现
class Alipay {
    void pay(double amount) {
        System.out.println("支付宝支付: " + amount + "元");
    }
}

class OrderProcessor {
    private Alipay alipay; // 直接依赖具体类

    public OrderProcessor() {
        this.alipay = new Alipay(); // 硬编码实例化
    }

    void processOrder() {
        // ... 订单处理逻辑
        alipay.pay(100.0); // 直接调用支付宝
    }
}

问题:若想改用微信支付,必须修改 OrderProcessor 的代码。

  1. 解耦方案(使用接口)
java

// 步骤1:定义支付接口(抽象)
interface PaymentProcessor {
    void pay(double amount);
}

// 步骤2:实现多种支付方式
class Alipay implements PaymentProcessor {
    @Override
    public void pay(double amount) {
        System.out.println("支付宝支付: " + amount + "元");
    }
}

class WechatPay implements PaymentProcessor {
    @Override
    public void pay(double amount) {
        System.out.println("微信支付: " + amount + "元");
    }
}

// 步骤3:订单类依赖接口而非具体实现
class OrderProcessor {
    private PaymentProcessor paymentProcessor; // 依赖接口

    // 通过依赖注入传入实现
    public OrderProcessor(PaymentProcessor processor) {
        this.paymentProcessor = processor;
    }

    void processOrder() {
        // ... 订单处理逻辑(无需关心具体支付方式)
        paymentProcessor.pay(100.0);
    }
}

// 步骤4:使用时灵活选择实现
public class Main {
    public static void main(String[] args) {
        // 使用支付宝
        OrderProcessor order1 = new OrderProcessor(new Alipay());
        order1.processOrder();

        // 改为微信支付,无需修改OrderProcessor代码!
        OrderProcessor order2 = new OrderProcessor(new WechatPay());
        order2.processOrder();
    }
}

解耦带来的优势
灵活扩展
新增支付方式(如 BankTransfer)只需实现 PaymentProcessor,无需改动已有代码。

易于测试
测试时可传入Mock对象:

java

// 单元测试示例(使用Mock框架如Mockito)
PaymentProcessor mockProcessor = mock(PaymentProcessor.class);
OrderProcessor processor = new OrderProcessor(mockProcessor);
processor.processOrder();
verify(mockProcessor).pay(100.0); // 验证调用是否正确

符合开闭原则
对扩展开放(新增支付实现),对修改关闭(不修改 OrderProcessor)。

降低模块耦合度
OrderProcessor 只关心“支付能力”,不关心具体如何支付。

关键结论
通常项目不会轻易去改service层的代码,所以用接口先代替类,service只负责接收接口,然后main方法只要传入实现了接口的类就可以了,这样就算后期如果有新的实现接口类,也不用 改service层的代码,只要改写main方法就行了。

数据库中的char类型是不是对应java数据类型中的string?

是的,但需要注意一些细节。

在数据库中,CHAR 类型是一种定长字符串类型,无论实际存储的字符串长度是多少,都会占用固定长度的空间,不足时会用空格补齐。

在 Java 中,没有char数据库类型的直接对应类型;
通常我们会将数据库的CHAR映射为 Java 的 String 类型(对应 JDBC 中的 java.lang.String)。不过需要注意:

说明:

数据库 CHAR 类型(例如 CHAR(10))是用于存储定长字符串的类型,不足长度时右侧会用空格补齐。
Java String 类型 是可变长度的字符串(不可变对象,指的是引用的字符串内容本身不可变)。
在 Java 中,char(小写)是一个基本数据类型,用来存单个字符(UTF-16 编码,占 2 个字节),跟数据库的 CHAR 概念不同。
因此,虽然数据库的 CHAR 听起来像 Java 的 char,但实际开发中数据库 CHAR 会对应 Java 的 String,而不是 Java 的 char。
总结:
如果char(10),但是数据库这个字段可以存在10个字符,如果按不同的编码集可能会占20-30个字节
数据库 CHAR(n) → Java String
数据库 VARCHAR(n) → Java String
数据库 CHAR(1) → Java String(有时手动转成 char)
注意事项:

如果数据库中 CHAR(n) 存的内容不满 n 位,取出来可能带有空格,需要 trim() 处理。
如果你要映射单个字符,可以在应用层将 String 转为 Java 的 char 类型。
示例:

java

// 假设数据库字段 CHAR(10),值为 "abc"
String s = resultSet.getString("col"); // s = "abc       "(可能有空格)
String trimmed = s.trim(); // "abc"

结论:
数据库中的 CHAR 类型在 Java 中通常对应 String,而不是基本类型 char。
仅在你确定存储的是单字符时,才考虑用 Java char 表示。

数据库类型与 Java 类型映射表
数据库类型(常见) Java 对应类型 说明
CHAR(n) java.lang.String 定长字符串,不足补空格,需要注意 .trim()
VARCHAR(n) java.lang.String 变长字符串
TEXT / CLOB java.lang.String / java.sql.Clob 大文本可用 Clob 或直接 String(取决于驱动支持)
NCHAR(n) java.lang.String 定长 Unicode 字符串
NVARCHAR(n) java.lang.String 变长 Unicode 字符串
NCLOB java.lang.String / java.sql.NClob 大文本(Unicode)
CHAR(1)(单字符保存) java.lang.String(常用)或 char JDBC 默认取 String,可自行转换为 char
INT / INTEGER java.lang.Integer / int 32 位整数
SMALLINT java.lang.Short / short 16 位整数
BIGINT java.lang.Long / long 64 位整数
DECIMAL / NUMERIC java.math.BigDecimal 精确小数,金额等场景
FLOAT / REAL java.lang.Float / float 单精度浮点
DOUBLE / DOUBLE PRECISION java.lang.Double / double 双精度浮点
DATE java.sql.Date 只有年月日
TIME java.sql.Time 只有时分秒
TIMESTAMP / DATETIME java.sql.Timestamp 年月日 + 时分秒 + 纳秒
BOOLEAN / BIT java.lang.Boolean / boolean 布尔值
BLOB java.sql.Blob / byte[] 二进制数据

posted @ 2025-02-22 22:44  乘加法  阅读(51)  评论(0)    收藏  举报