1. 面向对象编程的核心进阶
面向对象编程(OOP)是Java语言的核心特性,也是区分初级与中级开发者的重要分水岭。在掌握了类与对象、继承与多态等基础概念后,我们需要深入理解几个关键的高级特性:抽象类与接口、内部类、枚举类型以及注解机制。这些特性在实际开发中无处不在,比如Spring框架大量使用接口和注解,Android开发频繁使用内部类,而枚举则是处理固定集合的利器。
提示:学习这些特性时,建议结合具体场景思考其设计意图,比如"为什么需要抽象类?"、"接口解决了什么问题?",这样理解会更加深刻。
1.1 抽象类与接口的本质区别
抽象类(abstract class)和接口(interface)都是实现多态的重要方式,但它们的定位和适用场景有本质不同:
java复制// 抽象类示例
public abstract class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public abstract void makeSound();
public void sleep() {
System.out.println(name + " is sleeping");
}
}
// 接口示例
public interface Flyable {
void fly();
default void emergencyLand() {
System.out.println("Emergency landing!");
}
}
抽象类的特点:
- 可以包含抽象方法和具体方法
- 可以有成员变量
- 构造方法不能被直接调用
- 单继承限制(一个类只能继承一个抽象类)
接口的特点(Java 8+):
- 方法默认是public abstract的
- 可以包含default方法和static方法
- 可以定义常量(public static final)
- 支持多实现(一个类可以实现多个接口)
选择抽象类还是接口?我的经验法则是:
- 当需要定义模板方法模式或部分实现时,用抽象类
- 当需要定义行为契约或多继承时,用接口
- 在Java 8之后,接口的default方法模糊了二者的界限,此时更倾向于使用接口
1.2 内部类的四种形态与使用场景
内部类是Java中一个强大但容易被误用的特性,根据声明位置和方式不同,可以分为四种类型:
- 成员内部类(Member Inner Class):
java复制class Outer {
private int x = 10;
class Inner {
void display() {
System.out.println("x is " + x); // 可以直接访问外部类私有成员
}
}
}
使用场景:当内部类需要与外部类紧密耦合,且需要访问外部类实例成员时。比如Android中的Adapter经常作为View的内部类。
- 静态内部类(Static Nested Class):
java复制class Outer {
static class StaticInner {
// 不能直接访问外部类的非静态成员
}
}
使用场景:当内部类不需要访问外部类实例时。比如集合类的Iterator实现通常使用静态内部类。
- 方法局部内部类(Local Inner Class):
java复制class Outer {
void method() {
class LocalInner {
// 类定义在方法内部
}
}
}
使用场景:仅在某个方法内部需要使用的辅助类。实际开发中较少使用,通常可以用lambda表达式替代。
- 匿名内部类(Anonymous Inner Class):
java复制button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// 处理点击事件
}
});
使用场景:需要快速实现接口或继承类,且只使用一次的场合。Java 8之后,很多匿名内部类场景可以用lambda表达式简化。
注意:匿名内部类持有外部类引用可能导致内存泄漏,在Android开发中尤其需要注意。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 枚举与注解:更优雅的代码组织方式
2.1 枚举类型的高级用法
枚举(enum)远不止是简单的常量集合,它实际上是继承自java.lang.Enum的类。我们可以为枚举添加方法、实现接口,甚至定义抽象方法让每个枚举实例实现:
java复制public enum Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
public double apply(double x, double y) { return x - y; }
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
public abstract double apply(double x, double y);
@Override
public String toString() {
return symbol;
}
}
枚举的实用技巧:
- 使用枚举实现单例模式(线程安全且防反射攻击)
- 枚举可以用于策略模式,每个枚举实例代表一种策略
- EnumSet和EnumMap是针对枚举优化的高性能集合
- 枚举可以与switch语句完美配合
2.2 注解机制与元编程
注解(Annotation)是Java 5引入的元数据机制,现代框架如Spring、Hibernate都重度依赖注解。理解注解需要掌握:
- 内置注解:
- @Override:检查方法是否真的重写了父类方法
- @Deprecated:标记过时API
- @SuppressWarnings:抑制编译器警告
- @FunctionalInterface:标记函数式接口
- 元注解(用于定义注解的注解):
- @Target:指定注解可以应用的目标(类、方法、字段等)
- @Retention:指定注解保留策略(源码、class文件、运行时)
- @Documented:是否包含在Javadoc中
- @Inherited:是否允许子类继承
- 自定义注解示例:
java复制@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Benchmark {
long timeout() default 1000; // 默认超时时间1秒
String description() default "";
}
- 注解处理:
- 编译时处理:通过注解处理器(APT)生成代码,如Lombok
- 运行时处理:通过反射读取注解信息,如Spring的@Component
实际经验:注解虽然强大,但过度使用会使代码难以理解。建议只在框架开发或需要明显元数据标记时使用注解。
3. 面向对象设计原则与模式
3.1 SOLID原则实践指南
SOLID原则是面向对象设计的基石,理解这些原则比死记设计模式更重要:
- 单一职责原则(SRP):
- 一个类应该只有一个引起变化的原因
- 实际开发中,我常用"能否用一句话描述这个类的职责"来检验
- 违反示例:一个类既处理订单业务,又负责数据库访问
- 开闭原则(OCP):
- 对扩展开放,对修改关闭
- 通过抽象和接口实现,比如策略模式
- 实际案例:Java的Collections.sort()方法对比较策略开放
- 里氏替换原则(LSP):
- 子类必须能够替换它们的基类
- 避免重写父类非抽象方法改变行为
- 典型违反:正方形继承长方形导致setWidth/setHeight行为异常
- 接口隔离原则(ISP):
- 客户端不应该被迫依赖它不使用的接口
- 将大接口拆分为更小、更具体的接口
- 实际应用:Java的Closeable和AutoCloseable接口分离
- 依赖倒置原则(DIP):
- 高层模块不应该依赖低层模块,二者都应该依赖抽象
- 抽象不应该依赖细节,细节应该依赖抽象
- Spring的依赖注入就是这一原则的完美体现
3.2 常用设计模式实现
- 工厂模式:
java复制public interface Shape {
void draw();
}
public class ShapeFactory {
public Shape getShape(String shapeType) {
if (shapeType == null) return null;
switch (shapeType.toLowerCase()) {
case "circle": return new Circle();
case "rectangle": return new Rectangle();
default: throw new IllegalArgumentException("Unknown shape");
}
}
}
使用场景:当创建逻辑复杂或需要统一管理对象创建时。Spring的BeanFactory就是工厂模式的扩展。
- 观察者模式:
java复制public class NewsAgency {
private String news;
private List<Channel> channels = new ArrayList<>();
public void addObserver(Channel channel) {
channels.add(channel);
}
public void setNews(String news) {
this.news = news;
channels.forEach(ch -> ch.update(news));
}
}
public interface Channel {
void update(String news);
}
Java标准库提供了Observable类和Observer接口,但在实际开发中,建议使用PropertyChangeListener或第三方库如RxJava。
- 装饰器模式:
java复制public interface Coffee {
double getCost();
String getDescription();
}
public class SimpleCoffee implements Coffee {
// 基础实现
}
public abstract class CoffeeDecorator implements Coffee {
protected final Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
}
Java I/O流就是装饰器模式的经典应用,如BufferedInputStream装饰FileInputStream。
4. Java内存模型与面向对象实践
4.1 对象生命周期管理
理解对象在JVM中的生命周期对编写健壮代码至关重要:
- 对象创建:
- new关键字触发类加载(如果未加载)
- 分配堆内存(可能触发GC)
- 初始化成员变量(默认值→显式初始化→构造器赋值)
- 对象使用:
- 栈上的引用变量指向堆中的对象
- 方法调用时的参数传递是值传递(引用拷贝)
- 静态变量与实例变量的生命周期不同
- 对象销毁:
- 当没有引用指向对象时,成为GC候选
- finalize()方法不推荐使用(不可靠且影响性能)
- 对于需要清理的资源(如文件句柄、数据库连接),应实现AutoCloseable接口配合try-with-resources使用
内存泄漏常见场景:
- 静态集合持有对象引用
- 未正确关闭资源(数据库连接、流等)
- 监听器未注销
- 不合理的缓存实现
4.2 面向对象最佳实践
- 不可变对象设计:
java复制public final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
// 只有getter方法,没有setter
public int getX() { return x; }
public int getY() { return y; }
// 返回新对象而不是修改状态
public ImmutablePoint move(int dx, int dy) {
return new ImmutablePoint(x + dx, y + dy);
}
}
不可变对象的优点:线程安全、易于缓存和共享、避免意外修改。
- 组合优于继承:
java复制// 不推荐
public class Stack extends ArrayList {
// 容易暴露ArrayList的不相关方法
}
// 推荐
public class Stack {
private final ArrayList list = new ArrayList();
public void push(Object obj) {
list.add(obj);
}
public Object pop() {
if (list.isEmpty()) throw new EmptyStackException();
return list.remove(list.size() - 1);
}
}
继承破坏了封装性,而且Java的单继承限制使得组合更加灵活。
- 防御性拷贝:
java复制public class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
this.start = new Date(start.getTime()); // 防御性拷贝
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException();
}
}
public Date getStart() {
return new Date(start.getTime()); // 返回拷贝
}
}
防止外部代码修改对象内部状态,特别是在涉及可变对象(如Date)时。
- 正确实现equals和hashCode:
java复制@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
遵循的规则:
- 自反性:x.equals(x)必须为true
- 对称性:x.equals(y)和y.equals(x)结果相同
- 传递性:如果x.equals(y)且y.equals(z),则x.equals(z)
- 一致性:多次调用结果相同
- 非空性:x.equals(null)必须为false
- hashCode必须与equals一致:相等的对象必须有相同的hashCode
在实际项目中,我经常使用Lombok的@EqualsAndHashCode注解来自动生成这些方法,但理解其原理非常重要。
