继承
何为继承
继承(Inheritance)是面向对象编程(OOP)中的一个基本概念,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。继承是一种机制,通过这种机制,可以创建一个新类,这个新类是现有类的修改版或扩展版。
继承的特点:
-
代码复用:继承支持代码复用。子类可以继承父类的代码,无需重新编写相同的代码。
-
层次结构:继承支持创建类之间的层次结构,这有助于组织和管理大型软件项目。
-
扩展性:子类可以扩展父类的功能,通过添加新的方法或覆盖现有方法来实现。
-
多态性:继承是实现多态性的一个基础。通过继承,子类可以被看作是父类的一种特殊类型,这允许将子类对象视为父类对象使用。
-
访问控制:子类可以访问父类的公共(public)和受保护(protected)成员,但不能直接访问私有(private)成员。
-
方法重写:子类可以重写(Override)父类的方法,提供特定的实现。
-
构造器:子类继承父类的属性,但需要调用父类的构造器来初始化这些属性。
-
super 关键字:在子类中,可以使用
super
关键字来引用父类的成员,包括调用父类的构造器或方法。 -
类型兼容性:在Java中,子类是其父类的一个子类型。因此,子类的对象可以在任何期望父类对象的地方使用。
-
最终类:如果一个类被声明为
final
,则它不能被其他类继承。
示例:
// 父类
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + " is eating.");
}
public void sleep() {
System.out.println(name + " is sleeping.");
}
}
// 子类
public class Dog extends Animal { // Dog 继承自 Animal
public Dog(String name) {
super(name); // 调用父类的构造器
}
@Override // 注解表示我们正在重写一个方法
public void eat() {
System.out.println(name + " is eating dog food.");
}
public void bark() {
System.out.println(name + " says bark!");
}
}
// 使用继承
public class Main {
public static void main(String[] args) {
Dog myDog = new Dog("Buddy");
myDog.eat(); // 使用继承并重写的方法
myDog.bark(); // Dog类特有的方法
}
}
在这个示例中,Dog
类通过 extends
关键字继承自 Animal
类。Dog
类继承了 Animal
类的 name
属性和 eat()
、sleep()
方法,并且重写了 eat()
方法,添加了 bark()
方法。这展示了继承如何用于代码复用和功能扩展。
继承的优缺点
继承作为面向对象编程的一个核心概念,提供了代码复用和建立层次结构的能力,但同时也带来了一些设计上的挑战。
优点:
-
代码复用:继承允许子类继承父类的属性和方法,减少了代码的重复编写,提高了开发效率。
-
层次结构:继承支持创建清晰的类层次结构,有助于组织和管理大型软件项目。
-
多态性:继承是实现多态性的基础,允许将子类对象视为父类对象使用,提高了程序的灵活性。
-
扩展性:子类可以扩展父类的功能,通过添加新的方法或覆盖现有方法来实现特定的行为。
-
简化性:使用继承可以简化复杂的系统设计,将通用功能抽象到父类中,子类专注于特定功能。
-
维护性:当父类的方法需要修改时,所有继承自该父类的子类都会自动获得更新,简化了维护工作。
缺点:
-
紧密耦合:继承创建了类之间的强关联,父类的改变可能会影响到所有子类,这可能导致代码难以维护。
-
脆弱性:高度的继承结构可能使得系统变得脆弱,一个小的修改可能会在意想不到的地方引起问题。
-
限制性:继承是静态的,一旦定义了继承关系,就很难改变。这限制了类的灵活性。
-
继承深度:过度使用继承可能导致继承层次过深,使得代码难以理解和跟踪。
-
不灵活:继承不支持多重继承,这限制了从多个父类继承不同功能的能力。
-
滥用:继承有时被滥用,例如,为了获得一些工具方法而继承一个不相关的类,这违反了OOP的基本原则。
-
隐藏细节:继承可能会隐藏实现细节,使得代码阅读者难以理解代码的真正行为。
-
修改限制:如果父类的方法被声明为
final
,则子类无法重写该方法,这限制了子类的扩展性。 -
继承而非组合:有时开发者可能会优先使用继承而不是组合,这可能导致设计问题,因为继承并不总是实现代码复用的最好方式。
总的来说,继承是一个强大的工具,但应该谨慎使用。在某些情况下,组合或其他设计模式可能是更好的选择。正确使用继承可以带来代码复用和清晰的层次结构,但也需要考虑到它可能引入的耦合性和维护挑战。
Object类
在Java中,Object
类是所有Java类的根类,位于类继承层次结构的顶端。这意味着Java中的每个类都隐式地继承了Object
类,除非你使用final
关键字明确地阻止进一步继承。Object
类位于java.lang
包中,它是Java语言的基础。
Object
类的特点:
-
通用性:作为根类,
Object
类定义了所有Java对象共有的基本行为。 -
方法:
equals(Object obj)
: 用于比较两个对象的等价性,默认实现比较对象的内存地址,但通常被重写以提供有意义的等价性比较。hashCode()
: 返回对象的哈希码,通常与equals
方法一起重写,以确保相等的对象有相同的哈希码。toString()
: 返回对象的字符串表示,默认实现返回对象的类名和内存地址的无符号十六进制表示,但通常被重写以提供更有意义的信息。clone()
: 创建并返回对象的一个副本,具体行为依赖于Cloneable
接口的实现。finalize()
: 在对象被垃圾回收器回收前进行清理操作,但不推荐使用,因为Java 9开始已被标记为过时,并在Java 14中被移除。
-
序列化:
Object
类实现了Serializable
接口,这意味着所有Java对象都可以被序列化和反序列化。 -
等待和通知机制:
wait()
: 导致当前线程等待,直到另一个线程调用对象的notify()
或notifyAll()
方法。notify()
: 唤醒在此对象上等待的单个线程。notifyAll()
: 唤醒在此对象上等待的所有线程。
-
内存管理:
Object
类的方法支持Java的内存管理机制,特别是垃圾回收。 -
根类:由于
Object
类是所有Java类的父类,它为所有对象提供了一个公共的类型。 -
反射:
Object
类是反射的起点,因为所有类都隐式地继承自Object
,所以Object
类的属性和方法在反射API中是可用的。 -
实例创建:使用
new
关键字创建对象时,所有对象都会隐式地调用Object
类的构造器。
示例:
public class MyClass {
private int value;
public MyClass(int value) {
this.value = value;
}
@Override
public String toString() {
return "MyClass{" + "value=" + value + '}';
}
public static void main(String[] args) {
MyClass myObject = new MyClass(42);
System.out.println(myObject.toString()); // 重写Object类的toString()方法
}
}
在这个例子中,MyClass
隐式地继承了Object
类,因此可以使用Object
类的toString()
方法。MyClass
重写了toString()
方法,以提供更具体的字符串表示。
作为根类,Object
类为Java语言提供了一个统一的基础,确保了所有对象都具有一些基本的行为和特性。
常用方法有哪些
Java的Object
类定义了一些常用方法,这些方法在Java编程中非常普遍,经常被重写以提供特定功能。
-
equals(Object obj)
:- 用于比较两个对象的等价性。默认实现检查对象的内存地址是否相同,但通常被重写以提供基于属性的比较。
-
hashCode()
:- 返回对象的哈希码,用于在哈希表中定位对象。如果
equals
方法被重写,通常也需要重写hashCode
方法,以维护equals
和hashCode
的一致性。
- 返回对象的哈希码,用于在哈希表中定位对象。如果
-
toString()
:- 返回对象的字符串表示。默认实现返回对象的类名和内存地址的无符号十六进制表示,但通常被重写以提供更有意义的信息。
-
`clone()``:
- 创建并返回对象的一个副本。这个方法是受保护的,需要在子类中被重写为
public
,并且通常还要实现Cloneable
接口。
- 创建并返回对象的一个副本。这个方法是受保护的,需要在子类中被重写为
-
`finalize()``(已在Java 14中移除):
- 允许对象在被垃圾回收器回收前进行清理操作。由于不可预测性和替代机制的存在,此方法不推荐使用。
-
`getClass()``:
- 返回对象的
Class
对象,表示对象的运行时类。
- 返回对象的
-
`wait()``:
- 导致当前线程等待,直到另一个线程调用对象的
notify()
或notifyAll()
方法。
- 导致当前线程等待,直到另一个线程调用对象的
-
`notify()``:
- 唤醒在此对象上等待的单个线程,选择是任意的。
-
`notifyAll()``:
- 唤醒在此对象上等待的所有线程。
示例:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = 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 age == person.age && (name != null ? name.equals(person.name) : person.name == null);
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
// 其他方法...
}
在这个Person
类的例子中,我们重写了equals
、hashCode
和toString
方法。equals
方法用于比较两个Person
对象是否相等,基于它们的name
和age
属性。hashCode
方法返回一个基于属性的哈希码。toString
方法提供了Person
对象的字符串表示,这对于调试和记录日志非常有用。
方法重写
方法重写(Method Overriding)是面向对象编程中的一个概念,它发生在子类中,子类提供一个已在父类中定义的方法的新实现。方法重写使得子类可以改变从父类继承来的方法的行为。
方法重写的特点:
-
方法签名相同:被重写的方法必须具有与父类中相同的方法签名,即方法名、参数列表和返回类型(协变返回类型除外)必须一致。
-
访问级别:子类中重写的方法不能有比父类更严格的访问级别。
-
非静态方法:只能重写父类中的非静态方法。静态方法不能被子类重写。
-
重写注解:在Java中,可以使用
@Override
注解来明确地表示一个方法是重写自父类的方法。如果方法没有正确地重写父类方法,编译器会报错。 -
多态性:方法重写是多态性的一个重要方面,它允许通过父类引用调用子类重写的方法。
-
调用
super
方法:在子类的方法中,可以使用super
关键字调用父类中被重写的方法。 -
方法的可见性:如果子类重写的方法使用了更宽松的访问修饰符,那么子类的这个方法可以被子类的子类访问。
-
抽象类和接口:如果父类是一个抽象类或实现了接口,子类必须重写所有抽象方法。
示例:
class Animal {
public void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Dog(); // 多态性
myAnimal.sound(); // 输出: Dog barks
}
}
在这个示例中,Dog
类重写了 Animal
类的 sound
方法。Dog
类的 sound
方法提供了一个新的实现,当通过 Animal
类型的引用调用时,实际执行的是 Dog
类中重写的方法。这展示了多态性,即同一个接口可以有不同的实现。
方法重写规则
-
方法重写(Method Overriding)在面向对象编程中遵循一系列规则,确保继承体系中方法的正确调用和行为。
-
继承:方法重写发生在子类与其父类之间。子类必须继承父类的方法才能重写它。
-
方法签名:重写的方法必须具有与被重写方法相同的方法签名,即相同的方法名、参数列表和返回类型(对于返回类型,子类的方法可以返回父类方法返回类型的子类型,这称为协变返回类型)。
-
访问级别:重写的方法不能缩小访问级别。例如,如果父类中的方法声明为
public
,则子类中的重写方法不能声明为protected
或private
。 -
非静态方法:只能重写父类中的非静态方法。静态方法不能被子类重写,但可以被子类隐藏(Hide)。
-
重写注解:使用
@Override
注解可以帮助编译器检查方法是否正确重写了父类的方法。如果方法没有正确重写父类中的方法,编译器将报错。 -
final关键字:如果父类的方法被声明为
final
,则不能被子类重写。 -
super调用:在子类的重写方法中,可以使用
super
关键字来调用父类中被重写的方法。 -
多态性:方法重写是多态性的一个重要方面。当通过父类的引用调用重写的方法时,实际执行的是子类中的方法。
-
抽象方法:如果父类是一个抽象类,子类必须重写所有抽象方法,除非子类也是抽象类。
-
方法的可见性:如果子类重写的方法使用了比父类更宽松的访问修饰符,那么子类的这个方法可以被子类的子类访问。
-
接口方法:如果父类是一个接口,那么实现该接口的类必须提供接口中所有方法的具体实现。
-
构造器:构造器不能被重写,但可以被子类通过调用父类的构造器来扩展。
-
私有方法:私有方法不能被子类重写,因为它们在类的外部不可见。
-
方法重写与方法重载的区别
方法重写(Method Overriding)和方法重载(Method Overloading)是面向对象编程中的两个不同的概念,它们在Java等语言中有着不同的应用和规则:
方法重写(Method Overriding):
-
定义:发生在子类和父类之间,子类提供一个已在父类中定义的方法的新实现。
-
目的:改变继承自父类的行为。
-
方法签名:必须与被重写的方法具有相同的方法签名(方法名、参数列表和返回类型)。
-
访问控制:子类重写的方法不能有比父类更严格的访问控制。
-
使用场景:实现多态性,允许通过父类引用调用子类的方法。
-
例子:
class Animal { void sound() { System.out.println("Animal makes a sound"); } } class Dog extends Animal { @Override void sound() { System.out.println("Dog barks"); } }
方法重载(Method Overloading):
-
定义:在同一个类中,存在多个同名方法,但它们的参数列表不同(参数数量、类型或顺序至少有一项不同)。
-
目的:提供相同功能的多个变体,允许调用者根据参数的不同选择不同的方法。
-
方法签名:必须有所不同,至少在一个方面(参数的数量、类型或顺序)。
-
访问控制:各个重载方法可以有不同的访问控制修饰符。
-
使用场景:提供多种方法调用选项,使得调用者可以根据需要选择最合适的方法。
-
例子:
class Calculator { int add(int a, int b) { return a + b; } double add(double a, double b) { return a + b; } }
主要区别:
- 继承:方法重写需要继承关系,发生在子类和父类之间;方法重载发生在同一个类中。
- 方法签名:方法重写要求方法签名相同;方法重载要求方法签名不同。
- 返回类型:方法重写要求返回类型与被重写的方法一致或为其子类型;方法重载的方法可以有不同的返回类型。
- 访问级别:方法重写不能缩小访问级别;方法重载的各个方法可以独立设置访问级别。
- 多态性:方法重写是实现多态性的一种方式;方法重载不是多态性。
super关键字
在Java中,super
关键字是一种特殊的引用,它指向当前对象的直接父类实例。
super
的作用:
-
访问父类的成员:
使用super
可以访问直接父类中的成员变量和方法,特别是当子类中有同名成员时。public class Parent { protected int value = 10; public void printValue() { System.out.println(value); } } public class Child extends Parent { private int value = 20; public void printValue() { super.printValue(); // 访问父类的方法 System.out.println(value); // 访问子类的变量 } }
-
调用父类的构造器:
使用super()
可以显式调用父类的构造器,这通常在子类的构造器中进行。public class Parent { public Parent() { // 父类的构造代码 } } public class Child extends Parent { public Child() { super(); // 调用父类的构造器 // 子类的构造代码 } }
-
在重写的方法中调用父类的方法:
使用super
可以在子类重写的方法中调用父类被重写的方法。public class Parent { public void show() { System.out.println("Parent's show()"); } } public class Child extends Parent { @Override public void show() { super.show(); // 在重写的方法中调用父类的方法 System.out.println("Child's show()"); } }
-
区分成员变量和参数:
当方法参数与类的成员变量同名时,super
可以用来引用父类的成员变量。public class Parent { protected int value; } public class Child extends Parent { public Child(int value) { super.value = value; // 将传入的参数赋值给父类的成员变量 } }
-
静态上下文中使用:
在静态方法中,super
关键字不能用来引用非静态成员,因为静态方法不属于类的实例。但是,它可以用于调用父类的静态方法。public class Parent { public static void staticMethod() { System.out.println("Parent's static method"); } } public class Child extends Parent { public static void staticMethod() { super.staticMethod(); // 调用父类的静态方法 } }
super
关键字是Java中实现继承和多态的重要工具,它允许子类访问和调用父类的成员,即使在子类中存在同名的成员或方法。正确使用 super
可以提高代码的可读性和维护性。
final关键字
final
关键字的作用:
-
创建不可继承的类:
- 将类声明为
final
可以阻止其他类继承它。这通常用于工具类或者那些不应该被继承的类。
public final class UtilityClass { // 工具类方法 }
- 将类声明为
-
定义不可重写的方法:
- 将方法声明为
final
可以防止子类重写该方法。这有助于保护方法的核心行为不被改变。
public final void criticalMethod() { // 重要的方法实现 }
- 将方法声明为
-
声明常量:
- 将变量声明为
final
并结合static
关键字,可以创建类级别的常量。这些常量的值在初始化后不能被改变。
public static final int MAX_VALUE = 100;
- 将变量声明为
-
保证变量不可变:
- 对于实例变量,声明为
final
意味着它们只能在对象构造时被初始化一次,之后不能被重新赋值。这有助于创建不可变对象。
public final int instanceVariable;
- 对于实例变量,声明为
-
优化性能:
final
关键字可以作为编译器优化的提示,因为final
变量的值是固定的,编译器可以在某些情况下进行更有效的优化。
-
限制匿名内部类对外部局部变量的访问:
- 在匿名内部类中,只有被声明为
final
或不可变的局部变量才能被访问。
final int value = 10; new Thread(() -> { System.out.println(value); }).start();
- 在匿名内部类中,只有被声明为
-
Lambda表达式中的变量捕获:
- 在Lambda表达式中,如果外部的局部变量被声明为
final
或事实上不可修改(effectively final),则它可以在Lambda表达式中被捕获。
final int value = 10; Runnable runnable = () -> System.out.println(value);
- 在Lambda表达式中,如果外部的局部变量被声明为
-
限制方法的覆盖:
- 当一个方法被声明为
final
时,它不能被子类覆盖,这可以用来实现特定的方法行为不被改变。
- 当一个方法被声明为
使用 final
关键字有助于明确代码的意图,提高代码的可读性和可维护性,同时也为编译器提供了优化的机会。