一、基础问题
1.Java程序的入口——main函数
在Java中某些类含有main函数作为程序入口(至少一个类中有main函数),这些main函数存在的意义是方便程序员在调用某个类的时候,能够对这个类中的某些方法(函数)直接进行一些测试并查看效果,初次以为,main函数中的参数 String[] args 的作用是在你测试你的main函数时,可以通过输入一些命令行参数使得String[] args被赋值,然后再在你的main函数中调用这个字符串数组。
public class Greet {
public static void main(String[] args) {
if (args.length > 0) {
System.out.println("Hello, " + args[0] + "!");
} else {
System.out.println("Hello, World!");
}
}
}
在以上代码中,你可以通过修改你的命令行参数,来修改String[] args的值,并在main函数中使用。
2.Java中的抽象类和接口有什么区别
抽象类的定义
abstract class Animal {
String name;
// 具体方法
void eat() {
System.out.println(name + " is eating");
}
// 抽象方法
abstract void makeSound();
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof!");
}
}
接口的定义
interface Animal {
void makeSound();
}
interface Machine {
void start();
}
// 正确的多继承方式,一个类可以实现多个接口
class RobotDog implements Animal, Machine {
@Override
public void makeSound() {
System.out.println("RobotDog makes a mechanical sound");
}
@Override
public void start() {
System.out.println("RobotDog starting");
}
}
二者共同点
1.二者都不能被实例化,意思也就是不能通过这个类直接定义对象,只能其他子类继承它之后,通过子类来定义对象。
2.二者中都可以进行抽象方法的定义(写一个函数但不写函数的内容),方便再后续子类的继承中可以对该函数进行多样化的续写(简称多态)
二者不同点
1.抽象类中允许定义一个具体的方法(写一个函数可以写出该函数的具体内容),而接口当中不能存在具体的方法,只能有抽象方法。
2.当一个类想要继承抽象类时,因为java是单继承,所以只能继承一个抽象类中的方法。 而当一个类需要继承接口时,它可以继承多个接口(如接口类定义中代码所示)。
3.抽象类的定义用abstract,接口的定义用interface,抽象类的继承用extends,接口的继承用implements,对方法进行重写都是@override。
3.对象的引用
Java中很多时候会在 等号右边调用构造函数创建一个对象 然后等号左边再创建一个对象去引用这个对象。
引用的作用是允许程序访问和操作位于内存中的对象。通过引用
,
可以调用这个类的成员方法或访问它的成员变量。
区分声明和创建实例
User abc;这是声明了一个User类的对象,要声明一个类的对象只需要在对应的文件上方导入User类的包就可以
User abc =new User();最右边的new User()调用了User类的构造函数,这才是创建了一个User类的实例,而一般情况下我们是声明对象和创建实例同时进行,并且把创建好的对象实例让声明的对象进行引用。
哪些情况下可以去进行这种引用:
(1)相同类型的对象
如果等号左边和右边的类型是相同的,那么赋值是完全合法的。
String str1 = new String("Hello");
String str2 = str1; // str2 和 str1 是相同类型
(2)右边是左边的子类
在面向对象编程中,如果右边的对象是左边类型的子类,那么这种赋值也是合法的。因为子类对象可以被父类的引用类型变量引用。这种情况下是面向对象中的“多态”特性。
Animal animal = new Dog(); // Dog 是 Animal 的子类
(3)右边实现了左边的接口
如果等号左边的引用类型是一个接口类型,而等号右边是一个实现了这个接口的类的对象,那么这种赋值是合法的。(这里所谓的实现了这个接口的类,指的就是继承了这个接口的类)
Runnable runnable = new MyRunnable(); // MyRunnable 实现了 Runnable 接口
静态方法创建的对象赋值给左边的引用
当你使用静态方法创建对象并将其赋值给一个引用时,这个引用的类型必须与静态方法返回的对象类型兼容。
ExecutorService executor = Executors.newFixedThreadPool(3);
- 等号右边:
Executors.newFixedThreadPool(3)
返回的类型是ExecutorService
的实现类对象(具体来说是ThreadPoolExecutor
)。 - 等号左边:
executor
是一个ExecutorService
类型的引用。
这种情况下,等号左边和右边的类型是兼容的,因为ThreadPoolExecutor
是ExecutorService
接口的实现类,而接口引用可以指向其实现类的对象。这里其实并不是第四种引用类型,依旧还是第三种引用类型。
4.Java中方法的修饰符
访问修饰符
Java中关于方法的访问修饰符限制了这些方法能被哪些类的对象访问:
(1)public
方法可以被任何类访问,无论它们位于同一个包中还是不同的包中。
(2)protected
同一个包中的类可以访问,其次就是子类可以访问(无论子类在不在同一个包当中),想被子类继承的方法但又不想被其他包中的类访问,就可以定义为protected。
(3)default
方法只能在同一个包中的类访问。
(4)private
方法只能在定义它的类内部访问。因为方法只能被它自己的类访问,所以子类不能重写该方法,即使子类定义了相同名字的方法,那也是一个与它毫无关系的新方法。
非访问修饰符
在访问修饰符之后可以有选择地添加非访问限制符用于控制方法的行为。
(1)static
将方法定义为类方法。静态方法可以通过类名直接调用,而无需实例化类(创建类的对象)。
静态方法只能访问静态变量和静态方法。
(2)final
防止方法被子类重写。final
方法可以被继承,但不能被修改。
(3)synchronized
用于线程同步,确保同一时间只有一个线程可以执行该方法。常用于多线程环境中保护共享资源。
不同修饰符的成员变量与不同修饰符的方法间的关系
修饰符 | public 方法 | protected 方法 | default 方法 | private 方法 |
---|---|---|---|---|
public 成员变量 | 可以访问 | 可以访问 | 可以访问 | 可以访问 |
protected 成员变量 | 可以访问 | 可以访问 | 同包内可以访问 | 仅类内可以访问 |
default 成员变量 | 可以访问 | 可以访问 | 同包内可以访问 | 仅类内可以访问 |
private 成员变量 | 仅类内可以访问 | 仅类内可以访问 | 仅类内可以访问 | 仅类内可以访问 |
说明:
public
:成员变量和方法对所有类、包和子类都可见,且无访问限制。protected
:成员变量和方法对同包中的类、子类可见,但对于非子类的外部类不可见。default
(包访问级别):未显式声明的修饰符,表示成员变量和方法对同一包中的类可见,但对包外的类不可见。private
:成员变量和方法仅对声明它们的类可见,无法被外部类访问,即使是子类。
5.Java包
(1)什么是包?
包其实就是一个文件夹,Java中为了方便管理,把你程序根目录下的某个文件夹叫做包。
举个例子,比如说你的程序在 D:\MyJavaProjects\MyApp 这个文件夹里,然后你设置了一个包为com.example.project,那么这个包就会对应你的电脑中的以下文件夹D:\MyJavaProjects\MyApp\src\com\example\project(这里中间多了个src夹层是因为你所使用的IDE自动管理的结果)
(2)包的使用
声明包:在Java文件的第一行,通过package
关键字声明该类所属的包。
如果在代码的第一行不去声明该类所属的包,则会导致这个文件下的代码会被放到默认包中,默认包中的代码不可以被其他的包导入使用。 一般只有小型的测试代码不需要去声明所属包,大型项目中的代码都需要声明所属的包。
package com.example.project;
public class MyClass {
// 类的内容
}
导入包:如果你想在一个类中使用另一个包中的类或接口,可以使用import
关键字。
import java.util.ArrayList;
public class MyOtherClass {
ArrayList<String> list = new ArrayList<>();
}
一个包中会包含很多类,但这里调用的时候必须置顶调用的是包中的哪个类,会在包名后跟类名的名字。
(3)包的作用
避免命名冲突:在大型项目中,不同开发者可能会编写名称相同的类。包通过提供命名空间,避免了这种冲突。
访问控制:包可以用于定义类的访问级别。包内的类可以访问其他包内的类,但外部包可能无法访问(取决于修饰符)。
6.Java中的异常处理
- 使用
try-catch
语句捕获和处理异常。try
块包围可能抛出异常的代码,catch
块用于处理该异常。finally
块中的代码无论是否发生异常都会执行,通常用于资源释放(如关闭文件、数据库连接等)。
try {
// 可能抛出异常的代码
} catch (IOException e1) {
// 处理IOException类型的异常
System.out.println("发生IO异常:" + e1.getMessage());
} catch (NullPointerException e2) {
// 处理NullPointerException类型的异常
System.out.println("空指针异常:" + e2.getMessage());
} finally {
// 必须执行的清理代码
System.out.println("无论如何都会执行的代码");
}
catch后面的括号中跟的是 异常类型 和 实例对象名称 ,e1就是引用的发生的异常的对象,e1在当前这个catch块中就代表了这个异常的对象,所有的异常都是异常类的一个对象,故也可以调用类的方法,在这里就调用了异常类的getMessage()方法,一般也只会用到这个。
至于异常类型,其实就是对应异常类的名称,常见的异常类型有以下几种:
(1)IOException
- 表示I/O操作失败或中断的异常,如文件未找到、无法读取文件等。
(2)SQLException
- 表示数据库操作中发生错误的异常,如数据库连接失败、SQL语法错误等。
(3)ClassNotFoundException
:
- 当应用程序尝试加载不存在的类时抛出,如使用
Class.forName()
加载类时找不到类文件
(4)InstantiationException
:
- 当试图通过反射机制创建一个抽象类或接口的实例时抛出。
(5)FileNotFoundException
:
- 当尝试打开文件路径错误或文件不存在时抛出,是
IOException
的子类。
7.链式调用
1. 链式调用的工作原理
在Java中,链式调用通常通过在每个方法末尾返回当前对象的引用(this
)来实现。这种方式允许连续调用多个方法而无需中断表达式,创建了一种流畅和声明式的代码风格。
public class Person {
private String name;
private int age;
public Person setName(String name) {
this.name = name;
return this;
}
public Person setAge(int age) {
this.age = age;
return this;
}
@Override
public String toString() {
return "Person{name='" + name + '\'' + ", age=" + age + '}';
}
}
使用此类的例子:
Person person = new Person()
.setName("John")
.setAge(30);
System.out.println(person);
2. 链式调用的优势
- 易于阅读和维护:链式调用减少了代码量,使得对象的配置步骤一目了然,易于阅读和维护。
- 增强代码流畅性:链式方法调用在构建流畅的API时非常有效,尤其是在构建DSL(领域特定语言)或者构建者(Builder)模式时。
- 减少临时变量:链式调用减少了临时变量的需要,因为你不需要单独存储中间结果就能进行多步操作。
3. 链式调用的应用场景
- Builder 模式:在创建复杂对象时,Builder模式经常使用链式调用来设置对象的多个属性。
- 流式接口(Fluent Interface):在设计API时,流式接口使用链式调用提供更加流畅的使用体验。
- 配置和设置:在需要多个配置步骤的情况下,链式调用可以使代码更加整洁和易于管理。
4. 注意事项
- 链中的异常处理:如果链式调用中的一个方法抛出异常,它可能会中断整个链并且需要合适的异常处理策略。
- 返回值类型:每个方法在返回
this
之前需要保证不会改变对象的状态,从而引发错误的结果。 - 调试:链式调用可能会使得调试变得更复杂,因为多个操作在单个表达式中完成。
8.Java反射
(1)反射的定义
反射主要依赖于java.lang.reflect
包中的类,例如Class
、Method
、Field
、Constructor
等。反射就是通过创建这些类的对象指向其他类。
反射具有的最明显特征就是动态性,你在编译的时候并不会知道要创建什么类的对象,知道程序在运行是,通过你写的类的名称、方法的名称、属性的名称才知道要创建什么类的对象,调用类的什么方法,修改类的什么属性,编译器才运行之前无法知道这些,只有在程序运行到那一行拿到那个名字去匹配才可以知道,这就叫动态性。
(2)反射的用法
1.找到类和类中属性、方法的信息
Class<?> clazz = Class.forName("com.example.MyClass"); // 找到这个电器(类)
Field[] fields = clazz.getDeclaredFields(); // 看看它有哪些“按钮”(属性)
Method[] methods = clazz.getDeclaredMethods(); // 看看它有哪些“功能”(方法)
创建为Class类的对象clazz之后,就可以调用Class类的getDeclaredFields()和getDeclaredMethod()方法将得到的关于MyClass类的内部变量(属性)的信息放入fields这个数组当中,将关于MyClass类的方法的信息放入methods这个数组当中,后续再调用Field类的方法和Method类的方法来对这些信息进行查看。
2.动态创建对象
MyClass myObject = (MyClass) clazz.newInstance(); // 动态创建一个对象(打开电器)
先调用Class类的方法 newInstance()来创建一个object类型的对象,再强制转换为MyClass类型的对象,然后再被创建的MyClass类型的对象myobject引用,这里所谓的动态创建对象就是指通过clazz.newInstance()创建对象,因为你在编译的时候不能确定会创建哪个类的对象,所以具有动态性,个人觉得很鸡肋。
3.调用方法
Method method = clazz.getDeclaredMethod("turnOn"); // 找到“开关”按钮(方法)
method.invoke(myObject); // 按下去(调用方法)
之前clazz已经指向过某个类了,所以在这里就可以根据方法的名称 turnOn,指向Class类的方法getDeclaredMethod去获取clazz指向的类中名叫turnOn的方法,并且创建Method类的对象method来引用这个方法。
引用了这个方法之后,想要使用这个方法就需要调用Method类的invoke方法
invoke
是Method
类中的一个方法,它的作用是调用Method
类的对象所代表的方法。myObject
是你希望在其上调用该方法的对象。换句话说,这里你在myObject
上调用了turnOn
方法。(这里的myObject对象可以是你动态创建的对象,也可以是你正常创建的Myclass类的对象,就比如Myclass myobject = new Myclass();这样正常创建的myobject对象,也可以通过以上的方式去在object上调用turnOn方法。- 如果
turnOn
方法需要参数,你可以在invoke
方法中传递这些参数,例如method.invoke(myObject, arg1, arg2)
,其中arg1
和arg2
是传递给turnOn
方法的实际参数。- 如果
turnOn
方法有返回值,invoke
会返回该方法的返回值。
4.修改属性
Field field = clazz.getDeclaredField("volume"); // 找到“音量”按钮(属性)
field.setAccessible(true); // 打开隐藏的按钮
field.set(myObject, 10); // 调低音量(修改属性)
这里Field类的对象field引用了clazz对应的类中名叫volume的属性(变量),然后再调用Field类的方法来对这个变量的值实施更改,关于这段代码 field.set(myObject, 10); 需要说明的是这里的myobject和上面调用方法那部分说的一样,可以是动态创建的对象,也可以是Myclass myobject = new Myclass()这样正常创建的对象。
9.什么叫正则表达式
正则表达式简单来讲就是创建Pattern类的一个对象,这个对象通过调用该类的方法,规定了你要寻找的字符串的格式,然后再创建一个Matcher 类的对象,调用Matcher 类的方法去在整个内容中去匹配寻找符合该格式的内容。你要寻找的字符串的格式是怎样的需要你按照一定的规则书写下来。
10.枚举类的作用
在写代码的时候,我们难免需要对使用一些客观存在且固定的名字,比如说一周只有七天,且星期一就是MONDAY而不是MOUDAY这种,我们为了防止我们使用的时候出现错误,就会写出来一个类来使用这些固定的名字,代码示例如下:
public enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
Day day = Day.MONDAY; // 类型安全,确保 day 变量只能接受 Day 类型的值
使用的时候因为是通过Day.MONDAY这种形式来来使用这个名字,所以当你写成DAY.MOUDAY时,系统会检测到枚举类中没有这个名字然后报错来提醒你。
二、Java中的输入和输出
1.对于控制台
Scanner
是一个高层次的工具,适合处理简单输入并且能够方便地解析各种基本数据类型。BufferedReader
更加低层次,但性能更高,适合处理大量数据和文件读取。
(1)使用Scanner类输入
代码示例:
import java.util.Scanner;
public class ScannerExample {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("Enter your name: ");
String name = scanner.nextLine();
System.out.print("Enter your age: ");
int age = scanner.nextInt();
System.out.println("Hello, " + name + ". You are " + age + " years old.");
}
}
这里使用Scanner类,要通过system.in作为输入流来创建Scanner对象,然后再通过调用Scanner类的各种方法,包括nextLine()来获取输入流(你在控制台的输入)中的内容,这里存在的缺点就是,你自己要去辨别你需要读取的内容是属于什么类型的数据,比如示例代码中的nextline()
(2)使用BufferedReader类输入
代码示例:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
public class BufferedReaderExample {
public static void main(String[] args) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
System.out.print("Enter your name: ");
String name = reader.readLine();
System.out.print("Enter your age: ");
int age = Integer.parseInt(reader.readLine());
System.out.println("Hello, " + name + ". You are " + age + " years old.");
}
}
这里的throws IOException只是为了告诉看你代码的人,这个地方可能会产生异常,并且产生异常之后整个程序也不会直接崩溃。
BufferedReader(new InputStreamReader(System.in)),这里的InputStreamReader类包装会将字节流转换成字符流,BufferedReader类包装会将字符流再进行缓冲,让BufferedReader类的对象可以调用特定方法一次性读取,提高效率(就比如这里的reader.readLine()就可以直接读取一行了)
这里的Integer.parseInt(reader.readLine())是调用了Integer类的parseInt方法,将字符流数据转换成数字,这里就涉及了一个知识点,为什么一个类中的方法可以不通过对象来调用,而是直接通过类名就调用了,因为parseInt方法是该类的静态方法,所以可以通过类名直接调用。
(3)输出
System.out.print()
和 System.out.println()
System.out.print()
:输出内容,但不换行。System.out.println()
:输出内容,并在末尾添加换行符。
return
是用于将结果返回给客户端,是用户可以看到的响应。System.out.println
是用于在服务器端输出调试信息,帮助开发者了解程序的运行状态,不会被用户看到。
2.对于文件
(1)使用 FileReader 和 BufferedReader类输入
示例代码:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class FileReadExample {
public static void main(String[] args) {
String filePath = "example.txt"; // 文件路径
try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
FileReader
:用于打开文件,并将其作为字符流处理。BufferedReader
:包装FileReader
,提供更高效的读取操作和readLine()
方法,可以逐行读取文件内容。try
语句后面紧跟一个圆括号,括号内声明的资源会在try
块执行完毕后自动关闭。
(2)使用Files类输入
示例代码:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
public class FilesReadExample {
public static void main(String[] args) {
String filePath = "example.txt"; // 文件路径
try {
List<String> lines = Files.readAllLines(Paths.get(filePath));
for (String line : lines) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
先调用Paths类的get()方法,将文件路径转换成一个Paths对象,然后readAllLines()方法作用在该Paths文件对象上。
调用了数据结构中的线性表List,将读取到的文件的每一行都存入线性表的每一个元素当中,然后紧接着使用增强型for循环来遍历线性表lines中的每一个元素,将每一个元素都自动赋值给line。
(3)输出
使用 FileWriter
和 PrintWriter
FileWriter
:用于写入字符到文件。可以选择覆盖(默认)或追加模式写入。PrintWriter
:提供了更高层次的字符输出,允许使用print()
和println()
方法写入数据。
示例代码:
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
public class FileOutputExample {
public static void main(String[] args) {
String filePath = "output.txt";
// 使用 try-with-resources 确保文件写入完成后自动关闭
try (FileWriter fileWriter = new FileWriter(filePath, true); // true 表示追加模式
PrintWriter printWriter = new PrintWriter(fileWriter)) {
printWriter.println("Hello, this is written to the file.");
printWriter.println("This is another line in the file.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
对于以上代码的详细解读:
-
FileWriter fileWriter = new FileWriter(filePath, true);
:- 创建一个
FileWriter
对象fileWriter
,用于将文本数据写入到指定的文件output.txt
中。 true
参数表示追加模式。如果文件已存在,新的内容将被追加到文件的末尾,而不是覆盖文件中的已有内容。如果省略或设置为false
,则会覆盖文件内容。
- 创建一个
-
PrintWriter printWriter = new PrintWriter(fileWriter);
:- 创建一个
PrintWriter
对象printWriter
,它包装了fileWriter
,提供了更方便的文本写入方法,比如print()
和println()
。 PrintWriter
提供了自动刷新缓冲区的功能,因此它在写入行时更加方便和安全。
- 创建一个
三、类的继承
1.子类中需要先调用父类的构造函数
父类与子类的构造函数是分开的,但是在子类的构造函数中,需要先调用父类的构造函数,然后再去完成子类的初始化。
(1)父类中如果有无参的构造函数
那在子类中调用不调用父类的构造函数都没有问题,如果你没有通过super()这个函数来显式地调用父类地构造函数,编译器也会去自动调用。(这里super()这个函数出现在子类地构造函数中就是去调用父类的构造函数的意思)。
(2)父类中只有有参的构造函数
这个时候就必须用super()这个函数显式地调用父类地构造函数,并在super后面的括号里填入父类构造函数所需要的参数。
class Parent {
Parent() {
System.out.println("Parent constructor");
}
}
class Child extends Parent {
Child() {
super(); // 显式调用父类的无参构造函数
System.out.println("Child constructor");
}
}
public class Main {
public static void main(String[] args) {
Child child = new Child();
}
}
这段程序最后打印的结果是:
Parent constructor
Child constructor
2.重写与重载
(1)重写
定义:重写是指子类重新定义从父类继承的方法,目的是修改或扩展父类方法的行为。
重写的方法必须与被重写的方法具有相同的方法名、参数列表和返回类型。
访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。
使用@Override
注解标识重写的方法。
在子类中重写父类方法时,子类方法的访问级别不能低于父类方法的访问级别。
四种方法的修饰符访问级别顺序(由高到低):
public
: 最高级别,方法可以被任何地方访问。protected
: 次高,可被同一包内的类和子类访问(即使子类在不同包中)。default
(包级私有,未显式修饰符): 可被同一包内的类访问,但不同包的子类不能访问。private
: 最低级别,只能在定义它的类中访问,子类不能继承或重写。
(2)重载
定义:重载是指在同一个类中可以定义多个方法,这些方法具有相同的方法名,但参数列表不同(参数的类型、数量或顺序不同)。
重载的方法可以具有不同的参数类型、参数数量或参数顺序,可以有不同的返回类型。
重写主要关注的是继承体系中的子类和父类之间的关系,而重载则主要关注同一个类中方法的多样性。
3.多态的定义
当你在子类中重写父类的方法时,即使创建的是父类类型的对象,但如果这个对象实际指向的是子类的实例,调用的依然是子类中重写后的方法。这是因为Java支持运行时的多态性,方法调用的决定是在运行时根据对象的实际类型来决定的,而不是根据引用类型。
(1)编译时:你可以将子类对象赋值给父类引用。
Parent p = new Child();
(2)运行时:当通过父类引用创建的对象调用父类中的方法时,实际调用的是子类当中对于父类中对应方法的重写方法(前提是子类中对于该方法重写了,如果没有重写调用的还是父类中的方法)。
并且如果你调用的方法在父类中不存在,只在子类中存在,那编译会报错(这就是父类引用会造成的效果,你只能调用父类中的方法,但如果你调用的方法在子类中被重写了,则会调用子类中被重写的方法)。
(3)在Java中,所有非final
、非static
和非private
的方法都可以被子类重写,并且这些方法在被子类重写时会表现出多态性。
四 、多线程编程
1.多线程编程的定义
多线程编程就是指不同线程的代码可以在同一时间并发执行。
在一个单核CPU的情况下,计算机实际上只能在任一时刻执行一个线程的代码,但操作系统会非常快速地切换线程,使得看起来像是多个线程在同时运行。这种快速的切换称为时间片轮转(time slicing)。
在多核CPU上,多线程可以真正实现并行,因为每个核可以同时运行一个线程的代码。如果你有两个CPU核,那么两个线程就可以同时运行,而无需等待另一个线程“休息”。
2.多线程编程的作用
要了解多线程编程的作用我们首先需要了解:在Java程序中,主线程是程序启动时自动创建的线程。它是执行
main()
方法的线程。而如果程序如果只有主线程,会大大降低程序运行的效率,这个时候就需要引入其他线程,并且让这些线程能够和主线程并发执行,提高效率。
(1)提高程序的响应性:在图形用户界面(GUI)应用中,多线程可以保证用户界面在执行耗时操作时仍然保持响应。
问题背景:
- 在图形用户界面(GUI)应用中,用户界面通常由主线程(也称为UI线程)控制。如果在这个主线程中执行耗时的操作,例如读取大文件、下载文件或者复杂的计算,UI线程会被占用,这时整个界面可能会卡顿,甚至出现“未响应”的情况。
多线程的作用:
- 通过将耗时操作放到其他线程中执行,主线程可以继续处理用户的输入和界面更新,从而保持程序的响应性。
详细解释:
- 主线程(UI线程):负责处理用户输入、按钮点击、菜单选择等界面操作。它应当尽量快速地处理这些事件,以保证界面流畅。
- 其他线程:用来执行那些可能会导致界面卡顿的耗时操作,如文件读取、网络请求等。
(2)利用多核处理器的优势:现代计算机通常具有多个CPU或多个内核,通过多线程编程,程序可以在多个核上并行执行,从而提高执行速度。
(3)简化模型:将复杂的任务分解成多个并发执行的子任务,简化了程序的设计和实现。
(4)处理大量I/O操作:在网络编程或文件操作中,多线程可以有效地处理大量I/O操作而不阻塞主线程。
问题背景:
- 在网络编程或文件操作中,I/O操作可能会非常耗时。如果所有这些操作都在主线程中进行,主线程会被阻塞,无法执行其他任务,这在处理大量并发请求时尤其成问题。
多线程的作用:
- 多线程可以让I/O操作在后台异步执行,这样主线程不会被阻塞,可以继续处理其他任务。多个I/O操作可以并发进行,从而提高程序的吞吐量和效率。
详细解释:
- 主线程:负责接收和分配任务,协调各个线程的工作,确保程序整体的流畅运行。
- I/O线程:负责执行具体的I/O操作,例如读取数据、发送和接收网络请求等。多个I/O操作可以同时进行,不会互相影响。
3.多线程编程的使用方法
线程的
start()
和run()
方法
start()
方法:当你调用thread1.start()
或thread2.start()
时,这些线程会进入就绪状态,等待操作系统的调度。它们并不是立刻开始运行,而是等待系统分配时间片来执行。这些线程是独立的,调用start()
并不会直接引发线程之间的交互或异常处理。
run()
方法:run()
方法是每个线程的执行体,当线程获得CPU资源后,run()
方法中的代码将开始执行。在这个方法中,如果你调用了Thread.sleep()
,这个线程会进入休眠状态,暂时让出CPU资源给其他线程。
(1)实现Runnable
接口
这是一种较为常用且推荐使用的方法 ,Runnable
接口是一个函数式接口,只包含一个run()
方法,在这个方法中定义线程要执行的任务。
代码示例:
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程要执行的任务
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
try {
Thread.sleep(1000); // 让线程休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread1 = new Thread(myRunnable); // 创建第一个线程
Thread thread2 = new Thread(myRunnable); // 创建第二个线程
thread1.start(); // 启动第一个线程
thread2.start(); // 启动第二个线程
}
}
先创建一个类MyRunnable去继承接口Runnable,然后再重写run()方法,这个方法中就是你这个线程中需要执行的操作,然后后面的try...catch...是为了让你的系统能将时间调度分配给其他并发执行的线程,这是适应单核cpu的情况,一个线程的时间片用完然后其他线程获得时间片开始执行,后面catch中的错误是为了防止该线程在wait()或sleep()阻塞休息的时候被打断。
在主线程main函数中,需要先创建MyRunnable的对象,然后再通过这个对象来创建进程,上面的代码创建进程时传入的是同一个MyRunnable的对象,所以那两个进程执行的操作一样。
如果你需要两个进程执行的操作不一样这里有两种办法:
1.你写接口Runnable的继承类的时候就不要只写一个继承类,你写个MyRunnable1和MyRunnable2两个继承类,然后继承类中的run()方法的内容写的不一样,然后在main函数中创建两个类各自的对象,然后创建两个的进程并传入这两个继承类各自的对象,这样就达成了两个进程执行不同操作的效果。
2.也可以只写一个接口Runnable的继承类,但是要写这个继承类的含参构造函数,然后在run()方法中加入判断语句,根据构造函数传入的参数的不同从而进行不同的操作。
人
然后再在主线程(main函数)中启动这两个线程(thread.start())就行。
(2)继承Thread
类
你需要创建一个新的类,继承Thread
类,然后重写run()
方法,在这个方法中定义线程要执行的任务。
代码示例:
class MyThread extends Thread {
@Override
public void run() {
// 线程要执行的任务
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
try {
Thread.sleep(1000); // 让线程休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
MyThread thread1 = new MyThread(); // 创建第一个线程
MyThread thread2 = new MyThread(); // 创建第二个线程
thread1.start(); // 启动第一个线程
thread2.start(); // 启动第二个线程
}
}
与上面的使用方法类似,只不过这次是编写
Thread
类的继承类,并且在主线程(main函数)是直接创建该继承类的两个对象(两个线程),然后都start,然后系统就会自动调度这两个线程使得他们并发执行,线程需要执行得操作依旧是在run()方法中进行编写。以上代码得两个线程进行得操作是相同的,如果想实现不同的操作依旧跟上面的方法类似,要么是编写两个继承类,然后创建两个继承类各自的对象,要么就还是编写一个继承类带参的构造函数,然后run()方法中再加入判断语句实现不同的操作。
(3)使用线程池(ExecutorService)
对于需要频繁创建和销毁线程的场景,使用线程池可以显著提高性能。Java提供了ExecutorService
接口来管理线程池,Executors
类提供了工厂方法来创建线程池。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class TaskA implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " - TaskA - " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class TaskB implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " - TaskB - " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3); // 创建一个固定大小为3的线程池
executor.execute(new TaskA()); // 提交TaskA给线程池执行
executor.execute(new TaskB()); // 提交TaskB给线程池执行
executor.shutdown(); // 关闭线程池
}
}
这里在最开始依旧是两个不同的类继承了Runnable接口,至于这两个类里面的run()方法中存在循环,是因为想让你在这段代码运行打印的时候让你看到这两个运行并发进行的过程,这个不重要。
紧接着在main函数中先通过调用Executors类的静态方法newFixedThreadPool(3)创建一个大小为3的线程池,这里线程池的大小指的是可以同时并发执行的线程的最大数量,所以后面执行的线程数大于或小于这个数都没有关系,如果小于这个数,就证明你的线程都可以同时执行,如果大于这个数就证明你提交执行的线程中有一部分需要等待。
因为右边静态方法的返回对象是左边接口类的实现类(继承类)的对象,所以这里可以实现引用。
然后调用在接口ThreadPoolExecutor中定义,在静态方法返回对象的类ThreadPoolExecutor中来实现的 方法execute()来执行这两个线程,跟Threat类中的start()方法类似。
shutdown()
:这个方法会将线程池标记为关闭状态。它会等待所有已经提交的任务(包括在队列中等待的任务)执行完毕后才会彻底关闭线程池。注意,shutdown()
不会中断正在执行的任务,只是拒绝接受新任务。
线程同步
在多线程编程中,多个线程可能会共享同一个资源,比如一个变量或对象。这时如果多个线程同时访问该资源,可能会导致数据不一致的问题。为了防止这种情况发生,我们需要使用线程同步机制。
同步代码块:
synchronized
关键字可以用于同步代码块,保证同一时间只有一个线程可以执行同步块中的代码。
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
class MyRunnable implements Runnable {
private Counter counter;
public MyRunnable(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread thread1 = new Thread(new MyRunnable(counter));
Thread thread2 = new Thread(new MyRunnable(counter));
thread1.start();
thread2.start();
thread1.join(); // 等待线程1执行完毕
thread2.join(); // 等待线程2执行完毕
System.out.println("最终计数值:" + counter.getCount());
}
}
最开始先写了个Counter类,Counter类中的方法increment()前面加了synchronized修饰符,确保这个方法同一时间只能被一个进程使用,然后写了Runnable接口的实现类MyRunnable
在MyRunnable类中又写了参数为Counter类对象的构造函数,然后在主线程(main函数)中
,创建Counter类的对象,然后再创建Thread类的对象(这里之气说过,Thread有构造函数参数为Runnable对象)然后两个线程开始执行,后面的join()方法的意思是,主线程(main函数)如果没有执行完调用join()方法的这个线程就不会往下执行,所以这里join的意思就是让主线程(main函数)不会往下进行,直到thread1和thread2这两个线程执行完毕才会继续执行main函数下面的代码。
五、Java中的数据结构
- List: 按顺序存放元素的数据结构。
- ArrayList: 基于动态数组实现,元素存储在连续的内存地址中,这使得访问元素非常快速。但在中间插入或删除元素的速度较慢,因为这涉及到元素的移动。
- LinkedList: 每个元素都包含前后元素的引用,不存储在连续的内存地址中。这使得添加和删除元素非常快,但访问特定元素的速度较慢,因为需要从头开始遍历。
- Map: 一个存储键值对的数据结构,每个键对应一个值。
- HashMap: 基于哈希表实现,使用键的哈希码映射到数组中的位置(称为“桶”)。如果多个键映射到同一个哈希码,它们会在同一个桶中以链表或树形结构存储,这可能导致查找速度下降。
- TreeMap: 基于红黑树实现,自动根据键的自然顺序或比较器来排序。红黑树结构确保了在最坏情况下查找、插入和删除操作的时间复杂度保持在对数级别。
在 Java 中,ArrayList
和 LinkedList
都实现了 List
接口,而 HashMap
和 TreeMap
都实现了 Map
接口。这意味着你可以使用 List
和 Map
接口类型来引用这些具体的实现类的对象。
List<String> list = new ArrayList<>(); // 使用 ArrayList 实现 List 接口
list.add("Hello");
list.add("World");
List<String> linkedList = new LinkedList<>(); // 使用 LinkedList 实现 List 接口
linkedList.add("Hello");
linkedList.add("Java");
Map<String, Integer> hashMap = new HashMap<>(); // 使用 HashMap 实现 Map 接口
hashMap.put("apple", 1);
hashMap.put("banana", 2);
Map<String, Integer> treeMap = new TreeMap<>(); // 使用 TreeMap 实现 Map 接口
treeMap.put("one", 1);
treeMap.put("two", 2);
1. 数组(Array)
定义与作用
- 数组是一个固定长度的容器,用于存储同一类型的元素。
- 数组在内存中是连续分配的,因此可以通过索引快速访问元素,具有O(1)的访问时间复杂度。
- 数组适用于需要快速访问元素但不需要频繁插入或删除操作的场景。
int[] numbers = new int[5]; // 创建一个长度为5的整数数组
numbers[0] = 10; // 向数组中添加元素
int firstNumber = numbers[0]; // 访问数组中的元素
优点
- 访问速度快:数组在内存中是连续分配的,可以通过索引快速访问元素。
- 简单易用:适合存储固定数量的元素。
缺点
- 长度固定:数组的大小在创建后不能改变。
- 插入和删除操作不便:在数组中插入或删除元素需要移动其他元素,时间复杂度为O(n)。
2. ArrayList
定义与作用
- ArrayList是Java中的动态数组,位于
java.util
包中。 - 它的大小是可变的,可以根据需要动态调整。
ArrayList
适用于频繁读操作、偶尔插入和删除的场景。
import java.util.ArrayList;
ArrayList<String> list = new ArrayList<>();
list.add("apple"); // 添加元素
list.add("banana");
String fruit = list.get(0); // 通过索引访问元素
list.remove("banana"); // 移除元素
优点
- 动态调整大小:
ArrayList
会自动调整其大小以适应元素的数量。 - 随机访问性能好:与数组类似,
ArrayList
允许快速的随机访问。
缺点
- 插入和删除效率低:在中间位置插入或删除元素时,需要移动其他元素。
- 线程不安全:
ArrayList
在多线程环境下需要手动同步。
3. LinkedList
定义与作用
- LinkedList是Java中的双向链表,位于
java.util
包中。 - 它由一系列节点组成,每个节点包含一个元素和指向前后节点的指针。
LinkedList
适用于频繁插入和删除操作的场景,特别是在中间位置的操作。
import java.util.LinkedList;
LinkedList<String> list = new LinkedList<>();
list.add("apple"); // 添加元素
list.addFirst("banana"); // 在列表开头添加元素
String fruit = list.get(0); // 访问元素
list.removeLast(); // 移除最后一个元素
优点
- 插入和删除操作效率高:在链表的任意位置插入或删除元素都可以在O(1)时间内完成。
- 动态大小:链表的大小是动态的,可以根据需要扩展。
缺点
- 随机访问效率低:无法像数组一样通过索引快速访问元素,必须从头开始遍历。
- 内存开销大:每个节点除了存储数据外,还需要存储前后指针,增加了内存消耗。
4.HashMap
(1)HashMap的定义
HashMap
是Java集合框架中的一个重要类,它提供了一个哈希表(Hash Table)的实现,允许我们以键值对的形式存储数据。HashMap
是一个无序的集合,存储的顺序并不保证与插入顺序一致。
(2)HashMap的基本特性
键值对存储:
HashMap
以键值对(key-value)的形式存储数据。每个键(key)都是唯一的,而每个键都对应一个值(value)。
允许null值:
HashMap
允许一个null
键和多个null
值。这意味着你可以使用null
作为键,且对应的值可以为null
。
无序存储:
HashMap
不保证存储的顺序。键值对的顺序可能与插入的顺序不同,HashMap
通过哈希表存储数据,具体的存储位置由键的哈希码决定。
非线程安全:
HashMap
是非线程安全的,这意味着在多线程环境下同时修改HashMap
可能会导致数据不一致。如果需要在多线程环境下使用,可以使用ConcurrentHashMap
或者在外部对HashMap
进行同步。
(3)HashMap的基本使用
创建一个HashMap
import java.util.HashMap;
public class Example {
public static void main(String[] args) {
// 创建一个HashMap对象
HashMap<String, String> map = new HashMap<>();
}
}
向HashMap中添加键值对
// 向map中添加元素
map.put("Java", "A programming language");
map.put("Python", "Another programming language");
map.put("JavaScript", "A language for web development");
put(K key, V value)
:将键值对插入到HashMap
中。如果键已经存在,那么对应的值会被新的值替换。
从HashMap中获取值
// 根据键获取对应的值
String javaDesc = map.get("Java");
System.out.println("Java: " + javaDesc);
get(Object key)
:根据键获取对应的值。如果键不存在,返回null
。
检查HashMap中是否包含某个键或值
boolean hasJava = map.containsKey("Java"); // 检查是否包含键"Java"
boolean hasPythonDesc = map.containsValue("Another programming language"); // 检查是否包含某个值
containsKey(Object key)
:检查HashMap
中是否包含指定的键。containsValue(Object value)
:检查HashMap
中是否包含指定的值。
删除HashMap中的元素
// 根据键删除对应的键值对
map.remove("Java");
remove(Object key)
:根据键删除对应的键值对。如果键存在,删除后返回该键对应的值;如果键不存在,返回null
。
遍历HashMap
// 遍历所有的键值对
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
entrySet()
:返回一个包含Map.Entry
对象的集合,Map.Entry
表示HashMap
中的一个键值对。可以通过entrySet()
遍历所有的键值对。
(4)HashMap的作用
高效的数据存储和检索:
HashMap
使用哈希表来存储数据,能够在O(1)时间复杂度内完成插入、删除和查找操作,因此非常适合需要快速查找的场景。
键值对的映射:
HashMap
提供了一种将唯一键映射到特定值的方式。例如,可以使用HashMap
来存储用户名和用户详细信息之间的映射关系。
缓存:
HashMap
可以用作缓存,在内存中存储一些数据,以便快速访问。例如,可以用来缓存数据库查询结果。
统计和计数:
可以用HashMap
来统计元素出现的次数。通过将元素作为键,出现次数作为值,可以轻松实现计数功能。
(5)使用HashMap
时的注意事项
避免使用可变对象作为键:
如果你使用一个可变对象(如一个对象实例)作为HashMap
的键,且该对象的属性在存入HashMap
后发生了变化,可能会导致哈希码变化,进而导致HashMap
无法正确找到对应的值。因此,尽量使用不可变对象(如String
)作为键。
多线程环境下的使用:
HashMap
在多线程环境下是非线程安全的。如果多个线程同时访问和修改同一个HashMap
,可能会导致数据不一致或抛出ConcurrentModificationException
。在这种情况下,可以使用ConcurrentHashMap
或者使用Collections.synchronizedMap()
来创建线程安全的HashMap
。
5. HashSet
定义与作用
- HashSet是Java中基于哈希表实现的集合,位于
java.util
包中。 - 它只存储唯一的元素,没有重复值。
HashSet
适用于需要快速去重或快速查找唯一元素的场景。
import java.util.HashSet;
HashSet<String> set = new HashSet<>();
set.add("apple"); // 添加元素
set.add("banana");
set.add("apple"); // 尝试添加重复元素,不会添加
boolean exists = set.contains("banana"); // 检查元素是否存在
优点
- 快速查找和去重:
HashSet
的查找、插入和删除操作时间复杂度为O(1)。 - 唯一性:确保集合中的元素没有重复。
缺点
- 无序:
HashSet
中的元素是无序的。 - 线程不安全:
HashSet
在多线程环境下需要手动同步。
6. TreeMap
定义与作用
- TreeMap是Java中的红黑树实现,位于
java.util
包中。 - 它存储键值对,并按键的自然顺序或指定的比较器顺序排序。
TreeMap
适用于需要有序键值对的场景。
import java.util.TreeMap;
TreeMap<String, Integer> map = new TreeMap<>();
map.put("apple", 1); // 添加键值对
map.put("banana", 2);
int value = map.get("apple"); // 通过键访问值
map.remove("banana"); // 移除键值对
优点
- 有序:
TreeMap
中的元素按键的自然顺序或比较器顺序排序。 - 范围操作:支持操作如
subMap
、headMap
、tailMap
。
缺点
- 性能较低:插入、删除、查找操作时间复杂度为O(log n)。
- 内存开销大:红黑树结构相比哈希表占用更多内存。
7. PriorityQueue
定义与作用
- PriorityQueue是Java中的优先级队列实现,位于
java.util
包中。 - 它存储元素,并根据自然顺序或指定的比较器顺序排序,保证队列的头元素始终是最小(或最大)的元素。
PriorityQueue
适用于需要动态排序或频繁获取最小/最大元素的场景。
import java.util.PriorityQueue;
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.add(3); // 添加元素
pq.add(1);
pq.add(2);
int min = pq.poll(); // 获取并移除最小元素
优点
- 动态排序:自动维护队列中元素的顺序。
- 高效的最小/最大值操作:获取最小/最大元素的时间复杂度为O(1)。
缺点
- 无序遍历:遍历
PriorityQueue
时,元素的顺序不一定是有序的。 - 随机访问效率低:
PriorityQueue
不支持高效的随机访问操作。
8. Stack
定义与作用
- Stack是Java中的栈实现,位于
java.util
包中。它继承自Vector
。 - 栈是一种先进后出(LIFO)的数据结构。
Stack
适用于递归、撤销操作、表达式求值等场景。
import java.util.Stack;
Stack<Integer> stack = new Stack<>();
stack.push(1); // 压入元素
stack.push(2);
int top = stack.pop(); // 弹出栈顶元素
优点
- 简单易用:提供基本的栈操作方法,如
push
、pop
、peek
。 - 适合LIFO操作:非常适合处理需要先进后出的场景。
缺点
- 线程安全导致性能较低:
Stack
是同步的,线程安全性会导致一些性能开销。 - Vector的遗留问题:
Stack
继承自Vector
,而Vector
是一个比较旧的同步类,在多线程场景下可能存在性能问题。
9. Deque(双端队列)
定义与作用
- Deque是Java中的双端队列接口,常见的实现类包括
ArrayDeque
和LinkedList
。 - 双端队列允许在两端插入和移除元素,既可以作为队列(FIFO),也可以作为栈(LIFO)使用。
Deque
适用于需要灵活操作的场景,如浏览器历史、任务调度等。
import java.util.ArrayDeque;
import java.util.Deque;
Deque<Integer> deque = new ArrayDeque<>();
deque.addFirst(1); // 在队列头部添加元素
deque.addLast(2); // 在队列尾部添加元素
int first = deque.removeFirst(); // 移除头部元素
int last = deque.removeLast(); // 移除尾部元素
优点
- 灵活:可以在两端进行插入和删除操作。
- 高效:相比
Stack
和LinkedList
,ArrayDeque
在大多数场景下性能更好。
缺点
- 无随机访问:无法像数组或
ArrayList
那样通过索引随机访问元素。
10. 总结
Java提供了多种数据结构,每种数据结构都有其独特的优势和适用场景。选择合适的数据结构是编写高效程序的关键。以下是一些常见场景和适用的数据结构:
- 快速访问、固定大小的元素集合:使用
数组
或ArrayList
。 - 频繁插入、删除操作:使用
LinkedList
。 - 键值对存储:使用
HashMap
或TreeMap
。 - 去重:使用
HashSet
。 - 有序数据:使用
TreeMap
或PriorityQueue
。 - LIFO操作:使用
Stack
或Deque
。 - FIFO操作:使用
Deque
。
六、Java中this
的使用详细讲解
this
关键字在Java中是非常重要的,它的主要功能是引用当前类的实例(也就是当前对象)。this
可以用于不同的场景,主要用于区分成员变量和局部变量或参数名的冲突,也可以在构造函数中调用其他构造函数,或者将当前实例作为参数传递给其他方法。
this
的几种常见使用场景:
1.区分成员变量和局部变量/参数
当构造函数或者方法中的参数名称与类的成员变量名称相同时,this
可以用于区分它们。
例如:
public class User {
private String name;
// 构造函数
public User(String name) {
this.name = name; // this.name 指的是成员变量,name 是构造函数的参数
}
}
在这个例子中,
this.name
指的是当前对象的成员变量name
,而name
是构造函数的参数。通过使用this
,可以明确区分两者,避免名称冲突。
2.在构造函数中调用其他构造函数
在Java中,this()
可以用于在一个构造函数中调用同一个类中的另一个构造函数。这样可以减少代码重复,并允许多个构造函数共享初始化逻辑。
例如:
public class User {
private String name;
private int age;
// 无参构造函数
public User() {
this("Unknown", 0); // 调用带两个参数的构造函数
}
// 带参构造函数
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
在这个例子中,
this("Unknown", 0)
用于调用另一个带参数的构造函数,实现默认构造逻辑。
3.作为方法参数传递当前对象
this
可以用于将当前对象的引用传递给其他方法或对象。
例如:
public class User {
private String name;
public User(String name) {
this.name = name;
}
public void printName() {
System.out.println("Name: " + this.name);
}
public void registerUser(Database db) {
db.addUser(this); // 传递当前User对象到数据库类的addUser方法
}
}
public class Database {
public void addUser(User user) {
// 处理user对象
}
}
在这个例子中,
this
用于将当前的User
对象传递给Database
类的方法addUser
。