文章目录
面向对象特性
Java 是一种纯面向对象的编程语言(除了基本数据类型)。面向对象编程(OOP)有三大核心特性:继承、封装、多态。例如属性声明为 private,提供 public 的 getter 和 setter 方法。
- 继承:子类可以继承父类的非私有属性和方法,并可以扩展或重写父类的行为,例如使用 extends 关键字,子类 Dog 继承父类 Animal的 eat() 方法。
java
public class Animal {
public void eat() {
System.out.println("动物正在吃东西");
}
}
public class Dog extends Animal {
// 继承 eat() 方法,还可以添加自己的方法
public void bark() {
System.out.println("狗在汪汪叫");
}
}
Dog dog = new Dog();
dog.eat(); // 继承自 Animal
dog.bark(); // 自己的方法
- 封装:将数据(属性)和操作数据的方法(行为)绑定在一起,并隐藏对象的内部细节,只对外暴露必要的访问接口。
java
public class Person {
private String name; // 私有属性,外部不能直接访问
private int age;
// 通过公开方法访问
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age > 0 && age < 150) { // 可以在 setter 中加入校验逻辑
this.age = age;
}
}
}
- 多态:同一个行为(方法)在不同的对象上具有不同的表现形式。多态分为编译时多态和运行时多态。
- 编译时多态:编译器在编译时可以识别一个方法的多种形态,它指的是方法的重载。
- 运行时多态:编译器在编译时无法最终确定匹配哪个方法,在真正运行时,才能确定执行哪个类的哪个方法,它指的是方法的重写。
java
// 父类
public class Animal {
public void makeSound() {
System.out.println("动物发出声音");
}
}
// 子类1
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("喵喵喵");
}
}
// 子类2
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("汪汪汪");
}
}
// 多态使用
public class Test {
public static void main(String[] args) {
Animal a1 = new Cat(); // 父类引用指向子类对象
Animal a2 = new Dog();
a1.makeSound(); // 输出:喵喵喵
a2.makeSound(); // 输出:汪汪汪
}
}
面向对象设计原则
- 单一职责原则:每一个类(接口)应该专注于做一件事情,一个类应该只有一个引起它变化的原因。
例如用户实体类,只负责保存用户信息;数据库操作类只负责用户数据的持久化
- 开闭原则:软件实体(类、模块、函数)应该对扩展开放,对修改关闭,即不修改原有代码的前提下增加新功能。
例如新增三角形时,只需新增 Triangle 实现 Shape,无需修改 AreaCalculator
java
// 抽象形状
public interface Shape {
double getArea();
}
// 矩形
public class Rectangle implements Shape {
private double width, height;
public Rectangle(double w, double h) { width = w; height = h; }
public double getArea() { return width * height; }
}
// 圆形
public class Circle implements Shape {
private double radius;
public Circle(double r) { radius = r; }
public double getArea() { return Math.PI * radius * radius; }
}
// 计算器(对扩展开放,对修改关闭)
public class AreaCalculator {
public double sum(Shape[] shapes) {
double total = 0;
for (Shape s : shapes) total += s.getArea();
return total;
}
}
- 里氏替换原则:子类对象必须能够替换掉所有父类对象,且程序行为不变。即子类不要重写/重载父类的非抽象方法以改变其预期行为。
java
// 父类矩形
class Rectangle {
protected int width, height;
public void setWidth(int w) { width = w; }
public void setHeight(int h) { height = h; }
public int getArea() { return width * height; }
}
// 子类正方形
class Square extends Rectangle {
@Override
public void setWidth(int w) { width = w; height = w; }
@Override
public void setHeight(int h) { width = h; height = h; }
}
// 测试函数
void resize(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
// 期望面积 = 20,但如果传入Square,面积会变成16(因为宽高都被设为4)
System.out.println(r.getArea());
}
java
//正确方式
public interface Shape {
int getArea();
}
public class Rectangle implements Shape { /* 独立宽高 */ }
public class Square implements Shape { /* 单独边长 */ }
- 接口隔离原则:客户端不应该被迫依赖它不使用的方法。即接口应该小而专。
例如一个多功能接口 Worker 包含 work(), eat(), sleep(),但机器人只能 work(),却被迫实现 eat() 和 sleep()。正确做法应该将接口进行拆分
- 依赖倒置原则:高层模块不应依赖低层模块,二者都应依赖抽象;抽象不应依赖细节,细节应依赖抽象。
java
// 抽象消息发送器
interface MessageSender {
void send(String message);
}
// 低层实现:邮件发送
class EmailSender implements MessageSender {
public void send(String message) { /* 发邮件 */ }
}
// 低层实现:短信发送
class SmsSender implements MessageSender {
public void send(String message) { /* 发短信 */ }
}
// 高层模块依赖抽象
class Notification {
private MessageSender sender;
public Notification(MessageSender sender) { // 构造注入
this.sender = sender;
}
public void notify(String msg) {
sender.send(msg);
}
}
// 使用时可以灵活切换具体实现
Notification noti = new Notification(new EmailSender());
- 最少知识原则 / 迪米特法则:一个对象应该对其他对象有最少的了解,只与直接的朋友通信(出现在成员变量、方法参数、方法返回值中的对象),不与陌生对象交互。
反例为 customer.getAddress().getCity().getName() 这种链式调用让 customer 知道了 Address 和 City 的内部结构。应该让 customer 直接提供 getCityName() 方法。
JAVA中的多态
JAVA中的多态表现形式有以下几种,可以划分为编译时多态和运行时多态,编译时的多态包括方法重载和泛型,运行时的多态包括方法重写(接口方式,抽象类和抽象方法都可以归纳为方法重写的一种),向上/向下转型是运行时多态的配套语法。
| 表现形式 | 绑定时机 | 理解 | 核心代码 |
|---|---|---|---|
| 方法重载 | 编译时多态 | 同一个类中,方法名相同但参数列表不同(类型、个数、顺序),编译时根据参数决定调用哪个方法 | int add(int a, int b) double add(double a, double b) |
| 泛型 | 编译时多态 | 参数化类型,在编译时进行类型检查和擦除,使同一份代码能处理多种类型,增强类型安全 | List list = new ArrayList<>(); Box<T> 泛型类 |
| 方法重写 | 运行时多态 | 子类重新定义父类中已有的方法(非静态、非 final),通过父类引用指向子类对象,运行时动态绑定实际调用的方法 | Animal a = new Dog(); a.sound(); // 调用 Dog 的 sound() |
| 接口方式 | 运行时多态 | 接口引用指向实现类对象,调用接口中声明的方法,实际执行的是实现类的具体逻辑 | List list = new ArrayList(); list.add("hello"); |
| 抽象类和抽象方法 | 运行时多态 | 抽象类定义抽象方法(没有方法体),子类必须实现这些抽象方法。父类引用指向子类对象时,调用的是子类实现的方法 | abstract class Animal { abstract void sound(); } Animal a = new Dog(); a.sound(); |
| 向上/向下转型 | 运行时多态的配套语法 | 向上转型将子类对象赋值给父类引用。向下转型将父类引用强制转回子类类型,调用子类特有方法 | Animal a = new Dog(); // 向上转型 Dog d = (Dog) a; // 向下转型 |
方法重载
在同一个类中,多个方法拥有相同的方法名,但参数列表不同(参数个数、类型或顺序不同)。编译器根据调用时传入的参数类型和数量,在编译阶段就确定要调用哪个方法,因此属于编译时多态
- 方法名必须相同
- 参数列表必须不同(类型、个数、顺序)
- 返回值类型、访问修饰符可以不同,但不能仅靠它们区分重载
- 可以发生在同一个类中,也可以发生在继承关系中(子类重载父类方法)
java
public class Calculator {
// 加法:两个整数
public int add(int a, int b) { return a + b;}
// 重载1:三个整数
public int add(int a, int b, int c) {return a + b + c;}
// 重载2:两个浮点数
public double add(double a, double b) {return a + b;}
// 重载3:整数 + 浮点数(参数顺序不同)
public double add(int a, double b) {return a + b;}
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(3, 5)); // 调用 add(int,int)
System.out.println(calc.add(3, 5, 7)); // 调用 add(int,int,int)
System.out.println(calc.add(3.0, 5.0)); // 调用 add(double,double)
System.out.println(calc.add(3, 5.0)); // 调用 add(int,double)
}
}
泛型
泛型属于编译时多态的一种特殊形式,即参数化多态。它通过类型参数使得类、接口、方法可以适用于多种类型,从而在编译时实现代码复用和类型安全。
详细参考:【JAVA基础面经】JAVA中的泛型
方法重写
类重新定义父类中已有的方法(非 private、非 final、非 static)。当通过父类引用调用该方法时,实际执行的是子类重写后的版本,这个过程在运行时动态决定,因此是运行时多态。
- 方法名、参数列表、返回值类型必须与父类方法一致(返回值可以是子类类型,即协变返回类型)。
- 访问修饰符不能比父类更严格(可以更宽松)
- 不能抛出比父类更宽泛的异常
java
class Animal {
public void sound() {System.out.println("动物发出声音");}
}
class Dog extends Animal {
@Override
public void sound() {System.out.println("汪汪");}
}
class Cat extends Animal {
@Override
public void sound() {System.out.println("喵喵");}
}
public class TestOverride {
public static void main(String[] args) {
Animal myAnimal;
myAnimal = new Dog();
myAnimal.sound(); // 输出:汪汪
myAnimal = new Cat();
myAnimal.sound(); // 输出:喵喵
}
}
抽象类和抽象方法
抽象类作为父类,定义了一组抽象方法(必须由子类重写)和具体方法(可被子类重写)。当使用抽象类引用指向具体子类对象时,调用抽象方法或重写的具体方法,都会执行子类的逻辑。
java
abstract class Animal {
abstract void sound(); // 抽象方法,强制子类重写
void sleep() { // 具体方法,子类可重写
System.out.println("睡大觉");
}
}
class Dog extends Animal {
void sound() { System.out.println("汪汪"); }
void sleep() { System.out.println("狗趴着睡"); }
}
public class Test {
public static void main(String[] args) {
Animal a = new Dog(); // 抽象类引用指向子类对象
a.sound(); // 输出:汪汪(动态绑定到 Dog.sound)
a.sleep(); // 输出:狗趴着睡(动态绑定到 Dog.sleep)
}
}
接口方式进行
接口定义了一组行为规范(抽象方法)。当使用接口引用指向实现类对象时,调用接口中声明的方法,会执行实现类中重写的方法。
java
interface Playable {
void play(); // 抽象方法
}
class Piano implements Playable {
public void play() { System.out.println("弹钢琴"); }
}
class Guitar implements Playable {
public void play() { System.out.println("弹吉他"); }
}
public class Test {
public static void main(String[] args) {
Playable p = new Piano(); // 接口引用指向实现类
p.play(); // 输出:弹钢琴(动态绑定)
}
}
向上转型和向下转型
向上转型和向下转型是 Java 多态使用过程中的类型转换操作,本身不是多态的实现方式,而是多态语法的一部分。
向上转型指将子类对象的引用赋给父类(或接口)类型的变量,通过该操作可以使用统一的父类类型操作不同的子类对象,从而写出通用的代码。
java
class Animal {
void eat() { System.out.println("吃"); }
}
class Dog extends Animal {
void bark() { System.out.println("叫"); }
@Override
void eat() { System.out.println("狗吃骨头"); }
}
public class Upcasting {
public static void main(String[] args) {
Animal animal = new Dog(); // 向上转型,自动完成
animal.eat(); // 多态调用,输出:狗吃骨头
// animal.bark(); // 编译错误,Animal 类型中没有 bark 方法
}
}
向下转型指将父类引用强制转换回子类类型,以便访问子类特有的成员。向上转型后,父类引用无法直接调用子类特有的方法,如果需要调用,就必须进行向下转型。
java
public class Downcasting {
public static void main(String[] args) {
Animal animal = new Dog(); // 向上转型
// 向下转型前进行类型检查
if (animal instanceof Dog) {
Dog dog = (Dog) animal; // 强制转换
dog.bark(); // 输出:叫
}
}
}