从0学Java--day7
本期是封装继承多态的一些介绍
Java封装、继承、多态详解
一、封装详解
封装是Java面向对象编程的基石,它把数据(属性)和操作数据的方法捆绑在一起,并隐藏内部细节,只暴露必要的接口。
知识点
- 核心思想:把对象的属性设为私有(private),通过公共(public)方法(getter和setter)来访问和修改属性。这就像把贵重物品锁在保险箱里(private字段),只给你钥匙(public方法)来操作。
- 访问修饰符:
private:只能在当前类内部访问,外部无法直接看到或修改。public:任何地方都能访问。protected:同一包内或子类可以访问(但本讲解以基础为主,避免混淆)。
- Getter和Setter方法:Getter用于读取属性值,Setter用于设置属性值,并可以在方法中添加验证逻辑。
- 好处:提高安全性(防止非法修改)、增强可维护性(内部修改不影响外部代码)、简化使用(用户只需调用方法,无需知道细节)。
易错点
- 忘记使用private:直接声明属性为public,导致外部代码随意修改,破坏封装性。
- Getter/Setter方法缺失或错误:不提供getter/setter,或方法名不规范(如写成
getAge()但属性是age),导致编译错误或运行时问题。 - Setter中无验证:在setter方法中不检查输入值(如年龄不能为负数),可能存入无效数据。
- 直接访问字段:在类内部方法中,错误地直接使用字段名而不通过getter/setter,这虽然能运行,但破坏了封装原则。
常见坑
- 字段暴露风险:如果属性是public,外部代码可以随意修改,比如把年龄设为-10,程序不会报错但逻辑错误。
- 方法签名错误:getter/setter方法名必须匹配属性名(如属性
name的getter必须是getName()),否则IDE可能不识别,导致使用困难。 - 忽略默认值:未在构造器或setter中初始化属性,可能导致null或0值引发异常。
- 过度封装:把简单属性也封装得过于复杂,增加代码冗余,降低可读性。
二、继承详解
继承允许一个类(子类)基于另一个类(父类)创建,子类自动获得父类的属性和方法,并可以添加或修改功能。
知识点
- 核心思想:使用
extends关键字建立继承关系(如class Dog extends Animal)。子类继承父类的非private成员(属性和方法),就像孩子继承父母的基因。 - 方法覆盖:子类可以重新定义父类的方法(使用相同方法名和参数),实现特定行为。覆盖时建议添加
@Override注解来避免错误。 - super关键字:用于在子类中访问父类的成员,如
super.methodName()调用父类方法,或super()调用父类构造器。 - 构造器链:子类构造器默认调用父类无参构造器(
super())。如果父类没有无参构造器,必须在子类构造器中显式调用super(参数)。 - 单继承限制:Java只支持单继承(一个子类只能有一个父类),不能多继承(如
class A extends B, C是错误的)。 - 继承层次:所有类都隐式继承
Object类(Java根类),提供toString()、equals()等方法。
易错点
- 忘记super调用:在子类构造器中未显式调用
super(),如果父类没有无参构造器,编译报错(说白了就是可以直接跳过子类的直接使用父类)。 - 方法覆盖失败:子类方法签名(方法名和参数)必须与父类完全一致,否则不是覆盖而是新方法(容易混淆)。
- 字段隐藏:如果子类声明了与父类同名的字段,父类字段会被“隐藏”(不是覆盖),访问时需用
super.fieldName。 - final类或方法:父类方法或类声明为
final时,子类不能覆盖或继承。 - 访问权限错误:子类不能访问父类的private成员,需通过public/protected方法。
常见坑
- 构造器顺序问题:子类构造器必须先调用父类构造器(
super()必须是第一行),否则编译错误。 - 覆盖与重载混淆:覆盖是相同签名的方法在子类重写,重载是相同方法名但不同参数。新手容易误用。
- 静态方法陷阱:静态方法不能被覆盖(只属于类,不属于对象),子类定义同名静态方法是“隐藏”而非覆盖。
- 无限递归:在覆盖方法中调用
super.method()时,如果方法名写错,可能调用自身导致栈溢出。 - 继承滥用:不适当地使用继承(如只为了重用代码),导致类层次复杂,难以维护。
三、多态详解
多态允许同一方法在不同对象中有不同实现。在Java中,主要通过方法覆盖和父类引用指向子类对象实现。
知识点
- 核心思想:父类引用可以指向子类对象(如
Animal myPet = new Dog()),调用方法时执行子类版本(运行时绑定)。这就像遥控器(父类引用)可以控制不同电器(子类对象),但按钮功能不同。 - 运行时多态:方法调用在运行时(非编译时)决定具体执行哪个类的方法,基于对象实际类型。
- 方法覆盖要求:多态依赖方法覆盖,子类方法必须与父类方法签名一致。
- 向上转型:将子类对象赋给父类引用(如
Animal a = new Dog()),这是自动的,安全但丢失子类特有方法。 - 向下转型:将父类引用转回子类类型(如
Dog d = (Dog) a),需要显式强制转换,如果类型不匹配会抛出ClassCastException。 - 动态绑定:Java在运行时根据对象类型绑定方法,确保调用正确的覆盖方法。
易错点
- 签名不一致:子类覆盖方法时参数或返回类型不同,导致不是覆盖,多态失效。
- 忘记@Override:不添加
@Override注解,可能因拼写错误误创建新方法而非覆盖。 - 静态方法多态失效:静态方法是编译时绑定,不能被覆盖,多态不适用。
- private方法问题:private方法不能被覆盖,子类定义同名方法是新方法,不参与多态。
- 字段多态无效:字段访问是编译时绑定(基于引用类型),不是运行时多态。
常见坑
- 类型转换异常:向下转型时,如果父类引用实际指向其他子类对象(如
Animal a = new Cat(); Dog d = (Dog) a),会抛出ClassCastException。 - 误用父类引用:向上转型后,只能调用父类定义的方法,不能直接调用子类特有方法(需向下转型)。
- 覆盖方法访问权限:子类覆盖方法不能比父类方法更严格(如父类方法是public,子类不能改为private)。
- 多态与构造器:构造器中调用可覆盖方法,可能因多态导致未初始化子类字段(危险,避免在构造器调用覆盖方法)。
- 性能误解:多态增加运行时开销(动态绑定),但现代JVM优化后影响小,新手可能过度担心。
四、5个基础实例详解
以下是5个基础实例,每个都严格使用封装、继承、多态之前的知识点(只涉及类、对象、方法、变量、访问修饰符、继承和方法覆盖)。每个实例包括代码、详细分析(执行过程、易错点避免、通俗解释),确保超级详细和通俗易懂。
实例1:纯封装演示(重点:封装基础)
场景:创建一个Person类,封装姓名和年龄,防止非法值。
public class Person {// 私有字段,外部无法直接访问private String name;private int age;// 构造器初始化public Person(String name, int age) {this.name = name;setAge(age); // 通过setter设置,添加验证}// Getter方法:读取namepublic String getName() {return name;}// Setter方法:设置age,添加验证public void setAge(int age) {if (age >= 0) { // 验证年龄不能为负this.age = age;} else {System.out.println("年龄不能为负数,设置为0");this.age = 0;}}// Getter方法:读取agepublic int getAge() {return age;}
}// 测试类
public class Main {public static void main(String[] args) {Person person = new Person("张三", 25);System.out.println("姓名:" + person.getName() + ", 年龄:" + person.getAge());person.setAge(-5); // 尝试设置非法年龄System.out.println("更新后年龄:" + person.getAge()); // 输出应为0}
}
详细分析:
- 代码解释:
Person类将name和age声明为private,外部只能通过getName()、getAge()和setAge()访问。构造器中调用setAge()确保初始化验证。测试代码创建对象并尝试设置非法年龄。 - 执行过程:运行
main方法时,先创建Person对象(年龄25),输出正确。然后调用setAge(-5),由于验证逻辑,年龄被设为0,输出"更新后年龄:0"。 - 易错点避免:
- 避免直接访问字段:如
person.age = -5会报错(编译错误),因为age是private。 - Setter验证:在
setAge()中添加if检查,防止负值,确保数据安全。 - Getter/Setter命名:方法名
getAge()匹配字段age,避免拼写错误。
- 避免直接访问字段:如
- 通俗比喻:这就像银行账户(
Person对象),余额(age)被锁在保险箱(private),你只能通过ATM机(public方法)存取,ATM会检查你输入的金额是否合法。 - 常见坑:如果不加验证,
setAge(-5)会让年龄为负,程序逻辑错误。这里通过setter防御,避免此坑。
实例2:纯继承演示(重点:继承基础)
场景:创建一个Animal父类和Dog子类,子类继承并覆盖父类方法。
// 父类
public class Animal {private String name;public Animal(String name) {this.name = name;}public void eat() {System.out.println(name + "正在吃东西");}public String getName() {return name;}
}// 子类继承Animal
public class Dog extends Animal {public Dog(String name) {super(name); // 调用父类构造器}// 覆盖父类eat方法@Overridepublic void eat() {System.out.println(getName() + "(狗)正在啃骨头");}// 子类特有方法public void bark() {System.out.println("汪汪叫!");}
}// 测试类
public class Main {public static void main(String[] args) {Dog myDog = new Dog("小黑");myDog.eat(); // 调用覆盖后的方法myDog.bark(); // 调用子类特有方法}
}
详细分析:
- 代码解释:
Animal是父类,有name字段和eat()方法。Dog子类用extends Animal继承,覆盖eat()方法(添加@Override),并添加特有方法bark()。测试代码创建Dog对象并调用方法。 - 执行过程:运行
main方法,创建Dog对象时,先调用父类构造器(super(name))初始化name。然后myDog.eat()执行子类覆盖的版本,输出"小黑(狗)正在啃骨头"。myDog.bark()输出"汪汪叫!"。 - 易错点避免:
- super调用:子类构造器中
super(name)显式调用父类构造器,避免父类无默认构造器导致的编译错误。 - 方法覆盖:
@Override注解确保签名正确,如果拼写错误(如eats()),编译器会警告。 - 字段访问:子类通过
getName()访问父类private字段,不能直接访问name。
- super调用:子类构造器中
- 通俗比喻:动物(父类)有吃饭行为,狗(子类)继承了吃饭,但改成啃骨头(覆盖方法),狗还有自己的叫声(特有方法)。
实例3:纯多态演示(重点:多态基础)
场景:使用Animal和Dog类,通过父类引用指向子类对象,展示多态。
// 父类(同实例2)
public class Animal {public void eat() {System.out.println("动物吃东西");}
}// 子类(同实例2)
public class Dog extends Animal {@Overridepublic void eat() {System.out.println("狗啃骨头");}
}// 测试类
public class Main {public static void main(String[] args) {Animal myAnimal = new Dog(); // 向上转型:父类引用指向子类对象myAnimal.eat(); // 多态:运行时调用Dog的eat方法// 向下转型示例if (myAnimal instanceof Dog) {Dog myDog = (Dog) myAnimal; // 强制向下转型myDog.eat(); // 安全调用}}
}
详细分析:
- 代码解释:
Animal父类和Dog子类(覆盖eat())。测试代码中,Animal myAnimal = new Dog()实现向上转型(父类引用指向子类对象)。调用myAnimal.eat()时,执行子类方法。然后演示向下转型((Dog) myAnimal),使用instanceof检查避免异常。 - 执行过程:运行
main方法,myAnimal.eat()输出"狗啃骨头",因为运行时绑定到Dog类的方法。向下转型后,myDog.eat()同样输出"狗啃骨头"。 - 易错点避免:
- 运行时绑定:
myAnimal.eat()在运行时确定对象是Dog,调用子类方法,避免编译时误解。 - 安全向下转型:先用
instanceof检查类型,再强制转换,防止ClassCastException。 - 方法覆盖:确保
Dog.eat()签名与父类一致,否则多态失效。
- 运行时绑定:
- 通俗比喻:把狗(子类对象)当作动物(父类引用)来操作,让它“吃饭”时,它实际执行狗的吃饭方式(多态)。这就像用通用遥控器(父类引用)控制电视(子类对象),但按钮功能不同。
- 常见坑:如果不检查直接向下转型(如
Dog d = (Dog) new Animal()),会抛出异常。这里用instanceof避免此坑
实例4:封装和继承结合演示(重点:封装在继承中的应用)
场景:创建一个Vehicle父类封装速度,Car子类继承并添加功能。
// 父类:封装速度
public class Vehicle {private int speed; // 私有字段public Vehicle(int speed) {setSpeed(speed); // 通过setter初始化}// Setter添加验证public void setSpeed(int speed) {if (speed >= 0) {this.speed = speed;} else {this.speed = 0;}}// Getterpublic int getSpeed() {return speed;}public void move() {System.out.println("交通工具移动,速度:" + speed);}
}// 子类继承Vehicle
public class Car extends Vehicle {public Car(int speed) {super(speed); // 调用父类构造器}// 覆盖父类方法@Overridepublic void move() {System.out.println("汽车行驶,速度:" + getSpeed()); // 通过getter访问父类私有字段}
}// 测试类
public class Main {public static void main(String[] args) {Car myCar = new Car(60);myCar.move(); // 调用覆盖方法myCar.setSpeed(-10); // 通过继承的setter设置速度System.out.println("当前速度:" + myCar.getSpeed()); // 输出应为0}
}
详细分析:
- 代码解释:
Vehicle父类封装speed(private),提供验证的setter。Car子类继承,覆盖move()方法。测试代码创建Car对象,调用覆盖方法和setter。 - 执行过程:运行
main方法,myCar.move()输出"汽车行驶,速度:60"。然后myCar.setSpeed(-10)触发验证,速度被设为0,输出"当前速度:0"。 - 易错点避免:
- 封装继承整合:子类通过
getSpeed()访问父类private字段,不能直接super.speed(编译错误)。 - Setter重用:子类继承父类的setter,直接使用
myCar.setSpeed(),无需重写。 - 覆盖方法:
@Override确保正确覆盖,避免新方法创建。
- 封装继承整合:子类通过
- 通俗比喻:交通工具(父类)有速度表(private字段),汽车(子类)继承了它,但用自己的方式显示速度(覆盖方法)。速度表只能通过特定按钮(setter)修改。
- 常见坑:如果父类字段不是private,子类可能直接修改导致错误。这里封装确保数据安全。
实例5:多态结合演示(重点:多态在集合中的应用)
场景:使用Animal父类和多个子类(Dog, Cat),通过数组展示多态。
// 父类
public class Animal {public void makeSound() {System.out.println("动物发出声音");}
}// 子类1
public class Dog extends Animal {@Overridepublic void makeSound() {System.out.println("狗:汪汪");}
}// 子类2
public class Cat extends Animal {@Overridepublic void makeSound() {System.out.println("猫:喵喵");}
}// 测试类
public class Main {public static void main(String[] args) {Animal[] animals = new Animal[2]; // 父类数组animals[0] = new Dog(); // 向上转型animals[1] = new Cat(); // 向上转型for (Animal animal : animals) {animal.makeSound(); // 多态:运行时调用不同子类方法}}
}
详细分析:
- 代码解释:
Animal父类有makeSound()方法。Dog和Cat子类覆盖它。测试代码创建父类类型数组,存储不同子类对象(向上转型),遍历调用makeSound()展示多态。 - 执行过程:运行
main方法,数组animals包含Dog和Cat对象。循环中,animal.makeSound()根据实际对象类型输出"狗:汪汪"和"猫:喵喵"。 - 易错点避免:
- 数组多态:父类数组存储子类对象是安全的,多态确保正确方法调用。
- 覆盖一致性:所有子类方法签名相同,避免编译错误。
- 无向下转型:这里不需要向下转型,因为只调用父类定义的方法。
- 通俗比喻:动物园笼子(父类数组)里关着狗和猫(子类对象),管理员让每个动物“叫”时(调用方法),狗汪汪叫,猫喵喵叫(多态)。
- 常见坑:如果子类未覆盖方法(如
Cat不写makeSound()),会调用父类默认输出。这里确保所有子类覆盖,避免此坑。
总结
- 封装:保护数据,通过private字段和public方法控制访问。易错在忘记private或setter验证。
- 继承:子类复用父类代码,可覆盖方法。易错在super调用或方法签名。
- 多态:同一方法不同实现,依赖覆盖和向上转型。易错在类型转换或静态方法。
以上是一些基础概念的介绍,真正掌握还需要多在实际敲代码中练习
