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