Skip to content

类和对象

在 Java 语言中,类是一种抽象的数据类型,是对一类事物的抽象描述。对象是类的实例,是类的具体实现。

面向对象概述

面向对象相较于面向过程,是两种解决问题的不同角度。面向过程注重于解决问题的步骤及顺序,而面向对象则更注重于解决问题的参与者、各自需要做什么。

面向对象的三大特征分别是:封装、继承和多态。

封装

封装是一种信息隐蔽技术,它的目的是使对象的使用者和生产者分开,使对象的定义和实现分离。

封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。要访问该类的代码和数据,必须通过严格的接口限制。

封装最主要的功能在于我们能够修改自己实现的代码,而不用修改那些调用我们代码的程序片段。适当的封装可以让程序代码更容易理解与维护,也加强了程序的安全性。

25021205.png

图 封装的特性的示意图

实现封装的步骤:

  1. 修改属性的可见性来限制对属性的访问(一般限制为 private)。例如下方的代码中,将 nameage 属性设置为私有的,只能本类才能访问,其他类都访问不了,如此就对信息进行了隐藏。

    java
    public class Person {
        private String name;
        private int age;
    }
  2. 对每个值属性提供对外的公共方法访问,也就是创建一对赋取值方法,用于对私有属性的访问。例如:采用 this 关键字是为了解决实例变量(private String name)和局部变量(setName(String name) 中的 name 变量)之间发生的同名的冲突。

    java
    public class Person {
        private String name;
        private int age;
    
        public int getAge() {
          return age;
        }
    
        public String getName() {
          return name;
        }
    
        public void setAge(int age) {
          this.age = age;
        }
    
        public void setName(String name) {
          this.name = name;
        }
    }

紧接着就可以访问 Person 类中的属性了:

java
public class Main {
    public static void main(String[] args) {
        Person person = new Person();
        person.setName("Alice");
        person.setAge(30);
        System.out.println("Name: " + person.getName());
        System.out.println("Age: " + person.getAge());
    }
}

继承

继承在面向对象思想中是一个非常重要的概念,是父类和子类之间共享数据和方法的机制。基本思想是指一个类可以派生出一个或多个子类,子类可以继承父类的属性和方法,也可以重写父类的方法,或者增加新的方法。子类继承父类的方法,并做出自己的改变和扩展。

在程序中复用一些已经定义完善的类不仅可以减少软件开发周期,还可以提高软件的可维护性和可扩展性。

25021206.png

图 图形类层次结构示意图

在 Java 不支持多继承,但支持多重继承。

25021902.png

继承的特性:

  • 子类拥有父类非 private 的属性、方法。
  • 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
  • 子类可以用自己的方式实现父类的方法。
  • Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 B 类继承 A 类,C 类继承 B 类,所以按照关系就是 B 类是 C 类的父类,A 类是 B 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。
  • 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。

在下方的示例中,Dog 类继承了 Animal 类,因此 Dog 类的实例可以调用 Animal 类的 eat() 方法。

java
// 父类
class Animal {
    void eat() {
        System.out.println("This animal eats food.");
    }
}

// 子类继承父类
class Dog extends Animal {
    void bark() {
        System.out.println("The dog barks.");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog myDog = new Dog();
        myDog.eat();  // 继承自父类的方法
        myDog.bark(); // 子类自己的方法
    }
}

25021401.png

图 Animal 与 Dog 类之间的继承关系

需要注意的是,当重写父类的方法时,修改方法的权限修饰符不能降低权限,但可以提高权限。当修改方法返回值类型时,返回值类型必须是父类方法返回值类型的子类。

在实例化子类对象时,会先调用父类的构造方法,然后再调用子类的构造方法。父类的有参构造不能被自动调用,只能使用 super 关键字显式调用父类的构造方法。

如果使用 finalize() 方法清理对象,则需要确保子类 finalize() 方法的最后一个操作是调用父类的 finalize() 方法。以确保父类的资源被正确释放,所有子类的资源也能被正确释放。

多态

对象之间进行通信的一种构造叫做消息。在收到消息时对象要予以响应,不同的对象在收到同一消息时可以产生不同结果,这一现象叫做多态。

多态(Polymorphism)是指同一个方法在不同的对象中有不同的实现。多态分为编译时多态(方法重载)和运行时多态(方法重写)。运行时多态是通过方法重写和向上转型实现的。

多态存在的三个必要条件:

  1. 继承:子类继承父类。
  2. 重写:子类重写父类方法。
  3. 父类引用指向子类对象:Parent p = new Child();

在下方的例子中,Animal 类的 sound() 方法在子类 DogCat 中被重写。通过向上转型,myDogmyCat 分别调用的是 DogCat 类中的 sound() 方法,这就是运行时多态的体现。

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

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Dog barks");
    }
}

class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("Cat meows");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Animal(); // Animal 对象
        Animal myDog = new Dog();      // Dog 对象
        Animal myCat = new Cat();     // Cat 对象

        myAnimal.sound(); // 输出: Animal makes a sound
        myDog.sound();    // 输出: Dog barks
        myCat.sound();    // 输出: Cat meows
    }
}

类是封装对象的属性和行为的载体,而对象的属性以成员变量的形式存在,对象的方法以成员方法的形式存在,属性和方法统称为类的成员。

一个类定义了一组大体上相似的对象。一个类所包含的数据和方法描述了这组对象共同的属性和行为。

在 Java 中,类中的对象的行为是以方法的形式定义的,对象的属性是以成员变量的形式定义的,所以类包括对象的属性和方法。

25021204.png

图 鸟类的结构

成员变量

对象的属性也称为成员变量,是类的一部分。成员变量定义在类中,方法体之外。成员变量可以是任何数据类型,包括基本数据类型和引用数据类型。

成员变量可以分为两种:

  • 实例变量:非 static 修饰的成员变量。
  • 类变量:static 修饰的成员变量。

例如下方的示例中,nameageheightPerson 类的成员变量:

java
public class Person {
    String name;
    int age;
    double height;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
}

成员方法

成员方法对应类对象的行为,定义在类中,成员方法可以访问类的成员变量。

例如下方的示例中,getName()setName()Person 类的成员方法:

java
public class Person {
    String name;
    int age;
    double height;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
}

权限修饰符

Java 中的类、成员变量和成员方法可以使用权限修饰符来限制访问权限。Java 中的权限修饰符有四种,分别是 publicprotectedprivate

  • public:公有的,可以被任何包中的任何类访问。
  • protected:受保护的,可以被同一个包内的类访问。
  • private:私有的,只能被同一个类访问。

25021207.png

表 Java 中的权限修饰符

局部变量

局部变量是在方法体中定义的变量,只在方法体中有效。局部变量在方法体中声明,方法体结束后局部变量的内存空间会被释放。

java
public class Person {
    public void print() {
        int age = 18;  // age 是局部变量
        System.out.println("年龄:" + age);
    }
}

局部变量有效范围

局部变量的有效范围(变量的作用域)是从声明开始到方法结束。局部变量的作用域是在声明的方法体内。

25021208.png

图 局部变量的有效范围

在相互不嵌套的两个方法中,可以使用相同的局部变量名,因为它们的作用域不同。

25021209.png

图 在相互不嵌套的区域可以定义相同名称和类型的局部变量

内部类

内部类是定义在另一个类中的类。根据定义的位置和方式,内部类可以分为以下几种:

  • 成员内部类
  • 局部内部类
  • 匿名内部类
  • 静态内部类

成员内部类

成员内部类是定义在类中的非静态类。

java
class Outer {
    private int outerField = 10;

    class Inner {
        void display() {
            System.out.println("Outer field: " + outerField);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.display();
    }
}

局部内部类

局部内部类是定义在方法中的类。

java
class Outer {
    void display() {
        class LocalInner {
            void show() {
                System.out.println("Local inner class.");
            }
        }
        LocalInner inner = new LocalInner();
        inner.show();
    }
}

public class Main {
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.display();
    }
}

匿名内部类

匿名内部类是没有名称的内部类,通常用于实现接口或继承类。

java
interface Greeting {
    void greet();
}

public class Main {
    public static void main(String[] args) {
        Greeting greeting = new Greeting() {
            @Override
            public void greet() {
                System.out.println("Hello from anonymous inner class.");
            }
        };
        greeting.greet();
    }
}

静态内部类

静态内部类是定义在类中的静态类。

java
class Outer {
    static class StaticInner {
        void display() {
            System.out.println("Static inner class.");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Outer.StaticInner inner = new Outer.StaticInner();
        inner.display();
    }
}

内部类的继承

内部类可以被继承,但需要注意外部类的引用。

java
class Outer {
    class Inner {
        void display() {
            System.out.println("Inner class.");
        }
    }
}

class ChildInner extends Outer.Inner {
    ChildInner(Outer outer) {
        outer.super();
    }
}

public class Main {
    public static void main(String[] args) {
        Outer outer = new Outer();
        ChildInner child = new ChildInner(outer);
        child.display();
    }
}

类的构造方法

构造方法是一种特殊的方法,用于创建对象。构造方法的名称与类名相同,没有返回值。

在构造方法中可以为成员变量赋值,这样当实例化对象时,对象的属性就有了初始值。

如果类中没有明确定义构造方法,编译器会自动提供一个无参构造方法。需要注意的是,如果类中定义了其他参数的构造方法,编译器就不会再自动提供无参构造方法。

类的主方法

主方法是 Java 程序的入口,是程序执行的起点。

语法如下:

java
public static void main(String[] args) {
    // 程序代码
}
  • 主方法是一个静态方法,因此在主方法中只能调用静态方法。
  • 主方法的修饰符是 public,返回值是 void,参数是一个字符串数组。
  • 主方法的形参 args 是一个字符串数组,用于接收命令行参数。

Object 类

在 Java 中,所有的类都直接或间接地继承自 Object 类。Object 类是 Java 类层次结构的根类,它提供了一些通用的方法,如 toString()equals()hashCode() 等。

在下方示例中,MyClass 类虽然没有显式地继承任何类,但它默认继承了 Object 类,因此可以调用 Object 类的 toString() 方法。

java
class MyClass {
    // 自定义类
}

public class Main {
    public static void main(String[] args) {
        MyClass obj = new MyClass();
        System.out.println(obj.toString()); // 调用 Object 类的 toString() 方法
    }
}

请注意,Object 类中的 getClass()notify()notifyAll()wait() 方法是 final 修饰的方法,不能被重写。

getClass() 方法

getClass() 方法返回对象运行时的 Class 实例。例如下面的示例中,调用 getName() 方法可以获取对象的类名。

java
class MyClass {
    // 自定义类
}

public class Main {
    public static void main(String[] args) {
        MyClass obj = new MyClass();
        System.out.println(obj.getClass().getName()); // 获取对象的类名
    }
}

toString() 方法

toString() 方法返回对象的字符串表示。默认情况下,toString() 方法返回的是对象的类名和哈希码的十六进制表示。在实际应用中,通常重写 toString() 方法以返回更有意义的字符串。

java
class MyClass {
    // 自定义类
    @Override
    public String toString() {
        return "This is a MyClass object.";
    }
}

public class Main {
    public static void main(String[] args) {
        MyClass obj = new MyClass();
        System.out.println(obj.toString()); // 调用重写的 toString() 方法
    }
}

equals() 方法

equals() 方法用于比较两个对象是否相等。默认情况下,equals() 方法比较的是两个对象的内容是否相等。

java
class MyClass {
    // 自定义类
    int value;

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

public class Main {
    public static void main(String[] args) {
        MyClass obj1 = new MyClass(10);
        MyClass obj2 = new MyClass(10);
        System.out.println(obj1.equals(obj2)); // 比较两个对象内容是否相等
    }
}

抽象类与接口

抽象类和接口是 Java 中实现多态和代码复用的两种重要机制。它们都可以用来定义规范,但它们在设计和使用上有一些关键区别。

抽象类

通常说四边形具有4条边,或者可以这样说,平行四边形是具有对边平行且相等的特殊四边形,等腰三角形是具有两边相等的特殊三角形。但对于一个叫 图形 的对象,却不能用具体的语言描述它是有几条边?究竟是什么图形?那么这样的类就需要被定义为抽象类。

抽象类是一种不能被实例化的类,通常用于作为其他类的基类。它可以包含抽象方法(没有实现的方法)和具体方法(有实现的方法)。抽象类的目的是为子类提供一个通用的模板,子类必须实现抽象方法。

抽象类的特点:

  • abstract 关键字声明。
  • 可以包含抽象方法和具体方法。
  • 可以包含成员变量、构造方法、静态方法等。
  • 子类必须实现抽象类中的所有抽象方法,除非子类也是抽象类。

在下方的示例中:

  • Animal 是一个抽象类,包含一个抽象方法 sound() 和一个具体方法 sleep()
  • Dog 类继承 Animal 并实现了 sound() 方法。
  • 抽象类不能被实例化,但可以通过子类对象调用其方法。
java
// 抽象类
abstract class Animal {
    // 抽象方法(没有方法体)
    abstract void sound();

    // 具体方法
    void sleep() {
        System.out.println("This animal sleeps.");
    }
}

// 子类继承抽象类
class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Dog barks.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myDog = new Dog(); // 向上转型
        myDog.sound(); // 调用子类实现的抽象方法
        myDog.sleep();  // 调用抽象类中的具体方法
    }
}

接口

接口是一种完全抽象的类,用于定义一组方法的规范。接口中的方法默认都是 public abstract 的(Java 8 之前),并且不能包含具体方法实现(Java 8 之后可以通过 default 方法实现)。接口的目的是定义行为规范,类通过实现这些接口来遵循规范。

接口的特点:

  • interface 关键字声明。
  • 方法默认是 public abstract 的(Java 8 之前)。
  • 不包含成员变量,但是可以包含常量(默认是 public static final 的)。
  • 类通过 implements 关键字实现接口,并且必须实现接口中的所有方法(除非是抽象类)。
  • 一个类可以实现多个接口(多继承)。

在下面的示例中:

  • Animal 是一个接口,定义了两个抽象方法 sound()sleep()
  • Dog 类实现了 Animal 接口,并提供了方法的具体实现。
  • 接口不能被实例化,但可以通过实现类的对象调用其方法。
java
// 定义接口
interface Animal {
    void sound(); // 抽象方法
    void sleep(); // 抽象方法
}

// 实现接口
class Dog implements Animal {
    @Override
    public void sound() {
        System.out.println("Dog barks.");
    }

    @Override
    public void sleep() {
        System.out.println("Dog sleeps.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myDog = new Dog(); // 向上转型
        myDog.sound();
        myDog.sleep();
    }
}

从 Java 8 开始,接口可以包含 default 方法和 static 方法,这使得接口的功能更加强大。

默认方法:

  • 使用 default 关键字声明。
  • 提供默认的实现,实现类可以选择重写或直接使用。
java
interface Animal {
    void sound(); // 抽象方法

    default void sleep() { // 默认方法
        System.out.println("This animal sleeps.");
    }
}

class Dog implements Animal {
    @Override
    public void sound() {
        System.out.println("Dog barks.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myDog = new Dog();
        myDog.sound();
        myDog.sleep(); // 调用接口的默认方法
    }
}

静态方法:

  • static 关键字声明。
  • 可以直接通过接口名调用。
java
interface Animal {
    static void info() {
        System.out.println("This is an animal interface.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal.info(); // 直接通过接口名调用静态方法
    }
}

对象

在面向对象系统中,对象是基本运行的实体,它既包括数据(属性),也包括作用于数据的操作(行为)。一个对象把属性和行为封装成一个整体。

从程序设计者来看,对象是一个程序模块;从用户来看,对象为他们提供了所希望的行为。在对象内的操作通常称为方法,一个对象通常由对象名、属性和方法组成。

25021201.png

图 识别对象的属性

25021202.png

图 识别对象的行为

25021203.png

图 描述对象与类之间的关系

对象的创建

在 Java 中,对象是通过实例化类创建的。类是对象的蓝图,对象是类的实例。创建对象通常包含以下几个步骤:

  1. 定义类:首先需要定义类,类中包含属性和方法。
java
public class Person {
    // 属性
    String name;
    int age;
    
    // 行为
    void speak() {
        System.out.println("My name is " + name + " and I am " + age + " years old.");
    }
}
  1. 实例化对象:使用 new 关键字创建类的实例(对象)。
java
Person person1 = new Person();

在这个例子中,变量 person1Person 类的一个实例,从内存的角度来看,person1 是一个指向 Person 类的对象的引用。

25021811.png

图 实例化对象在 JVM 内存模型中的分配情况
  1. 构造函数:构造函数用于初始化对象的状态。如果没有显式定义构造函数,Java 编译器会自动提供一个无参构造函数。
java
public class Person {
    String name;
    int age;

    // 构造函数
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    void speak() {
        System.out.println("My name is " + name + " and I am " + age + " years old.");
    }
}

// 使用构造函数创建对象
Person person2 = new Person("Alice", 30);

访问对象的属性和行为

一旦对象被创建,可以通过对象引用来访问其属性和方法。

  • 访问属性:使用点号(.)来访问对象的属性。
java
person1.name = "Bob";
person1.age = 25;
  • 调用方法:同样使用点号(.)来调用对象的方法。
java
person1.speak();  // 输出: My name is Bob and I am 25 years old.

对象的引用

对象是通过引用来操作的。引用是指向对象在内存中位置的指针。

  • 引用变量:声明一个对象时,实际上是在声明一个引用变量。
java
Person person31;  // person31 是一个引用变量,目前为 null
person31 = new Person("Charlie", 35);  // person31 现在指向一个新创建的 Person 对象
Person person32 = person31;  // person32 也指向同一个 Person 对象

25021301.png

图 person31、person32 均持有 Person 对象的引用
  • 引用传递:在 Java 中,对象引用是按值传递的。这意味着在方法调用时,传递的是引用的副本,而不是对象本身。
java
public void modifyPerson(Person p) {
    p.name = "David";
}

Person person4 = new Person("Eve", 40);
modifyPerson(person4);
System.out.println(person4.name);  // 输出: David

对象的比较

对象的比较有两种方式:引用比较和内容(值)比较。

  • 引用比较:使用 == 运算符比较两个对象的引用是否指向同一个内存地址。
java
Person person5 = new Person("Frank", 45);
Person person6 = person5;

System.out.println(person5 == person6);  // 输出: true
  • 内容(值)比较:使用 equals() 方法比较两个对象的内容是否相等。默认情况下,equals() 方法与 == 行为相同,但可以通过重写 equals() 方法来实现内容比较。
java
public 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 age == person.age && name.equals(person.name);
    }
}

Person person7 = new Person("Grace", 50);
Person person8 = new Person("Grace", 50);

System.out.println(person7.equals(person8));  // 输出: true

对象的销毁

对象的销毁是由垃圾回收器(Garbage Collector, GC)自动管理的。程序员不需要手动释放对象占用的内存。

  • 垃圾回收:当一个对象不再被任何引用变量引用时,它就变成了垃圾回收的候选对象。垃圾回收器会在适当的时候自动回收这些对象的内存。

    对象超过作用域时,对象的引用变量会被销毁,但对象本身不会被销毁。对象的销毁是由垃圾回收器自动管理的:

    java
    public void createPerson() {
        Person person = new Person("Harry", 55);
        // person 超过作用域,引用变量被销毁,但对象本身不会被销毁
        // 以下代码未引用 person 对象
    }

    不再引用任何对象,原对象成为垃圾回收的候选:

    java
    Person person9 = new Person("Hank", 55);
    person9 = null;  // person9 不再引用任何对象,原对象成为垃圾回收的候选
  • finalize() 方法:可以在类中重写 finalize() 方法,以便在对象被垃圾回收之前执行一些清理操作。然而,finalize() 方法的使用并不推荐,因为它不确定何时会被调用。

    java
    public class Person {
        String name;
        int age;
    
        @Override
        protected void finalize() throws Throwable {
            System.out.println("Person object is being garbage collected: " + name);
            super.finalize();
        }
    }

对象类型转换

向上转型

向上转型是指将子类对象赋值给父类引用。这种转型是安全的,因为子类对象包含了父类的所有属性和方法。

在下方的例子中,Dog 对象被向上转型为 Animal 类型,因此只能调用 Animal 类中定义的方法。

java
Animal myAnimal = new Dog(); // 向上转型
myAnimal.eat(); // 可以调用父类的方法
// myAnimal.bark(); // 错误:无法调用子类特有的方法

向下转型

向下转型是指将父类引用强制转换为子类类型。这种转型需要显式地进行,并且只有在父类引用实际指向子类对象时才是安全的。

在下方例子中,myAnimal 引用实际上指向一个 Dog 对象,因此可以安全地将其向下转型为 Dog 类型。

java
Animal myAnimal = new Dog(); // 向上转型
Dog myDog = (Dog) myAnimal; // 向下转型
myDog.bark(); // 可以调用子类特有的方法
编程洪同学服务平台是一个广泛收集编程相关内容和资源,旨在满足编程爱好者和专业开发人员的需求的网站。无论您是初学者还是经验丰富的开发者,都可以在这里找到有用的信息和资料,我们将助您提升编程技能和知识。
专业开发
高端定制
售后无忧
站内资源均为本站制作或收集于互联网等平台,如有侵权,请第一时间联系本站,敬请谅解!本站资源仅限于学习与参考,严禁用于各种非法活动,否则后果自行负责,本站概不承担!