一:异常处理:
1.1.Error
和 Exception
Throwable
(异常体系的根类)可分为两类:Error 和 Exception。分别对应着 java.lang.Error
与java.lang.Exception
两个类。
Error
:
Java 虚拟机无法解决的严重问题。如:JVM 系统内部错误、资源耗尽等严重情况。一般不编写针对性的代码进行处理。
• 例如:StackOverflowError
(栈内存溢出)和 OutOfMemoryError
P(堆内存溢出,简称 OOM
)。
Exception
:
其它因编程错误或偶然的外在因素导致的一般性问题,需要使用针对性的代码进行处理,使程序继续运行。否则一旦发生异常,程序也会挂掉。
例如:
• 空指针访问
• 试图读取不存在的文件
• 网络连接中断
• 数组角标越界
说明:
• 无论是 Error 还是 Exception,还有很多子类,异常的类型非常丰富。当代码运行出现异常时,特别是我们不熟悉的异常时,不要紧张,把异常的简单类名,拷贝到 API 中去查去认识它即可。
• 我们本章讲的异常处理,其实针对的就是 Exception。
在Java中,异常分为两种主要类型:编译时异常(Checked Exceptions)和运行时异常(Runtime Exceptions)。
下面我们来举例看看:
3.2 运行时异常
package com.atguigu.exception;
import org.junit.Test;
import java.util.Scanner;
public class TestRuntimeException {
@Test
public void test01(){
//NullPointerException
int[][] arr = new int[3][];
System.out.println(arr[0].length);
}
@Test
public void test02(){
//ClassCastException
Object obj = 15;
String str = (String) obj;
}
@Test
public void test03(){
//ArrayIndexOutOfBoundsException
int[] arr = new int[5];
for (int i = 1; i <= 5; i++) {
System.out.println(arr[i]);
}
}
@Test
public void test04(){
//InputMismatchException
Scanner input = new Scanner(System.in);
System.out.print("请输入一个整数:");//输入非整数
int num = input.nextInt();
input.close();
}
@Test
public void test05(){
int a = 1;
int b = 0;
//ArithmeticException
System.out.println(a/b);
}
}
3.3 编译时异常
package com.atguigu.exception;
import org.junit.Test;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class TestCheckedException {
@Test
public void test06() {
Thread.sleep(1000);//休眠 1 秒 InterruptedException
}
@Test
public void test07(){
Class c =
Class.forName("java.lang.String");//ClassNotFoundException
}
@Test
public void test08() {
Connection conn = DriverManager.getConnection("....");
//SQLException
}
@Test
public void test09() {
FileInputStream fis = new FileInputStream("尚硅谷 Java秘籍.txt"); //FileNotFoundException
}
@Test
public void test10() {
File file = new File("尚硅谷 Java秘籍.txt");
FileInputStream fis = new
FileInputStream(file);//FileNotFoundException
int b = fis.read();//IOException
while(b != -1){
System.out.print((char)b);
b = fis.read();//IOException
}
fis.close();//IOException
}
}
3.4.异常处理:

3.4.1. try-catch-finally
基本格式:
捕获异常语法如下:
try{
...... //可能产生异常的代码
}
catch(
异常类型 1 e ){
...... //当产生异常类型 1 型异常时的处置措施
}
catch(
异常类型 2 e ){
...... //当产生异常类型 2 型异常时的处置措施
}
finally{
...... //无论是否发生异常,都无条件执行的语句
}
举例:
try {
// 可能会抛出异常的代码
int result = 10 / 0; // 除零操作会抛出ArithmeticException
} catch (ArithmeticException e) {
// 捕获并处理异常
System.out.println("除零异常:" + e.getMessage());
}
注意,范围大的要写在下面。即子类异常要写在父类上面。

try-catch可以嵌套使用。

3.4.2.finally
的使用:
finally内部的代码一定会执行,除非强制退出。finally与catch是可选的,但是finally无法单独存在。
Q:为什么要finally呢,异常处理后后面的代码还是会执行,那岂不是可以不用finally?
A:因为在catch中可能也有错,此时用finally可以保证它的执行。

即使要输出返回值,也是会先输出test结束。
例题分析:

如果return ++num;
没有注释掉,如果传入10,那么返回值应该是11,而现在返回值是10,因为在内存中,操作是一步一步压入栈中的,而在return num
中,已经决定了返回值为10。

比如,把流的关闭放在finally中。
3.4.3.throws
:
如果在编写方法体的代码时,某句代码可能发生某个编译时异常,不处理编译不通过,但是在当前方法体中可能不适合处理或无法给出合理的处理方式,则此方法应显示地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理。就像把问题一层一层往上面丢,在当前层不报错,但是在上一层调用还是会报错,直到某一行彻底解决该问题。
注意,在main方法时该问题必须解决,不然就会抛给虚拟机栈,就会出错。
如果父类没有抛出异常,那么子类一定不能抛出异常,因为子类抛出的异常必须小于父类,所以在实际的开发中,有的明明没有异常的父类却抛出异常,是为了让子类能够抛出异常。
比如我们用多态性来解释一下原因,我们用person类来接收一个student类,我们对该对象调用的方法进行异常处理,因为运行看右边,编译看左边,所以我们针对编译的时候的报错,我们要catch父类person的错误,但是实际运行的时候我们是遇到了右边student的错误,如果子类的错误还大于了父类的错误,那么这个异常处理显然是盖不住的。
注意,这是针对编译异常的,运行异常写不写都无所谓。

声明异常格式:
修饰符 返回值类型 方法名(参数) throws异常类名 1,异常类名 2…{ },在 throws 后面可以写多个异常类型,用逗号隔开。
举例:
public void readFile(String file) throws FileNotFoundException,IOException {
...
// 读文件的操作可能产生 FileNotFoundException 或 IOException 类型的异
常
FileInputStream fis = new FileInputStream(file);
//...
}
3.4.4.手动抛出异常对象:throw:
Java 中异常对象的生成有两种方式:
• 由虚拟机自动生成:程序运行过程中,虚拟机检测到程序发生了问题,那么针对当前代码,就会在后台自动创建一个对应异常类的实例对象并抛出。
• 由开发人员手动创建:new异常类型([实参列表]);,如果创建好的异常对象不抛出对程序没有任何影响,和创建一个普通对象一样,但是一旦 throw 抛出,就会对程序运行产生影响了。
格式:
throw new异常类名(参数);
throw 语句抛出的异常对象,和 JVM
自动创建和抛出的异常对象一样。
• 如果是编译时异常类型的对象,同样需要使用 throws 或者 try…catch 处理,否则编译不通过。
• 如果是运行时异常类型的对象,编译器不提示。
• 可以抛出的异常必须是 Throwable
或其子类的实例。下面的语句在编译时将会产生语法错误:
throw new String("want to throw");
throw就相当于按照实际情况,自己抛出一个异常,因此,也是需要被解决的。如果当前方法没有 try…catch 处理这个异常对象,throw 语句就会代替 return语句提前终止当前方法的执行,并返回一个异常对象给调用者。因此,throw后面的代码无法被执行,直接就出去了。
public class TestThrow {
public static void main(String[] args) {
try {
System.out.println(max(4,2,31,1));
} catch (Exception e) {
e.printStackTrace();
}
try {
System.out.println(max(4));
} catch (Exception e) {
e.printStackTrace();
}
try {
System.out.println(max());
} catch (Exception e) {
e.printStackTrace();
}
}
public static int max(int... nums){
if(nums == null || nums.length==0){
throw new IllegalArgumentException("没有传入任何整数,无法获取最大值");
}
int max = nums[0];
for (int i = 1; i < nums.length; i++) {
if(nums[i] > max){
max = nums[i];
}
}
return max;
}
}
IllegalArgumentException
是Java中的一个标准异常类,是Java API
的一部分,可直接在Java使用,无需额外导入。当参数不合法时,Java系统会自动抛出IllegalArgumentException
异常。
e.printStackTrace()
是Java中Throwable
类的一个方法,它用于打印异常的堆栈信息到标准错误流。当异常发生时,可以使用e.printStackTrace()
方法将异常的详细信息打印到控制台
3.4.5.自定义异常:


举例:
//自定义异常类
class MyException extends Exception {
static final long serialVersionUID = 23423423435L;
private int idnumber;
public MyException(String message, int id) {
super(message);
this.idnumber = id;
}
public int getId() {
return idnumber;
}
}
public class MyExpTest {
public void regist(int num) throws MyException {
if (num < 0) throw new MyException("人数为负值,不合理", 3);
else System.out.println("登记人数" + num);
}
public void manager() {
try {
regist(100);
} catch (MyException e) {
System.out.print("登记失败,出错种类" + e.getId());
}
System.out.print("本次登记操作结束");
}
//main方法
public static void main(String args[]) {
MyExpTest t = new MyExpTest();
t.manager();
}
}
在 regist
方法中,当 num
小于0时,会抛出 MyException
异常。但是,MyException
类本身并不解决异常,它只是定义了异常的类型和包含的信息(在这个例子中,是一个字符串消息和一个id号)。
实际处理这个异常的是 manager
方法。在 manager
方法中,我们有一个 try-catch 块,它尝试调用 regist
方法,并捕获可能抛出的 MyException
异常。如果 MyException
被抛出,那么 catch 块就会执行,打印出 “登记失败,出错种类” 和异常的 id。这就是异常的处理方式。
所以,虽然 MyException
并没有解决异常,但它提供了异常的详细信息,而处理异常的责任在于调用可能抛出异常的方法的代码。
二:多线程:
1.程序、进程与线程:
• 程序(program):为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
• 进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。如:运行中的 QQ,运行中的网易音乐播放器。
– 每个进程都有一个独立的内存空间,系统运行一个程序即是一个进程从创建、运行到消亡的过程。(生命周期)
– 程序是静态的,进程是动态的。
– 进程作为操作系统调度和分配资源的最小单位(亦是系统运行程序的基本单位),系统在运行时会为每个进程分配不同的内存区域。
– 现代的操作系统,大都是支持多进程的,支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos 窗口等软件。
• 线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。一个进程中至少有一个线程。
– 一个进程同一时间若并行执行多个线程,就是支持多线程的。
– 线程作为 CPU调度和执行的最小单位。
– 一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
– 下图中,红框的蓝色区域为线程独享,黄色区域为线程共享。


2.并行与并发:
并行和并发是计算机科学中两个重要的概念。
并行是指两个或多个任务同时进行的能力。在计算机系统中,这意味着多个处理器或核心同时执行多个任务。并行计算可以显著提高计算速度和系统的性能。
并发是指多个任务在同一时间段内交替执行的能力。在计算机系统中,这意味着系统能够同时处理多个任务,即使实际上只有一个处理器或核心。并发可以提高系统的效率和资源利用率。
并行和并发的区别在于并行是指多个任务同时执行,而并发是指多个任务在同一时间段内交替执行。在实际应用中,这两个概念经常会同时出现,例如在多核处理器上同时执行多个线程就是并行和并发的结合。
总的来说,并行和并发都是为了提高计算机系统的性能和效率,但它们的实现方式和应用场景有所不同。
3.创建线程:
方式 1:继承 Thread 类:
Java 通过继承 Thread 类来创建并启动多线程的步骤如下:
1. 定义 Thread 类的子类,并重写该类的 run()方法,该 run()方法的方法体就代表了线程
需要完成的任务
2. 创建 Thread 子类的实例,即创建了线程对象
3. 调用线程对象的 start()方法来启动该线程
代码如下:
package com.atguigu.thread;
//自定义线程类
public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的 String 参数的构造方法,指定线程的名称
super(name);
}
/**
* 重写 run 方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}
如果此时main中有其他的方法执行,那么输出是交替的,体现了并发的特点。
注意,要调用start方法,而不是run方法。并且start只能调用一次!!!
或者这么写:
new MyThread(){
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}.test();
方式 2:实现 Runnable 接口:
Java 有单继承的限制,当我们无法继承 Thread 类时,那么该如何做呢?在核心类库中提供了 Runnable 接口,我们可以实现 Runnable 接口,重写 run()方法,然后再通过 Thread 类的对象代理启动和执行我们的线程体 run()方法
步骤如下:
1.定义 Runnable 接口的实现类,并重写该接口的 run()方法,该 run()方法的方法体
同样是该线程的线程执行体。
2.创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 参数来创建
3.调用线程对象的 start()方法,启动线程。调用 Runnable 接口实现类的 run 方法。
代码如下:
package com.atguigu.thread;
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + " "+ i);
}
}
}
测试类:
package com.atguigu.thread;
public class TestMyRunnable {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t = new Thread(mr, "长江");
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("黄河 " + i);
}
}
}
第一个参数是实现了Runnable接口的对象,它包含了要在新线程中执行的任务(即run方法)。第二个参数是线程的名称,它可以用来标识线程。也可以只传入一个变量,即接口的实现类。
通过实现 Runnable 接口,使得该类有了多线程类的特征。所有的分线程要执行的代码都在 run 方法里面。
在启动的多线程的时候,需要先通过 Thread 类的构造方法 Thread(Runnabletarget) 构造出对象,然后调用 Thread 对象的 start()方法来运行多线程代码。
实际上,所有的多线程代码都是通过运行 Thread 的 start()方法来运行的。因此,不管是继承 Thread 类还是实现 Runnable 接口来实现多线程,最终还是通过 Thread 的对象的 API 来控制线程的,熟悉 Thread 类的 API 是进行多线程编程的基础。
说明:Runnable 对象仅仅作为 Thread 对象的 target,Runnable 实现类里包含的 run()方法仅作为线程执行体。 而实际的线程对象依然是 Thread 实例,只是该Thread 线程负责执行其 target 的 run()方法。
当然,它也可以像第一种方法一样用匿名对象:
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":" +i);
}
}
}).start();
4.thread的常用方法:
4.1:常用结构:

4.2.常用方法:
• public void run() :此线程要执行的任务在此处定义代码。
• public void start() :导致此线程开始执行; Java 虚拟机调用此线程的 run 方法。
• public String getName() :获取当前线程名称。
• public void setName(String name):设置该线程名称。
• public static Thread currentThread() :返回对当前正在执行的线程对象的引用。在
Thread 子类中就是 this,通常用于主线程和 Runnable 实现类
• public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
• public static void yield():yield 只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了 yield 方法暂停之后,线程调度器又将其调度出来重新执行。
• public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚
未终止,则为活动状态。
• void join() :等待该线程终止。
void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果 millis 时
间到,将不再等待。
void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 +
nanos 纳秒。
代码如下:
// 创建一个新的线程
Thread thread = new Thread();
// 启动线程
thread.start();
// 使线程进入休眠状态
try {
Thread.sleep(1000); // 休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
// 暂停当前正在执行的线程对象,并执行其他线程
thread.yield();
// 在其他线程中调用当前线程的join方法,会将当前线程加入到调用者线程中,直到当前线程执行完毕,调用者才能继续执行
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 返回对当前正在执行的线程对象的引用
Thread currentThread = Thread.currentThread();
// 返回此线程的标识符
long id = thread.getId();
// 测试线程是否处于活动状态
boolean isAlive = thread.isAlive();
// 创建一个新的线程
Thread thread = new Thread();
// 设置线程的名字
thread.setName("MyThread");
// 获取线程的名字
String name = thread.getName();
System.out.println("Thread name is: " + name);
4.3.优先级:

代码如下:
// 创建一个新的线程
Thread thread = new Thread();
// 设置线程的优先级
thread.setPriority(Thread.MAX_PRIORITY);
// 获取线程的优先级
int priority = thread.getPriority();
System.out.println("Thread priority is: " + priority);
4.4.生命周期:
JDK5之前:

JDK5之后:

5.线程安全问题与解决:
举例:
火车站要卖票,我们模拟火车站的卖票过程。因为疫情期间,本次列车的座位共 100 个(即,只能出售 100 张火车票)。我们来模拟车站的售票窗口,实现多个窗口同时售票的过程。注意:不能出现错票、重票。
错误代码:
class TicketSaleThread extends Thread {
private static int ticket = 100;
public void run() {
while (ticket > 0) {
try {
Thread.sleep(10);//加入这个,使得问题暴露的更明显
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "卖出一张票,票号:" +ticket);
ticket--;
}
}
}
public class SaleTicketDemo3 {
public static void main(String[] args) {
TicketSaleThread t1 = new TicketSaleThread();
TicketSaleThread t2 = new TicketSaleThread();
TicketSaleThread t3 = new TicketSaleThread();
t1.setName("窗口 1");
t2.setName("窗口 2");
t3.setName("窗口 3");
t1.start();
t2.start();
t3.start();
}
}
会有重票的情况,因为当一张票被卖出后,还没有减去,别的线路又执行了。
注意,如果是继承的方法,那么票这个变量必须static修饰,如果是接口的方法,就可以不修饰。
解决方法:
1.同步代码块:

举例:
接口与同步代码块
public class TicketSeller implements Runnable {
private int tickets = 100; // 总共100张票
@Override
public void run() {
while (tickets > 0) {
// 使用同步代码块确保线程安全
synchronized (this) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "售出了第" + tickets + "张票");
tickets--;
}
}
}
}
public static void main(String[] args) {
TicketSeller ticketSeller = new TicketSeller();
Thread t1 = new Thread(ticketSeller, "窗口一");
Thread t2 = new Thread(ticketSeller, "窗口二");
Thread t3 = new Thread(ticketSeller, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
有三点注意:
synchronized (this)
,括号中可以写任一唯一对象,只是因为方便且合适,所以用this,因为this只new了一个。接口式的共享资源不用静态化,因为只new了一个对象,它们已经共享。
synchronized
不能写在while循环外面,这样当一个线程抢占到锁之后,会一直持续到循环结束。
继承与同步代码块:
public class TicketSeller extends Thread {
private static int tickets = 100; // 总共100张票
@Override
public void run() {
while (tickets > 0) {
// 使用同步代码块确保线程安全
synchronized (TicketSeller.class) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "售出了第" + tickets + "张票");
tickets--;
}
}
}
}
public static void main(String[] args) {
TicketSeller seller1 = new TicketSeller();
TicketSeller seller2 = new TicketSeller();
TicketSeller seller3 = new TicketSeller();
seller1.start();
seller2.start();
seller3.start();
}
}
有三点注意:
synchronized ()
,括号中要写任一唯一对象,如果用this,因为this只new了多个,所以会出错。继承的共享资源必须静态化,因为new了多个对象,它们还没有共享。
synchronized
不能写在while循环外面,这样当一个线程抢占到锁之后,会一直持续到循环结束。
2.同步方法:
举例:
同步方法与接口:
public class TicketSeller implements Runnable {
private int tickets = 100; // 总共100张票
public synchronized void sellTicket() {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "售出了第" + tickets + "张票");
tickets--;
}
}
@Override
public void run() {
while (tickets > 0) {
sellTicket();
}
}
public static void main(String[] args) {
TicketSeller ticketSeller = new TicketSeller();
Thread t1 = new Thread(ticketSeller, "窗口一");
Thread t2 = new Thread(ticketSeller, "窗口二");
Thread t3 = new Thread(ticketSeller, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
有三点注意:
synchronized
的接口实现类,默认就有this。接口式的共享资源不用静态化,因为只new了一个对象,它们已经共享。
synchronized
不能写在while循环外面,这样当一个线程抢占到锁之后,会一直持续到循环结束。
同步方法与继承:
public class TicketSeller extends Thread {
private static int tickets = 100; // 总共100张票
public static synchronized void sellTicket() {
if (tickets > 0) {
System.out.println(getName() + "售出了第" + tickets + "张票");
tickets--;
}
}
@Override
public void run() {
while (tickets > 0) {
sellTicket();
}
}
public static void main(String[] args) {
TicketSeller seller1 = new TicketSeller();
TicketSeller seller2 = new TicketSeller();
TicketSeller seller3 = new TicketSeller();
seller1.start();
seller2.start();
seller3.start();
}
}
有三点注意:
synchronized
,要有任一唯一对象,所以方法要加static。继承的共享资源必须静态化,因为new了多个对象,它们还没有共享。
synchronized
不能写在while循环外面,这样当一个线程抢占到锁之后,会一直持续到循环结束。
注意,不要看到继承就肯定要有static,可能用单独的一个类来储存金额,然后只new一次,传参都用这个对象即可。
3.synchronized
的锁是什么:
同步锁对象可以是任意类型,但是必须保证竞争“同一个共享资源”的多个线程必须使用同一个“同步锁对象”。
对于同步代码块来说,同步锁对象是由程序员手动指定的(很多时候也是指定为 this 或类名.class),但是对于同步方法来说,同步锁对象只能是默认的:
• 静态方法:当前类的 Class 对象(类名.class)
• 非静态方法:this
6.死锁:
懒汉式安全问题的解决:
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
注意,可能会有指令重排问题,可以加入volatile关键字处理。此外,在静态方法中,无法使用 this
关键字来获取类的 Class 对象。
死锁:
死锁是指在多个进程或线程争夺资源时,由于彼此之间的互相等待对方释放资源而导致的一种僵局状态。当多个进程或线程同时持有一些资源,并且彼此都在等待对方释放资源时,就会发生死锁。
死锁的发生通常是由于资源的竞争和分配不当所导致的。当一个进程或线程无法继续执行,因为它需要的资源正被其他进程或线程持有,并且这些进程或线程又在等待该进程或线程所持有的资源时,就会发生死锁。
下面是一个简单的Java代码示例,用于演示死锁的情况:
public class DeadlockExample {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
System.out.println("Thread 1: Waiting for lock 2...");
synchronized (lock2) {
System.out.println("Thread 1: Holding lock 1 and lock 2...");
}
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
System.out.println("Thread 2: Waiting for lock 1...");
synchronized (lock1) {
System.out.println("Thread 2: Holding lock 2 and lock 1...");
}
}
}
});
thread1.start();
thread2.start();
}
}
在这个示例中,线程1尝试获取lock1,然后等待获取lock2。而线程2尝试获取lock2,然后等待获取lock1。由于两个线程的获取顺序相反,它们会相互等待对方释放资源,从而导致死锁。
这个例子展示了死锁的原因:当多个线程相互等待对方释放资源时,就会发生死锁。要避免死锁,需要谨慎设计多线程的资源获取顺序,并且确保在获取资源后及时释放。

Lock锁:

class Window implements Runnable{
int ticket = 100;
//1. 创建 Lock 的实例,必须确保多个线程共享同一个 Lock 实例
private final ReentrantLock lock = new ReentrantLock();
public void run(){
while(true){
try{
//2. 调动 lock(),实现需共享的代码的锁定
lock.lock();
if(ticket > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticket--);
}else{
break;
}
}finally{
//3. 调用 unlock(),释放共享代码的锁定
lock.unlock();
}
}
}
}
public class ThreadLock {
public static void main(String[] args) {
Window t = new Window();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t2.start();
}
}
注意,为了确保解锁,可以把该步骤写在finally里面。
7.线程的通信:

例题:使用两个线程打印 1-100。线程 1, 线程 2 交替打印
class Communication implements Runnable {
int i = 1;
public void run() {
while (true) {
synchronized (this) {
notify();
if (i <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + i++);
} else
break;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
由于在run方法中使用了wait()和notify()来实现线程间的通信,每个线程在打印完一个数字后会调用wait()方法进入等待状态,并释放锁。然后另一个线程会被唤醒并继续执行,打印下一个数字。接着它也会调用wait()方法进入等待状态,释放锁,让另一个线程继续执行。如此交替进行,直到1-100全部打印完成。
实际情况举例:
public class ConsumerProducerTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
Consumer c1 = new Consumer(clerk);
Consumer c2 = new Consumer(clerk);
p1.setName("生产者 1");
c1.setName("消费者 1");
c2.setName("消费者 2");
p1.start();
c1.start();
c2.start();
}
}
// 生产者
class Producer extends Thread {
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println("=========生产者开始生产产品========");
while (true) {
try {
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 要求 clerk 去增加产品
clerk.addProduct();
}
}
}
// 消费者
class Consumer extends Thread {
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println("=========消费者开始消费产品========");
while (true) {
try {
Thread.sleep(90);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 要求 clerk 去减少产品
clerk.minusProduct();
}
}
}
// 资源类
class Clerk {
private int productNum = 0; // 产品数量
private static final int MAX_PRODUCT = 20;
private static final int MIN_PRODUCT = 1;
// 增加产品
public synchronized void addProduct() {
if (productNum < MAX_PRODUCT) {
productNum++;
System.out.println(Thread.currentThread().getName() + "生产了第" + productNum + "个产品");
// 唤醒消费者
this.notifyAll();
} else {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 减少产品
public synchronized void minusProduct() {
if (productNum >= MIN_PRODUCT) {
System.out.println(Thread.currentThread().getName() + "消费了第" + productNum + "个产品");
productNum--;
// 唤醒生产者
this.notifyAll();
} else {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 首先,创建一个
Clerk
对象,它代表了共享资源。在这个例子中,共享资源是产品数量productNum
。 - 然后,创建一个生产者线程
Producer
和两个消费者线程Consumer
。他们都需要Clerk
对象来进行操作。 - 当生产者线程开始运行时,它会尝试生产产品。如果当前产品数量
productNum
小于最大值MAX_PRODUCT
,生产者就会生产一个产品,并将产品数量productNum
加一。然后,它会调用notifyAll()
方法唤醒所有在Clerk
对象上等待的线程。 - 如果当前产品数量
productNum
已经达到最大值MAX_PRODUCT
,生产者线程就不能再生产产品。此时,它会调用wait()
方法让自己进入等待状态,并释放Clerk
对象的锁。 - 当消费者线程开始运行时,它会尝试消费产品。如果当前产品数量
productNum
大于等于最小值MIN_PRODUCT
,消费者就会消费一个产品,并将产品数量productNum
减一。然后,它会调用notifyAll()
方法唤醒所有在Clerk
对象上等待的线程。 - 如果当前产品数量
productNum
小于最小值MIN_PRODUCT
,消费者线程就不能再消费产品。此时,它会调用wait()
方法让自己进入等待状态,并释放Clerk
对象的锁。 - 这个过程会一直重复,直到程序结束。
8.callable与线程池:
举例:
// 1. 创建一个实现 Callable 的实现类
class NumThread implements Callable {
// 2. 实现 call 方法,将此线程需要执行的操作声明在 call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class CallableTest {
public static void main(String[] args) {
// 3. 创建 Callable 接口实现类的对象
NumThread numThread = new NumThread();
// 4. 将此 Callable 接口实现类的对象作为传递到 FutureTask 构造器中,创建 FutureTask 的对象
FutureTask futureTask = new FutureTask(numThread);
// 5. 将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start()
new Thread(futureTask).start();
// 接收返回值
try {
// 6. 获取 Callable 中 call 方法的返回值
// get()返回值即为 FutureTask 构造器参数 Callable 实现类重写的 call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
下面是一个使用线程池的Java代码示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 提交任务到线程池
for (int i = 0; i < 10; i++) {
final int taskID = i;
executorService.submit(new Runnable() {
public void run() {
System.out.println("正在执行任务 " + taskID);
try {
TimeUnit.SECONDS.sleep(2); // 模拟任务执行时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务 " + taskID + " 执行完毕");
}
});
}
// 关闭线程池
executorService.shutdown();
}
}
这段代码首先创建了一个固定大小为5的线程池。然后,它提交了10个任务到线程池。每个任务都是一个Runnable
对象,它打印出一条消息,然后休眠2秒钟,再打印出一条消息。因为线程池的大小为5,所以任何时候最多只有5个任务在并行执行。当一个任务完成后,线程池会自动开始执行队列中的下一个任务。
最后,当所有任务都提交到线程池后,我们调用shutdown()
方法来关闭线程池。这个方法不会立即停止线程池,而是等待所有已提交的任务都执行完毕后再关闭线程池。
三:常用类与基础APL:
3.1.String类:


注意:
1.常量池不可以存放两个相同的String类,也就是说:
String srt1 = "hello";
String str2 = "hello";
实际上它们是可以划等号的,指向相同的地址。
2.基于1,又有不可变性,因此:
String srt1 = "hello";
String str2 = "hello";
str2 += "a";
String str3 = str1.replace('l','w');
str1还是"hello",只不过str2,str3new了一块新地址。
3.2.String实例化的两种方式:
String str1 = "abc";
String str2 = new String("abc");
对于这两种方式,有:

那么它们的区别在哪里:

这就是两者的区别,所以S1与S3不一样,而S3,S4不是直接储存在字符串常量池的,所以可以有两个不同的地址,所以也不一样。
同理,对于类的String变量,内存实际上是这么分配的:

例题分析:


解释:

3.3.String的构造器与转换:
String与char数组:
//String与char数组的互化:
String str = "Hello";
char[] charArray = str.toCharArray(); // 将String转换为char数组
char[] charArray = {'H', 'e', 'l', 'l', 'o'};
String newStr = new String(charArray); // 将char数组转换为String
String与byte数组的互化:
String与byte数组的互化以及使用不同的字符集:
使用默认字符集进行转换:
String str = "Hello";
byte[] byteArray = str.getBytes(); // 将String转换为byte数组
String newStr = new String(byteArray); // 将byte数组转换为String
使用指定的字符集进行转换:
String str = "你好";
byte[] byteArray = str.getBytes("GBK"); // 将String转换为byte数组,使用GBK字符集
String newStr = new String(byteArray, "GBK"); // 将byte数组转换为String,使用GBK字符集
补充:
1.字符集:
UTF-8和GBK是两种常见的字符编码方式,它们的主要区别如下:
字符集覆盖范围:GBK是专门用来解决中文编码的,包含全部中文字符。而UTF-8则包含全世界所有国家需要用到的字符。
编码字节长度:GBK是双字节编码,不论中英文都是双字节。而UTF-8是一种变长的编码方式,它对英文使用8位(即一个字节),中文使用24位(三个字节)来编码。
适用性:如果你主要做中文程序的开发,客户也主要是中国人的话就用GBK吧,因为UTF-8编码的中文使用了三个字节,用GBK节省了空间。如果做英文网站开发,还是用utf-8吧,因为utf-8中英文只占一个字节。GBK中英文也是两个字节的,并且国外客户访问GBK要下载语言包。如果你的网站是中文的,但国外用户也不少,最好也用UTF-8的吧。
2.编码与解码:
String与byte数组互化时,要使用一样的字符集,不然会出错,其次,在byte转化成String时,可以不显示的提供字符集参数:String newStr = new String(byteArray);
,一般会默认为编码时的字符集。
3.构造器:
• public String() :初始化新创建的 String 对象,以使其表示空字符序列。
• String(String original): 初始化一个新创建的 String 对象,使其表示一个
与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。
• public String(char[] value) :通过当前参数中的字符数组来构造新的
String。
• public String(char[] value,int offset, int count) :通过字符数组的
一部分来构造新的 String。
• public String(byte[] bytes) :通过使用平台的默认字符集解码当前参数中的
字节数组来构造新的 String。
• public String(byte[] bytes,String charsetName) :通过使用指定的字符
集解码当前参数中的字节数组来构造新的 String。
3.4.常用类:
3.4.1.基础方法:

3.4.2.查找方法:
(11)boolean contains(xx):是否包含 xx 。
(12)int indexOf(xx):从前往后找当前字符串中 xx,即如果有返回第一次出现的下标,要是没有返回-1
(13)int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始搜索。
(14)int lastIndexOf(xx):从后往前找当前字符串中 xx,即如果有返回最后一次出现的下标,要是没有返回-1
(15)int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索。
3.4.3.截取方法:
(16)String substring(int beginIndex) :返回一个新的字符串,它是此字符串的从 beginIndex开始截取到最后的一个子字符串。
(17)String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从 beginIndex 开始截取到 endIndex(不包含)的一个子字符串。
3.4.4.和字符/字符数组相关:
(18)char charAt(index):返回[index]位置的字符
(19)char[] toCharArray(): 将此字符串转换为一个新的字符数组返回
(20)static String valueOf(char[] data) :返回指定数组中表示该字符序列的 String
(21)static String valueOf(char[] data, int offset, int count) : 返回指定数组中表示该字符序列的 String
(22)static String copyValueOf(char[] data): 返回指定数组中表示该字符序列的 String (23)static String copyValueOf(char[] data, int offset, int count):返回指定数组中表示该字符序列的 String
3.4.5.开头与结尾
(24)boolean startsWith(xx):测试此字符串是否以指定的前缀开始
(25)boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的
子字符串是否以指定前缀开始
(26)boolean endsWith(xx):测试此字符串是否以指定的后缀结束
3.4.6.替换:
(27)String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。 不支持正则。
(28)String replace(CharSequence target, CharSequence replacement):使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
(29)String replaceAll(String regex, String replacement):使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
(30)String replaceFirst(String regex, String replacement):使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。
例题:

注意,如果在方法内部不改变str的值,那么方法中的str与外面的str其实指向的是同一块地址,但是String有不可变性,所以重新分配了内存,而ch数组就没有这样的规定。
3.5.String,Stringbuffer与StringBuilder:
区分 String、StringBuffer、StringBuilder
– String:不可变的字符序列; 底层使用 char[]数组存储(JDK8.0 中)
– StringBuffer:可变的字符序列;线程安全(方法有 synchronized 修饰),效率低;底层使用 char[]数组存储 (JDK8.0 中)
– StringBuilder:可变的字符序列; jdk1.5 引入,线程不安全的,效率高;底层使用 char[]数组存储(JDK8.0 中)
因为在底层的源码中,StringBuffer与StringBuilder没有像String一样的finally修饰,所以可在原有的基础上改变。但是,它们在new对象时就将StringBuffer或者StringBuilder的长度定好为16,但是用length方法得到的还是实际的长度,我们在后面可以对它扩容,每次会扩大到原来的2倍加2,但是为了效率更高,我们可以在开始就指定它的大小:
StringBuffer sb = new StringBuffer(10);
常用APL:
(1)StringBuffer append(xx):提供了很多的 append()方法,用于进行字符串追加的方式拼接 (2)StringBuffer delete(int start, int end):删除[start,end)之间字符
(3)StringBuffer deleteCharAt(int index):删除[index]位置字符
(4)StringBuffer replace(int start, int end, String str):替换[start,end)范围的字符序列为 str
(5)void setCharAt(int index, char c):替换[index]位置字符
(6)char charAt(int index):查找指定 index 位置上的字符
(7)StringBuffer insert(int index, xx):在[index]位置插入 xx (8)int length():返回存储的字符数据的长度
(9)StringBuffer reverse():反转
其他APL:
(1)int indexOf(String str):在当前字符序列中查询 str 的第一次出现下标
(2)int indexOf(String str, int fromIndex):在当前字符序列[fromIndex,最后]中查询 str 的第一次出现下标
(3)int lastIndexOf(String str):在当前字符序列中查询 str 的最后一次出现下标
(4)int lastIndexOf(String str, int fromIndex):在当前字符序列[fromIndex,最后]中查询 str 的最后一次出现下标
(5)String substring(int start):截取当前字符序列[start,最后]
(6)String substring(int start, int end):截取当前字符序列[start,end)
(7)String toString():返回此序列中数据的字符串表示形式
(8)void setLength(int newLength) :设置当前字符序列长度为 newLength
三者的效率为:
StringBuilder>StringBuffer>String
例题:

y=x
,让方法中的y指向了x的地址,所以外面的y不变。

注意,如果为null,那么会把这四个字符存进去。
3.6.日期与时间:

Date类有两种包下的用法:
3.6.1.Java.util.Date:
3.6.2.Java.sql.Date:
在Java中,java.sql.Date
和java.util.Date
之间的转换如下:
import java.util.Date;
import java.sql.Date as SqlDate;
public class Main {
public static void main(String[] args) {
// java.util.Date 转 java.sql.Date
Date utilDate = new Date();
SqlDate sqlDate = new SqlDate(utilDate.getTime());
System.out.println("java.util.Date to java.sql.Date: " + sqlDate);
// java.sql.Date 转 java.util.Date
sqlDate = SqlDate.valueOf("2024-02-06");
utilDate = new Date(sqlDate.getTime());
System.out.println("java.sql.Date to java.util.Date: " + utilDate);
}
}
在这个例子中,我们首先创建了一个java.util.Date
对象,然后使用getTime()
方法获取其毫秒值,然后使用这个毫秒值创建了一个java.sql.Date
对象。
然后,我们创建了一个java.sql.Date
对象,然后使用getTime()
方法获取其毫秒值,然后使用这个毫秒值创建了一个java.util.Date
对象。
3.6.3.SimpleDateFormat:
在Java中,SimpleDateFormat
类是一个非常方便的类,用于格式化和解析日期。以下是一个例子,展示了如何使用SimpleDateFormat
将Date
对象转换为字符串,以及如何将字符串解析为Date
对象:
import java.text.SimpleDateFormat;
import java.util.Date;
public class Main {
public static void main(String[] args) throws Exception {
// 创建一个Date对象
Date date = new Date();
// 创建一个SimpleDateFormat对象
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 使用format方法将Date对象转换为字符串
String dateString = sdf.format(date);
System.out.println("Formatted date: " + dateString);
// 使用parse方法将字符串解析为Date对象
Date parsedDate = sdf.parse(dateString);
System.out.println("Parsed date: " + parsedDate);
}
}
这个例子中,我们首先创建了一个Date
对象,然后创建了一个SimpleDateFormat
对象,并指定了日期和时间的格式。然后,我们使用format
方法将Date
对象转换为字符串。最后,我们使用parse
方法将字符串解析回Date
对象。
注意,yyyy-MM-dd HH:mm:ss
为指定的格式,我们也可以用空参构造器,即使用默认的格式,但是,我们用parse时,要与最开始的格式一致,prase即把字符串变成Date类型,如果new对象时的指定格式为yyyy-MM-dd HH:mm:ss,那么prase传入的字符串必须也是这种格式。
3.6.4.Calendar:
Java的Calendar
类是一个抽象类,它提供了一些方法来操作日期。以下是一些基本的用法:
import java.util.Calendar;
public class Main {
public static void main(String[] args) {
// 获取当前日期和时间
Calendar calendar = Calendar.getInstance();
// 获取年、月、日、小时、分钟
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH); // 注意:月份是从0开始的,所以实际月份是这里的值+1
int day = calendar.get(Calendar.DAY_OF_MONTH);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
System.out.println("当前时间: " + year + "-" + (month + 1) + "-" + day + " " + hour + ":" + minute);
// 修改日期和时间
calendar.set(Calendar.YEAR, 2025);
calendar.set(Calendar.MONTH, Calendar.FEBRUARY);
calendar.set(Calendar.DAY_OF_MONTH, 25);
calendar.set(Calendar.HOUR_OF_DAY, 10);
calendar.set(Calendar.MINUTE, 20);
year = calendar.get(Calendar.YEAR);
month = calendar.get(Calendar.MONTH);
day = calendar.get(Calendar.DAY_OF_MONTH);
hour = calendar.get(Calendar.HOUR_OF_DAY);
minute = calendar.get(Calendar.MINUTE);
System.out.println("修改后的时间: " + year + "-" + (month + 1) + "-" + day + " " + hour + ":" + minute);
}
}
这个例子中,我们首先使用 Calendar.getInstance()
获取了一个表示当前日期和时间的 Calendar
对象。然后,我们使用 get
方法获取了年、月、日、小时和分钟。注意,月份是从0开始的,所以实际月份是这里的值+1。
然后,我们使用 set
方法修改了年、月、日、小时和分钟。最后,我们再次使用 get
方法获取了修改后的年、月、日、小时和分钟。
注意,在Java中,Calendar
是一个抽象类,不能直接实例化。
3.6.5.JDK8的新的日期与时间:
上面两种方法可能遇到的问题:
• 可变性:像日期和时间这样的类应该是不可变的。
• 偏移性:Date 中的年份是从 1900 开始的,而月份都从 0 开始。
• 格式化:格式化只对 Date 有用,Calendar 则不行。
• 此外,它们也不是线程安全的;不能处理闰秒等。
java.time
包是Java 8引入的新的日期和时间API,它修复了旧的java.util.Date
和java.util.Calendar
的许多问题,并提供了更强大、更灵活的日期和时间处理功能。以下是一些主要的类:
LocalDate
:表示日期(年、月、日)。LocalTime
:表示时间(小时、分钟、秒)。LocalDateTime
:表示日期和时间。Instant
:表示一个时间戳,可以用来记录事件发生的瞬间。Period
:表示两个日期之间的时间段。Duration
:表示两个时间之间的时间段。
以下是一些使用示例:
import java.time.*;
public class Main {
public static void main(String[] args) {
// 获取当前日期和时间
LocalDate date = LocalDate.now();
LocalTime time = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.now();
System.out.println("当前日期: " + date);
System.out.println("当前时间: " + time);
System.out.println("当前日期和时间: " + dateTime);
// 创建日期和时间
LocalDate dateOfBirth = LocalDate.of(1990, Month.JANUARY, 1);
LocalTime teaTime = LocalTime.of(17, 0);
LocalDateTime dateTimeOfBirth = LocalDateTime.of(dateOfBirth, teaTime);
System.out.println("出生日期: " + dateOfBirth);
System.out.println("茶时间: " + teaTime);
System.out.println("出生日期和时间: " + dateTimeOfBirth);
// 计算时间段
Period period = Period.between(dateOfBirth, date);
System.out.println("从出生到现在的天数: " + period.getDays());
Duration duration = Duration.between(teaTime, time);
System.out.println("从茶时间到现在的秒数: " + duration.getSeconds());
}
}
now()/ now(ZoneId zone) 静态方法,根据当前时间创建对象/指定时区的对象
of(xx,xx,xx,xx,xx,xxx) 静态方法,根据指定日期/时间创建对象
getDayOfMonth()/getDayOfYear() 获得月份天数(1-31) /获得年份天数(1-366)
getDayOfWeek() 获得星期几(返回一个 DayOfWeek 枚举值)
getMonth() 获得月份, 返回一个 Month 枚举值
getMonthValue() / getYear() 获得月份(1-12) /获得年份
getHours()/getMinute()/getSecond() 获得当前对象对应的小时、分钟、秒withDayOfMonth()/withDayOfYear()/withMonth()/withYear()将月份天数、年份天数、月份、年份修改为指定的值并返回新的对象
with(TemporalAdjuster t) 将当前日期时间设置为校对器指定的日期时间
plusDays(), plusWeeks(),plusMonths(),plusYears(),plusHours()向当前对象添加几天、几周、几月、几年、几小时
minusMonths() /minusWeeks()/minusDays()/minusYears()/minusHours()从当前对象减去几月、几周、几天、几年、几小时
plus(TemporalAmountt)/minus(TemporalAmount t)添加或减少一个 Duration 或 Period
isBefore()/isAfter() 比较两个 LocalDate
isLeapYear() 判断是否是闰年(在 LocalDate 类中声明)
format(DateTimeFormatter t) 格式化本地日期、时间,返回一个字符串
parse(Charsequence text) 将指定格式的字符串解析为日期、时间
3.7.Java比较器:
3.7.1.Comparable:

package com.atguigu.api;
import java.util.Arrays;
public class Student implements Comparable<Student> {
private int id;
private String name;
private int score;
private int age;
public Student(int id, String name, int score, int age) {
this.id = id;
this.name = name;
this.score = score;
this.age = age;
}
// ... 其他getter和setter方法 ...
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", score=" + score +
", age=" + age +
'}';
}
@Override
public int compareTo(Student other) {
return this.id - other.id;
}
}
public class TestStudent {
public static void main(String[] args) {
Student[] students = new Student[5];
students[0] = new Student(3, "张三", 90, 23);
students[1] = new Student(1, "熊大", 100, 22);
students[2] = new Student(5, "王五", 75, 25);
students[3] = new Student(4, "李四", 85, 24);
students[4] = new Student(2, "熊二", 85, 18);
//单独比较两个对象
System.out.println(arr[0].compareTo(arr[1]));
System.out.println("所有学生:");
for (Student student : students) {
System.out.println(student);
}
System.out.println("按照学号排序:");
Arrays.sort(students);
for (Student student : students) {
System.out.println(student);
}
}
}
3.7.2.Comparator:
举例:
package com.atguigu.api;
import java.util.Comparator;
// 定义定制比较器类
public class StudentScoreComparator implements Comparator {
@Override
public int compare(Object o1, Object o2) {
if (!(o1 instanceof Student) || !(o2 instanceof Student)) {
throw new IllegalArgumentException("Objects must be of type Student");
}
Student s1 = (Student) o1;
Student s2 = (Student) o2;
int result = s1.getScore() - s2.getScore();
return result != 0 ? result : s1.getId() - s2.getId();
}
}
// 测试类
package com.atguigu.api;
public class TestStudent {
public static void main(String[] args) {
Student[] arr = new Student[5];
arr[0] = new Student(3, "张三", 90, 23);
arr[1] = new Student(1, "熊大", 100, 22);
arr[2] = new Student(5, "王五", 75, 25);
arr[3] = new Student(4, "李四", 85, 24);
arr[4] = new Student(2, "熊二", 85, 18);
System.out.println("所有学生:");
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
System.out.println("按照成绩排序");
StudentScoreComparator sc = new StudentScoreComparator();
for (int i = 1; i < arr.length; i++) {
for (int j = 0; j < arr.length - i; j++) {
if (sc.compare(arr[j], arr[j + 1]) > 0) {
Student temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
这是直接造了一个类的方法。当然,我们还可以
Arrays.sort(all, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
Goods g1 = (Goods) o1;
Goods g2 = (Goods) o2;
return g1.getName().compareTo(g2.getName());
}
});
用匿名对象的方法进行重写。
注意,如果要反序,则在return后面加上负号即可。
3.8.其他类APL:
3.8.1.java.lang.System 类:
java.lang.System 类代表当前 Java 程序的运行平台,系统级的很多属性和控制方法都放置在该类的内部。它包含了一些有用的类字段和方法。以下是一些主要的特性:
类声明:public final class System extends Object。由于该类的构造方法是 private 的,所以无法创建该类的对象,也就是无法实例化该类。其内部的成员方法和成员变量都是 static(静态)的,所以也可以很方便地调用他。
字段:包括 static PrintStream err(标准错误输出流)、static InputStream in(标准输入流)和 static PrintStream out(标准输出流)。
类方法:包括但不限于 arraycopy()(从指定源数组中复制一个数组)、clearProperty()(删除指定键指示的系统属性)、console()(返回与当前 Java 虚拟机关联的唯一 Console 对象)、currentTimeMillis()(以毫秒为单位返回当前时间)、exit()(终止当前正在运行的 Java 虚拟机)、gc()(运行垃圾收集器)、getenv()(返回当前系统环境的不可修改的字符串映射视图)等。
3.8.2.java.lang.Runtime 类:
这个类允许应用程序与运行应用程序的环境进行交互。它提供了一系列的方法,包括执行系统命令、获取可用处理器数量、运行垃圾收集器等。这个类是单例的,可以通过 Runtime.getRuntime()
方法获取当前运行时对象的引用。
3.8.3.java.lang.Math类:
这个类包含用于执行基本数值运算的方法,例如初等指数、对数、平方根和三角函数。所有的这些方法都是静态的,可以直接通过类名调用。
3.8.4.java.math 包:
这个包提供了用于执行任意精度整数算术 (BigInteger) 和任意精度十进制算术 (BigDecimal) 的类。
3.8.5.java.util.Random类:
这个类用于生成伪随机数。它提供了一系列的方法,可以生成不同类型的随机数,包括整数、浮点数、高斯分布的数等。这个类是线程安全的,但在多线程环境中,建议使用 ThreadLocalRandom
以获得更好的性能。
四:集合框架:
(1):Collection:
Collection 接口及方法:
• JDK 不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set 和 List)去实现。
• Collection 接口是 List 和 Set 接口的父接口,该接口里定义的方法既可用于操作 Set集合,也可用于操作 List 集合。方法如下:
1.1添加:
(1)add(E obj):添加元素对象到当前集合中
(2)addAll(Collection other):添加 other 集合中的所有元素对象到当前集合中
举例:
注意:add 和 addAll 的区别
package com.atguigu.collection;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
public class TestCollectionAdd {
@Test
public void testAdd(){
//ArrayList 是 Collection 的子接口 List 的实现类之一。
Collection coll = new ArrayList();
coll.add("小李广");
coll.add("扫地僧");
coll.add("石破天");
System.out.println(coll);
}
@Test
public void testAddAll(){
Collection c1 = new ArrayList();
c1.add(1);
c1.add(2);
System.out.println("c1集合元素的个数:" + c1.size());//2
System.out.println("c1 = " + c1);
Collection c2 = new ArrayList();
c2.add(1);
c2.add(2);
System.out.println("c2集合元素的个数:" + c2.size());//2
System.out.println("c2 = " + c2);
Collection other = new ArrayList();
other.add(1);
other.add(2);
other.add(3);
System.out.println("other集合元素的个数:" + other.size());//3
System.out.println("other = " + other);
System.out.println();
c1.addAll(other);
System.out.println("c1集合元素的个数:" + c1.size());//5
System.out.println("c1.addAll(other) = " + c1);
c2.add(other);
System.out.println("c2集合元素的个数:" + c2.size());//3
System.out.println("c2.add(other) = " + c2);
}
}
add也可以添加一个对象,不过是把这个对象当作整体,算成一个元素。
1.2 判断:
(3)int size():获取当前集合中实际存储的元素个数
(4)boolean isEmpty():判断当前集合是否为空集合
(5)boolean contains(Object obj):判断当前集合中是否存在一个与 obj 对象 equals 返回 true 的元素
(6)boolean containsAll(Collection coll):判断 coll 集合中的元素是否在当前集合中都存在
。即 coll 集合是否是当前集合的“子集”
(7)boolean equals(Object obj):判断当前集合与 obj 是否相等
注意:Collection类有时需要重写equals方法,是因为在使用集合类存储对象时,需要比较对象的内容而不是引用地址。默认的equals方法是比较对象的引用地址,而不是对象的内容。因此,如果需要在集合中存储自定义对象,并且希望能够比较对象的内容而不是引用地址,就需要重写equals方法来实现自定义的对象比较逻辑。这样可以确保集合类能够正确地判断两个对象是否相等,从而保证集合类的功能正常运作。
举例:
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = 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);
}
}
public class Main {
public static void main(String[] args) {
Person person1 = new Person("Alice", 25);
Person person2 = new Person("Bob", 30);
Person person3 = new Person("Alice", 25);
List personList = new ArrayList();
personList.add(person1);
// 使用contains方法来检查是否包含某个Person对象
System.out.println(personList.contains(person2)); // 输出false,因为内容不同
System.out.println(personList.contains(person1)); // 输出true,因为内容相同
// 使用containsAll方法来检查是否包含另一个Collection中的所有元素
List anotherList = new ArrayList();
anotherList.add(person1);
anotherList.add(person3);
System.out.println(personList.containsAll(anotherList)); // 输出true,因为personList包含anotherList中的所有元素
// 使用equals方法来比较两个Collection是否相等
List personList2 = new ArrayList();
personList2.add(person1);
personList2.add(person2);
System.out.println(personList.equals(personList2)); // 输出false,因为内容不同
}
}
1.3 删除:
(8)void clear():清空集合元素
(9) boolean remove(Object obj) :从当前集合中删除第一个找到的与 obj 对象 equals 返回 true 的元素。
(10)boolean removeAll(Collection coll):从当前集合中删除所有与 coll 集合中相同
的元素。即 this = this - this ∩ coll
(11)boolean retainAll(Collection coll):从当前集合中删除两个集合中不同的元素,使得当前集合仅保留与 coll 集合中的元素相同的元素,即当前集合中仅保留两个集合的交集,即 this = this ∩ coll;
1.4 其它:
(12)Object[] toArray():返回包含当前集合中所有元素的数组
(13)hashCode():获取集合对象的哈希值
(14)iterator():返回迭代器对象,用于集合遍历
在Java中,可以使用Arrays类的asList()方法将数组转换为List,然后再使用集合的构造函数将List转换为集合。
import java.util.*;
public class ArrayToSet {
public static void main(String[] args) {
Integer[] arr = {1, 2, 3, 4, 5};
List<Integer> list = Arrays.asList(arr);
Set<Integer> setArr = new HashSet<>(list);
System.out.println(setArr);
}
}
输出:
[1, 2, 3, 4, 5]
例题分析:
迭代器是一种用于遍历集合类的对象,它提供了一种统一的方式来访问集合中的元素。在Java中,可以使用迭代器来遍历Collection类的实例,如List、Set和Map。
下面是一个使用迭代器遍历List集合的示例代码:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("orange");
// 使用迭代器遍历List集合
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}
}
}
在上面的示例中,我们首先创建了一个ArrayList实例,并向其中添加了一些元素。然后,我们使用list.iterator()方法获取了一个迭代器对象,然后使用while循环和iterator.hasNext()方法来遍历集合中的元素,使用iterator.next()方法获取每个元素的值并进行处理。
类似地,我们也可以使用迭代器来遍历其他类型的集合,如Set和Map。迭代器提供了一种通用的方式来遍历集合中的元素,可以帮助我们更方便地对集合进行操作。
1.5.增强for循环:
增强for循环是一种简化迭代集合或数组的方式,它可以更简洁地遍历集合中的元素。在Java中,增强for循环的语法如下:
for (元素类型 元素变量 : 集合或数组) {
// 循环体
}
其中,元素类型是集合或数组中元素的类型,元素变量是用来存储每个元素的变量名,集合或数组是要遍历的对象。
例如,遍历一个整型数组可以这样写:
int[] nums = {1, 2, 3, 4, 5};
for (int num : nums) {
System.out.println(num);
}
这段代码会依次输出数组中的每个元素。
增强for循环不仅可以遍历数组,还可以遍历集合类,如ArrayList、LinkedList等。它能够简化代码,提高代码的可读性,并且减少了迭代器的使用,使得代码更加简洁和易于理解。
注意,在内部改变临时变量的值,可能不影响集合中的值。
(2):List:
2.1.常用方法:
• 插入元素
– void add(int index, Object ele):在 index 位置插入 ele 元素
– boolean addAll(int index, Collection eles):从 index 位置开始将 eles 中的所有元素添加进来
• 获取元素
– Object get(int index):获取指定 index 位置的元素
– List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex 位置的子集合
• 获取元素索引
– int indexOf(Object obj):返回 obj 在集合中首次出现的位置
– int lastIndexOf(Object obj):返回 obj 在当前集合中末次出现的位置
• 删除和替换元素
– Object remove(int index):移除指定 index 位置的元素,并返回此元素
– Object set(int index, Object ele):设置指定 index 位置的元素为ele
2.2.实现类:
(3):set:
3.1.基础知识:
也就是说,即使是LinkedHashSet,也是无序储存的,只不过在储存时记录了前后节点,所以在一定程度上保留了顺序。
因为在判断时,除了equals方法,还有hashcode所以,我们:
3.2.Treeset:
比如,两个对象的age相同,我们自定义的比较方法认为它们相同,那么它们无法同时存在。
TreeSet 两种排序方法:
自然排序和定制排序。默认情况下,TreeSet 采用自然排序。
–
自然排序:TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列。
• 如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现Comparable 接口。
• 实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过compareTo(Object obj) 方法的返回值来比较大小。
–
定制排序:如果元素所属的类没有实现 Comparable 接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。定制排序,通过 Comparator 接口来实现。需要重写compare(T o1,T o2)方法。
• 利用 int compare(T o1,T o2)方法,比较 o1 和 o2 的大小:如果方法返回正整数,则表示 o1 大于 o2;如果返回 0,表示相等;返回负整数,表示 o1 小于 o2。
• 要实现定制排序,需要将实现 Comparator 接口的实例作为形参传递给 TreeSet 的构造器。
举例:
import java.util.*;
class CustomComparator implements Comparator {
public int compare(Object a, Object b) {
return ((Integer)b).intValue() - ((Integer)a).intValue(); // 降序排序
}
}
public class TreeSetExample {
public static void main(String[] args) {
// 自然排序
Set naturalOrderSet = new TreeSet();
naturalOrderSet.add(new Integer(5));
naturalOrderSet.add(new Integer(1));
naturalOrderSet.add(new Integer(3));
System.out.println("自然排序: " + naturalOrderSet); // 输出: [1, 3, 5]
// 定制排序
Set customOrderSet = new TreeSet(new CustomComparator());
customOrderSet.add(new Integer(5));
customOrderSet.add(new Integer(1));
customOrderSet.add(new Integer(3));
System.out.println("定制排序: " + customOrderSet); // 输出: [5, 3, 1]
}
}
这些代码演示了如何在不使用泛型的情况下,使用TreeSet的两种比较方式。
(4):Map:
4.1.基础知识:
Java中的Map是一种键值对的集合,它存储着一组键值对,并且允许通过键来查找值。Map接口是java.util包中的一部分,它是一个泛型接口,可以用来存储任意类型的键值对。
Map中的键是唯一的,每个键对应着一个值。可以通过键来获取对应的值,也可以通过键来添加、删除或更新键值对。常用的Map实现类包括HashMap、TreeMap和LinkedHashMap。
注:Hashtable中可以应该更正为不可以。
4.2.常用方法:
在Java中,Map接口中的entrySet()方法可以返回一个包含Map.Entry对象的Set集合,其中每个Map.Entry对象包含一个键值对。可以通过entrySet()方法来遍历Map中的键值对。
Map map = new HashMap();
// 添加键值对
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
Set entrySet = map.entrySet();
Iterator iterator = entrySet.iterator();
while (iterator.hasNext()) {
Map.Entry entry = iterator.next();
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
}
2.使用增强for循环遍历entrySet
for (Map.Entry entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
}
3.使用forEach方法遍历entrySet
map.forEach((key, value) -> {
System.out.println("Key: " + key + ", Value: " + value);
});
以上是Map中entry的多种遍历方法,可以根据实际情况选择适合的遍历方式。
4.3.TreeMap:
TreeMap是Java中的一个基于红黑树的有序映射,它可以根据键的自然顺序或者提供的Comparator进行排序。下面是一个例子,展示了如何在TreeMap中使用自然排序和定制排序:
import java.util.*;
class CustomComparator implements Comparator {
public int compare(Object a, Object b) {
return ((String)b).compareTo((String)a); // 降序排序
}
}
public class TreeMapExample {
public static void main(String[] args) {
// 自然排序
Map naturalOrderMap = new TreeMap();
naturalOrderMap.put("Mike", 18);
naturalOrderMap.put("Joney", 28);
naturalOrderMap.put("Dan", 14);
System.out.println("自然排序: " + naturalOrderMap); // 输出: {Dan=14, Joney=28, Mike=18}
// 定制排序
Map customOrderMap = new TreeMap(new CustomComparator());
customOrderMap.put("Mike", 18);
customOrderMap.put("Joney", 28);
customOrderMap.put("Dan", 14);
System.out.println("定制排序: " + customOrderMap); // 输出: {Mike=18, Joney=28, Dan=14}
}
}
在这个例子中,我们首先创建了一个TreeMap,并添加了一些元素。由于我们没有提供自定义的Comparator,所以TreeMap使用键的自然排序。然后,我们创建了另一个TreeMap,并提供了一个自定义的Comparator来实现降序排序。当我们添加元素并打印TreeMap时,我们可以看到元素是按照我们指定的顺序排序的。
4.4.properties:
Properties类是Java中的一个用于处理属性文件的工具类,它继承自Hashtable1。Properties类常用于存储程序的配置信息,例如数据库连接信息、日志输出配置、应用程序设置等1。使用Properties类,可以将这些信息存储在一个文本文件中,并在程序中读取这些信息1。
下面是一个简单的例子,演示了如何使用Properties类来读取和写入属性文件:
import java.io.*;
import java.util.Properties;
public class PropertiesExample {
public static void main(String[] args) {
Properties prop = new Properties();
try {
// 读取属性文件
prop.load(new FileInputStream("config.properties"));
// 获取属性值
String username = prop.getProperty("username");
String password = prop.getProperty("password");
// 输出属性值
System.out.println("username: " + username);
System.out.println("password: " + password);
// 修改属性值
prop.setProperty("username", "newUsername");
prop.setProperty("password", "newPassword");
// 保存属性到文件
prop.store(new FileOutputStream("config.properties"), null);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
在这个例子中,我们创建了一个Properties对象,然后使用load()方法从配置文件中读取属性。接着,我们使用getProperty()方法获取属性值,并输出到控制台。然后,我们使用setProperty()方法修改属性值,并使用store()方法将修改后的属性保存回文件。
(5):Collections工具类:
Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法(均为static 方法):
排序操作:
• reverse(List):反转 List 中元素的顺序
• shuffle(List):对 List 集合元素进行随机排序
• sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
• sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
• swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
查找:
• Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
• Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
• Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素
• Object min(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最小元素
• int binarySearch(List list,T key)在 List 集合中查找某个元素的下标,但是 List 的元素必须是 T 或 T 的子类对象,而且必须是可比较大小的,即支持自然排序的。而且集合也事先必须是有序的,否则结果不确定。
• int binarySearch(List list,T key,Comparator c)在 List 集合中查找某个元素的下标,但是 List 的元素必须是 T 或 T 的子类对象,而且集合也事先必须是按照 c 比较器规则进行排序过的,否则结果不确定。
• int frequency(Collection c,Object o):返回指定集合中指定元素的出现次数
注意,它默认最右边为最大值。
复制、替换:
• void copy(List dest,List src):将 src 中的内容复制到 dest 中
• boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值
• 提供了多个 unmodifiableXxx()方法,该方法返回指定 Xxx 的不可修改的视图,即只可以读。
注意,复制时要注意集合的大小关系。
添加:
• boolean addAll(Collection c,T... elements)将所有指定元素添加到指定 collection中。