类(Class) - 类是创建对象的蓝图或模板。一个类定义了对象的数据成员(字段)和成员函数(方法)。
类的定义
在Java中,类是一个用户定义的类型,它描述了具有相同属性(数据成员/字段)和行为(成员函数/方法)的一组对象。类定义了对象的结构和行为。一个类可以包含以下元素:
字段(Fields):也称作成员变量或属性,表示对象的状态。字段可以是基本类型(如 int, float)或是引用类型(如 String, 其他类)。
构造器(Constructors):用于初始化新创建的对象。构造器的名字必须与类名相同,并且没有返回类型。
方法(Methods):表示对象的行为。方法可以修改对象的状态或者返回一些值。
嵌套类(Nested Classes):可以是内部类(Inner Classes)、静态内部类(Static Nested Classes)、匿名类(Anonymous Classes)。
示例
下面是一个简单的类的定义:
private String name; // 字段
private int age;
// 默认构造器
public Person() {
this.name = "";
this.age = 0;
}
// 带参数的构造器
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Getter 方法
public String getName() {
return name;
}
// Setter 方法
public void setName(String name) {
this.name = name;
}
// 方法
public void displayInfo() {
System.out.println("Name: " + name + ", Age: " + age);
}
}
使用类
定义好类之后,就可以使用 new 关键字来创建类的实例(即对象):
public class Main {
public static void main(String[] args) {
Person person1 = new Person(); // 使用默认构造器创建对象
person1.setName("Alice"); // 设置名字
person1.displayInfo(); // 显示信息
Person person2 = new Person("Bob", 30); // 使用带参数的构造器创建对象
person2.displayInfo(); // 显示信息
}
}
对象(Object) - 对象是一个类的实例。在Java中,一切皆是对象,每个对象都是特定类的一个实例
对象的创建
要创建一个对象,首先需要定义一个类,然后使用 new 关键字加上类的构造器来创建该类的实例。例如:
// 假设我们有一个名为 Car 的类
public class Car {
private String model;
private int year;
public Car(String model, int year) {
this.model = model;
this.year = year;
}
public void displayInfo() {
System.out.println("Model: " + model + ", Year: " + year);
}
}
public class Main {
public static void main(String[] args) {
// 创建 Car 类的一个实例
Car myCar = new Car("Toyota Corolla", 2022);
// 调用方法
myCar.displayInfo();
}
}
在这个例子中,myCar 是 Car 类的一个实例,它有自己的状态(model 和 year)以及行为(displayInfo 方法)。
对象的使用
一旦创建了对象,就可以通过对象的引用访问其字段和方法。对象的引用是一个指向对象内存位置的变量,它允许我们访问对象的状态和行为。例如:
// 访问字段
System.out.println(myCar.model); // 输出 "Toyota Corolla"
// 调用方法
myCar.displayInfo(); // 输出 "Model: Toyota Corolla, Year: 2022"
对象的生命周期
对象的生命周期开始于使用 new 关键字创建对象,并结束于对象被垃圾回收器回收。Java 的垃圾回收机制会自动回收不再使用的对象占用的内存空间。
对象的比较
在Java中,比较两个对象是否相等通常意味着比较它们的状态是否一致。但是,默认情况下,使用 == 操作符比较两个对象引用实际上是检查它们是否指向同一个对象。如果要比较对象的状态,通常需要重写 equals 方法。
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
Car other = (Car) obj;
return model.equals(other.model) && year == other.year;
}
对象的克隆
有时候,可能需要复制一个对象的所有状态信息来创建一个新的对象。Java 提供了 Cloneable 接口来支持对象的克隆。
public class Car implements Cloneable {
// ...
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
对象的序列化
如果需要将对象的状态保存到文件中,或者通过网络传输对象,就需要使用序列化。Java 支持对象序列化,需要实现 Serializable 接口。
public class Car implements Serializable {
// ...
}
封装(Encapsulation) - 封装是指隐藏对象的属性和实现细节,并对外提供公共访问方法。封装有助于提高代码的安全性和可维护性。
封装的好处
安全性:通过限制对内部状态的直接访问,可以防止意外或恶意的修改。
可维护性:当内部实现发生变化时,只要公共接口保持不变,就不需要修改使用该类的其他代码。
灵活性:可以在内部实现中添加逻辑,如验证、日志记录等,而不会影响到外部使用者。
封装的实现
在Java中,封装主要通过以下几种方式实现:
私有化字段
将类的字段声明为 private,这样就只能通过类内的方法访问这些字段。外部类无法直接访问这些字段。
public class BankAccount {
private double balance; // 余额字段私有化
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public double getBalance() {
return balance;
}
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
public boolean withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false;
}
}
使用 getter 和 setter 方法
提供 getter 方法来读取私有字段的值,以及 setter 方法来设置这些字段的值。这使得可以在设置值之前进行验证或附加逻辑。
public class BankAccount {
private double balance;
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
if (balance >= 0) {
this.balance = balance;
} else {
throw new IllegalArgumentException("Balance cannot be negative.");
}
}
}
隐藏实现细节
除了隐藏字段外,还可以隐藏方法的实现细节。这意味着外部代码只知道如何调用方法,但不知道方法是如何工作的。
public class BankAccount {
private double calculateInterest(double amount) {
return amount * 0.05; // 5% 利息率
}
public double getInterest(double amount) {
return calculateInterest(amount);
}
}
使用 final 修饰符
对于那些不应该被修改的字段,可以使用 final 修饰符来标记,这样可以确保一旦赋值就不能再更改。
public class BankAccount {
private final String accountNumber; // 不可更改的账号
public BankAccount(String accountNumber) {
this.accountNumber = accountNumber;
}
public String getAccountNumber() {
return accountNumber;
}
}
封装的原则
当设计类时,应该遵循以下原则来确保良好的封装:
最小权限原则:只暴露完成任务所需的最少的公共API。
信息隐藏:内部实现的细节不应该暴露给外部。
数据完整性:通过控制对数据的访问来保证数据的完整性和一致性。
通过遵循这些原则,可以编写出更加健壮、易于维护和扩展的代码。封装是面向对象编程中非常重要的一个概念,也是编写高质量软件的基础。
继承(Inheritance) - 继承允许一个类(子类)继承另一个类(父类)的特性。这使得代码可以被重用,并且可以构建具有共同特性的类层次结构。
继承是面向对象编程的一个核心概念,它允许新的类(子类或派生类)继承现有类(父类或基类)的特性。通过继承,可以实现代码的重用,并且能够构建出具有共同特性的类的层次结构。以下是关于Java中继承的一些关键点及其应用:
继承的语法
在Java中,通过使用 extends 关键字来声明一个类继承自另一个类。例如:
public class Vehicle { // 父类
protected int wheels;
protected String color;
public Vehicle(int wheels, String color) {
this.wheels = wheels;
this.color = color;
}
public void startEngine() {
System.out.println("Engine started.");
}
}
public class Car extends Vehicle { // 子类继承自 Vehicle
private int numberOfDoors;
public Car(int wheels, String color, int numberOfDoors) {
super(wheels, color); // 调用父类的构造器
this.numberOfDoors = numberOfDoors;
}
public void honk() {
System.out.println("Beep Beep!");
}
}
继承的特点
重用性:子类可以直接使用父类中的方法和字段,无需重复编写相同的代码。
扩展性:子类可以添加新的方法或字段,或者覆盖(override)父类的方法来提供新的实现。
多态性:由于继承的存在,子类可以被当作父类来使用,从而实现多态性。
继承的规则
单继承:Java 中一个类只能继承一个父类,但可以通过实现多个接口来弥补这一限制。
构造器调用:子类构造器中必须显式调用父类的构造器,通常通过 super 关键字实现。
访问修饰符:子类只能访问父类中 public 和 protected 的成员,不能访问 private 成员。
继承中的方法覆盖(Overriding)
子类可以覆盖父类的方法,提供自己特有的实现。为了表明一个方法是覆盖父类的方法,可以使用 @Override 注解。例如:
public class Vehicle {
public void startEngine() {
System.out.println("Engine started.");
}
}
public class ElectricCar extends Vehicle {
@Override
public void startEngine() {
System.out.println("Electric engine started silently.");
}
}
继承中的方法重载(Overloading)
虽然不是继承特有的,但在同一个类中,可以有多个同名但参数不同的方法,这种现象称为方法重载。例如:
public class Vehicle {
public void startEngine() {
System.out.println("Engine started.");
}
public void startEngine(String keyType) {
System.out.println("Engine started with " + keyType + ".");
}
}
继承中的抽象类和接口
抽象类:含有至少一个抽象方法的类称为抽象类,抽象方法是没有方法体的方法。抽象类不能被实例化,主要用于被子类继承。
接口:接口定义了一组方法的集合,但不提供任何实现。接口中的所有方法默认都是 public abstract 的。
public abstract class Vehicle {
public abstract void startEngine();
}
public interface Drivable {
void drive();
}
public class Car extends Vehicle implements Drivable {
@Override
public void startEngine() {
System.out.println("Car engine started.");
}
@Override
public void drive() {
System.out.println("Car is driving.");
}
}
通过继承,可以有效地组织代码,减少冗余,并使代码更易于理解和维护。然而,过度使用继承可能导致复杂度增加,因此在实际开发中应当谨慎使用。
多态(Polymorphism) - 多态指的是同一个操作作用于不同的对象,可以有不同的解释,并产生不同的执行结果。在Java中,多态可以通过方法重载(overloading)和方法覆盖(overriding)来实现。
多态是面向对象编程的一个重要特性,它允许程序员编写更加灵活、可扩展的代码。多态意味着同一接口可以用来表示不同的类的对象,这些对象可以根据各自的具体类型表现出不同的行为。在Java中,多态可以通过方法重载(overloading)和方法覆盖(overriding)来实现。
方法重载(Overloading)
方法重载指的是在一个类中可以有多个同名的方法,但它们的参数列表不同(即参数的数量、类型或顺序不同)。这是编译时的多态性。
例子
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
public double add(double a, double b, double c) {
return a + b + c;
}
}
在这个例子中,Calculator 类有三个 add 方法,根据传入参数的不同,调用相应的 add 方法。这是基于静态类型的多态性,因为编译器在编译时期就能确定调用哪个方法。
方法覆盖(Overriding)
方法覆盖指的是子类可以重新定义从父类继承的方法,以提供自己的实现。这是运行时的多态性,即在运行时根据对象的实际类型确定调用哪个方法。
例子
public class Animal {
public void sound() {
System.out.println("The animal makes a sound.");
}
}
public class Dog extends Animal {
@Override
public void sound() {
System.out.println("The dog barks.");
}
}
public class Cat extends Animal {
@Override
public void sound() {
System.out.println("The cat meows.");
}
}
在这个例子中,Dog 和 Cat 类都覆盖了从 Animal 类继承来的 sound 方法。当通过一个 Animal 类型的引用调用 sound 方法时,实际执行的方法取决于引用所指向的对象的实际类型。
java
深色版本
public class Main {
public static void main(String[] args) {
Animal myPet1 = new Dog();
Animal myPet2 = new Cat();
myPet1.sound(); // 输出 "The dog barks."
myPet2.sound(); // 输出 "The cat meows."
}
}
多态的实现
多态的实现依赖于以下几个方面:
继承:子类继承自父类,这是实现多态的前提条件。
覆盖:子类重写父类的方法,以提供新的实现。
引用类型:使用父类或接口类型的引用变量来指向子类对象,这样可以根据对象的实际类型动态选择合适的方法。
多态的优势
代码的可重用性:子类可以重用父类的代码。
代码的灵活性:通过接口或抽象类定义通用的行为,子类可以根据需要提供具体的实现。
扩展性:可以在不影响现有代码的情况下添加新的子类来扩展系统功能。
注意事项
当使用 instanceof 操作符来检测对象的类型时,可以更灵活地调用适当的方法或执行特定的操作。
在覆盖方法时,子类方法的访问级别不能比父类方法更严格。
如果父类的方法被声明为 final,那么这个方法不能被子类覆盖。
多态是面向对象编程中一个强大的工具,它可以简化代码的设计,使其更加灵活和易于维护。理解和应用多态性是成为一个熟练的面向对象程序员的关键技能之一。
抽象(Abstraction) - 抽象是通过抽象类(abstract class)和接口(interface)来实现的。抽象类可以包含抽象方法(没有实现的方法),而接口则完全由抽象方法组成。它们都允许定义行为规范而不关心具体的实现。
抽象是面向对象编程中的一个重要概念,它允许我们定义一组行为规范,而不关注具体的实现细节。通过抽象,我们可以隐藏复杂性,并提供一个简化的接口来与对象交互。在Java中,抽象主要通过抽象类(abstract class)和接口(interface)来实现。
抽象类(Abstract Class)
抽象类是一种特殊的类,它不能被实例化,但可以被继承。抽象类可以包含抽象方法(没有方法体的方法)和具体方法(有方法体的方法)。抽象方法没有具体的实现,必须由子类来实现这些方法。抽象类允许我们定义一组共有特性的类,并要求子类提供具体的实现。
例子
public abstract class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
// 抽象方法,没有具体的实现
public abstract void makeSound();
}
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
// 实现父类的抽象方法
@Override
public void makeSound() {
System.out.println("Woof!");
}
}
public class Cat extends Animal {
public Cat(String name) {
super(name);
}
// 实现父类的抽象方法
@Override
public void makeSound() {
System.out.println("Meow!");
}
}
在这个例子中,Animal 是一个抽象类,它定义了一个抽象方法 makeSound。Dog 和 Cat 类分别继承了 Animal 类,并提供了 makeSound 方法的具体实现。
接口(Interface)
接口定义了一组方法的集合,但不提供任何实现。接口中的所有方法默认都是 public abstract 的。一个类可以实现一个或多个接口,从而允许多重继承的行为。接口可以用来定义一组相关的行为规范,而不需要指定具体实现。
public interface Swimmable {
void swim();
}
public class Fish implements Swimmable {
@Override
public void swim() {
System.out.println("Fish is swimming.");
}
}
public class Duck implements Swimmable {
@Override
public void swim() {
System.out.println("Duck is swimming.");
}
}
在这个例子中,Swimmable 接口定义了一个 swim 方法。Fish 和 Duck 类分别实现了 Swimmable 接口,并提供了 swim 方法的具体实现。
抽象类 vs 接口
尽管抽象类和接口都可以用来实现抽象,但它们之间存在一些区别:
抽象类:
可以包含具体的方法实现。
可以包含字段。
一个类只能继承一个抽象类。
抽象类可以提供默认的行为。
接口:
只能包含抽象方法(从Java 8开始可以包含默认方法和静态方法)。
不包含任何字段。
一个类可以实现多个接口。
接口主要用于定义行为规范。
使用抽象的好处
代码复用:抽象类和接口允许代码的复用。
定义规范:可以定义一组行为规范,让子类或实现类去具体实现。
增强灵活性:通过抽象定义的规范可以在不修改现有代码的基础上轻松扩展。
示例代码
下面是一个结合了抽象类和接口的示例:
public abstract class Vehicle {
private String brand;
public Vehicle(String brand) {
this.brand = brand;
}
public String getBrand() {
return brand;
}
public abstract void move();
}
public interface HasWheels {
void wheelCount();
}
public class Car extends Vehicle implements HasWheels {
public Car(String brand) {
super(brand);
}
@Override
public void move() {
System.out.println("Car is moving.");
}
@Override
public void wheelCount() {
System.out.println("Car has 4 wheels.");
}
}
public class Bicycle extends Vehicle implements HasWheels {
public Bicycle(String brand) {
super(brand);
}
@Override
public void move() {
System.out.println("Bicycle is moving.");
}
@Override
public void wheelCount() {
System.out.println("Bicycle has 2 wheels.");
}
}
在这个例子中,Vehicle 是一个抽象类,定义了一个抽象方法 move。HasWheels 是一个接口,定义了一个方法 wheelCount。Car 和 Bicycle 分别继承了 Vehicle 并实现了 HasWheels 接口。
通过抽象类和接口,我们可以定义一套清晰的行为规范,并且允许子类或实现类提供具体的实现细节。这种方法不仅增强了代码的灵活性,还使得代码更加模块化和易于维护。