JAVA从零开始11_面向对象的进一步理解

本文详细介绍了Java中的面向对象特性,包括static关键字的作用,如静态变量、方法和内部类的使用场景;封装的概念,通过工具类展示了封装的意义和实现;this关键字的用法,以及在非静态方法中的作用;继承的概念,解释了继承的特性、内存中的体现以及super关键字的使用;最后讨论了继承关系中的访问特性,解析了不同访问修饰符在继承中的影响。文章旨在帮助读者深入理解Java面向对象编程的核心概念。
摘要由CSDN通过智能技术生成

一、static关键字

在之前已经讲解了面向对象的基本概念http://t.csdn.cn/klGR4,现在进一步讲解封装的概念

首先要了解static关键字

static 关键字主要用于创建与类本身而非实例相关的成员,在 Java 中有很多用途,它可以修饰变量、方法、内部类和代码块。
以下是 static 关键字的用途和使用场景:

  1. 静态变量(类变量):当一个变量被声明为 static 时,它变成了一个静态变量。这意味着它不再属于类的任何实例,而是属于类本身。这样的变量在内存中只有一个副本,所有类的实例共享该变量。静态变量通常用于存储跟类相关的全局数据,而不是实例相关的数据。

    使用场景:例如,如果你有一个类表示银行账户,可以使用静态变量来存储创建的账户总数。

  2. 静态方法:当一个方法被声明为 static 时,它变成了一个静态方法。这意味着它可以在不创建类实例的情况下直接通过类名调用。静态方法不能访问类的非静态成员变量,因为它们不依赖于类的实例。

    使用场景:例如,实用程序类(如 Math 类)中的方法通常是静态的,因为它们只执行一些独立的操作,而不涉及特定对象的状态。

  3. 静态内部类:静态内部类与非静态内部类的主要区别在于它不持有对外部类的实例的引用。这使得静态内部类可以在没有外部类实例的情况下独立存在。静态内部类通常用于创建与外部类关联的辅助类,但不依赖于外部类的实例状态。

    使用场景:例如,在实现数据结构时,可以使用静态内部类表示节点。

  4. 静态代码块:静态代码块是在类加载时执行的一段代码。它通常用于初始化静态成员变量或执行类相关的一次性设置操作。

    使用场景:例如,可以使用静态代码块加载配置文件或初始化类级别的资源。

在内存中,静态成员(变量、方法和静态内部类)与非静态成员有不同的存储和管理方式。

  1. 静态变量:静态变量在内存中只有一个副本,它们存储在 Java 虚拟机(JVM)的方法区(Java 8 及之前的版本称为永久代,Java 8 之后称为元空间)。当一个类被加载到 JVM 时,静态变量就会被分配内存空间。所有类的实例共享这些静态变量,而不是每个实例都有自己的副本。这意味着更少的内存消耗,因为只需要为静态变量分配一次内存。

  2. 静态方法:静态方法也存储在 JVM 的方法区中。与静态变量一样,静态方法在类被加载时分配内存空间。静态方法可以直接通过类名调用,而不需要创建类的实例。因为静态方法没有与特定实例关联,所以它们不能访问非静态成员变量。静态方法主要用于实现与类相关的功能,而不是与实例相关的功能。

  3. 静态内部类:静态内部类与外部类的静态成员一样,存储在方法区中。它们可以独立于外部类实例存在。静态内部类不持有对外部类实例的引用,这意味着它们不能访问外部类的非静态成员。

  4. 非静态成员:与静态成员相反,非静态成员(变量和方法)与类的实例关联。每个实例在堆内存中都有自己的一份非静态成员副本。当创建一个新的实例时,JVM 会在堆内存中为非静态成员分配空间。非静态方法可以访问实例的非静态和静态成员。

总的来说,静态成员(包括静态变量、静态方法和静态内部类)存储在 Java 虚拟机(JVM)的方法区中。方法区不属于堆内存或栈内存。非静态成员变量存储在堆内存中。每个类的实例都有自己的非静态成员变量副本,这些变量存储在堆内存中的实例对象内。非静态方法的字节码(即编译后的代码)存储在方法区中。但是,在执行非静态方法时,局部变量和方法调用的参数会存储在栈内存中。对于每次方法调用,栈内存中都会为这些局部变量和参数分配单独的空间。

在使用静态方法时,应注意以下几点:

  1. 无法访问非静态成员:静态方法不能访问类的非静态成员(属性和方法),因为非静态成员需要通过类的实例进行访问,而静态方法在没有创建类实例的情况下就可以被调用。

  2. 不能被重写:静态方法不具备多态性,因此不能在子类中被重写。当然,子类可以定义与父类相同的静态方法,但这不是重写,而是隐藏了父类的方法。

  3. 不适用于实例相关的操作:如果一个方法需要访问和操作实例的状态,它不应该被声明为静态方法。静态方法更适合那些不依赖于实例状态的操作。

  4. 线程安全问题:静态方法在多线程环境下可能会遇到线程安全问题,因为多个线程可能同时访问静态方法。当静态方法访问共享资源或修改共享状态时,需要确保线程安全,例如通过使用synchronized关键字或其他同步机制。

  5. 避免过多使用静态方法:过多地使用静态方法可能导致代码变得难以维护和测试。静态方法破坏了面向对象编程的原则,因为它们不属于任何实例,而且不能轻易地在运行时替换或扩展。在适当的情况下,考虑使用实例方法和面向对象的设计。

  6. 避免静态方法产生副作用:尽量确保静态方法是无副作用的,即不修改任何全局状态。这有助于提高代码的可测试性和可维护性。

那对于静态方法,可以总结为:1. 静态方法中没有this关键字 2. 静态方法只能访问静态 3. 非静态方法可以访问所有

  1. 静态方法中没有this关键字:由于静态方法属于类而非类的实例,因此它们不依赖于任何特定实例的状态。在静态方法中尝试使用this关键字会导致编译错误。

  2. 静态方法只能访问静态:静态方法只能访问所属类的静态成员(包括静态变量和静态方法)。它们无法访问非静态成员,因为非静态成员依赖于实例的状态,而静态方法不依赖于实例。

  3. 非静态方法可以访问所有:非静态方法可以访问所属类的所有成员(包括静态和非静态)。这是因为非静态方法依赖于实例的状态,因此它们可以访问实例的非静态成员以及类的静态成员。

从内存角度来理解这三点:

  1. 静态方法中没有this关键字:静态方法属于类,它们在类被加载时加载到方法区(JDK 8及之前)或元空间(JDK 9及之后),并且对于同一个类的所有实例共享。由于静态方法不依赖于特定实例,它们无法访问实例存储在堆内存中的状态。因此,静态方法中没有this关键字。

  2. 静态方法只能访问静态:静态成员(如静态变量和静态方法)与静态方法一样,位于方法区(JDK 8及之前)或元空间(JDK 9及之后)。因为它们都位于相同的内存区域,静态方法可以直接访问静态成员。然而,非静态成员存储在实例的堆内存中,它们的访问需要通过具体实例的引用。由于静态方法没有与任何实例关联,它们无法访问非静态成员。

  3. 非静态方法可以访问所有:非静态方法与实例关联,因此它们可以访问实例存储在堆内存中的状态(即非静态成员)。同时,非静态方法也可以访问静态成员,因为静态成员位于方法区(JDK 8及之前)或元空间(JDK 9及之后),对所有实例共享,不受实例的影响。


二、封装的具体实现

工具类(Utility Class)是一种特殊类型的类,它通常包含一组静态方法,这些方法用于执行与其所属领域相关的常见操作。工具类的设计目的是为了将常用的功能封装起来,以便在多个地方重复使用,从而避免代码重复和提高代码可维护性。

工具类的特点如下:

  1. 只包含静态方法:工具类通常只包含静态方法,因为静态方法可以直接通过类名调用,而无需创建类的实例。

  2. 无法实例化:工具类通常被设计成无法实例化,因为它们的目的是提供一组功能,而不是表示特定的对象状态。通常通过将构造方法声明为私有来实现这一点。

  3. 无需维护状态:由于工具类通常不包含实例变量,因此它们不需要维护任何状态。

  4. 通常是不可继承的:工具类通常被声明为final,以防止它们被继承,因为它们的目的是提供一组功能,而不是作为其他类的基类。

Java中的一些常见工具类包括:

  1. java.util.Arrays:提供了用于操作数组的工具方法,例如排序、查找和比较数组。
  2. java.util.Collections:提供了用于操作集合(例如List、Set)的工具方法,例如排序、查找和比较集合。
  3. java.lang.Math:提供了一组用于执行数学运算(例如三角函数、对数、指数和取整)的静态方法。
  4. java.util.Objects:提供了一组用于操作对象的工具方法,例如比较对象、计算哈希值和生成字符串表示。
  5. java.nio.file.Files:提供了一组用于操作文件系统中的文件和目录的工具方法。

JavaBean类和工具类都是面向对象封装概念的具体实现。这两者都将数据和操作封装在类中,但它们的用途和使用方式有所不同。

  1. JavaBean类:JavaBean通常是一种用于封装数据的简单类。JavaBean类的主要特点是具有私有属性(用于存储数据)和公有的getter和setter方法(用于访问和修改这些属性)。通过封装数据和提供访问数据的方法,JavaBean类实现了封装原则,确保数据的完整性和安全性。JavaBean通常用于表示实体或数据传输对象(DTO),在多层架构中,负责在各个层之间传递数据。

  2. 工具类:工具类是一种封装了一组静态方法的类,这些方法通常执行一些通用的、与特定对象无关的任务。工具类的主要目的是提供方便的方法来处理一些常见的操作,如字符串处理、数学计算等。工具类中的静态方法通常不需要实例化类就可以使用,它们实现了面向对象的封装原则,将相关功能封装在一个类中,使代码更加模块化和易于维护。

总之,JavaBean类和工具类都是面向对象封装概念的具体体现。JavaBean类主要用于封装数据和实现数据的安全访问,而工具类用于封装一组与特定对象无关的静态方法,提供方便的功能。

封装主要目的是将数据(成员变量)和操作数据的方法(成员方法)捆绑在一起,从而隐藏对象内部的实现细节。这使得类的使用者只需要关心类提供的功能(通过公共方法提供),而不需要知道类内部的具体实现。封装有助于提高代码的可维护性和可复用性,同时也可以保护对象内部数据的完整性。

对于封装我们可以理解为:对象代表什么,就需要封装对应的数据,并提供数据对应的行为


三、this关键字

this 关键字指代当前对象。在方法中,可以用 this 引用调用该方法的对象。

使用场景:

  1. 区分成员变量和局部变量(例如,在方法参数和成员变量重名时)。
  2. 调用当前类的其他构造方法(必须放在构造方法的第一行)。
  3. 调用当前类的方法或访问当前类的成员变量。

特点:

  1. 只能在非静态方法中使用。
  2. 可以链式调用(如 this.setX(1).setY(2))。

在静态方法中,无法使用this关键字,原因如下:

  1. 静态方法与类实例无关:静态方法属于类,而不是类的实例。它们可以在没有创建类的实例的情况下被调用。因此,静态方法不依赖于任何特定实例的状态。

  2. this关键字表示当前实例:this关键字在非静态方法中用于引用当前实例。由于静态方法不属于任何实例,所以没有当前实例可供引用。在静态方法中尝试使用this关键字会导致编译错误。

  3. 静态方法不具备多态性:静态方法在编译时确定,而不是运行时。这意味着静态方法不具备多态性,无法被子类重写。this关键字的主要用途之一是实现多态性,但在静态方法中这一点并不适用。

由于静态方法与类实例无关,没有当前实例可供引用,因此在静态方法中无法使用this关键字。

而在非静态方法中,this关键字是隐式地存在的。它代表当前实例,即调用该非静态方法的对象。当您在非静态方法中访问成员变量或调用其他非静态方法时,实际上是通过this关键字来访问和调用的。

例如,假设有以下类:

public class MyClass {
    private int value;

    public void setValue(int value) {
        this.value = value;
    }

    public int getValue() {
        return value; // 实际上是访问 this.value
    }
}

在setValue和getValue方法中,当访问value时,实际上是通过隐式的this关键字访问this.value。这意味着您正在访问当前实例的value属性。如果您在这些方法中显式地使用this关键字,代码的行为是相同的。
上面这段代码也等同于下面,它将this关键字作为方法参数显式地传递给setValue和getValue方法:

public class MyClass {
    private int value;

    public void setValue(MyClass this,int value) {
        this.value = value;
    }

    public int getValue(MyClass this) {
        return value; // 实际上是访问 this.value
    }

所以,可以认为在非静态方法中,this关键字是隐式地存在的。


四、继承的概念

继承是面向对象编程的一个核心概念,它允许一个类(子类)从另一个类(父类)继承属性和方法。继承的主要目的是实现代码的复用和模块化,它有助于减少重复代码、提高代码的可维护性,同时也体现了面向对象编程的一种抽象思维。

以下是继承的一些特点:

  1. 子类继承了父类的所有属性和方法(除了构造方法),子类可以直接使用这些属性和方法(如果访问权限允许)。
  2. 子类可以覆盖或扩展父类的方法,以实现更具体或不同的功能。
  3. 子类可以添加新的属性和方法,以实现更多的功能。
  4. 子类具有父类的类型,可以作为父类的实例使用。这种特性称为“多态”。

继承的使用场景和实际运用:

  1. 当存在多个类具有相同的属性和方法时,可以将这些共享的属性和方法提取到一个公共的父类中,然后让这些类继承自该父类。这有助于减少重复代码,并提高代码的可维护性。
  2. 当需要对一个类进行扩展或定制时,可以通过创建一个继承自该类的子类来实现。这样,子类可以覆盖或扩展父类的方法,以实现不同的功能,同时保留父类的原有功能。
  3. 在实际项目中,继承经常用于创建具有层次结构的类。例如,在一个图形界面库中,可能有一个基本的“图形”类,具有一些共享的属性和方法,然后有若干个继承自“图形”类的具体图形类,如“矩形”、“圆形”等,这些具体图形类可以添加或覆盖父类的方法,以实现各自特有的功能。

在内存中,继承主要涉及到两个方面:方法区(Method Area)和堆区(Heap)。

  1. 方法区:当子类被加载到内存时,它的类定义信息(包括继承自父类的方法和属性)将存储在方法区。子类和父类的方法区中都包含了各自的成员方法的字节码。子类会继承父类的成员方法(除了私有方法),所以子类的方法区中也会包含对父类方法的引用。需要注意的是,子类只继承父类的成员方法引用,而不是成员方法的实际字节码。

  2. 堆区:当创建子类对象时,堆区中将分配一块内存来存储该对象。子类对象的内存结构包括子类自己的成员变量和继承自父类的成员变量。在堆区中,子类对象会有一个指向父类对象的引用,这样子类对象就可以访问父类中定义的成员变量和方法。

在内存中,继承体现在子类对象可以访问父类对象的成员变量和成员方法。子类的方法区中包含对父类方法的引用,堆区中的子类对象包含子类自身的成员变量和继承自父类的成员变量。通过这种方式,子类对象可以在内存中访问和操作父类的成员。

从内存的角度来看,当子类对象被创建时,子类会继承父类的以下内容:

  1. 非私有成员变量(包括公共、受保护和默认访问权限的成员变量):子类对象在内存中会为这些变量分配空间,并且可以直接访问和修改它们。

  2. 私有成员变量:虽然子类不能直接访问私有成员变量,但在内存中,子类对象仍然会为这些私有变量分配空间。子类可以通过调用父类提供的 getter 和 setter 方法(如果有的话)间接地访问和修改这些私有成员变量。

  3. 非私有方法(包括公共、受保护和默认访问权限的方法):子类可以继承并直接调用这些方法。在内存中,子类对象的虚方法表会包含这些方法的入口地址。如果子类重写了这些方法,那么虚方法表中将存储子类重写的方法的地址。

总之,在内存中,子类对象会包含父类的所有成员变量(无论是否可访问)和非私有方法。子类可以直接访问和使用非私有成员,但是对于私有成员,子类只能通过间接方式(如父类提供的 getter 和 setter 方法)进行访问。

需要注意的是,过度使用继承可能导致类结构复杂、难以维护。因此,在实际项目中,应适当使用继承,并结合接口、组合等其他方式来设计类结构。


五、super关键字和方法重写

super 关键字在 Java 中用于访问父类的成员(方法、变量、构造函数)。在子类中,当你需要调用父类的方法、访问父类的成员变量或者调用父类的构造函数时,可以使用 super 关键字。

方法重写(Override)是指子类实现了一个与父类签名相同的方法。当你在子类中创建一个与父类方法具有相同方法名和参数列表的方法时,子类方法就会覆盖父类的方法。方法重写允许子类修改或扩展父类的行为。

以下是 super 关键字和方法重写的示例:

class Animal {
    void makeSound() {
        System.out.println("The animal makes a sound");
    }
}

class Dog extends Animal {
    void makeSound() {
        System.out.println("The dog barks");
    }

    void makeSoundAndParentSound() {
        super.makeSound(); // 调用父类(Animal)的 makeSound() 方法
        makeSound(); // 或者 this.makeSound(); 调用当前类(Dog)的 makeSound() 方法
    }
}

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

输出:

The animal makes a sound
The dog barks

在这个例子中,我们有一个基类 Animal 和一个继承自 Animal 的子类 Dog。Dog 类重写了 makeSound() 方法,以输出特定于狗的信息。Dog 类还定义了一个名为 makeSoundAndParentSound() 的方法,它使用 super 关键字来调用父类 Animal 的 makeSound() 方法,然后调用自己的 makeSound() 方法。

方法重写,也称为覆盖(override),是子类继承父类时对父类方法的重新实现。方法重写有以下几点要求和注意事项:

  1. 方法签名必须相同:子类中重写的方法必须与父类中被重写的方法具有相同的方法名、参数列表和返回类型。

  2. 访问权限不能降低:子类中重写的方法的访问权限不能比父类中被重写的方法的访问权限更严格。例如,如果父类方法是 public,则子类重写的方法也必须是 public。

  3. 抛出的异常不能扩大:子类中重写的方法抛出的异常不能比父类中被重写的方法抛出的异常范围更广泛。换句话说,子类方法所抛出的异常类型必须是父类方法抛出的异常类型的子类或相同的类型。

  4. 不能重写 final 方法:被声明为 final 的方法是不允许被重写的,因为 final 方法是不可修改的。

  5. 不能重写静态方法:静态方法与类相关,而不是与实例相关,因此不能被子类重写。如果子类中定义了与父类静态方法相同签名的方法,这实际上是子类中的一个新的静态方法,而不是重写父类的静态方法。

  6. 构造方法不能被重写:构造方法是类的特殊方法,用于创建和初始化对象。它们不能被继承,因此也不能被重写。

  7. 遵循里氏替换原则:当使用子类对象替换父类对象时,程序的行为应该保持不变。子类中重写的方法应确保不破坏父类方法的行为和契约。

遵循这些要求和注意事项可以确保方法重写的正确实现,从而使得子类可以自然地继承和扩展父类的功能。

方法重写的使用场景和意义:

  1. 多态:方法重写是多态的一个关键部分。多态允许程序以统一的接口处理不同类型的对象。通过重写父类方法,子类可以改变或扩展父类的行为,从而使得父类引用可以指向子类对象,并在运行时调用子类的实现。这使得代码更灵活、易于维护和扩展。

  2. 代码重用:通过继承,子类可以复用父类的方法和属性。方法重写使得子类可以在保留父类方法的基本结构和功能的同时,自定义或改进某些部分。这有助于减少重复代码,并提高代码的可维护性。

  3. 封装:方法重写有助于封装特定于子类的实现细节。子类可以根据自己的需求对父类方法进行重写,而不需要修改父类的代码。这有助于遵循“开放-封闭原则”,即对扩展开放,对修改封闭。

为什么要进行方法重写而不是创建一个新的方法:

  1. 一致性:通过重写父类方法,子类可以保持与父类相同的方法签名和语义。这有助于提高代码的一致性和可读性,使得使用子类和父类更加直观。

  2. 替换原则:方法重写符合里氏替换原则(Liskov Substitution Principle),即子类型必须能够替换它们的基类型。当子类重写父类方法时,客户端代码可以在不知道具体子类类型的情况下,通过父类引用来调用方法。这使得代码更加灵活,易于扩展和维护。

  3. 避免名称冲突:如果子类创建一个与父类相似功能的新方法,可能会导致方法名称冲突和混乱。通过重写父类方法,子类可以清楚地表示它们之间的关系,并避免不必要的名称冲突。

总之,方法重写是面向对象编程中一个重要的概念,它有助于实现多态、代码重用、封装等关键特性,并提高代码的一致性、灵活性和可维护性。

this 和 super 关键字在 Java 中具有不同的含义和用途:

this 关键字:

  1. 指代当前对象。在方法中,可以用 this 引用调用该方法的对象。
    使用场景:
    区分成员变量和局部变量(例如,在方法参数和成员变量重名时)。
    调用当前类的其他构造方法(必须放在构造方法的第一行)。
    调用当前类的方法或访问当前类的成员变量。

  2. 特点:
    只能在非静态方法中使用。
    可以链式调用(如 this.setX(1).setY(2))。

  3. super 关键字:

    指代当前对象的父类。在子类中,可以用 super 引用父类的方法和成员变量。
    使用场景:
    调用父类的构造方法(必须放在子类构造方法的第一行)。
    调用父类的方法或访问父类的成员变量。
    在方法重写时,调用父类的被重写方法。

  4. 特点:
    只能在非静态方法中使用。

在构造方法中,this 和 super 的实际意义:

  1. this:

    当一个类有多个构造方法时,可以使用 this 关键字在构造方法中调用其他构造方法,以避免代码重复。
    使用 this 调用其他构造方法必须放在构造方法的第一行。

  2. super:
    子类的构造方法需要初始化父类的成员变量,使用 super 调用父类的构造方法可以实现这一目的。
    使用 super 调用父类构造方法必须放在子类构造方法的第一行。如果没有显式调用,编译器会自动插入对父类无参构造方法的调用。


六、继承关系中的访问特性

在继承中,成员变量、成员方法和构造方法的访问特性如下:

  1. 成员变量:

    public:可以在任何地方访问,包括类外部、子类和同一包内的其他类。
    protected:可以在同一包内的其他类和子类中访问,但不能在类外部访问。
    default(没有访问修饰符):仅在同一包内的类和子类中可以访问,不能在类外部访问。
    private:仅在类内部可以访问,子类无法访问。

  2. 成员方法:

    public:可以在任何地方访问,包括类外部、子类和同一包内的其他类。
    protected:可以在同一包内的其他类和子类中访问,但不能在类外部访问。
    default(没有访问修饰符):仅在同一包内的类和子类中可以访问,不能在类外部访问。
    private:仅在类内部可以访问,子类无法访问。

  3. 构造方法:

    public:可以在任何地方创建类的实例。
    protected:可以在同一包内的其他类和子类中创建类的实例,但不能在类外部创建实例。
    default(没有访问修饰符):仅在同一包内的类和子类中可以创建类的实例,不能在类外部创建实例。
    private:仅在类内部可以创建实例,通常用于实现单例模式或限制类的实例化。

注意:构造方法不能被子类继承,子类需要定义自己的构造方法。不过,子类可以通过 super() 调用父类的构造方法来初始化父类的成员变量。

继承的主要目的是实现代码复用和扩展。通过继承,子类可以继承父类的属性和方法,从而减少代码的重复编写。同时,继承也允许子类对父类的行为进行扩展和定制,使得类之间具有更强的关联性。继承有助于提高代码的可维护性和可扩展性,同时也实现了类型的层次化。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值