JavaSE-02-面向对象(详细版)
JavaSE-02-面向对象(详细版)
Java是面向对象语言,本文将结合丰富的案例讲解面向对象的语法。熟练运用面向对象思维,助力写出高内聚低耦合的漂亮代码,同时很好地实现业务需求,项目代码具有扩展性,同时也能锤炼看问题的角度。
一、面向过程与面向对象
说到面向对象,就不得不说面向过程,这是两种不同的编程方式,但是这两者并不割裂。早期的高级编程语言,如:C语言,便是面向过程的代表,代码讲究如何一步步实现需求,实现的方案有优有劣,这便是[算法],所以早期提出编程=算法+数据结构这样的高度总结。优秀的算法,对于同样的机器,能够高效地计算出结果,重点落在算法上,而数据只是凑合辅助,更有价值的是算法。
而如今的面向过程语言,如:Java,C++等,更多是先聚焦在如何高效组织数据上,而将视角拉高一些,俯瞰这些数据,将其归类封装,变成了一个个有边界的数据集合,同时加上操作这些数据的行为,便可以打包为一个个的对象。当一个个具体的对象,有相似之处,便可以再次抽象一层,称之为对象模版,类。面向对象的视角,更有益于实现大型项目,互相协作,高效并行。
上文所说,面向过程与面向对象虽说大体是两个不同的编程方式,编程语言的特点。但是因为面向对象是后来者,所以其身上或多或少会有面向过程的影子。这也很好理解,因为使用面向对象进行高纬度封装后,聚焦到一个个方法函数,其实现也是需要一步步去做的,才能实现,这不就是面向过程吗?
总结来说,先面向对象从高层面划分类对象,这些对象封装着属性以及改变属性状态的行为;然后具体到行为如何实现时,使用面向过程的思维,思考如何使用高效的算法实现该行为,从广州到北京的路线千千万万,哪条才是最优的呢[面向过程]?而决定从广州去北京,而不是从海南去北京,那便是面向对象整体的规划了[面向对象]。
二、类与对象
什么是类与对象?
现实生活中:对象,就是一个个具体的客观存在的物理实体,有形有状,摸得着看得见,如:一辆辆具体的车;类,则是制造具体车的模具,工人们根据模具,便可以造出一辆辆的车。
从总结归纳的角度看:对象是普遍的现象,类是对具体现象的抽象,将其特性行为抽取出来,形成共性。可以根据所得规律(类)判断新的现象(对象)是否符合要求。
Java世界有个高度的总结:万物皆对象。没有对象,那就new一个出来。对象是具体的,类是抽象的,模板的。
类的组成成分
那么类的组成成分有哪些呢?
- 成员变量(属性)
- 成员方法(行为)
- 构造方法(new实例化+属性初始化)
- 代码块(补充构造方法)
- 内部类(类中类)
public class Person { private String name; // 姓名 private int age; // 年龄 private char sex; // 性别 public Person () {} // 无参构造 public Person (String name, int age, char sex) { // 有参构造 System.out.println( "构造方法执行" ); this .name = name; // 初始化属性…… this .age = age; this .sex = sex; } { System.out.println( "代码块执行" ); // 每次new对象都执行 } static { System.out.println( "静态代码块执行" ); // 只执行一次 } public String getName () { // 行为:获取姓名 return name; } public void setName (String name) { // 行为:设置姓名 this .name = name; } public int getAge () { // 行为:获取年龄 return age; } public static class Heart { .... // 一样的类成分:属性、行为、构造方法、代码块、内部类 } }
上述的类成分,可以随意搭配,不是一定都全部齐全才叫类,一般的类,就只有:属性、行为、构造方法这三种成分,也是最常见的实体类。类有了,下一步便是new一个对象出来见见世面。
public class Main { public static void main (String[] args) { Person water = new Person ( "water" , 18 , '男' ); String name = water.getName(); System.out.println(name); System.out.println( "===============" ); Person mary = new Person ( "Mary" , 18 , '女' ); System.out.println(mary.getAge()); } } // 输出 静态代码块执行 代码块执行 构造方法执行 water =============== 代码块执行 构造方法执行 18
new实例化对象时,执行顺序:静态代码块 > 代码块 > 构造方法
构造方法
- 每个类都有一个默认的无参构造方法,可以不显式
- 如果提供了有参构造,而又需要无参构造,则需要显式
- 作用:创建对象,初始化成员属性
- 一般建议提供两个构造方法:无参构造,全参构造
new对象过程分析
Person p = new Person("water",18,'男')创建对象的细节:代码执行到这一行时,先在堆内存区域new出一块空间(划分,有地址,如:0x001),接着进行初始化,如果有入参,则会给成员属性赋值,若没有,则使用默认值,如:引用类型默认null,基本整型为0,布尔类型为false等;第三步,完成实例化和初始化后,将这片空间的地址0x001指向一个变量存储,这里是p,那么此刻p存储着对象的地址值,根据地址可以找到对象,这就是对象引用,通过p引用对象的内容,包括调用对象的行为方法,改变其属性状态等等。这一刻,p就代表着water这个人。
构建标准类
- 成员属性私有化,private修饰
- 若需要设置和访问,则提供public的set/get方法
- 提供无参构造,全参构造
- 提供其他必须的成员方法
小结
- 类或对象的成分:属性、行为、构造方法,内部类,代码块
- 产生对象,new 构造方法
- 有参构造方法,可以对属性进行初始化赋值
- 实例化对象时,静态代码块最先执行并且只执行一次
- 代码块每次都先于构造方法执行,像是构造方法的补充
三、权限修饰符
顾名思义,这是Java用于控制调用者对资源访问的权限控制,符合日常认知,保证数据安全,操作规范。
四种权限符
权限修饰符,指的是:
private,私有default,默认、缺省protected,受保护public,公开
这四种权限,保护属性、行为、类不被滥用,能更好地保证数据安全,调用安全。其权限范围如下:
| 访问范围 | 本类 | 同包 | 子类 | 不同包 |
|---|---|---|---|---|
| private | √ | × | × | × |
| default | √ | √ | × | × |
| protected | √ | √ | √ | × |
| public | √ | √ | √ | √ |
认识包
包相当于电脑中的文件夹,便于管理代码文件;而包的命名,根据公司的域名倒写,如:www.water.com则包名com.water.project.module.funtiction这里包名使用.隔开。域名+项目名+模块名等。
测试验证
设计两个包,和三个类,测试权限修饰符。结果如下:
可以看到Main类中的main方法是我们的执行入口,分别实例化Cat、Dog、DogSon三个对象,调用不同方法,发现有三处地方报错,这对象各自属性也引用了其他包的对象或同包对象,有些方法调用是报错的!印证了上述的权限修饰符作用范围。
使用建议
private用于类内大方法的封装复用default接口JDK8后新增默认方法使用,定义接口实现类公共行为时使用protected父类允许子类使用的方法public任何类都能调用的公开方法,对外提供能力
四、封装性
什么是封装?
面向对象三大特性之一:封装性。使用private来修饰属性、方法、类,意味着将其封装起来,避免访问,就像黑屋子,外面的人,看不到里面的构造,具有安全性。同时降低了调用者的使用难度,因为private外面压根调用不了;而只有在本类可以使用,那么证明这个是安全的,开发者也可以大胆改造。
该露的露,该隐藏的隐藏。
使用方式
- private修饰属性,提供公开的getter/setter方法进行访问和赋值
- private修饰方法,表明方法实现细节不想暴露出去,仅供本类使用
- private修饰构造方法,表明不想被外面new出对象,一般用于单例
- private修饰类,表明仅能其外部类能访问
错误案例:
public class Person { String name; int age; } public static void main (String[] args) { Person p = new Person (); p.name = "water" ; p.age = 15 ; // 随意继续赋值 p.name = "Bob" ; // 就像自家后花园 }
正确做法:
public class Person { private String name; private ing age; public String getName () { return name; } public void setName () { this .name = name; } .... } public static void main (String args[]) { Person p = new Person (); p.setName( "water" ); String name = p.getName(); System.out.println(name); // 输出water }
this关键字
this表示正在调用该方法的对象引用,哪个对象调用则代表哪个对象。成员方法中隐去了这个对象入参。就像下面那样。
public void setName (Person this ) { // 隐去了该具体对象引用 this .name = name; }
作用:
- 方法入参变量(局部变量)和成员变量同名时,用于区分
- this,指代当前对象
- this(),表示当前对象的无参构造方法
- this(xx),表示当前对象的有参构造方法
五、继承性
什么是继承?
面向对象三大特性之一:继承性。Java是单继承,只能继承一个父类,但可以间接继承(父类的父类),而这个的最顶级老祖宗,则是Object类,所以Java是一个联系非常紧密的类语言,没有一个类是孤立的,即使没有明显地extends某个父类,也会默认继承Object类,同时继承是可以传递的,子类继承父类的一切,包括属性和行为,但不代表都能访问,需要遵循权限修饰符的限制;所以子类真的集万千宠爱于一身,并且还能发展自己的事业(扩展自己的属性、行为)张扬自己的才华横溢。
基本使用
public class Father { private String name; public Father () { System.out.println( "父类无参构造方法" ); } public Father (String name) { System.out.println( "父类有参构造方法" ); this .name = name; } public void dance () { System.out.println( "跟着张学友学跳舞" ); } private void cook () { // 子类拥有,但访问不了,private修饰了 System.out.println( "爸爸烧得一手好菜" ); } public String getName () { return name; } public void setName (String name) { this .name = name; } }
继承格式:修饰符 子类 extends父类
// 使用extends关键字表示继承 public class Son extends Father { private String hobby; // 子类扩展属性 public Son () { super (); // 可以忽略,默认第一行调用父类构造方法 } public Son (String name, String hobby) { super (name); // 调用父类的构造方法,有父亲才有儿子 this .hobby = hobby; System.out.println( "子类有参构造方法" ); } @Override public void dance () { // 子类重写父类的方法 System.out.println( "跟着坤哥学跳舞" ); } public void study () { // 子类新增方法 System.out.println( "学习" ); } public String study (String bookName) { // 方法重载:方法名相同,参数不同,返回值不同 return "正在学习" + bookName; } public String getHobby () { return hobby; } public void setHobby (String hobby) { this .hobby = hobby; } @Override public String toString () { // 覆盖重写祖宗Object类的toString方法 return "Son{" + "hobby='" + hobby + '\'' + '}' ; }
父子类执行顺序
new子类,看看调用顺序是怎样的?答案是:先有父亲,再有儿子。代码层面,也是在构造方法中或隐藏或显式地调用super父类的构造方法,优先对其父类进行初始化。
public static void main (String[] args) { Son son = new Son ( "Father Mike" , "Basketball" ); String programming = son.study( "Programming" ); System.out.println(programming); } // 输出 父类有参构造方法 子类有参构造方法 正在学习Programming // 结论: 1 、先执行父类构造 2 、再执行子类构造(子类构造引用了 super (xx)或 super ())
内存分析
Java数据内存分析,基本数据类型存储在栈,引用类型new出来的对象和数组存储在堆中。一个个方法执行的过程,是进栈压栈的过程,每个方法调用都会生成一个独有的stack frame,在这片空间进行逻辑运算,而基本类型的局部变量就存储在这,生命周期随方法调用结束而结束;若涉及的引用类型变量,如果是局部变量,那么也就成为了无引用对象,会被后台的GC线程回收;而从入参而来的引用类型变量,则还保留在调用者的空间上,有引用,改变的仅仅是对象属性的值,而引用(对象地址值)则没变化。
一个类new出多个对象,每个对象都有其自己的成员属性;而成员方法的调用则是公用的,毕竟脱胎于同一个类。
Person p1 = new Person ( "water" ); // p1 指向已经初始化name=water的对象空间,0x001 Person p2 = null ; // p2 指向null,表示空,不代表任何具体对象引用 p2 = p1; // p2 是变量,可以随意变换存储的值,指向了p1所指向的对象,也是 0x001 p2.setName( "Bob" ); // 所以p2也就可以操控0x001对象的行为,改变属性name值 String name = p2.getName(); // 此时name已经被改变为Bob System.out.println(name); // 输出 Bob System.out.println(p == p2); // true
super关键字
super表示超类,父类,用于区分子类和父类;super()表示父类的无参构造方法,super(xx)表示父类的有参构造方法,同时super.name父类属性 与name入参属性的区分。
小结
extends继承关键字- 子类可以继承父类的一切,但并不一定能访问所有资源,遵循权限规则
- 继承的属性、行为,可以传递,意味着子类可以使用祖宗类的能力
Object类为默认祖宗类- 所有类都有父类
- 按类向上寻找,最终都能找到
Object - new 子类时,先调用父类的构造方法,初始化父类属性,才到子类
- 本质是子类拥有父类的空间的引用(不是一个显式对象),复用父类代码
- 子类可以覆盖重写父类的方法,并且修饰权限不能小于父类的
- Java是值传递,对象有一个十六进制的地址值,代表整个对象,指向具体的对象
六、抽象类
什么是抽象类?
在 Java 中,使用 abstract 关键字修饰的类称为抽象类。抽象类不能被实例化(即不能通过 new 创建对象),它的主要作用是作为其他类的基类,提供共性逻辑和接口定义。
public abstract class Animal { // 可以有构造函数、成员变量、具体方法等 }
抽象类的核心特点
| 特点 | 说明 |
|---|---|
| 不能实例化 | new AbstractClass() 是非法的 |
| 可以包含抽象方法 | 如: public abstract void run(); |
| 可以包含具体方法 | 支持实现业务逻辑 |
| 子类必须实现抽象方法 | 否则子类也必须声明为抽象类 |
| 构造函数可用 | 抽象类可以有构造函数,用于初始化成员 |
| 继承关系单一 | Java 是单继承语言,只能继承一个抽象类 |
抽象类 vs 普通类
| 对比项 | 抽象类 | 普通类 |
|---|---|---|
| 是否能被实例化 | ❌ 否 | ✅ 是 |
| 是否可含抽象方法 | ✅ 是 | ❌ 否 |
| 是否必须被继承 | ❌ 否 | ❌ 否 |
| 是否有构造函数 | ✅ 是 | ✅ 是 |
| 是否支持多继承 | ❌ 否 | ❌ 否 |
抽象类的常见用途
模板方法设计模式(Template Method Pattern)
这是抽象类最经典的使用方式。父类中定义算法骨架,将不确定步骤延迟到子类实现。
- 确定步骤:由抽象类统一实现。
- 不确定步骤:声明为抽象方法,由子类实现。
- 钩子方法:提供默认实现,子类可选择性重写。
- 强制实现方法:不提供实现,调用时抛异常,子类若需使用必须重写。
工具类封装(防误用)
有时候我们希望某个类只提供静态方法,不允许创建实例。此时可以用抽象类来“封印”构造器:
public abstract class CollectionUtil { public static <T> boolean isEmpty (Collection<T> collection) { return collection == null || collection.isEmpty(); } }
⚠️ 小贴士:也可以用私有构造器 + final 类实现类似效果,但抽象类更直观。
模板方法实战案例
定义抽象类 AbstractInvoke
import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 抽象模板类,封装通用调用流程: * 验证请求 -> 转换请求 -> 执行调用 -> 校验结果 -> 记录日志 */ public abstract class AbstractInvoke { private static final Logger log = LoggerFactory.getLogger(AbstractInvoke.class); // 模板方法,定义了调用的标准流程 public Object invoke (Object request) { verify(request); Object convert = convertRequest(request); log.info( "请求参数{}" , convert); Object response = doInvoke(convert); Object result = verifyResult(response); log.info( "响应结果{}" , result); return result; } // 钩子方法:验证请求,子类可选择性重写 protected void verify (Object request) { // 默认空实现 } // 请求转换方法,由子类实现 protected abstract Object convertRequest (Object request) ; // 抽象方法:执行实际调用,子类必须实现 protected abstract Object doInvoke (Object convertedRequest) ; // 响应校验方法,子类可覆盖扩展 protected Object verifyResult (Object response) { if (response == null ) { throw new RuntimeException ( "响应为空" ); } return response; } // 获取重试次数,默认3次,子类可重写 protected int getRetry () { return 3 ; } // 描述方法,若子类未重写则抛异常,表示“必须重写才能使用” public String desc () { throw new RuntimeException ( "desc 方法必须被重写" ); } }
子类实现示例
a. 一般调用场景
class NormalInvoke extends AbstractInvoke { @Override public Object doInvoke (Object convertedRequest) { return "Normal Response" ; } @Override public Object convertRequest (Object request) { return "Normal Converted Request" ; } @Override public String desc () { return "normal method invoke" ; } }
b. 重要调用场景
class ImportantInvoke extends AbstractInvoke { @Override public Object doInvoke (Object convertedRequest) { return "Important Response" ; } @Override public Object convertRequest (Object request) { return "Important Converted Request" ; } @Override public void verify (Object request) { // 增加额外校验逻辑 System.out.println( "ImportantInvoke: performing enhanced validation..." ); } @Override public int getRetry () { return 5 ; // 更高重试次数 } @Override public String desc () { return "important method invoke" ; } }
最佳实践
| 实践 | 说明 |
|---|---|
| 定义算法骨架 | 使用模板方法固定流程,提升可维护性 |
| 抽象方法明确职责 | 强制子类完成核心逻辑 |
| 钩子方法灵活扩展 | 提供默认行为,子类可选覆写 |
| 异常抛出式方法 | 父类不实现,子类如需使用必须重写 |
| 工具类防误用 | 利用抽象类无法实例化的特性,防止滥用 |
| 命名规范清晰 | 抽象类命名建议以 Abstract 开头,如 AbstractService , AbstractHandler |
小结
- 抽象类是面向对象编程的重要组成部分,适用于提取公共逻辑、定义接口契约。
- 模板方法模式是其最经典的应用,通过定义算法骨架提高代码复用性和扩展性。
- 抽象类不一定含有抽象方法,但含有抽象方法的类必须是抽象类。
- 抽象类除了抽象方法,还可以包含默认实现、钩子方法、异常抛出方法等多种形式。
- 抽象类不能被实例化,但其组成结构与普通类一致(属性、方法、构造函数等)。
七、接口
什么是接口?
在 Java 中,接口(Interface) 是一种更高层次的抽象机制。它定义了一组行为规范,不关心这些行为如何实现,只关注方法签名和返回值。类通过 implements 实现接口,从而具备某种能力。
public interface Person { String getName () ; void setName (String name) ; int getAge () ; void setAge ( int age) ; void run () ; }
JDK8 及以后版本中,接口支持默认方法、静态方法和私有方法,使其更加灵活。
接口的核心特点
| 特性 | 描述 |
|---|---|
| 所有方法默认是 public abstract | 即使省略修饰符 |
| 支持常量定义 | 默认 public static final |
| 不可实例化 | 不能使用 new Interface() |
| 多实现 | 一个类可以实现多个接口 |
| 方法无实现(JDK8前) | JDK8+ 支持默认方法和静态方法 |
| 从 JDK9 起支持私有方法 | 用于辅助默认方法实现 |
接口 vs 抽象类
| 对比项 | 接口 | 抽象类 |
|---|---|---|
| 方法实现 | JDK8+ 前只有抽象方法 | 可以有具体方法 |
| 成员变量 | 必须是 public static final 常量 | 可以是任意类型 |
| 构造函数 | 没有构造函数 | 有构造函数 |
| 继承关系 | 支持多实现 | 单继承 |
| 访问权限 | 所有方法默认 public | 支持 protected , private 等 |
| 设计目的 | 表示“能做什么”(can-do) | 表示“是什么”(is-a) |
接口的组成演变(JDK 版本演进)
| JDK 版本 | 新增特性 | 说明 |
|---|---|---|
| JDK1.0 - JDK7 | 只有抽象方法和常量 | 最纯粹的抽象 |
| JDK8 | 默认方法(default)、静态方法 | 提供默认实现,增强扩展性 |
| JDK9 | 私有方法 | 辅助默认方法内部逻辑复用 |
示例:JDK8+ 接口新特性
public interface Logger { // 抽象方法 void log (String message) ; // 默认方法 default void info (String message) { log( "[INFO] " + message); } // 静态方法 static void warn (String message) { System.out.println( "[WARN] " + message); } // 私有方法(JDK9+) private void formatLog (String msg) { System.out.println( "Formatted: " + msg); } }
接口的使用方式
定义行为规范
interface Person { String getName () ; void setName (String name) ; int getAge () ; void setAge ( int age) ; void run () ; }
类必须实现所有抽象方法,否则该类也必须声明为抽象类。
使用接口作为常量类(推荐替代方案)
interface MyNumber { int ONE = 1 ; int TWO = 2 ; }
更推荐使用枚举或普通类封装常量,避免接口被滥用。
接口的实现类
a. 抽象类部分实现接口
public abstract class AbstractPerson implements Person { @Override public void run () { System.out.println( "Running..." ); } }
b. 具体实现类继承抽象类并实现接口
public class RichPerson extends AbstractPerson implements Person { @Override public String getName () { return "rich" ; } @Override public void setName (String name) { // ... } @Override public int getAge () { return 30 ; } @Override public void setAge ( int age) { // ... } }
注:
implements Person可省略,但保留有助于明确表达类的行为契约。
解决接口冲突问题
当多个接口存在同名方法时:
- 实现类只需重写一次即可。
- 如果想只实现其中某些方法,可以通过中间类做空实现来规避。
示例:中间类处理接口方法
interface A { void methodA () ; void commonMethod () ; } interface B { void methodB () ; void commonMethod () ; } abstract class Middle implements A , B { @Override public void commonMethod () {} } class MyClass extends Middle { @Override public void methodA () { System.out.println( "Only implement A" ); } }
接口的设计哲学
“我只给你们这些资源,你们给我打造一个赚钱的产品,不管你们怎么实现。”
这句话形象地描述了接口的本质:上层设计只提供方法签名,下层实现自由发挥。接口是系统间解耦的关键手段。
接口的最佳实践
| 实践 | 说明 |
|---|---|
| 明确职责 | 接口应代表一类行为,如 Runnable , Serializable |
| 小而专 | 接口方法不宜过多,建议单一职责原则 |
| 合理使用默认方法 | 避免破坏已有实现的前提下扩展功能 |
| 不要滥用作常量类 | 推荐使用 enum 或 final class 替代 |
| 避免多重实现冲突 | 若接口方法重复,需谨慎设计 |
| 命名规范 | 接口命名建议以形容词或动词开头,如 Runnable , Comparable |
三大实战场景
解耦设计(Decoupling)
场景描述:通过接口定义行为规范,实现类可自由替换,调用方无需关心具体实现,降低模块之间的依赖关系。
示例:支付系统解耦设计
// 定义支付接口 public interface Payment { void pay ( double amount) ; } // 支付宝支付实现 public class Alipay implements Payment { @Override public void pay ( double amount) { System.out.println( "使用支付宝支付:" + amount + " 元" ); } } // 微信支付实现 public class WechatPay implements Payment { @Override public void pay ( double amount) { System.out.println( "使用微信支付:" + amount + " 元" ); } } // 上层业务类 public class OrderService { private Payment payment; public OrderService (Payment payment) { this .payment = payment; } public void checkout ( double amount) { payment.pay(amount); } }
使用方式:
OrderService orderService = new OrderService ( new Alipay ()); orderService.checkout( 100 ); // 输出:使用支付宝支付... orderService = new OrderService ( new WechatPay ()); orderService.checkout( 200 ); // 输出:使用微信支付...
这种设计使得
OrderService与具体支付方式完全解耦,只需更换实现即可扩展功能。
插件化架构(Plugin Architecture)
场景描述:通过接口定义统一的插件规范,外部开发者可以按照接口实现自己的插件逻辑,主程序动态加载插件。
示例:日志插件系统
// 插件接口 public interface LogPlugin { void log (String message) ; } // 控制台日志插件 public class ConsoleLogPlugin implements LogPlugin { @Override public void log (String message) { System.out.println( "[CONSOLE] " + message); } } // 文件日志插件 public class FileLogPlugin implements LogPlugin { @Override public void log (String message) { // 实际写入文件操作略 System.out.println( "[FILE] " + message); } } // 插件管理器 public class PluginManager { private List<LogPlugin> plugins = new ArrayList <>(); public void addPlugin (LogPlugin plugin) { plugins.add(plugin); } public void executeLog (String message) { for (LogPlugin plugin : plugins) { plugin.log(message); } } }
使用方式:
PluginManager manager = new PluginManager (); manager.addPlugin( new ConsoleLogPlugin ()); manager.addPlugin( new FileLogPlugin ()); manager.executeLog( "系统启动成功" ); // 输出: // [CONSOLE] 系统启动成功 // [FILE] 系统启动成功
主程序无需知道插件如何实现,只需按接口调用即可,便于构建灵活、可扩展的插件系统。
回调机制(Callback)
场景描述:通过接口传递一个“任务完成后的处理方法”,常用于异步操作、事件监听等场景。
示例:网络请求回调
// 回调接口 public interface HttpCallback { void onSuccess (String response) ; void onFailure (String error) ; } // 模拟网络请求工具 public class HttpClient { public void get (String url, HttpCallback callback) { new Thread (() -> { try { Thread.sleep( 1000 ); // 模拟网络延迟 if ( "http://success" .equals(url)) { callback.onSuccess( "响应数据" ); } else { callback.onFailure( "请求失败" ); } } catch (InterruptedException e) { callback.onFailure( "请求中断" ); } }).start(); } }
使用方式:
HttpClient client = new HttpClient (); client.get( "http://success" , new HttpCallback () { @Override public void onSuccess (String response) { System.out.println( "请求成功:" + response); } @Override public void onFailure (String error) { System.out.println( "请求失败:" + error); } });
这种方式实现了异步非阻塞调用,并将结果处理交给调用者决定,广泛应用于 GUI、事件驱动、异步编程中。
总结对比
| 场景 | 描述 | 优点 |
|---|---|---|
| 解耦设计 | 将业务逻辑与实现分离 | 提高可维护性、可测试性 |
| 插件化 | 动态加载第三方功能模块 | 易于扩展、支持热插拔 |
| 回调机制 | 异步执行完成后通知调用方 | 避免阻塞、提高并发能力 |
最佳实践建议
| 实践 | 说明 |
|---|---|
| 接口命名清晰 | 如 Runnable , Comparable ,体现行为意图 |
| 单一职责原则 | 一个接口只定义一类行为 |
| 合理使用默认方法 | 扩展功能时避免破坏已有实现 |
| 不要滥用作常量容器 | 推荐使用枚举或 final class 替代 |
| 接口组合优于继承 | 多个接口组合比多重继承更灵活安全 |
小结
| 关键点 | 说明 |
|---|---|
| 接口是更纯粹的抽象 | 只定义方法签名,不包含实现(JDK8+ 例外) |
| 必须实现所有抽象方法 | 否则类必须声明为抽象类 |
| 接口是行为契约 | 表示“能做什么”,不是“是什么” |
| 支持多实现 | Java 类可以实现多个接口 |
| 支持默认方法/静态方法/私有方法 | 自 JDK8 起增强灵活性 |
| 可用于设计解耦、插件化、回调等场景 | 是构建模块化系统的基石 |
八、static关键字
什么是 static?
在 Java 中,static 是一个关键字,用来修饰 类的成员变量、方法、代码块和内部类。它表示该成员属于类本身,而不是类的实例(对象)。
简单理解:被
static修饰的成员是“类级别的”,而非“对象级别的”。
static 用法
| 成员类型 | 示例 | 说明 |
|---|---|---|
| 静态变量 | public static int count; | 类的所有实例共享一份值 |
| 静态方法 | public static void log(); | 只能访问静态成员,不能直接访问非静态成员 |
| 静态代码块 | static { ... } | 类加载时执行一次,用于初始化静态资源 |
| 静态内部类 | public static class Inner { ... } | 不依赖外部类实例即可创建 |
static 加载机制
JVM 类加载机制:懒加载(Lazy Loading),JVM 在首次使用某个类时才会加载该类。使用方式包括:
- 创建对象(如
new Person()) - 调用类的静态方法或访问静态变量
- 反射调用类
- 初始化子类(会先初始化父类)
类加载顺序原则,当类被加载时,JVM 按照以下顺序进行初始化:
父类静态成员 → 子类静态成员 → 父类构造器 → 子类构造器
实战分析
class Static1 { static { System.out.println( "静态1号加载" ); } } class Static2 { static { System.out.println( "静态2号加载" ); } public Static2 () { System.out.println( "静态2号构造执行" ); } } class Static3 { static String name = "water" ; static Static2 s2 = new Static2 (); } public class StaticTest { public static void main (String[] args) { String name = Static3.name; System.out.println(name); } }
输出结果:
静态2号加载 静态2号构造执行 water
分析过程:
-
main方法中访问了Static3.name,触发Static3类的加载。
-
- 加载
Static3时,优先初始化所有静态变量:name = "water":基本赋值操作,无复杂逻辑;s2 = new Static2():需要创建Static2实例;触发Static2类加载;
- 加载
-
执行构造函数:输出
"静态2号构造执行"; -
执行静态代码块:输出
"静态2号加载"; -
- 最后输出
name值为"water"。
- 最后输出
使用场景
| 场景 | 说明 | 示例 |
|---|---|---|
| 工具类封装 | 提供静态方法,无需实例化 | StringUtils.isEmpty(...) |
| 全局状态管理 | 维护类级别状态,如计数器 | public static int userCount; |
| 单例模式实现 | 控制类的唯一实例 | private static Singleton instance; |
| 缓存数据 | 保存类级别的缓存信息 | private static Map cache; |
| 静态工厂方法 | 替代构造器创建对象 | public static User createDefaultUser() |
注意事项
| 注意点 | 说明 |
|---|---|
| 无法访问非静态成员 | 静态方法/代码块不能直接访问非静态字段或方法 |
| 不可重写 | 静态方法不能被重写,只能被隐藏(通过子类定义同名静态方法) |
| 不参与多态 | 静态方法绑定的是编译时类型,不是运行时类型 |
| 线程安全问题 | 多线程下修改静态变量需注意同步控制 |
| 内存泄漏风险 | 静态引用长生命周期对象可能导致内存泄漏 |
推荐原则
- 尽量避免全局状态滥用;
- 工具类建议配合
final class和私有构造器使用; - 静态方法应保持无副作用,便于测试和复用。
1、尽量避免全局状态滥用,全局状态通常是指通过 public static 变量暴露出来的可变数据,例如:
public class GlobalState { public static int count = 0 ; }
这类变量在程序的任何地方都可以被修改,容易导致:
- 状态不可控
- 多线程下并发问题
- 单元测试困难
- 难以维护和调试
❌ 反例:滥用静态全局变量
public class UserManager { public static List<User> users = new ArrayList <>(); public static void addUser (User user) { users.add(user); } }
问题分析:
- 全局可变状态
users可能被任意代码修改。 - 多线程环境下可能出现并发异常。
- 测试时难以控制初始状态。
- 不利于模块解耦。
✅ 正例:封装状态 + 提供接口访问
public class UserManager { private static final List<User> users = new CopyOnWriteArrayList <>(); public static void addUser (User user) { users.add(user); } public static List<User> getAllUsers () { return new ArrayList <>(users); // 返回副本,防止外部修改 } }
改进点:
- 使用
private static封装状态,避免外部直接访问。 - 提供只读方法或受控操作,增强安全性。
- 更适合多线程环境(如使用线程安全集合)。
2、工具类建议配合 final class 和私有构造器使用,工具类一般只包含静态方法,不应该被实例化。如果不做限制,可能会出现如下情况:
StringUtils utils = new StringUtils (); // 没有意义,浪费内存
❌ 反例:未限制实例化的工具类
public class StringUtils { public static boolean isEmpty (String str) { return str == null || str.isEmpty(); } }
问题分析:
- 可以随意
new StringUtils(),虽然不会出错,但语义上不合理。 - 容易误导其他开发者误用。
✅ 正例:使用 final class + 私有构造器
public final class StringUtils { private StringUtils () { throw new UnsupportedOperationException ( "工具类不能实例化" ); } public static boolean isEmpty (String str) { return str == null || str.isEmpty(); } }
改进点:
final类防止被继承;- 私有构造器防止被实例化;
- 抛异常提示开发者不要尝试创建对象;
- 更加规范、专业、易于维护。
3、静态方法应保持无副作用,便于测试和复用无副作用的静态方法指的是:
- 输入相同,输出一定相同;
- 不依赖外部状态;
- 不修改外部变量;
- 不产生隐藏行为(如打印日志、修改数据库等);
这样可以保证方法是纯函数式的,便于:
- 单元测试;
- 并发调用;
- 日后重构;
- 被多个模块安全复用。
❌ 反例:带副作用的静态方法
public class Logger { private static int logCount = 0 ; public static void log (String message) { System.out.println(message); logCount++; } }
问题分析:
- 方法内部修改了静态变量
logCount,造成副作用; - 同一个输入多次调用结果不同;
- 不利于并发或单元测试;
- 隐藏状态,增加维护成本。
✅ 正例:无副作用的静态方法
public final class MathUtils { private MathUtils () {} public static int add ( int a, int b) { return a + b; } public static double square ( double x) { return x * x; } }
优点:
- 所有方法都是无副作用的;
- 输入输出明确;
- 易于测试(比如使用 JUnit);
- 可放心用于并发场景;
- 可缓存、可并行执行。
总结对比
| 对比项 | 普通成员 | 静态成员 |
|---|---|---|
| 所属对象 | 属于对象 | 属于类 |
| 生命周期 | 与对象一致 | 与类一致,类加载即存在 |
| 访问权限 | 可访问静态和非静态成员 | 只能访问静态成员 |
| 是否可以重写 | ✅ 是 | ❌ 否(只能隐藏) |
| 是否支持多态 | ✅ 是 | ❌ 否 |
| 是否推荐滥用 | ❌ 否 | ❌ 否(需谨慎使用) |
设计模式应用
| 模式 | 使用方式 |
|---|---|
| 单例模式 | 使用 private static 实例 + 静态方法获取 |
| 工厂模式 | 静态方法返回实例 |
| 工具类设计 | 静态方法 + 私有构造器防止实例化 |
| 缓存机制 | 静态 Map 缓存数据 |
九、final关键字
final 关键字概述
- 含义:表示“最终的”、“不可更改的”
- 作用对象:类、方法、变量(包括局部变量、成员变量、参数)
- 目的:
- 提高程序安全性
- 防止继承或重写
- 增强代码可读性和维护性
- 优化 JVM 性能(如内联)
final 的使用场景与作用
1. 修饰类(类不能被继承)
public final class MathUtils { // ... }
❌ 不允许有子类:
// 编译错误:Cannot subclass final class 'MathUtils' class MyMath extends MathUtils {}
典型应用:String、Integer 等包装类、工具类、安全类。
2. 修饰方法(方法不能被重写)
public class Animal { public final void eat () { System.out.println( "Animal is eating" ); } }
❌ 子类不能重写该方法:
class Dog extends Animal { // 编译错误:Overridden method is final public void eat () {} }
用途:
- 防止子类修改关键逻辑
- 类似设计模式中的模板方法
- 可用于性能优化(JVM 可以内联)
3. 修饰变量(值不变)
(1) 基本类型:值不能变
final int MAX_AGE = 100 ; MAX_AGE = 101 ; // 编译错误:cannot assign a value to final variable MAX_AGE
(2) 引用类型:引用地址不能变,但对象内容可以变
final List<String> names = new ArrayList <>(); names.add( "Alice" ); // 允许添加元素 names = new ArrayList <>(); // ❌ 编译错误:不能重新赋值
初始化时机:
- 声明时直接赋值
- 构造器中赋值(适用于实例变量)
- 实例初始化块赋值
- 静态变量可在静态初始化块中赋值
4. 修饰方法参数(参数不可变)
public void print ( final String msg) { msg = "hello" ; // ❌ 编译错误 }
用途:
- 明确参数不可修改,增强方法安全性
- 在匿名内部类中访问外部方法参数时必须为
final(JDK8 后隐式 final)
5. 与线程安全
final字段保证了初始化安全性(Final Field Safety)- JVM 会确保
final字段在构造完成之后对其他线程可见,防止重排序问题
public class User { private final String name; public User (String name) { this .name = name; } public String getName () { return name; } }
即使没有 synchronized 或 volatile,多线程访问 name也是安全的。
设计模式结合使用
| 模式 | 使用方式 |
|---|---|
| 不可变对象 | 所有字段设为 final ,构造函数初始化 |
| 单例模式 | final static 实例 + 静态内部类实现懒加载 |
| 模板方法 | 父类定义 final 方法,子类不能重写 |
| 工具类 | 类设为 final ,构造器私有,防止继承 |
final 与常量定义
- 推荐将常量定义为
public static final
public class Constants { public static final String APP_NAME = "MyApp" ; }
注意事项:
final常量是编译时常量,会被编译器优化(内联),如果多个类引用此常量,在修改后需重新编译所有引用类。- 若希望运行时动态获取值,避免使用
final。
final 与 static 的区别
| 特性 | final | static |
|---|---|---|
| 表示意义 | 不可变 | 属于类本身 |
| 修饰对象 | 变量、方法、类 | 变量、方法、内部类、代码块 |
| 是否绑定实例 | 否(变量) | 是(非静态) |
| 多线程安全 | final 字段安全 | static 需手动同步 |
常见误区
| 误区 | 正确认识 |
|---|---|
| final List 不可变 | 错,只是引用不可变,内容仍可变 |
| final 一定能提高性能 | 错,JVM 会自动优化,通常无需手动加 |
| final 能替代 private | 错,两者互不冲突,应配合使用 |
| final 方法一定不会被重写 | 对,但可通过反射绕过(仅限测试/破坏性行为) |
推荐使用场景
| 场景 | 是否建议使用 final |
|---|---|
| 不可变类字段 | ✅ |
| 单例实例 | ✅ |
| 方法参数 | ✅(增强代码可读性) |
| 工具类 | ✅(类 + 构造器私有) |
| 模板方法 | ✅ |
| 临时变量 | ⚠️ 视情况而定 |
| 多态需要重写的方法 | ❌ 不适合 |
代码示例
不可变类+单列+final参数
public final class Person { // 类不可继承 private final String name; // 字段不可变 private final int age; public Person ( final String name, final int age) { // 参数不可变 this .name = name; this .age = age; } public String getName () { return name; } public int getAge () { return age; } // 单例持有者 private static class Holder { static final Person INSTANCE = new Person ( "Tom" , 20 ); // final 实例 } public static Person getInstance () { return Holder.INSTANCE; } }
小结
| 使用位置 | 作用 | 示例 |
|---|---|---|
| 类前 | 类不能被继承 | public final class A {} |
| 方法前 | 方法不能被重写 | public final void doSomething() |
| 变量前 | 变量值不能变 | private final int count = 10; |
| 方法参数 | 参数不可变 | void method(final String s) |
| 内部类 | 成员内部类可访问 final 变量 | final int x = 10; new Runnable() {...} |
十、多态性
什么是多态?
多态(Polymorphism) 是面向对象编程的三大特性之一(封装、继承、多态),其含义是:
同一种类型,在不同的上下文中表现出不同的行为。
核心特征:
- 一个接口,多种实现
- 一个父类引用指向不同子类对象
- 方法重写 + 运行时绑定
表现形式
| 类型 | 描述 | 示例 |
|---|---|---|
| 接口多态 | 一个接口被多个类实现 | Animal a = new Dog(); |
| 方法重载 | 同名方法参数列表不同 | eat(String food) vs eat(String food, String drink) |
| 继承多态 | 父类引用指向子类对象 | Animal a = new Cat(); |
多态本质
“编译看左,运行看右”,这是理解多态的关键口诀。
编译阶段
- 看变量声明的类型(左边)
- 只能调用该类型中定义的方法和属性
运行阶段
- 看实际创建的对象类型(右边)
- 执行的是该对象的方法体(如果子类重写了)
Animal animal = new Dog (); animal.run(); // 调用的是 Dog 的 run()
应用场景
-
- 接口作为方法参数(解耦)
public void testAnimal (Animal animal) { animal.run(); // 支持所有 Animal 实现类 }
-
- 抽象类/接口统一处理多种行为
List<Animal> animals = Arrays.asList( new Dog (), new Cat ()); animals.forEach(Animal::run); // 自动适配具体实现
-
- 配合策略模式、工厂模式等设计模式使用
Payment payment = PaymentFactory.getPayment( "alipay" ); payment.pay( 100 ); // 不同支付方式自动匹配
实战示例
接口定义
interface Animal { void run () ; }
子类实现
class Dog implements Animal { @Override public void run () { System.out.println( "Dog run" ); } } class Cat implements Animal { @Override public void run () { System.out.println( "Cat run" ); } public void talk () { System.out.println( "Cat talks" ); } } class HelloCat extends Cat { @Override public void talk () { System.out.println( "HelloCat say hello" ); } public void eat (String foodName) { System.out.println( "吃 " + foodName); } public void eat (String foodName, String drinkName) { System.out.println( "吃 " + foodName + ";喝 " + drinkName); } }
测试类
public class PolymorphismTest { public static void main (String[] args) { Animal dog = new Dog (); Animal cat = new Cat (); Animal helloCat = new HelloCat (); testAnimal(dog); // 输出: Dog run testAnimal(cat); // 输出: Cat run testAnimal(helloCat); // 输出: Cat run if (cat instanceof Cat) { Cat myCat = (Cat) cat; myCat.talk(); // 输出: Cat talks } if (helloCat instanceof Cat) { Cat myHelloCat = (Cat) helloCat; myHelloCat.talk(); // 输出: HelloCat say hello } if (helloCat instanceof HelloCat) { HelloCat dearHelloCat = (HelloCat) helloCat; dearHelloCat.eat( "鱼" ); // 输出: 吃 鱼 dearHelloCat.eat( "鸡腿" , "可乐" ); // 输出: 吃 鸡腿;喝 可乐 } } public static void testAnimal (Animal animal) { animal.run(); } }
向下转型(Downcasting)
当需要访问子类扩展的方法时,必须进行向下转型。
✅ 安全做法:配合 instanceof 判断
if (animal instanceof Cat) { Cat cat = (Cat) animal; cat.talk(); }
❌ 错误做法:直接强制转换
Cat cat = (Cat) animal; // 若 animal 实际不是 Cat 类型,会抛 ClassCastException
最佳实践
| 优势 | 说明 |
|---|---|
| 提高代码复用性 | 通过统一接口操作不同对象 |
| 增强扩展性 | 新增子类无需修改已有逻辑 |
| 解耦合 | 上层逻辑不依赖具体实现类 |
| 支持策略化设计 | 动态切换行为,符合开闭原则 |
最佳实践建议:
- 尽量使用接口或抽象类作为方法入参
- 方法内部如需子类特有方法,应先判断类型再转型
- 多态结合设计模式(如策略、模板、工厂)效果更佳
- 避免过多依赖 instanceof,考虑重构为多态结构
多态 vs 重载 vs 重写 对比
| 特性 | 多态 | 重写(Override) | 重载(Overload) |
|---|---|---|---|
| 发生位置 | 父类与子类之间 | 子类覆盖父类方法 | 同一类中方法名相同 |
| 参数要求 | 不变 | 参数不变 | 参数不同 |
| 返回值 | 不变 | 可协变返回 | 必须不同签名 |
| 访问权限 | 不能缩小 | 不能比父类更严格 | 无关 |
| 异常 | 不能抛出新异常 | 不能比父类更宽泛 | 无关 |
| 决定时机 | 运行时动态绑定 | 运行时决定 | 编译时决定 |
设计模式
在实际业务开发中,基于多态构建规则引擎和状态机 是一种非常常见且优雅的设计方式。它通过接口或抽象类定义统一行为,子类实现具体逻辑,使得系统具备良好的 扩展性、可维护性和解耦性。
举例说明:
- 基于多态的规则引擎(Rule Engine)
- 基于多态的状态机(State Machine)
规则引擎
假设我们有一个促销系统,需要根据不同的用户等级(普通用户、VIP 用户、SVIP 用户)应用不同的折扣策略。
核心设计思想
- 定义一个
DiscountStrategy接口 - 每个等级对应一个实现类
- 使用工厂类动态获取对应的策略
-
业务层统一调用策略接口方法
-
- 定义策略接口
public interface DiscountStrategy { double applyDiscount ( double price) ; }
-
- 实现不同策略
public class NormalUserDiscount implements DiscountStrategy { @Override public double applyDiscount ( double price) { return price * 0.95 ; // 95折 } } public class VipUserDiscount implements DiscountStrategy { @Override public double applyDiscount ( double price) { return price * 0.85 ; // 85折 } } public class SuperVipUserDiscount implements DiscountStrategy { @Override public double applyDiscount ( double price) { return price * 0.75 ; // 75折 } }
-
- 策略工厂类
public class DiscountFactory { public static DiscountStrategy getDiscountStrategy (String userType) { switch (userType.toLowerCase()) { case "vip" : return new VipUserDiscount (); case "svip" : return new SuperVipUserDiscount (); default : return new NormalUserDiscount (); } } }
-
- 业务使用示例
public class ShoppingCart { private DiscountStrategy discountStrategy; public void setDiscountStrategy (DiscountStrategy discountStrategy) { this .discountStrategy = discountStrategy; } public double checkout ( double totalPrice) { return discountStrategy.applyDiscount(totalPrice); } }
-
- 调用示例
public class Main { public static void main (String[] args) { String userType = "svip" ; DiscountStrategy strategy = DiscountFactory.getDiscountStrategy(userType); ShoppingCart cart = new ShoppingCart (); cart.setDiscountStrategy(strategy); double finalPrice = cart.checkout( 100 ); System.out.println( "最终价格:" + finalPrice); // 输出:75.0 } }
6.优势分析
| 特点 | 描述 |
|---|---|
| 扩展性强 | 新增策略只需添加新类,不修改已有代码 |
| 解耦清晰 | 业务逻辑与具体策略分离 |
| 可测试性强 | 每个策略独立,易于单元测试 |
| 易于替换 | 运行时可动态切换策略 |
状态机
假设我们要处理订单状态流转:新建(NEW)、已支付(PAID)、已发货(SHIPPED)、已完成(COMPLETED),每种状态有不同的操作限制。
核心设计思想:
- 定义一个
OrderState抽象类或接口 - 每个状态为一个子类,实现对应行为
- 订单对象持有当前状态引用
-
状态之间可以互相转换
-
- 定义状态接口
public abstract class OrderState { public abstract void pay (Order order) ; public abstract void ship (Order order) ; public abstract void complete (Order order) ; }
-
- 实现各个状态类
public class NewState extends OrderState { @Override public void pay (Order order) { System.out.println( "订单已支付" ); order.setState( new PaidState ()); } @Override public void ship (Order order) { throw new IllegalStateException ( "订单未支付,无法发货" ); } @Override public void complete (Order order) { throw new IllegalStateException ( "订单未支付,无法完成" ); } } public class PaidState extends OrderState { @Override public void pay (Order order) { System.out.println( "订单已支付,请勿重复支付" ); } @Override public void ship (Order order) { System.out.println( "订单已发货" ); order.setState( new ShippedState ()); } @Override public void complete (Order order) { throw new IllegalStateException ( "订单未发货,无法完成" ); } } public class ShippedState extends OrderState { @Override public void pay (Order order) { System.out.println( "订单已支付" ); } @Override public void ship (Order order) { System.out.println( "订单已发货,请勿重复发货" ); } @Override public void complete (Order order) { System.out.println( "订单已完成" ); order.setState( new CompletedState ()); } } public class CompletedState extends OrderState { @Override public void pay (Order order) { System.out.println( "订单已完成,无需支付" ); } @Override public void ship (Order order) { System.out.println( "订单已完成,无需发货" ); } @Override public void complete (Order order) { System.out.println( "订单已完成" ); } }
-
- 定义订单类
public class Order { private OrderState state; public Order () { this .state = new NewState (); } public void setState (OrderState state) { this .state = state; } public void pay () { state.pay( this ); } public void ship () { state.ship( this ); } public void complete () { state.complete( this ); } }
-
- 调用示例
public class Main { public static void main (String[] args) { Order order = new Order (); order.pay(); // 输出:订单已支付 order.ship(); // 输出:订单已发货 order.complete(); // 输出:订单已完成 } }
5.优势分析
| 特点 | 描述 |
|---|---|
| 状态隔离 | 每个状态只关注自己的行为,互不影响 |
| 行为封装 | 不同状态的行为由各自类封装 |
| 易于扩展 | 新增状态只需新增类,不修改已有逻辑 |
| 可读性强 | 状态流转清晰直观,便于理解维护 |
总结对比
| 功能 | 规则引擎 | 状态机 |
|---|---|---|
| 设计模式 | 策略模式 | 状态模式 |
| 主要结构 | 接口+多个实现类 | 抽象类+多个状态子类 |
| 行为控制 | 根据条件选择策略 | 根据状态变化执行不同行为 |
| 应用场景 | 折扣计算、风控规则、审批流程等 | 订单状态、任务状态、权限流转等 |
| 优点 | 扩展灵活、逻辑清晰 | 状态管理集中、避免 if-else |
进阶建议
| 场景 | 建议 |
|---|---|
| 复杂规则组合 | 结合责任链(Chain of Responsibility)模式 |
| 状态流转复杂 | 引入状态图配置化或数据库驱动 |
| 需要动态加载 | 使用 Spring IOC 容器注入策略/状态类 |
| 性能要求高 | 缓存策略实例、避免频繁创建 |
小结
| 关键点 | 说明 |
|---|---|
| 多态是面向对象的核心机制 | 表现为“一种类型,多种行为” |
| “编译看左,运行看右” | 是理解多态的核心口诀 |
| 多态主要体现在接口和继承中 | 可用于统一处理多种实现 |
| 向下转型要配合 instanceof | 防止 ClassCastException |
| 多态适用于策略、插件、事件驱动等场景 | 提高系统灵活性和扩展性 |
十一、枚举
枚举的本质与特性
- 枚举本质是类:每个枚举类型都隐式继承自
java.lang.Enum类。所以不能显式继承其他类(Java 单继承限制)。 - 线程安全:JVM 在加载时初始化枚举实例,确保其线程安全。
- 多例模式:枚举本质上是一个有限的多例集合,适用于状态、行为固定的情况。
public enum Season { SPRING, SUMMER, FALL, WINTER; }
等价于:
public final class Season extends Enum <Season> { public static final Season SPRING = new Season (); public static final Season SUMMER = new Season (); ... }
枚举的成员定义与使用
- 可以定义字段、构造函数、方法、抽象方法等。
- 构造器必须是私有或默认访问权限。
- 每个枚举常量可以有自己的实现(如策略枚举)。
示例:带属性和方法的枚举
public enum TradeStatus { PAYING( 1 , "支付中" ), PAID( 2 , "已支付" ), CANCEL( 3 , "取消支付" ); private final int code; private final String desc; TradeStatus( int code, String desc) { this .code = code; this .desc = desc; } public int getCode () { return code; } public String getDesc () { return desc; } public static TradeStatus getByCode ( int code) { return Arrays.stream(values()) .filter(s -> s.code == code) .findFirst() .orElseThrow(() -> new RuntimeException ( "未找到对应交易状态" )); } }
枚举与策略模式结合
- 枚举中定义抽象方法,每个枚举值提供具体实现,是一种轻量级策略模式实现方式。
public enum Pay { ALIPAY( "alipay" ) { @Override public void pay (Object request) { // 支付宝支付逻辑 } }, WECHAT( "wechat" ) { @Override public void pay (Object request) { // 微信支付逻辑 } }; private final String name; Pay(String name) { this .name = name; } public abstract void pay (Object request) ; }
调用方式:
Pay.ALIPAY.pay(request);
枚举常用方法
| 方法名 | 描述 |
|---|---|
| values() | 返回枚举数组,包含所有枚举值 |
| valueOf(String) | 根据名称获取枚举实例 |
| name() | 获取枚举常量名称 |
| ordinal() | 获取枚举常量的索引位置 |
枚举在 switch 中的应用
TradeStatus status = TradeStatus.PAID; switch (status) { case PAYING: System.out.println( "支付中" ); break ; case PAID: System.out.println( "已支付" ); break ; default : System.out.println( "未知状态" ); }
最佳实践
| 优势 | 说明 |
|---|---|
| 线程安全 | JVM 加载保证唯一性 |
| 避免魔法值 | 替代 int 常量,提高可读性和维护性 |
| 封装性强 | 可封装业务逻辑、属性、方法 |
| 易扩展 | 可添加字段、方法、甚至接口实现 |
| 序列化安全 | 枚举的序列化机制自动处理单例问题 |
数据库字段映射:使用 code字段与数据库状态码对应,便于持久化;提供 getByCode() 方法用于反向查找。
注意事项
- 不适合频繁变动的状态或大量数据。
- 枚举不支持动态添加新的枚举值。
- 枚举类中不能有
abstract字段。 - 枚举中可以有抽象方法。
- 枚举类不能被继承。
单例模式(枚举版)
public enum Singleton { INSTANCE; public void doSomething () { // 实现逻辑 } }
调用:
Singleton.INSTANCE.doSomething();
枚举版单例模式:线程安全,防止反射攻击(通过枚举反编译发现无法创建新实例),并且自动支持序列化/反序列化。
小结
| 使用场景 | 示例 |
|---|---|
| 状态表示 | 订单状态、用户角色 |
| 分类标记 | 支付渠道、日志级别 |
| 常量管理 | 错误码、配置项 |
| 策略模式 | 多种支付方式、多种算法 |
| 单例模式 | 工具类、配置中心 |
十二、内部类
内部类(Inner Class)是定义在另一个类中的类。它与外部类有强关联性,可以访问外部类的成员,包括私有成员。
内部类分类
| 类型 | 定义位置 | 是否静态 | 特点 |
|---|---|---|---|
| 成员内部类 | 外部类中,方法外 | 否 | 可以访问外部类所有成员,必须依附于外部类实例 |
| 静态内部类 | 外部类中,方法外 | 是 | 不依赖外部类实例,只能访问外部类的静态成员 |
| 局部内部类 | 方法或代码块内 | 否 | 作用域仅限于当前方法或代码块 |
| 匿名内部类 | 表达式中 | 否 | 没有类名,用于简化接口/抽象类实现 |
成员内部类(非静态)
public class Outer { private String name = "Outer" ; public class Inner { public void show () { System.out.println(name); // 可以访问外部类的私有字段 } } public static void main (String[] args) { Outer outer = new Outer (); Outer. Inner inner = outer. new Inner (); // 必须先创建外部类实例 inner.show(); } }
⚠️ 注意:成员内部类不能拥有静态成员(除非是
static final常量)
静态内部类
public class Outer { private static String name = "Static Outer" ; public static class StaticInner { public void show () { System.out.println(name); // 只能访问外部类的静态成员 } } public static void main (String[] args) { StaticInner inner = new StaticInner (); // 不需要外部类实例 inner.show(); } }
✅ 常用于单例模式(懒加载 + 线程安全)
public class Service { private Service () {} private static class ServiceHolder { static final Service INSTANCE = new Service (); // JVM保证线程安全 } public static Service getInstance () { return ServiceHolder.INSTANCE; } }
局部内部类
定义在方法或代码块中,作用域受限。
public class Test { public void method () { class LocalInner { void sayHi () { System.out.println( "Hello from local inner class" ); } } new LocalInner ().sayHi(); } }
⚠️ 注意:
- 不能使用访问修饰符(如
private,public) - 可以访问外部类成员,也可以访问方法中的 final 变量(JDK8+ 可隐式 final)
匿名内部类(Anonymous Inner Class)
没有类名,通常用于一次性使用的接口或抽象类实现。
interface Talkable { void talk () ; } public class Main { public static void talkSome (Talkable talk) { talk.talk(); } public static void main (String[] args) { talkSome( new Talkable () { @Override public void talk () { System.out.println( "说话好听" ); } }); } }
✅ Lambda 表达式是对匿名内部类的简化写法(函数式接口)
talkSome(() -> System.out.println( "说话好听" ));
应用场景
| 场景 | 示例说明 |
|---|---|
| 单例模式 | 使用静态内部类实现线程安全、懒加载的单例 |
| 事件监听器 | GUI 开发中使用匿名内部类处理点击事件 |
| 箖合对象 | 成员内部类可紧密耦合外部类逻辑 |
| 简化回调 | 匿名内部类用于网络请求回调、Runnable 等 |
| 工具类封装 | 静态内部类作为工具辅助类(如 Builder 模式) |
注意事项
| 事项 | 说明 |
|---|---|
| 访问权限 | 成员内部类可以访问外部类所有成员,包括私有 |
| 静态限制 | 成员内部类不能定义静态成员(除非是 final static 常量) |
| 实例绑定 | 成员内部类必须绑定一个外部类实例才能创建 |
| 编译生成 | 匿名内部类编译后会生成类似 Main$1.class 的文件 |
| Lambda 替代 | 匿名内部类可用 Lambda 表达式替代(适用于函数式接口) |
| 性能开销 | 内部类会增加类数量和内存占用,避免滥用 |
Builder 模式 + 静态内部类
public class User { private final String name; private final int age; private User (Builder builder) { this .name = builder.name; this .age = builder.age; } public static class Builder { private String name; private int age; public Builder setName (String name) { this .name = name; return this ; } public Builder setAge ( int age) { this .age = age; return this ; } public User build () { return new User ( this ); } } } // 使用 User user = new User .Builder() .setName( "Tom" ) .setAge( 20 ) .build();
小结
| 类型 | 是否依赖外部类实例 | 是否可访问外部类成员 | 是否常用 |
|---|---|---|---|
| 成员内部类 | 是 | 是 | ✅ 常用 |
| 静态内部类 | 否 | 仅静态成员 | ✅ 常用 |
| 局部内部类 | 是 | 是 | ❌ 较少 |
| 匿名内部类 | 是 | 是 | ✅ 常用 |
内部类命名建议
- 静态内部类:推荐使用大驼峰命名法,如
ServiceHolder - 局部/匿名内部类:无需显式命名(匿名类)
- 功能明确:内部类应职责单一,避免复杂逻辑嵌套
十三、总结
Java面向对象的内容非常多,但又非常重要;因为只有积极锻炼面向对象的思维,才能助力写出高内聚低耦合的代码。弄懂:类与对象、封装性、继承性、多态性、抽象类、接口、内部类、this、super、static、final等核心概念是重中之重,保证语法正确,分辨最佳使用场景,警惕各种坑,才能写出优雅安全的代码。这也是读懂其他框架源码的基础,源码也是使用这些核心概念,经过设计者使用各种设计模式一步步反复迭代锤炼起来的。
万丈高楼平台起,夯实基础才能更快登高望远!
时不时回顾本文内容,开发过程中设计类、设计变量时遇到的问题,基本都能在本文找到答案。多敲代码,多思考,多检查。review代码,问问自己:
这里使用接口设计、抽象类设计是不是更有益于解耦,更利于动态扩展;使用static关键字是不是多实例共享,是不是节省资源,提高性能了;使用final是不是值初始化后就不变了,这样修饰变量,该变量在多线程环境下是不是更安全了。
多看看自己的代码,结合Java面向对象的各个核心知识点的最佳实践,一步步修复调整优化,争取写出健壮优雅的漂亮代码,如诗一样。
更多推荐


所有评论(0)