Java

Java 知识量:11 - 45 - 220

5.3 面向对象设计概要><

常量- 5.3.1 -

在Java编程语言中,常量是一种固定的值,它在程序的生命周期中无法更改。在Java中,常量的常见类型包括整型、浮点型、字符型和布尔型。

要声明一个常量,通常会使用 final 关键字。以下是一些例子:

final int MY_CONSTANT = 10;  // 整型常量  
final double PI = 3.14159;    // 浮点型常量  
final char MY_CHAR = 'A';    // 字符型常量  
final boolean DEBUG = false; // 布尔型常量

这些常量的值在声明后就不能改变。如果试图去修改它们,那么编译器会报错。

常量名通常全部使用大写字母,以示区别。这是一种常见的Java编程惯例,但并非强制规定。

除了基本类型,Java也允许常量引用不可变的对象。例如,可以使用 String 类型的字面量创建常量:

final String MY_STRING = "Hello, world!"; // 字符串常量

在Java中,字面量和基本类型的值都是常量,因为它们在初始化后无法更改。然而,对于对象引用,即使引用不可变(如 String),如果引用的对象包含可变的部分(如数组或集合),那么这些部分仍然可以更改。在这种情况下,严格来说,不能称其为常量,但通常仍会使用 "常量" 这个术语来描述这种不可变的对象引用。

接口与抽象类- 5.3.2 -

Java中的接口(Interface)和抽象类(Abstract Class)都是定义对象行为的两种机制,但它们之间存在一些主要的区别。

接口(Interface):

  • 接口是一个完全抽象的类,它只包含抽象方法(没有方法体)。

  • 在Java中,接口不能被实例化。它主要用于定义一个行为规范,告诉其他类该做什么。

  • 接口中声明的所有方法都是公开和默认的(public和default),这意味着任何类都可以实现接口,但实现类可以选择如何实现这些方法。

  • 接口可以用于实现多态。

  • Java 8及更高版本中的接口可以包含默认方法和静态方法。

  • 在Java 9及更高版本中,接口可以包含私有方法,但只能通过接口自身的内部方法访问。

抽象类(Abstract Class):

  • 抽象类是一种特殊的类,可以包含抽象方法和非抽象方法。

  • 可以有实例化的子类,子类必须实现(覆盖)父类中的所有抽象方法。

  • 抽象类中的所有非抽象方法默认都是隐式的,即它们只有方法签名,没有方法体。

  • 抽象类可以用来定义部分实现,为子类提供一些通用功能。

  • 子类必须被声明为继扭自抽象类(或接口),但不必实现所有的抽象方法。

  • 抽象类可以实现多态。

总的来说,接口和抽象类都是为了实现多态和代码复用,但它们的主要区别在于:接口是行为的完全定义,要求所有实现类都要遵守;而抽象类是已有代码的复用,它提供了部分实现的便利性。

默认方法- 5.3.3 -

接口可以包含默认方法(default method)和静态方法(static method),这是Java为解决在不影响现有代码的情况下对接口进行扩展的问题而引入的特性。

默认方法是一种有默认实现的方法,可以在不破坏已有代码的情况下为接口添加新的方法。默认方法使用default关键字进行声明,可以在接口内部提供默认的实现。

以下是一个示例:

interface MyInterface {  
    void method1();  
      
    default void method2() {  
        System.out.println("This is a default method.");  
    }  
}

在这个示例中,MyInterface接口包含了一个抽象方法method1()和一个默认方法method2()。默认方法提供了默认的实现,可以在不破坏已有代码的情况下扩展接口的功能。

需要注意的是,如果一个类实现了接口,但没有实现接口中的所有默认方法,那么这个类必须被声明为抽象类。如果一个类实现了接口,并且也实现了接口中的所有默认方法,那么这个类不必被声明为抽象类。

使用默认方法可以使接口更加灵活和可扩展。同时,也可以减少对现有代码的修改,因为可以在不破坏原有实现的基础上为接口添加新的方法。

实例方法与类方法- 5.3.4 -

实例方法与类方法之间存在一些主要的差异。

实例方法:

  • 每个实例(对象)可以有其自己的实例变量。

  • 实例方法操作这些实例变量,或进行其他操作,如调用其他实例方法或类方法等。

  • 实例方法是与特定对象关联的,所以调用一个实例方法时,通常会创建一个类的实例。

  • 实例方法可以有参数,并可以返回值。

例如:

public class MyClass {  
    private int instanceVariable;  
  
    public void instanceMethod() {  
        // 操作 instanceVariable 或其他实例  
    }  
}

类方法:

  • 类方法不与特定的对象实例关联,它们与类本身关联。

  • 类方法没有访问实例变量的权限(除非这些变量被声明为静态)。

  • 类方法主要用于执行与类本身相关的操作,如计算类实例的数量,或进行类的配置设置等。

  • 类方法可以有参数,但通常不返回值(除非需要记录或配置一些设置)。

例如:

public class MyClass {  
    private static int classVariable;  
  
    public static void classMethod() {  
        // 操作 classVariable 或其他类级别操作  
    }  
}

注意:在Java中,类方法必须使用static关键字进行声明,而实例方法则不需要。通过static关键字,告诉编译器该方法是类的一部分,而不是对象的一部分。

组合与继承- 5.3.5 -

组合和继承是两种不同的代码重用和组织结构方式。

组合(或“组合关系”)是一种结构上的关系,表示一个类包含另一个类的实例。组合让一个类有更多的职责,但并没有任何继承关系。例如,一个汽车类可以有一个引擎类的实例。这种关系在代码中通常通过字段或者构造函数参数来表示。

例如:

public class Car {  
    private Engine engine;  
  
    public Car(Engine engine) {  
        this.engine = engine;  
    }  
}

在这个例子中,Car类通过包含一个Engine类的实例来组合它。

继承是一种更强的组合关系,它表示一个类是另一个类的特殊版本。子类继承了父类的所有公共方法和属性。例如,我们可以有一个动物类和一个狗类,狗类继承了动物类的某些特性。这种关系在Java中通过 extends 关键字来表示。

例如:

public class Animal {  
    public void eat() {  
        System.out.println("Eating...");  
    }  
}  
  
public class Dog extends Animal {  
    // Dog inherits the eat() method from Animal  
}

在这个例子中,Dog类继承了Animal类的eat()方法。

总的来说,组合和继承都是代码组织和重用的重要方式,但它们在使用上有所不同。组合更强调的是一种“部分-整体”的关系,而继承更强调的是一种“一般-特殊”的关系。

字段继承和访问器- 5.3.6 -

类的字段和访问器(也称为方法)都可以被继承。字段是类中的变量,而访问器是用于访问和修改这些变量的方法。

当一个类继承另一个类时,它会自动继承所有父类的字段和访问器。然而,访问器的行为可以根据父类和子类之间的访问修饰符而有所不同。

下面是一个例子,展示了如何使用Java中的继承和访问器:

public class Animal {  
    private String name;  
  
    public Animal(String name) {  
        this.name = name;  
    }  
  
    public String getName() {  
        return name;  
    }  
  
    public void setName(String name) {  
        this.name = name;  
    }  
}  
  
public class Dog extends Animal {  
    private String breed;  
  
    public Dog(String name, String breed) {  
        super(name); // inherit name field and accessors from Animal class  
        this.breed = breed;  
    }  
  
    public String getBreed() {  
        return breed;  
    }  
  
    public void setBreed(String breed) {  
        this.breed = breed;  
    }  
}

在这个例子中,Animal类有一个私有的名字字段和公有的名字访问器。Dog类继承了Animal类,并添加了一个私有的品种字段和公有的品种访问器。在Dog类的构造函数中,使用super(name)来继承父类的名字字段和访问器。

现在可以使用Dog类的对象来访问名字和品种字段,以及它们的访问器:

Dog myDog = new Dog("Buddy", "Golden Retriever");  
System.out.println(myDog.getName()); // prints "Buddy"  
System.out.println(myDog.getBreed()); // prints "Golden Retriever"  
  
myDog.setName("Max");  
myDog.setBreed("German Shepherd");  
System.out.println(myDog.getName()); // prints "Max"  
System.out.println(myDog.getBreed()); // prints "German Shepherd"

在这个例子中,创建了一个Dog对象并调用了它的名字和品种的访问器。注意:虽然Animal类的名字字段和访问器是私有的,但Dog类可以继承它们并使用它们。

单例模式- 5.3.7 -

单例模式(Singleton Pattern)是一种常用的软件设计模式,该模式的主要目标是确保一个类只有一个实例,并提供一个全局访问点。这种模式在需要频繁使用某一类,且资源消耗较大的情况下非常有用,比如文件系统、数据库连接、内存管理等。

以下是一个基本的 Java 单例模式的实现:

public class Singleton {  
    private static Singleton instance;  
  
    private Singleton() {}  
  
    public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

在这个例子中,构造函数是私有的,这样就阻止了其他类实例化这个单例类。getInstance() 方法是公共的,并使用了 synchronized 关键字来避免在多线程环境下可能出现的并发问题。当首次调用 getInstance() 方法时,如果 instance 为 null,就会创建一个新的 Singleton 实例。

然而,这种实现方式有一个问题,那就是每次调用 getInstance() 方法时,都需要进行同步,这会消耗大量的计算资源。因此,更常见的实现方式是使用 "双重检查锁定" 来实现:

public class Singleton {  
    private static volatile Singleton instance;  
  
    private Singleton() {}  
  
    public static Singleton getInstance() {  
        if (instance == null) {  
            synchronized (Singleton.class) {  
                if (instance == null) {  
                    instance = new Singleton();  
                }  
            }  
        }  
        return instance;  
    }  
}

在这个版本中,只有在 instance 为 null,即还未创建单例的情况下,才进行同步。并且,只有在同步块内部,才真正创建单例对象。由于在 Java 中,对 volatile 字段的读写操作具有原子性,因此这种实现方式可以在多线程环境下安全地创建单例。

这只是单例模式最基础的实现方式,实际上,单例模式的实现还有许多其他方式,如在 Java 枚举、静态内部类等方式来实现,每种方式都有其适用的场景和优缺点。