Java封装、继承与多态详解
Java 封装
封装的概念
在 Java 中,封装是一种将数据和操作数据的方法捆绑在一起,对外部隐藏对象内部细节的机制。简单来说,就是把对象的属性和方法包装起来,只对外提供一些必要的接口来访问和操作这些属性和方法。这种方式提高了代码的安全性和可维护性。
例如,我们有一个 Person
类,它包含了 name
、age
等属性。如果不进行封装,外部代码可以随意修改这些属性,可能会导致数据的不一致或不符合业务逻辑。通过封装,我们可以控制对这些属性的访问,只允许在符合业务规则的情况下进行修改。
访问修饰符
- private
private
是最严格的访问修饰符。被private
修饰的属性或方法只能在本类内部被访问。- 示例代码:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 提供公共的方法来获取和设置 name 属性
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// 提供公共的方法来获取和设置 age 属性
public int getAge() {
return age;
}
public void setAge(int age) {
if (age >= 0 && age <= 120) {
this.age = age;
} else {
System.out.println("年龄不合法");
}
}
}
- 在上述代码中,
name
和age
属性被声明为private
,外部代码不能直接访问。只能通过getName
、setName
、getAge
和setAge
这些公共方法来访问和修改属性。setAge
方法中还增加了对年龄合法性的检查,这体现了封装的优势,能够对数据进行有效的控制。
- default(默认,不写修饰符)
- 具有默认访问修饰符的属性或方法可以被同一个包内的其他类访问。如果一个类没有指定包名,它被认为是在默认包中。
- 示例代码:
package com.example.demo;
class Animal {
String species;
void showSpecies() {
System.out.println("物种是:" + species);
}
}
class Dog extends Animal {
public Dog() {
species = "狗";
}
}
- 在这个例子中,
Animal
类中的species
属性和showSpecies
方法都没有显式的访问修饰符,属于默认访问级别。Dog
类与Animal
类在同一个包com.example.demo
中,所以Dog
类可以访问Animal
类的species
属性并进行赋值。
- protected
protected
修饰的属性或方法可以被同一个包内的其他类以及不同包中的子类访问。- 示例代码:
package com.example.base;
public class Plant {
protected String name;
protected void grow() {
System.out.println(name + "正在生长");
}
}
package com.example.sub;
import com.example.base.Plant;
public class Flower extends Plant {
public Flower(String name) {
this.name = name;
}
@Override
public void grow() {
System.out.println(name + "作为花,正绚丽生长");
}
}
- 在上述代码中,
Plant
类在com.example.base
包中,Flower
类在com.example.sub
包中并且继承自Plant
类。Plant
类的name
属性和grow
方法被声明为protected
,Flower
类可以访问并使用它们。
- public
public
是最宽松的访问修饰符。被public
修饰的属性或方法可以被任何类访问,无论在哪个包中。- 示例代码:
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
Calculator
类中的add
方法是public
的,任何其他类只要能访问到Calculator
类的实例,就可以调用add
方法。
封装的优点
- 数据安全性
- 通过限制对属性的直接访问,只允许通过特定的方法进行修改,可以确保数据的完整性和一致性。例如在前面
Person
类的setAge
方法中,对年龄进行了合法性检查,防止非法数据的设置。
- 通过限制对属性的直接访问,只允许通过特定的方法进行修改,可以确保数据的完整性和一致性。例如在前面
- 可维护性
- 封装使得类的内部实现细节对外部隐藏。如果类的内部实现需要修改,只要保持对外接口不变,其他依赖该类的代码就不需要修改。比如,
Person
类中如果修改了name
属性的存储方式,只要getName
和setName
方法的接口不变,外部调用代码不受影响。
- 封装使得类的内部实现细节对外部隐藏。如果类的内部实现需要修改,只要保持对外接口不变,其他依赖该类的代码就不需要修改。比如,
- 代码复用性
- 封装良好的类可以在不同的项目或模块中复用。其他开发者只需要了解类提供的公共接口,而不需要关心内部实现细节,就可以轻松使用该类。例如,
Calculator
类的add
方法可以在各种需要进行加法运算的场景中复用。
- 封装良好的类可以在不同的项目或模块中复用。其他开发者只需要了解类提供的公共接口,而不需要关心内部实现细节,就可以轻松使用该类。例如,
Java 继承
继承的概念
继承是 Java 面向对象编程中的一个重要特性,它允许一个类(子类)从另一个类(父类)获取属性和方法。子类可以继承父类的非 private
属性和方法,并且可以根据自身需求进行扩展和重写。通过继承,代码的复用性得到了极大提高,同时也便于进行代码的维护和扩展。
例如,我们有一个 Vehicle
类作为父类,它包含了一些通用的属性和方法,如 speed
(速度)和 start
(启动)方法。Car
类和 Bicycle
类可以作为子类继承 Vehicle
类,它们继承了 speed
属性和 start
方法,同时还可以有自己特有的属性和方法,如 Car
类可以有 numOfDoors
(车门数量)属性,Bicycle
类可以有 numOfGears
(挡位数量)属性。
继承的语法
在 Java 中,使用 extends
关键字来实现继承。语法如下:
class ParentClass {
// 父类的属性和方法
}
class ChildClass extends ParentClass {
// 子类新增的属性和方法
}
例如:
class Animal {
String name;
void eat() {
System.out.println(name + "正在吃东西");
}
}
class Dog extends Animal {
String breed;
void bark() {
System.out.println(name + "正在汪汪叫,品种是" + breed);
}
}
在上述代码中,Dog
类继承自 Animal
类。Dog
类拥有 Animal
类的 name
属性和 eat
方法,同时又定义了自己的 breed
属性和 bark
方法。
构造方法与继承
- 子类构造方法调用父类构造方法
- 子类的构造方法在执行时,会默认先调用父类的无参构造方法。如果父类没有无参构造方法,子类构造方法必须显式调用父类的有参构造方法。
- 示例代码:
class Shape {
int side;
Shape(int side) {
this.side = side;
}
}
class Square extends Shape {
Square(int side) {
super(side);
}
void printInfo() {
System.out.println("正方形边长为:" + side);
}
}
- 在
Square
类的构造方法中,通过super(side)
显式调用了Shape
类的有参构造方法。如果不这样做,编译器会报错,因为Shape
类没有无参构造方法。
- super 关键字
super
关键字用于在子类中访问父类的属性和方法。除了在构造方法中调用父类构造方法外,还可以在子类方法中访问父类被重写的方法。- 示例代码:
class Animal {
void sound() {
System.out.println("动物发出声音");
}
}
class Dog extends Animal {
@Override
void sound() {
super.sound();
System.out.println("狗汪汪叫");
}
}
- 在
Dog
类的sound
方法中,通过super.sound()
调用了父类Animal
的sound
方法,然后再输出子类特有的声音描述。
方法重写
- 重写的概念
- 当子类继承父类后,子类可以根据自身需求重新实现父类的方法,这就是方法重写。重写的方法必须与父类被重写方法具有相同的方法名、参数列表和返回类型(返回类型可以是父类返回类型的子类,这称为协变返回类型)。
- 重写的规则
- 访问修饰符:子类重写方法的访问修饰符不能比父类被重写方法的访问修饰符更严格。例如,如果父类方法是
protected
,子类重写方法可以是protected
或public
,但不能是private
。 - 异常抛出:子类重写方法不能抛出比父类被重写方法更多的异常,或者抛出比父类被重写方法更宽泛的异常类型。例如,如果父类方法抛出
IOException
,子类重写方法可以抛出IOException
或者IOException
的子类异常,但不能抛出SQLException
等无关异常。
- 访问修饰符:子类重写方法的访问修饰符不能比父类被重写方法的访问修饰符更严格。例如,如果父类方法是
- 示例代码
class Shape {
double area() {
return 0.0;
}
}
class Circle extends Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
double area() {
return Math.PI * radius * radius;
}
}
- 在上述代码中,
Circle
类继承自Shape
类,并重写了area
方法。Circle
类根据圆的面积计算公式重新实现了area
方法,以满足自身需求。
继承的优缺点
- 优点
- 代码复用:子类可以复用父类的属性和方法,减少了重复代码的编写。例如多个具体的动物类继承自
Animal
类,复用了Animal
类的eat
方法。 - 便于扩展:在不修改父类代码的情况下,子类可以添加新的属性和方法,或者重写父类方法来实现新的功能。比如
Dog
类继承Animal
类后,添加了bark
方法。
- 代码复用:子类可以复用父类的属性和方法,减少了重复代码的编写。例如多个具体的动物类继承自
- 缺点
- 继承层次过深导致维护困难:如果继承层次过于复杂,一个父类的修改可能会影响到多个子类,增加了代码维护的难度。例如,如果修改了
Animal
类的某个方法签名,所有继承自Animal
类且重写了该方法的子类都需要相应修改。 - 打破封装性:虽然继承在一定程度上提高了代码复用,但也可能破坏父类的封装性。因为子类可以访问父类的非
private
属性和方法,如果子类随意修改父类的属性,可能会导致父类的内部状态不一致。
- 继承层次过深导致维护困难:如果继承层次过于复杂,一个父类的修改可能会影响到多个子类,增加了代码维护的难度。例如,如果修改了
Java 多态
多态的概念
多态是 Java 面向对象编程的核心特性之一,它允许同一个引用类型在不同的情况下表现出不同的行为。简单来说,就是“一个接口,多种实现”。多态通过继承和方法重写来实现,使得程序在运行时能够根据对象的实际类型来决定调用哪个方法。
例如,我们有一个 Animal
类,Dog
类和 Cat
类继承自 Animal
类,并且都重写了 Animal
类的 sound
方法。当我们使用 Animal
类型的引用指向 Dog
或 Cat
对象时,调用 sound
方法会根据对象的实际类型(Dog
或 Cat
)来执行相应的 sound
方法实现,这就是多态的体现。
多态的实现方式
- 基于继承的多态
- 当子类继承父类并重写父类的方法时,就可以实现多态。通过父类类型的引用指向子类对象,调用重写的方法时会执行子类的实现。
- 示例代码:
class Animal {
void sound() {
System.out.println("动物发出声音");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("狗汪汪叫");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("猫喵喵叫");
}
}
public class PolymorphismDemo {
public static void main(String[] args) {
Animal animal1 = new Dog();
Animal animal2 = new Cat();
animal1.sound();
animal2.sound();
}
}
- 在
PolymorphismDemo
类的main
方法中,animal1
是Animal
类型的引用,但指向了Dog
对象,animal2
是Animal
类型的引用,但指向了Cat
对象。当调用sound
方法时,会根据对象的实际类型分别执行Dog
类和Cat
类的sound
方法,输出“狗汪汪叫”和“猫喵喵叫”。
- 基于接口的多态
- 一个类实现一个或多个接口,并实现接口中定义的方法。通过接口类型的引用指向实现该接口的类的对象,调用接口方法时会执行具体实现类的方法。
- 示例代码:
interface Shape {
double area();
}
class Circle implements Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle implements Shape {
double width;
double height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
public class InterfacePolymorphismDemo {
public static void main(String[] args) {
Shape shape1 = new Circle(5);
Shape shape2 = new Rectangle(4, 6);
System.out.println("圆形面积:" + shape1.area());
System.out.println("矩形面积:" + shape2.area());
}
}
- 在
InterfacePolymorphismDemo
类中,Shape
是一个接口,Circle
和Rectangle
类实现了Shape
接口。shape1
和shape2
是Shape
类型的引用,分别指向Circle
和Rectangle
对象。调用area
方法时,会根据对象的实际类型执行相应的area
方法实现,输出圆形和矩形的面积。
编译时多态与运行时多态
- 编译时多态(方法重载)
- 编译时多态是通过方法重载实现的。方法重载是指在同一个类中,多个方法具有相同的方法名,但参数列表不同(参数个数、参数类型或参数顺序不同)。编译器在编译时根据调用方法的参数列表来决定调用哪个方法。
- 示例代码:
class Calculator {
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
}
- 在
Calculator
类中,有三个add
方法,它们的参数列表不同。当调用add
方法时,编译器会根据传入的参数类型和个数来确定调用哪个add
方法。例如,Calculator cal = new Calculator(); int result1 = cal.add(2, 3);
会调用第一个add
方法,而double result2 = cal.add(2.5, 3.5);
会调用第二个add
方法。
- 运行时多态(方法重写)
- 运行时多态是通过方法重写和对象的动态绑定实现的。在运行时,Java 虚拟机(JVM)根据对象的实际类型来决定调用哪个重写的方法。这是基于继承和向上转型的机制。
- 例如前面基于继承的多态示例中,
Animal
类型的引用在运行时根据实际指向的Dog
或Cat
对象来调用相应的sound
方法,这就是运行时多态的体现。
多态的优点
- 提高代码的可维护性和可扩展性:当需要添加新的子类时,只需要让新子类继承父类或实现接口,并实现相应的方法,而不需要修改其他已有的代码。例如,如果要添加一个
Bird
类继承自Animal
类,只需要在Bird
类中重写sound
方法,其他使用Animal
类多态特性的代码不需要修改。 - 增强代码的灵活性:多态使得程序可以根据对象的实际类型来执行不同的行为,这在很多复杂的业务场景中非常有用。比如在图形绘制程序中,通过
Shape
接口和不同的实现类(如Circle
、Rectangle
等),可以方便地实现不同图形的绘制逻辑,并且可以很容易地扩展新的图形类型。 - 提高代码的复用性:父类或接口中定义的通用方法可以被多个子类复用,同时子类又可以根据自身需求重写这些方法,实现个性化的行为。例如,
Animal
类的eat
方法可以被各种动物子类复用,而不同动物子类可以重写eat
方法来表现出不同的进食行为。