MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java抽象类与具体类的区别

2021-07-121.6k 阅读

Java抽象类与具体类的定义

在Java中,具体类是最常见的类类型,它是可以被实例化的类,即可以使用new关键字创建对象的类。具体类包含了完整的方法实现,为程序提供具体的功能。例如,我们定义一个简单的Dog类:

public class Dog {
    private String name;

    public Dog(String name) {
        this.name = name;
    }

    public void bark() {
        System.out.println(name + " is barking.");
    }
}

在上述代码中,Dog类是一个具体类,我们可以通过new Dog("Buddy")来创建Dog类的实例,并调用bark方法来实现具体的功能。

而抽象类则有所不同,抽象类是使用abstract关键字修饰的类,它不能被实例化。抽象类的主要目的是为其他类提供一个通用的框架,它可以包含抽象方法和具体方法。抽象方法是只有方法声明而没有方法体的方法,同样使用abstract关键字修饰。例如:

public abstract class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public void eat() {
        System.out.println(name + " is eating.");
    }

    public abstract void makeSound();
}

在上述Animal抽象类中,eat方法是具体方法,有完整的方法体,而makeSound方法是抽象方法,只有声明。任何试图直接实例化Animal类,如new Animal("Generic Animal"),都会导致编译错误。

从实例化角度看两者区别

具体类的主要特性之一就是可以被实例化,这使得它在程序中能够直接创建对象并使用对象的方法和属性。例如:

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog("Max");
        dog.bark();
    }
}

上述代码通过new Dog("Max")创建了Dog类的实例dog,并调用bark方法输出Max is barking.

与之相对,抽象类不能被实例化。假设我们尝试实例化Animal抽象类:

public class Main {
    public static void main(String[] args) {
        Animal animal = new Animal("Invalid attempt"); // 编译错误
    }
}

编译器会报错,提示Animal是抽象的,无法实例化。这是因为抽象类存在的意义更多是作为一种抽象概念或通用框架,为子类提供基础,而不是直接创建实例。

方法实现的差异

具体类中的所有方法都必须有完整的实现,除非该具体类实现了某个接口中的抽象方法(但在这种情况下,具体类也必须提供这些抽象方法的实现)。以Dog类为例,bark方法有完整的实现逻辑,用于输出狗叫的信息。

抽象类则不同,它可以包含具体方法和抽象方法。具体方法为所有子类提供了通用的实现,如Animal类中的eat方法。而抽象方法则强制子类必须提供具体的实现,这使得子类在继承抽象类时需要根据自身的特性来实现这些抽象方法。例如,我们定义Cat类继承自Animal抽象类:

public class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }

    @Override
    public void makeSound() {
        System.out.println(getName() + " is meowing.");
    }
}

Cat类中,必须实现Animal抽象类中的makeSound抽象方法,以符合Java的规则。如果Cat类没有实现makeSound方法,那么Cat类也必须被声明为抽象类。

继承关系与设计目的

具体类通常用于实现具体的业务逻辑和功能,它们可以继承自其他具体类或抽象类。当一个具体类继承自另一个具体类时,它可以复用父类的属性和方法,并根据需要进行扩展或重写。例如,如果我们有一个Mammal具体类,Dog类可以继承自Mammal类,并添加一些狗特有的属性和方法。

抽象类主要用于定义一种通用的抽象概念或框架,它作为其他类的基类,为子类提供一个统一的接口和部分实现。抽象类的设计目的是为了提高代码的复用性和可维护性。通过将一些通用的属性和方法放在抽象类中,子类可以继承这些内容,避免重复编写代码。例如,Animal抽象类定义了所有动物共有的属性和行为(如name属性和eat方法),不同的动物子类(如DogCat等)继承自Animal类,并根据自身特点实现抽象方法(如makeSound方法)。

多态性体现差异

多态性是Java的重要特性之一,具体类和抽象类在多态性方面也有不同的体现。对于具体类,多态性主要通过方法重写和对象的向上转型来实现。例如:

class Shape {
    public void draw() {
        System.out.println("Drawing a shape.");
    }
}

class Circle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle.");
    }
}

class Rectangle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle.");
    }
}

public class Main {
    public static void main(String[] args) {
        Shape shape1 = new Circle();
        Shape shape2 = new Rectangle();

        shape1.draw();
        shape2.draw();
    }
}

在上述代码中,CircleRectangle类继承自Shape具体类,并重写了draw方法。通过将CircleRectangle类的对象向上转型为Shape类型,我们可以根据对象的实际类型来调用相应的draw方法,体现了多态性。

对于抽象类,多态性同样通过子类对抽象方法的实现和向上转型来实现,但由于抽象类本身不能被实例化,它更多地作为一种抽象类型来使用。例如:

abstract class Vehicle {
    public abstract void move();
}

class Car extends Vehicle {
    @Override
    public void move() {
        System.out.println("Car is moving on the road.");
    }
}

class Boat extends Vehicle {
    @Override
    public void move() {
        System.out.println("Boat is moving on the water.");
    }
}

public class Main {
    public static void main(String[] args) {
        Vehicle vehicle1 = new Car();
        Vehicle vehicle2 = new Boat();

        vehicle1.move();
        vehicle2.move();
    }
}

在这个例子中,Vehicle是抽象类,CarBoat类继承自Vehicle并实现了move抽象方法。通过向上转型,我们可以以Vehicle类型来调用不同子类的move方法,展示了多态性。但需要注意的是,我们不能创建Vehicle类的实例,只能使用其子类的实例来体现多态性。

内存分配差异

当创建具体类的实例时,Java虚拟机(JVM)会为该对象分配内存空间,包括对象的属性和方法。例如,当创建Dog类的实例时:

Dog dog = new Dog("Rex");

JVM会在堆内存中为dog对象分配空间,存储name属性的值,并且该对象的方法(如bark方法)的代码存储在方法区,通过对象的引用可以调用这些方法。

而对于抽象类,由于它不能被实例化,JVM不会为抽象类直接分配对象内存。但当抽象类的子类被实例化时,JVM会为子类对象分配内存,并且会包含从抽象类继承的属性和方法(如果有具体方法的话)。例如,当创建Cat类的实例时:

Cat cat = new Cat("Whiskers");

JVM为cat对象分配内存,其中包括从Animal抽象类继承的name属性以及eat具体方法和makeSound方法(虽然makeSound方法在Animal类中是抽象的,但在Cat类中已经实现)。

序列化与反序列化差异

在Java中,序列化是将对象转换为字节流以便存储或传输的过程,反序列化则是将字节流恢复为对象的过程。具体类如果要支持序列化,需要实现Serializable接口。例如:

import java.io.Serializable;

public class Employee implements Serializable {
    private String name;
    private int age;

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

Employee具体类实现了Serializable接口,就可以进行序列化和反序列化操作。

对于抽象类,由于它不能被实例化,通常情况下不会直接进行序列化。但如果抽象类的子类实现了Serializable接口,那么子类对象在序列化时会包含从抽象类继承的属性和状态。例如:

import java.io.Serializable;

abstract class Person {
    protected String name;

    public Person(String name) {
        this.name = name;
    }
}

class Student extends Person implements Serializable {
    private int studentId;

    public Student(String name, int studentId) {
        super(name);
        this.studentId = studentId;
    }
}

在上述代码中,Student类继承自Person抽象类并实现了Serializable接口。当对Student对象进行序列化时,name属性(从Person抽象类继承)和studentId属性都会被序列化。

访问修饰符使用差异

具体类可以使用各种访问修饰符,如publicprivateprotected和默认(包访问权限)。public修饰的具体类可以被任何其他类访问,private修饰的类只能在其所在的类内部被访问(这种情况很少见,通常用于内部类),protected修饰的类可以被同一包内的类以及子类访问,默认访问权限的类只能被同一包内的类访问。

对于抽象类,同样可以使用这些访问修饰符。但需要注意的是,抽象类中的抽象方法的访问修饰符不能是private,因为private修饰的方法不能被子类访问,而抽象方法的目的就是让子类实现。例如:

abstract class AbstractClass {
    public abstract void publicAbstractMethod();
    protected abstract void protectedAbstractMethod();
    // 以下代码会导致编译错误
    // private abstract void privateAbstractMethod();
}

在上述代码中,publicAbstractMethodprotectedAbstractMethod是合法的抽象方法声明,但如果将抽象方法声明为private,编译器会报错。

反射机制下的差异

Java的反射机制允许程序在运行时获取类的信息并操作类的成员。对于具体类,通过反射可以获取类的构造函数、方法、字段等信息,并创建对象、调用方法、访问字段等。例如:

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflectionExample {
    public static void main(String[] args) throws Exception {
        Class<Dog> dogClass = Dog.class;

        // 获取构造函数并创建对象
        Constructor<Dog> constructor = dogClass.getConstructor(String.class);
        Dog dog = constructor.newInstance("Sparky");

        // 获取方法并调用
        Method barkMethod = dogClass.getMethod("bark");
        barkMethod.invoke(dog);

        // 获取字段并访问
        Field nameField = dogClass.getDeclaredField("name");
        nameField.setAccessible(true);
        String dogName = (String) nameField.get(dog);
        System.out.println("Dog's name is: " + dogName);
    }
}

在上述代码中,通过反射获取Dog具体类的构造函数创建对象,获取方法并调用,以及获取字段并访问。

对于抽象类,通过反射同样可以获取其构造函数(如果有非私有构造函数,虽然抽象类不能直接实例化,但构造函数可用于子类实例化时调用父类构造函数)、方法、字段等信息。但由于抽象类不能被实例化,无法像具体类那样直接通过反射创建对象。例如:

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class AbstractReflectionExample {
    public static void main(String[] args) throws Exception {
        Class<Animal> animalClass = Animal.class;

        // 获取构造函数
        Constructor<Animal> constructor = animalClass.getConstructor(String.class);
        // 以下代码会导致运行时异常,因为Animal是抽象类不能实例化
        // Animal animal = constructor.newInstance("Invalid attempt");

        // 获取方法
        Method eatMethod = animalClass.getMethod("eat");
        // 由于无法创建Animal实例,不能直接调用eatMethod.invoke(animal)

        // 获取字段
        Field nameField = animalClass.getDeclaredField("name");
        nameField.setAccessible(true);
        // 同样由于无法创建Animal实例,不能直接访问nameField.get(animal)
    }
}

在上述代码中,虽然可以通过反射获取Animal抽象类的构造函数、方法和字段,但由于抽象类不能被实例化,无法像具体类那样进行完整的反射操作。

应用场景差异

具体类适用于实现具体的业务逻辑和功能。例如,在一个电商系统中,Product类可以是一个具体类,用于表示商品的各种属性(如名称、价格、库存等)和行为(如计算总价、更新库存等)。具体类可以直接创建对象并使用,为系统提供实际的功能支持。

抽象类适用于定义一些通用的概念或框架,为子类提供统一的接口和部分实现。例如,在图形绘制系统中,Shape抽象类可以定义一些通用的属性(如颜色、位置等)和抽象方法(如draw方法),具体的图形类(如CircleRectangle等)继承自Shape抽象类并实现draw方法。这样可以提高代码的复用性和可维护性,当需要添加新的图形时,只需要创建一个新的子类继承自Shape抽象类并实现相应的抽象方法即可。

又如,在一个游戏开发中,Character抽象类可以定义角色的通用属性(如生命值、攻击力等)和抽象方法(如moveattack等),不同类型的角色(如WarriorMage等)继承自Character抽象类并根据自身特点实现这些抽象方法。

总结两者本质区别

从本质上来说,具体类是具体功能的实现载体,它可以直接创建对象并用于实际的业务逻辑处理。具体类的所有方法都有完整的实现,以满足具体的需求。

而抽象类是一种抽象概念的体现,它不能被实例化,主要作用是为子类提供一个通用的框架。抽象类可以包含抽象方法,强制子类根据自身特性进行实现,从而实现代码的复用和多态性。抽象类更侧重于定义一种规范或接口,让子类去遵循和扩展。

通过深入理解Java中抽象类与具体类的区别,开发者可以更合理地设计和组织代码,提高代码的质量和可维护性,充分发挥Java面向对象编程的优势。无论是在小型项目还是大型企业级应用中,正确使用抽象类和具体类都是构建健壮、可扩展软件系统的关键。