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栈帧
}
}
执行流程与内存交互
-
类加载阶段
方法区:加载 Chef、Noodles、Restaurant 类的元数据,包括:
方法代码(cookNoodles、boilWater 等字节码)
常量池(如字符串"王师傅", "生的")
静态变量(此例中无静态变量). -
main方法栈帧(线程栈)
调用main方法时创建栈帧:
局部变量表:
args: 命令行参数(此处为空)
chef: 存储堆中Chef对象的引用(new Chef("王师傅")的地址) -
对象创建
堆区:
new Chef("王师傅") 分配内存:
对象头:指向方法区中的Chef类元数据(用于方法调用时动态链接)。
实例变量:name = "王师傅"(实际值存在堆中)。 -
调用 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方法的栈帧。
-
serve方法栈帧
局部变量表:
this: 指向堆中的Chef对象
noodles: 接收从cookNoodles栈帧传递的引用(同一堆地址)
执行过程:
调用noodles.getStatus()时,根据堆中的noodles对象动态链接到方法区的getStatus()方法,返回"熟的"。 -
方法返回
方法依次出栈:
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; // 非原子操作
}
- 步骤分解:
- 线程 A 读取
i=0。 - 线程 A 计算
0+1=1。 - 此时线程切换,线程 B 读取
i=0。 - 线程 B 计算
0+1=1,并写回i=1。 - 线程 A 恢复执行,写回
i=1。
- 线程 A 读取
- 结果:两次
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. 完整流程示例:生产一辆乐高汽车
-
类加载
- 工厂加载
Car.class设计图,检查并初始化静态变量(比如默认颜色为红色)。
- 工厂加载
-
创建对象
- 在堆中生产一辆红色乐高汽车(
Car car = new Car();)。
- 在堆中生产一辆红色乐高汽车(
-
方法调用
- 工人(线程)在栈中记录组装步骤:
car.assembleWheels()→car.assembleBody()。
- 工人(线程)在栈中记录组装步骤:
-
执行指令
- 解释器或JIT执行每一步操作(装轮子、装车身)。
-
垃圾回收
- 如果汽车被丢弃(
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的区别?
- 数组(Array)
定义:固定长度、同类型数据的集合。
声明方式:int[] arr = new int[5];
存储:按下标(索引)访问,连续内存空间。
特点:
长度不可变,一旦定义不能增删元素。
元素类型要一致(如 int[], String[])。
访问速度快,适合存储数量已知、类型固定的对象。
示例:
java
String[] arr = {"a", "b", "c"};
arr[1]; // 访问b
- 列表(List)
定义:元素有序、可重复的集合,类似动态数组。
主要实现类:ArrayList(常用)、LinkedList。
重点特点:
有序(保持插入顺序)。
可动态增删元素。
允许重复值。
可以通过下标访问,如 list.get(0)。
常用用法:
java
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("a"); // 允许重复
- Set
定义:元素无序、不可重复的集合。
主要实现类:HashSet、TreeSet、LinkedHashSet。
主要特点:
不允许重复元素(即使add多次,只保留一次)。
无序(HashSet中,元素存储顺序不保证)。
没有下标,不能通过get(index)访问。
示例:
java
Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("a"); // 该操作无效,a只会出现一次
- 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,它需要调用支付功能。
- 未解耦方案(直接依赖具体类)
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 的代码。
- 解耦方案(使用接口)
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[] 二进制数据

浙公网安备 33010602011771号