前言
- 博客仅作为个人梳理学习内容的方式
- 从代码出发学习知识点
- 初学java,表达不当处望谅解
1.为什么需要这些机制?
在实际开发中,我们常会遇到几个问题:
如何保证某个类只有一个实例?
如何避免“魔法值”,限制变量的合法取值范围?
如何复用代码,避免重复?
如何降低模块之间的耦合度?
👉 本文的四个核心内容,正是对这些问题的回答:
| 机制 | 解决问题 |
|---|---|
| 单例模式 | 控制实例数量 |
| 枚举 | 限制取值范围 |
| 抽象类 | 模板复用 |
| 接口 | 解耦与扩展 |
2.单例模式
概念:单例模式的目标是“一个类在系统中只有一个实例”。模式即为实现方式,而实现单例有以下几种常见方式
a.饿汉式
- 构造器私有化
- 静态成员提前创建实例
- 静态方法取得实例(可选)
ublic class A {
//1.将构造方法私有化,避免创建多个对象
private A(){}
//2.创建一个静态变量,保存对象
//因为 static 变量在内存中只有一份,所以确保了整个系统中只有一个 A 的实例
//无论调用多少次 getInstance(),返回的都是同一个对象
private static A instance = new A();
//也可以通过final 关键字修饰变量,这样创建对象时就会报错,这样就不需要第三步了
// private static final A instance = new A();
//3.创建一个静态方法,返回对象
public static A getInstance(){
return instance;
}
}
2.懒汉式
- 构造器私有
- 静态成员定义对象,第一次调用getInstance时才创建对象
(以下的代码在多线程下存在线程安全问题)
//懒汉单例模式,区别于饿汉单例模式,在第一次调用才的时候创建对象
public class B {
private static B instance;//声明了引用,但对象未创建
private B() {
}
public static B getInstance() {
if (instance == null) {
instance = new B();
}
return instance;
}
}
3.单例模式的实际意义
在实际开发工作中,单例模式的意义有以下几点:
a.节约内存:对于一些内存要求高的对象,如线程池,如果每次调用都要创建不现实
b.实现控制行为:对于某些调度类,如任务管理器,日志中心,如果存在多个实例可能会出现重复调用或者执行冲突
c.保证全局状态一致:满足一些配置类的需求
d.提供全局访问对象:通过getInstance方法调用对象
下面给出一段体现第二点“控制行为”的实际代码
//因为控制器只需要一个,采用单例模式
public class SmartController{
private static SmartController instance = new SmartController();
private SmartController(){}
public static SmartController getInstance(){
return instance;
}
void control(Appliance appliances){
System.out.println("设备名称 "+appliances.getName()+" 当前设备状态 "+appliances.getStatus());
appliances.press();
System.out.println("设备名称 "+appliances.getName()+" 当前设备状态 "+appliances.getStatus());
}
void printAll(Appliance[] appliances){
for (Appliance appliance : appliances) {
System.out.println("设备名称 "+appliance.getName()+" 当前设备状态 "+appliance.getStatus());
}
}
}
3.枚举类
1.定义:枚举类是一种表示“有限个固定取值”的特殊类,枚举不仅是“语法糖”,本质是一个继承自 java.lang.Enum 的类
public enum A {
//第一行只能罗列枚举对象的名称,这些名称本质上是常量
//枚举类代码被反汇编后,就类似一个多例类,即每个对象枚举对象都是是单例的,因此枚举类也是多例的
//枚举都是final的,不可被继承,枚举构造器也是私有的(这两个性质和单例子类类一致)
//枚举对象由jvm管理,具有线程安全,因此可以通过枚举创建一个枚举对象来实现单例模式
x,y,z;
}
2.枚举类与单例模式的联系
将上面的枚举类代码进行反汇编,得到的结果如下
public static final A x;
public static final A y;
public static final A z;
不难看出,A作为一个枚举类,在定义时本质上是创建了多个单例对象。并且由于枚举对象是由JVM统一管理的,具有线程安全,因此在实现单例模式时,利用枚举类是一种合理的选择
public enum A {
x;
}
3.实际意义
枚举类在开发中的有一些常见用途,下面以其信息标志和分类作用为例子
场景:实现数字华容道的盘游戏,用枚举类表示移动方向,同时以用常量定义方向的方式作为对比
public class TestDirection {
public static void main(String[] args) {
//方法1.通过常量来做信息标志和分类,但缺点是参数不受控制
move(Constant.UP);
move(Constant.DOWN);
//方法2.使用枚举类做信息标志和分类,参数受枚举类约束
move1(Direction.up);
move1(Direction.down);
}
👉 枚举带来:
类型安全
可读性强
避免非法值
//枚举类
public enum Direction {
up,down,left,right;
}
//常量
public class Constant {
public static final int UP=0;
public static final int DOWN=1;
public static final int RIGHT=2;
public static final int LEFT=3;
}
- move这个基于常量实现的方法虽然是可行的,但是存在诸多缺陷,最明显的一点就是这个方法的参数本质上是一个int类型的数据,参数不受控制,容易传入无意义的值,比如move(99)
- 使用枚举类做信息标志和分类,参数受枚举类约束,不会出现传无意义值的情况。而且从swicth语句可以看到,在分类是甚至可以不用完整写法 Direction.up,而是可以直接用枚举类的值作为分类参数,大大提升了代码的可读性,这一点也侧面体现出java官方对于枚举类信息标记和分类作用的认可
public static void move1(Direction direction) {
switch (direction) {
case up:
System.out.println("向上移动");
break;
case down:
System.out.println("向下移动");
break;
case right:
System.out.println("向右移动");
break;
case left:
System.out.println("向左移动");
break;
default:
System.out.println("无效的移动方向");
}
}
public static void move(int direction) {
switch (direction) {
case Constant.UP:
System.out.println("向上移动");
break;
case Constant.DOWN:
System.out.println("向下移动");
break;
case Constant.RIGHT:
System.out.println("向右移动");
break;
case Constant.LEFT:
System.out.println("向左移动");
break;
default:
System.out.println("无效的移动方向");
}
}
}
4.抽象方法
1.定义
抽象类用于提取“共性字段 + 共性行为 + 强制子类实现的差异行为”,换句话说,就是复用与约束
以下面的代码为例子
其中的模版就是dailyRountine方法
package com.ithema.noobStudy.abstractdemo;
//抽象类可以不定义抽象方法,但有抽象方法的类一定要定成抽象类
public abstract class Animal {
private String name;
private String gender;
//模版方法,减少代码重复
//标准写法是加上final,避免在子类中重写
public final void dailyRoutine(){
System.out.println("早晨醒来");
cry();
System.out.println("夜晚睡去");
}
public abstract void cry();
子类一定要实现抽象方法(体现“约束),否则也定义为抽象类
//一定要实现抽象方法,要么就也定义为抽象类
public class Cat extends Animal {
@Override
public void cry() {
System.out.println("meow");
}
}
public class Dog extends Animal{
@Override
public void cry() {
System.out.println("wolf");
}
}
5.接口
1.定义:1.接口(Interface)是一种抽象类型,用于描述类应具备的行为规范,接口的两大优势是解耦合和多继承
- 接口是不能直接被创建的,必须通过实现类来实现
- 区别于类,接口是可以被多继承的
public static void main(String[] args) {
//接口有两大好处:多继承和解耦合,这里主要体现多继承的好处:可以拥有多能力
Bunnies bunnies = new Bunnies();
bunnies.cry();
bunnies.go();
}
}
//区分与继承,接口是可以多实现的,得重写所有的抽象方法
class Bunnies implements Hanni, Minji {
@Override
public void cry() {
}
@Override
public void go() {
}
}
- 接口中的字段默认是常量,方法默认是抽象方法
public interface Minji {
String name = "Minji";//静态常量
public static final String id = "123456"; //可以看到接口中的常量是默认定义的,不需要我们显示的声明为final
public void cry();//抽象方法的定义也是默认的
}
- jdk8后,接口除了定义常量和抽象方法,还新增了三个功能
默认方法,静态方法和私有方法
public interface A {
default void show(){
System.out.println("默认方法被调用");
show2();
}
static void show1(){
System.out.println("静态方法被调用");
}
private void show2(){
System.out.println("私有方法在类内部被调用");
}
}
默认方法是新增功能其中比较有实际意义的,比如想要给已经上线的项目增加功能,如果没有默认方法而只有抽象方法,那么只能通过实现类一个个重写,很麻烦。现在只要将新功能在接口的默认方法实现即可
2.接口VS抽象类
| 维度 | 接口(interface) | 抽象类(abstract class) |
|---|---|---|
| 设计定位 | 行为规范(能力) | 类的抽象(模板) |
| 关系语义 | “能做什么”(can-do) | “是什么”(is-a) |
| 成员变量 | 仅常量(public static final) | 可以有实例字段 |
| 方法 | 抽象方法 + default/static(Java 8+) | 抽象方法 + 已实现方法 |
| 构造器 | ❌ 无 | ✅ 有(供子类调用) |
| 多继承 | ✅ 可实现多个接口 | ❌ 单继承 |
| 代码复用 | 较弱(少量默认方法) | 强(可复用字段与方法) |
| 适用场景 | 解耦、可替换、扩展点 | 模板方法、共享状态/逻辑 |
二者的侧重点不同:
接口更像是一种“声明”,用于规定实现类需要完成什么(What),至于具体如何完成,接口并不关心;
抽象类则更偏向于“模板 + 部分实现”,不仅规定要做什么,还对部分实现过程进行了约束(What + How)。其中模板方法通常用于定义整体流程,一般不希望被子类重写(可通过 final 限制),而具体的差异行为由子类通过实现抽象方法来完成。
3.面向接口编程
- 引言:面向接口编程体现了接口的另一个主要优势:解耦合,耦合就是:类与类之间的依赖强度,而引入接口后代码就可以依赖接口而不是具体类
场景:A公司提供一个接口(规范思想),内部的实现方案让B和C公司提供实现类
public interface ServiceInterface {
public void getInfo(Student[] students);
public void getAvgGrade(Student[] students);
}
B公司负责实现:打印学生的信息,打印平均分
public class ServiceB implements ServiceInterface{
@Override
public void getInfo(Student[] students){
for (int i = 0; i < students.length; i++) {
System.out.println("学生姓名: "+students[i].getName()+" 学生成绩: "+students[i].getScore());
}
System.out.println("总人数: "+students.length);
}
@Override
public void getAvgGrade(Student[] students) {
if (students == null || students.length == 0) {
System.out.println("平均分: 0");
return;
}
int sum = 0;
for (Student student : students) {
sum += student.getScore();
}
double avg = (double) sum / students.length;
System.out.println("平均分: " + avg);
}
}
C公司实现:打印学生信息(包括性别),打印平均分(除去最高和最低分)
public class ServiceC implements ServiceInterface{
@Override
public void getInfo(Student[] students) {
int femaleCount = 0;
int maleCount = 0;
for (Student student : students) {
System.out.println(student);
if(student.getGender().equals("female")){
femaleCount++;
}
else{
maleCount++;
}
}
System.out.println("总人数: "+students.length+" 女生:"+femaleCount+" 男生:"+maleCount);
}
@Override
public void getAvgGrade(Student[] students) {
double sum = 0;
int max = students[0].getScore();
int min = students[0].getScore();
for (Student student : students) {
if(student.getScore() > max){
max = student.getScore();
}
if(student.getScore() < min){
min = student.getScore();
}
}
for (Student student : students) {
sum += student.getScore();
}
sum -= max;
sum -= min;
System.out.println("去掉最大最小值平均分: "+ sum / (students.length - 2));
}
}
学生类
import lombok.AllArgsConstructor;
import lombok.Data;//getter,setter,tosString
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private String name;
private String gender;
private int score;
}
测试类
public class Test {
public static void main(String[] args) {
//手动创建学生对象
Student[] allStudents = new Student[5];
allStudents[0] = new Student("Minji","female" ,100);
allStudents[1] = new Student("Hanni","female" ,99);
allStudents[2] = new Student("Danielle","female" ,98);
allStudents[3] = new Student("Haerin","female" ,97);
allStudents[4] = new Student("Hyrein","female" ,96);
//提供两套业务实现方案,支持灵活切换,体现面向接口编程,也体现了接口的另一个好处:接耦合
//A公司提供一个接口(规范思想),内部的实现方案让B和C公司提供实现类
ServiceInterface service1 = new ServiceB();
ServiceInterface service2 = new ServiceC();
service1.getInfo(allStudents);
service1.getAvgGrade(allStudents);
System.out.println("-------------------------");
service2.getInfo(allStudents);
service2.getAvgGrade(allStudents);
}
}
用接口接收服务对象,可以自由的选取实现类,代码依赖接口而不是实现,从而实现“可替换、可扩展”的设计
**6.总结:
单例:控制实例数量(唯一性)
枚举:限制取值范围(类型安全)
抽象类:复用代码 + 模板控制
接口:定义规范 + 实现解耦**