模式切换
类和对象
在 Java 语言中,类是一种抽象的数据类型,是对一类事物的抽象描述。对象是类的实例,是类的具体实现。
面向对象概述
面向对象相较于面向过程,是两种解决问题的不同角度。面向过程注重于解决问题的步骤及顺序,而面向对象则更注重于解决问题的参与者、各自需要做什么。
面向对象的三大特征分别是:封装、继承和多态。
封装
封装是一种信息隐蔽技术,它的目的是使对象的使用者和生产者分开,使对象的定义和实现分离。
封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。要访问该类的代码和数据,必须通过严格的接口限制。
封装最主要的功能在于我们能够修改自己实现的代码,而不用修改那些调用我们代码的程序片段。适当的封装可以让程序代码更容易理解与维护,也加强了程序的安全性。
图 封装的特性的示意图
实现封装的步骤:
修改属性的可见性来限制对属性的访问(一般限制为
private
)。例如下方的代码中,将name
和age
属性设置为私有的,只能本类才能访问,其他类都访问不了,如此就对信息进行了隐藏。javapublic class Person { private String name; private int age; }
对每个值属性提供对外的公共方法访问,也就是创建一对赋取值方法,用于对私有属性的访问。例如:采用
this
关键字是为了解决实例变量(private String name
)和局部变量(setName(String name)
中的name
变量)之间发生的同名的冲突。javapublic 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());
}
}
继承
继承在面向对象思想中是一个非常重要的概念,是父类和子类之间共享数据和方法的机制。基本思想是指一个类可以派生出一个或多个子类,子类可以继承父类的属性和方法,也可以重写父类的方法,或者增加新的方法。子类继承父类的方法,并做出自己的改变和扩展。
在程序中复用一些已经定义完善的类不仅可以减少软件开发周期,还可以提高软件的可维护性和可扩展性。
图 图形类层次结构示意图
在 Java 不支持多继承,但支持多重继承。
继承的特性:
- 子类拥有父类非
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(); // 子类自己的方法
}
}
图 Animal 与 Dog 类之间的继承关系
需要注意的是,当重写父类的方法时,修改方法的权限修饰符不能降低权限,但可以提高权限。当修改方法返回值类型时,返回值类型必须是父类方法返回值类型的子类。
在实例化子类对象时,会先调用父类的构造方法,然后再调用子类的构造方法。父类的有参构造不能被自动调用,只能使用 super
关键字显式调用父类的构造方法。
如果使用 finalize()
方法清理对象,则需要确保子类 finalize()
方法的最后一个操作是调用父类的 finalize()
方法。以确保父类的资源被正确释放,所有子类的资源也能被正确释放。
多态
对象之间进行通信的一种构造叫做消息。在收到消息时对象要予以响应,不同的对象在收到同一消息时可以产生不同结果,这一现象叫做多态。
多态(Polymorphism)是指同一个方法在不同的对象中有不同的实现。多态分为编译时多态(方法重载)和运行时多态(方法重写)。运行时多态是通过方法重写和向上转型实现的。
多态存在的三个必要条件:
- 继承:子类继承父类。
- 重写:子类重写父类方法。
- 父类引用指向子类对象:
Parent p = new Child();
。
在下方的例子中,Animal
类的 sound()
方法在子类 Dog
和 Cat
中被重写。通过向上转型,myDog
和 myCat
分别调用的是 Dog
和 Cat
类中的 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 中,类中的对象的行为是以方法的形式定义的,对象的属性是以成员变量的形式定义的,所以类包括对象的属性和方法。
图 鸟类的结构
成员变量
对象的属性也称为成员变量,是类的一部分。成员变量定义在类中,方法体之外。成员变量可以是任何数据类型,包括基本数据类型和引用数据类型。
成员变量可以分为两种:
- 实例变量:非
static
修饰的成员变量。 - 类变量:
static
修饰的成员变量。
例如下方的示例中,name
、age
和 height
是 Person
类的成员变量:
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 中的权限修饰符有四种,分别是 public
、protected
和 private
。
public
:公有的,可以被任何包中的任何类访问。protected
:受保护的,可以被同一个包内的类访问。private
:私有的,只能被同一个类访问。
表 Java 中的权限修饰符
局部变量
局部变量是在方法体中定义的变量,只在方法体中有效。局部变量在方法体中声明,方法体结束后局部变量的内存空间会被释放。
java
public class Person {
public void print() {
int age = 18; // age 是局部变量
System.out.println("年龄:" + age);
}
}
局部变量有效范围
局部变量的有效范围(变量的作用域)是从声明开始到方法结束。局部变量的作用域是在声明的方法体内。
图 局部变量的有效范围
在相互不嵌套的两个方法中,可以使用相同的局部变量名,因为它们的作用域不同。
图 在相互不嵌套的区域可以定义相同名称和类型的局部变量
内部类
内部类是定义在另一个类中的类。根据定义的位置和方式,内部类可以分为以下几种:
- 成员内部类
- 局部内部类
- 匿名内部类
- 静态内部类
成员内部类
成员内部类是定义在类中的非静态类。
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(); // 直接通过接口名调用静态方法
}
}
对象
在面向对象系统中,对象是基本运行的实体,它既包括数据(属性),也包括作用于数据的操作(行为)。一个对象把属性和行为封装成一个整体。
从程序设计者来看,对象是一个程序模块;从用户来看,对象为他们提供了所希望的行为。在对象内的操作通常称为方法,一个对象通常由对象名、属性和方法组成。
图 识别对象的属性
图 识别对象的行为
图 描述对象与类之间的关系
对象的创建
在 Java 中,对象是通过实例化类创建的。类是对象的蓝图,对象是类的实例。创建对象通常包含以下几个步骤:
- 定义类:首先需要定义类,类中包含属性和方法。
java
public class Person {
// 属性
String name;
int age;
// 行为
void speak() {
System.out.println("My name is " + name + " and I am " + age + " years old.");
}
}
- 实例化对象:使用
new
关键字创建类的实例(对象)。
java
Person person1 = new Person();
在这个例子中,变量 person1
是 Person
类的一个实例,从内存的角度来看,person1
是一个指向 Person
类的对象的引用。
图 实例化对象在 JVM 内存模型中的分配情况
- 构造函数:构造函数用于初始化对象的状态。如果没有显式定义构造函数,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 对象
图 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)自动管理的。程序员不需要手动释放对象占用的内存。
垃圾回收:当一个对象不再被任何引用变量引用时,它就变成了垃圾回收的候选对象。垃圾回收器会在适当的时候自动回收这些对象的内存。
对象超过作用域时,对象的引用变量会被销毁,但对象本身不会被销毁。对象的销毁是由垃圾回收器自动管理的:
javapublic void createPerson() { Person person = new Person("Harry", 55); // person 超过作用域,引用变量被销毁,但对象本身不会被销毁 // 以下代码未引用 person 对象 }
不再引用任何对象,原对象成为垃圾回收的候选:
javaPerson person9 = new Person("Hank", 55); person9 = null; // person9 不再引用任何对象,原对象成为垃圾回收的候选
finalize()
方法:可以在类中重写finalize()
方法,以便在对象被垃圾回收之前执行一些清理操作。然而,finalize()
方法的使用并不推荐,因为它不确定何时会被调用。javapublic 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(); // 可以调用子类特有的方法