目录
1. 什么是面向对象
在Java中,面向对象是一种编程思想和方法,它将程序设计的重点放在了对象上。面向对象编程 (Object-Oriented Programming,简称OOP)可以将现实世界中的事物看作是对象,并通过定义类来描述对象的属性(特征)和方法(行为)。
在面向对象编程中,类是对象的蓝图,它定义了对象的属性和方法。对象是类的实例化,通过创建对象来使用类定义的属性和方法。
面向对象编程的核心原则是封装、继承和多态。
- 封装 (Encapsulation):将对象的属性和方法封装在一起,通过访问修饰符控制属性和方法的可见性,保护数据安全性。
- 继承 (Inheritance):基于已有的类创建新的类,继承已有类的属性和方法,支持代码复用和扩展。
- 多态 (Polymorphism):同样的方法名可以在不同的类中具有不同的实现,提供了更灵活的代码结构和更高的可扩展性。
通过面向对象编程,我们可以更好地组织代码,将复杂的问题拆解成简单的对象和交互关系,提高代码的可读性、可维护性和可扩展性。
2. 类与对象
在Java中,类是对象的模板或蓝图,用于定义对象的属性和方法。对象是类的实例,它具有类定义的属性和方法。
类的定义包含以下几个部分:
-
类的修饰符:用于控制类的访问权限,如public、private、protected等。
-
类的名称:类的名称应该以大写字母开头,并且符合驼峰命名法。
-
类的父类:可以使用关键字extends指定类的父类,Java中只支持单继承。
-
类的实现的接口:可以使用关键字implements指定类实现的接口,Java中支持多接口实现。
-
类的属性:可定义类的成员变量,用于存储对象的状态。
-
类的方法:可定义类的成员方法,用于操作对象的行为。
下面是一个示例的类定义:
public class Person {
// 类的属性
private String name;
private int age;
// 类的构造方法
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 类的方法
public void sayHello() {
System.out.println("Hello, my name is " + name + " and I am " + age + " years old.");
}
}
上面的代码定义了一个名为Person的类,该类有两个属性name和age,一个构造方法用于初始化属性,一个方法sayHello用于输出问候语。
在类的定义完成后,可以创建该类的对象,即实例化一个类。创建对象需要使用关键字new,然后调用类的构造方法。例如:
Person person = new Person("John", 25);
上面的代码创建了一个名为person的对象,通过调用Person类的构造方法来初始化对象的属性。
然后可以使用对象.属性和对象.方法的方式,来访问对象的属性和调用对象的方法。例如:
System.out.println(person.name); // 输出John
person.sayHello(); // 输出Hello, my name is John and I am 25 years old.
上面的代码通过对象person来访问其属性name和调用其方法sayHello。
通过类和对象的使用,可以实现面向对象的编程,将数据和操作数据的方法封装在对象中,提高代码的复用性和可维护性。
3. 方法
在Java中,方法是一种用于执行特定任务的代码块。方法可以在程序中被多次调用,并且可以接受参数和返回值。
在Java中,方法的定义包括以下几个部分:
-
方法的修饰符:修饰符是可选的,可以用来控制方法的访问级别和行为。
- public:表示方法对所有类可见。
- private:表示方法只能在定义它的类内部访问。
- protected:表示方法可以在同一包内的其他类以及其他包中的子类中访问。
- 默认修饰符:表示方法可以在同一包内的其他类中访问。
-
方法的返回值类型:返回值类型是指方法执行完毕后返回的数据类型。如果方法不返回任何值,则返回值类型为void。
-
方法的名称:方法的名称用于标识方法,并且可以在程序中被调用。
-
方法的参数列表:方法的参数列表包括在括号中的一组参数,用于接受输入数据。参数列表可以包含多个参数,每个参数都有一个参数类型和一个参数名。
-
方法的代码块:代码块是方法执行的实际操作。代码块由一对花括号{}包围,其中包含要执行的一系列语句。
下面是一个示例方法的定义:
public int sum(int num1, int num2) {
int result = num1 + num2;
return result;
}
在此示例中,方法的修饰符是public,返回值类型是int,方法名称是sum,参数列表包括两个参数num1和num2。方法的代码块将num1和num2相加,并将结果存储在result变量中。最后,使用return语句将结果返回给调用方。
要调用一个方法,可以使用方法名称加上一对括号,括号中包含方法的参数。例如:
int result = sum(3, 5);
System.out.println(result); // 输出结果为8
在此示例中,使用sum方法计算3和5的和,并将结果存储在result变量中,然后将结果打印到控制台。
4. 方法传参机制
在Java中,成员方法可以通过参数列表接收传递给方法的数据。方法的参数列表可以包含多个参数,每个参数由参数类型和参数名称组成。
传参机制有两种方式:按值传递和按引用传递。
- 按值传递:
- 当方法的参数是基本数据类型(如int、float、double等)时,传递的是该数据的副本,方法中对参数的修改不会影响原始数据。
- 示例代码如下:
在上述代码中,main方法创建一个整数变量num并将其初始化为10。 然后调用modifyValue方法,该方法接收一个int类型参数并将其乘以2。 尽管在modifyValue方法内部将参数值修改为20,但原始变量num的值仍然保持不变。public class PassByValueExample { public static void main(String[] args) { int num = 10; System.out.println("调用方法前: " + num); // 输出: 调用方法前: 10 modifyValue(num); System.out.println("调用方法后: " + num); // 输出: 调用方法后: 10 } public static void modifyValue(int value) { value = value * 2; System.out.println("方法内部: " + value); // 输出: 方法内部: 20 } }
- 按引用传递:
- 当方法的参数是对象类型时,传递的是对象的引用,方法中对参数引用所指向的对象的修改会影响到原始对象。
- 示例代码如下:
在上述代码中,main方法创建一个StringBuilder对象sb并初始化为"Hello"。然后调用modifyValue方法,该方法接收一个StringBuilder对象作为参数并在其末尾添加" World"。由于Java中对象的传递是按引用传递的,原始对象sb的内容也被修改为"Hello World"。public class PassByReferenceExample { public static void main(String[] args) { StringBuilder sb = new StringBuilder("Hello"); System.out.println("调用方法前: " + sb); // 输出: 调用方法前: Hello modifyValue(sb); System.out.println("调用方法后: " + sb); // 输出: 调用方法后: Hello World } public static void modifyValue(StringBuilder value) { value.append(" World"); System.out.println("方法内部: " + value); // 输出: 方法内部: Hello World } }
需要注意的是,虽然传递的是引用,但是在方法内部重新分配一个新的对象引用,原始对象引用不会受到影响。
传参机制既能保护数据的安全性,又能提高效率和灵活性,在方法的设计和使用时需要根据需求选择合适的传参方式。
5. 方法递归调用
方法递归调用是指一个方法在其自身内部进行调用的过程。在Java中,方法递归调用通常用于解决需要重复执行某个任务的问题,可以简化代码逻辑,提高程序的可读性和可维护性。
方法递归调用包括两个关键要素:递归的终止条件和递归的调用过程。
-
递归的终止条件:在递归方法中,需要定义一个或多个基本情况,当满足某个条件时,不再进行递归调用,直接返回结果。这样做是为了防止陷入无限循环的递归调用。在确定终止条件时,需要保证递归的过程最终能够收敛到终止条件。
-
递归的调用过程:在递归方法中,需要调用自身并传递不同的参数,以解决更小规模的问题。递归方法的调用过程可以看作是将大问题不断分解为更小的子问题,直到达到终止条件。
下面是一个示例,展示如何使用递归实现计算阶乘的方法:
public class Factorial {
public static int calculate(int n) {
// 终止条件
if (n == 0 || n == 1) {
return 1;
} else {
// 递归调用
return n * calculate(n - 1);
}
}
public static void main(String[] args) {
int result = calculate(5);
System.out.println("5的阶乘为:" + result);
}
}
在上述示例中,calculate方法是一个递归方法,用于计算给定整数n的阶乘。当n等于0或1时,满足终止条件,直接返回1;否则,调用自身并传递n-1作为参数,实现了将问题分解为更小规模的子问题。最终,通过不断地递归调用和分解问题,直到达到终止条件,计算出了给定整数的阶乘。
需要注意的是,递归调用过程中,每一层递归都会创建一个新的方法栈帧,保存局部变量和调用参数。递归调用过深或者没有合适的终止条件,可能导致堆栈溢出错误(StackOverflowError)。
在使用递归方法时,需要确保终止条件正确且能够被满足,避免无限循环;同时,需要注意递归调用的次数和性能,避免过深的递归调用导致堆栈溢出。
6. 方法重载
方法重载是指在一个类中可以定义多个同名但参数列表不同的方法。Java编译器通过参数列表的不同来区分不同的方法,并根据方法参数的类型、个数、顺序来确定调用哪个方法。方法重载可以提高代码的复用性和可读性,使代码更加灵活。
方法重载的规则如下:
- 方法名必须相同,但参数列表必须不同。
- 参数列表可以通过参数类型、参数个数和参数顺序进行区分。
- 方法的返回类型可以相同也可以不同。
- 方法的修饰符可以相同也可以不同。
- 只有方法名相同、参数列表不同的方法才能称为方法重载。
以下是一个示例,展示如何使用方法重载:
public class OverloadExample {
public int sum(int x, int y) {
return x + y;
}
public double sum(double x, double y) {
return x + y;
}
public String sum(String str1, String str2) {
return str1 + str2;
}
public static void main(String[] args) {
OverloadExample example = new OverloadExample();
int result1 = example.sum(2, 3);
System.out.println("两个整数的和:" + result1);
double result2 = example.sum(2.5, 3.7);
System.out.println("两个浮点数的和:" + result2);
String result3 = example.sum("Hello", " World");
System.out.println("两个字符串的和:" + result3);
}
}
在上述示例中,定义了三个同名但参数列表不同的sum方法。第一个sum方法接收两个整数参数并返回它们的和;第二个sum方法接收两个浮点数参数并返回它们的和;第三个sum方法接收两个字符串参数并返回它们的连接结果。在main方法中,通过不同的参数调用了不同的sum方法。
需要注意的是,方法重载只能通过参数列表来区分方法,与方法的返回类型无关。因此,在进行方法重载时,只有返回类型不同而参数列表相同的方法是不允许的。
另外,虽然方法重载可以提高代码的复用性和可读性,但在使用时需要注意避免过多的重载方法,以免造成代码混乱和理解困难。
7. 可变参数
可变参数(Variable Arguments)是Java语言提供的一种方便的语法,它允许方法接受可变数量的参数,而无需在方法声明中指定参数的个数。可变参数可以让我们更方便地使用和调用方法,同时提高代码的灵活性。
使用可变参数的语法为在方法的参数列表中使用三个连续的点(...)来表示可变参数,例如:
public void methodName(type... parameterName) {
// 方法体
}
以下是一些关于可变参数的详细介绍:
- 可变参数的类型可以是任意合法数据类型,包括基本数据类型和引用数据类型。
- 可变参数只能出现在方法的参数列表的最后一个位置。如果方法有多个参数,可变参数必须是最后一个。
- 在方法内部,可变参数被当作一个数组来处理。我们可以使用普通的数组操作和相关方法对可变参数进行操作。
- 调用可变参数的方法时,可以传递任意数量的参数,包括0个参数。
- 如果方法声明中同时存在可变参数和固定参数,固定参数要放在可变参数之前。
以下是一个示例,展示如何使用可变参数:
public class VarArgsExample {
public void printNumbers(int... numbers) {
for (int number : numbers) {
System.out.print(number + " ");
}
System.out.println();
}
public static void main(String[] args) {
VarArgsExample example = new VarArgsExample();
example.printNumbers(); // 不传递参数
example.printNumbers(1, 2, 3); // 传递多个整数
example.printNumbers(4); // 传递一个整数
}
}
在上述示例中,定义了一个printNumbers方法,接受可变数量的整数参数。在方法内部,使用增强的for循环遍历参数数组并打印每个整数。在main方法中,我们可以通过不同数量的整数参数来调用printNumbers方法,包括不传递参数、传递多个整数和传递一个整数。
需要注意的是,可变参数只能在方法的声明和调用中使用,不能在其他地方使用。在方法内部,我们可以将可变参数当作数组来处理,但不能将可变参数作为数组类型传递给其他方法。
8. 作用域
作用域(Scope)是指在程序中某个特定区域内声明的变量可见和可访问的范围。
在Java中,作用域可以分为以下几种类型:
-
类级作用域(Class-level scope):在类中定义的成员变量(字段)和静态方法具有类级作用域,可以在整个类的范围内访问。这些变量和方法可以通过类名直接访问。
-
方法级作用域(Method-level scope):在方法内部定义的变量和方法参数拥有方法级作用域,只能在方法内部使用。方法结束后,这些变量将被销毁。
-
代码块级作用域(Block-level scope):在代码块中定义的变量拥有代码块级作用域。代码块可以是包含在方法中的任意花括号{}括起来的代码段,例如if语句、for循环等。在代码块内部定义的变量只能在该代码块内部使用。
-
局部变量作用域(Local variable scope):在方法内部或代码块中定义的变量拥有局部变量作用域,只能在声明的代码块内使用。局部变量在声明时需要初始化,并且在方法或代码块结束后都会被销毁。
-
成员变量作用域(Instance variable scope):在类中定义的成员变量拥有类级作用域,可以在整个类的范围内访问,不需要初始化,会随着对象的创建而创建,随着对象的销毁而销毁。
作用域规则如下:
-
内部作用域可以访问外部作用域的变量,但外部作用域无法直接访问内部作用域的变量。
-
当在内部作用域中声明了与外部作用域同名的变量时,内部作用域的变量将会屏蔽外部作用域的同名变量。
-
当使用变量时,将按照就近原则访问最近的声明。
需要注意的是,类级作用域和对象级作用域是不同的概念。类级作用域是指在整个类中可见的范围,而对象级作用域是指在对象的生命周期内可见的范围。对象级作用域包含了成员变量的作用域和方法的局部变量作用域。
9. 构造方法
构造方法(Constructor)是一种特殊的方法,在创建对象时被调用,用于初始化对象的状态。构造方法的名称必须与类名完全相同,没有返回类型,并且不能被直接调用,而是在使用关键字"new"创建对象时自动调用。
构造方法的作用是为对象提供初始值,并执行必要的初始化操作。它们在对象被创建后立即执行,可以用来设置对象的初始属性、分配内存和执行其他必要的操作。
Java中的构造方法有以下特点:
- 构造方法的名称必须与类名完全相同,大小写也要一致。
- 构造方法没有返回类型,包括void,因为它们的任务是创建和初始化对象,并不返回值。
- 一个类可以有多个不同参数列表的构造方法,称为构造方法的重载。
- 如果没有显式地定义构造方法,Java会自动提供一个默认的无参构造方法。如果类中显式地定义了构造方法,则默认构造方法不会被提供。
- 构造方法可以有访问修饰符,如public、protected、private,用于控制构造方法的可见性。
构造方法的使用场景:
- 创建对象:构造方法在使用关键字"new"创建对象时调用,用于初始化对象的状态。
- 初始化属性:构造方法可以接收参数,用来初始化对象的属性。
- 执行必要的操作:构造方法可以执行一些必要的操作,如分配内存、连接数据库等。
示例:
public class Car {
private String brand;
private String color;
// 无参构造方法
public Car() {
brand = "unknown";
color = "unknown";
}
// 带参构造方法
public Car(String brand, String color) {
this.brand = brand;
this.color = color;
}
public String getBrand() {
return brand;
}
public String getColor() {
return color;
}
}
// 创建Car对象
Car car1 = new Car(); // 调用无参构造方法
Car car2 = new Car("Toyota", "red"); // 调用带参构造方法
在上面的示例中,Car类定义了一个无参构造方法和一个带参构造方法,用于初始化Car对象的品牌和颜色属性。通过使用关键字"new"创建Car对象时,会根据调用的构造方法来设置对象的属性。
10. 对象的创建
在Java中,对象的创建是通过调用类的构造方法来实现的。以下是详细介绍对象的创建的步骤:
-
导入类:首先需要导入所需的类。可以使用
import
语句来导入类,或者使用完全限定类名来引用类。 -
定义引用变量:通过类名声明一个对象引用变量。例如,
Person person;
-
创建对象:使用
new
关键字来创建对象。例如,person = new Person();
这将调用Person
类的无参构造方法创建一个新的对象,并将对象的引用赋给person
变量。 -
调用构造方法:在创建对象时,会自动调用该类的构造方法。构造方法用于初始化对象的属性和状态。
-
访问对象的成员:通过对象引用变量使用点操作符(.)访问对象的成员变量和成员方法。例如,
person.setName("John");
可以调用对象的方法来设置对象的属性。
完整的对象创建示例代码如下:
import com.example.Person; // 导入Person类
public class Main {
public static void main(String[] args) {
Person person; // 定义对象引用变量
person = new Person(); // 创建对象,并将引用赋给变量
person.setName("John"); // 调用对象的方法设置属性
person.setAge(25);
System.out.println("Name: " + person.getName());
System.out.println("Age: " + person.getAge());
}
}
在上述示例中,首先导入了Person
类。然后定义了一个person
对象引用变量。接着使用new
关键字创建了一个Person
对象,并将对象引用赋给person
变量。最后通过调用对象的方法设置属性,并通过对象的方法获取属性值并进行打印输出。
对象的创建是实例化类的过程,通过创建对象可以实现对类中成员变量和成员方法的调用和访问。
11. 对象创建底层原理
在Java中,对象创建的底层原理涉及到类加载、堆内存分配、引用对象和垃圾回收等过程。
-
类加载:
- 当使用一个类时,JVM会检查该类是否已经被加载。如果没有加载,则通过类加载器加载该类的字节码文件。
- 类加载的过程包括加载、链接和初始化。加载阶段将字节码文件加载到内存中,链接阶段将符号引用转换为直接引用,初始化阶段为类的静态变量赋初值,并执行静态代码块。
- 加载后的类信息存放在方法区中,包括类的结构信息、静态变量等。
-
堆内存分配:
- 当使用
new
关键字创建对象时,JVM会在堆内存中为对象分配空间。 - 堆内存是Java中用于存放对象实例的内存区域,由垃圾回收器进行自动管理。堆内存分为两部分:新生代(Young Generation)和老年代(Old Generation)。
- 对象的实例变量存放在堆内存中。
- 当使用
-
引用对象:
- 在堆内存分配空间后,会返回一个引用(reference)指向该对象的内存地址。
- 引用存放在栈内存中,栈内存用于存放方法调用和局部变量。栈内存由线程独立分配,速度快,生命周期与线程相同。
- 如果有多个引用指向同一个对象,则它们会共享相同的对象实例,可以通过引用修改对象的状态。
-
垃圾回收:
- Java具有自动内存管理机制,通过垃圾回收器进行垃圾回收。
- 当对象不再被引用时,垃圾回收器会自动回收该对象占用的内存空间,释放资源。
- 垃圾回收器在特定的时间点进行垃圾回收,将不再使用的对象标记为垃圾,并进行内存回收和内存碎片整理,以提供新的内存空间。
通过类加载、堆内存分配、引用对象和垃圾回收等步骤,Java实现了对象的创建和内存管理。这种自动化的内存管理机制减少了开发者手动分配和释放内存的复杂性,提高了程序的安全性、可靠性和性能。
12. this 关键字
在Java中,this
是一个特殊的关键字,它可以被用来引用当前对象。它可以在对象的内部上下文中使用,用于区分类的成员变量和局部变量的名称冲突。
this
关键字的主要用途如下:
-
引用当前对象的成员变量:当一个成员变量的名称与方法的参数名称或局部变量的名称相同时,
this
关键字可以用来引用对象的成员变量。这是因为在方法中,方法的参数和局部变量的优先级高于成员变量。示例代码:
public class MyClass { private int num; public void setNum(int num) { this.num = num; // 使用this关键字引用成员变量 } }
-
调用当前对象的其他方法:在一个方法内部,可以使用
this
关键字来调用当前对象的其他方法。示例代码:
public class MyClass { public void method1() { System.out.println("调用method1方法"); } public void method2() { this.method1(); // 使用this关键字调用其他方法 } }
-
在构造方法中调用其他构造方法:在一个构造方法中,可以使用
this
关键字来调用同一类的其他构造方法。这种方式被称为构造方法的重载。示例代码:
public class MyClass { private int num; public MyClass() { this(0); // 调用带参数的构造方法 } public MyClass(int num) { this.num = num; } }
请注意以下几点:
this
关键字调用其他构造方法时,必须放在构造方法的第一行。this
关键字只能在非静态方法中使用,因为静态方法不属于任何对象。this
关键字只能在类的内部使用,不能在类的外部使用。this
关键字不能用于调用静态成员和静态方法,因为它是与对象相关的。