目录
2-1-2 继承 (extends)
展示了 继承(extends)的基本用法。继承是面向对象编程中的一个重要概念,它使得子类能够继承父类的属性和方法,从而实现代码复用和扩展。
关键概念
- 继承:
- 子类可以继承父类的属性和方法,使用
extends关键字来定义继承关系。 - 子类可以访问父类的 public 和 protected 成员,但不能直接访问父类的 private 成员。
- 子类可以继承父类的属性和方法,使用
- 父类(Super Class):
- 在这个例子中,
Animal类是父类,它包含了name、variety、age、food等属性以及eat、sleep等方法。
- 在这个例子中,
- 子类(Sub Class):
Dogs和Cats类是子类,它们通过extends关键字继承了Animal类的所有公共和保护成员。这样,子类就可以复用父类的代码,并且可以根据需要扩展或覆盖父类的方法。
代码分析
1. Animal 类(父类)
@Getter
@Setter
@ToString
public class Animal {
private String name;
private String variety;
private int age;
private String food;
private static String plot = "NanG"; // 静态变量,所有实例共享
public static String getPlot() {
return plot;
}
public Animal() { }
public Animal(String name, String variety, int age, String food) {
this.name = name;
this.variety = variety;
this.age = age;
this.food = food;
}
public static void goplot() {
System.out.println("所有狗都进了小区");
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
if (age < 0 || age > 100) {
this.age = 0;
} else {
this.age = age;
}
}
public void eat() {
System.out.println(this.name + " 正在吃饭");
}
public void sleep() {
System.out.println(this.name + " 正在睡觉");
}
}
- 构造方法:
Animal类有无参构造方法和有参构造方法,允许你创建Animal类的对象并设置其属性。
- 静态变量和静态方法:
plot是一个静态变量,所有Animal类的实例共享这个值。goplot是一个静态方法,它输出所有狗都进了小区的消息。
- 普通方法:
eat和sleep是实例方法,可以在实例对象上调用,打印出狗的行为。
2. Dogs 和 Cats 类(子类)
// Dogs 子类
public class Dogs extends Animal {
// 继承 Animal 类的属性和方法
}
// Cats 子类
public class Cats extends Animal {
// 继承 Animal 类的属性和方法
}
-
继承
Animal类:
Dogs和Cats都继承了Animal类,因此它们自动获得了Animal类的所有属性(如name、age、food)和方法(如eat、sleep)。
-
无新功能
:
- 在这个例子中,
Dogs和Cats类没有定义新的属性或方法,它们完全继承了Animal类的行为。
- 在这个例子中,
3. Application 类(测试)
import com.nix.demo.Cats;
import com.nix.demo.Dogs;
public class Application {
public static void main(String[] args) {
// 创建 Dog 类实例
Dogs zhangDog = new Dogs();
zhangDog.setName("Jerry");
zhangDog.setAge(2);
System.out.println("zhangDog = " + zhangDog);
// 创建 Cat 类实例
Cats liCat = new Cats();
liCat.setName("Tom");
liCat.setAge(1);
System.out.println("liCat = " + liCat);
// 调用继承自 Animal 类的方法
zhangDog.eat();
liCat.sleep();
// 调用静态方法 goplot
Cats.goplot();
// 访问静态变量 plot
System.out.println("Cat.getPlot() = " + Cats.getPlot());
}
}
- 创建子类对象:
- 创建了
Dogs类和Cats类的实例,并设置它们的属性,如name和age。
- 创建了
- 继承方法的调用:
zhangDog.eat()和liCat.sleep()调用了从Animal类继承来的eat和sleep方法。
- 调用静态方法和访问静态变量:
Cats.goplot()调用了Animal类中的静态方法goplot()。Cats.getPlot()输出静态变量plot的值。
输出结果
zhangDog = Dogs(name=Jerry, variety=null, age=2, food=null)
liCat = Cats(name=Tom, variety=null, age=1, food=null)
Jerry 正在吃饭
Tom 正在睡觉
所有狗都进了小区
Cat.getPlot() = NanG
总结
- 继承:
Dogs和Cats继承了Animal类,复用了Animal类的属性和方法。 - 静态变量和方法:
plot和goplot()是静态的,所有Animal类及其子类的实例共享这些静态成员。 - 方法重用:子类无需重新实现
Animal类中的eat和sleep方法,直接通过继承使用。 - 静态方法和变量访问:即使子类
Cats继承自Animal,它也可以直接访问静态方法和变量。
2-2-2 饿狼传说之多层继承
提到的概念是 多层继承,以及 Java 不支持多重继承 的特性。在 Java 中,一个类只能继承一个父类,但它可以通过 多层继承 的方式间接继承多个类的属性和方法。
1. 单继承 vs 多继承
- 单继承:Java 是一个单继承的语言,即一个子类只能有一个直接父类。你不能写类似于
class A extends B, C这样的代码。 - 多层继承:虽然 Java 不支持多重继承,但可以通过多层继承来实现类似的功能。这意味着一个类可以继承另一个类,而这个父类本身又继承自另一个类,从而间接获得多个父类的属性和方法。
2. 多层继承的示例
在你的代码中,Dogs 类继承自 Animal 类,而 Animal 类又是你的父类。因此,Dogs 类通过继承 Animal 间接继承了 Animal 的所有成员。
示例解析
1. Animal 类(父类)
public class Animal {
private String name;
private String variety;
private int age;
private String food;
public void eat() {
System.out.println(this.name + " 正在吃饭");
}
public void sleep() {
System.out.println(this.name + " 正在睡觉");
}
// 其他成员
}
2. Dogs 类(子类)
public class Dogs extends Animal {
// Dogs 类继承了 Animal 类的所有成员,包括 eat() 和 sleep()
}
在这个例子中,Dogs 类直接继承了 Animal 类,因此 Dogs 类将会拥有 Animal 类中定义的属性和方法,像是 name、age 以及 eat() 和 sleep() 方法。
3. Cats 类(另一个子类)
public class Cats extends Animal {
// 同样继承了 Animal 类的所有成员
}
这里 Cats 类也继承了 Animal 类,因此它也具备了 Animal 类中的所有成员。
3. 多层继承的效果
public class Application {
public static void main(String[] args) {
// Dogs 类对象
Dogs zhangDog = new Dogs();
zhangDog.eat(); // 继承自 Animal 类
// Cats 类对象
Cats liCat = new Cats();
liCat.sleep(); // 继承自 Animal 类
}
}
4. 为什么 Java 不支持多重继承?
-
复杂性:多重继承会导致继承链复杂,如果父类之间有相同的方法或者属性,可能会导致二义性问题(即无法确定使用哪个父类的方法)。
-
钻石问题(Diamond Problem):如果类
C继承了类A和类B,而A和B都有相同的方法,类C无法明确选择调用A还是B中的那个方法。举个例子:
class A { void speak() { System.out.println("A speaks"); } } class B extends A { void speak() { System.out.println("B speaks"); } } class C extends A, B { // 不能直接写多继承 // 这里就会出现二义性,C 不知道该调用 A 还是 B 的 speak 方法 }
5. 如何解决多继承问题?
Java 通过 接口(interface)来解决多继承的问题。一个类可以实现多个接口,这样可以在不引入多重继承复杂性的情况下,获取多个不同类的功能。
通过接口实现多继承
interface CanFly {
void fly();
}
interface CanSwim {
void swim();
}
class Duck implements CanFly, CanSwim {
public void fly() {
System.out.println("Duck is flying");
}
public void swim() {
System.out.println("Duck is swimming");
}
}
在这个例子中,Duck 类同时实现了 CanFly 和 CanSwim 接口,获得了两个接口的功能。
总结
- 单继承:Java 只允许一个类继承另一个类,但通过多层继承,子类可以间接继承多个类的功能。
- 多继承问题:Java 不支持类的多继承,避免了复杂性和钻石问题。
- 接口:如果需要多继承的功能,可以通过实现多个接口来实现。接口为类提供了多种行为的能力。
2-2-3 方式的重写与 2-2-4 super啃老
现在正在介绍的是 方法重写(Override)和 super关键字。这些是继承中的重要概念。
1. 方法重写(Method Overriding)
方法重写是子类对父类方法的重新实现。当子类需要改变或增强父类的方法功能时,可以通过方法重写来实现。方法重写有以下特点:
- 子类的方法名、参数列表和返回类型必须和父类被重写的方法完全相同。
- 重写方法时,子类可以提供不同的实现。
在你的例子中,Dogs 类和 Cats 类都重写了父类 Animal 中的 barking 方法。
@Override
public void barking() {
System.out.println("汪汪汪~~~");
}
Dogs类中的barking方法重写了父类Animal中的barking方法,打印的是 “汪汪汪~~~”。Cats类中的barking方法重写了父类Animal中的barking方法,打印的是 “喵喵喵~~~”。
这样,通过重写,子类可以根据需要改变父类的方法行为。
2. super 关键字(啃老)
super 关键字用于访问父类的成员(属性、方法)或调用父类的构造方法。在子类中,可以通过 super 来调用父类被重写的方法。如果你在子类的重写方法中调用 super,就会执行父类的原始方法,而不是子类的重写版本。
示例:
@Override
public void barking() {
super.barking(); // 调用父类的 barking 方法
System.out.println("汪汪汪~~~"); // 然后再执行子类的 barking 方法
}
- 如果不调用
super.barking(),那么Dogs和Cats类中的barking方法会完全替代父类Animal的实现。 - 如果调用
super.barking(),则在子类方法中先执行父类的barking方法,然后再执行子类的实现。
在你的 Dogs 和 Cats 类中,barking 方法如果注释掉了 super.barking(),则不调用父类的 barking 方法。否则,会先调用父类的 barking() 方法。
3. 代码分析
你在代码中的重写示例如下:
// Dogs 类重写了 barking 方法
@Override
public void barking() {
// 如果注释掉 super.barking(),则不会调用父类的方法
// super.barking();
System.out.println("汪汪汪~~~");
}
// Cats 类重写了 barking 方法
@Override
public void barking() {
// 如果注释掉 super.barking(),则不会调用父类的方法
// super.barking();
System.out.println("喵喵喵~~~");
}
4. 测试代码(Application 类)
public class Application {
public static void main(String[] args) {
// 创建 Dogs 对象
Dogs zhangDog = new Dogs();
zhangDog.setName("Jerry");
zhangDog.setAge(1);
zhangDog.setFood("狗粮");
// 创建 Cats 对象
Cats liCat = new Cats();
liCat.setName("Tom");
liCat.setAge(3);
liCat.setFood("猫粮");
// 调用 eat 和 sleep 方法
zhangDog.eat();
liCat.sleep();
// 调用 barking 方法
zhangDog.barking(); // 输出: 汪汪汪~~~
liCat.barking(); // 输出: 喵喵喵~~~
}
}
- Dogs 类 会输出 “汪汪汪~~~”,这是重写的
barking方法。 - Cats 类 会输出 “喵喵喵~~~”,这是重写的
barking方法。
5. 总结
- 方法重写:子类可以重写父类的方法,以实现不同的行为。
super关键字:super用于调用父类的方法或构造函数,常常用于在子类中调用父类的行为,或在重写方法时使用。- 重写的应用:通过重写,可以根据实际需要在子类中定义不同的实现,改变父类的默认行为。
2-2-5 启发性构造方法与继承
在 Java 中,继承是面向对象编程的一个重要特性,但继承的同时有一些关键点需要注意,比如父类构造函数的继承和调用。Java 是 单继承 的语言,子类不能直接继承父类的构造函数,但子类可以通过 super 关键字调用父类的构造函数。
1. 父类的构造方法
在代码中,Animal 类提供了多个构造函数(重载的构造函数)。这些构造函数允许初始化动物的不同属性。
public Animal(String name, String variety, int age, String food) {
this.name = name;
this.variety = variety;
this.age = age;
this.food = food;
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
Animal类提供了两个构造函数,一个用于初始化所有属性,另一个只需要name和age。
2. 子类调用父类的构造方法
在你的 Dogs 类中,子类调用了父类的构造函数:
public Dogs(String name, String variety, int age, String food) {
super(name, variety, age, food); // 调用父类构造函数
}
public Dogs(String name, int age) {
super(name, age); // 调用父类构造函数
}
Dogs类使用super关键字调用父类Animal的构造方法。通过super(),子类可以初始化父类的属性。- 如果你不显式调用父类构造方法,Java 编译器会隐式地调用父类的无参构造函数(
super()),这就是为什么你在Dogs类中可以有一个无参构造函数(public Dogs())。
3. 子类重写父类的方法
你重写了 barking 方法,子类 Dogs 和 Cats 都有不同的实现。
@Override
public void barking() {
System.out.println("汪汪汪~~~"); // Dogs 类中重写 barking 方法
}
@Override
public void barking() {
super.barking(); // 调用父类的 barking 方法
System.out.println("喵喵喵~~~"); // Cats 类中重写 barking 方法
}
- 在
Dogs类中,barking方法完全重写了父类的方法,输出 “汪汪汪~~~”。 - 在
Cats类中,barking方法调用了父类Animal的barking方法(通过super.barking()),然后输出 “喵喵喵~~~”。
4. 总结 - 启发性构造方法与继承
- 父类构造方法:子类不能继承父类的构造方法,但可以通过
super调用父类的构造方法。这是为了确保父类的初始化可以在子类中进行。 - 构造函数重载:通过多个构造函数,子类可以根据需要调用父类不同的构造方法。
- 方法重写:子类可以重写父类的方法,提供自己特有的实现,或者通过
super调用父类的方法并扩展它。
5. 改进建议
你的代码已经实现了继承和方法重写的核心概念。为了增强代码的可读性和维护性,建议考虑以下几点:
- 确保每个类的职责单一。例如,可以将
barking方法抽象到一个更适合的名字,避免使用过于通用的名称。 - 如果有多个构造函数,确保每个构造函数都有明确的用途并加以注释,帮助其他开发者理解为何需要这些构造函数。
完整代码示例
package com.nix.demo;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class Animal {
private String name;
private String variety;
private int age;
private String food;
// 默认构造函数
public Animal() {}
// 参数化构造函数
public Animal(String name, String variety, int age, String food) {
this.name = name;
this.variety = variety;
this.age = age;
this.food = food;
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(this.name + " 正在吃饭..正在吃:" + this.food);
}
public void sleep() {
System.out.println(this.name + " 正在睡觉");
}
public void barking() {
System.out.println("我是你爹 父类");
}
}
public class Dogs extends Animal {
// 默认构造函数
public Dogs() {}
public Dogs(String name, String variety, int age, String food) {
super(name, variety, age, food);
}
public Dogs(String name, int age) {
super(name, age);
}
@Override
public void barking() {
System.out.println("汪汪汪~~~");
}
}
public class Cats extends Animal {
@Override
public void barking() {
super.barking(); // 调用父类的 barking 方法
System.out.println("喵喵喵~~~");
}
}
import com.nix.demo.Animal;
import com.nix.demo.Cats;
import com.nix.demo.Dogs;
public class Application {
public static void main(String[] args) {
Dogs zhangDog = new Dogs("Jerry", "null", 1, "狗粮");
Cats liCat = new Cats();
liCat.setName("Tom");
liCat.setAge(3);
liCat.setFood("猫粮");
zhangDog.eat();
liCat.sleep();
zhangDog.barking();
liCat.barking();
}
}
在这个完整的例子中:
Dogs类和Cats类都继承自Animal类,并重写了barking方法。- 通过
super调用父类的构造方法和方法。
你可以根据需要继续扩展和修改类的实现,增加新的特性或行为。
2-2-6 final 关键字
在 Java 中,final 关键字用于表示“最终”的意思,用来修饰类、方法、变量等,禁止它们被修改、继承或重写。
1. final 修饰类
当一个类被声明为 final 时,它不能被继承。也就是说,无法创建该类的子类。例如:
public final class Labrador extends Dogs {
@Override
public boolean isGuideBlindness() {
return true;
}
}
Labrador类被声明为final,这意味着它不能再被其他类继承。尝试继承Labrador会导致编译错误。- 在你的例子中,
Dogs类是可以继承的,但Labrador类一旦定义为final,任何其他类都不能继承它。
2. final 修饰方法
当一个方法被声明为 final 时,表示该方法不能被子类重写。这是为了防止子类改变父类的方法实现。例如:
public final boolean isGuideBlindness() {
return false;
}
- 如果在
Dogs类中的isGuideBlindness方法被声明为final,则无法在任何子类(如Labrador)中重写它。这确保了该方法的行为在所有子类中都保持一致。
3. final 修饰变量
当一个变量被声明为 final 时,表示该变量的值一旦被初始化,就不能再被改变。通常用于常量的定义。例如:
private static final String TEXT_COMMUNITY_NAME = "新正苑";
TEXT_COMMUNITY_NAME是一个常量,其值在初始化时设定之后不能更改。- 常量通常使用大写字母和下划线分隔(如
TEXT_COMMUNITY_NAME)来提高代码的可读性。
4. 总结 final 关键字的用途
- 修饰类:禁止类被继承,确保该类是最终的,不允许其他类扩展它。
- 修饰方法:禁止方法被重写,确保方法的实现不可变。
- 修饰变量:定义常量,确保变量的值在初始化后不可改变。
完整代码分析
以下是完整的代码实现,展示了 final 关键字的不同用法:
package com.nix.demo;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class Animal {
private String name;
private String variety;
private int age;
private String food;
public Animal() {}
public Animal(String name, String variety, int age, String food) {
this.name = name;
this.variety = variety;
this.age = age;
this.food = food;
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
if (age < 0 || age > 100) {
this.age = 0;
} else {
this.age = age;
}
}
public void eat() {
System.out.println(this.name + " 正在吃饭..正在吃:" + this.food);
}
public void sleep() {
System.out.println(this.name + " 正在睡觉");
}
// final修饰变量,不能更改
private static final String TEXT_COMMUNITY_NAME = "新正苑";
public static String getTextCommunityName() {
return TEXT_COMMUNITY_NAME;
}
public void barking() {
System.out.println("我是你爹 父类");
}
}
public class Cats extends Animal {
@Override
public void barking() {
super.barking(); // 调用父类的 barking 方法
System.out.println("喵喵喵~~~");
}
}
public class Dogs extends Animal {
// alt + insert 生成构造函数
public Dogs() {}
public Dogs(String name, String variety, int age, String food) {
super(name, variety, age, food);
}
public Dogs(String name, int age) {
super(name, age);
}
@Override
public void barking() {
System.out.println("汪汪汪~~~");
}
// 这个方法没有被声明为 final,因此子类可以覆盖
public boolean isGuideBlindness() {
return false;
}
}
// Labrador 类是 final 类,不能被继承
public final class Labrador extends Dogs {
@Override
public boolean isGuideBlindness() {
return true;
}
}
import com.nix.demo.Cats;
import com.nix.demo.Dogs;
import com.nix.demo.Labrador;
public class Application {
public static void main(String[] args) {
Dogs zhangDog = new Dogs("Jerry", "null", 1, "狗粮");
Cats liCat = new Cats();
liCat.setName("Tom");
liCat.setAge(3);
liCat.setFood("猫粮");
zhangDog.eat();
liCat.sleep();
zhangDog.barking();
liCat.barking();
// 使用 final 修饰的常量
System.out.println("猫的小区名:" + Cats.getTextCommunityName());
// Labrador 是 final 类,不能再被继承
Labrador labrador = new Labrador();
System.out.println(labrador.isGuideBlindness());
}
}
关键点总结:
final修饰类:Labrador类是final,它不能被继承。final修饰方法:isGuideBlindness方法可以被子类重写,除非它被声明为final。final修饰变量:TEXT_COMMUNITY_NAME是常量,一旦赋值后不能再修改。
通过使用 final 关键字,你可以确保一些关键的类、方法和变量不被修改,从而确保代码的安全性和可维护性。
2-2-7 引出抽象类的概念
在你的问题中,确实存在一个非常重要的设计原则:“Animal 类是否能被直接实例化?”。
1. Animal 类不能被实例化
Animal类是一个基类,用于继承,不是一个具体的类。我们通常不会直接创建Animal类型的对象。- 类似地,你提到的
new Animal()实际上在实际应用中是没有意义的。Animal是一个通用的类,表示所有动物的共性特征(如名字、年龄、食物等),但它不能直接创建实例。
2. 为什么不可以实例化 Animal?
Animal类有很多具体的行为,但也有很多行为是所有动物通用的,这些通用行为可以由具体的子类去继承和实现。因此,Animal类更多的是作为一个 父类 来存在。- 比如,
Animal类中定义了barking()(动物叫)的行为,但是每个具体的动物(如狗、猫)都应该有不同的叫声(比如狗叫、猫叫),这时候就需要用到 抽象类。
3. 什么是抽象类?
- 抽象类 是一个 不能被实例化的类。它通常包含一些 抽象方法(没有具体实现的方法),这些方法必须在 子类 中实现。
- 通过抽象类,我们可以确保所有子类都有相同的接口,但具体的实现细节是由每个子类来提供的。
4. 如何使用抽象类来解决问题?
假设我们要为所有动物定义一个 叫声,但是每个动物的叫声都不一样,我们可以将 barking() 方法定义为 抽象方法,这样子类必须重写它并提供具体实现。
修改 Animal 类,使用抽象类
package com.nix.demo;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
// Animal 是一个抽象类
@Getter
@Setter
@ToString
public abstract class Animal { // 关键字 abstract 标明这是一个抽象类
private String name;
private String variety;
private int age;
private String food;
public Animal() {}
public Animal(String name, String variety, int age, String food) {
this.name = name;
this.variety = variety;
this.age = age;
this.food = food;
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
if (age < 0 || age > 100) {
this.age = 0;
} else {
this.age = age;
}
}
public void eat() {
System.out.println(this.name + " 正在吃饭..正在吃:" + this.food);
}
public void sleep() {
System.out.println(this.name + " 正在睡觉");
}
// 这是一个抽象方法,必须在子类中实现
public abstract void barking(); // 这里不提供实现,交给具体的动物去实现
}
修改 Dogs 和 Cats 类,继承并实现抽象方法
package com.nix.demo;
// Dogs 是具体类,继承自抽象类 Animal
public class Dogs extends Animal {
public Dogs() {}
public Dogs(String name, String variety, int age, String food) {
super(name, variety, age, food);
}
@Override
public void barking() {
System.out.println("汪汪汪~~~"); // 具体实现狗的叫声
}
}
package com.nix.demo;
// Cats 是具体类,继承自抽象类 Animal
public class Cats extends Animal {
public Cats() {}
public Cats(String name, String variety, int age, String food) {
super(name, variety, age, food);
}
@Override
public void barking() {
System.out.println("喵喵喵~~~"); // 具体实现猫的叫声
}
}
具体使用
import com.nix.demo.Dogs;
import com.nix.demo.Cats;
public class Application {
public static void main(String[] args) {
// 不能创建 Animal 的实例,因为它是抽象类
// Animal animal = new Animal(); // 编译错误
Dogs zhangDog = new Dogs("Jerry", "Golden Retriever", 2, "狗粮");
Cats liCat = new Cats("Tom", "Siamese", 3, "猫粮");
zhangDog.eat();
liCat.sleep();
zhangDog.barking(); // 狗叫
liCat.barking(); // 猫叫
}
}
关键点解释:
- 抽象类 (
abstract class):Animal类定义为抽象类,不能直接实例化。- 抽象类可以包含 抽象方法(没有具体实现的方法),这些方法在子类中必须重写。
- 抽象方法 (
abstract method):barking()方法被声明为抽象方法,子类(如Dogs和Cats)必须提供具体实现。barking()在Dogs类中实现为汪汪汪~~~,在Cats类中实现为喵喵喵~~~。
- 不能实例化抽象类:
Animal类是抽象类,不能直接使用new Animal()创建对象。- 你只能通过它的具体子类(
Dogs或Cats)来创建对象。
结论:
- 通过将
Animal类定义为抽象类,你确保了它只能作为一个基类,不能直接实例化。 Dogs和Cats类继承自Animal类,并实现了barking()方法的具体行为。- 这样设计可以避免在某些情况下创建不完整的对象,并且让每个具体的动物提供自己独特的行为实现。
通过 抽象类 和 抽象方法,我们实现了对不同行为的具体实现,同时确保了通用的接口一致性。这是面向对象设计中常用的技巧之一。
2-3-3 接口(Interface)
在 Java 中,接口(Interface)是一种特殊的类,它完全由 抽象方法 组成,并且类通过实现(implements)接口来提供这些方法的具体实现。接口本质上定义了一些行为,而不关心具体的实现。
1. 接口的基本特征
- 接口中不能有具体的方法实现(Java 8 后可以有默认方法和静态方法,但通常接口方法是抽象的)。
- 接口不能有构造方法。
- 类可以通过
implements关键字来实现一个或多个接口,并且必须实现接口中定义的所有抽象方法。
2. 如何使用接口:
你定义了一个接口 Human,其中包含了两个方法 eat() 和 run(),它们是 抽象方法。然后你创建了两个类 Chinese 和 Westerner,它们都实现了 Human 接口,并为接口方法提供了具体实现。
代码示例解析
package com.nix.demo;
// 定义接口 Human
public interface Human {
public void eat();
public void run();
}
Human接口定义了两个抽象方法:eat()和run()。- 接口本身不能实例化,因此你不能直接创建
Human类型的对象。
package com.nix.demo;
// 实现 Human 接口的 Chinese 类
public class Chinese implements Human {
@Override
public void eat() {
System.out.println("吃中餐");
}
@Override
public void run() {
System.out.println("小跑");
}
}
Chinese类实现了Human接口,并提供了eat()和run()方法的具体实现。- 这里使用了
@Override注解,表示我们在子类中重写了接口中的方法。
package com.nix.demo;
// 实现 Human 接口的 Westerner 类
public class Westerner implements Human {
@Override
public void eat() {
System.out.println("吃西餐");
}
@Override
public void run() {
System.out.println("大跑");
}
}
- 同样,
Westerner类也实现了Human接口,提供了自己版本的eat()和run()方法。
import com.nix.demo.Chinese;
import com.nix.demo.Westerner;
import com.nix.demo.Human;
public class Application {
public static void main(String[] args) {
// 创建 Chinese 类型的对象
Human chinese = new Chinese();
chinese.eat(); // 输出: 吃中餐
chinese.run(); // 输出: 小跑
// 创建 Westerner 类型的对象
Human westerner = new Westerner();
westerner.eat(); // 输出: 吃西餐
westerner.run(); // 输出: 大跑
}
}
- 在
Application类中,创建了Chinese和Westerner类型的对象,并通过接口Human类型来引用它们。 - 这展示了接口的多态性:你可以通过接口类型来操作不同的具体实现。
总结
- 接口的定义和实现:
- 接口定义了一些方法,类通过
implements关键字来实现这些接口,提供具体的行为实现。
- 接口定义了一些方法,类通过
- 接口的多态性:
- 通过接口引用对象,你可以实现不同类的多态性。例如,在
Application中,chinese和westerner都是Human类型,可以通过相同的接口方法(eat()和run())来操作它们,尽管它们是不同的类。
- 通过接口引用对象,你可以实现不同类的多态性。例如,在
- 接口和抽象类的区别:
- 抽象类可以有具体方法(有方法实现),而接口中所有的方法通常是抽象的。
- 一个类可以实现多个接口,而只能继承一个抽象类。
扩展
你还可以使用接口来解决多个类共享行为但不具有共同父类的情况。例如,如果你希望让 Hamsters 也实现 Human 接口,虽然 Hamsters 并不是 Human 类的子类,但它可以通过实现接口来拥有 eat() 和 run() 方法。
package com.nix.demo;
// 实现 Human 接口的 Hamsters 类
public class Hamsters implements Human {
@Override
public void eat() {
System.out.println("吃坚果");
}
@Override
public void run() {
System.out.println("小步跑");
}
}
这样,你的 Hamsters 类也实现了 Human 接口,具有了 eat() 和 run() 方法,从而在程序中可以与其他实现了 Human 接口的类保持一致的接口使用方式。
2-3-4 class 和 interface 的区别:哲学气息
从 哲学 的角度来看,抽象类 和 接口 其实代表了不同的 抽象化层次 和 设计理念。
1. 抽象类:针对具体事物的抽象
抽象类像是 一个模板,它定义了某种事物(例如 “动物”)的通用特征和行为。抽象类是用来描述 事物的本质属性,它更加偏向于 事物的存在,而不仅仅是 行为。
- 存在:抽象类试图捕捉事物的共同特性,例如所有“动物”都应该有“名字”、“年龄”、“吃饭”的功能。
- 抽象:抽象类在描述事物的特征时,可以有部分方法的实现(即具体方法)来提供默认行为,也可以有一些抽象方法(即没有具体实现的方法)来要求子类实现。
举个例子:
abstract class Animal {
String name;
int age;
// 这是抽象类的行为,所有动物都可以吃饭
public void eat() {
System.out.println(name + " 正在吃饭");
}
// 这是抽象方法,要求子类实现具体的叫声
abstract void makeSound();
}
- 哲学层面:抽象类代表了事物的 本质属性(如“吃饭”)。它是对事物的一个整体抽象,给出了事物共有的行为规范,但也允许事物的具体行为(如“叫声”)根据不同的实例来变动。
2. 接口:针对动作、行为的抽象
接口则完全不同,它代表了 事物能做的事情。它并不关心事物的具体存在形式,而是专注于 事物的行为。接口更多的是 描述一系列行为 或 动作的集合,而且通常避免使用名词,更多是动词。
- 行为:接口定义了事物能做的事情,像是“吃饭”、“跑步”或“飞行”等动作。
- 抽象:接口中没有具体的实现方法,所有的方法都是 抽象的,它强调的是 能力和动作,不关心事物的内在属性和具体状态。
举个例子:
interface AnimalActions {
void eat(); // 动作:吃
void run(); // 动作:跑
}
- 哲学层面:接口代表了事物 所能表现出的行为。它不关心“事物是什么”,而关心“事物做什么”。例如,不同的动物(鸟、鱼、狗)都可以有相同的“吃”和“跑”行为,它们通过实现接口来表明自己具有这些行为能力。
3. 本质区别总结:
- 抽象类:针对具体事物进行抽象,它关心的是事物的结构,即事物有哪些属性以及部分行为如何实现。抽象类是事物的抽象定义,它可能包含具体的实现。
- 接口:针对行为和动作进行抽象,专注于描述事物能做什么而非事物本身。接口像是行动的契约,定义了事物应该具备的行为,但不关心这些行为的具体实现方式。
4. 举例对比:
-
抽象类:
Animal类作为一个抽象类,关心的是所有动物的基本属性和行为,如“吃”。abstract class Animal { abstract void makeSound(); // 声音 } -
接口:
AnimalActions接口关心的是行为,如动物是否能“吃”和“跑”。interface AnimalActions { void eat(); void run(); }
5. 类比:
- 抽象类的类比:可以想象成一个大类的类别或种类,例如“车”类。它描述了所有车的共同属性(如车的颜色、型号)以及可以有部分共同的行为(如启动车)。
- 接口的类比:可以想象成一个工具包或能力列表,例如“驾驶能力”。它仅仅列出一个驾驶者可以执行的动作,而不关心他们驾驶的是哪种车。每个驾驶者都可能有这个能力,但驾驶的具体细节则由车辆类型决定。
总结
- 抽象类像是对事物的整体抽象,它有自己的属性和部分实现。它描述了事物的一些共性特征,同时也可以为子类提供一些默认行为。
- 接口则像是对事物行为的抽象,它只关心事物能做什么,而不关心事物本身。它提供了一种行为契约,任何实现了接口的类都能执行这些行为。
从哲学角度来说,抽象类更像是对事物“存在”的抽象,而接口则是对事物“行动”的抽象。
2-4-1 多态 — 花木兰替父从军
1. 什么是多态?
多态是面向对象编程中的一个重要特性,它使得同一操作在不同的对象上产生不同的效果。换句话说,多态允许同一个方法调用在不同的类中表现出不同的行为。
多态的实现需要满足三个必要条件:
- 继承:子类继承父类,或者实现接口。
- 重写:子类重写父类的方法,提供具体的实现。
- 父类引用指向子类对象:父类引用可以指向子类对象,从而动态调用子类的实现。
2. 代码分析:
你的代码展示了 向上转型 和 向下转型 的例子。
向上转型
向上转型是指把子类对象赋值给父类引用变量。这种情况下,虽然父类引用指向子类对象,但是调用的方法仍然是子类重写的方法(如果子类有重写的话)。这种机制称为 动态绑定 或 运行时绑定。
// 向上转型:父类引用指向子类对象
HuaHu huaHu = new HuaMuLan();
这里,huaHu 是父类 HuaHu 类型的引用,但它指向的是子类 HuaMuLan 的对象。
向下转型
向下转型是指将父类引用强制转换为子类引用。向下转型之前,必须保证父类引用实际指向的是子类对象,否则会抛出 ClassCastException。
// 向下转型:父类引用强制转换为子类引用
HuaMuLan huaMuLan = (HuaMuLan) huaHu;
这里,huaHu 是父类 HuaHu 类型的引用,但它指向的是子类 HuaMuLan 的对象,因此可以通过强制转换将 huaHu 转回 HuaMuLan 类型。
3. 程序执行的过程:
- 向上转型后,父类
HuaHu引用huaHu指向了子类HuaMuLan的对象。调用huaHu.fight()时,实际调用的是HuaMuLan类中的重写方法(如果有重写)。 - 向下转型后,我们把
huaHu转回HuaMuLan类型,然后访问HuaMuLan中的属性和方法(如dressing())。
4. 多态的表现:
-
父类引用调用子类重写的方法:
HuaHu huaHu = new HuaMuLan(); huaHu.fight(); // 调用 HuaMuLan 中重写的 fight 方法即使
huaHu的声明类型是HuaHu,它实际指向的是HuaMuLan对象,因此调用的是HuaMuLan中的fight()方法(如果有重写的话)。 -
父类引用指向子类对象时,动态绑定: 在运行时,JVM 会根据对象的实际类型(而不是引用类型)来决定调用哪个方法,这就是多态的核心。
5. 输出结果:
HuaMuLan huaMuLan = (HuaMuLan) huaHu;
System.out.println(huaMuLan.name); // 输出:HuaMuLan
System.out.println(huaMuLan.age); // 输出:19
huaMuLan.dressing(); // 输出:HuaMuLan化妆...
HuaMuLan.sayMe(); // 输出:大家好!我叫HuaMuLan,我今年19!
6. 总结:
- 多态的关键在于:父类引用指向子类对象,从而使得调用的方法在运行时表现出不同的行为。
- 向上转型和向下转型是多态的一部分。向上转型简化了代码,但有时需要进行向下转型来访问子类特有的成员。
7. 扩展:
多态使得代码更加灵活和可扩展。例如,如果你要添加一个新的类 HuaZhong 继承自 HuaHu,并实现自己的 fight() 方法,你只需要通过向上转型和多态调用,即可无缝地扩展代码。
2-4-2 匿名内部类
1. 什么是匿名内部类?
匿名内部类(Anonymous Inner Class)是没有名字的内部类。它是一种在方法内临时创建类的机制,通常用于只需要一次的类定义。匿名内部类可以用来简化代码,特别是当需要创建一个实现接口或继承抽象类的类时。
匿名内部类的特点:
- 它没有类名。
- 它继承一个类或实现一个接口。
- 通过实例化对象并直接提供接口或类的实现代码。
2. 匿名内部类的用法:
2.1 实现接口
匿名内部类的常见用途之一是实现接口。你可以在创建接口实例时,直接提供实现代码,而不需要显式创建一个类。
Human jerry = new Human() {
@Override
public void eat() {
System.out.println("吃中国菜");
}
@Override
public void sleep() {
// 实现 sleep 方法,留空
}
};
jerry.eat(); // 输出:吃中国菜
-
解释
:
new Human()创建了一个匿名内部类实例,这个类实现了Human接口。- 接着,匿名内部类中的
eat()方法被重写,输出吃中国菜。 - 匿名内部类没有类名,而是在创建实例时直接定义其实现。
2.2 继承抽象类
匿名内部类也可以继承一个抽象类,并实现其抽象方法。
Human jerry = new Human() {
@Override
public void eat() {
System.out.println("吃中国菜");
}
@Override
public void sleep() {
// 可以选择是否重写 sleep 方法
}
};
jerry.eat();
在此,Human 是一个接口,匿名类实现了该接口并提供了 eat() 方法的具体实现。
3. 匿名内部类的使用场景:
-
临时实现接口或抽象类:当你只需要某个接口或类的实现一次时,使用匿名内部类可以简化代码并提高可读性。
-
事件监听器:在GUI编程中,事件监听器常常使用匿名内部类来实现。例如在 Java Swing 中,按钮的点击事件通常使用匿名内部类来处理。
button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("按钮被点击了!"); } }); -
回调机制:在多线程编程或异步编程中,回调函数通常使用匿名内部类来定义。
4. 优缺点:
优点:
- 简洁:匿名内部类不需要额外创建类文件,代码更简洁。
- 临时性:适用于仅使用一次的类实例,避免了类的定义。
- 提高可读性:使得代码结构更紧凑,避免冗余。
缺点:
- 可读性差:匿名内部类没有名字,可能使得调试和理解代码时稍显困难。
- 不可复用:匿名内部类不能被复用,只能在当前上下文中使用。
5. 总结:
匿名内部类是用来临时创建类并实现接口或继承抽象类的方便方式。当你需要一个类的实例,但只在一个地方用到它时,使用匿名内部类会让代码更简洁。然而,对于复杂的类实现或需要多次使用的情况,使用普通类会更合适。
3-1-1 权限修饰符
在 Java 中,权限修饰符(Access Modifiers)用于控制类、方法、字段等成员的访问权限。Java 提供了四种主要的权限修饰符,它们控制了程序中不同类或对象对类、方法和成员变量的访问权限。
1. 四种权限修饰符:
publicprotecteddefault(无修饰符,包级访问权限)private
2. 权限修饰符的作用:
public: 最高权限,可以被任何类访问。protected: 可以在同一个包中的类访问,或者在不同包中的子类访问。default(包级别,未指定修饰符):只在同一个包中的类可以访问。private: 只能在当前类内部访问,无法在外部访问。
3. 权限修饰符的具体说明:
| 访问修饰符 | 类的访问权限 | 同一包中的其他类 | 子类中的访问权限 | 其他包中的类 |
|---|---|---|---|---|
public | 可以访问 | 可以访问 | 可以访问 | 可以访问 |
protected | 可以访问 | 可以访问 | 可以访问 | 只有子类可以访问 |
default | 只在同包内可见 | 只在同包内可见 | 不能访问 | 不能访问 |
private | 只在本类内可见 | 不能访问 | 不能访问 | 不能访问 |
4. public 访问修饰符:
- 类:
public修饰的类可以被任何其他类访问。 - 字段/方法:
public修饰的字段或方法可以被任何其他类直接访问。
public class MyClass {
public int number; // 任何类都能访问这个字段
public void display() { // 任何类都能调用这个方法
System.out.println("Hello, World!");
}
}
5. protected 访问修饰符:
- 类:不能修饰类,只能修饰字段和方法。
- 字段/方法:
protected修饰的字段或方法可以在同一包中访问,也可以在不同包中的子类中访问。
class Animal {
protected String name;
protected void speak() {
System.out.println("Animal speaks");
}
}
class Dog extends Animal {
public void speak() {
System.out.println("Dog barks");
}
}
6. default(包级访问权限):
- 类:默认情况下,类在没有指定访问修饰符时是
default。 - 字段/方法:没有指定修饰符时,也是
default。即只能在同一包内访问。
class MyClass {
int number; // 默认访问权限,只能在同包中访问
void display() { // 默认访问权限,只能在同包中访问
System.out.println("Hello from default");
}
}
7. private 访问修饰符:
- 类:
private不能修饰类。 - 字段/方法:
private修饰的字段或方法只能在当前类内部访问,外部类无法访问。
class MyClass {
private int number; // 只能在本类内部访问
private void display() { // 只能在本类内部调用
System.out.println("Hello from private method");
}
public void accessPrivate() {
// 可以在本类内部访问 private 成员
display();
}
}
8. private、default、protected 与 public 的使用建议:
public:如果类、字段或方法需要在整个项目中广泛访问,可以使用public修饰符。private:为了信息隐藏和封装,应该尽量将字段和方法设置为private,并通过公共的getter和setter方法来访问。protected:当字段或方法仅在子类或同一包内可见时,可以使用protected。但是尽量避免过多使用protected,以防止设计不当。default:如果字段或方法只在同一包中使用,可以使用default,通常用于实现内部的共享方法。
9. 示例代码:
// 父类
public class Parent {
public String publicField = "Public Field";
protected String protectedField = "Protected Field";
String defaultField = "Default Field";
private String privateField = "Private Field";
public void publicMethod() {
System.out.println("Public Method");
}
protected void protectedMethod() {
System.out.println("Protected Method");
}
void defaultMethod() {
System.out.println("Default Method");
}
private void privateMethod() {
System.out.println("Private Method");
}
}
// 子类
class Child extends Parent {
public void accessParentFields() {
System.out.println(publicField); // 可以访问 public
System.out.println(protectedField); // 可以访问 protected
// System.out.println(defaultField); // 错误,无法访问 default
// System.out.println(privateField); // 错误,无法访问 private
}
public void accessParentMethods() {
publicMethod(); // 可以访问 public
protectedMethod(); // 可以访问 protected
// defaultMethod(); // 错误,无法访问 default
// privateMethod(); // 错误,无法访问 private
}
}
10. 总结:
public:最开放,任何地方都可以访问。protected:子类和同包类可以访问。default(包级):同包中的类可以访问。private:只有当前类内部可以访问。
正确使用权限修饰符可以帮助我们进行更好的封装和代码设计,确保类、方法、字段的访问权限符合设计需求。
3-1-2 Object 类
在 Java 中,Object 是所有类的父类。所有的类都直接或间接继承自 Object 类。如果一个类没有显式继承父类,那么它默认继承自 Object 类。这个类提供了一些常用的方法,所有继承自 Object 的类都能使用这些方法。
1. Object 类的作用:
- 所有类的基类:每一个类都直接或间接地继承自
Object类。 - 提供基础的方法:
Object类定义了一些基础方法,如equals()、hashCode()、toString()等,所有类可以重写这些方法来满足特定需求。
2. Object 类的常用方法:
-
toString():返回对象的字符串表示形式,通常需要重写。默认情况下,
toString()方法返回的是对象的类名和哈希值,但我们通常会重写它以便输出更有意义的信息。class Person { String name; int age; @Override public String toString() { return "Person{name='" + name + "', age=" + age + "}"; } } public class Application { public static void main(String[] args) { Person p = new Person(); p.name = "John"; p.age = 25; System.out.println(p.toString()); // 输出: Person{name='John', age=25} } } -
equals(Object obj):判断当前对象与另一个对象是否相等。默认实现是比较对象的内存地址(即是否是同一个对象),但通常会重写此方法来比较对象的内容。class Person { String name; int age; @Override public boolean equals(Object obj) { if (this == obj) return true; // 判断是否是同一个对象 if (obj == null || getClass() != obj.getClass()) return false; // 类型检查 Person person = (Person) obj; // 强制类型转换 return name.equals(person.name) && age == person.age; // 内容比较 } } public class Application { public static void main(String[] args) { Person p1 = new Person(); p1.name = "John"; p1.age = 25; Person p2 = new Person(); p2.name = "John"; p2.age = 25; System.out.println(p1.equals(p2)); // 输出: true } } -
hashCode():返回对象的哈希码,通常需要与equals()一起重写。如果两个对象相等(equals()返回true),那么它们的哈希码也必须相等。class Person { String name; int age; @Override public int hashCode() { return 31 * name.hashCode() + age; // 根据对象的字段计算哈希值 } } public class Application { public static void main(String[] args) { Person p1 = new Person(); p1.name = "John"; p1.age = 25; Person p2 = new Person(); p2.name = "John"; p2.age = 25; System.out.println(p1.hashCode() == p2.hashCode()); // 输出: true } } -
getClass():返回当前对象的类类型。此方法常用于反射。class Person {} public class Application { public static void main(String[] args) { Person p = new Person(); System.out.println(p.getClass()); // 输出: class Person } } -
clone():创建并返回当前对象的一个副本(浅拷贝)。该方法是Object类中的protected方法,因此需要在子类中重写并实现Cloneable接口才能使用。class Person implements Cloneable { String name; int age; @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } } public class Application { public static void main(String[] args) throws CloneNotSupportedException { Person p1 = new Person(); p1.name = "John"; p1.age = 25; Person p2 = (Person) p1.clone(); System.out.println(p1 == p2); // 输出: false,说明是不同的对象 } } -
finalize():在对象被垃圾回收前调用,用于清理资源。通常不推荐使用,因为垃圾回收器并不保证什么时候调用finalize()方法。class Person { @Override protected void finalize() throws Throwable { System.out.println("对象被垃圾回收之前调用"); } } public class Application { public static void main(String[] args) { Person p = new Person(); p = null; // 设置对象为null,可能会被垃圾回收 System.gc(); // 强制进行垃圾回收 } }
3. Object 类的继承与重写
- 继承:所有的 Java 类默认继承自
Object类,所以每个类都可以使用Object类中的方法。 - 重写方法:如上所述,
Object类提供了很多方法,可以在自定义类中根据需要进行重写,来实现自定义行为。例如,重写toString()来输出自定义的对象信息,重写equals()来比较两个对象的内容是否相同。
4. Object 类中的常用方法小结:
toString():提供对象的字符串表示,常常被重写以提供更加有意义的信息。equals():用于比较对象是否相等,通常需要重写。hashCode():返回对象的哈希码,常常与equals()一起重写。getClass():返回当前对象的类类型。clone():复制当前对象,需实现Cloneable接口才能使用。finalize():在对象被垃圾回收前执行,用于清理资源。
5. 总结:
Object类是 Java 中所有类的父类,所有类默认继承它,提供了许多方法。toString()、equals()、hashCode()等方法提供了基础的对象操作能力,可以根据需要进行重写。- 通过继承
Object类,Java 为我们提供了标准的行为(如比较、字符串表示等),使得我们在编写代码时能够实现更一致的接口。
2万+

被折叠的 条评论
为什么被折叠?



